finanza/interest
Time-value-of-money helpers built on
finanza/decimal.
Every function takes its inputs as decimals, computes in decimal,
and rounds the final result with HalfEven (“banker’s”) to the
caller-supplied number of decimal places.
Precision
Iterative computations (future_value, present_value,
payment, effective_annual_rate, compound_interest) target
7 decimal digits of internal working precision. Before
every multiplication inside the iterative growth-factor loop
the accumulator is rounded adaptively to the largest digit
count for which the resulting product still fits under
2^53 − 1 (the JavaScript safe-integer ceiling enforced by
finanza/decimal). For typical inputs the
target precision is always reached; only when the growth
factor swells (long horizons at very high rates, growth above
~10⁵) does the per-step precision shed digits to keep the
multiplication safe.
Honest-precision ceiling. The iterative loop can deliver at
most ~7 honest decimal digits regardless of the caller’s
digits argument. After issue #25 (decimal.round is
trim-only; the final step is now decimal.rescale), the
returned value’s exponent matches the requested -digits, so
callers asking for digits > 7 get a Decimal with the right
shape — but the trailing digits − 7 decimal places are zero
padding from the final rescale, not computed precision. The
numeric value is unchanged by those trailing zeros; the
rendered form is more verbose without being more accurate. If
you need more than 7 honest digits, compute the closed form in
a higher-precision package (Python decimal with prec=50,
for instance).
Concrete consequence: results match textbook 50-digit references
(Python decimal, numpy_financial, Excel) to the cent at
digits = 2 and to ~10⁻⁶ at digits = 6 for monthly rates and
horizons up to about 30 years. For lending-grade work where the
answer must be reproducible against external industry tooling,
stick to those typical-input ranges or compute the closed form
in a higher-precision package.
Types
Errors raised by interest functions.
pub type InterestError {
NegativePrincipal
RateBelowMinusOne
NegativeRate
PeriodsOutOfRange
CompoundsOutOfRange
NegativeDigits
ArithmeticError(error: decimal.ArithmeticError)
}
Constructors
-
NegativePrincipalprincipalwas negative. -
RateBelowMinusOneratewas below-1.0(i.e. a return of less than-100%). Returns less than-100%have no meaningful interpretation in the TVM formulas — the resulting future value would be negative for a positive principal. Rates in the range[-1.0, ∞)are accepted (deflation, depreciation, total-loss at exactly-1.0).The legacy
NegativeRatevariant is no longer emitted; it is retained so existing pattern matches keep compiling. -
NegativeRateDeprecated alias for
RateBelowMinusOne. Retained so callers that pattern-matched on the previous “any negative rate” reject path keep compiling, but the library never returns this variant — it always emitsRateBelowMinusOne. -
PeriodsOutOfRangeperiodswas zero or negative, or exceeded the supported range1..=1200(100 years of monthly compounding). -
CompoundsOutOfRangecompounds_per_yearwas zero or negative. -
NegativeDigitsdigitswas negative. -
ArithmeticError(error: decimal.ArithmeticError)Underlying decimal arithmetic produced an error.
Values
pub fn compound_interest(
principal principal: decimal.Decimal,
annual_rate annual_rate: decimal.Decimal,
years years: Int,
compounds_per_year compounds_per_year: Int,
digits digits: Int,
) -> Result(decimal.Decimal, InterestError)
Future value under compound interest:
FV = principal × (1 + annual_rate / compounds_per_year)^(compounds_per_year × years)
pub fn effective_annual_rate(
nominal_rate nominal_rate: decimal.Decimal,
compounds_per_year compounds_per_year: Int,
digits digits: Int,
) -> Result(decimal.Decimal, InterestError)
Effective annual rate from a nominal rate compounded
compounds_per_year times per year:
EAR = (1 + nominal_rate / compounds_per_year)^compounds_per_year - 1
See the module-level Precision section for the 7-working-digit target and the adaptive overflow guard.
pub fn future_value(
present present: decimal.Decimal,
rate_per_period rate_per_period: decimal.Decimal,
periods periods: Int,
digits digits: Int,
) -> Result(decimal.Decimal, InterestError)
Future value of present after periods periods at rate_per_period.
See the module-level Precision section for the 7-working-digit target and the adaptive overflow guard.
pub fn payment(
principal principal: decimal.Decimal,
rate_per_period rate_per_period: decimal.Decimal,
periods periods: Int,
digits digits: Int,
) -> Result(decimal.Decimal, InterestError)
Periodic payment for a fully-amortising loan:
PMT = principal × rate / (1 - (1 + rate)^(-periods))
When rate_per_period is zero, returns straight-line
principal / periods.
See the module-level Precision section for the 7-working-digit target and the adaptive overflow guard.
pub fn present_value(
future future: decimal.Decimal,
rate_per_period rate_per_period: decimal.Decimal,
periods periods: Int,
digits digits: Int,
) -> Result(decimal.Decimal, InterestError)
Present value of future discounted at rate_per_period for
periods periods.
See the module-level Precision section for the 7-working-digit target and the adaptive overflow guard.
pub fn simple_interest(
principal principal: decimal.Decimal,
rate rate: decimal.Decimal,
periods periods: Int,
digits digits: Int,
) -> Result(decimal.Decimal, InterestError)
Simple interest: I = P × r × t.
periods == 0 is a valid input and returns 0 rescaled to
digits — mathematically there is no interest over zero time,
and accepting the trivial case lets callers reuse this function
inside schedule loops without inserting a branch for the
“as-of period 0” row. Negative periods is still rejected
with PeriodsOutOfRange.