はじめに
PHPで数式を計算する際、安易にeval()関数を使ってしまうと、意図しないコードが実行されるセキュリティリスクを伴います。特に、ユーザーからの入力を元に計算を行うような場面では、eval()の使用は避けるべきです。本記事では、eval()に頼らず、安全に数式を計算するための手法として「逆ポーランド記法(後置記法)」への変換と、その計算方法をPHPで実装する方法を解説します。
基礎知識:逆ポーランド記法とは?
逆ポーランド記法(Reverse Polish Notation; RPN)は、演算子をオペランド(被演算子、つまり数字)の後に配置する記法です。例えば、通常の数式で1 + 2と書くところを、逆ポーランド記法では1 2 +と表現します。
この記法の利点は、括弧が不要になることと、スタック(後入れ先出しのデータ構造)を用いることで、非常にシンプルかつ効率的に計算できる点にあります。
例えば、1 (2 + 3) 4 - 5という数式を逆ポーランド記法に変換すると、1 2 3 + 4 5 -となります。
実装:逆ポーランド記法への変換と計算
ここでは、数式文字列を逆ポーランド記法に変換する関数toRpn()と、逆ポーランド記法で表された数式を計算する関数calcRpn()を実装します。
1. toRpn()関数:数式を逆ポーランド記法に変換
この関数は、入力された数式文字列を解析し、逆ポーランド記法に変換します。数式の要素(数字、演算子、括弧)を順に処理し、演算子の優先順位や括弧を考慮しながら、出力用配列と一時的な演算子スタックを操作します。
2. calcRpn()関数:逆ポーランド記法を計算
この関数は、toRpn()によって変換された逆ポーランド記法の文字列を受け取り、スタックを使って計算を実行します。数字が現れたらスタックに積んでいき、演算子が現れたらスタックからオペランドを2つ取り出して計算し、その結果を再びスタックに積む、という処理を繰り返します。
サンプルプログラム
以下に、上記2つの関数と、それらを使用したサンプルコードを示します。
- @param string $expression 数式文字列 (例: ‘1 (2 + 3) 4 – 5’)
- @return string 逆ポーランド記法に変換された文字列 (例: ‘1 2 3 + 4 5 -‘)
/
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 {
// 演算子が現れた場合
if (!empty($stack)) {
// スタックの末尾にある演算子と、現在の演算子の優先順位を比較
while (true) {
$end = end($stack);
// スタックの末尾に演算子があり、かつ現在の演算子より優先順位が低いか同等なら
if ($end && $priorities[$part] <= $priorities[$end]) {
$output []= array_pop($stack); // スタックから演算子を取り出して出力へ
} else {
// 優先順位が現在の演算子より高いか、スタックが空ならループを抜ける
break;
}
}
}
// 現在の演算子をスタックに積む
$stack[] = $part;
}
}
// 全ての要素を処理した後、スタックに残っている演算子を全て出力へ
while (count($stack) > 0) {
$output[] = array_pop($stack);
}
// 出力配列の要素をスペースで連結して返す
return implode(‘ ‘, $output);
}
/
- 逆ポーランド記法で表された数式を計算する関数
- @param string $expression 逆ポーランド記法に変換された文字列 (例: ‘1 2 3 + 4 5 -‘)
- @return float 計算結果
/
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つ取り出す
// 後ろから2つ取り出すため、bが先にpopされる
$b = (float)array_pop($stack);
$a = (float)array_pop($stack);
// 演算子に応じて計算を実行
switch ($part) {
case “+”:
$x = $a + $b;
break;
case “-“:
$x = $a – $b;
break;
case “”:
$x = $a $b;
break;
case “/”:
// ゼロ除算のチェックは省略していますが、実運用では必要です。
$x = $a / $b;
break;
}
// 計算結果をスタックに積む
array_push($stack, $x);
}
}
// 最終的にスタックに残った値が計算結果
return $stack[0];
}
// — 実行例 —
$expression = ‘1 (2 + 3) 4 – 5’;
echo “元の数式: ” . $expression . “\n”;
// 逆ポーランド記法に変換
$rpn = toRpn($expression);
echo “逆ポーランド記法: ” . $rpn . “\n”;
// 計算を実行
echo “計算結果: ” . calcRpn($rpn) . “\n”;
?>
出力結果
元の数式: 1 (2 + 3) 4 – 5
逆ポーランド記法: 1 2 3 + 4 5 –
計算結果: 15
応用・注意点
- エラーハンドリング: 上記の
calcRpn()関数では、ゼロ除算や、オペランド不足(例: `+ 2` のような不正なRPN)に対するエラーハンドリングを実装していません。実運用では、これらのケースに対応するためのチェックを追加することを強く推奨します。 - 数値形式: 現在のコードでは整数を扱いますが、浮動小数点数や負の数、指数表記など、より複雑な数値形式に対応するには、
is_numeric()の判定やpreg_match_all()の正規表現を調整する必要があります。 - 演算子の拡張: べき乗(
)や剰余(%)など、より多くの演算子に対応させる場合は、$priorities配列とcalcRpn()のswitch文にそれらを追記してください。 - コードの可読性:
toRpn()関数は「Shunting-yard algorithm(構文解析アルゴリズム)」と呼ばれる標準的なアルゴリズムに基づいています。より詳細なアルゴリズムの解説は、参考リンクなどを参照すると理解が深まります。
eval()関数を使わずに数式を安全に処理できるこの方法は、計算機能を持つアプリケーション開発において非常に有用です。