src/finanza@interest.erl
-module(finanza@interest).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/finanza/interest.gleam").
-export([simple_interest/4, compound_interest/5, future_value/4, present_value/4, payment/4, effective_annual_rate/3]).
-export_type([interest_error/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" Time-value-of-money helpers built on\n"
" [`finanza/decimal`](./decimal.html).\n"
"\n"
" Every function takes its inputs as decimals, computes in decimal,\n"
" and rounds the final result with `HalfEven` (\"banker's\") to the\n"
" caller-supplied number of decimal places.\n"
).
-type interest_error() :: negative_principal |
negative_rate |
periods_out_of_range |
compounds_out_of_range |
negative_digits |
{arithmetic_error, finanza@decimal:arithmetic_error()}.
-file("src/finanza/interest.gleam", 193).
-spec straight_line_payment(finanza@decimal:decimal(), integer(), integer()) -> {ok,
finanza@decimal:decimal()} |
{error, interest_error()}.
straight_line_payment(Principal, Periods, Digits) ->
_pipe = finanza@decimal:divide(
Principal,
finanza@decimal:from_int(Periods),
Digits,
half_even
),
gleam@result:map_error(
_pipe,
fun(Field@0) -> {arithmetic_error, Field@0} end
).
-file("src/finanza/interest.gleam", 296).
-spec pow_loop(
finanza@decimal:decimal(),
integer(),
finanza@decimal:decimal(),
integer()
) -> {ok, finanza@decimal:decimal()} | {error, interest_error()}.
pow_loop(Base, Exponent, Acc, Digits) ->
gleam@bool:guard(
Exponent =< 0,
{ok, Acc},
fun() ->
gleam@result:'try'(
begin
_pipe = finanza@decimal:multiply(Acc, Base),
gleam@result:map_error(
_pipe,
fun(Field@0) -> {arithmetic_error, Field@0} end
)
end,
fun(Product) ->
Trimmed = finanza@decimal:round(Product, Digits, half_even),
pow_loop(Base, Exponent - 1, Trimmed, Digits)
end
)
end
).
-file("src/finanza/interest.gleam", 285).
?DOC(
" `(1 + rate)^periods`, computed by repeated multiplication and\n"
" rounded to `digits` decimal places between steps so the\n"
" coefficient cannot grow unboundedly.\n"
).
-spec growth_factor(finanza@decimal:decimal(), integer(), integer()) -> {ok,
finanza@decimal:decimal()} |
{error, interest_error()}.
growth_factor(Rate, Periods, Digits) ->
gleam@result:'try'(
begin
_pipe = finanza@decimal:add(finanza@decimal:one(), Rate),
gleam@result:map_error(
_pipe,
fun(Field@0) -> {arithmetic_error, Field@0} end
)
end,
fun(Base) -> pow_loop(Base, Periods, finanza@decimal:one(), Digits) end
).
-file("src/finanza/interest.gleam", 311).
-spec check_principal(finanza@decimal:decimal()) -> {ok, nil} |
{error, interest_error()}.
check_principal(P) ->
gleam@bool:guard(
finanza@decimal:is_negative(P),
{error, negative_principal},
fun() -> {ok, nil} end
).
-file("src/finanza/interest.gleam", 319).
-spec check_rate(finanza@decimal:decimal()) -> {ok, nil} |
{error, interest_error()}.
check_rate(R) ->
gleam@bool:guard(
finanza@decimal:is_negative(R),
{error, negative_rate},
fun() -> {ok, nil} end
).
-file("src/finanza/interest.gleam", 332).
-spec check_compounds(integer()) -> {ok, nil} | {error, interest_error()}.
check_compounds(N) ->
gleam@bool:guard(
N =< 0,
{error, compounds_out_of_range},
fun() -> {ok, nil} end
).
-file("src/finanza/interest.gleam", 337).
-spec check_digits(integer()) -> {ok, nil} | {error, interest_error()}.
check_digits(D) ->
gleam@bool:guard(D < 0, {error, negative_digits}, fun() -> {ok, nil} end).
-file("src/finanza/interest.gleam", 324).
-spec check_periods(integer()) -> {ok, nil} | {error, interest_error()}.
check_periods(P) ->
gleam@bool:guard(
(P =< 0) orelse (P > 1200),
{error, periods_out_of_range},
fun() -> {ok, nil} end
).
-file("src/finanza/interest.gleam", 44).
?DOC(" Simple interest: `I = P × r × t`.\n").
-spec simple_interest(
finanza@decimal:decimal(),
finanza@decimal:decimal(),
integer(),
integer()
) -> {ok, finanza@decimal:decimal()} | {error, interest_error()}.
simple_interest(Principal, Rate, Periods, Digits) ->
gleam@result:'try'(
check_principal(Principal),
fun(_) ->
gleam@result:'try'(
check_rate(Rate),
fun(_) ->
gleam@result:'try'(
check_periods(Periods),
fun(_) ->
gleam@result:'try'(
check_digits(Digits),
fun(_) ->
gleam@result:'try'(
begin
_pipe = finanza@decimal:multiply(
Principal,
Rate
),
gleam@result:map_error(
_pipe,
fun(Field@0) -> {arithmetic_error, Field@0} end
)
end,
fun(Pr) ->
gleam@result:map(
begin
_pipe@1 = finanza@decimal:multiply(
Pr,
finanza@decimal:from_int(
Periods
)
),
gleam@result:map_error(
_pipe@1,
fun(Field@0) -> {arithmetic_error, Field@0} end
)
end,
fun(Product) ->
finanza@decimal:round(
Product,
Digits,
half_even
)
end
)
end
)
end
)
end
)
end
)
end
).
-file("src/finanza/interest.gleam", 71).
?DOC(
" Future value under compound interest:\n"
"\n"
" ```text\n"
" FV = principal × (1 + annual_rate / compounds_per_year)^(compounds_per_year × years)\n"
" ```\n"
).
-spec compound_interest(
finanza@decimal:decimal(),
finanza@decimal:decimal(),
integer(),
integer(),
integer()
) -> {ok, finanza@decimal:decimal()} | {error, interest_error()}.
compound_interest(Principal, Annual_rate, Years, Compounds_per_year, Digits) ->
gleam@result:'try'(
check_principal(Principal),
fun(_) ->
gleam@result:'try'(
check_rate(Annual_rate),
fun(_) ->
gleam@result:'try'(
check_periods(Years),
fun(_) ->
gleam@result:'try'(
check_compounds(Compounds_per_year),
fun(_) ->
gleam@result:'try'(
check_digits(Digits),
fun(_) ->
Total_periods = Years * Compounds_per_year,
gleam@result:'try'(
check_periods(Total_periods),
fun(_) ->
Work_digits = gleam@int:min(
Digits + 4,
6
),
gleam@result:'try'(
begin
_pipe = finanza@decimal:divide(
Annual_rate,
finanza@decimal:from_int(
Compounds_per_year
),
Work_digits,
half_even
),
gleam@result:map_error(
_pipe,
fun(Field@0) -> {arithmetic_error, Field@0} end
)
end,
fun(Rate_per_period) ->
gleam@result:'try'(
growth_factor(
Rate_per_period,
Total_periods,
Work_digits
),
fun(Growth) ->
gleam@result:map(
begin
_pipe@1 = finanza@decimal:multiply(
Principal,
Growth
),
gleam@result:map_error(
_pipe@1,
fun(Field@0) -> {arithmetic_error, Field@0} end
)
end,
fun(
Product
) ->
finanza@decimal:round(
Product,
Digits,
half_even
)
end
)
end
)
end
)
end
)
end
)
end
)
end
)
end
)
end
).
-file("src/finanza/interest.gleam", 109).
?DOC(" Future value of `present` after `periods` periods at `rate_per_period`.\n").
-spec future_value(
finanza@decimal:decimal(),
finanza@decimal:decimal(),
integer(),
integer()
) -> {ok, finanza@decimal:decimal()} | {error, interest_error()}.
future_value(Present, Rate_per_period, Periods, Digits) ->
gleam@result:'try'(
check_rate(Rate_per_period),
fun(_) ->
gleam@result:'try'(
check_periods(Periods),
fun(_) ->
gleam@result:'try'(
check_digits(Digits),
fun(_) ->
Work_digits = gleam@int:min(Digits + 4, 6),
gleam@result:'try'(
growth_factor(
Rate_per_period,
Periods,
Work_digits
),
fun(Growth) ->
gleam@result:map(
begin
_pipe = finanza@decimal:multiply(
Present,
Growth
),
gleam@result:map_error(
_pipe,
fun(Field@0) -> {arithmetic_error, Field@0} end
)
end,
fun(Product) ->
finanza@decimal:round(
Product,
Digits,
half_even
)
end
)
end
)
end
)
end
)
end
).
-file("src/finanza/interest.gleam", 132).
?DOC(
" Present value of `future` discounted at `rate_per_period` for\n"
" `periods` periods.\n"
).
-spec present_value(
finanza@decimal:decimal(),
finanza@decimal:decimal(),
integer(),
integer()
) -> {ok, finanza@decimal:decimal()} | {error, interest_error()}.
present_value(Future, Rate_per_period, Periods, Digits) ->
gleam@result:'try'(
check_rate(Rate_per_period),
fun(_) ->
gleam@result:'try'(
check_periods(Periods),
fun(_) ->
gleam@result:'try'(
check_digits(Digits),
fun(_) ->
Work_digits = gleam@int:min(Digits + 4, 6),
gleam@result:'try'(
growth_factor(
Rate_per_period,
Periods,
Work_digits
),
fun(Growth) ->
gleam@result:map(
begin
_pipe = finanza@decimal:divide(
Future,
Growth,
Digits,
half_even
),
gleam@result:map_error(
_pipe,
fun(Field@0) -> {arithmetic_error, Field@0} end
)
end,
fun(Quotient) -> Quotient end
)
end
)
end
)
end
)
end
).
-file("src/finanza/interest.gleam", 207).
-spec amortising_payment(
finanza@decimal:decimal(),
finanza@decimal:decimal(),
integer(),
integer()
) -> {ok, finanza@decimal:decimal()} | {error, interest_error()}.
amortising_payment(Principal, Rate, Periods, Digits) ->
Work_digits = gleam@int:min(Digits + 4, 6),
gleam@result:'try'(
growth_factor(Rate, Periods, Work_digits),
fun(Growth) ->
gleam@result:'try'(
begin
_pipe = finanza@decimal:divide(
finanza@decimal:one(),
Growth,
Work_digits,
half_even
),
gleam@result:map_error(
_pipe,
fun(Field@0) -> {arithmetic_error, Field@0} end
)
end,
fun(Inv_growth) ->
gleam@result:'try'(
begin
_pipe@1 = finanza@decimal:subtract(
finanza@decimal:one(),
Inv_growth
),
gleam@result:map_error(
_pipe@1,
fun(Field@0) -> {arithmetic_error, Field@0} end
)
end,
fun(Denominator) ->
gleam@result:'try'(
begin
_pipe@2 = finanza@decimal:multiply(
Principal,
Rate
),
gleam@result:map_error(
_pipe@2,
fun(Field@0) -> {arithmetic_error, Field@0} end
)
end,
fun(Numerator) ->
_pipe@3 = finanza@decimal:divide(
Numerator,
Denominator,
Digits,
half_even
),
gleam@result:map_error(
_pipe@3,
fun(Field@0) -> {arithmetic_error, Field@0} end
)
end
)
end
)
end
)
end
).
-file("src/finanza/interest.gleam", 167).
?DOC(
" Periodic payment for a fully-amortising loan:\n"
"\n"
" ```text\n"
" PMT = principal × rate / (1 - (1 + rate)^(-periods))\n"
" ```\n"
"\n"
" When `rate_per_period` is zero, returns straight-line\n"
" `principal / periods`.\n"
).
-spec payment(
finanza@decimal:decimal(),
finanza@decimal:decimal(),
integer(),
integer()
) -> {ok, finanza@decimal:decimal()} | {error, interest_error()}.
payment(Principal, Rate_per_period, Periods, Digits) ->
gleam@result:'try'(
check_principal(Principal),
fun(_) ->
gleam@result:'try'(
check_rate(Rate_per_period),
fun(_) ->
gleam@result:'try'(
check_periods(Periods),
fun(_) ->
gleam@result:'try'(
check_digits(Digits),
fun(_) ->
gleam@bool:guard(
finanza@decimal:is_zero(Rate_per_period),
straight_line_payment(
Principal,
Periods,
Digits
),
fun() ->
amortising_payment(
Principal,
Rate_per_period,
Periods,
Digits
)
end
)
end
)
end
)
end
)
end
).
-file("src/finanza/interest.gleam", 250).
?DOC(
" Effective annual rate from a nominal rate compounded\n"
" `compounds_per_year` times per year:\n"
"\n"
" ```text\n"
" EAR = (1 + nominal_rate / compounds_per_year)^compounds_per_year - 1\n"
" ```\n"
).
-spec effective_annual_rate(finanza@decimal:decimal(), integer(), integer()) -> {ok,
finanza@decimal:decimal()} |
{error, interest_error()}.
effective_annual_rate(Nominal_rate, Compounds_per_year, Digits) ->
gleam@result:'try'(
check_rate(Nominal_rate),
fun(_) ->
gleam@result:'try'(
check_compounds(Compounds_per_year),
fun(_) ->
gleam@result:'try'(
check_digits(Digits),
fun(_) ->
Work_digits = gleam@int:min(Digits + 4, 6),
gleam@result:'try'(
begin
_pipe = finanza@decimal:divide(
Nominal_rate,
finanza@decimal:from_int(
Compounds_per_year
),
Work_digits,
half_even
),
gleam@result:map_error(
_pipe,
fun(Field@0) -> {arithmetic_error, Field@0} end
)
end,
fun(Rate_per_period) ->
gleam@result:'try'(
growth_factor(
Rate_per_period,
Compounds_per_year,
Work_digits
),
fun(Growth) ->
gleam@result:map(
begin
_pipe@1 = finanza@decimal:subtract(
Growth,
finanza@decimal:one()
),
gleam@result:map_error(
_pipe@1,
fun(Field@0) -> {arithmetic_error, Field@0} end
)
end,
fun(Ear_raw) ->
finanza@decimal:round(
Ear_raw,
Digits,
half_even
)
end
)
end
)
end
)
end
)
end
)
end
).