search
LoginSignup
2

posted at

updated at

【Delphi】とても長い数値の計算 (BCD)

はじめに

最近 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$80XOR すれば符号反転、SignSpecialPlaces$3FAND すれば絶対値となります。

特殊ビット

Special フィールドは通常 0 で、値が格納されていない時に 1 になるようですが、普通に使う分にはこのビットが 1 になる事はない気がします。そもそも誰がこのビットを立てているのでしょうか?

Double でのテスト

まずは精度の問題を Double 型で確認してみましょう。

DoubleTest.dpr
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 で計算してみましょう。

BCDTest.dpr
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)

では、どうすればいいかというと…

BCDTest.dpr
  ...

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

もっと長い桁でも…

BCDTest.dpr
...

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:

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
2