57
金勘定のためのBigDecimal そしてMoney and Currency API 2015-06-11 Java女子部勉強会 ハッシュタグ: #javajo 宮川 拓

金勘定のためのBigDecimalそしてMoney and Currency API

Embed Size (px)

Citation preview

金勘定のためのBigDecimal

そしてMoney and Currency API

2015-06-11 Java女子部勉強会

ハッシュタグ: #javajo

宮川 拓

@miyakawa_taku

JJUG幹事

SI屋で賃労働

オレオレJVM言語Kinkを作っています

https://bitbucket.org/kink/kink

尾上部屋の里山関のファンです

自己紹介 #javajo

2/57

みたいな金勘定を考えます

ペンキ赤 321円 × 1.3リットル = 417.3円

ペンキ青 432円 × 2.2リットル = 950.4円

ペンキ緑 543円 × 1.6リットル = 868.8円

小計(端数切り捨て) 2,236円

消費税(四捨五入) 179円

合計 2,415円

テーマ: 金勘定

小数の

演算

端数

処理

#javajo

3/57

セッション内容

double/floatで金勘定してはいけない

BigDecimalで金勘定しよう

先取りMoney and Currency API

#javajo

4/57

double/floatは固定精度の

2進浮動小数点数型です

10進小数が正確に表せないので、

金勘定には使えません

基本的には統計・科学技術計算のための

データ型です

double/float

「精度」

=「有効桁数」

#javajo

5/57

ありがちな言い訳

キリの良い金額しか使わないし……

誤差が出るような計算しないし……

精度足りてるからdouble/floatで大丈夫!

ナーンセンス!

double/float #javajo

6/57

double/floatではダメな理由

金額の10進小数

double/float

2進の浮動小数点数型

近似値に

丸められてしまう

以下、なぜこうなるのか説明します

#javajo

7/57

10進小数は

各桁に10nを掛けた数の和を表します

たとえば10進の12.625:

そもそも10進小数とは?

1 2 . 6 2 5× × × × ×

10(101)

1(100)

1/10(10-1)

1/100(10-2)

1/1000(10-3)

#javajo

8/57

同じように2進小数は

各桁に2nを掛けた数の和を表します

たとえば2進の1100.101:

では2進小数とは?

1 1 0 0 . 1 0 1× × × × × × ×

8(23)

4(22)

2(21)

1(20)

1/2(2-1)

1/4(2-2)

1/8(2-3)

#javajo

9/57

10進小数→2進小数

10進小数でピッタリ表せる値を

2進小数で表そうとすると、一般に

無限に桁が続く循環小数になります

0.1 = 0.0 0011 0011 0011 0011 0011 00...

0.2 = 0. 0011 0011 0011 0011 0011 001...

0.3 = 0.0 1001 1001 1001 1001 1001 10...

#javajo

10/57

double/floatは固定精度、

つまり有効桁数は有限です

floatの32ビットの中身:

double/floatの桁数は有限

01000000110010100000000000000000

符号

0:正 1:負

指数

小数点の場所

仮数 2進の桁

桁数は固定で有限

#javajo

11/57

#javajo10進小数→double/float

無限の桁を有限桁に押し込もうとすると

10進小数 0.3

2進小数(無限桁数)

0.01001100110011...

double/float(有限桁数)

0.010011001...110011

近似値に丸められる

12/57

double/floatで金勘定すると

何が起きるのでしょうか?

#javajo

13/57

double/floatで金勘定すると...

int 定価 = 1000;

double 値引率 = 0.07;

int 売値 = (int) (定価 * (1 - 値引率));

System.out.println(売値);

😬

#javajo

?929

14/57

double/floatまとめ

double/floatで金勘定してはいけません

例外:

集計・サマリ

証券のリスク計算

そのほか正確な金額がいらない場合

正確な金勘定をするためには

BigDecimalクラスを使いましょう

#javajo

15/57

セッション内容

double/floatで金勘定してはいけない

BigDecimalで金勘定しよう

先取りMoney and Currency API

#javajo

16/57

BigDecimal

BigDecimal: 10進浮動小数点数

10進小数が正確に表せます

金勘定に使えます

#javajo

17/57

BigDecimalで金勘定

int 定価 = 1000;

BigDecimal 値引率 = new BigDecimal("0.07");

int 売値 = BigDecimal.ONE.subtract(値引率)

.multiply(BigDecimal.valueOf(定価))

.setScale(0, RoundingMode.FLOOR)

.intValue();

System.out.println(売値);

930 😄

#javajo

18/57

BigDecimalの構成

BigDecimal: (整数値, スケール)

整数値任意桁数の10進の桁を表す

BigInteger値

スケール小数点を右端からいくつ左に

動かすかを表す int値

1 2 3 4 5スケール:3

整数値:

12345

#javajo

19/57

?12345000

BigDecimalの例

整数値 スケール 表される値

12345 0 12345

12345 3 12.345

12345 6 0.012345

12345 -3

#javajo

スケール≧0の時、

スケールは小数点以下の桁数です

20/57

BigDecimalの例

同じ数を表す(整数値, スケール)の

組み合わせは複数存在します

整数値 スケール 表される値

12345 3 12.345

1234500 5 12.34500

#javajo

同じ数

21/57

#javajoBigDecimalの生成

主にnew BigDecimal(String)や

valueOf(long)で値を生成します

new BigDecimal("1.23")

new BigDecimal("1.2300")

BigDecimal.valueOf(42L)

1.23スケール2

1.2300スケール4

42スケール0

22/57

#javajoBigDecimalの演算

演算メソッドはオブジェクトを変更せず

結果となる新しい値を返します

BigDecimal x = new BigDecimal("2.34");

BigDecimal y = new BigDecimal("4.560");

println(x.add(y));

println(x.multiply(y));

6.900

10.67040

println(x); 2.34(変わらず)

和:

積:

23/57

BigDecimalの演算

足し算・引き算では、大きい方の

スケールに合わせて演算されます

2.34(スケール2)

4.560(スケール3)

+

6.900(スケール3)

#javajo

24/57

BigDecimalの演算

掛け算では、

スケールが足し合わされます

2.34(スケール2)

4.560(スケール3)

×

10.67040(スケール5)

#javajo

25/57

BigDecimalの丸め

金額の端数処理の例:

金額の小数部を四捨五入

小数点以下2桁を残して切り捨て

端数処理に使うメソッド:

setScale(新しいスケール, 丸めモード)

#javajo

BigDecimal 丸められた数

= 元の数 .setScale(2, RoundingMode.FLOOR);

例: オブジェクトは

変更されません

26/57

#javajoBigDecimalの丸め

setScale()による丸め:

3 2 FLOOR 3.00

1.7320 2 FLOOR 1.73

1.7320 2 CEILING 1.74

元の数 スケール 丸め 結果

27/57

BigDecimalの丸め

丸めモード:

CEILING / FLOOR 切り上げ / 切り捨て

UP / DOWN 0から遠い方 / 近い方

HALF_UP 四捨五入

HALF_DOWN 五捨五超入

HALF_EVEN 最近接偶数への丸め

UNNECESSARY丸めが必要になってしまったら

ArithmeticException

#javajo

28/57

× round(MathContext)

round(MathContext)メソッドは

金勘定の端数処理には使えません

#javajo

setScaleスケール(小数点以下の桁数)を指定して丸める

round最大精度(整数値全体の桁数)を指定して丸める

roundは、演算を繰り返すときに精度が膨れ上がって

メモリを食い潰さないようにするためのメソッドです

29/57

× round(MathContext)

例: 精度3で切り捨てる

MathContext mc = new MathContext(3, RoundingMode.FLOOR);

ふつう、金勘定でこんな処理はしないはずです

#javajo

new BigDecimal("1.234").round(mc)

new BigDecimal("56.789").round(mc)

new BigDecimal("0.0001").round(mc)

1.23

56.7

0.0001

30/57

BigDecimalの落とし穴

#javajo

31/57

BigDecimalの落とし穴1

int 定価 = 1000;

BigDecimal 値引率 = new BigDecimal(0.07);

int 売値 = BigDecimal.ONE.subtract(値引率)

.multiply(BigDecimal.valueOf(定価))

.setScale(0, RoundingMode.FLOOR)

.intValue();

System.out.println(売値);

😬

double!

#javajo

?929

32/57

BigDecimalの落とし穴1

BigDecimalの生成にdoubleを使っては

なにもかも台無しです

new BigDecimal(String)を使いましょう

#javajo

33/57

BigDecimalの落とし穴1: 修正

int 定価 = 1000;

BigDecimal 値引率 = new BigDecimal("0.07");

int 売値 = BigDecimal.ONE.subtract(値引率)

.multiply(BigDecimal.valueOf(定価))

.setScale(0, RoundingMode.FLOOR)

.intValue();

System.out.println(売値);

930 😄

文字列を

渡しましょう

#javajo

34/57

BigDecimalの落とし穴2

BigDecimal 総資産 = new BigDecimal("100");

BigDecimal 負債 = new BigDecimal("33.3");

BigDecimal 純資産 = new BigDecimal("66.7");

System.out.println(

総資産.equals(負債.add(純資産)));

😬

スケールが

異なるequalsが

使われている

#javajo

?false

35/57

#javajoBigDecimalの落とし穴2

スケールが違うとequalsは成立しません

33.3 + 66.7

new BigDecimal("100")

100.0(スケール1)

100(スケール0)

! equals

36/57

BigDecimalの落とし穴2

BigDecimal.equalsはスケールも

比較します

スケールを気にしないときは

compareToを使いましょう

#javajo

37/57

BigDecimalの落とし穴2: 修正a

BigDecimal 総資産=new BigDecimal("100.0");

BigDecimal 負債 = new BigDecimal("33.3");

BigDecimal 純資産 = new BigDecimal("66.7");

System.out.println(

総資産.equals(負債.add(純資産)));

true

スケールを

合わせる

😄 または...

#javajo

38/57

BigDecimalの落とし穴2: 修正b

BigDecimal 総資産=new BigDecimal("100");

BigDecimal 負債 = new BigDecimal("33.3");

BigDecimal 純資産 = new BigDecimal("66.7");

System.out.println(

総資産.compareTo(負債.add(純資産) == 0));

true 😄

compareToを

使う

#javajo

39/57

BigDecimalまとめ

BigDecimalは整数値とスケールで

構成されます

new BigDecimal(String)で定数を

作りましょう

端数はsetScaleで丸めましょう

BigDecimal.equalsはスケールも

比較対象です

#javajo

40/57

追補: BigDecimalの割り算

#javajo

41/57

#javajo割り算

割り算はフクザツ

ゼロ除算

割り切れないことがある

結果の桁数が大きくなるかも

例: 3/64 = 0.046875

可能なら逆数を掛ける方が良いです

例: numを4で割る

num.mupltiply(new BigDecimal("0.25"))

42/57

#javajoBigDecimalの割り算

金勘定のための割り算メソッド:

divide(除数, スケール, 丸めモード)

指定したスケールで結果を戻す

割り切れない時は丸める

結果が指定したスケールに収まらない

ときは丸める

divideと名のつく他のメソッドは罠です

43/57

例: 費用配賦 #javajo

年間の費用を12ヶ月ごとに均等割します

ただし、ひと月の費用は小数点下2桁までに

切り捨てて、端数は最後の月に寄せます

年間の費用を12ヶ月ごとに均等割します

ただし、ひと月の費用は小数点下2桁までに

切り捨てて、端数は最後の月に寄せます

注意! 費用がマイナスの時は?

0の方向に丸める: -0.123 → -0.12

-∞の方向に丸める: -0.123 → -0.13

ここでは-∞の方向に丸めることにします

44/57

例: 費用配賦 #javajo

BigDecimal 年間費用 = new BigDecimal( "49.00" );

BigDecimal 月ごと費用 = 年間費用.divide(

BigDecimal.valueOf(12), 2, RoundingMode.FLOOR);

BigDecimal 最終月費用 = 年間費用.subtract(

月ごと費用.multiply(BigDecimal.valueOf(11)));

System.out.println(月ごと費用);

System.out.println(最終月費用);

年間の費用を12ヶ月ごとに均等割します

ただし、ひと月の費用は小数点下2桁までに

切り捨てて、端数は最後の月に寄せます

4.08

4.12

45/57

セッション内容

double/floatで金勘定してはいけない

BigDecimalで金勘定しよう

先取りMoney and Currency API

#javajo

46/57

Money and Currency API

JSR 354 Money and Currency APIは、

金勘定に関するAPIです

通貨、金額、丸め、通貨換算、書式化…

Java SE 9で採用予定です

クレディ・スイスが主導しています

#javajo

47/57

なぜクレディ・スイス?

スイスの特殊条件が影響?

周りは全部ユーロ圏

決済用代替通貨

WIRフラン , WIRユーロが流通している

現金決済の最小単位は

5ラッペン(5/100スイスフラン)

現金以外の最小単位は1ラッペン

たしかに標準APIが欲しくなりそうです

#javajo

48/57

JSRのステータス

2012-01 JSR Review

2012-02 Expert Group組成

2013-01 Spec Lead交代

2013-03 Early Draft

2013-10 Public Review

2014-03 Public Review 2

2015-03 Proposed Final Draft

2015-05 Final Release

#javajo

そこそこ難産

49/57

構成

API: javax.money 実装

通貨インタフェース

金額インタフェース

丸めインタフェース

SPI

SPI

SPI

通貨実装のプロバイダ

金額実装のプロバイダ

丸め実装のプロバイダ

実装はAPIから独立しています(参照実装: Moneta)

複数実装を組み合わせて使うことも可能です

#javajo

50/57

通貨: CurrencyUnit

CurrencyUnit 日本円

= Monetary

.getCurrency("JPY");

通貨コード

に対応する通貨

CurrencyUnit 地域の通貨

= Monetary

.getCurrency(Locale.getDefault());

ロケールに

対応する通貨

#javajo

51/57

金額: MonetaryAmount

Monetaが提供する金額実装:

Money: 任意スケールの金額

FastMoney: 固定スケール(5)の金額

RoundedMoney: 演算ごとに丸める金額

Money 定価 = Money.of(1000, 日本円);

System.out.println(定価);

#javajo

JPY 1000

52/57

丸め: MonetaryRounding

MonetaryRounding 整数に切捨

= Monetary

.getRounding(RoundingQueryBuilder.of()

.setScale(0)

.set(RoundingMode.FLOOR)

.build());

Money 売上 = Money.of(

new BigDecimal("42.3"), "JPY");

Money 請求額 = 売上.with(整数に切捨);

System.out.println(請求額);

結果のスケール

丸め方

#javajo

JPY 42

53/57

通貨換算: CurrencyConversion

CurrencyConversion toUSD

= MonetaryConversions

.getExchangeRateProvider()

.getCurrencyConversion(米ドル);

Money 金額JPY = Money.of(42, 日本円);

Money 金額USD = 金額JPY.with(toUSD);

System.out.println(金額USD);

米ドルへの換算

Monetaの換算レートプロバイダはECB/IMFのオンラインの

換算表を使います

通常、自前で換算レートプロバイダを実装するべきでしょう

#javajo

USD 0.3509...

54/57

Money and Currency APIまとめ

結構な重量級APIです

つかいでのありそうなシステム

扱う通貨が可変

複数通貨間の換算を行う

関連情報へのリンク集:https://java.net/projects/jjug/pages/Adopt-a-JSR-JavaSE9

#javajo

55/57

セッション内容

double/floatで金勘定してはいけない

BigDecimalで金勘定しよう

先取りMoney and Currency API

#javajo

56/57

#javajo改版履歴

2015-04-11 JJUG CCC 2015 Spring

2015-06-11 Java女子部勉強会

「数値型おさらい&金勘定ことはじめ

( BigDecimal入門、Money and Currency API紹介 )」

• BigDecimalの割り算を追補

• JSR-354の仕様変更に追随

• JSR-354がFinal Releaseされたことを反映

2015-06-14 微修正

57/57