diff --git a/src/app/components/FireCalculatorForm.tsx b/src/app/components/FireCalculatorForm.tsx
index ea77d75..bbd7dc7 100644
--- a/src/app/components/FireCalculatorForm.tsx
+++ b/src/app/components/FireCalculatorForm.tsx
@@ -40,7 +40,12 @@ const formSchema = z.object({
desiredMonthlyAllowance: z.coerce
.number()
.min(0, "Monthly allowance must be a non-negative number"),
- swr: z.coerce.number().min(0.1, "Withdrawal rate must be at least 0.1%"),
+ inflationRate: z.coerce
+ .number()
+ .min(0, "Inflation rate must be a non-negative number"),
+ lifeExpectancy: z.coerce
+ .number()
+ .min(50, "Life expectancy must be at least 50"),
});
// Type for form values
@@ -49,6 +54,8 @@ type FormValues = z.infer;
interface CalculationResult {
fireNumber: number | null;
retirementAge: number | null;
+ inflationAdjustedAllowance: number | null;
+ retirementYears: number | null;
error?: string;
}
@@ -59,67 +66,169 @@ export default function FireCalculatorForm() {
const form = useForm({
resolver: zodResolver(formSchema),
defaultValues: {
- startingCapital: 10000,
- monthlySavings: 500,
- currentAge: 30,
+ startingCapital: 50000,
+ monthlySavings: 1500,
+ currentAge: 25,
cagr: 7,
desiredMonthlyAllowance: 2000,
- swr: 4,
+ inflationRate: 2,
+ lifeExpectancy: 90,
},
});
function onSubmit(values: FormValues) {
setResult(null); // Reset previous results
- const sc = values.startingCapital;
- const ms = values.monthlySavings;
- const ca = values.currentAge;
- const annualRate = values.cagr / 100;
- const monthlyAllowance = values.desiredMonthlyAllowance;
- const safeWithdrawalRate = values.swr / 100;
+ const startingCapital = values.startingCapital;
+ const monthlySavings = values.monthlySavings;
+ const currentAge = values.currentAge;
+ const annualGrowthRate = values.cagr / 100;
+ const initialMonthlyAllowance = values.desiredMonthlyAllowance;
+ const annualInflation = values.inflationRate / 100;
+ const lifeExpectancy = values.lifeExpectancy;
- // Calculate FIRE number (the amount needed for retirement)
- const fireNumber = (monthlyAllowance * 12) / safeWithdrawalRate;
+ const monthlyGrowthRate = Math.pow(1 + annualGrowthRate, 1 / 12) - 1;
+ const monthlyInflationRate = Math.pow(1 + annualInflation, 1 / 12) - 1;
+ const maxIterations = 1000; // Safety limit for iterations
- let currentCapital = sc;
- let age = ca;
- const monthlyRate = Math.pow(1 + annualRate, 1 / 12) - 1;
- const maxYears = 100; // Set a limit to prevent infinite loops
+ // Binary search for the required retirement capital
+ let low = initialMonthlyAllowance * 12; // Minimum: one year of expenses
+ let high = initialMonthlyAllowance * 12 * 100; // Maximum: hundred years of expenses
+ let requiredCapital = 0;
+ let retirementAge = 0;
+ let finalInflationAdjustedAllowance = 0;
- if (currentCapital >= fireNumber) {
- setResult({ fireNumber, retirementAge: age });
- return;
- }
+ // First, find when retirement is possible with accumulation phase
+ let canRetire = false;
+ let currentCapital = startingCapital;
+ let age = currentAge;
+ let monthlyAllowance = initialMonthlyAllowance;
+ let iterations = 0;
- for (let year = 0; year < maxYears; year++) {
- const capitalAtYearStart = currentCapital;
+ // Accumulation phase simulation
+ while (age < lifeExpectancy && iterations < maxIterations) {
+ // Simulate one year of saving and growth
for (let month = 0; month < 12; month++) {
- currentCapital += ms;
- currentCapital *= 1 + monthlyRate;
+ currentCapital += monthlySavings;
+ currentCapital *= 1 + monthlyGrowthRate;
+ // Update allowance for inflation
+ monthlyAllowance *= 1 + monthlyInflationRate;
}
age++;
+ iterations++;
- if (currentCapital >= fireNumber) {
- setResult({ fireNumber, retirementAge: age });
- return;
+ // Check each possible retirement capital target through binary search
+ const mid = (low + high) / 2;
+ if (high - low < 1) {
+ // Binary search converged
+ requiredCapital = mid;
+ break;
}
- // Prevent infinite loop if savings don't outpace growth required
- if (currentCapital <= capitalAtYearStart && ms <= 0) {
- setResult({
- fireNumber: null,
- retirementAge: null,
- error: "Cannot reach FIRE goal with current savings and growth rate.",
- });
- return;
+
+ // Test if this retirement capital is sufficient
+ let testCapital = mid;
+ let testAge = age;
+ let testAllowance = monthlyAllowance;
+ let isSufficient = true;
+
+ // Simulate retirement phase with this capital
+ while (testAge < lifeExpectancy) {
+ for (let month = 0; month < 12; month++) {
+ // Withdraw inflation-adjusted allowance
+ testCapital -= testAllowance;
+ // Grow remaining capital
+ testCapital *= 1 + monthlyGrowthRate;
+ // Adjust allowance for inflation
+ testAllowance *= 1 + monthlyInflationRate;
+ }
+ testAge++;
+
+ // Check if we've depleted capital before life expectancy
+ if (testCapital <= 0) {
+ isSufficient = false;
+ break;
+ }
+ }
+
+ if (isSufficient) {
+ high = mid; // This capital or less might be enough
+ if (currentCapital >= mid) {
+ // We can retire now with this capital
+ canRetire = true;
+ retirementAge = age;
+ requiredCapital = mid;
+ finalInflationAdjustedAllowance = monthlyAllowance;
+ break;
+ }
+ } else {
+ low = mid; // We need more capital
}
}
- // If loop finishes without reaching FIRE number
- setResult({
- fireNumber: null,
- retirementAge: null,
- error: `Could not reach FIRE goal within ${maxYears.toString()} years.`,
- });
+ // If we didn't find retirement possible in the loop
+ if (!canRetire && iterations < maxIterations) {
+ // Continue accumulation phase until we reach sufficient capital
+ while (age < lifeExpectancy && iterations < maxIterations) {
+ // Simulate one year
+ for (let month = 0; month < 12; month++) {
+ currentCapital += monthlySavings;
+ currentCapital *= 1 + monthlyGrowthRate;
+ monthlyAllowance *= 1 + monthlyInflationRate;
+ }
+ age++;
+ iterations++;
+
+ // Test with current capital
+ let testCapital = currentCapital;
+ let testAge = age;
+ let testAllowance = monthlyAllowance;
+ let isSufficient = true;
+
+ // Simulate retirement with current capital
+ while (testAge < lifeExpectancy) {
+ for (let month = 0; month < 12; month++) {
+ testCapital -= testAllowance;
+ testCapital *= 1 + monthlyGrowthRate;
+ testAllowance *= 1 + monthlyInflationRate;
+ }
+ testAge++;
+
+ if (testCapital <= 0) {
+ isSufficient = false;
+ break;
+ }
+ }
+
+ if (isSufficient) {
+ canRetire = true;
+ retirementAge = age;
+ requiredCapital = currentCapital;
+ finalInflationAdjustedAllowance = monthlyAllowance;
+ break;
+ }
+ }
+ }
+
+ if (canRetire) {
+ setResult({
+ fireNumber: requiredCapital,
+ retirementAge: retirementAge,
+ inflationAdjustedAllowance: finalInflationAdjustedAllowance,
+ retirementYears: lifeExpectancy - retirementAge,
+ error: undefined,
+ });
+ } else {
+ setResult({
+ fireNumber: null,
+ retirementAge: null,
+ inflationAdjustedAllowance: null,
+ retirementYears: null,
+ error:
+ iterations >= maxIterations
+ ? "Calculation exceeded maximum iterations."
+ : "Cannot reach FIRE goal before life expectancy with current parameters.",
+ });
+ }
}
// Helper function to format currency
@@ -218,7 +327,7 @@ export default function FireCalculatorForm() {
render={({ field }) => (
- Desired Monthly Allowance in Retirement
+ Desired Monthly Allowance (Today's Value)
(
- Safe Withdrawal Rate (%)
+ Annual Inflation Rate (%)
)}
/>
+ (
+
+ Life Expectancy (Age)
+
+
+
+
+
+ )}
+ />
+ {result.inflationAdjustedAllowance && (
+
+
+
+ {formatCurrency(result.inflationAdjustedAllowance)}
+
+
+ )}
+ {result.retirementYears && (
+
+
+
+ {result.retirementYears}
+
+
+ )}
)}