ダッシュで奪取

レガシーSIer → webエンジニア(見習い) 新しく知ったことのメモを書いています。

【JavaScript】計算機で計算ができるようにした


前回は数値を入力できるようにしたので、四則演算ができるようにしました。
HTML、CSSは以前から変更していません。
サンプルページ

目次

  1. やりたいこと
  2. 全体像
  3. 共通部分
  4. ボタン振り分け処理
  5. ACボタン等の処理
  6. ACボタンの処理
  7. イコールボタンの処理
  8. 演算子ボタンの処理
  9. 計算処理
  10. 数字ボタンの処理
  11. 桁数チェック
  12. 計算結果を画面に表示する
  13. ひとこと

1.やりたいこと

  • 数値が入力できるようになったので、計算ができるようにする
  • 小数の計算は考慮しない。
  • 表示可能な桁数をオーバーしたら、何もせずそのままにする(エラー表示したり、e表示はしない)

2.全体像

// 画面の値を取得するための変数
var target = document.getElementById("display_text");
var displayNum = 0;

var beforeNum = ""; // 1つ前に入力された数値
var afterNum = ""; // 入力された数値
var beforeKey = ""; // 直前に押されたボタン
var calc = ""; // 押されている演算子

// ボタン振り分け処理
function pushBtnFnc(key) {
  if (!isNaN(key)) {
    numberBtnFnc(key);
  } else {
    actionBtnFnc(key);
  }
  // ボタンごとの処理が終わったら、直前に押されたボタンを上書きする
  beforeKey = key;
}

// ACボタン等の処理
function actionBtnFnc(key) {
  switch (key) {
    case "ac":
      acBtnFnc(); // ACボタンの処理
      break;
    case "equ":
      equBtnFnc(); // イコールボタンの処理
      break;
    default:
      symbolBtnFnc(key); // 演算子ボタンの処理
      break;
  }
}

// ACボタンの処理
function acBtnFnc() {
  // 画面表示を0にする(クリア)
  displayNum = "0";
  beforeNum = "";
  afterNum = "";
  beforeKey = "";
  calc = "";
  setCalc(0);
}

// イコールボタンの処理
function equBtnFnc() {
  // 計算値が両方埋まっている場合のみ、演算子に従って処理を行う
  if (beforeNum && afterNum) {
    calcFnc(calc);
  }
}

// 演算子ボタンの処理
function symbolBtnFnc(key) {
  // 直前に数値が押されていたら、前に押されていた演算子で計算処理を行う
  if (!isNaN(beforeKey) && beforeNum && afterNum) {
    calcFnc(calc);
  }
  // 押された演算子を上書きする
  calc = key;
}

// 計算処理
function calcFnc(calc) {
  switch (calc) {
    case "add":
      displayNum = Number(beforeNum) + Number(afterNum); // + 加算
      break;
    case "sub":
      displayNum = Number(beforeNum) - Number(afterNum); // − 減算
      break;
    case "mul":
      displayNum = Number(beforeNum) * Number(afterNum); // × 乗算
      break;
    case "div":
      displayNum = Number(beforeNum) / Number(afterNum); // ÷ 除算
      break;
  }
  setCalc(displayNum);
}

// 数字ボタンの処理
function numberBtnFnc(num) {
  // 12桁目以上の入力は受け付けない(ここで終了)
  if (!chkLength(displayNum) && !isNaN(beforeKey)) {
    return;
  }

  // 0しか入力されていなかった場合、入力値で置き換える
  if (displayNum == "0") {
    displayNum = num;

    // 入力されていた値を記憶
    beforeNum = afterNum;
  } else if (!isNaN(beforeKey) || beforeKey == "equ") {
    // 1つ前が数字またはイコールだったら、画面に表示されている値と入力値を連結
    displayNum += String(num);
  } else {
    // 1つ前が記号だったら、画面の値を入力値で置き換える
    displayNum = num;

    // 入力されていた値を記憶
    beforeNum = afterNum;
  }
  setCalc(displayNum);
}

// 桁数チェック
function chkLength(num) {
  // 11桁以上であればfalseを返す
  if (String(num).length >= 11) {
    return false;
  } else {
    return true;
  }
}

// 計算結果を画面に表示する
function setCalc(displayNum) {
  // 12桁目以上の入力は受け付けない(ここで終了)
  if (!chkLength(displayNum)) {
    return;
  }
  target.innerText = Number(displayNum).toLocaleString();

  // 画面に表示した値を、入力された数値とする
  afterNum = displayNum;
}

