diff --git a/README.md b/README.md index 18d97f6..324b946 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,14 @@ PV(rate, n, pmt, fv float64, when Timing) float64 PMT(rate, n, pv, fv float64, when Timing) float64 NPer(rate, pmt, pv, fv float64, when Timing) (float64, error) Rate(n, pmt, pv, fv float64, when Timing) (float64, error) +SolveTVM(n, i, pv, pmt, fv float64, when Timing) (float64, error) +``` + +`SolveTVM` is the unified solver — pass `math.NaN()` for exactly one variable. + +```go +i, _ := engine.SolveTVM(360, math.NaN(), 300000, -1798.65, 0, engine.End) +fmt.Printf("%.4f%%\n", i*12*100) // 6.00% APR ``` ### Cash flows diff --git a/engine/finance.go b/engine/finance.go index 7005d18..a6219e7 100644 --- a/engine/finance.go +++ b/engine/finance.go @@ -92,6 +92,45 @@ func Rate(n, pmt, pv, fv float64, when Timing) (float64, error) { return 0, ErrNoConvergence } +// SolveTVM solves for the missing time-value-of-money variable. Pass +// math.NaN() for exactly one of n, i, pv, pmt, or fv. i is the periodic +// rate as a fraction (0.05 = 5%). The result is the solved value or an +// error if the solver fails to converge. +func SolveTVM(n, i, pv, pmt, fv float64, when Timing) (float64, error) { + var missing int + if math.IsNaN(n) { + missing++ + } + if math.IsNaN(i) { + missing++ + } + if math.IsNaN(pv) { + missing++ + } + if math.IsNaN(pmt) { + missing++ + } + if math.IsNaN(fv) { + missing++ + } + if missing != 1 { + return 0, ErrNoSolution + } + switch { + case math.IsNaN(n): + return NPer(i, pmt, pv, fv, when) + case math.IsNaN(i): + return Rate(n, pmt, pv, fv, when) + case math.IsNaN(pv): + return PV(i, n, pmt, fv, when), nil + case math.IsNaN(pmt): + return PMT(i, n, pv, fv, when), nil + case math.IsNaN(fv): + return FV(i, n, pv, pmt, when), nil + } + return 0, ErrNoSolution +} + // NPV returns the net present value of a cash-flow stream discounted at rate // (a fraction). cashflows[0] is the period-0 flow (typically the initial // outlay); cashflows[k] occurs at the end of period k. diff --git a/engine/financial.go b/engine/financial.go index 1f713ac..c7f3d85 100644 --- a/engine/financial.go +++ b/engine/financial.go @@ -8,45 +8,33 @@ func (e *Engine) SetPV() { e.FinPV = e.X } func (e *Engine) SetPMT() { e.FinPMT = e.X } func (e *Engine) SetFV() { e.FinFV = e.X } -func (e *Engine) SolveN() { - n, err := NPer(e.FinI/100, e.FinPMT, e.FinPV, e.FinFV, Timing(e.Flags.Begin)) +func (e *Engine) SolveN() { e.solve(NaNf64(), e.FinI/100, e.FinPV, e.FinPMT, e.FinFV) } +func (e *Engine) SolveI() { + v, err := SolveTVM(e.FinN, NaNf64(), e.FinPV, e.FinPMT, e.FinFV, Timing(e.Flags.Begin)) if err != nil { e.X = math.NaN() return } e.Push() - e.X = clamp(n) + e.X = clamp(v * 100) e.Flags.StackLift = true } +func (e *Engine) SolvePV() { e.solve(e.FinN, e.FinI/100, NaNf64(), e.FinPMT, e.FinFV) } +func (e *Engine) SolvePMT() { e.solve(e.FinN, e.FinI/100, e.FinPV, NaNf64(), e.FinFV) } +func (e *Engine) SolveFV() { e.solve(e.FinN, e.FinI/100, e.FinPV, e.FinPMT, NaNf64()) } -func (e *Engine) SolveI() { - r, err := Rate(e.FinN, e.FinPMT, e.FinPV, e.FinFV, Timing(e.Flags.Begin)) +func (e *Engine) solve(n, i, pv, pmt, fv float64) { + v, err := SolveTVM(n, i, pv, pmt, fv, Timing(e.Flags.Begin)) if err != nil { e.X = math.NaN() return } e.Push() - e.X = clamp(r * 100) - e.Flags.StackLift = true -} - -func (e *Engine) SolvePV() { - e.Push() - e.X = clamp(tvmPV(e.FinI/100, e.FinN, e.FinPMT, e.FinFV, e.Flags.Begin)) - e.Flags.StackLift = true -} - -func (e *Engine) SolvePMT() { - e.Push() - e.X = clamp(tvmPMT(e.FinI/100, e.FinN, e.FinPV, e.FinFV, e.Flags.Begin)) + e.X = clamp(v) e.Flags.StackLift = true } -func (e *Engine) SolveFV() { - e.Push() - e.X = clamp(tvmFV(e.FinI/100, e.FinN, e.FinPV, e.FinPMT, e.Flags.Begin)) - e.Flags.StackLift = true -} +func NaNf64() float64 { return math.NaN() } func tvmPV(i, n, pmt, fv float64, begin bool) float64 { if isZero(i) {