Der folgende Code demonstriert die Funktionen des Motors:
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
Bevor Sie mit der Beschreibung der Architektur der Formel-Engine beginnen, sollten Sie einige Anmerkungen machen:
- Das Calculator-Objekt kann eine Datenquelle für Tabellenkalkulationszellen in Form einer Map als Argument verwenden, wobei der Schlüssel der Zellenname im A1-Format ist und der Wert ein einzelnes Token oder ein Array von Token-Objekten ist, in die die Formelzeichenfolge beim Erstellen analysiert wird. In diesem Beispiel werden keine Zellen in den Formeln verwendet, daher wird die Datenquelle als null angegeben.
- Funktionen werden im Format [Funktionsname] ([Argument1]; [Argument2]; ...) geschrieben.
- Leerzeichen werden beim Schreiben von Formeln nicht berücksichtigt. Beim Aufteilen einer Formelzeichenfolge in Token werden alle Leerzeichen zuvor entfernt.
- Der Dezimalteil einer Zahl kann entweder durch einen Punkt oder ein Komma getrennt werden. Wenn Sie eine Formelzeichenfolge in Token aufteilen, wird der Dezimalpunkt in einen Punkt konvertiert.
- Division durch 0 ergibt 0, da in angewandten Berechnungen in Situationen möglicher Division durch 0 die Funktion [if (Divisor! = 0; Dividende / Divisor; 0)]
Im Internet finden Sie eine ganze Reihe von Materialien zur polnischen Notation. Es ist daher besser, sofort mit der Beschreibung des Codes zu beginnen. Der Quellcode der Formel-Engine selbst wird unter https://github.com/leossnet/bizcalc unter der MIT-Lizenz unter / js / data gehostet und enthält die Dateien calculator.js und token.js . Sie können den Taschenrechner sofort im Geschäft unter bizcalc.ru ausprobieren .
Beginnen wir also mit den Arten von Token, die im Types-Objekt konzentriert sind:
const Types = {
Cell: "cell" ,
Number: "number" ,
Operator: "operator" ,
Function: "function",
LeftBracket: "left bracket" ,
RightBracket: "right bracket",
Semicolon: "semicolon",
Text: "text"
};
Die folgenden Typen wurden im Vergleich zu Standard-Motorimplementierungen hinzugefügt:
- Zelle: "Zelle" ist der Name einer Zelle in einer Tabelle, die Text, eine Zahl oder eine Formel enthalten kann.
- Funktion: "Funktion" - Funktion;
- Semikolon: "Semikolon" - Funktionsargumenttrennzeichen, in diesem Fall ";";
- Text: "Text" - Text, der von der Berechnungsmaschine ignoriert wird.
Wie bei jedem anderen Motor wird die Unterstützung für fünf Hauptbetreiber implementiert:
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) }, //
};
Zum Testen des Motors werden folgende Funktionen konfiguriert (die Liste der Funktionen kann erweitert werden):
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) }
};
Ich denke, der obige Code spricht für sich. Betrachten Sie als nächstes den Code der Token-Klasse:
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, '\\$&');
}
}
Die Token-Klasse ist ein Container zum Speichern unteilbarer Texteinheiten, in die eine Reihe von Formeln unterteilt ist, von denen jede eine bestimmte Funktionalität enthält.
Der Konstruktor der Token-Klasse verwendet als Argument den Token-Typ aus den Feldern des Types-Objekts und als Wert - eine unteilbare Texteinheit, die aus der Formelzeichenfolge extrahiert wird.
Interne private Felder der Token-Klasse, in denen der Wert der Priorität und der ausgewertete Ausdruck gespeichert sind, werden im Konstruktor basierend auf den Werten der Operatoren- und Funktionsobjekte definiert.
Als Hilfsmethode wird die statische Funktion Escape (str) implementiert, der Code, der von der ersten gefundenen Seite im Internet stammt und Zeichen maskiert, die das RegExp-Objekt als besonders wahrnimmt.
Die wichtigste Methode in der Token-Klasse ist die statische Funktion getTokens, die die Formelzeichenfolge analysiert und ein Array von Token-Objekten zurückgibt. In der Methode ist ein kleiner Trick implementiert: Vor dem Aufteilen in Token wird das Symbol "&" zu den Trennzeichen (Operatoren und Klammern) hinzugefügt, das in Formeln nicht verwendet wird, und erst dann wird das Symbol "&" aufgeteilt.
Die Implementierung der getTokens-Methode selbst ist ein Schleifenvergleich aller empfangenen Token mit Vorlagen, bei dem der Token-Typ ermittelt, ein Objekt der Token-Klasse erstellt und dem resultierenden Array hinzugefügt wird.
Damit sind die Vorarbeiten zur Vorbereitung der Berechnungen abgeschlossen. Der nächste Schritt sind die Berechnungen selbst, die in der Calculator-Klasse implementiert sind:
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));
}
}
Wie in der üblichen Formel-Engine werden alle Berechnungen in der Hauptfunktion calc (Formel) ausgeführt, wobei entweder eine Formelzeichenfolge oder ein vorgefertigtes Array von Token als Argument übergeben wird. Wenn eine Formelzeichenfolge an die calc-Methode übergeben wird, wird sie in ein Array von Token vorkonvertiert.
Als Hilfsmethode wird die calcExpression-Methode verwendet, die als Argumente den Operandenstapel, den Operatorstapel und die minimale Operatorrangfolge verwendet, um den Ausdruck auszuwerten.
Als Erweiterung der üblichen Formel-Engine wird eine relativ einfache Funktion calcFunction implementiert, die den Namen der Funktion als Argumente sowie eine beliebige Anzahl von Argumenten für diese Funktion verwendet. Die calcFunction wertet den Wert der Formelfunktion aus und gibt ein neues Token-Objekt mit einem numerischen Typ zurück.
Um Funktionen innerhalb des allgemeinen Berechnungszyklus zu berechnen, werden ein Funktionsstapel und eine Zuordnung für Funktionsargumente zu den Stapeln von Operanden und Operatoren hinzugefügt, wobei der Schlüssel der Name der Funktion und die Werte das Array von Argumenten sind.
Abschließend werde ich ein Beispiel geben, wie Sie eine Datenquelle in Form eines Hash von Zellen und deren Werten verwenden können. Zunächst wird eine Klasse definiert, die die vom Taschenrechner verwendete Schnittstelle implementiert:
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) );
}
}
Nun, dann ist es einfach. Wir erstellen eine Datenquelle mit Zellwerten. Dann definieren wir eine Formel, in der die Operanden Zellreferenzen sind. Und abschließend machen wir Berechnungen:
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
Vielen Dank für Ihre Aufmerksamkeit.