【JS応用】任意の量の括弧で合計する

任意の量の括弧で合計する:ネストされた構造を安全かつ効率的に処理するアルゴリズム

フロントエンド開発において、ユーザーが入力した数式や、APIから取得した複雑なネスト構造を持つ文字列をパースし、その合計値を算出するタスクは、計算機アプリや予算管理ツール、あるいは設定ファイルの動的評価などで頻繁に遭遇する課題です。特に、任意の深さまでネストされた括弧(例: `(1 + (2 * 3) + (4 + (5 + 6)))`)を扱う場合、単なる正規表現による置換では限界があります。本稿では、スタックを用いたアルゴリズムを中心に、堅牢で拡張性の高い実装手法を詳細に解説します。

ネストされた括弧の計算における技術的課題

括弧が入れ子になっている場合、最も内側の括弧から順に計算を解決していく必要があります。これは、データ構造における「スタック(Stack)」の概念そのものです。再帰的なアプローチも考えられますが、スタックを使用することで、メモリ消費を抑えつつ、ループによる命令的な処理が可能となり、JavaScriptのコールスタック制限(最大再帰深度)を回避できるというメリットがあります。

また、単なる数値の加算だけでなく、演算子の優先順位(四則演算のルール)をどのように扱うかも重要です。単純な「合計」という要件であっても、括弧内の式を独立したスコープとして捉え、それぞれの計算結果を親スコープへと還元していくプロセスが鍵となります。

スタックを利用したパースアルゴリズムの実装詳細

計算を処理する際、以下のステップでロジックを組み立てます。

1. 文字列をトークン(数値、演算子、括弧)に分解する。
2. 現在の計算スコープをスタックで管理する。
3. 左括弧 `(` が現れたら、新しいスタック階層を作成し、現在の計算状態を退避させる。
4. 右括弧 `)` が現れたら、現在の階層の計算を終了し、結果を親階層に統合する。
5. 数値や演算子が現れたら、現在の階層のバッファに追加する。

このアプローチの利点は、任意の深さまで対応できる点にあります。また、エラーハンドリングを各ステップで挿入することで、不正な括弧の閉じ忘れ(バランスエラー)を即座に検知可能です。

サンプルコード:安全な括弧計算機の実装

以下に、ネストされた括弧を含む文字列をパースし、合計値を算出する堅牢な実装例を示します。ここでは、シンプルにするために加算と括弧に焦点を当てますが、必要に応じて乗除算のロジックを拡張可能です。


/**
 * 任意のネストされた括弧を含む数式を評価する関数
 * @param {string} expression - 例: "(1 + (2 + 3) + 4)"
 * @returns {number}
 */
function calculateNestedExpression(expression) {
    const tokens = expression.replace(/\s+/g, '').split('');
    const stack = [0]; // 各階層の合計値を保持するスタック
    let currentNumber = '';
    let lastOperator = '+';

    const applyOperation = (op, num) => {
        const value = parseInt(num, 10);
        if (isNaN(value)) return;
        
        if (op === '+') {
            stack[stack.length - 1] += value;
        } else if (op === '-') {
            stack[stack.length - 1] -= value;
        }
    };

    for (let i = 0; i < tokens.length; i++) {
        const char = tokens[i];

        if (/\d/.test(char)) {
            currentNumber += char;
        } else if (char === '(') {
            stack.push(0); // 新しいスコープを開始
            lastOperator = '+'; // 括弧内は初期状態から計算
        } else if (char === ')') {
            applyOperation(lastOperator, currentNumber);
            currentNumber = '';
            const result = stack.pop(); // 現在のスコープの結果を取得
            // 結果を数値として次の計算に回すための処理
            currentNumber = result.toString();
            lastOperator = '+';
        } else if (char === '+' || char === '-') {
            applyOperation(lastOperator, currentNumber);
            currentNumber = '';
            lastOperator = char;
        }
    }

    // 最後の数値を処理
    applyOperation(lastOperator, currentNumber);
    return stack[0];
}

console.log(calculateNestedExpression("(1 + (2 + 3) + 4)")); // 出力: 10

実務における最適化と安全性の確保

実務のフロントエンド開発において、このような計算ロジックを実装する際には、以下の点に留意してください。

1. 入力のサニタイズ: ユーザー入力には悪意のあるコードが含まれる可能性があります。`eval()` を使用することは論外ですが、上記のようにトークンを一つずつ解析するロジック(スキャナ)を使用することで、安全に計算を行えます。
2. パフォーマンスの考慮: 数式が非常に長い場合、文字列の連結や分割を繰り返すとメモリ負荷が高まります。可能な限り、インデックスを保持して文字列を走査するポインタベースの手法を採用してください。
3. エラーハンドリングの強化: 括弧の数が合わない場合、`stack` の長さが不正になります。`try...catch` ブロックだけでなく、スタックのアンダーフローを検知するバリデーションを導入し、UI上で「無効な数式です」とユーザーにフィードバックすることが重要です。
4. 拡張性: 今回の例では加減算のみですが、乗除算を導入する場合、「演算子の優先順位」を考慮する必要があります。その場合は、スタックに数だけでなく演算子も保持する「逆ポーランド記法(RPN)」への変換アルゴリズムを併用するのが標準的です。

まとめ

「任意の量の括弧で合計する」という課題は、一見単純な数学的処理に見えますが、その裏側にはスタックデータ構造の理解、トークン解析、そして計算順序の制御という、プログラミングの基礎知識が凝縮されています。

フロントエンドにおいて、複雑な入力値を安全かつ高速に処理することは、UXを向上させる上で不可欠です。特に、ライブラリやフレームワークに依存せず、アルゴリズムの原理原則を理解して実装することで、将来的な仕様変更やパフォーマンス改善にも柔軟に対応できる強固なコードベースを維持できます。今回紹介したスタックベースのアプローチを、ぜひ実際のプロジェクトの入力バリデーションや計算ロジックに取り入れてみてください。複雑なネスト構造を扱う際の心理的な障壁が、技術的な理解によって確かな自信へと変わるはずです。

コメント

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