Skip to content

fix(xlsx): honor PMT/FV/PV/NPER type argument for annuity-due - #232

Open
mvanhorn wants to merge 1 commit into
iOfficeAI:mainfrom
mvanhorn:fix/213-financial-functions-type-argument
Open

fix(xlsx): honor PMT/FV/PV/NPER type argument for annuity-due#232
mvanhorn wants to merge 1 commit into
iOfficeAI:mainfrom
mvanhorn:fix/213-financial-functions-type-argument

Conversation

@mvanhorn

Copy link
Copy Markdown

Fixes #213.

Summary

Makes PMT, FV, PV, and NPER honor their optional 5th type argument (annuity-due). The four evaluators in src/officecli/Core/Formula/FormulaEvaluator.Functions.cs now read args[4] and apply the standard beginning-of-period adjustment, so =PMT(0.05/12,12,1000,0,1) returns the annuity-due value instead of the ordinary-annuity value. One file, one root cause.

Background

Excel's PMT, FV, PV, and NPER accept a 5th type argument that controls when payments occur: 0 = end of each period (ordinary annuity, the default), any nonzero value = beginning of each period (annuity-due).

EvalPmt, EvalFv, EvalPv, and EvalNper only read args[0..3] and never read args[4], so the type argument was silently ignored: =PMT(0.05/12,12,1000,0,1) returned the ordinary-annuity result (-85.607...) instead of the annuity-due result (-85.252...) that Excel and LibreOffice produce. Every type=1 call returned the same number as type=0.

Fix

src/officecli/Core/Formula/FormulaEvaluator.Functions.cs only. Each of the four functions now reads an optional type from args[4] and applies the annuity-due adjustment derived from Excel's time-value-of-money identity:

fv + pv*(1+rate)^nper + pmt*(1 + rate*type)*((1+rate)^nper - 1)/rate = 0
  • PMT divides the payment by (1 + rate*type).
  • FV / PV multiply the payment-driven term by (1 + rate*type).
  • NPER solves with the effective payment pmt*(1 + rate*type).

type is normalized to 0/1 (any nonzero value is treated as beginning-of-period), matching Excel, which treats the argument as a boolean flag. The rate == 0 short-circuit branches are unchanged (timing has no effect at zero rate), and type=0/omitted-arg results are bit-for-bit identical to before, so the common case is unaffected. PPMT's internal delegation to PMT is kept on the ordinary-annuity path so PPMT output is unchanged (annuity-due for PPMT/IPMT is a separate concern, out of scope for this issue).

This is one atomic change: a single root cause (the uniformly-ignored type parameter) across one cohesive function family.

Validation (before / after)

Built locally (dotnet build src/officecli/officecli.csproj) and ran the CLI. Command sequence:

officecli create demo.xlsx
officecli set demo.xlsx /Sheet1/A1 --prop formula="PMT(0.05/12,12,1000,0,0)"
officecli set demo.xlsx /Sheet1/A2 --prop formula="PMT(0.05/12,12,1000,0,1)"
officecli view demo.xlsx text

Before the fix (type ignored — A2 wrongly equals A1):

A1=-85.6074817884675
A2=-85.6074817884675   <- WRONG: type=1 returns the type=0 result

After the fix (A2 is the annuity-due value):

A1=-85.6074817884675   <- unchanged (type=0)
A2=-85.2522640217104   <- CORRECT: annuity-due

The same before/after pattern holds for FV, PV, and NPER (before: type=1 equals type=0; after: type=0 unchanged, type=1 differs). All post-fix values match Microsoft Excel and LibreOffice Calc:

Formula type=0 (unchanged) type=1 after fix Excel / LibreOffice
PMT(0.05/12,12,1000,0,t) -85.6074817884675 -85.2522640217104 -85.2522640217
FV(0.05/12,12,-100,0,t) 1227.88554916159 1233.00173894976 1233.00173894976
PV(0.05/12,12,-100,0,t) 1168.12220042982 1172.98937626494 1172.98937626494
NPER(0.05/12,-100,1000,0,t) 10.2355725207823 10.1921884395789 10.1921884395788

Nonzero type values other than 1 also match Excel's beginning-of-period behavior, e.g. PMT(0.05/12,12,1000,0,2) now returns -85.2522640217104 (was -84.89998...).

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

# Issue 3: [BUG] PMT/FV/PV/NPER silently ignore the 5th "type" argument (annuity-due)

1 participant