導入
Webアプリケーション開発において、ユーザーが入力した数式を動的に計算したい場面に遭遇することがあります。PHPには標準で eval() 関数が用意されていますが、これは任意のPHPコードを実行できてしまうため、深刻なセキュリティリスクを孕んでいます。本記事では、外部ライブラリに頼らず、かつ安全に数式を解析・計算するための「逆ポーランド記法(RPN)」を用いた手法を解説します。
基礎知識
逆ポーランド記法(後置記法)とは、演算子を被演算子の後ろに配置する記法です。例えば「1 + 2」は「1 2 +」となります。この記法の最大の特徴は、括弧を使わずに演算の優先順位を完全に表現できる点です。
コンピュータが計算する際は、左から順に数値をスタック(積み上げ)に格納し、演算子が現れたらスタックから必要な数値を取り出して計算し、結果を再びスタックに戻すというアルゴリズムをとります。これにより、複雑な数式もループ処理のみで安全に解くことが可能になります。
実装/解決策
実装は大きく分けて「中置記法(通常の数式)を逆ポーランド記法に変換するプロセス」と「逆ポーランド記法を計算するプロセス」の2段階で行います。
変換には「シャントヤード・アルゴリズム」を用います。スタックを使って演算子の優先順位(掛け算割り算は足し算引き算より高いなど)を管理し、適切な順序に並び替えます。
サンプルプログラム
以下のコードは、数式文字列をRPN形式に変換し、それを計算する一連の処理です。
/
$expression = ‘1 (2 + 3) 4 – 5’;
$rpn = toRpn($expression);
echo “逆ポーランド記法: ” . $rpn . PHP_EOL;
echo “計算結果: ” . calcRpn($rpn) . PHP_EOL;
// 1. 中置記法を逆ポーランド記法に変換する関数
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 ($end = end($stack)) {
if ($end == ‘(‘) { array_pop($stack); break; }
$output[] = array_pop($stack);
}
} else {
// 演算子の優先順位に基づきスタックから取り出す
while (!empty($stack) && isset($priorities[end($stack)]) && $priorities[$part] <= $priorities[end($stack)]) {
$output[] = array_pop($stack);
}
$stack[] = $part;
}
}
while (!empty($stack)) $output[] = array_pop($stack);
return implode(' ', $output);
}
// 2. 逆ポーランド記法を計算する関数
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 {
// スタックから2つ取り出し計算
$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];
}
?>
応用・注意点
1. エラーハンドリング: 上記サンプルは計算可能な前提で作成されています。実務では「ゼロ除算」や「不正な記号の混入」が発生する可能性があるため、例外処理を追加してください。
2. 負の数の扱い: 今回のロジックでは「-5」のような先頭のマイナス記号の扱いに工夫が必要です。必要に応じて正規表現によるトークン分割を強化してください。
3. セキュリティ: eval() を排除することで、コードインジェクションのリスクは完全に排除できます。堅牢なアプリケーションを作るための基本として、ぜひ活用してください。