な、長い…
ネット上探してみると色々な解法が出てきますが、なるべく見ないで自分で書くようにしました。

3.共通部分

var target = document.getElementById("display_text");
var displayNum = 0;

var beforeNum = ""; // 1つ前に入力された数値
var afterNum = ""; // 入力された数値
var beforeKey = ""; // 直前に押されたボタン
var calc = ""; // 押されている演算子

前回からの変更点として、displayNumに初期値の0を入れるようにしました。
後でこの変数の中身を参照したり、条件判定に使用したり色々やっているのですが、その際に空文字だと意図した動きをしてくれない部分があったので…

以降の処理の流れとしては、beforeNumafterNumを、calcに入っている演算子(+とか−とか)で計算させて、画面に表示するイメージです。

4.ボタン振り分け処理

function pushBtnFnc(key) {
  if (!isNaN(key)) {
    numberBtnFnc(key);
  } else {
    actionBtnFnc(key);
  }
  // ボタンごとの処理が終わったら、直前に押されたボタンを上書きする
  beforeKey = key;
}

どのボタンを押しても、まずこの関数に飛びます。

数値(1)→演算子(+)→数値(2)と入力した場合と、数値(1)→数値(2)→演算子(+)と入力した場合とで挙動が違うため、直前に押されたボタンが何かを取っておく必要があります(前者は1+2、後者は12+となります)。

5.ACボタン等の処理

function actionBtnFnc(key) {
  switch (key) {
    case "ac":
      acBtnFnc(); // ACボタンの処理
      break;
    case "equ":
      equBtnFnc(); // イコールボタンの処理
      break;
    default:
      symbolBtnFnc(key); // 演算子ボタンの処理
      break;
  }
}

前回は普通にif文を書いていましたが、多分岐になったのでswitch文で書き直しました。
この場合、keyの値がacならACボタンの処理、equならイコールボタンの処理に入るようにしています。

break;までの処理を行うため、記載を忘れると全部の処理が実行されてしまうので注意…

6.ACボタンの処理

function acBtnFnc() {
  // 画面表示を0にする(クリア)
  displayNum = "0";
  beforeNum = "";
  afterNum = "";
  beforeKey = "";
  calc = "";
  setCalc(0);
}

各変数に計算したい値をどんどん溜めていくため、AC(オールクリア)ボタンが押されたら全ての変数を初期化しています。 初期化しないと、次に別のボタンを押した時に、残っていた以前の値で計算が行われてしまう可能性があるためです…

7.イコールボタンの処理

function equBtnFnc() {
  // 計算値が両方埋まっている場合のみ、演算子に従って処理を行う
  if (beforeNum && afterNum) {
    calcFnc(calc);
  }
}

四則演算のボタンを押す前の数値をbeforeNum、後の数値をafterNumに入れる予定です。

両方の数値が埋まっている = beforeNumafterNumcalcの全ての値が揃っている状態になるので、メインの計算処理へ飛ばしています。

演算子ボタンを押していない状態でイコールボタンを操作されてしまうことがあるかもしれないので、このような書き方をしています。

8.演算子ボタンの処理

function symbolBtnFnc(key) {
  // 直前に数値が押されていたら、前に押されていた演算子で計算処理を行う
  if (!isNaN(beforeKey) && beforeNum && afterNum) {
    calcFnc(calc);
  }
  // 押された演算子を上書きする
  calc = key;
}

イコールボタンを押さずに連続で計算されることがあるかもしれないので(1+2+3+…と押された等)、その場合直前に押されていた演算子で計算をしておきたいです。

1 → + → 2 → −と来た場合(今は−を押してこの処理にたどり着いた状態です)、−ボタンを押した時点で、1+2の計算が行われるようにします。
上記の場合、まずcalcFncで+ボタンの計算処理をしてから、−ボタンが押されたということをcalcの中に取っておきます。

次に数値→何らかの演算子の順で押されてここに来たら、今度は−ボタンの処理が行われるようになります。

beforeNumafterNumの中身もチェックしている理由ですが、初回(1+と押した場合等)もこの処理に来てしまうため、無駄な処理を行わないようにしています。計算に使う変数の中身が空の場合計算が行われないようなので、変数の中身チェック部分は無くても同じように動作します…

9.計算処理

