電卓を作ろう

Javascript

こんにちはkazutoです。今回は、JSで電卓を作っていきましょう。

事前準備

まずは、事前準備を行いましょう。下記のソースコードをコピペをしてください。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script src="main.js"defer></script>
  <link rel="stylesheet" href="style.css">
</head>
<body>
    <main>
      <div class="calculator">
        <div class="display">
          <input type="text" id="clacultorFiled">
        </div>
        <div class="clacultorButtons">
        </div>
      </div>
    </main>
</body>
</html>
body{
    background-color:royalblue;
}
.calcBody {
    background-color: rgba(125,125,125,0.8);
    border-spacing: 2px;
}
.display{
  padding-top: 10px;
}
input[type="text"] {
  margin: auto;
  margin-bottom: 15px;
  display: block;
  margin-top: 10px;
  width: 70%;
  height: 5%;
  text-align: right;
  font-size: 25px;
  background-color: #000000;
  color: #ffffff;
  padding: 10px 20px;
  border: 10px solid #222222;
  border-radius: 30px;
}

main{
    margin: auto;
    border-spacing: 2px;
    border-radius: 30px;
    padding: 0px;
 }

.calculator{
  border: 10px solid;
  width: 380px;
  height: 550px;
  background-color: #333;
  margin: auto;
  border-spacing: 2px;
  border-radius: 30px;
  padding: 0px;
  margin: auto;
  margin-top: 50px;
}
.calculate {
  width: 300px;
  height: 400px;
  background-color: black;
  border: 1px solid;
  margin-left: auto;
  margin-right: auto;

}

button {
  color: white;
  display: block;
  width: 100%;
  height: 100%;
  font-size: 20px;
  background: rgba(0,0,0,0.7);
  background-color:#333333;
  margin: 0 0 0 0;
  padding: 0 0 0 0;
  border: none;

}

td {
  width: 50px;
  padding: 0 0 0 0;
  margin: 0 0 0 0;
}
const buttons = [
  ["(",")","%",'AC'],
  ["7",'8','9','÷'],
  ["4",'5','6','×'],
  ["3",'2','1','-'],
  ["0",'.','=','+'],
]

const table= document.createElement("table")
const tbody = document.createElement('tbody')
table.className = "calculate"
for (let i = 0; i < buttons.length; i++) {
  const tr =document.createElement('tr')
  for (let j = 0; j < buttons.length-1; j++) {
    tr.insertAdjacentHTML('beforeend',`<td ><button class="clacultorButton">${buttons[i][j]}</button></td>`)
  }
  table.appendChild(tr)
}
document.querySelector('.clacultorButtons').appendChild(table)

※必ず全てコピペをしてください。上手く動かない恐れがあります。

関数を定義をしよう

では、電卓を作成していきます。イベントを登録してフロントに動きを付けていく前に関数について解説をしていきます。

  • validate
  • CalculationRymbolReplacement
  • ExtractedFromTheArray
  • SafetyEvail
  • FormulaPatternCheck
  • ArithmeticProcessing

validate

function validate(value) {
  switch (true) {
    case value ==".":
    case value == ")":
    case value == "/":
    case value == "*":
    case value == "=":
    case value == "+":
    case value == "-":
    case value == "×":
    case value == "÷":
    case value == "%":
      return true
    default:
      break;
  }
}

関数validateは、入力制御をする関数です。

switch (true) {
    case value ==".":
    case value == ")":
    case value == "/":
    case value == "*":
    case value == "=":
    case value == "+":
    case value == "-":
    case value == "×":
    case value == "÷":
    case value == "%":
      return true
    default:
      return false
  }
//イメージ
//if(validate(this.value))return

switch文を用いて、条件分岐をしています。入力を制御したい値の場合にtrueを返し、条件に該当しない場合は、falseを返り値として返します。イベント処理に組み込んでいく際は、if文を用いて、関数validateの返り値が、trueな場合、return文を用いて、強制終了し、以降の処理を作動させない様、ロジックを組んでいきます。

以上で、「関数を定義をしよう」の解説を終了します。

CalculationRymbolReplacement

const CalculationRymbolReplacement =(str)=>
   str.split("").map(str=>{
          switch (str) {
            case "×":
              return "*"
            case "÷":
              return "/"
            default:
              return str
         }
  }).join("")

関数CalculationRymbolReplacementは、任意な計算記号の場合に計算記号を置換する関数です。

  • “×”(乗算)
  • “÷”(除法)

