| | | @@ 変更前 1行目〜 / 変更後 1行目〜 @@ |
| 1 | 1 | | //! 預金利息の計算。 |
| 2 | | − | //! v1: f64 と powi(整数の指数)で計算し、利息は round、税は floor で丸める素直な書き方。 |
| 2 | + | //! v2: 年利を分数にして利息を正確に計算し、円への四捨五入と税の切り捨てを整数で行う。 |
| 3 | + | //! f64 と powi だと、10万円・年0.5%・2年の利息 1,002.5円が 1002.4999999999709 になり、1,002円に落ちる。 |
| 4 | + | //! 複利の (1 + 年利)^50 は u128 に入らないので、自作の多倍長整数(biguint.rs)で計算する。割り算は無いので、 |
| 5 | + | //! 「q × 2 × 分母 ≤ 2 × 分子 + 分母」を満たす最大の q(=四捨五入した利息)を二分探索で求める。 |
| 3 | 6 | | |
| 7 | + | use crate::biguint::BigUint; |
| 4 | 8 | | use crate::json::{n, obj, s, Value}; |
| 5 | 9 | | |
| 6 | | − | fn valid_rate(r: &str) -> bool { |
| 7 | | − | let (int, frac) = r.split_once('.').unwrap_or((r, "0")); |
| 8 | | − | !int.is_empty() && int.bytes().all(|b| b.is_ascii_digit()) && (1..=3).contains(&frac.len()) && frac.bytes().all(|b| b.is_ascii_digit()) |
| 10 | + | fn parse_rate(r: &str) -> Option<(u128, u128)> { |
| 11 | + | let (int, frac) = r.split_once('.').unwrap_or((r, "")); |
| 12 | + | let ok = !int.is_empty() && int.bytes().all(|b| b.is_ascii_digit()) && frac.len() <= 3 && frac.bytes().all(|b| b.is_ascii_digit()) |
| 13 | + | && (r.contains('.') == !frac.is_empty()); |
| 14 | + | if !ok { |
| 15 | + | return None; |
| 16 | + | } |
| 17 | + | Some((format!("{int}{frac}").parse().ok()?, 10u128.pow(frac.len() as u32) * 100)) // 年利 ÷ 100 = rn / rd |
| 9 | 18 | | } |
| 10 | 19 | | |
| 11 | 20 | | pub fn calc(principal: i64, rate: &str, years: i64, mode: &str) -> Result<Value, String> { |
| 12 | | − | if !(1..=10_000_000_000).contains(&principal) || !valid_rate(rate) || !(1..=50).contains(&years) || !(mode == "simple" || mode == "compound") { |
| 13 | | − | return Ok(obj(vec![("error", s("invalid_input"))])); |
| 21 | + | let invalid = || Ok(obj(vec![("error", s("invalid_input"))])); |
| 22 | + | if !(1..=10_000_000_000).contains(&principal) || !(1..=50).contains(&years) || !(mode == "simple" || mode == "compound") { |
| 23 | + | return invalid(); |
| 14 | 24 | | } |
| 15 | | − | let r: f64 = rate.parse::<f64>().map_err(|e| e.to_string())? / 100.0; |
| 16 | | − | if r > 0.2 { |
| 17 | | − | return Ok(obj(vec![("error", s("invalid_input"))])); |
| 25 | + | let Some((rn, rd)) = parse_rate(rate) else { return invalid() }; |
| 26 | + | if rn * 5 > rd { |
| 27 | + | return invalid(); |
| 18 | 28 | | } |
| 19 | | − | let p = principal as f64; |
| 20 | | − | let gross = if mode == "simple" { p * r * years as f64 } else { p * (1.0 + r).powi(years as i32) - p }; |
| 21 | | − | let interest = gross.round() as i64; |
| 22 | | − | let tax = (interest as f64 * 0.15315).floor() as i64 + (interest as f64 * 0.05).floor() as i64; |
| 29 | + | let p = principal as u128; |
| 30 | + | let interest: u128 = if mode == "simple" { |
| 31 | + | let (num, den) = (p * rn * years as u128, rd); |
| 32 | + | (2 * num + den) / (2 * den) |
| 33 | + | } else { |
| 34 | + | let den = BigUint::from_u128(rd).pow(years as u64); |
| 35 | + | let num = BigUint::from_u128(rd + rn).pow(years as u64).mul_small(p).sub(&den.mul_small(p)); |
| 36 | + | let target = num.mul_small(2).add(&den); // 2 × 分子 + 分母 |
| 37 | + | let den2 = den.mul_small(2); |
| 38 | + | let (mut lo, mut hi) = (0u128, 1u128 << 60); // 利息は 2^60 円未満 |
| 39 | + | while lo < hi { |
| 40 | + | let mid = (lo + hi + 1) / 2; |
| 41 | + | if den2.mul_small(mid) <= target { lo = mid } else { hi = mid - 1 } |
| 42 | + | } |
| 43 | + | lo |
| 44 | + | }; |
| 45 | + | let interest = interest as i64; |
| 46 | + | let tax = interest * 15315 / 100000 + interest * 5 / 100; // それぞれ1円未満切り捨て |
| 23 | 47 | | let net = interest - tax; |
| 24 | 48 | | Ok(obj(vec![("gross_total", n(principal + interest)), ("interest", n(interest)), ("tax", n(tax)), ("net_interest", n(net)), ("net_total", n(principal + net))])) |
| 25 | 49 | | } |