金額計算で小数点誤差のない計算を行えるDartのパッケージのrationalの実装を見てみる

by

@wapa5pow

金額計算を行うときにdoubleflootで金額を扱うと小数点誤差が出ることがあります。
浮動小数点数は、内部的に2進数で表現されているため、10進数での計算結果と異なることがあるためです。

例えば以下のようにa=0.1b=0.2とした場合、sumは0.30000000000000004となります。

void main() {
  double a = 0.1;
  double b = 0.2;
  double sum = a + b;
  print(sum);               // 出力: 0.30000000000000004
  print((sum-0.3).floor()); // 出力: 0
  print((sum-0.3).ceil());  // 出力: 1
}

ChatGPTに、この誤差を防ぐためのパッケージを各種プログラミング言語でどうすればいいか聞いたら以下のように教えてくれました。

  • Dart: rational
  • Python: fractions(標準ライブラリ)
  • JavaScript: big.js、bignumber.js、decimal.js、fraction.js
  • Java: Apache Commons Math
  • Ruby: Rational(標準ライブラリ)

今回はDartのrationalパッケージの実装を見ていきます。

rationalパッケージの実装

rational.dartを見ればいいのですが特徴的なところだけ紹介します。

分子・分母でそれぞれBigIntで定義して値を保持する

rationalは英語で有理数という意味です。有理数は整数か分数の形で表せる数の総称です。
パッケージ内のRationalクラスはnumerator(分子)とdenominator(分母)を以下のように定義してフィールドとして持っています。
intで持つと桁数が限られてしまうので、BigIntで持っています。BigIntで持てばメモリが許す限りの桁を保持できます。

  final BigInt numerator;

  final BigInt denominator;

Rational同士の足し算は以下のように定義されています。
小学校などで習う通分そのままに計算しています。

  Rational operator +(Rational other) => Rational(
        numerator * other.denominator + other.numerator * denominator,
        denominator * other.denominator,
      );

約分する

上記のまま掛け算をしていくと分子・分母が大きくなり過ぎてしまうのでコンストラクタでちゃんと約分しています。
gcd(Greatest Common Divisor、最大公約数)は、2つの整数の最大の共通の約数を出すので分子・分母を約数で割ればそれぞれを小さくできます。

  factory Rational(BigInt numerator, [BigInt? denominator]) {
    if (denominator == null) return Rational._fromCanonicalForm(numerator, _i1);
    if (denominator == _i0) {
      throw ArgumentError('zero can not be used as denominator');
    }
    if (numerator == _i0) return Rational._fromCanonicalForm(_i0, _i1);
    if (denominator < _i0) {
      numerator = -numerator;
      denominator = -denominator;
    }
    // TODO(a14n): switch back when https://github.com/dart-lang/sdk/issues/46180 is fixed
    // final gcd = numerator.abs().gcd(denominator.abs());
    final gcd = _gcd(numerator.abs(), denominator.abs());
    return Rational._fromCanonicalForm(numerator ~/ gcd, denominator ~/ gcd);
  }

まとめ

Dartでは浮動小数点誤差をなくすためにはrationalパッケージを使えばいいです。
rationalパッケージは分子・分母という形でそれぞれBigIntで値を保持し、約分しながら各種計算を行なっています。
誤差をなくして安全な金額計算などをこれからもできるといいですね。