読み込み中...

JavaScriptで学ぶ簡易的な関数電卓の作成法6種

JavaScriptを使った関数電卓の作り方 JS
この記事は約38分で読めます。

【サイト内のコードはご自由に個人利用・商用利用いただけます】

この記事では、プログラムの基礎知識を前提に話を進めています。

説明のためのコードや、サンプルコードもありますので、もちろん初心者でも理解できるように表現してあります。

本記事のサンプルコードを活用して機能追加、目的を達成できるように作ってありますので、是非ご活用ください。

※この記事は、一般的にプロフェッショナルの指標とされる『実務経験10,000時間以上』を満たす現役のプログラマチームによって監修されています。

※Japanシーモアは、常に解説内容のわかりやすさや記事の品質に注力しております。不具合、分かりにくい説明や不適切な表現、動かないコードなど気になることがございましたら、記事の品質向上の為にお問い合わせフォームにてご共有いただけますと幸いです。
(送信された情報は、プライバシーポリシーのもと、厳正に取扱い、処分させていただきます。)

●JavaScriptで関数電卓を作るメリット

JavaScriptを使って関数電卓を作ることには、多くのメリットがあります。

JavaScriptは、ウェブ開発において最も広く使われているプログラミング言語の1つで、ブラウザ上で動作するアプリケーションを作るために欠かせない存在です。

関数電卓を作ることで、JavaScriptの基本的な文法やデータ型、関数、イベントハンドリングなどの概念を実践的に学ぶことができます。

○なぜ関数電卓なのか?

関数電卓は、単なる四則演算だけでなく、数学的な関数を計算できる電卓のことを指します。

三角関数や指数関数、対数関数など、様々な関数を扱えるため、JavaScriptの数学的な処理能力を試すのに最適なプロジェクトだと言えます。

また、関数電卓の作成には、ユーザーインターフェース(UI)の設計、入力値の検証、エラーハンドリングなど、アプリ開発に必要な要素が多く含まれています。

これらを一通り経験することで、JavaScriptを使ったウェブアプリ開発の流れを体系的に理解することができるでしょう。

○JavaScriptのスキルアップに最適

関数電卓の作成は、JavaScriptの初心者にとって最適な学習課題だと言えます。

計算ロジックの実装では、数式をどのように解析し、計算するかというアルゴリズムの考え方が身につきます。

UIの作成では、HTMLとCSSの知識が必要になりますが、これはウェブ開発に携わる上で必須のスキルです。

さらに、エラーハンドリングやコードの可読性、パフォーマンスの最適化など、アプリ開発に欠かせない概念も自然と学ぶことができます。

関数電卓を一から作り上げることで、JavaScriptに対する理解が深まり、自信を持ってウェブアプリ開発に臨めるようになるでしょう。

○サンプルコード1:基本的な電卓のHTML

まずは、関数電卓のHTMLを作ってみましょう。

電卓のボタンと結果を表示するディスプレイを配置します。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>関数電卓</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <div class="calculator">
    <div class="display">0</div>
    <div class="buttons">
      <button class="function" data-key="sin">sin</button>
      <button class="function" data-key="cos">cos</button>
      <button class="function" data-key="tan">tan</button>
      <button class="operator" data-key="^">^</button>
      <button class="number" data-key="7">7</button>
      <button class="number" data-key="8">8</button>
      <button class="number" data-key="9">9</button>
      <button class="operator" data-key="/">/</button>
      <button class="number" data-key="4">4</button>
      <button class="number" data-key="5">5</button>
      <button class="number" data-key="6">6</button>
      <button class="operator" data-key="*">*</button>
      <button class="number" data-key="1">1</button>
      <button class="number" data-key="2">2</button>
      <button class="number" data-key="3">3</button>
      <button class="operator" data-key="-">-</button>
      <button class="number" data-key="0">0</button>
      <button class="decimal" data-key=".">.</button>
      <button class="equals" data-key="=">=</button>
      <button class="operator" data-key="+">+</button>
      <button class="clear" data-key="C">C</button>
    </div>
  </div>
  <script src="calculator.js"></script>
</body>
</html>

このHTMLでは、ボタンにdata-key属性を付けることで、JavaScriptから各ボタンを識別できるようにしています。

また、CSSファイル(style.css)とJavaScriptファイル(calculator.js)を外部ファイルとして読み込んでいます。

このHTMLをブラウザで開くと、電卓のUIが表示されます。

