-module(finanza@interest@amortization).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/finanza/interest/amortization.gleam").
-export([index/1, payment/1, interest/1, principal_paid/1, balance/1, schedule/4]).
-export_type([period/0, schedule_state/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(
" Amortization schedule generation for an amortising loan.\n"
"\n"
" The schedule lists, for each period, the periodic payment, the\n"
" portion of that payment that goes to interest, the portion that\n"
" reduces principal, and the resulting outstanding balance.\n"
).
-opaque period() :: {period,
integer(),
finanza@decimal:decimal(),
finanza@decimal:decimal(),
finanza@decimal:decimal(),
finanza@decimal:decimal()}.
-type schedule_state() :: {schedule_state,
list(period()),
finanza@decimal:decimal()}.
-file("src/finanza/interest/amortization.gleam", 29).
?DOC(" 1-based period number.\n").
-spec index(period()) -> integer().
index(P) ->
erlang:element(2, P).
-file("src/finanza/interest/amortization.gleam", 34).
?DOC(" Total payment for this period.\n").
-spec payment(period()) -> finanza@decimal:decimal().
payment(P) ->
erlang:element(3, P).
-file("src/finanza/interest/amortization.gleam", 39).
?DOC(" Interest component of the payment.\n").
-spec interest(period()) -> finanza@decimal:decimal().
interest(P) ->
erlang:element(4, P).
-file("src/finanza/interest/amortization.gleam", 44).
?DOC(" Principal component of the payment.\n").
-spec principal_paid(period()) -> finanza@decimal:decimal().
principal_paid(P) ->
erlang:element(5, P).
-file("src/finanza/interest/amortization.gleam", 49).
?DOC(" Remaining balance after this period.\n").
-spec balance(period()) -> finanza@decimal:decimal().
balance(P) ->
erlang:element(6, P).
-file("src/finanza/interest/amortization.gleam", 139).
-spec finalise_row(
integer(),
integer(),
finanza@decimal:decimal(),
finanza@decimal:decimal(),
finanza@decimal:decimal(),
finanza@decimal:decimal(),
integer()
) -> {ok,
{finanza@decimal:decimal(),
finanza@decimal:decimal(),
finanza@decimal:decimal()}} |
{error, finanza@interest:interest_error()}.
finalise_row(Index, Periods, Payment, Principal, Balance, Interest, Digits) ->
gleam@bool:guard(
Index /= Periods,
{ok, {Payment, Principal, Balance}},
fun() ->
Target_zero = finanza@decimal:new(0, - Digits),
gleam@result:'try'(
begin
_pipe = finanza@decimal:add(Principal, Balance),
gleam@result:map_error(
_pipe,
fun(Field@0) -> {arithmetic_error, Field@0} end
)
end,
fun(Adjusted_principal) ->
gleam@result:map(
begin
_pipe@1 = finanza@decimal:add(
Interest,
Adjusted_principal
),
gleam@result:map_error(
_pipe@1,
fun(Field@0) -> {arithmetic_error, Field@0} end
)
end,
fun(Adjusted_payment) ->
{Adjusted_payment, Adjusted_principal, Target_zero}
end
)
end
)
end
).
-file("src/finanza/interest/amortization.gleam", 87).
-spec build_schedule(
finanza@decimal:decimal(),
finanza@decimal:decimal(),
integer(),
integer(),
integer(),
schedule_state()
) -> {ok, schedule_state()} | {error, finanza@interest:interest_error()}.
build_schedule(Payment, Rate, Periods, Digits, Index, State) ->
gleam@bool:guard(
Index > Periods,
{ok, State},
fun() ->
gleam@result:'try'(
begin
_pipe = finanza@decimal:multiply(
erlang:element(3, State),
Rate
),
gleam@result:map_error(
_pipe,
fun(Field@0) -> {arithmetic_error, Field@0} end
)
end,
fun(Raw_interest) ->
Rounded_interest = finanza@decimal:round(
Raw_interest,
Digits,
half_even
),
gleam@result:'try'(
begin
_pipe@1 = finanza@decimal:subtract(
Payment,
Rounded_interest
),
gleam@result:map_error(
_pipe@1,
fun(Field@0) -> {arithmetic_error, Field@0} end
)
end,
fun(Principal_part) ->
gleam@result:'try'(
begin
_pipe@2 = finanza@decimal:subtract(
erlang:element(3, State),
Principal_part
),
gleam@result:map_error(
_pipe@2,
fun(Field@0) -> {arithmetic_error, Field@0} end
)
end,
fun(New_balance) ->
gleam@result:'try'(
finalise_row(
Index,
Periods,
Payment,
Principal_part,
New_balance,
Rounded_interest,
Digits
),
fun(_use0) ->
{Final_payment,
Final_principal,
Final_balance} = _use0,
Row = {period,
Index,
Final_payment,
Rounded_interest,
Final_principal,
Final_balance},
build_schedule(
Payment,
Rate,
Periods,
Digits,
Index + 1,
{schedule_state,
[Row |
erlang:element(2, State)],
Final_balance}
)
end
)
end
)
end
)
end
)
end
).
-file("src/finanza/interest/amortization.gleam", 59).
?DOC(
" Build a full amortisation schedule for an amortising loan.\n"
"\n"
" Each row in the returned list is a [`Period`](#Period). All\n"
" monetary values are rounded to `digits` decimal places with\n"
" `HalfEven`. The final row is adjusted so the outstanding balance\n"
" closes to zero exactly.\n"
).
-spec schedule(
finanza@decimal:decimal(),
finanza@decimal:decimal(),
integer(),
integer()
) -> {ok, list(period())} | {error, finanza@interest:interest_error()}.
schedule(Principal, Rate_per_period, Periods, Digits) ->
gleam@result:'try'(
finanza@interest:payment(Principal, Rate_per_period, Periods, Digits),
fun(Payment_amount) ->
Initial = {schedule_state, [], Principal},
gleam@result:map(
build_schedule(
Payment_amount,
Rate_per_period,
Periods,
Digits,
1,
Initial
),
fun(Built) -> lists:reverse(erlang:element(2, Built)) end
)
end
).