JavaScriptで電卓を作る① 🧮
(入力制御編:0 / 00 / 小数点 / 演算子)
こんにちは、tushiko23です 👋
今回は JavaScriptで電卓を作ります。
普段、買い物や家計簿で何気なく電卓アプリを開いていますが、「これって中でどう動いてるんだろう?」と気になって作ってみました。
特に大変だったのは 入力制御(エラー制御) の部分です。
-
+ や × を連続で押してもおかしくならないようにする
-
小数点が .. みたいに連続して入らないようにする
-
00 が不自然に入らないようにする
など、「普通の電卓なら当たり前」 を実装するのが意外と難しくてかなり勉強になりました🔥
この記事を読めば、
✅ 仕様を文章化して整理する力
✅ 関数分割して実装する力
がつくと思うので、1つずつ見ていきましょう!
必要な機能 ✅
- 0〜9 の数字を押すとディスプレイに反映される
- 数字・演算子(+ - × ÷)・小数点を入力すると文字列として連結できる
- = を押すと計算結果をディスプレイに返す
- AC を押すとディスプレイが 0 に戻る
「ボタンを押したら反映されるだけじゃん」と思うかもですが、
入力制御を入れると一気に難しくなります 😂
実装の手順 🛠️
① 電卓画面を HTML / CSS で作成
② ボタンを押したら display に反映
③ AC を押したら 0 に戻す
④ 入力制御①(小数点 / 0 / 00 / 演算子の基礎)
⑤ 入力制御②(- の符号/演算子判定、表示×÷と内部*/の変換)
⑥ 計算処理(配列化 → * / 優先 → + - 計算)
今回は ①〜⑤の途中(入力制御中心) を解説します!
完成コード(折りたたみ)📦
index.html
Calculator<div class="buttons">
<button class="button" onclick="inputNumber('7')">7</button>
<button class="button" onclick="inputNumber('8')">8</button>
<button class="button" onclick="inputNumber('9')">9</button>
<button class="button" onclick="inputOperator('+')">+</button>
</div>
<div class="buttons">
<button class="button" onclick="inputNumber('4')">4</button>
<button class="button" onclick="inputNumber('5')">5</button>
<button class="button" onclick="inputNumber('6')">6</button>
<button class="button" onclick="inputOperator('-')">-</button>
</div>
<div class="buttons">
<button class="button" onclick="inputNumber('1')">1</button>
<button class="button" onclick="inputNumber('2')">2</button>
<button class="button" onclick="inputNumber('3')">3</button>
<button class="button" onclick="inputOperator('*')">×</button>
</div>
<div class="buttons">
<button class="button" onclick="inputNumber('0')">0</button>
<button class="button" onclick="inputNumber('00')">00</button>
<button class="button" onclick="inputDot('.')">.</button>
<button class="button" onclick="inputOperator('/')">÷</button>
</div>
<div class="buttons">
<button class="button" onclick="reset()">AC</button>
<button class="button" onclick="equal()">=</button>
</div>
main.css
.wrapper { margin: 1.5rem; }.display {
box-sizing: border-box;
background-color: black;
font-size: 2rem;
line-height: 2.5rem;
color: white;
text-align: right;
border: none;
padding: 5px;
width: 35%;
}
.buttons {
display: flex;
width: 35%;
}
.button {
flex: 4;
font-size: 1.5rem;
padding: .25rem 0;
margin: .2rem;
border-radius: .25rem;
}
main.js
const display = document.querySelector(".display");
// 数字の入力
function inputNumber(num) {
const value = display.value;
if (value === "0では除算できません") {
if (num === "0" || num === "00") {
display.value = "0";
return;
}
display.value = num;
return;
}
if (display.value === "0" && num !== "00") {
display.value = num;
return;
}
if (num == "00") {
const expr = display.value;
const lastNumber = expr.split(/[+\-×÷]/).pop();
if (lastNumber === "" || display.value === "0") {
return;
} else {
display.value += "00";
return;
}
}
display.value += num;
};
// 小数点の入力
function inputDot() {
const value = display.value;
if (value === "0では除算できません") {
display.value = '0';
return;
}
const expr = display.value;
const lastNumber = expr.split(/[+\-×÷]/).pop();
if (expr === "") return;
if (lastNumber === "" || lastNumber.includes(".")) return;
display.value += ".";
};
// 演算子の入力
function inputOperator(op) {
const value = display.value;
const lastRaw = value.slice(-1);
if (value === "0では除算できません") {
display.value = "0";
return;
}
// 画面表示のx÷を引数で受け取った時にそれを*/で返して内部側で処理
const toInternal = (c) => {
if (c === "×") return "*";
if (c === "÷") return "/";
return c;
}
const last = toInternal(lastRaw);
// 内部表示の*/を引数で受け取った時にそれをx÷で返して外部側で処理
const toDisplay = (c) => {
if (c === "*") return "×";
if (c === "/") return "÷";
return c;
}
const opForDisplay = toDisplay(op);
if (display.value === "0" && op === "-") {
display.value = "-";
return;
}
if ("+-*/".includes(last)) {
// 押したのが-で直前が-でない時-を符号として許可する、
if (op === "-" && last !== "-") {
display.value += "-";
return;
}
// 演算子が連続で押された時に全部を捨てて1つにする処理
// 6 + - x → 6x
let trimmed = value;
while ("+-*/".includes(toInternal(trimmed.slice(-1)))) {
trimmed = trimmed.slice(0, -1);
}
display.value = trimmed + opForDisplay;
return;
};
display.value += opForDisplay;
};
// -(マイナス)の処理 (符号か演算子を判定)
function mergeUnaryMinus(tokens) {
const result = [];
for (let i = 0; i < tokens.length; i++) {
const t = tokens[i];
const prev = result[result.length - 1];
const next = tokens[i + 1];
if (
t === "-" &&
next !== undefined &&
!isNaN(Number(next)) &&
(i === 0 || ["+", "-", "*", "/"].includes(prev))
) {
result.push("-" + next);
i++;
} else {
result.push(t);
}
}
return result;
};
// 演算子*/を優先して計算する処理
function applyMulDiv(tokens) {
const result = [];
let i = 0;
while (i < tokens.length) {
const t = tokens[i];
if (t === "*" || t === "/"){
const prev = Number(result.pop());
const next = Number(tokens[i + 1]);
let calc;
if (t === "*" ) {
calc = prev * next;
} else {
if (next === 0) {
display.value = "0では除算できません";
return null;
}
calc = prev / next;
};
result.push(String(calc));
i += 2;
} else {
result.push(t);
i++;
};
}
return result;
};
// =(イコール)押下時の処理
function equal () {
const exprDisplay = display.value;
const exprForCalc = exprDisplay.replace(/×/g, "*").replace(/÷/g, "/");
let tokens = exprForCalc.match(/(\d+(?:\.\d+)?|[+\-*/])/g);
if (!tokens) return;
if (exprDisplay === "0では除算できません") {
display.value = "0";
return;
}
if (!exprDisplay) return;
// 末尾が演算子なら、計算せずに何もしない
const lastChar = exprDisplay.slice(-1); // 末尾1文字だけ取り出す
if ("+-*/×÷".includes(lastChar)) {
return;
}
tokens = mergeUnaryMinus(tokens);
tokens = applyMulDiv(tokens);
// mergeUnaryMinusとapplyMulDiv関数実行後のtokens配列の空列を防止
if (!tokens) return;
let result = Number(tokens[0]);
for (let i = 1; i < tokens.length; i += 2) {
const op = tokens[i];
const num = Number(tokens[i+1]);
if (op === "+") {
result = result + num;
} else if (op === "-") {
result = result - num;
}
}
// 電卓で小数点以下9~10桁を四捨五入し値の誤差を防ぐ
result = Math.round(result * 1000000000) / 1000000000;
display.value = result;
};
// Cボタン
function reset() {
display.value = "0";
};
(1) 数字・小数点・演算子を表示に反映する 📥
HTMLではonclickで関数を呼び、値を渡します。
<input class="display" readonly type="text" value="0">
<button onclick="inputNumber('7')">7</button>
<button onclick="inputDot('.')">.</button>
<button onclick="inputOperator('+')">+</button>
displayはreadonlyにして手入力できないようにしています
ボタンを押したときに 関数へ値を渡して表示を更新します
JavaScript側ではこう👇
const display = document.querySelector(".display");
function inputNumber(num) {
display.value += num;
}
function inputDot() {
display.value += ".";
}
function inputOperator(op) {
display.value += op;
}
この時点では「押した値を連結するだけ」です。
✅ 例
1 → 2 で 12
3 → + → 2 で 3+2
(2) ACを押すと0に戻す ♻️
<button onclick="reset()">AC</button>
function reset() {
display.value = "0";
}
これでリセットは完成です 👍
(3) ここからが本番:入力制御を入れる 🚧
このままだと、普通の電卓では起きない挙動が出ます。
-
0 → 1 で 01 になってしまう
-
1.. みたいに小数点が連続入力できてしまう
-
3+00 ができてしまう
なので、「入力できていいケースだけ通す」 制御を入れていきます。
数字の入力制御(0 / 00)🔢
✅ 仕様(こうしたい)
-
0 のとき 1〜9 を押したら 上書き(01にしない)
-
0 のとき 00 を押しても 何もしない
-
3+ の直後など(最後の数が空)のとき 00 を押しても 何もしない
それ以外は普通に連結
✅ 実装:0の上書き(01対策)
if (display.value === "0" && num !== "00") {
display.value = num;
return;
}
+= だと 0 に 1 が足されて 01 になってしまうので、
ここだけは 代入(=)で上書き しています。
✅ 実装:00 が押せるか判定する
if (num == "00") {
const expr = display.value;
const lastNumber = expr.split(/[+\-×÷]/).pop();
if (lastNumber === "" || display.value === "0") {
return;
} else {
display.value += "00";
return;
}
}
ポイントはここ👇
split(/[+\-×÷]/) で 演算子ごとに分割
.pop()で 最後の数字だけ取り出す
それが ""(空)なら、今は 演算子直後なので 00 を禁止
✅ 例
3+ のときは分割結果が ['3', '']になり、最後が ''
→ 00 を入力させない
小数点の入力制御(..対策)🔸
✅ 仕様
- すでにその数に . が入っていたら 2回目は無視
- 演算子の直後(最後の数が空)は 入力させない
✅ 実装
const expr = display.value;
const lastNumber = expr.split(/[+\-×÷]/).pop();
if (expr === "") return;
if (lastNumber === "" || lastNumber.includes(".")) return;
display.value += ".";
これで、
-
1.2.(2個目の小数点) → 入らない
-
3+.(演算子直後の小数点) → 入らない
が実現できます ✅
マイナス以外の演算子(×÷の表示と内部処理)✖️➗
電卓では表示上は × ÷ を出したいけど、
JavaScriptの計算処理では * / を使いたいです。
そこで変換する関数を用意します。
const toInternal = (c) => {
if (c === "×") return "*";
if (c === "÷") return "/";
return c;
};
const toDisplay = (c) => {
if (c === "*") return "×";
if (c === "/") return "÷";
return c;
};
表示は × ÷
内部の判定や計算は * /
という設計にしておくと、後の実装がめちゃくちゃ楽になります 💡
次回予告 🚀
次回はついに、電卓の鬼門である -(マイナス) に入ります!
-
を「符号」として入力できるケース
-
を「引き算」として扱うケース
演算子が連続したときの整理(上書き)
= を押したときの計算(配列化 → 優先順位)
このあたりを 具体例つきで丁寧に 解説します。
ここまで読んでいただきありがとうございました〜!🙌