と上記の2つの計算記号は、JSでは、使えません。したがって、JSの環境に合わせなければいけません。なので、対応する計算記号に変換をしていきます。

 str.split("").map(str=>{
  }).join("")

まずは、処理の手順を確認をしましょう。splitを用いて、文字列を分割をして、mapを用いて、一つ一つの文字列にアクセスをしていき、任意な計算記号の場合に計算記号を置換をします。その後、mapの返り値の配列を結合をして、計算記号の置換は終了になります。最終的に関数CalculationRymbolReplacementの返り値は、計算記号の置換が完了した文字列になります

次に置換する条件を確認をしていきましょう。

map(str=>{
          switch (str) {
            case "×":
              return "*"
            case "÷":
              return "/"
            default:
              return str
         }
  })

switch文を用いて、分割した文字列に対して、条件分岐を行っています。

case "×":
   return "*"

strが”×”だった場合は、乗算になりますので、JSで乗算に対応する”*”を返り値として返します。

case "÷":
  return "/"

strが”÷”だった場合は、除算になりますので、JSで乗算に対応する”/”を返り値として返します。

default:
  return str

デフォルトでは、シンプルにstrを返します。注意点として、返り値の明示的に示さないとmapの配列の中に期待している値とは違う、undefinedが混ざってしまいます。なので、置換したい条件以外にも、デフォルトで文字列を返り値として返す必要があります。

以上で、「関数CalculationRymbolReplacement」の解説を終了します。

ExtractedFromTheArray

const  ExtractedFromTheArray = (str)=> Array.prototype.filter.bind(clacultorButtons,button=>button.textContent == str)()

関数ExtractedFromTheArrayは、配列から値を抽出をする関数です。今回、電卓のボタンの中で、要所要所で

  • ACボタン
    →計算した情報をすべて消去
  • CEボタン
    →最後に入力した数値だけを消去

を入れ替えていきます。

計算中にはCEボタンを表示をして、計算前、計算結果表示後にACボタンを表示をします。

事前準備」のhtml構造を確認をして頂ければ、ボタンのノードの取得方法に察しがつくと思います。
今回は、querySelectorAllメソッドを用いて、ボタンのノードをノードリストオブジェクトとして取得をしていきます。

Array.prototype.filter.bind(clacultorButtons,button=>button.textContent == str)()

ノードリストオブジェクトのままですと、fileterメソッドを使えません(正確には配列では無いため)。したがって、bindメソッドを用いて、配列に変換をして、配列系のメソッドを使える様にします。

button=>button.textContent == str

bindの第2引数には、filterメソッドで使う、コールバック関数を渡しています。一つ一つのボタンにアクセスをしてstr(AC OR CE)と一致する文字列のノードを取得をします。

関数ExtractedFromTheArrayの最終的な返り値は、filterメソッドでフィルタリングをした配列です。

const  ExtractedFromTheArray = (str)=>{
  return Array.prototype.filter.bind(clacultorButtons,button=>{
    return button.textContent == str
  })()
}

また、関数ExtractedFromTheArrayは、かなり省略をしてしまいました。省略前の状態は上記の通りです。省略した関数の状態だと理解しづらい場合は、こちらの記述でも、問題ありません。

以上で関数ExtractedFromTheArrayの解説を終了します。

SafetyEvail

function SafetyEvail(fn) {
  return new Function(`"use strict"; return ${fn}`)();
}

関数SafetyEvailは、文字列の計算式を評価をして演算する関数です。evailという関数を使えば、関数を定義をせずに文字列を演算できたのですが、evailは、セキュリティ面でよろしく無いので、関数コンストラクタを用いて、evailに似た関数を作成しました。

FormulaPatternCheck

