預金利息の計算(単利・複利・税引後)

元本・年利・年数から、単利または複利の利息と、税(所得税等15.315%・住民税5%)を引いたあとの金額を円単位で出す。

2進小数の誤差0.5 の丸め方大きな整数

言語ごとの結果

正解表 15件のうち、正解と一致した件数です。

PHPPHP 8.4.26

よくある書き方12 / 15

直した書き方15 / 15

PythonPython 3.14.7

よくある書き方9 / 15

直した書き方15 / 15

GoGo 1.27.2

よくある書き方11 / 15

直した書き方15 / 15

JavaScriptNode 24.15.0

よくある書き方12 / 15

直した書き方15 / 15

JavaJava 21.0.12

よくある書き方12 / 15

直した書き方15 / 15

C#.NET 10.0.402

よくある書き方10 / 15

直した書き方15 / 15

RustRust 1.99.0

よくある書き方12 / 15

直した書き方15 / 15

RubyRuby 4.0.7

よくある書き方12 / 15

直した書き方15 / 15

KotlinKotlin 2.4.21

よくある書き方12 / 15

直した書き方15 / 15

SwiftSwift 6.3.3

よくある書き方12 / 15

直した書き方15 / 15

なぜ割れたのか

よくある書き方が落ちたケースを、原因ごとにまとめた解説です(2本)。書いてある「落ちた言語」が今回の検証結果と一致することを確かめてから載せています。

2進小数の誤差

10万円・年0.5%・2年の利息 1,002.5円を、10言語とも 1,002円にした

対象のケースc005c006c007 落ちた言語PHPPythonGoJavaScriptJavaC#RustRubyKotlinSwift

10万円を年0.5%の複利で2年預けると(c006)、利息は 100,000 × 1.005² − 100,000 = 1,002.5円 です。円に四捨五入して 1,003円 が正解ですが、10言語すべてが 1,002円 と答えました。

1,000万円・年0.25%・2年(c007)の 50,062.5円も、10言語とも 50,062円でした。利息が1円少ないと、税額の切り捨ても変わり、税額まで1円ずれます。

1,000円・年0.35%・1年の単利(c005)の利息 3.5円は、10言語中9言語が 3円と答えました。正解の 4円を返したのは C# だけです。

なぜ少なくなったのか

計算正確な値2進数の小数での結果
100000 × 1.005² − 1000001002.51002.4999999999709
1000 × 0.0035 × 13.53.4999999999999996

0.005 も 0.0035 も2進数では正確に表せず、累乗や掛け算の結果が 0.5 にわずかに届きません。C# 以外の9言語は、利息を2進数の小数(double・f64・Float など)で計算し、複利は累乗(pow や **)で求めたので、この誤差がそのまま答えに出ました。

C# は、お金なので decimal(十進の小数)で計算しました。単利の 3.5円(c005)は正確に表せて正解しました。ところが decimal には累乗の関数がありません。複利の (1 + 年利)^年数 だけは Math.Pow(double)で計算して decimal に戻したため、そこで2進数の誤差が入り、c006・c007 は外れました。

どう直したか

年利を分数(分子・分母の整数)にして、利息を正確に求めてから円に丸めました。年利 0.005%・50年の複利では、(1 + 年利)^50 の分子と分母が250桁を超えます。そこで、多倍長整数を使いました。

使ったもの言語
標準の多倍長整数Python・Ruby(標準の整数)、Java・C#(BigInteger)、Go(math/big)、JavaScript(BigInt)、PHP(GMP)
自分で書いた多倍長整数Rust・Swift(標準ライブラリに無いので、必要な分だけ)
十進の小数Kotlin(BigDecimal の pow で (1 + 年利)^年数 を正確に計算)

0.5 の丸め方

0.5円ちょうどの利息を、Python と C# は 0円にした(Go は CPU で答えが変わった)

対象のケースc003c004c011 落ちた言語PythonGoC#

1,000円を年0.005%の単利で10年預けると、利息は 0.5円 ちょうどです(c003)。円に四捨五入して 1円 が正解です。10言語中8言語は 1円を返しましたが、Python と C# は 0円 と答えました。1,300円・年0.5%・5年(c011)の 32.5円も、同じ2言語が 32円にしました(残りの8言語は正解の 33円)。