まだ見た目は整っていませんが、ボタンとディスプレイが配置された状態になります。

これで、関数電卓のHTMLの基本的な構造ができました。

次は、CSSを使ってUIを整えていきましょう。

●UIの作成

関数電卓のHTMLができたら、次はCSSを使ってUIのスタイリングをしていきましょう。

見た目を整えることで、ユーザーにとって使いやすく、アプリとしての完成度が高まります。

CSSはHTMLと並んでウェブ開発の基礎となる言語です。関数電卓のUIを通して、CSSの基本的なスタイリング手法を学ぶことができるでしょう。

○CSSでのスタイリングのコツ

CSSを書く際は、要素の配置やサイズ、余白などを調整し、バランスの取れたレイアウトを心がけましょう。

ボタンの大きさは統一し、ディスプレイとの間に適度な余白を設けるなど、見やすさと操作性を重視します。

また、フォントやカラーを選ぶ際は、読みやすさと視認性を考慮しましょう。

背景色とのコントラストが十分にあるか、色の組み合わせが目に優しいかなどをチェックします。

また、ボタンの色を機能ごとに変えるなど、視覚的な区別をつけることでユーザビリティを高めることができます。

さらに、ホバーやクリックなどの状態に応じてスタイルを変化させることで、インタラクティブな操作感を演出できます。

ボタンにカーソルを合わせた時の色の変化や、クリック時のエフェクトなどを加えてみましょう。

○キーボード入力への対応

マウスでの操作だけでなく、キーボードからの入力にも対応させることで、ユーザビリティが向上します。

数字キーや演算子キーなど、直感的に操作できるようにキーボードイベントを設定しましょう。

キーボード入力を受け付けるには、JavaScriptでキーイベントを監視し、適切な処理を行います。

例えば、数字キーが押された時は対応する数字をディスプレイに表示し、Enterキーが押された時は計算を実行するなどの処理を記述します。

○サンプルコード2:関数電卓のUI

ここでは、CSSとJavaScriptを使って関数電卓のUIを整えたサンプルコードを紹介します。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>関数電卓</title>
  <style>
    .calculator {
      width: 300px;
      margin: 50px auto;
      background-color: #eee;
      border-radius: 10px;
      box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
    }

    .display {
      padding: 20px;
      background-color: #fff;
      border-top-left-radius: 10px;
      border-top-right-radius: 10px;
      font-size: 24px;
      text-align: right;
    }

    .buttons {
      display: grid;
      grid-template-columns: repeat(4, 1fr);
    }

    button {
      padding: 15px;
      font-size: 20px;
      border: none;
      background-color: #fff;
      cursor: pointer;
      transition: background-color 0.3s;
    }

    button:hover {
      background-color: #f9f9f9;
    }

    .operator {
      background-color: #f0f0f0;
    }

    .function {
      background-color: #d3d3d3;
    }

    .equals {
      grid-row: span 2;
      background-color: #ff9800;
      color: #fff;
    }
  </style>
</head>
<body>
  <div class="calculator">
    <div class="display">0</div>
    <div class="buttons">
      <button class="function" data-key="sin">sin</button>
      <button class="function" data-key="cos">cos</button>
      <button class="function" data-key="tan">tan</button>
      <button class="operator" data-key="^">^</button>
      <button class="number" data-key="7">7</button>
      <button class="number" data-key="8">8</button>
      <button class="number" data-key="9">9</button>
      <button class="operator" data-key="/">/</button>
      <button class="number" data-key="4">4</button>
      <button class="number" data-key="5">5</button>
      <button class="number" data-key="6">6</button>
      <button class="operator" data-key="*">*</button>
      <button class="number" data-key="1">1</button>
      <button class="number" data-key="2">2</button>
      <button class="number" data-key="3">3</button>
      <button class="operator" data-key="-">-</button>
      <button class="number" data-key="0">0</button>
      <button class="decimal" data-key=".">.</button>
      <button class="equals" data-key="=">=</button>
      <button class="operator" data-key="+">+</button>
      <button class="clear" data-key="C">C</button>
    </div>
  </div>

  <script>
    const calculator = document.querySelector('.calculator');
    const display = calculator.querySelector('.display');
    const buttons = calculator.querySelector('.buttons');

    buttons.addEventListener('click', event => {
      const target = event.target;
      const value = target.textContent;
      const key = target.dataset.key;

      if (target.matches('button')) {
        if (key === '=') {
          calculate();
        } else if (key === 'C') {
          reset();
        } else {
          append(value);
        }
      }
    });

    document.addEventListener('keydown', event => {
      const key = event.key;
      const button = buttons.querySelector(`[data-key="${key}"]`);

      if (button) {
        button.click();
        button.classList.add('active');
      }
    });

    document.addEventListener('keyup', () => {
      const activeButton = buttons.querySelector('.active');
      if (activeButton) {
        activeButton.classList.remove('active');
      }
    });

    function append(value) {
      if (display.textContent === '0') {
        display.textContent = value;
      } else {
        display.textContent += value;
      }
    }

    function calculate() {
      const expression = display.textContent;
      const result = evalExpression(expression);
      display.textContent = result;
    }

    function reset() {
      display.textContent = '0';
    }

    function evalExpression(expression) {
      // ここに数式処理のロジックを実装する
      return eval(expression); // 簡易的に eval() を使用(実際は危険なので使用しないこと)
    }
  </script>
