2016年夏季講習の課題 電卓をつくろう

第一回 夏季講習が開催されました!

8/13 と 8/14 に、ラシュリエの「2016年夏季講習(一泊二日の合宿)」がありました!
トラブル(夕飯)もありましたが(笑)無事終わることができました。
とても楽しかったです。青春だなー(笑)

合宿の開催と場所を提供してくださった吉岡夫妻、合宿中癒しをくれた吉岡夫妻ご息女(コーンおいしかったね♪)、講習に参加くださった shirotoraさん、maruさん、yusukeさん、本当にありがとうございました。
また、機会があればぜひ参加したいです。

さて、今回のブログでは夏季講習の課題になっていた「電卓をつくろう!」について、自分がやってみた感想をお話しようと思います。

※ご紹介するソースコードもかなりわかりづらく、考え方がおかしい部分やバグもはらんでいる可能性大です!
※でも、何かの参考になればと思い書かせていただきました!

課題の仕様確認

今回の合宿の課題は電卓をつくることでしたが、以下の仕様が決められていました。

  • 整数の四則演算ができる
  • 連続して計算できる
  • C(AC)ボタンで初期化できる
  • javascriptを使用して、webブラウザで実行する

ググってみての感想

夏季講習中に “JavaScript” “電卓” とかでググってみた方はいたと思いますが、本物の電卓っぽく動くソースコードは見つからなかったと思います。
私も実際にググってみましたが、たいていディスプレイに表示された計算式(例えば 1+3)を eval で計算しているものが多く、通常の電卓のように計算式そのものがディスプレイに表示されない作りのソースコードはあまり見つかりませんでした。

なので、このブログでは、できる限り通常の電卓に近い動きを再現してみようと思います。

とりあえず文字におこしてみる

私の場合、かなり簡単なプログラムであれば頭の中でできるようになってきましたが、少し複雑なものになるとわけがわからなくなります。
そのため、文字などにやることを先に書いておくことが多いです。ソース内のコメントに書くのでもいいかもしれません。

まず、電卓には主に以下のボタンがあります。

  • 数値のボタン(0-9)
  • 演算子のボタン
  • イコールのボタン
  • C(クリアするやつ)のボタン

自分で作ってみてから気づいたのですが、他に小数点とかありますね。。。
今回は忘れたので省略します(笑)

続いて、それぞれのボタンを押したときの処理を JavaScript の関数で書けばよさそうです。
主に以下です。

  • 数値のボタンを押された時の処理
  • 演算子が押された時の処理
  • イコールが押された時の処理
  • C が押された時の処理

実際の電卓の動きを確かめつつ、これらの処理を一つずつ考えていきます。

数値のボタンを押された時の処理

実際の電卓で確認してみると、数値ボタンを押したときの挙動は主に二つあるようです。

a) すでにある数値に連結させる場合
b) 新しい数値として扱う場合

a) は、すでに 1 という数値がディスプレイに表示されている状態で、2 を押したときに 1 と 2 が連結されて 12 になるケースです。
b) は、1 と押したあとに + を押して、その後に 2 を押したケースです。この場合、12 とはならず、別の数値の 2 として認識されます。

これらの違いは、前に押されたボタンが演算子かどうか?で判断できそうです。
なので、is_operator というフラグ用の変数を用意して、以下のようにしてみます。

– 数値ボタンが押されたとき —> is_operator = false にする
– 演算子ボタンが押されたとき —> is_operator = true にする

そして、数値のボタンが押された時の処理内では、まず is_operator が true か false を if で評価して、
true(一つ前が演算子) なら b) の処理、
false(一つ前が数値) なら a) の処理、というようにしてみるとよさそうです。

なお、電卓では、電源をいれたあとに 0 が表示されてます。
そして、数値ボタンをおすと、その数値が 0 の代わりに表示されます。つまり、電源をいれた直後の最初の入力で数値ボタンをおすと b) の処理が行われていることになります。

演算子ボタンが押された時の処理

演算子ボタンが押された時にぱっと思いつく動作としては以下がありました。

a) 演算子が押されたことをフラグで記憶する
b) どの演算子が押されたかを記憶する
c) 押された演算子をどこかに表示する

a) は、is_operator = true にすることに値します。
b) は、どの演算子が押されたかを変数 current_operator に保存するようにしました。
c) は、押された演算子を div の innerHTML にいれることにしました。

ただ、他にも、演算子ボタンを押したときの動作がありました。
通常の電卓では、1 + 2 を計算する場合、計算結果がディスプレイに表示されるのは、 2 の後に演算子ボタン(= も)を入力したタイミングです。
つまり、
d) 演算子ボタンがおされたときに計算を実行する、という動作もあります。

——————–
1. 電源をいれる
2.1 ボタンを押す。1 が表示される
3.+ ボタンを押す。+ 演算子が押されたことをどこかに表示する
4. 2 ボタンを押す。2 が表示される
5. = ボタン、もしくは、他の演算子ボタンを押す。ディスプレイに計算結果の 3 が表示される
——————–

なので、演算子ボタンを押した場合には、「一つ前数値」と「一つ前の演算子」と「現在ディスプレイに表示されている数値」で計算すればいいことになります。

上の例だと、

「一つ前数値」–> 1
「一つ前の演算子」—> +
「現在ディスプレイに表示されている数値」—> 2

のようになります。

「現在ディスプレイに表示されている数値」は、input type=”text の value から取得すればよさそうです。
「一つ前の演算子」は、current_operator に保存されています。
「一つ前数値」は、該当する変数がないので、新たに result という変数を定義して、ここに保管するようにします。

ただし、計算を実行するのは、一つ前に入力されたのが”数値の場合に限って”です。
一つ前に + が押されており、次に – が押されたようなケースでは、単に current_operator の値を + から – に置き換えるだけです。計算はしません。

ここで一つ疑問がでてきます。
上の流れでいうと、2. の + ボタンが押された時も、計算が実行されるのではないか?ということです。
2. の時点では、一つ前に 1 というボタンを押しただけです。
なので、

「一つ前数値」–> ?
「一つ前の演算子」—> ?
「現在ディスプレイに表示されている数値」—> 1

このように、「一つ前数値」と 「一つ前の演算子」 に心当たりがありません。。。。

ここはこのように考えました。

「0 + 1」であると。

つまり、以下のようになります。

「一つ前数値」–> 0
「一つ前の演算子」—> +
「現在ディスプレイに表示されている数値」—> 1

電源をいれた直後(もしくは c ボタンを押したあと)に表示される 0 に対して、どんな値を加算しようが、その値になります。

0 に 1 を足すと 1
0 に 5 を足すと 5
0 に 100 を足すと 100

そのため、電源をいれた直後に数値ボタンが押された場合は、

「一つ前数値」–> 0
「一つ前の演算子」—> +

とすれば良さそうです。
電源をいれた直後かどうかを評価するには、current_operator に何もはいっていないかを if で評価すれば良さそうです。

ここで、数値がおされた時の関数と、+ が押された時の関数を記載します。

数値が押された時の関数

input_num = function(val){
    
    //初期化直後の入力の場合、入力された値は0に加算するものとする
    if(current_operator == ''){
        result = 0;
        current_operator = '+';
        disp.value = val;
    }else{
        if(is_operator){
            //前の入力が演算子の場合、入力された数値を新規の値として display に表示
            disp.value = val;
        }else{
            //前の入力が数値の場合、入力された数値を文字列として連結
            disp.value = "" + disp.value + val;
        }
    }
    
    is_operator = false;
    
}

+ ボタンが押された時の関数

add = function(){
    //一つ前に押されたボタンが数値の場合のみ計算を実行
    if(!is_operator){
        exec_calc(); //計算を実行する関数
    }
    
    current_operator = '+'; //どの演算子を押されたかを記憶
    show_current_ope(); //ディスプレイにどの演算子が押されたかを表示する関数
    is_operator = true; //演算子が押されたことを示すフラグを true へ
    
}

