From 9e3e6c70db955b84260c76b1d8865e84dca0cdd0 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Thu, 16 Jul 2026 17:05:18 -0700 Subject: [PATCH] fix(xlsx): honor PMT/FV/PV/NPER type argument for annuity-due PMT, FV, PV, and NPER accept an optional 5th type argument (0 = payment at period end, the default; 1 = payment at beginning / annuity-due). The four evaluators only read args[0..3] and never read args[4], so type=1 silently returned the ordinary-annuity result. Read the optional type from args[4] (default 0) and apply the standard annuity-due adjustment (the (1 + rate*type) factor from Excel's TVM identity). type=0 and the omitted-5th-arg case are unchanged; the rate==0 short-circuits are left intact. PPMT's internal PMT delegation is kept on the ordinary-annuity path so its observable output does not change. --- .../Core/Formula/FormulaEvaluator.Functions.cs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/officecli/Core/Formula/FormulaEvaluator.Functions.cs b/src/officecli/Core/Formula/FormulaEvaluator.Functions.cs index 6b7e80a3c..1047f0e5c 100644 --- a/src/officecli/Core/Formula/FormulaEvaluator.Functions.cs +++ b/src/officecli/Core/Formula/FormulaEvaluator.Functions.cs @@ -1367,8 +1367,9 @@ private static bool MatchesAllCriteria(List<(FormulaResult?[] Range, string Crit if (args.Count < 3) return null; double rate = args[0] is FormulaResult r ? r.AsNumber() : 0, nper = args[1] is FormulaResult r2 ? r2.AsNumber() : 0, pv = args[2] is FormulaResult r3 ? r3.AsNumber() : 0; var fv = args.Count > 3 && args[3] is FormulaResult r4 ? r4.AsNumber() : 0; + var type = args.Count > 4 && args[4] is FormulaResult r5 && r5.AsNumber() != 0 ? 1 : 0; if (rate == 0) return FR(-(pv + fv) / nper); - return FR(-(rate * (pv * Math.Pow(1 + rate, nper) + fv) / (Math.Pow(1 + rate, nper) - 1))); + return FR(-(rate * (pv * Math.Pow(1 + rate, nper) + fv) / (Math.Pow(1 + rate, nper) - 1)) / (1 + rate * type)); } private static FormulaResult? EvalFv(List args) @@ -1376,8 +1377,9 @@ private static bool MatchesAllCriteria(List<(FormulaResult?[] Range, string Crit if (args.Count < 3) return null; double rate = args[0] is FormulaResult r ? r.AsNumber() : 0, nper = args[1] is FormulaResult r2 ? r2.AsNumber() : 0, pmt = args[2] is FormulaResult r3 ? r3.AsNumber() : 0; var pv = args.Count > 3 && args[3] is FormulaResult r4 ? r4.AsNumber() : 0; + var type = args.Count > 4 && args[4] is FormulaResult r5 && r5.AsNumber() != 0 ? 1 : 0; if (rate == 0) return FR(-(pv + pmt * nper)); - return FR(-(pv * Math.Pow(1 + rate, nper) + pmt * (Math.Pow(1 + rate, nper) - 1) / rate)); + return FR(-(pv * Math.Pow(1 + rate, nper) + pmt * (1 + rate * type) * (Math.Pow(1 + rate, nper) - 1) / rate)); } private static FormulaResult? EvalPv(List args) @@ -1385,8 +1387,9 @@ private static bool MatchesAllCriteria(List<(FormulaResult?[] Range, string Crit if (args.Count < 3) return null; double rate = args[0] is FormulaResult r ? r.AsNumber() : 0, nper = args[1] is FormulaResult r2 ? r2.AsNumber() : 0, pmt = args[2] is FormulaResult r3 ? r3.AsNumber() : 0; var fv = args.Count > 3 && args[3] is FormulaResult r4 ? r4.AsNumber() : 0; + var type = args.Count > 4 && args[4] is FormulaResult r5 && r5.AsNumber() != 0 ? 1 : 0; if (rate == 0) return FR(-(fv + pmt * nper)); - return FR(-(fv / Math.Pow(1 + rate, nper) + pmt * (1 - Math.Pow(1 + rate, -nper)) / rate)); + return FR(-(fv / Math.Pow(1 + rate, nper) + pmt * (1 + rate * type) * (1 - Math.Pow(1 + rate, -nper)) / rate)); } private static FormulaResult? EvalNper(List args) @@ -1394,8 +1397,10 @@ private static bool MatchesAllCriteria(List<(FormulaResult?[] Range, string Crit if (args.Count < 3) return null; double rate = args[0] is FormulaResult r ? r.AsNumber() : 0, pmt = args[1] is FormulaResult r2 ? r2.AsNumber() : 0, pv = args[2] is FormulaResult r3 ? r3.AsNumber() : 0; var fv = args.Count > 3 && args[3] is FormulaResult r4 ? r4.AsNumber() : 0; + var type = args.Count > 4 && args[4] is FormulaResult r5 && r5.AsNumber() != 0 ? 1 : 0; if (rate == 0) return pmt != 0 ? FR(-(pv + fv) / pmt) : null; - return FR(Math.Log((-fv * rate + pmt) / (pv * rate + pmt)) / Math.Log(1 + rate)); + var pmtEff = pmt * (1 + rate * type); + return FR(Math.Log((-fv * rate + pmtEff) / (pv * rate + pmtEff)) / Math.Log(1 + rate)); } private static FormulaResult? EvalNpv(List args) @@ -1431,7 +1436,7 @@ private static bool MatchesAllCriteria(List<(FormulaResult?[] Range, string Crit // as nper). var pmtArgs = new List { args[0], args[2], args[3] }; if (args.Count > 4) pmtArgs.Add(args[4]); - if (args.Count > 5) pmtArgs.Add(args[5]); + // Annuity-due support for PPMT/IPMT is out of scope; do not half-apply it through PMT. var pmt = EvalPmt(pmtArgs)?.AsNumber() ?? 0; var ipmt = EvalIpmt(args)?.AsNumber() ?? 0; return FR(pmt - ipmt);