2進法の足し算はとても単純です。10進数との違いは 1 + 1 で繰り上がるということだけです。
0 | 1 | |
---|---|---|
0 | 0 | 1 |
1 | 1 | 10 |
4ビットで正の整数だけ扱うときには 0 から 15 までを表現できました。これを2つ用意して足し算をします。
4ビットでは計算結果が 0 以上 15 以下ならば正しい答が出ます。
それを越すと正しい答えになりません。その様子を観察してください。
内部表現 | 符号なし整数 (十進数) |
|||||
---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | ||
+) | 0 | 0 | 0 | 0 | 0 | |
計算値 | 正しい値 |
内部表現の薄い青の背景色部分のビットをクリックして数値を設定し、[加算実行]で和を求めます。
もう少し大きな数を扱いたいときは8ビットを見てください。
結果の表です。
コンピュータの 内部表現 |
計算値 | 正しい値 |
---|---|---|
0000 | 0 | 0 |
0001 | 1 | 1 |
0010 | 2 | 2 |
0011 | 3 | 3 |
0100 | 4 | 4 |
0101 | 5 | 5 |
0110 | 6 | 6 |
0111 | 7 | 7 |
1000 | 8 | 8 |
1001 | 9 | 9 |
1010 | 10 | 10 |
1011 | 11 | 11 |
1100 | 12 | 12 |
1101 | 13 | 13 |
1110 | 14 | 14 |
1111 | 15 | 15 |
10000 | 0 | 16 |
10001 | 1 | 17 |
10010 | 2 | 18 |
10011 | 3 | 19 |
10100 | 4 | 20 |
10101 | 5 | 21 |
10110 | 6 | 22 |
10111 | 7 | 23 |
11000 | 8 | 24 |
11001 | 9 | 25 |
11010 | 10 | 26 |
11011 | 11 | 27 |
11100 | 12 | 28 |
11101 | 13 | 29 |
11110 | 14 | 30 |
11111 | 15 | 31 |
コンピュータのCPUは32ビット、64ビットなどの桁数の計算を一度に行う演算装置を持っています。2進法1桁が1ビットに相当します。10進法に直すと32ビットでは4294967295、64ビットで18446744073709551615までの整数が扱えますので、これで十分なときには問題ありません。しかし計算結果がこのビット数を越えると正しい答えになりません。このことを理解するためにわざと4ビットしかない演算装置を用意しました。
足し算の結果が限界を超えることをオーバーフローした、溢れた、などといいますが、これを検出して計算を止めたり、複数回の計算をして64ビットの装置を128ビットの装置として使ったり、整数でなく浮動小数点数という形式で数を表して計算するなどの工夫で克服しています。
負の整数の表現を2の補数を使って説明したからには、引き算を足し算で済ますことができるという所まで説明しなければ意味がないと思います。その前置きとしての正の整数の足し算です。実はやっていることは負の整数も正の整数も同じで結果の解釈だけが違うということに気がついて欲しいところです。
コンピュータで数値を扱う時の方式と、正しく扱える範囲を意識することはとても重要です。
8ビットのCPUであった時代には、高級言語で扱う整数は2バイトが普通で、符号なし整数(つまり正の整数のみ)で 0 〜 65535 まで、符号付き整数で -32768 〜 32767 でしたから、それ以上の値になる可能性があるので注意しなければなりませんでした。今は4バイト(32ビット)が普通で、0 〜 4294967295 または、-2147483648 〜 2147483647 ですし、何なら64ビットの整数も普通に使えますから、油断しているかもしれません。
扱う数値が範囲内であっても、計算途中で限界を超えるかもしれません。
物理的な電卓は8桁のものが普通で財務用に12桁のものがありました。このため日常生活でも桁数の限界を感じることがありましたが、スマートホンの電卓アプリでは桁数が多くなると指数表記に切り替わったりで、何桁まで計算できるかを意識しづらくなっています。
Javascriptでは数値はすべて64ビットの浮動小数点数として扱われているそうで、整数の限界を超える実験はできませんが、gccは unsigned int では、4294967295 に +1 すると 0 に戻りますし、Javaのintでは 2147483647 に +1 すると、-2147483648 になって、4ビットで観察したのと同様の現象が起こります。
もちろん、これは計算速度やメモリ容量を考慮して定められているものなので、ハードウェアの性能とプログラミング言語によります。
今回は作業をステップに分けて説明していきます。最終的なものは最後にあります。
まず、0,1をマウスでセットする部分です。説明のためサブセットを作りました。クリックしてみてください
0 | 0 | 0 | 0 | 0 |
htmlでは次のようになっています。0,1に変化する4ビットには onclick= が書かれています。イベントハンドラとよばれるもので、この要素がクリックされたらflipk1()という関数を呼び出しますが、引数にthisを入れると、関数側では引数からクリックされた要素を特定することができます。
<table class="kaisetsu"> <tr id="kaisetsuTr1"> <td></td> <td onclick="flipk1(this)" class="bittd">0</td> <td onclick="flipk1(this)" class="bittd">0</td> <td onclick="flipk1(this)" class="bittd">0</td> <td onclick="flipk1(this)" class="bittd">0</td> <td>0</td> </tr> </table>
関数側では、.getElementById()で得られた要素を指す変数であるかのように使うことができます。
要素の値を調べて、0,1を反転させています。
<script> function flipk1(e){ var bit = e.firstChild.nodeValue; if ( bit == 0 ){ bit = 1; }else{ bit = 0; } e.firstChild.nodeValue = bit; } </script>
ビットの値が変更されたら、4ビット全体での値を計算し、最後の欄に書き加えます。
trにidを設定し、そのtrの中のtdを配列で得て、0,1を読み出し、また書き込む欄もそれで指定します。
0 | 0 | 0 | 0 | 0 |
htmlは省略しますが、ステップ1とほぼ同じでonclick=の値がflipk1からflipk2に変わっているだけです。dispk2();を加えるため、別にしました。
<script> function flipk2(e){ var bit = e.firstChild.nodeValue; if ( bit == 0 ){ bit = 1; }else{ bit = 0; } e.firstChild.nodeValue = bit; dispk2(); } var bitsk2 = 4; function dispk2(){ var tgt = document.getElementById("kaisetsuTr2"); var tgttd = tgt.getElementsByTagName("td"); var bstr = ""; for (var i=1;bitsk2>=i;i++){ bstr += tgttd[i].firstChild.nodeValue; } var na = parseInt(bstr,2); tgttd[bitsk2+1].firstChild.nodeValue = na.toString(10); } </script>
グローバル変数としてbitsk2を宣言し値を4にしています。現代的なjavascriptとして推奨されるものかは、ちゃんと調べる必要を感じていますが、筋は通っているし、うまく動きます。本番ではdispk2()だけなく加算実行にも4ビットだと伝えなくてはなりません。8ビットのページを作る時にはここだけを変えようという魂胆です。
tgttdがtd要素を指す配列。0番は+)などを入れる都合で使用していて1から4がビット。for内で1つずつ読んで文字列として連結し、bstrに格納しています。
parseInt()で2進法の文字列として整数に変換します。引数の2が2進法を示します。
.toString(10)で10進法の文字列に変換してtgttd[5]に格納します。10は10進法です。
解説のプログラムは以上ですが、本番のプログラムでは、2つの数についてこれを行ないます。また、加算結果の欄もクリアしています。本当はクリックされた方の数だけ表示し直すので良いし、加算結果をクリアするのは1回でよいのですが、1ビット変更するたびに全部繰り返しています。必要か不必要かの判断の手間・手数と余計なことをする手数の量を比較して無駄でも全部書きなおすことにしました。
[乱数の発生]のボタンを押すと、足し算をする代わりに乱数を発生させて、同様に書き込んでみます。
2進法4桁で表現できる数は0〜15なので、加えると0〜30。2進法では1桁から5桁の数字です。末尾の4桁を書き出したいのですが、不足の時は0を補う必要があります。新しいJavascriptの規格ではぴったりな関数があるのですが、全部のブラウザで動くようになっていません。"000"を前に連結して後ろ4桁を切り出します。
切り出した文字列を1桁ずつ書き、10進法で「末尾4桁」の欄に書き、切り出す前の値を「正しい値」の欄に書きます。
正しい値にならないときは、背景色を薄い赤にします。
"000 "+乱数 | 10進表現 | |||||
末尾 4桁 | 正しい値 | |||||
0,1,30といった数値が出にくいので専用ボタンを追加しました。桁数が変わっても末尾4桁を切り出すのがよく解ると思います。
<script> var na,nb; function doaddk3n(nn){ na = nn; nb = 0; doaddk3(); } function rand4doaddk3(){ na = Math.floor(Math.random()*16); //k3追加 nb = Math.floor(Math.random()*16); //k3追加 doaddk3(); } function doaddk3(){ var nsum = na + nb; var bstrtd = document.getElementById("bstr"); //k3追加 var tgt = document.getElementById("kaisetsuTr3"); var tgttd = tgt.getElementsByTagName("td"); bstrtd.firstChild.nodeValue = "000 " + nsum.toString(2); //k3追加 var bstr = "000" + nsum.toString(2); bstr = bstr.substring(bstr.length-bits); for (var i=1;bits>=i;i++){ tgttd[i].firstChild.nodeValue = bstr.substring(i-1,i); } var bgcolor = "#ffffff"; var nc = parseInt(bstr,2); if (nsum != nc){ bgcolor = "#ffcccc"; } tgttd[bits+1].firstChild.nodeValue = nc.toString(10); tgttd[bits+1].style.backgroundColor = bgcolor; tgttd[bits+2].firstChild.nodeValue = nsum.toString(10); } </script>
tdタグに onclick="flip(this)" などのイベントハンドラを加える方法です。
今回はonclickが必要な全てのtdの内容が同じなので、コピーすればよいといえばそれまでですが、8ビットにしたいなどの変更には有効です。
この例では5ビットにしています。
[ON]のクリックで0,1の反転ができるようになります。OFFでストップします。本番部分ではクリックではなく、onloadでセットされるようになっています。
0 | 0 | 0 | 0 | 0 | |
<Script> var bitsk4 = 5; //ONで呼ばれる function setlistnerk4(){ var tgt = document.getElementById("kaisetsuTr4"); var tgttd = tgt.getElementsByTagName("td"); for (var i=1;bitsk4>=i;i++){ tgttd[i].onclick = function(){flipk1(this);}; tgttd[i].className = "bittd"; } } //OFFで呼ばれる function clearlistnerk4(){ var tgt = document.getElementById("kaisetsuTr4"); var tgttd = tgt.getElementsByTagName("td"); for (var i=1;bitsk4>=i;i++){ tgttd[i].onclick = ""; tgttd[i].className = ""; } } </script>
イベントハンドラで設定されるのはステップ1のflipk1(this)です。
最近ではaddEventListener()というメソッドが使えるようですが、ここで示した方法は古いIEを含んでいろいろなブラウザで広く使えたものです。
イベントハンドラの取り付け、新しい言い方ではイベントリスナーの取り付け方法については、後日改めて吟味します。
もうひとつ、forを使ってのイベントハンドラの取り付けには、Javascript特有の仕組みがあります。ここではとりあえずこれでできるというところに止め、これも後日吟味します。
さらにもうひとつ、イベントハンドラを無効にする関数をとりあえず書きましたが、これはここだけの間に合わせです。
<script> var na=0; var nb=0; var bits = 4; function flip(e){ var bit = e.firstChild.nodeValue; if ( bit == 0 ){ bit = 1; }else{ bit = 0; } e.firstChild.nodeValue = bit; disp(); } function disp(){ var tgt = document.getElementById("cala"); var tgttd = tgt.getElementsByTagName("td"); var bstr = ""; for (var i=1;bits>=i;i++){ bstr += tgttd[i].firstChild.nodeValue; } na = parseInt(bstr,2); tgttd[bits+1].firstChild.nodeValue = na.toString(10); tgt = document.getElementById("calb"); tgttd = tgt.getElementsByTagName("td"); bstr = ""; for (var i=1;bits>=i;i++){ bstr += tgttd[i].firstChild.nodeValue; } nb = parseInt(bstr,2); tgttd[bits+1].firstChild.nodeValue = nb.toString(10); tgt = document.getElementById("calc"); tgttd = tgt.getElementsByTagName("td"); bstr = ""; for (var i=1;tgttd.length>i;i++){ tgttd[i].firstChild.nodeValue = " "; tgttd[bits+1].style.backgroundColor = "#ffffff"; } } function doadd(){ var nsum = na + nb; var tgt = document.getElementById("calc"); var tgttd = tgt.getElementsByTagName("td"); var bstr = "000" + nsum.toString(2); bstr = bstr.substring(bstr.length-bits); for (var i=1;bits>=i;i++){ tgttd[i].firstChild.nodeValue = bstr.substring(i-1,i); } var bgcolor = "#ffffff"; var nc = parseInt(bstr,2); if (nsum != nc){ bgcolor = "#ffcccc"; } tgttd[bits+1].firstChild.nodeValue = nc.toString(10); tgttd[bits+1].style.backgroundColor = bgcolor; tgttd[bits+2].firstChild.nodeValue = nsum.toString(10); } window.onload = function() { setlistner(); } function setlistner(){ var tgt = document.getElementById("cala"); var tgttd = tgt.getElementsByTagName("td"); for (var i=1;bits>=i;i++){ tgttd[i].onclick = function(){flip(this);} tgttd[i].className = "bittd"; } tgt = document.getElementById("calb"); tgttd = tgt.getElementsByTagName("td"); for (var i=1;bits>=i;i++){ tgttd[i].onclick = function(){flip(this);} tgttd[i].className = "bittd"; } tgt = document.getElementById("calc"); tgttd = tgt.getElementsByTagName("td"); for (var i=1;bits>=i;i++){ tgttd[i].className = "anstd"; } } </script>
<table id="addtbl"> <tr> <th></th> <th colspan="4">内部表現</th> <th>符号なし整数<br>(十進数)</th> <th></th> </tr> <tr id="cala"> <td></td> <td>0</td> <td>0</td> <td>0</td> <td>0</td> <td>0</td> <td> </td> </tr> <tr id="calb"> <td>+)</td> <td>0</td> <td>0</td> <td>0</td> <td>0</td> <td>0</td> <td> </td> </tr> <tr id="calc"> <td> </td> <td> </td> <td> </td> <td> </td> <td> </td> <td> </td> <td> </td> </tr> <tr> <td> </td> <td class="incdec" colspan="4"> <button type="button" onclick="doadd()">加算実行</button> </td> <td class="info">計算値</td> <td class="info">正しい値</td> </tr> </table>
firstChild.nodeValueで値を入れる要素にはあらかじめテキストが入っている必要があります。テキストノードがfirstChildになるからです。ここでは0または半角スペースが入っています。不用意に削除してはいけません。
#addtbl{ margin:1% 2%; border:none; border-collapse:collapse; } #addtbl td, #addtbl th{ border:none; } #cala td, #calb td, #calc td{ font-family:courier,monospace; font-size:200%; text-align :right; padding:2px 8px; height:1.5em; } td.incdec{ padding:16px 2px 1px 2px; text-align :center; } td.info{ padding-left:1em; text-align :right; } #addtbl td.bittd{ cursor:pointer; border:solid 1px #999999; background-color:#f0f0ff; } #addtbl td.anstd{ border:solid 1px #999999; } #addtbl tr#calc{ border-top:solid 1px #999999; } button{ cursor:pointer; min-width:5em; }
function doadd(){ var nsum = na + nb; var tgt = document.getElementById("calc"); var tgttd = tgt.getElementsByTagName("td"); var bstr = "000" + nsum.toString(2); bstr = bstr.substring(bstr.length-bits); //tgttd[0].firstChild.nodeValue = bstr; for (var i=1;bits>=i;i++){ tgttd[i].firstChild.nodeValue = bstr.substring(i-1,i); } var nc = parseInt(bstr,2); var sdnc = sdnx(nc,tgttd[1].firstChild.nodeValue); var truthsum = parseInt(sdna,10)+parseInt(sdnb,10); var bgcolor = "#ffffff"; if (sdnc != truthsum){ bgcolor = "#ffcccc"; } tgttd[bits+1].firstChild.nodeValue = sdnc.toString(10); tgttd[bits+1].style.backgroundColor = bgcolor; tgttd[bits+2].firstChild.nodeValue = truthsum.toString(10); }