function calcFnc(calc) {
  switch (calc) {
    case "add":
      displayNum = Number(beforeNum) + Number(afterNum); // + 加算
      break;
    case "sub":
      displayNum = Number(beforeNum) - Number(afterNum); // − 減算
      break;
    case "mul":
      displayNum = Number(beforeNum) * Number(afterNum); // × 乗算
      break;
    case "div":
      displayNum = Number(beforeNum) / Number(afterNum); // ÷ 除算
      break;
  }
  setCalc(displayNum);
}

メインの部分です!簡素です。 文字列として連結されてしまうことがないように、数値変換してから計算しています。

今更ですが、減算は「subtraction」、乗算は「multiplication」、除算は「division」と書くみたいなので、それぞれの頭3文字を演算子名として使用しています。

10.数字ボタンの処理

function numberBtnFnc(num) {
  // 11桁以上の入力は受け付けない(ここで終了)
  if (!chkLength(displayNum) && !isNaN(beforeKey)) {
    return;
  }

  // 0しか入力されていなかった場合、入力値で置き換える
  if (displayNum == "0") {
    displayNum = num;

    // 入力されていた値を記憶
    beforeNum = afterNum;
  } else if (!isNaN(beforeKey) || beforeKey == "equ") {
    // 1つ前が数字またはイコールだったら、画面に表示されている値と入力値を連結
    displayNum += String(num);
  } else {
    // 1つ前が記号だったら、画面の値を入力値で置き換える
    displayNum = num;

    // 入力されていた値を記憶
    beforeNum = afterNum;
  }
  setCalc(displayNum);
}

11桁以上の入力は受け付けない(ここで終了)

chkLengthは次に記載していますが、引数として渡された値の桁数がオーバーする場合にfalseを返しています。 表示可能桁数(10桁)をオーバーした場合、かつ、直前に数値が押された場合は何もせず終了としています。

1234567890と入力されている状態で1が押されたら何もせず終了、については前回もこのような書き方をしていたため、問題はなさそうです。

ただ、今回は演算子ボタンも考慮して、直前に押されたボタンが何かを条件に加える必要があります。
例えば、1234567890の後にを押して、次に1を押された場合は「直前に数値以外が押されている」ので桁数オーバーとならず、続行します(1234567890+1になります)。

0しか入力されていなかった場合、入力値で置き換える

0だけが入力されていた場合、今押したボタンの数値を初期値としたいため、この時点でbeforeNumに退避しています。

1つ前が数字またはイコールだったら、画面に表示されている値と入力値を連結

数値を連続で入力した場合、数値を連結して表示します。 また、1 → = → 1の順で入力された場合、これも連結して11としています。 実際の電卓とは挙動が違いますが、今回は取りあえずこのまま進めます…

1つ前が記号だったら、画面の値を入力値で置き換える

演算子 → 数値の順で入力された場合、今押された数値ボタンの値を画面に表示しておきたいです。

例えば今123と表示されていて、+4の順に押されたら、画面には4が表示されます。

11.桁数チェック

function chkLength(num) {
  // 11桁以上であればfalseを返す
  if (String(num).length >= 11) {
    return false;
  } else {
    return true;
  }
}

先に書いてしまいましたが、11桁以上であればfalse、そうでなければtrueを返しています。
桁数は、文字列でないと判定されないため一旦変換してからチェックしています。

前回は11桁まで入力可能にしていましたが、今回減算処理を入れた結果符号付きのマイナス値も登場するようになったので、表示可能桁数を10桁までに減らしました。

12.計算結果を画面に表示する

function setCalc(displayNum) {
  // 11桁以上の入力は受け付けない(ここで終了)
  if (!chkLength(displayNum)) {
    return;
  }
  target.innerText = Number(displayNum).toLocaleString();

  // 画面に表示した値を、入力された数値とする
  afterNum = displayNum;
}

9999999999 × 2のように、計算結果が表示桁数をオーバーする可能性があるので、表示する前にもう一度桁数チェックを行っています。

13.ひとこと

今の自分が考えられる範囲での考慮はしたつもりですが、まだ何か抜けているかも…ちょっと不安です。

コピペして眺めるのも良いですけど、1から考えて自分の言葉(コード?)で書いてみるのも結構楽しかったです。後でネット上のソースと比較して答え合わせします…

まだ押せないボタンがいくつかあるので、次はその中身を作っていきます。