fire-number-calc (empty) → 0.1.0
raw patch · 4 files changed
+127/−0 lines, 4 filesdep +base
Dependencies added: base
Files
- CHANGELOG.md +5/−0
- README.md +28/−0
- fire-number-calc.cabal +39/−0
- src/FireNumber/Calc.hs +55/−0
+ CHANGELOG.md view
@@ -0,0 +1,5 @@+# Change log++## 0.1.0++* Initial release: fireNumber, gapToTarget, yearsToTarget, budgetCost.
+ README.md view
@@ -0,0 +1,28 @@+# fire-number-calc++Fire number and timeline math for early retirement planning.++A fire number is annual spending divided by a withdrawal rate. The+honest version of the plan also counts the gap from today's balance to+that target and the years of saving it takes to close the gap when the+portfolio compounds at a fixed return.++The package follows the withdrawal-rate and sequence-of-returns math+published on [Firenomics](https://firenomics.com). It depends only on+base.++## Example++With $48,000 of annual spending at a 4% withdrawal rate:++* fire number: `fireNumber 48000 0.04` = $1,200,000+* gap from a $450,000 balance: `gapToTarget 450000 1200000` = $750,000+* years to close it saving $30,000 a year at a 6% return:+ `yearsToTarget 450000 30000 1200000 0.06 0` = 10 years+* the same plan with the spending budget growing 3% a year until+ retirement: 17 years++The last two numbers are the point. A budget that grows before+retirement pushes the crossing point out by seven working years in+this plan, and `budgetCost` returns both timelines so the comparison+is one call.
+ fire-number-calc.cabal view
@@ -0,0 +1,39 @@+cabal-version: 2.2+name: fire-number-calc+version: 0.1.0+synopsis: Fire number and timeline math for early retirement planning+description:+ The arithmetic behind a fire number: the balance a withdrawal rate has+ to support, the gap between today's balance and that target, and the+ years of saving it takes to close the gap when the portfolio compounds+ at a fixed return.+ .+ The timeline function also prices a growing budget: a spending target+ that rises each year before retirement pushes the crossing point out,+ and the difference between the flat and growing timelines is the+ working-year cost of that growth.+ .+ Companion to Firenomics, which publishes the withdrawal-rate and+ sequence-of-returns math these functions follow. Four pure functions+ on Doubles, no dependencies beyond base.+license: MIT+author: Poket Dev+maintainer: hello@poketdev.com+homepage: https://firenomics.com+bug-reports: https://github.com/scrapertweeter3-prog/fire-number-calc-hs/issues+copyright: 2026 Poket Dev LLC+category: Math+build-type: Simple+extra-doc-files: README.md+ , CHANGELOG.md++source-repository head+ type: git+ location: https://github.com/scrapertweeter3-prog/fire-number-calc-hs++library+ exposed-modules: FireNumber.Calc+ hs-source-dirs: src+ build-depends: base >=4 && <5+ default-language: Haskell2010+ ghc-options: -Wall
+ src/FireNumber/Calc.hs view
@@ -0,0 +1,55 @@+-- | Fire number math for early retirement planning.+--+-- A fire number is annual spending divided by a withdrawal rate, and the+-- honest version of the plan also counts the gap from your current+-- balance, the years of saving it takes to close, and what happens to+-- that timeline when the spending budget grows before you retire.+module FireNumber.Calc+ ( fireNumber+ , gapToTarget+ , yearsToTarget+ , budgetCost+ ) where++-- | The balance a withdrawal rate has to support a given annual spending.+-- A zero or negative rate guards to zero.+fireNumber :: Double -> Double -> Double+fireNumber annualSpending withdrawalRate+ | withdrawalRate <= 0 = 0+ | otherwise = annualSpending / withdrawalRate++-- | Dollars still missing between today's balance and the fire number.+-- Never negative.+gapToTarget :: Double -> Double -> Double+gapToTarget currentBalance target = max 0 (target - currentBalance)++-- | Whole years until the portfolio (balance plus annual saving,+-- compounding at 'returnRate') covers the fire number. The target itself+-- grows at 'spendingGrowth' each year, so a growing budget pushes the+-- crossing point out. Returns 0 when you are already there and positive+-- infinity when the budget outruns the savings forever.+yearsToTarget :: Double -> Double -> Double -> Double -> Double -> Double+yearsToTarget currentBalance annualSaving target returnRate spendingGrowth+ | currentBalance >= target = 0+ | otherwise = go (1 :: Int)+ where+ go n+ | n > 600 = 1 / 0+ | bal >= tgt = fromIntegral n+ | otherwise = go (n + 1)+ where+ grow = (1 + returnRate) ** fromIntegral n+ bal = currentBalance * grow+ + if returnRate > 0+ then annualSaving * (grow - 1) / returnRate+ else annualSaving * fromIntegral n+ tgt = target * (1 + spendingGrowth) ** fromIntegral n++-- | Both timelines: years to the target with a flat budget and with one+-- that grows each year until retirement. The difference is the price of+-- the growing budget in working years.+budgetCost :: Double -> Double -> Double -> Double -> Double -> (Double, Double)+budgetCost currentBalance annualSaving target returnRate spendingGrowth =+ ( yearsToTarget currentBalance annualSaving target returnRate 0+ , yearsToTarget currentBalance annualSaving target returnRate spendingGrowth+ )