1,000円・年0.05%・1年の複利(c004)も利息は 0.5円ちょうどで、ここでは Python・C# に加えて Go も 0円と答えました。

偶数丸め

0.5 と 32.5 は2進数の小数でも誤差なく表せるので、c003・c011 の違いは丸め方だけです。

丸め方言語0.5 →32.5 →
0.5 ちょうどを偶数の方へPython(round())・C#(Math.Round)032
0.5 ちょうどを大きい方(0 から遠い方)へPHP・Go・JavaScript・Java・Rust・Ruby・Kotlin・Swift133

Python の round() と C# の Math.Round は、どちらも既定が偶数丸めです。C# は decimal で計算しているので、値そのものは正確です。それでも丸め方の既定で外れました。

Go だけ、CPU によって答えが変わった

c004 で Go が 0円と答えたのは、丸め方の問題ではありません。Go の math.Round は 0.5 を切り上げます。

Go のコンパイラは、ARM64(Apple Silicon・AWS の Graviton など)で p*math.Pow(1+r, 1) - p のような「掛けて引く」を、FMA(積和演算)の1命令にまとめます。FMA は掛け算の結果を途中で丸めずに引き算をするので、答えが 0.5 ではなく 0.49999999999994493 になりました。

p, r := 1000.0, 0.0005
a := p*math.Pow(1+r, 1) - p          // ARM64: 0.49999999999994493(FMA にまとめられる)
b := float64(p*math.Pow(1+r, 1)) - p // 0.5(明示的に丸めると、まとめない)

同じコードを x86-64 で動かすと、0.5 になります。Go の仕様は、この合成を認めています。同じ言語・同じコードでも、CPU によって小数の答えが変わることがある、という例です。この検証は Mac(Apple Silicon)と AWS CodeBuild(ARM64)で回しているので、どちらでも 0円になりました。

どう直したか

利息を分数のまま計算し、「分子 × 2 + 分母」を「分母 × 2」で割った商(切り捨て)を、円に四捨五入した利息にしました。0.5 ちょうどは必ず繰り上がります。2進数の小数を通らないので、FMA の影響も受けません。

正解表と、よくある書き方の答え

正解は実装より先に決めて固定しています。右の欄は、よくある書き方の答えを言語ごとにまとめたものです(赤い値は実際に返した間違った答え)。

ケース入力正解よくある書き方の答え(10言語)
c001基本・複利
principal
1000000
rate
0.2
years
10
mode
compound
gross_total
1020181
interest
20181
tax
4099
net_interest
16082
net_total
1016082

✓10言語すべて正解

c002基本・単利
principal
1000000
rate
0.2
years
10
mode
simple
gross_total
1020000
interest
20000
tax
4063
net_interest
15937
net_total
1015937

✓10言語すべて正解

c003利息がちょうど 0.5円(単利) 0.5円 → 1円。2進数でも 0.5 ちょうどなので、偶数丸めの言語は 0円にする なぜ? 解説を読む
principal
1000
rate
0.005
years
10
mode
simple
gross_total
1001
interest
1
tax
0
net_interest
1
net_total
1001
  • Python・C#gross_total1000interest0net_interest0net_total1000
  • PHP・Go・JavaScript・Java・Rust・Ruby・Kotlin・Swift✓ 正解
c004利息がちょうど 0.5円(複利1年) 0.5円 → 1円 なぜ? 解説を読む
principal
1000
rate
0.05
years
1
mode
compound
gross_total
1001
interest
1
tax
0
net_interest
1
net_total
1001
  • Python・Go・C#gross_total1000interest0net_interest0net_total1000
  • PHP・JavaScript・Java・Rust・Ruby・Kotlin・Swift✓ 正解
c005利息がちょうど 3.5円 3.5円 → 4円。2進数の小数では 3.4999999999999996 になり 3円に落ちる なぜ? 解説を読む
principal
1000
rate
0.35
years
1
mode
simple
gross_total
1004
interest
4
tax
0
net_interest
4
net_total
1004
  • PHP・Python・Go・JavaScript・Java・Rust・Ruby・Kotlin・Swiftgross_total1003interest3net_interest3net_total1003
  • C#✓ 正解
