導入
Webアプリケーション開発において、ユーザーが入力した数式をプログラムで計算したい場面は多々あります。しかし、PHPの eval() 関数を使用して文字列をそのまま実行するのは、非常に危険です。悪意のあるコードが注入された場合、サーバーの乗っ取りやデータ漏洩などの深刻なセキュリティ事故につながる恐れがあるからです。本記事では、eval() を使わずに安全に数式を解析・計算するための「逆ポーランド記法(後置記法)」を用いた実装手法を解説します。
基礎知識
逆ポーランド記法とは、演算子を対象となる数値の後ろに配置する記法です。通常、私たちが使っている「1 + 2」のような記法は「中置記法」と呼ばれます。
逆ポーランド記法(1 2 +)に変換することで、括弧を排除し、左から順にスタック(データの積み上げ)を利用して計算を進めるだけで答えが得られるようになります。これにより、計算順序を意識した複雑な制御構造を自前で実装する必要がなくなります。
実装/解決策
数式の計算は、大きく分けて2つのステップで行います。
1. 中置記法から逆ポーランド記法への変換:演算子の優先順位と括弧を考慮し、スタックを用いて並び替えます。
2. 逆ポーランド記法の計算:数値をスタックに積み、演算子が現れたらスタックから2つ取り出して計算、結果を再度スタックに戻すという手順を繰り返します。
サンプルプログラム
以下のコードは、数式文字列を逆ポーランド記法に変換し、それを評価して計算結果を出力する実用的なサンプルです。
/
function toRpn($expression) {
$expression = preg_replace(‘/\s/’, ”, $expression);
preg_match_all(‘/[0-9]+|\+|-|\|\/|\(|\)/’, $expression, $matches);
$parts = $matches[0];
$stack = [];
$output = [];
// 演算子の優先順位定義
$priorities = [‘/’ => 2, ” => 2, ‘+’ => 1, ‘-‘ => 1, ‘(‘ => -1, ‘)’ => -1];
foreach ($parts as $part) {
if (is_numeric($part)) {
$output[] = $part;
} elseif ($part == ‘(‘) {
$stack[] = $part;
} elseif ($part == ‘)’) {
while (count($stack) > 0) {
$end = end($stack);
if ($end == ‘(‘) {
array_pop($stack);
break;
} else {
$output[] = array_pop($stack);
}
}
} else {
while (!empty($stack) && end($stack) !== ‘(‘ && $priorities[$part] <= $priorities[end($stack)]) {
$output[] = array_pop($stack);
}
$stack[] = $part;
}
}
while (count($stack) > 0) {
$output[] = array_pop($stack);
}
return implode(‘ ‘, $output);
}
/
- 逆ポーランド記法の計算結果を算出する
/
function calcRpn($expression) {
$parts = preg_split(‘/\s/’, $expression, -1, PREG_SPLIT_NO_EMPTY);
$stack = [];
foreach ($parts as $part) {
if (is_numeric($part)) {
$stack[] = (float)$part;
} else {
$b = array_pop($stack);
$a = array_pop($stack);
switch ($part) {
case “+”: $stack[] = $a + $b; break;
case “-“: $stack[] = $a – $b; break;
case “”: $stack[] = $a $b; break;
case “/”: $stack[] = $a / $b; break;
}
}
}
return $stack[0];
}
$expression = ‘1 (2 + 3) 4 – 5’;
$rpn = toRpn($expression);
echo “逆ポーランド記法: ” . $rpn . PHP_EOL;
echo “計算結果: ” . calcRpn($rpn);
?>
応用・注意点
1. 例外処理:上記のサンプルは簡易的なものです。実務では、「0による除算」や「不正な数式(括弧の不一致など)」が入力された場合に備え、try-catchブロックで例外をキャッチする実装を推奨します。
2. 浮動小数点数:計算精度が必要な場合は、float ではなく BCMath 拡張モジュールを使用して、文字列として正確な計算を行うようにしてください。
3. セキュリティ:eval() を使わないこのアプローチであれば、ユーザーからの入力を安全に処理できますが、数式の長さや複雑さに制限を設けるなど、リソース枯渇攻撃(DoS)対策も検討しておきましょう。