通貨計算における「浮動小数点誤差」の回避:Currency型とDecimal型の正しい選択
業務自動化の現場において、Excel VBAで金融系ツールや厳密な単価計算、税率の端数処理を行うシステムを構築する際、もっとも恐れべきは「目に見えない数理の罠」である。
「なぜ、100円 × 1.1 × 10 が 1100.0000000000002 になるのか?」
「金額の突き合わせで、人間の目には合っているのに、VBAのIF判定で `False` が返されるのはなぜか?」
もし君が、金額を扱う変数に `Double` 型や `Single` 型を使っているなら、今すぐそのコードを止めてほしい。それは時限爆弾を抱えたまま稼働しているようなものだ。
今回は、浮動小数点演算のメカニズムの本質を突き、実務で絶対にバグを踏まないための「Currency型とDecimal型」の正しい選択と堅牢な設計手法を、チーフアーキテクトの視点から授けよう。
—
1. なぜ `Double` 型は金融計算で使ってはならないのか
コンピュータは、人間が使う「10進数」をそのまま理解できない。内部ではすべて「2進数(0と1)」で処理している。
ここに根本的な問題がある。10進数の小数(例: `0.1`)は、2進数に変換すると「無限小数」になってしまうのだ。
有限のメモリ領域に収めるためにどこかで丸めざるを得なくなり、その結果として蓄積されるのが「浮動小数点誤差(Rounding Error)」である。
実務で起こる致命傷
- 請求書の合計金額と、内訳の合計が「0.01円」ズレる。
- データベース(SQL Server等)との突合クエリで、数値の一致条件(`WHERE 金額 = 1000`)から漏れる。
- 消費税の端数処理(四捨五入・切り捨て)で、本来切り捨てられるべき値が繰り上がってしまう。
監査法人や経理部門を相手にするシステムにおいて、「コンピュータの都合で誤差が出ました」言い訳は絶対に通用しない。
—
2. VBAにおける「正確な数値型」の二大巨頭:Currency と Decimal
VBAで誤差を排した計算を行うためのカードは、実質的に以下の2つに絞られる。それぞれの特性をアーキテクトの視点で正確に把握してほしい。
① `Currency` 型(推奨・王道の選択)
- データ型構造: 8バイトの整数型(内部で10,000倍して保持する固定小数点型)
- 有効桁数: 整数部 15桁、小数部 4桁(固定)
- メリット:
- 処理速度が非常に高速(整数演算と同等)。
- VBAのネイティブデータ型であるため、宣言してそのまま計算に組み込める。
- 通常の日本円の計算(小数第2位〜第4位までの処理)であれば、これで95%の要件が満たせる。
② `CDec`(`Decimal` 型)(高度な選択)
- データ型構造: 14バイトのバリアントsubtype
- 有効桁数: 最大28〜29桁(小数点位置可変)
- メリット:
- 圧倒的な精度。仮想通貨の微小な単価計算や、海外通貨の多桁計算など、小数第4位を超える精度が必要な場合に不可欠。
- デメリット:
- 変数宣言として直接 `Dim x As Decimal` が使えず、`Dim x As Variant` の後に `CDec()` でラップする必要がある。
- `Double` に比べて演算速度が落ちる(とはいえ通常の業務ツールレベルでは体感できない範囲だが)。
—
3. 【実践】プロダクションコード:堅牢な消費税・端数処理エンジン
それでは、実務の現場でそのままコピペして使える、誤差を完全に排除した堅牢なモジュールを提示しよう。
ここでは、日本国内の商慣習である「外税計算(消費税10%・切り捨て)」を `Currency` 型を用いて安全に処理する例を示す。
Option Explicit
‘ =================================================================00審
‘ モジュール名: mFinancialCalculator
‘ 概要: 浮動小数点誤差を完全に排除した堅牢な金額計算・端数処理ライブラリ
‘ 著者: チーフアーキテクト
‘ =================================================================
Public Sub ExecuteBillingCalculationSample()
On Error GoTo ErrorHandler
‘ 【重要】金額を扱う変数は絶対に Double や Single を使わず Currency を選定する
Dim unitPrice As Currency
Dim quantity As Long
Dim subTotal As Currency
Dim taxAmount As Currency
Dim grandTotal As Currency
‘ サンプルデータの設定
unitPrice = 1280@ ‘@サフィックスによりCurrency型としてリテラル代入
quantity = 3
‘ 1. 小数点を含まない乗算はCurrency型であれば誤差は一切生じない
subTotal = unitPrice quantity
‘ 2. 消費税計算(10%): 割り算が入るため、厳密な端数処理関数を挟む
‘ 誤った例: taxAmount = subTotal 0.1 (これでもCurrencyならDoubleよりマシだが端数丸めが曖昧になる)
taxAmount = CalculateTax(subTotal, 0.1, “Floor”)
‘ 3. 総合計の算出
grandTotal = subTotal + taxAmount
‘ 結果のイミディエイト出力(確認用)
Debug.Print “小計: ” & Format$(subTotal, “#,
0″) & ” 円”
Debug.Print “消費税: ” & Format$(taxAmount, “#,
0″) & ” 円”
Debug.Print “合計: ” & Format$(grandTotal, “#,
0″) & ” 円”
Exit Sub
ErrorHandler:
MsgBox “予期せぬエラーが発生しました: ” & Err.Description, vbCritical
End Sub
‘ =================================================================
‘ 厳密な端数処理を行う税額計算関数
‘ @param targetAmount 対象金額 (Currency)
‘ @param taxRate 税率 (Doubleで受けても内部でCurrency変換して安全に処理)
‘ @param roundMode 丸めモード (“Floor”=切り捨て, “Ceiling”=切り上げ, “Round”=四捨五入)
‘ @return 計算された税額 (Currency)
‘ =================================================================
Public Function CalculateTax(ByVal targetAmount As Currency, ByVal taxRate As Double, ByVal roundMode As String) As Currency
Dim rawTax As Currency
‘ 税率を掛ける(Currency同士の乗算にするためCDecまたはCurrency化)
rawTax = targetAmount CCur(taxRate)
Select Case UCase$(roundMode)
Case “FLOOR”
‘ 切り捨て(VBAのInt関数は負数で挙動が異なるため、正の金額前提のIntまたはFixを使用)
CalculateTax = Fix(rawTax)
Case “CEILING”
‘ 切り上げ
CalculateTax = -Int(-rawTax)
Case “ROUND”
‘ 四捨五入(銀行丸めではなく一般的な四捨五入とする場合)
CalculateTax = Int(rawTax + 0.5)
Case Else
‘ デフォルトは切り捨て
CalculateTax = Fix(rawTax)
End Select
End Function
コードの設計ポイント
1. リテラルサフィックス `@` の活用:
VBAでは `1280` と書くとIntegerやLongとみなされ、意図せぬ型推論の罠に嵌ることがある。`1280@` と記述することで、VBAコンパイラに対して「これは最初からCurrency型である」と明示的に伝えることができる。
2. 端数処理のブラックボックス化:
税計算や比率計算で発生する小数は、必ず専用の関数(`Fix`, `Int`)を通し、場当たり的な四捨五入をコード内に散在させない。これにより仕様変更(例:「端数はすべて切り上げに変更」)があった際も1箇所の修正で対応できる。
—
4. データベース(SQL Server / Access)や外部ファイル連携時の注意点
堅牢なVBAコードを書いても、データを書き出す先や読み込む先で浮動小数点の罠を踏んでは意味がない。
① データベース(RDB)側の型マッピング
- SQL Server: `MONEY` 型、あるいは `DECIMAL(18, 4)` 型を採用すること。`FLOAT` や `REAL` 型を金額カラムに指定するのは絶対にNG。
- Access (ACEエンジン): `通貨型 (Currency)` または `マネー型` を選択する。
VBAの `Currency` 型は、Accessの「通貨型」やSQL Serverの「MONEY型」と完全なバイナリ互換を持つ。ADO経由でレコードセットに書き込む際も、型の変換ロスや誤差が発生しないため、このパイプラインを維持するのがベストプラクティスである。
② Excelのワークシートセルとのやり取り
Excelのセル(Range)は、内部的にすべて `Double` 型の浮動小数点として値を保持している。
そのため、セルから値を取得して変数に代入する瞬間、およびセルに書き戻す瞬間に注意が必要だ。
‘ セルから値を取得する際は、明示的に CCur でキャストして取り込む
Dim unitPrice As Currency
unitPrice = CCur(Range(“A1”).Value)
このワンクッション(`CCur` による型強制)を入れることで、セルの浮動小数点データがVBAの安全な固定小数点エンジンへと安全にコンバートされる。
—
5. アーキテクトからの提言:甘い型設計がプロジェクトを破綻させる
「動けばいいや」と `Double` 型のまま放置された自動化ツールは、運用開始から数ヶ月後、データ量が膨らんだときや、税率改定・端数処理の厳密な監査が入ったときに必ず破綻する。そして、その不具合の調査と修正にかかるコストは、最初から正しい型設計をしておくコストの何十倍にも膨れ上がる。
- 金額・数量・単価には `Currency` 型 を使う。
- 超高精度な計算が必要な場合は `Decimal` 型(`CDec`) を召喚する。
- セルや外部DBとの境界線では、必ず明示的な型変換(`CCur`)を行う。
この鉄則をチーム全体の共通認識とし、美しく、そして絶対に狂わない業務システムを構築してほしい。
