A bare Decimal rate is ambiguous between the fraction 0,0341 and the percentage 3,41, and nothing in the type system separated them. from_fraction and from_percentage force the call site to say which one it means. Migrating the call sites showed the ambiguity was already there: several tests passed Decimal::new(45, 2), which the old code consumed as the fraction 0,45, that is 45 %, while it reads naturally as 0,45 %. Those tests exercise capital and duration rather than the rate, so their outcome is unchanged, but the reading was a coin flip before and is now explicit. The type deliberately validates nothing. Comparing buying against investing the same cash needs rates for expected return and inflation, where a negative value is meaningful and a 99 % ceiling is nonsense. Range stays a LoanOffer rule, which is where the domain knows what a plausible loan rate is. LoanOffer::annual_nominal_rate_in_percent is replaced by InterestRate:: as_percentage, so the conversion stays in immo-core instead of immo-web having to multiply by 100 itself. It normalises: dividing by 100 and multiplying back does not restore the original scale, so 3,41 % round-tripped through Display as "3.4100 %". This fixes the fraction versus percentage axis only. InterestRate carries no period, so proportional_monthly_interest_rate still returns a bare Decimal and naming alone separates annual from monthly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
73 lines
1.8 KiB
Rust
73 lines
1.8 KiB
Rust
use std::fmt::Display;
|
|
|
|
use rust_decimal::Decimal;
|
|
|
|
#[derive(Debug, PartialEq, Eq, Clone, Copy, PartialOrd, Ord)]
|
|
pub struct InterestRate {
|
|
rate_as_fraction: Decimal,
|
|
}
|
|
|
|
impl InterestRate {
|
|
pub const ZERO: Self = InterestRate {
|
|
rate_as_fraction: Decimal::ZERO,
|
|
};
|
|
|
|
pub fn from_percentage(rate_in_percent: Decimal) -> Self {
|
|
InterestRate {
|
|
rate_as_fraction: rate_in_percent / Decimal::ONE_HUNDRED,
|
|
}
|
|
}
|
|
|
|
pub const fn from_fraction(rate_as_fraction: Decimal) -> Self {
|
|
InterestRate { rate_as_fraction }
|
|
}
|
|
|
|
pub const fn as_fraction(self) -> Decimal {
|
|
self.rate_as_fraction
|
|
}
|
|
|
|
pub fn as_percentage(self) -> Decimal {
|
|
(self.rate_as_fraction * Decimal::ONE_HUNDRED).normalize()
|
|
}
|
|
}
|
|
|
|
impl Display for InterestRate {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
write!(f, "{} %", self.as_percentage())
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn create_from_percentage_yields_correct_rate() {
|
|
assert_eq!(
|
|
InterestRate::from_percentage(Decimal::new(415, 2)).as_fraction(),
|
|
Decimal::new(415, 4)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn create_from_fraction_yields_correct_rate() {
|
|
assert_eq!(
|
|
InterestRate::from_fraction(Decimal::new(415, 4)).as_fraction(),
|
|
Decimal::new(415, 4)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn create_from_fraction_is_properly_output_as_percentage() {
|
|
assert_eq!(
|
|
InterestRate::from_fraction(Decimal::new(314, 4)).as_percentage(),
|
|
Decimal::new(314, 2)
|
|
)
|
|
}
|
|
|
|
#[test]
|
|
fn display_works_as_expected_with_no_trailing_zeros() {
|
|
let interest_rate = InterestRate::from_fraction(Decimal::new(3145, 5));
|
|
assert_eq!(format!("{}", interest_rate), "3.145 %");
|
|
}
|
|
}
|