يوضح الكود التالي قدرات المحرك:
const formula = "if( 1; round(10,2); 2*10)";
const formula1 = "round2(15.542 + 0.5)";
const formula2 = "max(2*15; 10; 20)";
const formula3 = "min(2; 10; 20)";
const formula4 = "round4(random()*10)";
const formula5 = "if ( max(0;10) ; 10*5 ; 15 ) ";
const formula6 = "sum(2*15; 10; 20)";
const calculator = new Calculator(null);
console.log(formula+" = "+calculator.calc(formula)); // if( 1; round(10,2); 2*10) = 10
console.log(formula1+" = "+calculator.calc(formula1)); // round2(15.542 + 0.5) = 16.04
console.log(formula2+" = "+calculator.calc(formula2)); // max(2*15; 10; 20) = 30
console.log(formula3+" = "+calculator.calc(formula3)); // min(2; 10; 20) = 2
console.log(formula4+" = "+calculator.calc(formula4)); // round4(random()*10) = 5.8235
console.log(formula5+" = "+calculator.calc(formula5)); // if ( max(0;10) ; 10*5 ; 15 ) = 50
console.log(formula6+" = "+calculator.calc(formula6)); // sum(2*15; 10; 20) = 60
قبل البدء في وصف بنية محرك الصيغة ، يجب عمل بعض الملاحظات:
- يمكن أن يتخذ كائن الحاسبة كوسيطة مصدر بيانات لخلايا جدول البيانات في شكل خريطة ، حيث يكون المفتاح هو اسم الخلية بتنسيق A1 ، وتكون القيمة رمزًا مميزًا واحدًا أو مجموعة من الكائنات المميزة التي يتم تحليل سلسلة الصيغة فيها عند إنشائها. في هذا المثال ، لا يتم استخدام أي خلايا في الصيغ ، لذلك يتم تحديد مصدر البيانات على أنه فارغ.
- تتم كتابة الدالات بالتنسيق [اسم_الوظيفة] ([وسيطة 1] ؛ [وسيطة 2] ؛ ...).
- لا تؤخذ المسافات في الاعتبار عند كتابة الصيغ - عند تقسيم سلسلة الصيغة إلى رموز مميزة ، تتم إزالة جميع أحرف المسافات البيضاء مسبقًا.
- يمكن فصل الجزء العشري من الرقم بنقطة أو فاصلة - عند تقسيم سلسلة صيغة إلى رموز مميزة ، يتم تحويل الفاصلة العشرية إلى نقطة.
- ينتج عن القسمة على 0 إلى 0 ، لأنه في الحسابات المطبقة في حالات القسمة المحتملة على 0 ، يتم استبدال الوظيفة [if (divisor = 0 ؛ivid / divisor؛ 0)]
يمكنك العثور على الكثير من المواد على الإنترنت حول الترميز البولندي نفسه ، لذلك من الأفضل أن تبدأ على الفور في وصف الكود. تتم استضافة الكود المصدري لمحرك الصيغة نفسه على https://github.com/leossnet/bizcalc بموجب ترخيص MIT في قسم / js / data ويتضمن ملفات calculator.js و token.js . يمكنك تجربة الآلة الحاسبة على الفور في الأعمال التجارية على bizcalc.ru .
لذلك ، لنبدأ بأنواع الرموز المميزة التي تتركز في كائن الأنواع:
const Types = {
Cell: "cell" ,
Number: "number" ,
Operator: "operator" ,
Function: "function",
LeftBracket: "left bracket" ,
RightBracket: "right bracket",
Semicolon: "semicolon",
Text: "text"
};
تمت إضافة الأنواع التالية مقارنة بتطبيقات المحرك القياسية:
- الخلية: "الخلية" هي اسم خلية في جدول بيانات يمكن أن تحتوي على نص أو رقم أو صيغة ؛
- الوظيفة: "الوظيفة" - الوظيفة ؛
- فاصلة منقوطة: "فاصلة منقوطة" - فاصل وسيطة دالة ، في هذه الحالة "؛"؛
- النص: "نص" - النص الذي يتم تجاهله بواسطة محرك الحساب.
كما هو الحال في أي محرك آخر ، يتم دعم خمسة مشغلين رئيسيين:
const Operators = {
["+"]: { priority: 1, calc: (a, b) => a + b }, //
["-"]: { priority: 1, calc: (a, b) => a - b }, //
["*"]: { priority: 2, calc: (a, b) => a * b }, //
["/"]: { priority: 2, calc: (a, b) => a / b }, //
["^"]: { priority: 3, calc: (a, b) => Math.pow(a, b) }, //
};
لاختبار المحرك ، يتم تكوين الوظائف التالية (يمكن توسيع قائمة الوظائف):
const Functions = {
["random"]: {priority: 4, calc: () => Math.random() }, //
["round"]: {priority: 4, calc: (a) => Math.round(a) }, //
["round1"]: {priority: 4, calc: (a) => Math.round(a * 10) / 10 },
["round2"]: {priority: 4, calc: (a) => Math.round(a * 100) / 100 },
["round3"]: {priority: 4, calc: (a) => Math.round(a * 1000) / 1000 },
["round4"]: {priority: 4, calc: (a) => Math.round(a * 10000) / 10000 },
["sum"]: {priority: 4, calc: (...args) => args.reduce( (sum, current) => sum + current, 0) },
["min"]: {priority: 4, calc: (...args) => Math.min(...args) },
["max"]: {priority: 4, calc: (...args) => Math.max(...args) },
["if"]: {priority: 4, calc: (...args) => args[0] ? args[1] : (args[2] ? args[2] : 0) }
};
أعتقد أن الكود أعلاه يتحدث عن نفسه. بعد ذلك ، ضع في اعتبارك رمز فئة الرمز المميز:
class Token {
// "+-*/^();""
static separators = Object.keys(Operators).join("")+"();";
// "[\+\-\*\/\^\(\)\;]"
static sepPattern = `[${Token.escape(Token.separators)}]`;
// "random|round|...|sum|min|max|if"
static funcPattern = new RegExp(`${Object.keys(Functions).join("|").toLowerCase()}`, "g");
#type;
#value;
#calc;
#priority;
/**
* , ,
*
*/
constructor(type, value){
this.#type = type;
this.#value = value;
if ( type === Types.Operator ) {
this.#calc = Operators[value].calc;
this.#priority = Operators[value].priority;
}
else if ( type === Types.Function ) {
this.#calc = Functions[value].calc;
this.#priority = Functions[value].priority;
}
}
/**
*
*/
/**
*
* @param {String} formula -
*/
static getTokens(formula){
let tokens = [];
let tokenCodes = formula.replace(/\s+/g, "") //
.replace(/(?<=\d+),(?=\d+)/g, ".") // ( )
.replace(/^\-/g, "0-") // 0 "-"
.replace(/\(\-/g, "(0-") // 0 "-"
.replace(new RegExp (Token.sepPattern, "g"), "&$&&") // &
.split("&") // &
.filter(item => item != ""); //
tokenCodes.forEach(function (tokenCode){
if ( tokenCode in Operators )
tokens.push( new Token ( Types.Operator, tokenCode ));
else if ( tokenCode === "(" )
tokens.push ( new Token ( Types.LeftBracket, tokenCode ));
else if ( tokenCode === ")" )
tokens.push ( new Token ( Types.RightBracket, tokenCode ));
else if ( tokenCode === ";" )
tokens.push ( new Token ( Types.Semicolon, tokenCode ));
else if ( tokenCode.toLowerCase().match( Token.funcPattern ) !== null )
tokens.push ( new Token ( Types.Function, tokenCode.toLowerCase() ));
else if ( tokenCode.match(/^\d+[.]?\d*/g) !== null )
tokens.push ( new Token ( Types.Number, Number(tokenCode) ));
else if ( tokenCode.match(/^[A-Z]+[0-9]+/g) !== null )
tokens.push ( new Token ( Types.Cell, tokenCode ));
});
return tokens;
}
/**
*
* @param {String} str
*/
static escape(str) {
return str.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
}
}
فئة الرمز المميز عبارة عن حاوية لتخزين وحدات نصية غير قابلة للتجزئة ، حيث يتم تقسيم سطر من الصيغ ، كل منها يحمل وظيفة معينة.
يأخذ مُنشئ فئة الرمز المميز نوع الرمز المميز من حقول كائن الأنواع كوسيطة ، وكقيمة - وحدة نصية غير قابلة للتجزئة مستخرجة من سلسلة الصيغة.
يتم تحديد الحقول الخاصة الداخلية لفئة الرمز المميز التي تخزن قيمة الأولوية والتعبير الذي تم تقييمه في المُنشئ استنادًا إلى قيم كائنات Operators و Functions.
كطريقة مساعدة ، يتم تنفيذ الهروب من الوظيفة الثابتة (str) ، وهو الكود المأخوذ من أول صفحة تم العثور عليها على الإنترنت ، متجاوزًا الأحرف التي يعتبرها كائن RegExp خاصة.
الطريقة الأكثر أهمية في فئة Token هي دالة getTokens الثابتة ، والتي تحلل سلسلة الصيغة وتعيد مجموعة من الكائنات Token. يتم تنفيذ خدعة صغيرة في الطريقة - قبل التقسيم إلى رموز مميزة ، تتم إضافة الرمز "&" إلى الفواصل (عوامل التشغيل والأقواس) ، والتي لا تُستخدم في الصيغ ، وعندها فقط يتم تقسيم الرمز "&".
يعد تنفيذ طريقة getTokens نفسها مقارنة حلقة لجميع الرموز المميزة المستلمة مع القوالب ، وتحديد نوع الرمز المميز ، وإنشاء كائن من فئة الرموز وإضافته إلى المصفوفة الناتجة.
هذا يكمل العمل التمهيدي لإعداد الحسابات. الخطوة التالية هي العمليات الحسابية نفسها ، والتي يتم تنفيذها في فئة الحاسبة:
class Calculator {
#tdata;
/**
*
* @param {Map} cells ,
*/
constructor(tableData) {
this.#tdata = tableData;
}
/**
*
* @param {Array|String} formula -
*/
calc(formula){
let tokens = Array.isArray(formula) ? formula : Token.getTokens(formula);
let operators = [];
let operands = [];
let funcs = [];
let params = new Map();
tokens.forEach( token => {
switch(token.type) {
case Types.Number :
operands.push(token);
break;
case Types.Cell :
if ( this.#tdata.isNumber(token.value) ) {
operands.push(this.#tdata.getNumberToken(token));
}
else if ( this.#tdata.isFormula(token.value) ) {
let formula = this.#tdata.getTokens(token.value);
operands.push(new Token(Types.Number, this.calc(formula)));
}
else {
operands.push(new Token(Types.Number, 0));
}
break;
case Types.Function :
funcs.push(token);
params.set(token, []);
operators.push(token);
break;
case Types.Semicolon :
this.calcExpression(operands, operators, 1);
//
let funcToken = operators[operators.length-2];
//
params.get(funcToken).push(operands.pop());
break;
case Types.Operator :
this.calcExpression(operands, operators, token.priority);
operators.push(token);
break;
case Types.LeftBracket :
operators.push(token);
break;
case Types.RightBracket :
this.calcExpression(operands, operators, 1);
operators.pop();
//
if (operators.length && operators[operators.length-1].type == Types.Function ) {
//
let funcToken = operators.pop();
//
let funcArgs = params.get(funcToken);
let paramValues = [];
if ( operands.length ) {
//
funcArgs.push(operands.pop());
//
paramValues = funcArgs.map( item => item.value );
}
//
operands.push(this.calcFunction(funcToken.calc, ...paramValues));
}
break;
}
});
this.calcExpression(operands, operators, 0);
return operands.pop().value;
}
/**
* ()
* @param {Array} operands
* @param {Array} operators
* @param {Number} minPriority
*/
calcExpression (operands, operators, minPriority) {
while ( operators.length && ( operators[operators.length-1].priority ) >= minPriority ) {
let rightOperand = operands.pop().value;
let leftOperand = operands.pop().value;
let operator = operators.pop();
let result = operator.calc(leftOperand, rightOperand);
if ( isNaN(result) || !isFinite(result) ) result = 0;
operands.push(new Token ( Types.Number, result ));
}
}
/**
*
* @param {T} func -
* @param {...Number} params -
*/
calcFunction(calc, ...params) {
return new Token(Types.Number, calc(...params));
}
}
كما هو الحال في محرك الصيغة المعتاد ، يتم إجراء جميع العمليات الحسابية في حساب الوظيفة الرئيسية (الصيغة) ، حيث يتم تمرير إما سلسلة صيغة أو صفيف جاهز من الرموز المميزة كوسيطة. إذا تم تمرير سلسلة الصيغة إلى طريقة calc ، فسيتم تحويلها مسبقًا إلى مصفوفة من الرموز المميزة.
كطريقة مساعدة ، يتم استخدام طريقة calcExpression ، والتي تأخذ كوسطات مكدس المعامل ومكدس عوامل التشغيل والحد الأدنى من أسبقية عامل التشغيل لتقييم التعبير.
كامتداد لمحرك الصيغة المعتاد ، يتم تنفيذ دالة calcFunction بسيطة نوعًا ما ، والتي تأخذ اسم الوظيفة كوسائط ، بالإضافة إلى عدد عشوائي من الوسائط لهذه الوظيفة. تقوم دالة calcFunction بتقييم قيمة دالة الصيغة وإرجاع كائن فئة رمز مميز جديد بنوع رقمي.
لحساب الوظائف ضمن الدورة العامة للحسابات ، تتم إضافة مكدس وظائف وخريطة لوسائط الدالة إلى مجموعات المعاملات والمشغلين ، حيث يكون المفتاح هو اسم الوظيفة ، والقيم هي مصفوفة الوسائط.
في الختام ، سأقدم مثالاً على كيفية استخدام مصدر بيانات في شكل تجزئة من الخلايا وقيمها. أولاً ، يتم تعريف الفئة التي تنفذ الواجهة التي تستخدمها الآلة الحاسبة:
class Data {
#map;
//
constructor() {
this.#map = new Map();
}
//
add(cellName, number) {
this.#map.set(cellName, number);
}
// , , Calculator.calc()
isNumber(cellName) {
return true;
}
// , Calculator.calc()
getNumberToken (token) {
return new Token (Types.Number, this.#map.get(token.value) );
}
}
حسنًا ، الأمر بسيط. نقوم بإنشاء مصدر بيانات يحتوي على قيم الخلايا. ثم نحدد صيغة تكون المعاملات فيها مراجع خلايا. وفي الختام نقوم بالحسابات:
let data = new Data();
data.add("A1", 1);
data.add("A2", 1.5);
data.add("A3", 2);
let formula = "round1((A1+A2)^A3)";
let calculator = new Calculator(data);
console.log(formula+" = "+calculator.calc(formula)); // round1((A1+A2)^A3) = 6.3
شكرآ لك على أهتمامك.