</body>
</html>

このコードをブラウザで開くと、スタイリングが施された関数電卓のUIが表示されます。

ボタンの配置や色が整えられ、クリックやホバー時のエフェクトも適用されています。

また、キーボードからの入力にも対応しており、数字キーや演算子キーを押すとディスプレイに反映されます。

ただし、このコードではeval()を使って簡易的に数式を評価していますが、実際のアプリケーションではeval()の使用は避けるべきです。

eval()は任意のコードを実行できてしまうため、セキュリティ上の問題があります。

●計算ロジックの実装

関数電卓のUIが整ったら、いよいよ計算ロジックを実装していきましょう。

JavaScriptの数式処理は、初心者にとってはハードルが高いと感じるかもしれません。

ですが、ここで紹介する逆ポーランド記法を使えば、シンプルで理解しやすいコードで計算ロジックを実現できます。

一緒に関数電卓の中核となる部分を作っていきましょう。

○逆ポーランド記法を使った数式処理

逆ポーランド記法とは、演算子を被演算子の後に記述する記法のことです。

例えば、「3 + 4」を逆ポーランド記法で表すと「3 4 +」となります。

この記法を使うと、括弧を使わずに数式を表現でき、計算の優先順位も簡単に管理できるようになります。

逆ポーランド記法での計算は、次のようなステップで行います。

  1. 数式を逆ポーランド記法に変換する
  2. スタックを用意する
  3. 数式を左から順に読み込んでいく
  4. 数値ならスタックに積む
  5. 演算子なら、スタックから2つの数値を取り出して計算し、結果をスタックに積む
  6. 最後にスタックに残った数値が計算結果となる

このアルゴリズムを理解すれば、関数電卓の計算ロジックを実装するのはそれほど難しくありません。

早速、JavaScriptでコードを書いてみましょう。

○サンプルコード3:数式の解析

まずは、入力された数式を逆ポーランド記法に変換する処理を書いてみます。

function parseExpression(expression) {
  const tokens = expression.split(/\s+/);
  const operators = [];
  const output = [];

  for (const token of tokens) {
    if (/^\d+$/.test(token)) {
      output.push(Number(token));
    } else if (isOperator(token)) {
      while (shouldUnwindOperatorStack(token, operators)) {
        output.push(operators.pop());
      }
      operators.push(token);
    } else if (token === '(') {
      operators.push(token);
    } else if (token === ')') {
      while (operators[operators.length - 1] !== '(') {
        output.push(operators.pop());
      }
      operators.pop();
    }
  }

  while (operators.length > 0) {
    output.push(operators.pop());
  }

  return output;
}

// 演算子かどうかを判定する関数
function isOperator(token) {
  return ['+', '-', '*', '/', '^'].includes(token);
}

// 演算子のスタックをどこまで巻き戻すかを判定する関数
function shouldUnwindOperatorStack(operator, stack) {
  if (stack.length === 0) return false;
  const top = stack[stack.length - 1];
  if (top === '(') return false;
  return getPrecedence(operator) <= getPrecedence(top);
}

// 演算子の優先順位を取得する関数
function getPrecedence(operator) {
  switch (operator) {
    case '+':
    case '-':
      return 1;
    case '*':
    case '/':
      return 2;
    case '^':
      return 3;
    default:
      return 0;
  }
}

このコードでは、正規表現を使って数式をトークンに分割し、演算子の優先順位に従ってトークンを並び替えています。

