はじめに
最近 SHARP 製のポケコンを触っているのですが、なんとなくこの記事を書きたくなりました。
BCD (Binary-coded decimal)
BCD は 2 進化 10 進数です。BCD を使うと精度の高い計算を行う事ができます。Delphi ではバージョン 6 以降、TBCD というレコードを使った BCD 計算が可能となっています。
・TBCD.Fraction
2 進化 10 進数の具体的な構造ですが、例えば 12.345
という値を格納した TBCD の各フィールドは次のようになっています。
フィールド | 値 |
---|---|
Precision | 6 |
SignSpecialPlaces | 4 |
Fraction (10進値) |
[18][52][80][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0] |
Fraction の値を 16 進数にすると解り易くなります。
フィールド | 値 |
---|---|
Precision | 6 |
SignSpecialPlaces | 4 |
Fraction (16進値) |
[12][34][50][00][00][00][00][00][00][00][00][00][00][00][00][00][00][00][00][00][00][00][00][00][00][00][00][00][00][00][00][00] |
1 バイトの上位桁 (4bit) と下位桁 (4bit) それぞれで 10 進値の 1 桁を表しています。この 4 bit をニブルと呼びます。
・TBCD.Precision
Precision
はその値を格納するのに必要な桁数です。Fraction
はバイトの配列なので 12345
のような 5 桁であっても、3 バイト分…つまり 6 ニブルが必要です。
・TBCD.SignSpecialPlaces
これは 3 つのビットフィールドから成ります。
フィールド | ビット | 説明 |
---|---|---|
Sign | bit 7 | 符号ビット。正の時 0、負の時 1 |
Special | bit 6 | 特殊ビット。値が格納されていない時に 1 (?) |
Places | bit 5..0 | 小数点以下の位置 |
12.345
は次のように格納されます。
フィールド | 値 |
---|---|
Precision | 6 |
Sign | 0 |
Special | 0 |
Places | 4 |
Fraction (16進値) |
[12][34][50][00][00][00][00][00][00][00][00][00][00][00][00][00][00][00][00][00][00][00][00][00][00][00][00][00][00][00][00][00] |
Precision
が 6 なので、6 ニブル分を Fraction
から取得します。
123450
Places
は 4 なので、下位桁から 4 桁の位置に小数点がきて 12.3450
となり、12.345
を表す事ができています。
符号ビット
Sign
フィールドは符号を表していますので、SignSpecialPlaces
と $80
を XOR すれば符号反転、SignSpecialPlaces
と $3F
を AND すれば絶対値となります。
特殊ビット
Special
フィールドは通常 0
で、値が格納されていない時に 1
になるようですが、普通に使う分にはこのビットが 1
になる事はない気がします。そもそも誰がこのビットを立てているのでしょうか?
Double でのテスト
まずは精度の問題を Double 型で確認してみましょう。
program DoubleTest;
{$APPTYPE CONSOLE}
uses
System.SysUtils;
procedure ShowDouble(d: Double);
begin
Writeln('Value: ' , d:1:20);
end;
begin
var v: Double := 1.234567890123456789;
ShowDouble(v);
v := v + 0.765432109876543211;
ShowDouble(v);
end.
この結果は次の通りです。Double 型の有効桁数の関係でこうなります。
Value: 1.23456789012345669000
Value: 2.00000000000000000000
TBCD でのテスト
では、BCD で計算してみましょう。
program BCDTest;
{$APPTYPE CONSOLE}
uses
System.SysUtils, Data.FmtBcd;
procedure ShowBCD(Bcd: TBCD);
begin
Writeln('Value: ' , String(Bcd));
Writeln('Precision: ', Bcd.Precision);
Writeln('Sign: ' , Bcd.SignSpecialPlaces shr 7);
Writeln('Special: ' , Bcd.SignSpecialPlaces and $7F shr 6);
Writeln('Places: ' , Bcd.SignSpecialPlaces and $3F);
Write('Fraction: ');
for var i:=Low(Bcd.Fraction) to High(Bcd.Fraction) do
Write(Bcd.Fraction[i].ToHexString(2));
Writeln; Writeln;
end;
begin
var v: TBCD := 1.234567890123456789;
ShowBCD(v);
v := v + 0.765432109876543211;
ShowBCD(v);
end.
この結果は次の通りです。精度はあまり変わっていないように見えますね。
Value: 1.23456789012346
Precision: 16
Sign: 0
Special: 0
Places: 15
Fraction: 1234567890123460000000000000000000000000000000000000000000000000
Value: 2.000000000000003
Precision: 16
Sign: 0
Special: 0
Places: 15
Fraction: 2000000000000003000000000000000000000000000000000000000000000000
これは、TBCD に代入する際に Double からの変換が入っているからです。Double の精度を超える値は代入できないのでしょうか?
TBCD でのテスト (その2)
では、どうすればいいかというと…
...
begin
var v: TBCD := '1.234567890123456789';
ShowBCD(v);
v := v + '0.765432109876543211';
ShowBCD(v);
Readln;
end.
文字列で代入すればよかったりします。
Value: 1.234567890123456789
Precision: 20
Sign: 0
Special: 0
Places: 19
Fraction: 1234567890123456789000000000000000000000000000000000000000000000
Value: 2
Precision: 20
Sign: 0
Special: 0
Places: 19
Fraction: 2000000000000000000000000000000000000000000000000000000000000000
もっと長い桁でも…
...
begin
var v: TBCD := '1.234567890123456789012345678901234567890';
ShowBCD(v);
v := v + '0.000000000000000000000000000000000000001';
ShowBCD(v);
Readln;
end.
大丈夫です!
Value: 1.23456789012345678901234567890123456789
Precision: 40
Sign: 0
Special: 0
Places: 39
Fraction: 1234567890123456789012345678901234567890000000000000000000000000
Value: 1.234567890123456789012345678901234567891
Precision: 40
Sign: 0
Special: 0
Places: 39
Fraction: 1234567890123456789012345678901234567891000000000000000000000000
DocWiki には「VarFMTBcdCreate() を使え」とありますが、普通に文字列を使った方がスッキリとしたコードが書けると思います。わざわざバリアント型を経由させる意味がよく解らないのですが…。
おわりに
ポケコンの BCD
何故ポケコンを触っていたら BCD の記事を書きたくなったのかというと、ポケコンの BASIC の数値データが BCD で格納されているからです。技術資料を見ていたら、数値データの内部構造が解説されていました。数値は SC61860 のポケコンだと 8 バイト 16 ニブルで格納されています。
ニブル | フィールド | 説明 |
---|---|---|
0..2 | 指数部 | 10 進数 2 桁で表される。先頭桁が 0 なら正。負数は補数で表される。 |
3 | 仮数部符号 | 負なら 8。正なら 0。 |
4..13 | 仮数部 | 10 桁の BCD。 |
14..15 | 演算用補正 | 演算時のみ使われ、通常は 0。 |
XE 以前の TBCD
Delphi XE2 以降だと BCD 演算を簡単に行えるよう、TBCD レコードに対する拡張が施されています。具体的には演算子のオーバーロードによる演算子での計算が行えます。
XE 以前ではサポートルーチンを使って BCD 演算を行う必要があるため、BCD 演算がやりにくいのですが、TBCDEx (uBCD.pas) というのがあり、これを使えば Delphi 2006~ XE においても XE2 以降のコードと同等のコードを書く事ができます。TBCD のコードをパクって作られている訳ではないので添付も配布も自由です。
0 の代入
TBCD 型に 0
を代入した時には、Precision
/ SignSpecialPlaces
/ Fraction
がすべて 0 で埋められることを期待するかもしれませんが、実際にはそのようにはなりません。
フィールド | 値 |
---|---|
Precision | 10 |
Sign | 0 |
Special | 0 |
Places | 2 |
Fraction (16進値) |
[00][00][00][00][00][00][00][00][00][00][00][00][00][00][00][00][00][00][00][00][00][00][00][00][00][00][00][00][00][00][00][00] |
実数の 00000000.00
が格納されています。これは本来の用途 (データベースのフィールド) のための処置だと考えられます。なお、1
を代入した時は 1.0
が格納されます。
なんらかの事情で、フィールドをすべて 0 で埋めたい (nil 値として使いたい) のなら、素直に NullBcd
を使うか、
var v := NullBcd;
TBCD 型の空の定数を別途用意するか、
const
BcdZero: TBcd = ();
begin
var v := BcdZero;
...
Default()
関数で初期化しましょう。
var v: TBCD := Default(TBCD);
バリアント型を使って空にする事もできますが、あまり意味はありません。
v := VarToBcd(VarFMTBcdCreate);
数えられない!
Delphi の TBCD は 64 ニブル、つまり 64 桁を格納できるので、自然数なら最大で
9999那由他9999阿僧祇9999恒河沙9999極9999載9999正9999澗9999溝9999穣9999𥝱9999垓9999京9999兆9999億9999万9千9百9十9
までを表せることになりますね。
See also: