33
33

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

システムエンジニアAdvent Calendar 2015

Day 13

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

Last updated at Posted at 2015-12-17

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

さもなくば、お馴染みの丸め誤差が発生します。
image
(@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を使いましょう。

33
33
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
33
33

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?