financial-calcs
Version:
Reusable financial calculation library for FERS, Social Security, retirement savings, and mortgage amortization
139 lines • 6.08 kB
JavaScript
export function validateRetirementSavingsInput(input) {
const errors = [];
const { startYear, birthYear, initialBalance, initialContribution, estimatedYield, estimatedWithdrawRate, contributionIncreaseRate, withdrawStartAge, yearsToProject, } = input;
if (startYear < 1900)
errors.push({ field: "startYear", message: "Start Year cannot be before 1900" });
if (birthYear < 1900)
errors.push({ field: "birthYear", message: "Birth Year cannot be before 1900" });
if (initialBalance < 0)
errors.push({ field: "initialBalance", message: "Initial balance cannot be negative" });
if (initialContribution < 0)
errors.push({ field: "initialContribution", message: "Contribution cannot be negative" });
if (estimatedYield < -100)
errors.push({ field: "estimatedYield", message: "Estimated yield cannot be less than -100%" });
if (estimatedWithdrawRate < 0)
errors.push({ field: "estimatedWithdrawRate", message: "Withdrawal rate cannot be negative" });
if (contributionIncreaseRate < -100)
errors.push({ field: "contributionIncreaseRate", message: "Contribution increase rate cannot be less than -100%" });
if (withdrawStartAge < 0 || withdrawStartAge > 80)
errors.push({ field: "withdrawStartAge", message: "Withdraw start age must be between 0 and 80" });
if (yearsToProject <= 0)
errors.push({ field: "yearsToProject", message: "Must project at least 1 year" });
return errors;
}
// --- Main Projection ---
export function calculateRetirementSavingsProjection(input) {
return calculateRetirementSavingsProjectionWithOverrides({ ...input, yearOverrides: {} });
}
export function calculateRetirementSavingsProjectionWithOverrides(input) {
const { startYear, birthYear, initialBalance, initialContribution, estimatedYield, estimatedWithdrawRate, contributionIncreaseRate, withdrawStartAge, yearsToProject, yearOverrides = {} } = input;
const errors = validateRetirementSavingsInput(input);
if (errors.length > 0) {
const err = new Error("Retirement Savings input validation failed");
err.validationErrors = errors;
throw err;
}
let balance = Math.max(initialBalance, 0);
let contribution = Math.max(initialContribution, 0);
const rows = [];
for (let i = 0; i < yearsToProject; i++) {
const year = startYear + i;
const age = year - birthYear;
const isWithdrawing = age >= withdrawStartAge;
const override = yearOverrides[year] || {};
const hasOverride = override.contribution !== undefined ||
override.yieldPercent !== undefined ||
override.withdrawRate !== undefined ||
override.annualWithdraw !== undefined ||
override.endingBalance !== undefined;
const beginningBalance = balance;
//
// ----- Contribution -----
//
if (override.contribution !== undefined) {
contribution = Math.max(override.contribution, 0);
}
else {
if (i > 0) {
contribution = isWithdrawing
? 0
: contribution * (1 + contributionIncreaseRate / 100);
}
else if (isWithdrawing) {
contribution = 0;
}
}
//
// ----- Withdrawal logic -----
//
let annualWithdraw = 0;
let withdrawRate = 0;
if (isWithdrawing) {
if (override.annualWithdraw !== undefined) {
annualWithdraw = Math.max(override.annualWithdraw, 0);
withdrawRate =
beginningBalance > 0
? (annualWithdraw / beginningBalance) * 100
: 0;
}
else if (override.withdrawRate !== undefined) {
withdrawRate = override.withdrawRate;
annualWithdraw = (withdrawRate / 100) * beginningBalance;
}
else {
withdrawRate = estimatedWithdrawRate;
annualWithdraw = (withdrawRate / 100) * beginningBalance;
}
}
//
// ----- Yield logic -----
//
let yieldPercent = override.yieldPercent ?? estimatedYield;
let yieldAmount = (yieldPercent / 100) * beginningBalance;
//
// ----- Normal ending balance (before override adjustments) -----
//
let endingBalance = beginningBalance + yieldAmount + contribution - annualWithdraw;
//
// ----- endingBalance Override (Highest precedence) -----
//
if (override.endingBalance !== undefined) {
const forcedEnding = Math.max(override.endingBalance, 0);
// Solve for yieldPercent needed to reach forced ending
// ending = beg + (beg * y%) + contrib − withdraw
// → y% = (ending - beg - contrib + withdraw) / beg * 100
if (beginningBalance > 0) {
yieldPercent =
((forcedEnding -
beginningBalance -
contribution +
annualWithdraw) /
beginningBalance) *
100;
yieldAmount = (yieldPercent / 100) * beginningBalance;
}
else {
// If beginningBalance is zero, yield cannot influence outcome
yieldPercent = 0;
yieldAmount = 0;
}
endingBalance = forcedEnding;
}
const monthlyWithdraw = annualWithdraw / 12;
rows.push({
year,
age,
beginningBalance,
contribution,
yieldPercent: Math.round(yieldPercent * 100) / 100,
withdrawRate: Math.round(withdrawRate * 100) / 100,
monthlyWithdraw,
annualWithdraw,
endingBalance,
hasOverride
});
balance = endingBalance;
}
return rows;
}
//# sourceMappingURL=savings.js.map