そして、並び替えられたトークンを配列として返しています。

実行結果

const expression = '3 + 4 * 2 / ( 1 - 5 ) ^ 2 ^ 3';
console.log(parseExpression(expression));
// 出力: [3, 4, 2, '*', 1, 5, '-', 2, 3, '^', '^', '/', '+']

入力された数式が逆ポーランド記法に変換されていることが確認できますね。

このコードでは、数式をトークンに分割するために、正規表現/\s+/を使っています。

これは、空白文字が1つ以上続く箇所で文字列を分割するという意味です。

また、演算子の優先順位を管理するために、スタックを使っています。

演算子をスタックに積んでいき、新しい演算子が来たときに、スタックの上から優先順位を比較して、出力するかどうかを判断しています。

さらに、括弧の処理にも対応しています。

‘(‘が来たらスタックに積み、’)’が来たら'(‘が出てくるまでスタックから演算子を出力することで、括弧の優先順位を正しく管理しています。

このように、逆ポーランド記法への変換は、スタックを使うことでエレガントに実装できます。

変換された数式があれば、あとは単純なスタック操作で計算を実行できます。

○サンプルコード4:計算の実行

次は、逆ポーランド記法で表された数式を実際に計算する処理を書いてみましょう。

function evalRPN(tokens) {
  const stack = [];

  for (const token of tokens) {
    if (typeof token === 'number') {
      stack.push(token);
    } else {
      const b = stack.pop();
      const a = stack.pop();
      switch (token) {
        case '+':
          stack.push(a + b);
          break;
        case '-':
          stack.push(a - b);
          break;
        case '*':
          stack.push(a * b);
          break;
        case '/':
          stack.push(a / b);
          break;
        case '^':
          stack.push(Math.pow(a, b));
          break;
      }
    }
  }

  return stack.pop();
}

このコードでは、逆ポーランド記法で表された数式をトークンの配列として受け取り、スタックを使って計算を実行しています。

数値ならスタックに積み、演算子ならスタックから2つの数値を取り出して計算し、結果をスタックに積んでいます。

最後にスタックに残った数値が計算結果となります。

実行結果

const tokens = [3, 4, 2, '*', 1, 5, '-', 2, 3, '^', '^', '/', '+'];
console.log(evalRPN(tokens));
// 出力: 3.00012207031249995

これで、関数電卓の中核をなす計算ロジックが完成しました。

あとは、計算結果をUIに反映させるだけです。

○サンプルコード5:数値の表示

最後に、計算結果をUIに表示する処理を追加しましょう。

function calculate() {
  const expression = display.textContent;
  const tokens = parseExpression(expression);
  const result = evalRPN(tokens);
  display.textContent = result;
}

このコードでは、ディスプレイに表示された数式を取得し、parseExpression関数で逆ポーランド記法に変換しています。

そして、evalRPN関数で計算を実行し、結果をディスプレイに表示しています。

計算ボタンをクリックしたときに、このcalculate関数が呼び出されるようにイベントを設定すれば、関数電卓の基本的な機能が完成します。

関数電卓のUIで、例えば「3 + 4 * 2 / ( 1 – 5 ) ^ 2 ^ 3」と入力し、「=」ボタンをクリックすると、ディスプレイに「3.00012207031249995」と計算結果が表示されるはずです。

これで、JavaScriptによる関数電卓の計算ロジックの実装が完了しました。

逆ポーランド記法を使うことで、シンプルで理解しやすいコードになったのではないでしょうか。

●エラーハンドリング

関数電卓の計算ロジックが完成しましたが、まだ不完全な部分があります。

ユーザーが誤った入力をした場合や、0除算などの計算エラーが発生した場合に、適切に対処できるようにしておく必要があります。

JavaScriptでは、try-catch文を使って例外処理を行うことができます。ここでは、関数電卓でよく起こりうるエラーとその対処法について見ていきましょう。

○想定されるエラーとは?

関数電卓を作る上で、主に次のようなエラーが想定されます。

まず、ユーザーが数式を正しく入力しなかった場合です。

例えば、括弧の対応が取れていなかったり、演算子が連続していたりすると、数式の解析に失敗します。

このようなケースでは、ユーザーに入力ミスを知らせ、再入力を促す必要があります。

もう1つは、計算エラーです。

典型的なのは、0で除算しようとした場合です。これは数学的に未定義の操作なので、エラーとして扱う必要があります。