イコールが押された時の処理

次はイコールボタンが押された時の処理です。

a) 計算を実行します。
b) current_operator を空にします。
c) 演算子が押されたことを表すため、is_operator = true にします。

eq = function(){
    exec_calc(); //計算を実行
    
    current_operator = ''; //current_operator を空にする。これは=の次に数値が押された時に初期化処理になるということ。input_num の if(current_operator == '') の処理
    is_operator = true; //演算子が押されたことを表すため、is_operator = true にする
}

c が押された時の処理

最後に c が押された時の処理です。
電源が押された直後に戻すイメージなので、以下のような初期化処理にしました。

c = function(){
    result = 0;
    is_operator = false;
    current_operator = '';
    disp.value = 0;
    ope_disp.innerHTML = '';
}

全体のコード

全体のコードを載せておきます。
小数点がない、符号+/-がない、キーボードから数値や演算子を入力することに対応してない、イコールを連続で入力すると変な挙動になる(笑)などバグをはらんでいますが、なんとなく本物の電卓っぽく動きました(笑)

余談ですが、- ボタン、x ボタン、= ボタンを押した時に無駄にアニメーションするようにしてみました。
スマホの画面にも一応収まるようになっています。
ボタン部分は、button 要素は使わず、DIV を使っています。
また、table の代わりに flex で横並びを実現しました。

