JavaScriptで電卓を作ってみたい。
作り方にはいくつか方法があります。
ただし次のチート的手法は使いません。
- eval関数を使った実装
たとえば eval('12-5+(3*4)/6') はコード実行により計算結果 9 を返してくれる。便利だがセキュリティリスクが大きく、将来的に廃止される可能性もあるのでダメ(詳細 : https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/eval)
- ライブラリによる実装
数式を構文解析できるライブラリもあるが、なんかチート的で面白くない。文字通りすべてをゼロから組み立てて作ってみたい(ちなみに構文解析できるライブラリ : https://pisuke-code.com/javascript-make-formula-parser/)
電卓UIの実装から数式の構文解析まで、
JavaScriptで0から電卓を作ることにします。
そういうチャレンジングな内容です。
このページの目次
作成する電卓の仕様について
次のような仕様で電卓を作ります。
- 独自の数式構文解析機構を持つ
- 数字は0~9の整数で少数は扱わない
- 演算子は + - × ÷ の4種類
- 括弧 () による数式記述にも対応
- ついでにAC・DELも実装する
ここでは整数のみを受け付けることにします。
もちろん小数対応することも可能だけど、電卓入門ということで省きました。小数に対応できないようなコード構成ではないので安心してください。
UI・計算機構も含めて全部自力で作ります。
1.電卓UIをグリッドレイアウトで作成
手始めに電卓UIから作っていきましょう。
電卓はボタンが等間隔で規則的に並んでます。
そういうUIにはグリッドレイアウトが最適です。
▼ CSSのグリッドレイアウトとは何か
グリッドは、列と行を定義する水平線と垂直線の集合が交差したものです。要素をグリッド上の行と列の中に配置することができます。 CSS グリッドレイアウトには次のような特徴があります。
引用元 : https://developer.mozilla.org/ja/docs/Web/CSS/CSS_grid_layout/Basic_concepts_of_grid_layout
▼ グリッドレイアウトの基本概念
グリッドの仕様はMDNなどで確認してください。
ここではグリッドレイアウトをメインで使います。
次が電卓の装飾なしのスケルトンUIです。
▼ 次のようなHTML
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<div class="calculator"> <div class="display"></div> <div class="btnac"><span>AC</span></div> <div class="btndel"><span>DEL</span></div> <div class="btnbrkt"><span>()</span></div> <div class="btndiv"><span>÷</span></div> <div class="btn1"><span>1</span></div> <div class="btn2"><span>2</span></div> <div class="btn3"><span>3</span></div> <div class="btnmul"><span>×</span></div> <div class="btn4"><span>4</span></div> <div class="btn5"><span>5</span></div> <div class="btn6"><span>6</span></div> <div class="btnsub"><span>-</span></div> <div class="btn7"><span>7</span></div> <div class="btn8"><span>8</span></div> <div class="btn9"><span>9</span></div> <div class="btnadd"><span>+</span></div> <div class="btn0"><span>0</span></div> <div class="btneq"><span>=</span></div> </div> |
▼ 必要最低限のCSS
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
.calculator{ display: grid; grid-template-columns: repeat(4, 1fr); grid-auto-rows: 80px; width: 350px; } .calculator > *{ display: flex; margin: 8px; background: lightgray; font-size: 20px; } .calculator > * > span{ margin: auto; } .display{ grid-column-start: 1; grid-column-end: 5; grid-row-start: 1; grid-row-end: 2; } .btn0{ grid-column-start: 1; grid-column-end: 4; } |
▼ このような電卓が表示できる
短いコードで電卓UIが作れました。
もし今までのフレックスレイアウトを使ったら、思い通りの配置にするのは難しいです。特にボタン0の横幅など、柔軟なレイアウトができるのがグリッドの強みですね。
最低限のUIはこれだけです。
2.ボタンに装飾やアニメーションを付与
次に大雑把だけど装飾をつけてみます。
角を丸くしたり、押下時のアニメなどです。
▼ 次のようなCSSを追加
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
.display, [class^="btn"]{ background-color: rgba(220, 220, 220, 1); border: 2px solid rgba(170, 170, 170, 1); border-radius: 6px; padding: 16px; } [class^="btn"]{ color: #555; font-weight: bold; text-align: center; text-decoration: none; display: inline-block; cursor: pointer; } [class^="btn"]:hover{ background-color: rgba(200, 200, 200, 1); } [class^="btn"]:active{ background-color: rgba(190, 190, 190, 1); transform: scale(0.95); } |
▼ 最終的な電卓UIの見た目
電卓らしさが一気に増しました。
レイアウト実装は以上で完了です。
3.数式の構文解析機構を実装する
ここからが電卓の根幹にかかわる部分です。
数式を構文解析するパーサーが必要になります。
用いるのは次の手法です。
再帰下降構文解析
難しそうな名前だけど、やることは簡単。再帰関数を使って構文を小さな単位まで分割していき、その末端から値を確定していく手法です。
参考にしたのは次の記事です。
▼ 構文解析 - アルゴリズム講習会
▼ 四則演算での構文解析のイメージ
ここでは四則演算は数字と括弧、+-*/の4つの演算子から成り立っているとします。演算子の優先順位も実際の四則演算の通り、掛け算と割り算が優先されます。ただし、全ての演算は整数だとします。以下は式の一例です。
1+2*6/(10-7)
まずは、四則演算の構文をBNF記法で表します。 BNF記法をあまり厳格に記述する必要はありませんが、演算子の優先順位はきっちり判別できるようにしておく必要があります。
最初に、式全体をexprという変数(非終端記号)で表すとします。 exprの中から計算の優先順位を低い順にterm, factor, numberという記号を対応付けると、次のようなBNF記法を作ることができます。 (exprやtermという名前は適当に決めたものです)
<expr> ::= <term> [ ('+'|'-') <term> ]*
<term> ::= <factor> [ ('*'|'/') <factor> ]*
<factor> ::= <number> | '(' <expr> ')'
<number> :== 1つ以上の数字このBNF表記での[...]*という表記は、括弧内のものが0回以上繰り返されることを意味します。
引用元 : https://dai1741.github.io/maximum-algo-2012/docs/parsing/
ここにC言語での実装方法が載っていました。
それをJavaScript用に書き直します。
再帰下降構文解析を使ったパーサー、
それは以下のようなコードで実装できます。
▼ 数式を解析して値を返すパーサー
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
/// 数式を構文解析するパーサー function formulaParser(formula){ /// 数式を解析して値を評価 return symbol_0(formula,0); /// 数式中の + - の解析 function symbol_0(s, i){ let val; [val,i] = symbol_1(s, i); while(s[i] === '+' || s[i] === '-'){ const ope = s[i]; i++; [val2,i] = symbol_1(s,i); if(ope==='+'){ val += val2; } else{ val -= val2; } } /// 値と式中での位置を返す return [val,i]; } /// 数式中の * / を再帰的に解析 function symbol_1(s, i){ let val; [val,i] = symbol_2(s,i); while(s[i] === '*' || s[i] === '/'){ const ope = s[i]; i++ [val2,i] = symbol_2(s,i); if(ope==='*'){ val *= val2; } else{ val /= val2; } } /// 値と式中での位置を返す return [val,i]; } /// 解析対象が数字なら数字を、 /// 括弧なら再帰的にその結果を返す function symbol_2(s,i){ if(isInteger(s[i])){ return symbol_3(s,i); } ++i; let ret = symbol_0(s,i); ret[1]++; /// 値と式中での位置を返す return ret; } /// 数字を解析して整数として返す function symbol_3(s,i){ let n = s[i++]; while(isInteger(s[i])){ n += s[i++]; } /// 整数として数字を返す return [Number.parseInt(n),i]; } /// ある文字が数字かどうかを判定 function isInteger(ch){ return Number.isInteger(Number.parseInt(ch)); } } |
再帰的に数式を最小パーツに分けて解析してます。
最小単位は数字または括弧で囲まれた数式です。最小単位(数字 or 括弧) ⇒ かけ算割り算 ⇒ 足し算引き算 とさかのぼって解析が行われます。
この手法の利点は複雑な数式をシンプルなロジックで処理でき、括弧などの入れ子構造も容易に解析できる点です。また新しい演算子も再帰関数を追加すれば事足ります
電卓の計算機構が完成しました。
4.電卓でボタン入力を受け取り結果を表示
最後にUIと機能を結びつけます。
ボタン入力を受け取り、計算結果の表示です。
▼ このようなコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
/// 数式および計算結果の表示要素 const $display = document.querySelector('.display'); /// ボタン要素を全て取得 const $btns = document.querySelectorAll('[class^="btn"]'); $btns.forEach(($btn)=>{ $btn.onclick = ()=>{ /// 数式の最後の文字を取得 const lastCh = $display.innerText.at(-1); /// ボタンの識別名を取得 const name = $btn.className.replace('btn',''); if(name==='ac') { $display.innerText = ''; } else if(name==='del') { $display.innerText = $display.innerText.slice(0,-1); } else if(name==='brkt') { /// 括弧の表示 if(lastCh.search(/\+|\-|\*|\//) !== -1){ $display.innerText += '('; }else if(lastCh.search(/[0-9]/)!==-1 || lastCh===')'){ let counter = 0; $display.innerText.split('').forEach((ch)=>{ if(ch==='('){ ++counter; } if(ch===')'){ --counter; } }); if(counter>0){ $display.innerText += ')'; } } } else if(name.search(/add|sub|mul|div/) !== -1 && (lastCh.search(/[0-9]/)!==-1 || lastCh===')') ){ /// 演算子の表示 $display.innerText += { 'add':'+', 'sub':'-', 'mul':'*', 'div':'/' }[name]; } else if(name==='eq'){ /// 計算結果を表示 $display.innerText = formulaParser($display.innerText)[0]; } else if(name.search(/[0-9]/)!==-1){ /// 数字を表示 $display.innerText += name; } } }); |
数式の構文通りに入力できるようにボタン入力を制御します。たとえば演算子は数字または閉じ括弧の前にしか置けなかったり、括弧はかならず演算子前のみ入力可能としたり…
それから = を押したときの挙動についてです。
1 2 3 4 |
} else if(name==='eq'){ /// 計算結果を表示 $display.innerText = formulaParser($display.innerText)[0]; } |
数式解析パーサーに数式を渡し、その結果をデイスプレイに表示してます。簡易的な電卓なので答え表示と数式表示は分離されていません。
各自で工夫してください。
電卓を動かしてみた(Gif動画)
実際に電卓をブラウザで動かしてみます。
▼ 簡単な計算をしてみた
UIから計算機構まで全て自力で作れました。
安易にライブラリを使ったり、セキュリティ度外視でevalを使うこともできたけど、電卓を0から作る楽しさはそこから得られないと思います。
以上、JavaScriptで電卓を作るでした。
質問などはコメント欄に書いてください。ではまた