I’m trying to practice some reduce_while and reduce functions in Elixir. I’m trying to model a debt payoff method using Elixir.
The problem I see is that I’m using the account.current_balance
for the calculate_step
function, but that value is not the real value that is updated on each loop as I pay off the debts.
I think that OK, I’ll carry over some variable in the reduce and then my brain starts getting looped and I go cross-eyed.
Do you guys have any tips on how I can complete this function?
Some pseudo-code of what I think I should do.
Here’s my code:
@doc """
Calculates the debt payoff using the snowball method with rollover.
The snowball method focuses on paying off the account with the lowest balance first,
while making minimum payments on all other accounts. Once the lowest balance is paid off,
the extra cash and the minimum payment from the paid off account are rolled over to the
next lowest balance account. This process is repeated until all accounts are paid off.
Although the snowball method is less efficient than the avalanche method, which prioritizes
paying off the account with the highest interest rate first, it can be more motivating for
some individuals.
The method follows these rules:
- Make minimum payments on all accounts.
- Apply any extra cash per month towards the account with the lowest balance.
- Once the lowest balance account is paid off, roll over the extra cash and the minimum
payment from the paid off account to the next lowest balance account.
- Repeat the process until all accounts are paid off.
Parameters:
- `extra_cash_per_month`: The additional amount available each month to pay off debt.
- `user`: The User.t() for whom the debt payoff is being calculated.
Returns:
- A list of maps representing the debt payoff steps for each account.
"""
def snowball_method(extra_cash_per_month, user) do
bank_accounts = Banks.list_nonzero_bank_accounts_for_user(user)
# Create a map where the keys are the account IDs and the values are empty lists
payoff_steps =
bank_accounts
|> Enum.map(fn account -> {account.id, []} end)
|> Enum.into(%{})
result =
Enum.reduce_while(
1..3,
%{
bank_accounts: bank_accounts,
payoff_steps: payoff_steps,
rollover_amount: Money.new(:USD, 0)
},
fn _i, acc ->
result =
acc.bank_accounts
|> Enum.sort_by(fn account -> Money.to_decimal(account.current_balance) end)
|> Enum.reduce(
%{
bank_accounts: acc.bank_accounts,
steps: [],
rollover_amount: Money.new(:USD, "0"),
leftover_amount: Money.new(:USD, "0"),
extra_cash_per_month: extra_cash_per_month
},
fn account, steps_accumulator ->
IO.inspect(steps_accumulator, label: "======== STARTING WITH ===========")
rollover_amount = get_rollover_amount(steps_accumulator.bank_accounts)
low_account = lowest_balance_account(steps_accumulator.bank_accounts)
# For the lowest balance account, pay the minimum payment and
# rollover_amount, leftover_amount and extra_cash_per_month.
step =
if account.id == low_account.id do
calculate_step(
# Here's the problem -- I'm using the `account.current_balance` which isn't carrying over the real balance as I calculate it each loop.
account.current_balance,
account.current_apr,
account.minimum_payment,
steps_accumulator.extra_cash_per_month,
steps_accumulator.rollover_amount
)
else
# For other accounts, pay the minimum_payment. Only if
# if the account has a positive non-zero balance.
calculate_step(
account.current_balance,
account.current_apr,
account.minimum_payment,
Money.new(:USD, "0"),
steps_accumulator.rollover_amount
)
end
step = Map.put(step, :account_id, account.id)
# Grab leftover funds from the step and add it to the leftover_amount
# in the steps_accumulator.
leftover_funds = step.leftover_funds
leftover_amount = Money.add!(steps_accumulator.leftover_amount, leftover_funds)
steps_accumulator = Map.put(steps_accumulator, :leftover_amount, leftover_amount)
# Finally update the account's balance with the new_balance from the step
# in the steps_accumulator.
updated_bank_accounts =
Enum.map(steps_accumulator.bank_accounts, fn account ->
if account.id == step.account_id do
%{account | current_balance: step.new_balance}
else
account
end
end)
steps_accumulator =
Map.put(steps_accumulator, :bank_accounts, updated_bank_accounts)
IO.inspect(steps_accumulator)
IO.inspect("=============")
steps_accumulator
end
)
#
{:cont, acc}
end
)
result
end
@doc """
Adds up every minimum payment from all accounts with a balance of 0.
"""
defp get_rollover_amount(bank_accounts) do
Enum.reduce(bank_accounts, Money.new(:USD, 0), fn account, accumulator ->
if Money.zero?(account.current_balance) do
Money.add!(accumulator, account.minimum_payment)
else
accumulator
end
end)
end
@doc """
Calculates the next step in the debt payoff process for a single account balance.
This function takes the following parameters:
- `balance`: The current balance of the account.
- `apr`: The Annual Percentage Rate (APR) of the account.
- `scheduled_minimum_payment`: The minimum payment required for the account.
- `extra_cash_per_month`: (Optional) The additional amount the user has allocated to pay off debts.
- `rollover_amount`: (Optional) The minimum payment from other paid off accounts, used when rollover is enabled.
The function returns a map containing the following keys:
- `:new_balance`: The updated balance after applying the payment.
- `:interest_paid`: The amount of interest paid in this step.
- `:principal_paid`: The amount of principal paid in this step.
- `:old_balance`: The balance before applying the payment.
- `:leftover_funds`: The amount of leftover funds after applying the payment.
## Examples
iex> calculate_step(Money.new(:USD, "100"), Decimal.new("2.5"), Money.new(:USD, "20"), Money.new(:USD, "120"))
%{
interest_paid: Money.new(:USD, "0.2083"),
leftover_funds: Money.new(:USD, "119.7917"),
new_balance: Money.new(:USD, "0"),
old_balance: Money.new(:USD, "100"),
principal_paid: Money.new(:USD, "100")
}
"""
def calculate_step(
balance,
apr,
scheduled_minimum_payment,
extra_cash_per_month \\ Money.new(:USD, 0),
rollover_amount \\ Money.new(:USD, 0)
) do
interest_rate = monthly_interest_rate_for_apr(apr)
interest_payment =
Money.mult!(balance, interest_rate) |> Money.div!(100) |> Money.round(currency_digits: 4)
principal_payment =
Money.sub!(scheduled_minimum_payment, interest_payment)
|> Money.add!(extra_cash_per_month)
|> Money.add!(rollover_amount)
|> Money.round(currency_digits: 4)
{principal_payment, leftover_funds} =
if Money.compare(principal_payment, balance) == :gt do
extras = Money.add!(extra_cash_per_month, rollover_amount)
{balance, Money.sub!(extras, interest_payment) |> Money.round(currency_digits: 4)}
else
{principal_payment, Money.new(:USD, "0")}
end
new_balance = Money.sub!(balance, principal_payment) |> Money.round(currency_digits: 4)
new_balance =
if Money.negative?(new_balance) do
Money.new(:USD, "0")
else
new_balance
end
%{
interest_paid: interest_payment,
principal_paid: principal_payment,
new_balance: new_balance,
old_balance: balance,
leftover_funds: leftover_funds
}
end
def lowest_balance_account([]), do: nil
def lowest_balance_account(accounts) do
positive_accounts =
accounts
|> Enum.filter(fn account -> Money.positive?(account.current_balance) end)
if positive_accounts == [] do
nil
else
positive_accounts
|> Enum.min_by(fn account -> account.current_balance end)
end
end
def monthly_interest_rate_for_apr(apr) do
Decimal.div(apr, 12)
|> Decimal.round(4)
end
end