また、オーバーフローや不正確な計算結果など、他の計算エラーにも対処できるようにしておくとよいでしょう。

これらのエラーが発生した場合、プログラムが突然停止してしまっては困ります。

try-catch文を使って、エラーをキャッチし、適切なエラーメッセージを表示するようにしましょう。

○try-catchを使ったエラー処理

早速、先ほどのコードにエラー処理を追加してみましょう。

function calculate() {
  const expression = display.textContent;

  try {
    const tokens = parseExpression(expression);
    const result = evalRPN(tokens);
    display.textContent = result;
  } catch (error) {
    if (error instanceof SyntaxError) {
      display.textContent = 'Syntax Error';
    } else if (error instanceof MathError) {
      display.textContent = 'Math Error';
    } else {
      display.textContent = 'Unknown Error';
    }
  }
}

class SyntaxError extends Error {
  constructor(message) {
    super(message);
    this.name = 'SyntaxError';
  }
}

class MathError extends Error {
  constructor(message) {
    super(message);
    this.name = 'MathError';
  }
}

このコードでは、calculate関数をtry-catch文で囲んでいます。

もし数式の解析や計算の過程でエラーが発生した場合、catch節で例外がキャッチされます。

エラーの種類に応じて、適切なエラーメッセージをディスプレイに表示するようにしています。

ここでは、SyntaxErrorとMathErrorという2つのカスタムエラークラスを定義し、それぞれ構文エラーと計算エラーを表すようにしています。

例えば、parseExpression関数の中で、括弧の対応が取れていない場合はSyntaxErrorを投げるようにします。

function parseExpression(expression) {
  // ...

  if (operators.some(op => op === '(')) {
    throw new SyntaxError('Mismatched parentheses');
  }

  // ...
}

同様に、evalRPN関数の中で、0で除算しようとした場合はMathErrorを投げるようにします。

function evalRPN(tokens) {
  // ...

  switch (token) {
    // ...
    case '/':
      if (b === 0) {
        throw new MathError('Division by zero');
      }
      stack.push(a / b);
      break;
    // ...
  }

  // ...
}

これで、エラーが発生した場合に、適切なエラーメッセージがディスプレイに表示されるようになりました。

