Skip to content

Instantly share code, notes, and snippets.

@nerevar
Created July 22, 2016 14:25
Show Gist options
  • Save nerevar/5dfa49511c03450c71fcce035df5d1ad to your computer and use it in GitHub Desktop.
Save nerevar/5dfa49511c03450c71fcce035df5d1ad to your computer and use it in GitHub Desktop.
PEG.js filters grammar
/*
* 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]
/**
* Тесты на грамматику, написанную на 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