function FormulaPatternCheck(str,filed){

      if( str.match(/[\(\-\+\÷\×\%][^CE\dAC\()]+/)){
        alert("数字を入力をしてください")
        filed.value= str.replace(/[\(\-\+\÷\×\%][^\d]+/g,'')
        return true
      }
      if ( str.match(/\([^CE\dAC]+/)) {
        alert("数字を入力をしてください")
        filed.value = str.replace(/\([^CE\dAC]+/g,'')
        return true
      }
      if ( str.match(/[^\d]\)/)) {
        alert("計算記号を入力をしてください")
        filed.value= str.replace(/[^\d]\)/g,'')
        return true
      }
      if (str.match(/[^\-\+\÷\×\%]\(/)) {
        alert("計算記号を入力をしてください")
        filed.value= str.replace(/[^\-\+\÷\×\%]\(/g,'')
        return true
      }
      if ( str.match(/\)[^\=\-\+\÷\×\%\CE\AC]/g,'')) {
        alert("計算記号を入力をしてください")
        filed.value= str.replace(/\)[^\-\+\÷\×\%\CE\AC]/g,'')
        return true
      }
       if (str.match(/\([\d]*\=/)) {
        filed.value = str.replace(/\=/g,'')
        alert("正しい計算")
        console.log(3);
        return true
       }
      if (str.match(/\([\d\-\+\÷\×\%\=]*[\d]\=/g)) {
        filed.value = str.replace(/\=/g,'')
        alert("正しい計算")
        return true
      }
}
//イメージ
//if( FormulaPatternCheck(value,filed))return

関数FormulaPatternCheckは、計算式のパターンが正しいかをチェックする関数です。

正規表現を用いるので、少し難しいと感じる方は、飛ばしても大丈夫です。

 if( str.match(/[\(\-\+\÷\×\%][^CE\dAC\()]+/)){
        alert("数字を入力をしてください")
        filed.value= str.replace(/[\(\-\+\÷\×\%][^\d]+/g,'')
        return true
}
#正
"-1"
#誤
"-aa"

こちらのif文では、計算記号の後に

  • “CE”
  • “AC”
  • 半角数字

以外の文字が入力された場合にマッチして、if文内の処理が読み込まれます。計算記号の後には必ず”数字”OR”CE”OR”AC”が入力されないと、計算式の保守性を保つ事ができません。なので、他の文字が入力された場合は、alertで警告をしてreplaceメソッドを用いて、余分な文字列を空白文字に置換をします。

  if ( str.match(/\([^CE\dAC]+/)) {
        alert("数字を入力をしてください")
        filed.value = str.replace(/\([^CE\dAC]+/g,'')
        return true
      }
#正:"(10"
#誤:"(+"

こちらのif文では、左括弧の後に

  • “CE”
  • “AC”
  • 半角数字

以外の文字が入力された場合にマッチして、if文内の処理が読み込まれます。前括弧の後には必ず”数字”OR”CE”OR”AC”が入力されないと、計算式の保守性を保つ事ができません。なので、他の文字が入力された場合は、alertで警告をしてreplaceメソッドを用いて、余分な文字列を空白文字に置換をします。

 if ( str.match(/[^\d]\)/)) {
        alert("計算記号を入力をしてください")
        filed.value= str.replace(/[^\d]\)/g,'')
        return true
}
#正:"2)"
#誤:"+)"

こちらのif文では、右括弧の前に数字以外の文字が入力された時にマッチして、if文内の処理が読み込まれます。右括弧の前に計算記号などが入力されてしまうとエラーが発生します。

 if (str.match(/[^\-\+\÷\×\%]\(/)) {
        alert("計算記号を入力をしてください")
        filed.value= str.replace(/[^\-\+\÷\×\%]\(/g,'')
        return true
  }
#正:"+("
#誤:"1("

こちらのif文では、左括弧の前に計算記号以外の文字が入力された時にマッチして、if文内の処理が読み込まれます。左括弧の前に計算記号以外の文字が入力されてしまうとエラーになります。

if ( str.match(/\)[^\=\-\+\÷\×\%\CE\AC]/g,'')) {
        alert("計算記号を入力をしてください")
        console.log(4);
        filed.value= str.replace(/\)[^\-\+\÷\×\%\CE\AC]/g,'')
        return true
      }
#正:")-"
#誤:")1"

こちらのif文では、右括弧の後に計算記号or”AC”or”CE”以外の文字が入力された場合にマッチをします。右括弧の後に数字が入力されてしまうとコンピューター側が上手く演算をしてくれずにエラーを起こしてしまいます。

 if (str.match(/\([\d]*\=/)) {
        filed.value = str.replace(/\=/g,'')
        alert("正しい計算")
        console.log(3);
        return true
   }
//正:(1111+
//誤:(1111=

こちらのif文では、括弧内の連続した数字の次に”=”が入力されたらマッチします。括弧内の連続した数字の次に”=”が入力されて演算が開始されてしまうと計算式が中途半端な状態で評価されてしまうので、エラーが起きてしまいます。

 if (str.match(/\([\d\-\+\÷\×\%\=]*[\d]\=/g)) {
        filed.value = str.replace(/\=/g,'')
        alert("正しい計算")
        return true
}
//正(11+11)
//誤(11+11=

こちらのif文では、連続した数字と計算記号の後に”=”が入力した場合にマッチをします。上記の「誤」例をご覧頂ければわかると思いますが、括弧を閉じず、演算が始まってしまうと、計算式が中途半端な状態で評価されてしまうので、エラーが起きてしまいます。

以上で、「FormulaPatternCheck」の解説を終了します。

ArithmeticProcessing

function ArithmeticProcessing(value,filed){
  const ACBUTTON= ExtractedFromTheArray("AC")
  if (ACBUTTON.length!=0) ACBUTTON[0].textContent = "CE"
  switch (true) {
    case  value == "." && filed.length <=1:
      return true
    case value == "=" && filed.length <=1:
      return true
    case value == "="  :
      let str=filed.value+value
      if (FormulaPatternCheck(str,filed))return true
      let aFormula = CalculationRymbolReplacement(filed.value)
      filed.value = SafetyEvail(aFormula)
      const CEBUTTON=ExtractedFromTheArray("CE")
      if (CEBUTTON.length != 0) CEBUTTON[0].textContent = "AC"
      return true
    case value == "CE":
      filed.value = filed.value.slice(0,-1)
      return true
    case value == "AC":
      filed.value =""
      const ACBUTTON= ExtractedFromTheArray("AC")
      if (ACBUTTON.length!=0 ){
        // ACBUTTON[0].textContent = "CE"
        clacultorFiled.value = ""
       }
      return true
  }
}

ArithmeticProcessingは、計算式を評価する関数です。要するに電卓機能の根幹を担う関数です。こちらの関数が上手く作動しないと計算ができませんので、注意深く、実装していきましょう。

const  ExtractedFromTheArray = (str)=> Array.prototype.filter.bind(clacultorButtons,button=>button.textContent == str)()
const ACBUTTON= ExtractedFromTheArray("AC")
  if (ACBUTTON.length!=0){
    ACBUTTON[0].textContent = "CE"
  }

まず初めに、関数ExtractedFromTheArrayを呼び出して、ACボタンを値として持っているノードを取得し、定数ACBUTTONに格納をします。
その後、定数ACBUTTONの長さが0でなければと条件分岐をします。つまり存在していれば、if文のスコープ内の処理が読み込まれます。

ACBUTTON[0].textContent = "CE"

条件が該当する場合は、”CE”と文字列を変更をします。

  switch (true) {
    case  value == "." && filed.length <=1:
      return true
    case value == "=" && filed.length <=1:
      return true
    case value == "="  :
      let str=filed.value+value
      if (FormulaPatternCheck(str,filed))return true
      let aFormula = CalculationRymbolReplacement(filed.value)
      filed.value =  eval(aFormula)
      const CEBUTTON=ExtractedFromTheArray("CE")
      if (CEBUTTON.length != 0) CEBUTTON[0].textContent = "AC"
      return true
    case value == "CE":
      filed.value = filed.value.slice(0,-1)
      return true
    case value == "AC":
      filed.value =""
      const ACBUTTON= ExtractedFromTheArray("AC")
      if (ACBUTTON.length!=0 ){
        ACBUTTON[0].textContent = "CE"
        clacultorFiled.value = ""
       }
      return true
  }
}

次にswitch文の中身を解説をしていきます。

 case  value == "." && filed.length <=1:
      return true

valueが”.”で且つ文字列の長さが1未満の場合、つまり、最初に入力された値が"."だった場合は、計算式として評価しません。したがって、return文を用いてtrueを返り値として返し以降の処理を読み込ませずに強制終了します。

case value == "=" && filed.length <=1:
    return true

valueが”=”で且つ文字列の長さが1未満の場合、つまり最初に入力された値が"="だった場合は、計算式として評価しません。したがって、return文を用いてtrueを返り値として返し以降の処理を読み込ませずに強制終了します。

case value == "="  :
      let str=filed.value+value
      if (FormulaPatternCheck(str,filed))return true
      let aFormula = CalculationRymbolReplacement(filed.value)
      filed.value =  eval(aFormula)
      const CEBUTTON=ExtractedFromTheArray("CE")
      if (CEBUTTON.length != 0) CEBUTTON[0].textContent = "AC"
      return true

valueが”=”だった場合は、文字列を計算式として評価をして演算していきます。

let str=filed.value+value
if (FormulaPatternCheck(str,filed))return true

まず、現在入力をされている文字列を取得、連結をして変数strに格納をします。その後、計算式のパターンをチェックをする、関数FormulaPatternCheckを呼び出して、正当な計算式かをチェックをします。もし、計算式のパターンが不当な場合は、return文を用いてtrueをを返して、処理を強制終了します。

const CalculationRymbolReplacement =(str)=>
   str.split("").map(str=>{
          switch (str) {
            case "×":
              return "*"
            case "÷":
              return "/"
            default:
              return str
         }
  }).join("")

let aFormula = CalculationRymbolReplacement(filed.value)

次に、関数CalculationRymbolReplacementを用いて、文字列(計算記号)を置換して、JSの環境に合わせていきましょう。返り値は、変数aFormulaに格納します。

function SafetyEvail(fn) {
  return new Function(`"use strict"; return ${fn}`)();
}
filed.value =  SafetyEvail(aFormula)

次に関数SafetyEvailを用いて、文字列の計算式を評価をして演算していきます。演算結果は、filed.valueに格納して、計算結果を画面に表示をします。

const CEBUTTON=ExtractedFromTheArray("CE")
      if (CEBUTTON.length != 0) CEBUTTON[0].textContent = "AC"
return true

こちらのcase文の条件が読み込まれるという事は、既に文字列の長さが1以上である事が分かります。したがって、関数ExtractedFromTheArrayを用いて、 CEボタンを値として持っているノードを取得し、定数CEBUTTONに格納をします。その後、定数CEBUTTONの長さが0でなければと条件分岐をします。つまり存在していれば、if文のスコープ内の処理が読み込まれます。

 CEBUTTON[0].textContent = "AC"

条件に該当する場合は、textContentを用いて、”AC”と文字列を変更します。

return true

最終的にtrueと返り値を返し、以降の処理を読み込まさせないで強制終了します。

 case value == "CE":
    filed.value = filed.value.slice(0,-1)
    return true

valueがCEだった場合は、sliceを用いて、計算式の末尾の文字をカットします。

case value == "AC":
      filed.value =""
      const ACBUTTON= ExtractedFromTheArray("AC")
      if (ACBUTTON.length!=0 ){
        ACBUTTON[0].textContent = "CE"
        clacultorFiled.value = ""
       }
      return true

value がACだった場合は、AC→CEに変更して、計算式を初期化します。つまりテキストフィールド に入力されている値を全て削除します。

以上で、「ArithmeticProcessing」の解説を終了します。

ReplaceFormula

function ReplaceFormula(){
  const reg =new RegExp(/[^0-9\=\*\(\)\=\+\-\×\÷\%\.\/]/g)
    if (this.value.match(reg)) {
      alert("無効な値は入力をしないでください")
     this.value= this.value.replace(reg,"")
    }
    if (this.value.match(/\*/)) {
      this.value= this.value.replace(/\*/,"×")
    }
    if (this.value.match(/\//)) {
      this.value= this.value.replace(/\//,"÷")
    }
    let str = this.value
    if (FormulaPatternCheck(str,this)) {return}
    if (this.value.match(/\=/)) {
      let str=this.value+clacultorButtons.textContent
      if (FormulaPatternCheck(str,this)) {return}
      let aFormula = CalculationRymbolReplacement(this.value.replace(/\=/,''))
      this.value =  SafetyEvail(aFormula)
      const CEBUTTON=ExtractedFromTheArray("CE")
      if (CEBUTTON.length != 0) CEBUTTON[0].textContent = "AC"
    }
}

関数ReplaceFormulaは、数式を置き換えていく関数です。テキストフィールド に入力された、計算式のパターンをチェックをして、

  • 置き換え
  • 制御

を行っていきます。(keyupイベント専用の関数です。)

const reg =new RegExp(/[^0-9\=\*\(\)\=\+\-\×\÷\%\.\/]/g)

まず、RegExpのインスタンスを生成をします。なお、正規表現の意味は「数字または計算記号以外にマッチ」という意味になります。要するに計算式に必要の無い文字にマッチします。

if (this.value.match(reg)) {
      alert("無効な値は入力をしないでください")
     this.value= this.value.replace(reg,"")
   }

こちらのif文では、計算式が数字または計算記号以外にマッチした場合と条件分岐をしています。条件に該当する場合は、alertで警告をして、replaceメソッド用いて、余計な文字を削除をします。

 if (this.value.match(/\*/)) {
      this.value= this.value.replace(/\*/,"×")
}

計算式にアスタリスクがマッチした場合、フロントには、✖︎を表示したいため、replaceメソッドを用いて、アスタリスクから✖︎に変更します。

if (this.value.match(/\//)) {
      this.value= this.value.replace(/\//,"÷")
}

計算式にスラッシュがマッチした場合、フロントには、➗を表示したいため、replaceメソッドを用いて、スラッシュから➗に変更します。

let str = this.value
if (FormulaPatternCheck(str,this)) {return}

次に、現在入力をされている文字列を取得、連結をして変数strに格納をします。その後、計算式のパターンをチェックをする、関数FormulaPatternCheckを呼び出して、正当な計算式かをチェックをします。もし、計算式のパターンが不当な場合は、return文を用いてtrueをを返して、処理を強制終了します。

if (this.value.match(/\=/)) {
      let str=this.value+clacultorButtons.textContent
      if (FormulaPatternCheck(str,this)) {return}
      let aFormula = CalculationRymbolReplacement(this.value.replace(/\=/,''))
      this.value =  SafetyEvail(aFormula)
      const CEBUTTON=ExtractedFromTheArray("CE")
      if (CEBUTTON.length != 0) CEBUTTON[0].textContent = "AC"
    }

計算式が”=”にマッチした場合と条件分岐を行っています。
処理内容自体は、「ArithmeticProcessing」で解説をしているので省略します。

以上で、「ReplaceFormula」の解説を終了します。

ボタンを押して計算できる様にしよう


 次に、ボタンを押して計算ができる様にしていきましょう。

  • ノードの取得
  • addEventListenerメソッドでイベント登録
  • イベント発火後の処理を実装

ノードの取得

まずは、ノードを取得をしていきましょう。

const clacultorFiled = document.querySelector("input[type='text']")
const clacultorButtons = document.querySelectorAll(".clacultorButton")

正しくノードを取得ができたかデバックをしていきましょう。

console.log(clacultorFiled)
console.log(clacultorButtons)

要素が取得ができているのを確認ができたら次のステップに進みましょう。

addEventListenerメソッドでイベント登録

次にイベントを登録をしていきます。ボタンを押して計算したいので、clickイベントを定数clacultorButtonsの各要素に登録をしていきます。

clacultorButtons.forEach(button=>button.addEventListener("click",function(){}))

正しくイベントが登録ができたか、デバックをしておきましょう。

clacultorButtons.forEach(button=>button.addEventListener("click",function(){
  console.log("test")
}))

コンソール結果にtestという文字列が表示がされていれば、イベントが登録ができた事になるので次のステップに進みましょう。

イベント発火後の処理を実装

イベントが登録できた所でイベント発火後の処理を実装していきましょう。

clacultorButtons.forEach(button=>button.addEventListener("click",function(){
  if (clacultorFiled.value.length == 0 &&  validate(this.textContent))   return
  let str= clacultorFiled.value+this.textContent
  if (FormulaPatternCheck(str,clacultorFiled)) {return}
  if (ArithmeticProcessing(this.textContent,clacultorFiled)) return
  clacultorFiled.value=clacultorFiled.value + this.textContent
}))

では、処理内容を解説をしていきます。

if (clacultorFiled.value.length == 0 &&  validate(this.textContent))   return

こちらは、テキストフィールドに入力がない場合で且つ関数validateが返り値としてtrueを返した場合と条件分岐をしています。つまり、計算式の先頭に入力されては、困る計算記号などが入力できない様になっています。

let str= clacultorFiled.value+this.textContent
if (FormulaPatternCheck(str,clacultorFiled)) {return}

次に計算式のパターンをチェックをしていきます。変数strに現在の計算式を格納して、関数FormulaPatternCheckを呼び出し、trueが返り値として返って来た場合は、計算式のパターンに問題があるので、return文で強制終了します。問題が無ければ以降の処理が読み込まれます。

if (ArithmeticProcessing(this.textContent,clacultorFiled)) return

計算式のパターンに問題がない場合は、関数ArithmeticProcessingを呼び出し、演算処理を行います。この段階で、計算式に問題があった場合は、演算処理を中断をして処理を止めます。問題が無ければ、演算結果が画面に表示がされます。関数ArithmeticProcessingの処理が動けば、返り値としてtrueを返すで、結果的にreturn文で処理が強制終了します。

clacultorFiled.value=clacultorFiled.value + this.textContent
  • ArithmeticProcessing
  • FormulaPatternCheck

で処理が発火しなかった場合は、テキストフィールドに入力したボタンの値を足します。

以上で、「イベント発火後の処理を実装」の解説を終了します。

テキストフィールドから計算できる様にしよう

では、次にテキストフィールドから計算できるにしていきましょう。

  • addEventListenerメソッドでイベント登録
  • イベント発火後の処理を実装

addEventListenerメソッドでイベント登録

ノードの取得」のトピックで、必要なノードを取得をしたので、イベント登録から初めていきます。「テキストフィールドから計算できる」という条件にマッチするイベントは、keyupイベントです。したがって、定数clacultorFiledに対してkeyupイベントを登録をしていきます。

clacultorFiled.addEventListener("keyup",function(){})

正しくイベントが登録ができたか、デバックをしておきましょう。

clacultorFiled.addEventListener("keyup",function(){
  console.log(this.value);
})

コンソール結果に入力した文字列が表示がされていれば、イベントが登録ができた事になるので次のステップに進みましょう。

イベント発火後の処理を実装

イベントが登録できた所でイベント発火後の処理を実装していきましょう。

clacultorFiled.addEventListener("keyup",function(e){
    if ( e.key=="Shift") {return }
    if (this.value.length <=1 &&validate(this.value))return this.value = ""
    const ACBUTTON= ExtractedFromTheArray("AC")
    if (ACBUTTON.length!=0){
     return  ACBUTTON[0].textContent = "CE"
    }
    ReplaceFormula.bind(this)()
})

それでは、解説をしていきます。

 if ( e.key=="Shift") {return }

シフトキーが入力された場合は、return文で強制終了します。

if (this.value.length <=1 &&validate(this.value))return this.value = ""

こちらは、テキストフィールドの値が1未満の場合で且つ関数validateがtrueを返した場合と条件分岐をしています。つまり、計算式の先頭に入力されては、困る計算記号などが入力できない様になっています。

 const ACBUTTON= ExtractedFromTheArray("AC")
    if (ACBUTTON.length!=0){
     return  ACBUTTON[0].textContent = "CE"
    }

次に関数ExtractedFromTheArrayを呼び出して、ACボタンを値として持っているノードを取得し、定数ACBUTTONに格納をします。
その後、定数ACBUTTONの長さが0でなければと条件分岐をします。つまり存在していれば、if文のスコープ内の処理が読み込まれます。

return  ACBUTTON[0].textContent = "CE"

条件が該当する場合は、”CE”と文字列を変更をします。

ReplaceFormula.bind(this)()

最後に関数ReplaceFormulaを呼び出して、計算式を置き換え、演算をして、画面を更新をします。

以上で、「イベント発火後の処理を実装」の解説を終了します。実装自体は、このトピックで終了しますので、完成したソースコードは、下記に貼り付けおきますのでご活用ください。

const buttons = [
  ["(",")","%",'AC'],
  ["7",'8','9','÷'],
  ["4",'5','6','×'],
  ["3",'2','1','-'],
  ["0",'.','=','+'],
]

const table= document.createElement("table")
const tbody = document.createElement('tbody')
table.className = "calculate"
for (let i = 0; i < buttons.length; i++) {
  const tr =document.createElement('tr')
  for (let j = 0; j < buttons.length-1; j++) {
    tr.insertAdjacentHTML('beforeend',`<td ><button class="clacultorButton">${buttons[i][j]}</button></td>`)
  }
  table.appendChild(tr)
}
document.querySelector('.clacultorButtons').appendChild(table)

const clacultorFiled = document.querySelector("input[type='text']")
const clacultorButtons = document.querySelectorAll(".clacultorButton")

function validate(value) {
  switch (true) {
    case value ==".":
    case value == ")":
    case value == "/":
    case value == "*":
    case value == "=":
    case value == "+":
    // case value == "-":
    case value == "×":
    case value == "÷":
    case value == "%":
      return true
    default:
    break;
  }
}

function SafetyEvail(fn) {
  return new Function(`"use strict"; return ${fn}`)();
}

const CalculationRymbolReplacement =(str)=>
   str.split("").map(str=>{
          switch (str) {
            case "×":
              return "*"
            case "÷":
              return "/"
            default:
              return str
          }
  }).join("")

const  ExtractedFromTheArray = (str)=> Array.prototype.filter.bind(clacultorButtons,button=>button.textContent == str)()

function FormulaPatternCheck(str,filed){
      if( str.match(/[\(\-\+\÷\×\%][^CE\dAC\()]+/)){
        alert("数字を入力をしてください")
        filed.value= str.replace(/[\(\-\+\÷\×\%][^\d]+/g,'')
        return true
      }
      if ( str.match(/\([^CE\dAC]+/)) {
        alert("数字を入力をしてください")
        filed.value = str.replace(/\([^CE\dAC]+/g,'')
        return true
      }
      if ( str.match(/[^\d]\)/)) {
        alert("計算記号を入力をしてください")
        filed.value= str.replace(/[^\d]\)/g,'')
        return true
      }
      if (str.match(/[^\-\+\÷\×\%]\(/)) {
        alert("計算記号を入力をしてください")
        filed.value= str.replace(/[^\-\+\÷\×\%]\(/g,'')
        return true
      }
      if ( str.match(/\)[^\=\-\+\÷\×\%\CE\AC]/g,'')) {
        alert("計算記号を入力をしてください")
        filed.value= str.replace(/\)[^\-\+\÷\×\%\CE\AC]/g,'')
        return true
      }
      if (str.match(/\([\d]*\=/)) {
        filed.value = str.replace(/\=/g,'')
        alert("正しい計算")
        return true
      }
      if (str.match(/\([\d\-\+\÷\×\%\=]*[\d]\=/g)) {
        filed.value = str.replace(/\=/g,'')
        alert("正しい計算")
        return true
      }
}

function ArithmeticProcessing(value,filed){
  const ACBUTTON= ExtractedFromTheArray("AC")
  if (ACBUTTON.length!=0) ACBUTTON[0].textContent = "CE"
  switch (true) {
    case  value == "." && filed.length <=1:
      return true
    case value == "=" && filed.length <=1:
      return true
    case value == "="  :
      let str=filed.value+value
      if (FormulaPatternCheck(str,filed))return true
      let aFormula = CalculationRymbolReplacement(filed.value)
      filed.value = SafetyEvail(aFormula)
      const CEBUTTON=ExtractedFromTheArray("CE")
      if (CEBUTTON.length != 0) CEBUTTON[0].textContent = "AC"
      return true
    case value == "CE":
      filed.value = filed.value.slice(0,-1)
      return true
    case value == "AC":
      filed.value =""
      const ACBUTTON= ExtractedFromTheArray("AC")
      if (ACBUTTON.length!=0 ){
        ACBUTTON[0].textContent = "CE"
        clacultorFiled.value = ""
       }
      return true
  }
}

function ReplaceFormula(){
  const reg =new RegExp(/[^0-9\=\*\(\)\=\+\-\×\÷\%\.\/]/g)
    if (this.value.match(reg)) {
      alert("無効な値は入力をしないでください")
     this.value= this.value.replace(reg,"")
    }
    if (this.value.match(/\*/)) {
      this.value= this.value.replace(/\*/,"×")
    }
    if (this.value.match(/\//)) {
      this.value= this.value.replace(/\//,"÷")
    }
    let str = this.value
    if (FormulaPatternCheck(str,this)) {return}
    if (this.value.match(/\=/)) {
      let str=this.value+clacultorButtons.textContent
      if (FormulaPatternCheck(str,this)) {return}
      let aFormula = CalculationRymbolReplacement(this.value.replace(/\=/,''))
      this.value =  SafetyEvail(aFormula)
      const CEBUTTON=ExtractedFromTheArray("CE")
      if (CEBUTTON.length != 0) CEBUTTON[0].textContent = "AC"
    }
}


function handleClick(){
  if (clacultorFiled.value.length == 0 &&  validate(this.textContent))   return
  let str= clacultorFiled.value+this.textContent
  if (FormulaPatternCheck(str,clacultorFiled)) {return}
  if (ArithmeticProcessing(this.textContent,clacultorFiled)) return
  clacultorFiled.value=clacultorFiled.value + this.textContent
}
clacultorButtons.forEach(button=>button.addEventListener("click",handleClick.bind(button)))

function handleKeyUp(e){
  if ( e.key=="Shift") {return }
  if (this.value.length <=1 &&validate(this.value))return this.value = ""
  const ACBUTTON= ExtractedFromTheArray("AC")
  if (ACBUTTON.length!=0){
   return  ACBUTTON[0].textContent = "CE"
  }
  ReplaceFormula.bind(this)()
}
clacultorFiled.addEventListener("keyup",handleKeyUp.bind(clacultorFiled))

まとめ:電卓を作ろう

今回は、電卓を作成していきました。計算式のパターンをチェックしたり、置換をしたりと、複雑な処理を実装していきましたので、難しく感じたと思います。特に正規表現については、何をしているのかが、わからなったと思います。なので、難しい概念などは繰り返し学習をして徐々に知識を定着していきましょう。

以上、kazutoでした。