# 【Delphi】とても長い数値の計算 (BCD) --- tags: Delphi programming Pascal embarcadero objectpascal created_at: 2022-12-08 updated_at: 2022-12-09 --- # はじめに 最近 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
(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 を**ニブル**と呼びます。 - [ニブル (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
(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 でのテスト (その2) では、どうすればいいかというと… ```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
(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)