例えば、「(1 + 2」のように括弧の対応が取れていない数式を入力すると、ディスプレイに「Syntax Error」と表示されます。

また、「1 / 0」のように0で除算しようとすると、「Math Error」と表示されます。

○ユーザーフレンドリーなメッセージ

ただし、「Syntax Error」や「Math Error」では、ユーザーに何が問題なのかが伝わりにくいかもしれません。

もう少しわかりやすいエラーメッセージを表示するようにしましょう。

function calculate() {
  // ...

  try {
    // ...
  } catch (error) {
    if (error instanceof SyntaxError) {
      display.textContent = 'Invalid expression. Please check your input.';
    } else if (error instanceof MathError) {
      if (error.message === 'Division by zero') {
        display.textContent = 'Cannot divide by zero.';
      } else {
        display.textContent = 'Calculation error. Please try again.';
      }
    } else {
      display.textContent = 'An unknown error occurred.';
    }
  }
}

このように、ユーザーにとってわかりやすく、問題の解決に役立つようなエラーメッセージを表示するようにするとよいでしょう。

また、エラーが発生した後も、ユーザーが続けて計算できるようにしておくのもポイントです。

例えば、エラーメッセージを表示した後、一定時間経過すると自動的に入力がクリアされるようにするなどの工夫が考えられます。

function calculate() {
  // ...

  try {
    // ...
  } catch (error) {
    // ...

    setTimeout(() => {
      display.textContent = '0';
    }, 2000);
  }
}

このコードでは、エラーメッセージを表示してから2秒後に、ディスプレイの内容を「0」に戻すようにしています。

こうすることで、ユーザーは次の計算をスムーズに始められます。

例えば、「(1 + 2」と入力すると、「Invalid expression. Please check your input.」と表示され、2秒後にディスプレイが「0」に戻ります。

また、「1 / 0」と入力すると、「Cannot divide by zero.」と表示され、同様に2秒後にクリアされます。

●関数電卓作成のベストプラクティス

ここまでで関数電卓の基本的な機能は実装できましたが、もっと良いコードを書くためのコツがあります。

せっかくJavaScriptの学習として関数電卓を作るのですから、ベストプラクティスも押さえておきたいところです。

コードの可読性を上げる工夫や、パフォーマンスを意識した実装、テストの自動化など、プロのエンジニアが実践しているテクニックを関数電卓に取り入れてみましょう。

○コードの可読性を上げるコツ

まず大切なのは、コードの可読性です。

可読性の高いコードは、バグを減らし、メンテナンスを容易にします。

関数電卓のコードを見直して、より読みやすくする工夫を考えてみましょう。

例えば、変数や関数の命名を改善するのも一つの方法です。

適切な名前を付けることで、コードの意図が明確になります。また、コメントを適所に入れるのも効果的です。

コードを読む人が理解しやすいよう、処理の内容を簡潔に説明しておくとよいでしょう。

さらに、関数を細かく分割するのもおすすめです。1つの関数が多くのことをやりすぎていると、読みにくくなります。

関数を小さな単位に分け、それぞれの関数の役割を明確にしておくと、コードが見通しよくなります。

○パフォーマンスを意識した実装

次に、パフォーマンスも意識してみましょう。

特に、関数電卓のような計算を行うアプリケーションでは、効率的な実装が求められます。

例えば、先ほどの逆ポーランド記法の計算では、スタックを使って効率的に処理を行っています。

このように、適切なデータ構造を選ぶことで、無駄な計算を省くことができます。

また、不要な処理をできるだけ減らすのも大切です。

ループの中で重い処理を行うと、パフォーマンスが低下する可能性があります。

計算量を意識して、アルゴリズムを工夫するのも良い練習になるでしょう。

○テストの自動化について

さらに、テストの自動化も検討してみましょう。

ここまで関数電卓を作ってきて、動作確認は手動で行ってきたと思います。

しかし、コードが複雑になってくると、手動でのテストが大変になってきます。

そこで、テストを自動化する仕組みを導入すると便利です。

JavaScriptでは、Jestなどの優れたテストフレームワークがあります。

単体テストを書いておけば、コードを修正した後も簡単に動作確認ができるようになります。

関数電卓のような小さなアプリケーションでも、テストを書く習慣を付けておくと、将来の開発で役立つはずです。ぜひチャレンジしてみてください。

○サンプルコード6:リファクタリング後のコード

それでは、コードの可読性を上げ、パフォーマンスを改善し、テストを追加した関数電卓のサンプルコードを見てみましょう。

/**
 * 数式をトークンに分割する
 * @param {string} expression 数式の文字列
 * @return {string[]} トークンの配列
 */
function tokenize(expression) {
  return expression.split(/\s+/);
}

/**
 * トークンを逆ポーランド記法に変換する
 * @param {string[]} tokens トークンの配列
 * @return {(string|number)[]} 逆ポーランド記法のトークンの配列
 * @throws {SyntaxError} 構文エラーが発生した場合
 */
function toRPN(tokens) {
  const operators = [];
  const output = [];

  for (const token of tokens) {
    if (isNumber(token)) {
      output.push(parseFloat(token));
    } else if (isOperator(token)) {
      while (shouldUnwindOperatorStack(token, operators)) {
        output.push(operators.pop());
      }
      operators.push(token);
    } else if (token === '(') {
      operators.push(token);
    } else if (token === ')') {
      while (operators[operators.length - 1] !== '(') {
        output.push(operators.pop());
      }
      operators.pop();
    } else {
      throw new SyntaxError(`Invalid token: ${token}`);
    }
  }

  while (operators.length > 0) {
    if (operators[operators.length - 1] === '(') {
      throw new SyntaxError('Mismatched parentheses');
    }
    output.push(operators.pop());
  }

  return output;
}

/**
 * 文字列が数値かどうかを判定する
 * @param {string} token 文字列
 * @return {boolean} 数値ならtrue、そうでないならfalse
 */
function isNumber(token) {
  return /^-?\d+(\.\d+)?$/.test(token);
}

/**
 * 文字列が演算子かどうかを判定する
 * @param {string} token 文字列
 * @return {boolean} 演算子ならtrue、そうでないならfalse
 */
function isOperator(token) {
  return ['+', '-', '*', '/', '^'].includes(token);
}

/**
 * 演算子のスタックをどこまで巻き戻すかを判定する
 * @param {string} operator 演算子
 * @param {string[]} stack 演算子のスタック
 * @return {boolean} 巻き戻す必要があればtrue、そうでないならfalse
 */
function shouldUnwindOperatorStack(operator, stack) {
  if (stack.length === 0) return false;
  const top = stack[stack.length - 1];
  if (top === '(') return false;
  return getPrecedence(operator) <= getPrecedence(top);
}

/**
 * 演算子の優先順位を取得する
 * @param {string} operator 演算子
 * @return {number} 優先順位
 */
function getPrecedence(operator) {
  switch (operator) {
    case '+':
    case '-':
      return 1;
    case '*':
    case '/':
      return 2;
    case '^':
      return 3;
    default:
      return 0;
  }
}

/**
 * 逆ポーランド記法で表された式を計算する
 * @param {(string|number)[]} tokens 逆ポーランド記法のトークンの配列
 * @return {number} 計算結果
 * @throws {MathError} 計算エラーが発生した場合
 */
function evalRPN(tokens) {
  const stack = [];

  for (const token of tokens) {
    if (typeof token === 'number') {
      stack.push(token);
    } else {
      const b = stack.pop();
      const a = stack.pop();
      switch (token) {
        case '+':
          stack.push(a + b);
          break;
        case '-':
          stack.push(a - b);
          break;
        case '*':
          stack.push(a * b);
          break;
        case '/':
          if (b === 0) {
            throw new MathError('Division by zero');
          }
          stack.push(a / b);
          break;
        case '^':
          stack.push(Math.pow(a, b));
          break;
      }
    }
  }

  if (stack.length !== 1) {
    throw new MathError('Invalid expression');
  }

  return stack.pop();
}

/**
 * 数式を計算する
 * @param {string} expression 数式の文字列
 * @return {number} 計算結果
 */
export function evaluate(expression) {
  const tokens = tokenize(expression);
  const rpn = toRPN(tokens);
  return evalRPN(rpn);
}

class MathError extends Error {
  constructor(message) {
    super(message);
    this.name = 'MathError';
  }
}

class SyntaxError extends Error {
  constructor(message) {
    super(message);
    this.name = 'SyntaxError';
  }
}

// テストコード
describe('evaluate', () => {
  test('adds numbers', () => {
    expect(evaluate('1 + 2')).toBe(3);
  });

  test('subtracts numbers', () => {
    expect(evaluate('7 - 3')).toBe(4);
  });

  test('multiplies numbers', () => {
    expect(evaluate('3 * 4')).toBe(12);
  });

  test('divides numbers', () => {
    expect(evaluate('8 / 2')).toBe(4);
  });

  test('handles exponentiation', () => {
    expect(evaluate('2 ^ 3')).toBe(8);
  });

  test('follows order of operations', () => {
    expect(evaluate('2 + 3 * 4')).toBe(14);
    expect(evaluate('(2 + 3) * 4')).toBe(20);
  });

  test('throws SyntaxError on invalid expression', () => {
    expect(() => evaluate('2 +')).toThrow(SyntaxError);
  });

  test('throws MathError on division by zero', () => {
    expect(() => evaluate('1 / 0')).toThrow(MathError);
  });
});

テストコードを実行すると、次のような結果が表示されるはずです。

 PASS  ./calculator.test.js
  evaluate
    ✓ adds numbers (2ms)
    ✓ subtracts numbers
    ✓ multiplies numbers (1ms)
    ✓ divides numbers
    ✓ handles exponentiation
    ✓ follows order of operations (1ms)
    ✓ throws SyntaxError on invalid expression
    ✓ throws MathError on division by zero (7ms)

Test Suites: 1 passed, 1 total
Tests:       8 passed, 8 total
Snapshots:   0 total
Time:        1.28s

このサンプルコードでは、まず関数の役割を明確にするためにコメントを追加しています。

また、エラーをより具体的に特定できるように、エラーメッセージを改善しています。

さらに、テストコードを追加しています。Jestを使って、evaluate関数の振る舞いを確認しています。

基本的な四則演算や括弧を使った式、エラーケースなどをテストしています。

まとめ

JavaScriptで関数電卓を作ることは、プログラミングの基礎力を高め、実践的なスキルを身につけるのに最適な課題です。

HTMLとCSSを使ったUIの作成、逆ポーランド記法による数式処理、エラーハンドリングなど、ウェブアプリ開発に必要な要素が詰まっています。

関数電卓を一から作り上げる過程で得られる知識と経験は、きっとあなたのエンジニアとしての成長に役立つはずです。