packages feed

fire-number-calc (empty) → 0.1.0

raw patch · 4 files changed

+127/−0 lines, 4 filesdep +base

Dependencies added: base

Files

+ 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+  )