Created
July 22, 2016 14:25
-
-
Save nerevar/5dfa49511c03450c71fcce035df5d1ad to your computer and use it in GitHub Desktop.
PEG.js filters grammar
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* PEG.js грамматика парсера фильтра результатов сравнения метрик | |
* Как обновлять грамматику: | |
* 1. Скопипастить исходник в http://pegjs.org/online | |
* 2. Указать Parser variable: window.filter_parser | |
* 3. Download parser | |
* 4. Минифицировать скачанный js код через: http://jscompress.com | |
* 5. Обновить файл filter_parser.pegjs.min.js | |
* 6. Запустить js unit-тесты: mocha filter_parser.pegjs.test.js | |
* 7. Обновить исходник в этом файле filter_parser.pegjs | |
*/ | |
/** | |
* Хелперы | |
*/ | |
{ | |
function makeInt(n) { | |
return parseInt(n.join(''), 10); | |
} | |
function makeFloat(n) { | |
return parseFloat(n.join('')); | |
} | |
function flattenArray(arr) { | |
var result = [] | |
for (var i = 0; i < arr.length; i++) { | |
if (arr[i]) { | |
if (Object.prototype.toString.call(arr[i]) === '[object Array]') { | |
result = result.concat(flattenArray(arr[i])); | |
} else { | |
result = result.concat(arr[i]); | |
} | |
} | |
} | |
return result; | |
} | |
} | |
start = any_terms* | |
/** | |
* Поддержка AND, OR и скобок | |
*/ | |
any_terms = terms_or | |
terms_or | |
= first:terms_and or last:terms_or { return ['|', first, last]; } | |
/ terms_and | |
terms_and | |
= first:term and last:terms_and { return ['&', first, last]; } | |
/ term | |
term | |
= _ '(' _ result:any_terms _ ')' _ { return result; } | |
/ block | |
/** | |
* Блоки | |
*/ | |
block | |
= days_block | |
/ pvalue_block | |
/ date_start_block | |
/ date_end_block | |
/ control_block | |
/ exp_block | |
/ diffpercent_block | |
/ diff_block | |
/ color_block | |
/ metric_block | |
/ exp_name_block | |
/ any_block | |
days_block | |
= variable:days _ comparator:comparator _ result:integer { return {key:variable, val: result, cmp: comparator}; } | |
/ result:integer _ variable:days { return {key:variable, val: result, cmp: '='}; } | |
days | |
= 'days' | |
/ 'day' { return 'days'; } | |
pvalue_block = variable:pvalue _ comparator:comparator _ result:float { return {key:variable, val: result, cmp: comparator}; } | |
pvalue = 'p-value' { return 'pvalue'; } | |
date_start_block = variable:date_start _ comparator:comparator _ result:date { return {key:variable, val: result, cmp: comparator}; } | |
date_start | |
= 'date_start' { return 'dateStart'; } | |
/ 'date-start' { return 'dateStart'; } | |
date_end_block = variable:date_end _ comparator:comparator _ result:date { return {key:variable, val: result, cmp: comparator}; } | |
date_end | |
= 'date_end' { return 'dateEnd'; } | |
/ 'date-end' { return 'dateEnd'; } | |
control_block = variable:control _ comparator:comparator _ result:float { return {key:variable, val: result, cmp: comparator}; } | |
control = 'control' { return 'controlValue'; } | |
exp_block = variable:exp _ comparator:comparator _ result:float { return {key:variable, val: result, cmp: comparator}; } | |
exp = 'exp' { return 'expValue'; } | |
diffpercent_block = variable:diffpercent _ comparator:comparator _ result:float { return {key:variable, val: result, cmp: comparator}; } | |
diffpercent = 'diff%' { return 'diffPercent'; } | |
diff_block = variable:diff _ comparator:comparator _ result:float { return {key:variable, val: result, cmp: comparator}; } | |
diff = 'diff' | |
color_block = variable:color _ comparator:comparator _ result:colors { return {key:variable, val: result, cmp: comparator}; } | |
color = 'color' | |
metric_block = variable:metric _ comparator:comparator _ result:string { return {key:variable, val: result, cmp: comparator}; } | |
metric = 'metric' { return 'metricName'; } | |
exp_name_block = variable:exp_name _ comparator:comparator _ result:string { return {key:variable, val: result, cmp: comparator}; } | |
exp_name = 'exp-name' { return 'expName'; } | |
any_block = result:any { return {key:'any', val: result.trim(), cmp: '~'}; } | |
any = result: [a-z_0-9-—−–+.а-я#@№;'"` ]i+ { return result.join('').trim(); } | |
/** | |
* Операторы сравнения, логические операции | |
*/ | |
and | |
= _ '&&' _ { return '&'; } | |
/ _ '&' _ { return '&'; } | |
or | |
= _ '||' _ { return '|'; } | |
/ _ '|' _ { return '|'; } | |
comparator | |
= '==' { return '='; } | |
/ '=' | |
/ '>=' | |
/ '<=' | |
/ '!=' | |
/ '<>' { return '!='; } | |
/ '>' | |
/ '<' | |
/ '~' | |
/** | |
* Типы данных | |
*/ | |
float = digits:([+-]? DIGIT+ '.'? DIGIT*) { return makeFloat(flattenArray(digits)); } | |
integer = digits:DIGIT+ { return makeInt(flattenArray(digits)); } | |
string = result: [a-z_0-9а-я\=\%\.\*\/\+\-\—\−\–\`\"\'\#\№\$\;\@\!\$\%\^ ]i+ { return result.join('').trim(); } | |
percent | |
= value:float '%' { return value; } | |
/ value:integer '%' { return value; } | |
date | |
= d:('2' '0' DIGIT DIGIT '-' [01] DIGIT '-' [0123] DIGIT) { return d.join(''); } | |
/ d:('2' '0' DIGIT DIGIT [01] DIGIT [0123] DIGIT) { return [d[0], d[1], d[2], d[3], '-', d[4], d[5], '-', d[6], d[7]].join(''); } | |
colors = 'red' / 'green' / 'gray' | |
_ "whitespace" = [ \t\n\r]* | |
DIGIT = [0-9] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Тесты на грамматику, написанную на PEG.js http://pegjs.org | |
* Предварительная установка: | |
* npm install mocha chai | |
* Запуск тестов: | |
* mocha filter_parser.pegjs.test.js | |
*/ | |
var assert = require('chai').assert; | |
var fs = require('fs'); | |
var vm = require('vm'); | |
var code = fs.readFileSync('filter_parser.pegjs.min.js'); | |
var global = {}; | |
vm.runInContext(code, vm.createContext({window: global})); | |
describe('filter_parser', function () { | |
var getResult = function (input) { | |
try { | |
return global.filter_parser.parse(input); | |
} catch (e) { | |
return [e.name, e.message]; | |
} | |
}; | |
var assertEq = function (input, output) { | |
assert.deepEqual(getResult(input), output); | |
}; | |
describe('-> должен возвращать объект с правилом any ~ для просто обычной строки', function () { | |
it('для простой строки', function () { | |
assertEq('test', [{key: 'any', val: 'test', cmp: '~'}]); | |
}); | |
it('с числами и русскими буквами и разным регистром', function () { | |
var s = 'ПривеТ0123456789Hello'; | |
assertEq(s, [{key: 'any', val: s, cmp: '~'}]); | |
}); | |
it('даже с пробелами', function () { | |
var s = 'hello world with spaces'; | |
assertEq(s, [{key: 'any', val: s, cmp: '~'}]); | |
}); | |
it('со всякими спецсимволами', function () { | |
var s = '-—−–+.#@№;'; | |
assertEq(s, [{key: 'any', val: s, cmp: '~'}]); | |
}); | |
it('с разными кавычками', function () { | |
var s = 'word\'1 word"2 word`3'; | |
assertEq(s, [{key: 'any', val: s, cmp: '~'}]); | |
}); | |
it('обрезая пробелы по краям', function () { | |
assertEq(' word1 word2 ', [{key: 'any', val: 'word1 word2', cmp: '~'}]); | |
}); | |
}); | |
describe('-> должен возвращать объект с правилом days при указании дней проведения эксперимента', function () { | |
it('7 days', function () { | |
assertEq('7 days', [{key: 'days', val: 7, cmp: '='}]); | |
}); | |
it('51 day', function () { | |
assertEq('51 day', [{key: 'days', val: 51, cmp: '='}]); | |
}); | |
it('days > 1', function () { | |
assertEq('days > 1', [{key: 'days', val: 1, cmp: '>'}]); | |
}); | |
it('days < 1000', function () { | |
assertEq('days < 1000', [{key: 'days', val: 1000, cmp: '<'}]); | |
}); | |
it('days >= 3', function () { | |
assertEq('days >= 3', [{key: 'days', val: 3, cmp: '>='}]); | |
}); | |
it('days <= 7', function () { | |
assertEq('days <= 7', [{key: 'days', val: 7, cmp: '<='}]); | |
}); | |
it('days != 11', function () { | |
assertEq('days != 11', [{key: 'days', val: 11, cmp: '!='}]); | |
}); | |
it('days <> 7', function () { | |
assertEq('days <> 7', [{key: 'days', val: 7, cmp: '!='}]); | |
}); | |
it('days ~ 1', function () { | |
assertEq('days ~ 1', [{key: 'days', val: 1, cmp: '~'}]); | |
}); | |
it('days = 7', function () { | |
assertEq('days = 7', [{key: 'days', val: 7, cmp: '='}]); | |
}); | |
it('days == 7', function () { | |
assertEq('days == 7', [{key: 'days', val: 7, cmp: '='}]); | |
}); | |
it('day == 7', function () { | |
assertEq('day == 7', [{key: 'days', val: 7, cmp: '='}]); | |
}); | |
}); | |
describe('-> должен возвращать объект с правилом pvalue при указании p-value', function () { | |
it('p-value <= 0.05', function () { | |
assertEq('p-value <= 0.05', [{key: 'pvalue', val: 0.05, cmp: '<='}]); | |
}); | |
it('p-value < 0.01', function () { | |
assertEq('p-value < 0.01', [{key: 'pvalue', val: 0.01, cmp: '<'}]); | |
}); | |
it('p-value < 0.00000001', function () { | |
assertEq('p-value < 0.00000001', [{key: 'pvalue', val: 0.00000001, cmp: '<'}]); | |
}); | |
}); | |
describe('-> должен возвращать объект с правилом dateStart при указании даты начала эксперимента', function () { | |
it('date-start >= 2016-07-07', function () { | |
assertEq('date-start >= 2016-07-07', [{key: 'dateStart', val: '2016-07-07', cmp: '>='}]); | |
}); | |
it('date-start < 20160708', function () { | |
assertEq('date-start < 20160708', [{key: 'dateStart', val: '2016-07-08', cmp: '<'}]); | |
}); | |
it('date_start >= 2016-07-07', function () { | |
assertEq('date_start >= 2016-07-07', [{key: 'dateStart', val: '2016-07-07', cmp: '>='}]); | |
}); | |
it('date_start < 20130505', function () { | |
assertEq('date_start < 20130505', [{key: 'dateStart', val: '2013-05-05', cmp: '<'}]); | |
}); | |
it('date_start > 19560505 - неправильный формат даты #1', function () { | |
assertEq('date_start < 19560505', ['SyntaxError', 'Expected "2" but "1" found.']); | |
}); | |
it('date_start > 2016-37-05 - неправильный формат даты #2', function () { | |
assertEq('date_start > 2016-37-05', ['SyntaxError', 'Expected [01] but "3" found.']); | |
}); | |
it('date_start > 2016-01-75 - неправильный формат даты #3', function () { | |
assertEq('date_start > 2016-01-75', ['SyntaxError', 'Expected [0123] but "7" found.']); | |
}); | |
}); | |
describe('-> должен возвращать объект с правилом dateEnd при указании даты окончания эксперимента', function () { | |
it('date-end >= 2016-07-07', function () { | |
assertEq('date-end >= 2016-07-07', [{key: 'dateEnd', val: '2016-07-07', cmp: '>='}]); | |
}); | |
it('date-end < 20160708', function () { | |
assertEq('date-end < 20160708', [{key: 'dateEnd', val: '2016-07-08', cmp: '<'}]); | |
}); | |
it('date_end >= 2016-07-07', function () { | |
assertEq('date_end >= 2016-07-07', [{key: 'dateEnd', val: '2016-07-07', cmp: '>='}]); | |
}); | |
it('date_end < 20130505', function () { | |
assertEq('date_end < 20130505', [{key: 'dateEnd', val: '2013-05-05', cmp: '<'}]); | |
}); | |
it('date_end > 19560505 - неправильный формат даты #1', function () { | |
assertEq('date_end < 19560505', ['SyntaxError', 'Expected "2" but "1" found.']); | |
}); | |
it('date_end > 2016-37-05 - неправильный формат даты #2', function () { | |
assertEq('date_end > 2016-37-05', ['SyntaxError', 'Expected [01] but "3" found.']); | |
}); | |
it('date_end > 2016-01-75 - неправильный формат даты #3', function () { | |
assertEq('date_end > 2016-01-75', ['SyntaxError', 'Expected [0123] but "7" found.']); | |
}); | |
}); | |
describe('-> должен возвращать объект с правилом controlValue при указании значения метрики в контроле', function () { | |
it('control > 0', function () { | |
assertEq('control > 0', [{key: 'controlValue', val: 0, cmp: '>'}]); | |
}); | |
it('control != 0', function () { | |
assertEq('control != 0', [{key: 'controlValue', val: 0, cmp: '!='}]); | |
}); | |
it('control < -1000.5', function () { | |
assertEq('control < -1000.5', [{key: 'controlValue', val: -1000.5, cmp: '<'}]); | |
}); | |
it('control >= 3.1415926', function () { | |
assertEq('control >= 3.1415926', [{key: 'controlValue', val: 3.1415926, cmp: '>='}]); | |
}); | |
}); | |
describe('-> должен возвращать объект с правилом expValue при указании значения метрики в эксперименте', function () { | |
it('exp > 0', function () { | |
assertEq('exp > 0', [{key: 'expValue', val: 0, cmp: '>'}]); | |
}); | |
it('exp != 0', function () { | |
assertEq('exp != 0', [{key: 'expValue', val: 0, cmp: '!='}]); | |
}); | |
it('exp < -1000.5', function () { | |
assertEq('exp < -1000.5', [{key: 'expValue', val: -1000.5, cmp: '<'}]); | |
}); | |
it('exp >= 3.1415926', function () { | |
assertEq('exp >= 3.1415926', [{key: 'expValue', val: 3.1415926, cmp: '>='}]); | |
}); | |
}); | |
describe('-> должен возвращать объект с правилом diff при указании диффа значения метрики', function () { | |
it('diff > 0', function () { | |
assertEq('diff > 0', [{key: 'diff', val: 0, cmp: '>'}]); | |
}); | |
it('diff != 0', function () { | |
assertEq('diff != 0', [{key: 'diff', val: 0, cmp: '!='}]); | |
}); | |
it('diff < -1000.5', function () { | |
assertEq('diff < -1000.5', [{key: 'diff', val: -1000.5, cmp: '<'}]); | |
}); | |
it('diff >= 3.1415926', function () { | |
assertEq('diff >= 3.1415926', [{key: 'diff', val: 3.1415926, cmp: '>='}]); | |
}); | |
}); | |
describe('-> должен возвращать объект с правилом diffPercent при указании диффа метрики в процентах', function () { | |
it('diff% > 0', function () { | |
assertEq('diff% > 0', [{key: 'diffPercent', val: 0, cmp: '>'}]); | |
}); | |
it('diff% != 0', function () { | |
assertEq('diff% != 0', [{key: 'diffPercent', val: 0, cmp: '!='}]); | |
}); | |
it('diff% < -1000.5', function () { | |
assertEq('diff% < -1000.5', [{key: 'diffPercent', val: -1000.5, cmp: '<'}]); | |
}); | |
it('diff% >= 3.1415926', function () { | |
assertEq('diff% >= 3.1415926', [{key: 'diffPercent', val: 3.1415926, cmp: '>='}]); | |
}); | |
}); | |
describe('-> должен возвращать объект с правилом color при указании цвета прокраски метрики', function () { | |
it('color = green', function () { | |
assertEq('color = green', [{key: 'color', val: 'green', cmp: '='}]); | |
}); | |
it('color == green', function () { | |
assertEq('color == green', [{key: 'color', val: 'green', cmp: '='}]); | |
}); | |
it('color == red', function () { | |
assertEq('color == red', [{key: 'color', val: 'red', cmp: '='}]); | |
}); | |
it('color == gray', function () { | |
assertEq('color == gray', [{key: 'color', val: 'gray', cmp: '='}]); | |
}); | |
it('color != gray', function () { | |
assertEq('color != gray', [{key: 'color', val: 'gray', cmp: '!='}]); | |
}); | |
}); | |
describe('-> должен возвращать объект с правилом metricName при указании названия метрики', function () { | |
it('metric ~ surplus', function () { | |
assertEq('metric ~ surplus', [{key: 'metricName', val: 'surplus', cmp: '~'}]); | |
}); | |
it('metric ~ loss=clicks', function () { | |
assertEq('metric ~ loss=clicks', [{key: 'metricName', val: 'loss=clicks', cmp: '~'}]); | |
}); | |
it('metric ~ loss=clicks and wins=shows', function () { | |
assertEq('metric ~ loss=clicks and wins=shows', [{ | |
key: 'metricName', | |
val: 'loss=clicks and wins=shows', | |
cmp: '~' | |
}]); | |
}); | |
it('metric ~ alpha = "0"', function () { | |
assertEq('metric ~ alpha = "0"', [{key: 'metricName', val: 'alpha = "0"', cmp: '~'}]); | |
}); | |
}); | |
describe('-> должен возвращать объект с правилом expName при указании названия эксперимента', function () { | |
it('exp-name ~ ухудшающий 1', function () { | |
assertEq('exp-name ~ ухудшающий 1', [{key: 'expName', val: 'ухудшающий 1', cmp: '~'}]); | |
}); | |
it('exp-name ~ rq=0.5', function () { | |
assertEq('exp-name ~ rq=0.5', [{key: 'expName', val: 'rq=0.5', cmp: '~'}]); | |
}); | |
}); | |
describe('-> должен возвращать массив из нескольких условий при объединении условий с "И" и "ИЛИ"', function () { | |
it('1 && 2', function () { | |
assertEq('1 && 2', [[ | |
"&", | |
{"key": "any", "val": "1", "cmp": "~"}, | |
{"key": "any", "val": "2", "cmp": "~"} | |
]]); | |
}); | |
it('rq & surplus', function () { | |
assertEq('rq & surplus', [[ | |
"&", | |
{"key": "any", "val": "rq", "cmp": "~"}, | |
{"key": "any", "val": "surplus", "cmp": "~"} | |
]]); | |
}); | |
it('rq & surplus && metric', function () { | |
assertEq('rq & surplus && metric', [ | |
[ | |
"&", | |
{"key": "any", "val": "rq", "cmp": "~"}, | |
[ | |
"&", | |
{"key": "any", "val": "surplus", "cmp": "~"}, | |
{"key": "any", "val": "metric", "cmp": "~"} | |
] | |
] | |
]); | |
}); | |
it('3 | 4', function () { | |
assertEq('3 | 4', [[ | |
"|", | |
{"key": "any", "val": "3", "cmp": "~"}, | |
{"key": "any", "val": "4", "cmp": "~"} | |
]]); | |
}); | |
it('yt || yamr', function () { | |
assertEq('yt || yamr', [[ | |
"|", | |
{"key": "any", "val": "yt", "cmp": "~"}, | |
{"key": "any", "val": "yamr", "cmp": "~"} | |
]]); | |
}); | |
it('metric ~ yt || metric ~ yamr', function () { | |
assertEq('metric ~ yt || metric ~ yamr', [[ | |
"|", | |
{"key": "metricName", "val": "yt", "cmp": "~"}, | |
{"key": "metricName", "val": "yamr", "cmp": "~"} | |
]]); | |
}); | |
it('metric 1 || metric 2 || metric 3 || metric 4', function () { | |
assertEq('metric 1 || metric 2 || metric 3 || metric 4', [ | |
[ | |
"|", | |
{"key": "any", "val": "metric 1", "cmp": "~"}, | |
[ | |
"|", | |
{"key": "any", "val": "metric 2", "cmp": "~"}, | |
[ | |
"|", | |
{"key": "any", "val": "metric 3", "cmp": "~"}, | |
{"key": "any", "val": "metric 4", "cmp": "~"} | |
] | |
] | |
] | |
]); | |
}); | |
it('diff != 0 && diff% >= 0', function () { | |
assertEq('diff != 0 && diff% >= 0', [[ | |
"&", | |
{"key": "diff", "val": 0, "cmp": "!="}, | |
{"key": "diffPercent", "val": 0, "cmp": ">="} | |
]]); | |
}); | |
it('1 && 2 || 3', function () { | |
assertEq('1 && 2 || 3', [[ | |
"|", | |
[ | |
"&", | |
{"key": "any", "val": "1", "cmp": "~"}, | |
{"key": "any", "val": "2", "cmp": "~"} | |
], | |
{"key": "any", "val": "3", "cmp": "~"} | |
]]); | |
}); | |
it('1 && (2 || 3)', function () { | |
assertEq('1 && (2 || 3)', [[ | |
"&", | |
{"key": "any", "val": "1", "cmp": "~"}, | |
[ | |
"|", | |
{"key": "any", "val": "2", "cmp": "~"}, | |
{"key": "any", "val": "3", "cmp": "~"} | |
] | |
]]); | |
}); | |
it(' (v1 && v2) ', function () { | |
assertEq(' (v1 && v2) ', [[ | |
"&", | |
{"key": "any", "val": "v1", "cmp": "~"}, | |
{"key": "any", "val": "v2", "cmp": "~"} | |
]]); | |
}); | |
it('(diff% > 0)', function () { | |
assertEq('(diff% > 0)', [{"key": "diffPercent", "val": 0, "cmp": ">"}]); | |
}); | |
it('(28036 || EXPERIMENTS-8188) && exp-name ~ высота && (metric ~ yt || metric ~ yamr) && metric ~ surplus && (p-value <= 0.05 || color != gray) && days >= 5 && date_start >= 2016-05-06 && ( diff > 0 || diff% > 0 )', function () { | |
assertEq('(28036 || EXPERIMENTS-8188) && exp-name ~ высота && (metric ~ yt || metric ~ yamr) && metric ~ surplus && (p-value <= 0.05 || color != gray) && days >= 5 && date_start >= 2016-05-06 && ( diff > 0 || diff% > 0 )', [ | |
[ | |
"&", | |
[ | |
"|", | |
{"key": "any", "val": "28036", "cmp": "~"}, | |
{"key": "any", "val": "EXPERIMENTS-8188", "cmp": "~"} | |
], | |
[ | |
"&", | |
{"key": "expName", "val": "высота", "cmp": "~"}, | |
[ | |
"&", | |
[ | |
"|", | |
{"key": "metricName", "val": "yt", "cmp": "~"}, | |
{"key": "metricName", "val": "yamr", "cmp": "~"} | |
], | |
[ | |
"&", | |
{"key": "metricName", "val": "surplus", "cmp": "~"}, | |
[ | |
"&", | |
[ | |
"|", | |
{"key": "pvalue", "val": 0.05, "cmp": "<="}, | |
{"key": "color", "val": "gray", "cmp": "!="} | |
], | |
[ | |
"&", | |
{"key": "days", "val": 5, "cmp": ">="}, | |
[ | |
"&", | |
{"key": "dateStart", "val": "2016-05-06", "cmp": ">="}, | |
[ | |
"|", | |
{"key": "diff", "val": 0, "cmp": ">"}, | |
{"key": "diffPercent", "val": 0, "cmp": ">"} | |
] | |
] | |
] | |
] | |
] | |
] | |
] | |
] | |
]); | |
}); | |
}) | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment