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

# はじめに

最近 SHARP 製のポケコンを触っているのですが、なんとなくこの記事を書きたくなりました。

# BCD (Binary-coded decimal)
**BCD** は 2 進化 10 進数です。BCD を使うと精度の高い計算を行う事ができます。Delphi ではバージョン 6 以降、**TBCD** というレコードを使った BCD 計算が可能となっています。

 - [二進化十進表現 (Wikipedia)](https://ja.wikipedia.org/wiki/%E4%BA%8C%E9%80%B2%E5%8C%96%E5%8D%81%E9%80%B2%E8%A1%A8%E7%8F%BE)
 - [TBcd (DocWiki)](https://docwiki.embarcadero.com/Libraries/Alexandria/ja/Data.FmtBcd.TBcd)

### ・TBCD.Fraction

2 進化 10 進数の具体的な構造ですが、例えば `12.345` という値を格納した TBCD の各フィールドは次のようになっています。

| フィールド | 値 |
|:---|:---|
| Precision | 6 |
| SignSpecialPlaces | 4 |
| Fraction<br>(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<br>(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 を**ニブル**と呼びます。

 - [ニブル (Wikipedia)](https://ja.wikipedia.org/wiki/%E3%83%8B%E3%83%96%E3%83%AB)

### ・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<br>(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 型で確認してみましょう。

```pascal: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 で計算してみましょう。

```pascal: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 でのテスト (その２)
では、どうすればいいかというと…

```pascal: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
```

もっと長い桁でも…

```pascal: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()](https://docwiki.embarcadero.com/Libraries/ja/Data.FmtBcd.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 のコードをパクって作られている訳ではないので添付も配布も自由です。

 - [uBCD.pas (Delphi Forum)](https://ht-deko.com/delphiforum/?vasthtmlaction=viewtopic&t=1252.0#postid-1778)

### 0 の代入

TBCD 型に `0` を代入した時には、`Precision` / `SignSpecialPlaces` / `Fraction` がすべて 0 で埋められることを期待するかもしれませんが、実際にはそのようにはなりません。

| フィールド | 値 |
|:---|:---|
| Precision | 10 |
| Sign | 0 |
| Special | 0 |
| Places | 2 |
| Fraction<br>(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` を使うか、

```pascal
  var v := NullBcd;
```

TBCD 型の空の定数を別途用意するか、

```pascal
const
  BcdZero: TBcd = ();
begin
  var v := BcdZero;
  ...
```

`Default()` 関数で初期化しましょう。

```pascal
  var v: TBCD := Default(TBCD);
```

バリアント型を使って空にする事もできますが、あまり意味はありません。

```pascal
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:**

 - [BCD サポートルーチン (DocWiki)](https://docwiki.embarcadero.com/RADStudio/ja/BCD_%E3%82%B5%E3%83%9D%E3%83%BC%E3%83%88_%E3%83%AB%E3%83%BC%E3%83%81%E3%83%B3)
 - [TBCDField (DocWiki)](https://docwiki.embarcadero.com/Libraries/ja/Data.DB.TBCDField) <- 高速だが内部 Currency。
 - [TFMTBCDField (DocWiki)](https://docwiki.embarcadero.com/Libraries/ja/Data.DB.TFMTBCDField) <- 内部 TBCD。
 - [12_BCD 値のバイナリデータの 16 進数表示 - 980_数値の 2 進数表示と 16 進数表示 (Mr.XRAY)](http://mrxray.on.coocan.jp/Delphi/plSamples/980_Binary_Hexadecimal.htm#12)
 - [10_Delphi XE2 以降の BCD 演算 - 890_計算誤差 (数値計算の誤差) と多倍長演算 (Mr.XRAY)](http://mrxray.on.coocan.jp/Delphi/plSamples/890_CalcError.htm#10)