Skip to content

Instantly share code, notes, and snippets.

@think49
Last active October 18, 2023 15:04
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save think49/54b074cab2145efddb48765652c74710 to your computer and use it in GitHub Desktop.
Save think49/54b074cab2145efddb48765652c74710 to your computer and use it in GitHub Desktop.
eval-calculation.js: 計算式の文字列を評価する

eval-calculation.js

概要

eval-calculation.js は引数として与えられた計算式の文字列を評価し、計算結果となる数値を返します。

設置方法

外部スクリプトとしてeval-calculation.jsを指定して下さい。

<script src="./eval-calculation.js"></script>
``

## 使い方

evalCalculation()の第一引数に計算式となる文字列を指定する事で計算結果となる数値を返します。

```JavaScript
evalCalculation('2*3');  // 6

数字と演算子の間に空白を入れても構いません。

evalCalculation('2 * 3 + 5 * 6 / 2');  // 21

負の数を解釈できます。

evalCalculation('-3 * -4');  // 12

.1 は 0.1 として評価されます。

evalCalculation('.1 * 5');  // 0.5

JavaScript では IEEE754 の仕様規定によって浮動小数点演算で丸め誤差が生じますが、evalCalculation() は内部的に整数に変換してから演算する為、丸め誤差は生じません。

0.6 * 3;                     // 1.7999999999999998
evalCalculation('0.6 * 3');  // 1.8

乗算と除算の複合式では左から順番に演算するのが通常ですが、左から演算していくと途中で循環小数になってしまい、演算結果が正しくならないかもしれません。 そこで乗算を先に演算する事で、一部の循環小数を経由する式に対して、循環小数を経由せずに演算するようにします。 例えば、下記式は (1 * 3) / 3 に置き換えて演算します。

evalCalculation('1 / 3 * 3');  // 1

括弧付きの計算式も正しく解釈します。

evalCalculation('(1+(2+3*4))/5');  // 3

eval() と違い、JavaScriptコードを評価出来ませんので、安全に計算式を評価できます。

eval('alert("hoge")');            // alert() が実行される
evalCalculation('alert("hoge")'); // SyntaxError: An expression starts with an unexpected token: a

仕様/既知の問題

  • 整数に変換する事で整数演算している為、IEEE754 の制約があります。つまり、内部処理で「-9007199254740991 ~ 9007199254740991」を超える整数演算をした場合には正確な値を算出できません。
  • パフォーマンス上の問題。現行仕様では「式文字列から演算可能な四則演算式を探して演算後、括弧を取り外していく方式」を採用しています。この方式は、何度も式文字列から全文検索する為、効率が悪い可能性があります。正確にはベンチマークをとる必要がありますが、「文字列の構文解析を一度に行い、優先順位の高い順番で演算していく方式」が現行仕様よりも高速に処理できる可能性があります。

更新履歴

ver.1.0.0 (2017/09/19)

  • 括弧付きの式に対応した
  • 不正な式に対し、SyntaxError を返すようにした

ver.0.9.2 (2017/09/19)

  • 除算に先んじて乗算を演算するようにした (※更新後にver.0.9.1で既に実現できていた事に気が付きましたが、せっかく作ったのでコードを残します) ver.0.9.1では実現できていませんでした。現代のブラウザでは 0.3333333333333333 * 3=== 1 が成立する事から ver.0.9.1 の挙動を誤解していました。意味のある処理の為、ver.0.9.2 で更新したコードは後継版でも残しました。

ver.0.9.1 (2016/11/11)

  • 初版

参考リンク

/**
* eval-calculation.js
* evaluate calculation formula.
*
* @version 1.0.0
* @author think49
* @url https://gist.github.com/think49/54b074cab2145efddb48765652c74710
* @license http://www.opensource.org/licenses/mit-license.php (The MIT License)
*/
var evalCalculation = (function (String, pow, max) {
'use strict';
function getDecimalPartLength (numberString) {
var result = /\.\d+$/.exec(numberString);
return result ? result[0].length - 1 : 0;
}
var calcMultiplyOrDivide = (function (push) {
return function calcMultiplyOrDivide (expression) {
var multiplyOrDivide = /\*(-?(?:\d+(?:\.\d+)?|\.\d+))|\/(-?(?:\d+(?:\.\d+)?|\.\d+))|(-?(?:\d+(?:\.\d+)?|\.\d+))/g, multiply = [], divide = [], number, token, i, len;
while (token = multiplyOrDivide.exec(expression)) {
if (token[1]) {
multiply.push(['*', token[1]]);
} else if (token[2]) {
divide.push(['/', token[2]])
} else if (token[3]) {
number = token[3];
} else {
throw new Error('Unknown exception');
}
}
push.apply(multiply, divide); // multiply before divide
i = 0;
len = multiply.length;
expression = number;
while (i < len) {
token = multiply[i++];
number = calcDyadicOperator('', number, token[0], token[1])
}
return number;
}
}(Array.prototype.push));
function calcDyadicOperator (matched, number1, operator, number2) {
var decimalPart = /\.\d+$/, powerNumber1, powerNumber2, result;
switch (operator) {
case '+':
powerNumber1 = pow(10, max(getDecimalPartLength(number1), getDecimalPartLength(number2)));
result = (powerNumber1 * number1 + powerNumber1 * number2) / powerNumber1;
break;
case '-':
powerNumber1 = pow(10, max(getDecimalPartLength(number1), getDecimalPartLength(number2)));
result = (powerNumber1 * number1 - powerNumber1 * number2) / powerNumber1;
break;
case '*':
powerNumber1 = pow(10, getDecimalPartLength(number1));
powerNumber2 = pow(10, getDecimalPartLength(number2));
result = (number1 * powerNumber1) * (number2 * powerNumber2) / (powerNumber1 * powerNumber2);
break;
case '/':
powerNumber1 = pow(10, max(getDecimalPartLength(number1), getDecimalPartLength(number2)));
result = (number1 * powerNumber1) / (number2 * powerNumber1);
break;
default:
console.warn('expression: ' + number1 + operator + number2);
throw new SyntaxError(operator + ' is not a operator');
}
return result;
}
function removeParentheses (matched, number) {
return number;
}
return function evalCalculation (expression) {
var multiplyOrDivide = /-?(?:\d+(?:\.\d+)?|\.\d+)(?:[*/]-?(?:\d+(?:\.\d+)?|\.\d+))+/g,
addOrSubtract = /(-?(?:\d+(?:\.\d+)?|\.\d+))([+-])(-?(?:\d+(?:\.\d+)?|\.\d+))/g,
numberWithParentheses = /\((-?(?:\d+(?:\.\d+)?|\.\d+))\)/g,
illegalString;
expression = String(expression).replace(/\s+/g, '');
if (illegalString = /^(?!-?(?:\d+(?:\.\d+)?|\.\d+)|\()[\s\S]/.exec(expression)) {
throw new SyntaxError('An expression starts with an unexpected token: ' + illegalString[0]);
}
if (illegalString = /[^\d)]$/.exec(expression)) {
throw new SyntaxError('An expression ends with an unexpected token: ' + illegalString[0]);
}
if (illegalString = /\d*(?:\.\d*){2,}/.exec(expression)) {
throw new SyntaxError('Illegal number: ' + illegalString[0]);
}
do {
expression = expression.replace(numberWithParentheses, removeParentheses);
while (multiplyOrDivide.test(expression)) {
expression = expression.replace(multiplyOrDivide, calcMultiplyOrDivide);
}
while (addOrSubtract.test(expression)) {
expression = expression.replace(addOrSubtract, calcDyadicOperator);
}
} while (numberWithParentheses.test(expression));
if (illegalString = /[^\d.+-]/.exec(expression)) {
throw new SyntaxError('An expression with an unexpected token: ' + illegalString[0])
}
return +expression; // ToNumber
};
}(String, Math.pow, Math.max));
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>test - calculation.js</title>
</head>
<body>
<script src="eval-calculation-1.0.0.js"></script>
<script>
'use strict';
console.assert(evalCalculation('2*3') === 6);
console.assert(evalCalculation('2 * 3 + 5 * 6 / 2') === 21);
console.assert(evalCalculation('-3 * -4') === 12);
console.assert(evalCalculation('.1 * 5') === 0.5);
console.assert(evalCalculation('0.6 * 3') === 1.8);
console.assert(evalCalculation('1 / 3 * 3') === 1);
console.assert(evalCalculation('(1+2)*(3-4)/5') === -0.6);
console.assert(evalCalculation('(1+(2+3*4))/5') === 3);
</script>
<script>
'use strict';
evalCalculation('(1+(2+3*4)/5'); // SyntaxError: An expression ends with an unexpected token: (
</script>
<script>
'use strict';
evalCalculation('alert("hoge")'); // SyntaxError: An expression starts with an unexpected token: a
</script>
</body>
</html>
@think49
Copy link
Author

think49 commented Nov 11, 2016

jsfiddleにもUPしていますが、v1.0.0を最後にjsfiddleの記事は更新しない予定です(更新の手間がかかる為)。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment