もう一つの金額計算のやり方


Javaで金額を扱う場合、「BigDecimalを使え」というのが定石になっています。

さもなくば、お馴染みの丸め誤差が発生します。

(@miyakawatakuさんの「金勘定のためのBigDecimalそしてMoney and Currency API」より)

しかし、

new BigDecimal(0.1)

みたいな初期化されると元も子もありませんし、複数回丸め処理を通っちゃうと、やはり誤差を産む可能性があります。

他によい案はないものでしょうか…

Ratio型

たいていのLispは分数型を持っているそうです。調べてみます。

Clojure
user=> (/ 22 7)
22/7
CommonLisp
[1]> (/ 22 7)
22/7
gauche
gosh> (/ 22 7)
22/7
MatzLisp
irb(main):001:0> 22r / 7
=> (22/7)

どれも、割り算は分数として扱います。したがって、丸め処理は計算途中では気にする必要がなく、最後の最後で1回やればよいだけです。

Clojure
user=> (* 1000 (- 1 (/ 7 100)))
930N

???「お金をあつかうならLispだなっ!!」

JavaでのRatioの実装

Ratio型の実装は簡単です。

public class Ratio {
    public long numerator;
    public long denominator;

    public Ratio(long numerator, long denominator) {
        this.numerator = numerator;
        this.denominator = denominator;
        reduce();
    }

    public Ratio plus(Ratio x) {
        if (x.denominator == this.denominator) {
            this.numerator += x.numerator;
        } else {
            long d = this.denominator * x.denominator;
            this.numerator = this.numerator * x.denominator + x.numerator * this.denominator;
            this.denominator = d;
        }

        reduce();
        return this;
    }

    public Ratio minus(Ratio x) {
        if (x.denominator == this.denominator) {
            this.numerator -= x.numerator;
        } else {
            long d = this.denominator * x.denominator;
            this.numerator = this.numerator * x.denominator - x.numerator * this.denominator;
            this.denominator = d;
        }
        reduce();
        return this;
    }

    public Ratio multiply(Ratio x) {
        this.numerator *= x.numerator;
        this.denominator *= x.denominator;
        reduce();
        return this;
    }

    public Ratio devide(Ratio x) {
        this.numerator *= x.denominator;
        this.denominator *= x.numerator;
        reduce();
        return this;
    }

    public long quotient() {
        return numerator / denominator;
    }

    public Ratio remainder() {
        return new Ratio(numerator % denominator, denominator);
    }

    @Override
    public boolean equals(Object anothor) {
        return anothor != null
                && anothor instanceof Ratio
                && ((Ratio) anothor).numerator == numerator
                && ((Ratio) anothor).denominator == denominator;

    }

    private void reduce() {
        long gcd = calcGcd(numerator, denominator);
        numerator /= gcd;
        denominator /= gcd;
    }

    private long calcGcd(long a, long b) {
        if (b == 0) return a;
        return calcGcd(b, a%b);
    }
}

(実際使う際には、Number型を継承したり、Comparableを実装した方がよいと思います)

下に示すデメリットに該当する場合は、numeratordenominatorの型をBigIntegerにしてください。

デメリット

Exampleのコードは毎回約分していますが、くりかえし計算する場合、約分のオーバヘッドがちょっとだけかかります。さらに複利計算のように同じ利率を掛ける場合、分子・分母が大きな数字になってメモリを喰う場合があります…(が、通常業務の金額計算では特に気にすることにはならないかと思います)

メリット

前述のデメリットを乗り越えられるならば、計算過程での誤差を気にする必要がなくなるので、安心して使えるようになります。

まとめ

BigDecimalを使っても問題起こしそうなプロジェクトは、分数型をベースとした金額型を作ることを検討するとよいのではないでしょうか。
さもなくば、Lispを使いましょう。