c006複利の利息がちょうど 1,002.5円 1.005² の誤差で 1002.4999999999709 になり、1,002円に落ちる なぜ? 解説を読む
principal
100000
rate
0.5
years
2
mode
compound
gross_total
101003
interest
1003
tax
203
net_interest
800
net_total
100800
  • PHP・Python・Go・JavaScript・Java・C#・Rust・Ruby・Kotlin・Swiftgross_total101002interest1002net_interest799net_total100799
c007複利の利息がちょうど 50,062.5円 利息が1円ずれると、税額(切り捨て)もずれる なぜ? 解説を読む
principal
10000000
rate
0.25
years
2
mode
compound
gross_total
10050063
interest
50063
tax
10170
net_interest
39893
net_total
10039893
  • PHP・Python・Go・JavaScript・Java・C#・Rust・Ruby・Kotlin・Swiftgross_total10050062interest50062tax10169
c008大きな元本・長期の複利 利息は約91兆円。倍精度の小数でも 2^53 未満に収まる
principal
10000000000
rate
20
years
50
mode
compound
gross_total
91004381500021
interest
90994381500021
tax
18485508601729
net_interest
72508872898292
net_total
72518872898292

✓10言語すべて正解

c009金利0%
principal
1000000
rate
0
years
10
mode
compound
gross_total
1000000
interest
0
tax
0
net_interest
0
net_total
1000000

✓10言語すべて正解

c010税の切り捨て・小さな利息 利息19円。15.315% は 2.90985 → 2円、5% は 0.95 → 0円
principal
19000
rate
0.1
years
1
mode
simple
gross_total
19019
interest
19
tax
2
net_interest
17
net_total
19017

✓10言語すべて正解

c011利息がちょうど 32.5円 32.5円 → 33円 なぜ? 解説を読む
principal
1300
rate
0.5
years
5
mode
simple
gross_total
1333
interest
33
tax
6
net_interest
27
net_total
1327
  • Python・C#gross_total1332interest32tax5
  • PHP・Go・JavaScript・Java・Rust・Ruby・Kotlin・Swift✓ 正解
c012期間0年はエラー
principal
1000000
rate
0.2
years
0
mode
compound
error
invalid_input

✓10言語すべて正解

c013年利が20%を超えるとエラー
principal
1000000
rate
20.5
years
1
mode
compound
error
invalid_input

✓10言語すべて正解

c014年利が小数第4位まであるとエラー
principal
1000000
rate
0.0005
years
1
mode
compound
error
invalid_input

✓10言語すべて正解

c015元本0円はエラー
principal
0
rate
0.2
years
1
mode
compound
error
invalid_input

✓10言語すべて正解

コード

「違い」では、よくある書き方から直した書き方へ、どの行をどう変えたかを示しています(赤が消した行、緑が足した行、濃い色が行の中で変わった部分)。計算の部分だけを載せ、入出力を受け渡す部分は除いています。

Deposit.php+17 −7よくある書き方(0605a4d)→ 直した書き方

