【PHP実践|実務向け】PHPで安全に数式を計算する:逆ポーランド記法による実装

はじめに

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()関数を使わずに数式を安全に処理できるこの方法は、計算機能を持つアプリケーション開発において非常に有用です。

    タイトルとURLをコピーしました