LoginSignup
8
2

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-12-08

はじめに

最近 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:

8
2
0

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
  3. You can use dark theme
What you can do with signing up
8
2