@@ 変更前 3行目〜 / 変更後 3行目〜 @@
33
44 /**
55 * 預金利息の計算。
6− * v1: float と pow で計算し、利息は round、税は floor で丸める素直な書き方。
6+ * v2: 年利を GMP の分数(分子・分母)にして利息を正確に計算し、円への四捨五入と税の切り捨てを整数で行う。
7+ * float と pow だと、10万円・年0.5%・2年の利息 1,002.5円が 1002.4999999999709 になり、1,002円に落ちる。
78 */
89 /** @param array{principal: int, rate: string, years: int, mode: string} $in */
910 function calcDeposit(array $in): array
1011 {
1112 ['principal' => $principal, 'rate' => $rate, 'years' => $years, 'mode' => $mode] = $in;
12− if (!is_int($principal) || $principal < 1 || $principal > 10 ** 10 || !preg_match('/^\d+(\.\d{1,3})?$/', $rate)
13+ if (!is_int($principal) || $principal < 1 || $principal > 10 ** 10 || !preg_match('/^(\d+)(?:\.(\d{1,3}))?$/', $rate, $m)
1314 || !is_int($years) || $years < 1 || $years > 50 || !in_array($mode, ['simple', 'compound'], true)) {
1415 return ['error' => 'invalid_input'];
1516 }
16− $r = (float) $rate / 100;
17− if ($r > 0.2) {
17+ $frac = $m[2] ?? '';
18+ $rn = gmp_init($m[1] . $frac, 10); // 基数を明示する("035" を8進数として読ませない)
19+ $rd = gmp_mul(gmp_pow(10, strlen($frac)), 100); // 年利 ÷ 100 = rn / rd
20+ if (gmp_cmp(gmp_mul($rn, 5), $rd) > 0) {
1821 return ['error' => 'invalid_input'];
1922 }
20− $gross = $mode === 'simple' ? $principal * $r * $years : $principal * pow(1 + $r, $years) - $principal;
21− $interest = (int) round($gross);
22− $tax = (int) floor($interest * 0.15315) + (int) floor($interest * 0.05);
23+ $p = gmp_init($principal);
24+ if ($mode === 'simple') {
25+ $num = gmp_mul(gmp_mul($p, $rn), $years);
26+ $den = $rd;
27+ } else {
28+ $den = gmp_pow($rd, $years);
29+ $num = gmp_sub(gmp_mul($p, gmp_pow(gmp_add($rd, $rn), $years)), gmp_mul($p, $den));
30+ }
31+ $interest = gmp_intval(gmp_div_q(gmp_add(gmp_mul($num, 2), $den), gmp_mul($den, 2))); // 円に四捨五入(0.5 は繰り上げ)
32+ $tax = intdiv($interest * 15315, 100000) + intdiv($interest * 5, 100); // それぞれ1円未満切り捨て
2333 $net = $interest - $tax;
2434 return ['gross_total' => $principal + $interest, 'interest' => $interest, 'tax' => $tax, 'net_interest' => $net,
2535 'net_total' => $principal + $net];

仕様

着手前に測ったこと(2026-10-16)

元ツールの式(2進数の小数・Math.pow・Math.round)で、元本9通り × 年利15通り × 1〜30年 × 単利・複利の8,100通りを、正確な値と比べた。

  • 利息を円に丸めた値が1円ずれるのは9通り(例: 10万円・年0.5%・2年・複利 → 利息 1,002.5円が 1,002円)
  • それより多いのは内訳のずれで、正確に計算しても 1,445通りで「利息 − 税額 ≠ 税引後の利息」 になる。利息・税額・税引後の利息を、それぞれ別に円へ丸めているため

そこで本 spec では、税を実務どおり(所得税・復興特別所得税 15.315% と住民税 5% をそれぞれ1円未満切り捨て)にし、税引後の利息は「利息 − 税額」と決めて、内訳が必ずそろうようにした。

入力

項目型説明
principal整数預入額(円)。1〜10,000,000,000
rate文字列(小数)年利(%)。0〜20、小数第3位まで("0.2"・"0.005")
years整数期間(年)。1〜50
mode文字列"simple"(単利)または "compound"(年1回の複利)

範囲外・形が違うときは {"error": "invalid_input"}。

出力(すべて整数・円)

項目定義
interest税引前の利息。単利は 元本 × 年利 ÷ 100 × 年数、複利は 元本 × (1 + 年利 ÷ 100)^年数 − 元本。円に四捨五入(0.5円ちょうどは繰り上げ)
tax⌊利息 × 0.15315⌋ + ⌊利息 × 0.05⌋(それぞれ1円未満切り捨て)
net_interest利息 − 税額
gross_total元本 + 利息
net_total元本 + 税引後の利息
  • 計算は正確な値(分数)で行い、丸めるのは利息の1回と税の切り捨てだけ
  • 複利は、税引前の元利合計を最後にまとめて計算する(元ツールと同じ目安。毎年の利息に課税して再投資する自動継続の計算ではない)

元ツールとの違い

  • 元ツールは税額を「利息 × 20.315%」の1回で計算し、利息・税額・税引後の利息・元利合計をそれぞれ別に四捨五入して表示する。本 spec は上のとおり税を2つに分けて切り捨て、内訳がそろうようにした
  • 元ツールは期間・元本に小数も受け付ける。本 spec は整数に限った

期待値の作り方

expect は oracle.py で、上の定義を分数(fractions.Fraction)でそのまま計算して生成した(2026-10-16)。実装ではない。runner は使わない。