<!DOCTYPE html>
<html lang="ja">
    <head>
        <title>cal</title>
        <meta charset="utf-8">
        <meta name="viewport" content="width= device-width, initial-scale=1.0">
        
        <style>
            #wrap{
                margin: 0 auto;
                width: 800px;
            }
            
            #input_wrap{
            }
            
            #wrap input[type="text"]{
                width: 75%;
                height: 50px;
                font-size: 2.0em;
            }
            #ope_disp{
                width: 5%;
                height: 50px;
                float:left;
                font-size: 1.5em;
                text-align: center;
                line-height: 50px;
            }
            #flex_wrap{
                display: flex;
                flex-wrap: wrap;
                width: 100%;
                margin: 0 auto;
            }
            #flex_wrap div{
                position: relative;
            }
            #flex_wrap div.num_button{
                color: #fff;
                background: #56c723;
                width: 20%;
                height: 150px;
                line-height: 150px;
                font-size: 2.5em;
                text-align: center;
                margin: 2px;
                cursor: pointer;
            }
            #flex_wrap div.num_button:hover{
                background: #30830b;
            }
            #flex_wrap div.ope{
                color: #fff;
                background: #cedb2f;
                width: 20%;
                height: 150px;
                line-height: 150px;
                font-size: 3.0em;
                text-align: center;
                margin: 2px;
                border-radius: 20px;
                cursor: pointer;
            }
            #flex_wrap div.ope:hover{
                background: #a5b114;
            }
            #display{
                text-align: right;
            }
            
            
            .myz_button_ani{
                transition: 1s;
            }
            
            #flex_wrap div span{
                display: block;
                position: absolute;
                font-size:1.5em;
                transition: 1s;
            }
            #flex_wrap div span:nth-of-type(1){
                left: 60px;
                top: 0px;
            }
            #flex_wrap div span:nth-of-type(2){
                left: 60px;
                top: 15px;
            }
            
            @media screen and (max-width: 760px) {
                #wrap {
                    width: 400px;
                }
                
                #flex_wrap div.num_button{
                    height: 75px;
                    line-height: 75px;
                    font-size: 1.5em;
                }
                
                #flex_wrap div.ope{
                    height: 75px;
                    line-height: 75px;
                    font-size: 1.5em;
                }
                
                #flex_wrap div span:nth-of-type(1){
                    top: -5px;
                    left: 30px;
                }
                
                #flex_wrap div span:nth-of-type(2){
                    top: 5px;
                    left: 30px;
                }
                
            }
            
            
        </style>
        
        
        

    </head>
    <body>
        <div id="wrap">
            <div id="input_wrap">
                <input id="display" type="text" value="0">
                <div id="ope_disp"></div>
            </div>

            <div id="flex_wrap">
                
                <div class="num_button myz_button_ani" onclick="input_num('7')">7</div>
                <div class="num_button myz_button_ani" onclick="input_num('8')">8</div>
                <div class="num_button myz_button_ani" onclick="input_num('9')">9</div>
                
                
                <div class="ope myz_button_ani" onclick="add()">+</div>

                <div class="num_button myz_button_ani" onclick="input_num('4')">4</div>
                <div class="num_button myz_button_ani" onclick="input_num('5')">5</div>
                <div class="num_button myz_button_ani" onclick="input_num('6')">6</div>
                <div id="sub_b" class="ope myz_button_ani myz_rotateX" onclick="sub('0')">-</div>
                
                
                <div class="num_button myz_button_ani" onclick="input_num('1')">1</div>
                <div class="num_button myz_button_ani" onclick="input_num('2')">2</div>
                <div class="num_button myz_button_ani" onclick="input_num('3')">3</div>
                <div class="ope myz_button_ani" onclick="div()">÷</div>
                
                <div class="num_button myz_button_ani" onclick="input_num('0')">0</div>
                <div class="ope myz_button_ani" onclick="c()">c</div>
                <div class="ope myz_button_ani" onclick="eq()">
                    <span id="eq_line1">-</span>
                    <span id="eq_line2">-</span>
                </div>
                <div id="mul_b" class="ope myz_button_ani myz_rotateY" onclick="mul()">x</div>
                
                
                
            </div>
        </div>
        
        
                <script>
            
            
                /**
                 * 変数
                 */
                var
                 result = 0, //結果値
                 is_operator = false, //前の入力が演算子かどうか
                 current_operator = '', //現在の演算子
                 
                 //DOM関連
                 disp = document.getElementById('display'),
                 ope_disp = document.getElementById('ope_disp'),
                 mul_b = document.getElementById('mul_b'),
                 sub_b = document.getElementById('sub_b'),
                 
                 //アニメーション関連
                 eq_line1 =  document.getElementById('eq_line1'),
                 eq_line2 =  document.getElementById('eq_line2'),
                 eq_ani_flag = false,
                 mul_ani_flag = false,
                 sub_ani_flag = false
                    
                ;
                
                //関数名の事前宣言
                var
                 add,
                 sub,
                 mul,
                 div,
                 input_num,
                 eq,
                 clear,
                 getCurrentVal,
                 exec_calc,
                 show_current_ope,
                 mul_ani,
                 sub_ani,
                 sub_b,
                 eq_ani,
                 my_debug
                ;
                
                    
                /**
                 * 関数
                 */
                input_num = function(val){
                    
                    //初期化直後の入力の場合、入力された値は0に加算するものとする
                    if(current_operator == ''){
                        result = 0;
                        current_operator = '+';
                        disp.value = val;
                        
                    }else{
                        if(is_operator){
                            //前の入力が演算子の場合、入力された数値を新規の値として display に表示
                            disp.value = val;
                        }else{
                            //前の入力が数値の場合、入力された数値を文字列として連結
                            disp.value = "" + disp.value + val;
                        }
                    }
                    
                    is_operator = false;
                    
                    //debug
                    my_debug();
                    
                }
                
                add = function(){
                    if(!is_operator){
                        exec_calc();
                    }
                    
                    current_operator = '+';
                    show_current_ope();
                    is_operator = true;
                    
                    //debug
                    my_debug();
                    
                }
                
                sub = function(){
                    if(!is_operator){
                        exec_calc();
                    }
                    
                    current_operator = '-';
                    show_current_ope();
                    is_operator = true;
                    
                    //アニメーション
                    sub_ani();
                    
                    //debug
                    my_debug();
                    
                }
                
                mul = function(){
                    if(!is_operator){
                        exec_calc();
        
                    }
                    
                    current_operator = '*';
                    show_current_ope();
                    is_operator = true;
                    
                    //アニメーション
                    mul_ani();
                    
                    //debug
                    my_debug();
                }
                
                div = function(){
                    if(!is_operator){
                        exec_calc();
                    }
                    
                    current_operator = '/';
                    show_current_ope();
                    is_operator = true;
                    
                    //debug
                    my_debug();
                }
                
                eq = function(){
                    exec_calc();
                    
                    current_operator = '';
                    is_operator = true;
                    
                    //アニメーション
                    eq_ani();
                    
                    //debug
                    my_debug();
                }
                
                c = function(){
                    result = 0;
                    is_operator = false;
                    current_operator = '';
                    disp.value = 0;
                    ope_disp.innerHTML = '';
                    
                    //debug
                    my_debug();
                   
                }
                
                /*
                 *内部的な関数
                 */
                
                //現在のディスプレイの値の取得
                getCurrentVal = function(){
                    return disp.value;
                }
                
                //計算を実行(evalを使用)
                exec_calc = function(o){
                    result = eval(result + current_operator + getCurrentVal());
                    disp.value = result;
                }
                
                //ope_disp へ現在の演算子を表示
                show_current_ope = function(){
                    ope_disp.innerHTML = convert_ope(current_operator);
                    
                    function convert_ope(o){
                        if(o === '*'){
                            return 'x';
                        }else if(o === '/'){
                            return '÷';
                        }else{
                            return o;
                        }
                    }
                }
                
                //x ボタン実行時のアニメーション
                mul_ani = function(){
                    if(!mul_ani_flag){
                        mul_b.style.transform = "rotateX(180deg)";  
                        mul_ani_flag = true;
                    }else{
                        mul_b.style.transform = "rotateX(-180deg)";  
                        mul_ani_flag = false;
                    }
                }
                
                // - ボタン実行時のアニメーション
                sub_ani = function(){
                    if(!sub_ani_flag){
                        sub_b.style.transform = "rotateY(180deg)";
                        sub_ani_flag = true;
                    }else{
                        sub_b.style.transform = "rotateY(-180deg)";
                        sub_ani_flag = false;
                    }
                }
                
                // = ボタン実行時のアニメーション
                eq_ani = function(){
                    if(!eq_ani_flag){
                        eq_line1.style.transform = "rotate(360deg)";
                        eq_line2.style.transform = "rotate(-360deg)";
                        eq_ani_flag = true;
                    }else{
                        eq_line1.style.transform = "rotate(0deg)";
                        eq_line2.style.transform = "rotate(0deg)";
                        eq_ani_flag = false;
                    }
                    
                }
                
                //debug コード
                my_debug = function(){
                    console.log("=====START======");
                    console.log('result: ' + result);
                    console.log('is_operator: ' + is_operator);
                    console.log('current_operator: ' + current_operator);
                    console.log('disp の値: ' + disp.value);
                    console.log('ope_disp の値: ' + ope_disp.innerHTML);
                    console.log("=====END======");
                }
            
                 
        </script>
        
        
    </body>
</html>

最後に

電卓をつくってみて、やっぱりプログラミングって楽しいなーと思いました。^^
世の中の大体のものは、コードで表せるんじゃないかと思います。
例えば、夕飯のカレーをお皿にもってテーブルに出すときは、こんな感じ。

1) 皿を用意
2) カレーライスをこぼれないように皿に盛る(皿の容量とカレーライスの量を計算)
3) テーブルに空きがあるかチェック。ない場合空きスペースを作る
4) 空きができたら、再度皿にカレーが盛られているかをチェック(誰かに食べられている可能性があるため)
5) テーブルに出す

他にも、家から駅に行くときや、だれかから告白されて付き合うかを判断するときも、知らず知らずのうちに for 分や if 分を使っていると思います。
いろいろなことをプログラムにできることは、表現力のひとつなのではないでしょうか。

これからも勉強して、もっと良い、刺激的なコードが書けるように精進したいです。