ずっと以前に、Javascriptでのプログラミング学習は、電卓を作るのが良いというブログ記事を書いた事がある。
[JavaScript] 初心者トレーニング「電卓」
この記事は、以前勤めていた会社でCTOをやっていた時に、非エンジニアの営業職や管理職などの若手従業員の人にせがまれて、
社内でプログラミング学習を行なう時に、参考資料として書いたブログなのだが、いろいろなサイトで電卓プログラムのコードが書かれているのを見かけるようになった。
そして、他のサイトでは、ほぼ同じコードが使いまわされているらしく、そのコードを少しブラッシュアップしてみたくなったので、電卓ソースコードの焼き直しをしてみようと思います。
デモ
- AC
- +/-
- %
- ÷
- 7
- 8
- 9
- ×
- 4
- 5
- 6
- ー
- 1
- 2
- 3
- +
- 0
- .
- =
ソースコード
calculater.html
<link rel="stylesheet" href="calculater.css">
<script type="module" src="calculater.js"></script>
<ul class="calculator">
<li class="result"></li>
<li class="clear">AC</li>
<li class="plus-minus">+/-</li>
<li class="percent">%</li>
<li class="unit" data-unit="/">÷</li>
<li class="num">7</li>
<li class="num">8</li>
<li class="num">9</li>
<li class="unit" data-unit="*">×</li>
<li class="num">4</li>
<li class="num">5</li>
<li class="num">6</li>
<li class="unit" data-unit="-">ー</li>
<li class="num">1</li>
<li class="num">2</li>
<li class="num">3</li>
<li class="unit" data-unit="+">+</li>
<li class="num" data-type="num-0">0</li>
<li class="point">.</li>
<li class="equal">=</li>
</ul>
calculater.css
.calculator{
--size-border : 2px;
width:250px;
margin:20px;
padding:0;
background-color:#444;
border:var(--size-border) solid black;
border-radius:20px;
overflow:hidden;
display:grid;
grid-template-columns: repeat(4, 1fr);
gap:var(--size-border);
}
.calculator,
.culculator *{
user-select: none;
}
.calculator li{
height:30px;
list-style:none;
margin:0;
padding:5px;
color:white;
font-size:20px;
cursor:pointer;
text-align:center;
}
.calculator li.result{
height:40px;
user-select: auto;
grid-column: 1 / 5;
cursor:initial;
text-align:right;
font-size:30px;
padding:10px;
}
.calculator li[data-type="num-0"]{
grid-column: 1 / 3;
}
.calculator li:not(.result):hover{
background-color:#eee;
}
.calculator li:not(.result):active{
background-color:#fdd;
}
.calculator li.unit,
.calculator li.equal{
background-color:#F80;
}
.calculator li.num,
.calculator li.point{
background-color:#888;
}
.calculator li.clear,
.calculator li.plus-minus,
.calculator li.percent{
background-color:#666;
}
calculater.js
class Main{
constructor(){
this.set_event()
}
get elm_result(){
return document.querySelector(`.calculator .result`)
}
set_event(){
document.querySelector(".calculator").addEventListener("click" , this.click.bind(this))
}
click(e){
const li = e.target.closest(`li`)
if(!li){return}
switch(li.getAttribute("class")){
case "point":
case "num":
new Num(e.target.textContent)
break
case "unit":
new Unit(e.target.getAttribute("data-unit"))
break
case "equal":
new Unit("=")
break
case "plus-minus":
new PlusMinus()
break
case "percent":
new Percent()
break
case "clear":
new Clear()
break
}
}
}
export class Calc{
constructor(num1, num2 , unit){
this.unit = unit
this.num1 = num1 || 0
this.num2 = num2 || 0
this.num = this.calc()
this.adjust()
}
calc(){
const max = Math.max(
(String(this.num1).split('.')[1] || '').length,
(String(this.num2).split('.')[1] || '').length,
)
const factor = Math.pow(10 , max)
const num1 = this.num1 * factor
const num2 = this.num2 * factor
const formula = this.unit ? `${num1} ${this.unit} ${num2}` : num2
const res = new Function(`return (${formula})`)()
return res / (factor * factor)
}
adjust(){
switch(this.num){
case Infinity:
case NaN:
this.num = null;
break
}
}
}
export class Clear{
constructor(){
Common.elm_result.textContent = ""
Unit.num = null
Unit.unit = null
}
}
export class Common{
// 数字を表示するエレメント
static get elm_result(){
return document.querySelector(`.calculator .result`)
}
// 入力して表示されている数値(文字列)を取得
static get current_str(){
return Common.elm_result.textContent
}
}
export class Num{
static input_flg = false
constructor(str){
if(!str){return}
if(!this.is_safe(str)){return}
if(!Num.input_flg){
Common.elm_result.textContent = ""
}
this.set_str(str)
Num.input_flg = true
}
// .(ドット)が2つ以上になる場合は入力しない
is_safe(str){
return str === "." && Common.current_str.indexOf(".") !== -1 ? false : true
}
set_str(str){
Common.elm_result.textContent += str
}
}
export class PlusMinus{
constructor(){
if(!this.result_str){return}
Common.elm_result.textContent = Number(this.result_str) * -1
}
get result_str(){
return Common.elm_result.textContent
}
}
export class Unit{
static num = null
static unit = null
constructor(unit){
this.unit = unit
if(!this.check()){return}
const num = this.calc()
switch(unit){
case "=":
this.view(num)
Unit.num = null
Unit.unit = null
break
default:
Num.input_flg = false
Unit.num = num || null
Unit.unit = unit
break
}
}
// 数値が入力されていない場合は何もしない
check(){
return !this.unit || !Common.elm_result.textContent ? false : true
}
// メモリされている数値と入力値の合計処理
calc(){
const current_num = Number(Common.current_str) || 0
// メモリ値が内場合の処理
if(Unit.num === null){
return current_num
}
// メモリ値がある場合の処理(計算処理)
else{
const memory_num = Unit.num || 0
const calc = new Calc(memory_num, current_num, Unit.unit)
return calc.num
}
}
// 表示処理
view(num){
Common.elm_result.textContent = num
}
}
export class Percent{
constructor(){
if(!this.result_str){return}
Common.elm_result.textContent = Number(this.result_str) /100
}
get result_str(){
return Common.elm_result.textContent
}
}
switch(document.readyState){
case "complete":
case "interactive":
new Main()
break
default:
window.addEventListener("DOMContentLoaded" , (()=>new Main()))
}
ブラッシュアップ・ポイントと解説
以下の内容にて前回からのバージョンアップを行いました。
- Tableタグを使わず、簡素なHTMLコードでCSSのFlexBox(gird)を使ったデザイン。(見た目をMacの電卓アプリに寄せる)
- イベント処理セットはHTML記述せずに、JSでセットする。
- JavascriptはModule-typeによるClass構造体で構築。
- 小数値誤差がでるJS特有の不具合を解消。(ex:1.1*3)
1. Tableタグを使わず、簡素なHTMLコードでCSSのFlexBox(gird)を使ったデザイン。(見た目をMacの電卓アプリに寄せる)
見た目も重要ということで、洗練されたOSのデフォルトアプリをそのまま採用してみました。
別に他のデザインでも良かったんですが、わかり易さに加えて、あまり他のサイトには無い、プラス・マイナス入れ替え機能や、%変換機能(100で割るだけ)があったので、そのまま使ってみたし、
見た目以外でも、挙動もなるべく同じになるようにしてみた。
2. イベント処理セットはHTML記述せずに、JSでセットする。
電卓サンプルをブログで掲載しているものは、ほぼ、HTMLにonclickイベントを書いて簡易に処理していますが、MVCみたいなプログラム構造体で、オブジェクト指向っぽく構築してみました。
3. JavascriptはModule-typeによるClass構造体で構築。
今どきのClass構造で構築することで、プログラミングとしての構造体にこだわってみました。
4. 小数値誤差がでるJS特有の不具合を解消。(ex:1.1*3)
JS計算の最大の弱点である、小数点の計算での誤差が出ないように処理を加えてみました。
方法は単純で、少数をその桁数分桁数を掛けて、整数にしてから計算して、また掛けた桁数分割ることで、少数の誤差をほぼ出ないように処理してみました。
試しに、JSで"1.1*3"を計算して、この電卓で同じ計算をしてみると、その違いが理解できるでしょう。
ちなみに、JSの小数点誤差は、10進数で割り切れない、10/3と同じように、2進数でも割り切れない数字がありそれがそのまま採用されているために起きる仕様としての誤差です。
あとがき
電卓は、昔から、プログラミングの学習として非常に重要な要素がたくさん入っているので、
安易なHello Worldで満足しているレベルではないのがわかると思います。
ぜひ、こんにちわ世界の次に、電卓を作って見ることをオススメします。
その次は、Twitterを作るという課題が、プログラミングスクールの定石っぽいですけどね。
0 件のコメント:
コメントを投稿