Стоит обратить внимание на то, что в коде присутствуют токены начинающиеся с "U" (Uncalculated -невычисляемый), при этом они имеют зеркальные токены без такой приставки. Например, BOOLFUNC, U_BOOLFUNC. Позднее в парсере выражений - невычисляемые токены остаются такими, какими были, а вычисляемые вычисляются. Все парсеры между собой отличаются только перечислением функций которые нужно вычсислять и какие нужно оставить для более позднего вычисления.
Например в коде парсера колонок (column.jison) это выглядит - так:
Все булевые функции, которые зависят от рядов - остаются, другие - вычисляются сразу.
Крайние случаи parser.jison и calculator.jison: если в первом все функции и переменные невычисляемые, то во втором невычисляемых - нет.
Парсер выражений
Program: RESULT EOF { return $1; };
/* RETURN RESULT */
RESULT:
MATH -> $1;
| U_MATH -> $1;
;
/* NUMBERS */
NUMERIC:
ZERO -> 0;
| '('NUMBER')' -> $2;
| NUMBER -> Number($1);
| CONSTANTA -> Number(CONTEXT[$1]);
| FUNC '(' ARGS ')' -> Number(LIB[$FUNC]($ARGS));
;
ZERO:
ZNUMBER -> $1;
| ZERO '-' '-' MATH -> 0+Number($4);
| MATH '-' '-' ZERO -> Number($1)+0;
| U_MATH '*' ZERO -> $3;
| ZERO '*' U_MATH -> $1;
| U_MATH '/' ZERO -> $3;
| ZERO '/' U_MATH -> $1;
| MATH '*' ZERO -> $3;
| ZERO '*' MATH -> $3;
| MATH '/' ZERO -> $3;
| ZERO '/' MATH -> $1;
| ZERO '/' ZERO -> $1;
| ZERO '*' ZERO -> $1;
| ZERO '+' ZERO -> $1;
| ZERO '-' ZERO -> $1;
| '('ZERO')' -> $2;
;
MATH:
NUMERIC -> $1;
| IF_THEN -> $1;
| IF_THEN_ELSE -> $1;
| SWITCH -> $1;
| MATH '+' MATH -> Number($1)+Number($3);
| MATH '-' MATH -> Number($1)-Number($3);
| MATH '*' MATH -> Number($1)*Number($3);
| MATH '/' MATH -> Number($1)/Number($3);
| MATH '+' ZERO -> Number($1);
| ZERO '+' MATH -> Number($3);
| MATH '-' ZERO -> Number($1);
| ZERO '-' MATH -> 0-Number($3);
| '-' MATH %prec UMINUS -> -Number($2);
| '('MATH')' -> $2;
;
/* Весь блок можно посмотреть в исходниках системы */
В данном блоке стоит обратить внимание на то, что модуль имеет отдельную структуру для работы с "0". С одной стороны это особенность, что деление на ноль здесь будет не бесконечность, а ноль. С другой стороны, в коде прописаны ситуации когда реализация простых правил ("что угодно" умножить на ноль = получается ноль и т.д.) позволяет значительно сократить объемы вычислений (не вычислять это "что угодно")
Как происходит вычисление: расмотрим простой пример 2+2*2
Поскольку мы не описали правил для сложений или умножения 2-х типов Number, то их тип повышается постепенно до Numeric и Math (на уровне Math арифметические операции мы уже описали)
Почему произошло сначала умножение, а потом сложение - потому, что мы это описали в правилах приоритетов вычислений:
%left '+' '-'
%left '*' '/'
%left '^'
%left UMINUS
%left OR
%left AND
%left NOT
Последняя часть модуля - реализация вычислений
%{
var IfThenElse = function (ifExpr, thenExpr, elseExpr) {
if (ifExpr) return thenExpr; else return elseExpr;
};
var Switch = function(funcName,Cases){
var Indexed = {};
var MatchFuncs = {
forcol:'colin',
forobj:'objin',
fordiv:'divin',
forperiod:'periodin'
}
Cases.forEach(function(Case){
Indexed[Case[0]] = Case[1];
})
var Result = 0;
for (var Args in Indexed){
var Value = Indexed[Args];
if (LIB[MatchFuncs[funcName]](Args.split(','))){
Result = Value;
break;
}
}
return Result;
}
var CONTEXT = {};
var LIB = {
get:function(field){
var Check = CONTEXT[field];
if (Check) {
if (Object.prototype.toString.call(Check)=="[object Array]"){
return LIB._args(Check);
} else {
return Check+'';
}
}
return 0;
},
_args:function(args){
var result = [];
args && args.forEach(function(a){
result.push((a+'').replace(/["']/g,''));
})
return result;
},
_argsNumeric:function(args){
var result = LIB._args(args);
result && result.forEach(function(a,i){
result[i] = Number(a);
})
return result;
},
_simpleCheck:function(key,args){
args = LIB._args(args);
var isOpposite = false;
if (args[0]=="!"){
args = args.splice(1);
isOpposite = true;
}
var test = LIB.get(key);
if (!isOpposite){
return args.indexOf(test) !=-1;
} else {
return args.indexOf(test) ==-1;
}
},
_arrCheck:function(key,args){
args = LIB._args(args);
var grps = LIB.get(key);
var intersect = grps.filter(function(n) {
return args.indexOf(n) != -1;
});
return intersect.length>0;
},
_tagsCheck:function(key,args){
args = LIB._args(args)||[];
var tags = LIB.get(key)||[];
var Result = false;
tags.forEach(function(T){
args.forEach(function(S){
if (('/'+T+'/').match(new RegExp(S.replace('*','.*?')))){
Result = true;
}
})
})
return Result;
},
try{
module.exports.setContext = function (myContext) {
CONTEXT = myContext;
};
} catch(e){
//console.log("Нет контекста, пропишите его руками");
}
%}
В этом модуле прописано, как ведет себя if, if...else, switch и прочии конструкции, но в целом - ничего интересного нет.
Несколько слов по копиляции парсеров. После того, как были внесены изменения в код jison парсеров, необходимо запустить консольную админку (node admin.js) и выбрать пункт компиляция парсеров
Окончание процесса компиляции сопровождается запуском mocha-тестов по проверке работы парсера
Сами тесты находятся в JSON файле ./classes/calculator/jison/mocha/Documents/blank.js и имеют следующий формат:
{
Formula: "if ( ( ismonth and not periodin( 11 ) ) or ( periodin (42,43,44,46,444,446,416,442) ) ,{1},{2})",
Contexts:[
{Context:{ismonth:true,period:19},Result : '1'},
{Context:{ismonth:false,period:442},Result : '1'},
{Context:{ismonth:true,period:11},Result : '2'}
]
}