JavaScriptで電卓を作る1
こんにちは。tushiko23です。
今回はJavaScriptで電卓を作ります。いつも、買い物や家計簿つけるときなにげにスマホを取り出して電卓アプリを開いているのですが、こんな感じでプログラミングされてるんだなとかなり勉強になりました。
特に、エラー制御の部分。演算子(+x)が連続して押されないようにしたり、初期値で小数点が入力されないように制御したりと実装にかなり時間を要しました。
Chat GPTで実装やエラーをなんとか相談・壁打ちしながらなんとか実装することができました。
この記事を読めば、関数の分割や実装の仕様も言語化して整理する力もかなり力がつくと思いますので、1つずつ見ていきましょう!よろしくお願いします!
必要な機能
- 0~9の数字を押すとディスプレイに反映される
- 数字・演算子(+-x÷)・小数点を入力すると文字列が連結できる
- =(イコール)を押すと計算結果をディスプレイに返す
- リセットを押すと、ディスプレイに0を表示する
ざっくりこんな感じでしょうか。「ボタンを押すと、入力が反映される」シンプルな機能ですが、これがなかなか難しいんです。なので、実装の前に今回は(ざっくりとしたものですが)文章に落とし込んでから実装することにします。
実装の手順
① 電卓画面をhtml・cssで作成。
ここから、 javascript。
② 指定しているボタンを押したら、displayに反映されるようにする。
③ ACを押したら、表示されている値がリセットされ、0を表示する。
④ 入力制御1
- 小数点の入力
- 0の入力
- 00の入力
- 演算子(+ x ÷)の入力
⑤ 入力制御2 - ー(マイナス)を符号のマイナスで入力できる、演算子の引くとして入力できるようにする
- / をディスプレイには÷ 内部では/として受け取る処理をする。
- をディスプレイにはx 内部ではとして受け取る処理をする。
⑥ 計算処理
- ディスプレイに連結表示したものを計算のために要素ごとにばらして配列にする処理
- -を符号として、数値にくっつける処理 -を演算子として配列の要素に入れる
- 演算子 * / を優先して計算する → その後配列にする
- +ーを計算した処理を返す処理
完成したコードの解説
index.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="main.css">
<title>Calculator</title>
</head>
<body>
<div class="wrapper">
<div>
<input class="display" readonly type="text" value="0">
</div>
<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>
</div>
<script src="main.js"></script>
</body>
</html>
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つずつ解説していきますね。
まずは、基本機能から。
(1) 数字・小数点・演算子の入力処理
<!-- 電卓の表示部分 -->
<div>
<input class="display" readonly type="text" value="0">
</div>
<!-- 数字 -->
<button class="button" onclick="inputNumber('7')">7</button>
<!-- 小数点 -->
<button class="button" onclick="inputDot('.')">.</button>
<!-- 演算子 -->
<button class="button" onclick="inputOperator('+')">+</button>
display部分はinputタグで実装します。打ち込みができないようにreadonlyを設けています。最初の何も入力されていない時は0を表示しています。
buttonタグにはonclick属性を用いて、関数を実行して値(value)を取得します。
main.js
// display要素の取得
const display = document.querySelector(".display");
// 数字の入力
function inputNumber(num) {
display.value += num;
};
// 関数:小数点の入力
function inputDot () {
display.value += ".";
};
// 関数:演算子の入力
function inputOperator (op) {
display.value += op;
querySelectorでdisplayクラスの要素を取得し、.valueメソッドで、タグに定義されているvalue値を出力しています。0 00 1~9の数字はタグ上で引数にnumを与えることにより、対応し、+=で入力された値に連結するようにしています。小数点、演算子も同様に定義します。
例: 1 → 2を入力 → 12 1 → .(小数点) → 1. → 2 →1.2 3 → + → 3+ 2 → 3+2
(2) ACを押すと、displayの値が0に戻る処理
<!-- ACでクリア -->
<button class="button" onclick="reset()">AC</button>
function reset () {
display.value = "0";
}
ACボタンを押すと、値がクリアされreset関数により、displayには0を返します。
(3)入力の制御をする
数字と小数点と演算子は入力できたと思います。次は、入力の制御をやっていきたいと思います。今の状態だと、ボタンを押されると押されたボタンの数字や演算子がdisplay上に連結されるという処理のみなので、一般的な電卓ではあり得ない挙動が起きています。
例えば、0→1を入力→01→2を入力→02 0 →小数点を入力 0. → 演算子-を入力→ 0.- といった具合です。
これらを制御していきます。制御する内容は複数あるので、まず箇条書きにしてどうしたいのかを実装します。
数字
- 0の時、00の入力ができないようにする。
- 0の時、0の入力ができないようにする。
- 数字+演算子の時、
4+ 4*などのとき00が入力されないようにする。 - 0の時、数字を入力すると
01とかではなく入力した数字が上書きされるようにする。
以下のように実装します。
// 0の時、数字を入力すると`01`とかではなく入力した数字が上書きされるようにする処理
// 0の時かつ、数字ボタンが押された時に00でなければ、引数の数字が表示される
if (display.value === "0" && num !== "00") {
display.value = num;
return;
}
display.value += num; だと押された数字が連結されますが、display.value = num; でdisplay.valueの値に代入してから、display.value += num; の処理は走らせることで0の時に01という挙動を1にすることができます。
// 0の時、00の入力ができないようにする。
// 押された値が00、displayが0の処理(&&でかけるが、数字+演算子の処理も書きたいのでこのように書く)
if (num === "00") {
if (display.value === "0") {
return;
}
}
// 3+ 3-といった数字+演算子の時に00が入力できないようにする
if (num == "00") {
const expr = display.value;
const lastNumber = expr.split(/[+\-×÷]/).pop();
if (lastNumber === "" || display.value === "0") {
return;
} else {
display.value += "00";
return;
}
00を演算子の後で入力されないように、splitメソッドで引数に正規表現で演算子を指定します。.split(/[+\-×÷]/) の /[+\-×÷]/ の部分は「+ - × ÷ のどれか1文字にマッチする正規表現」です。演算子のところで途切れ配列として値が返されます。
例: 3+ → 00 が入力 expr → 3+ が入り、expr.split(/[+\-×÷]/)によって、lastNumberが['3','']という配列になります。これを.pop()メソッドで最後の配列を削除した配列を返します。expr.split(/[+\-×÷]/).pop()で配列の取り除いた値の最後の要素が返り値となります。
なのでこの場合、配列の最後が['']なので、最終的なlastNumberは空となります。
lastNumber === "" を条件分岐に入れることで00が押されないように制御します。
それ以外、数字なら300 1.00など連結させたいので、else文に連結させます。
なお、この地点では、x÷が*/として判定されているので.splitメソッドの正規表現に引っかからず 3x 3÷ になり、expr.split(/[+\-×÷]/)で返す配列が [3x] [3÷]になります。.popメソッドを実行した時配列の最後が[3x] [3÷]となるので、今回のif文の処理が実行されず、else文に処理がいき、3*00 3÷00といった値が出力されるわけです。
これはこの後、演算子の入力制御で修正します。
小数点
-
0.など入力する前が小数点のとき、0..のように連続で入力できないようにする。 -
1+ 2-といった演算子の後に.を入力された時に入力できないようにする。
まずは、小数点が連続で入力されないようにする処理ですが、以下のように実装しています。
手前に小数点(.)があれば、連結されないようにする。
// 手前に小数点があれば、連結なしにします。
if (lastNumber.includes(".")) return;
1+ 2-といった演算子の後に.を入力された時に入力できないようにする処理は、数字の入力同様、.split,.popメソッドを使って返り値が空であることを判定して入力を制御します。
const expr = display.value;
const lastNumber = expr.split(/[+\-×÷]/).pop();
// ここで空なら連結されないようにします。
if (lastNumber === "" || lastNumber.includes(".")) return;
次回は演算子処理について触れていきます。マイナスがあるかないかで判定する必要があったので、こちらについても詳しく解説していきたいと思います!
