Skip to content

Prefix post-2007 function names with _xlfn so formulas work in Excel - #1742

Open
Mike-Crowley wants to merge 4 commits into
dfinke:masterfrom
Mike-Crowley:fix-1728-xlfn-prefix
Open

Prefix post-2007 function names with _xlfn so formulas work in Excel#1742
Mike-Crowley wants to merge 4 commits into
dfinke:masterfrom
Mike-Crowley:fix-1728-xlfn-prefix

Conversation

@Mike-Crowley

Copy link
Copy Markdown

Fixes #1728

Problem

Formulas using functions added to Excel after the 2007 file format was defined — IFS, CONCAT, TEXTJOIN, SWITCH, XLOOKUP, and ~150 others — show #NAME? when a file written by this module is opened in Excel, until each cell is manually re-entered (which is exactly the behavior reported in #1728). Calculate Now doesn't fix them, and Close-ExcelPackage -Calculate doesn't either.

Root cause

Excel stores these "future functions" in the sheet XML with an _xlfn. prefix (_xlfn.IFS, _xlfn.CONCAT; SORT and FILTER use _xlfn._xlws.). Excel adds the prefix invisibly when a formula is typed in and hides it on display — but when the formula is written directly into the file, as EPPlus does, the prefix must already be present. Without it, Excel resolves the name as an unknown defined name → #NAME?. Re-entering the cell makes Excel re-parse and store the prefixed form, which is why manual editing "fixes" it.

Verified with the issue's exact repro: the same formula stored as _xlfn.IFS(... _xlfn.CONCAT(...)) calculates to the expected value on open, with Excel displaying it exactly as the user wrote it. (The stray @ the issue also mentions is Excel's implicit-intersection marker being added around unrecognized names — it disappears once the functions resolve.)

Fix

  • New Private/XlFnFormula.ps1:
    • Expand-XlFnFormula inserts the prefixes into formula text. It leaves alone: "string literals", 'quoted sheet names', [structured references] (including nested [[#This Row],[Col]] forms and escaped chars), already-prefixed names, and user-defined names (MYCONCAT( etc.). Setting $env:NoXlFn disables the rewrite entirely.
    • Register-XlFnFunction registers _xlfn. aliases for every function EPPlus's calculation engine implements, so -Calculate keeps working for the functions it could calculate before (IFNA, RANK.AVG, STDEV.S, ISOWEEKNUM, …). Functions the engine never implemented still produce #NAME? in the cached value exactly as before — and since EPPlus flags formula cells for full calculation on load, Excel recomputes them correctly when the file opens.
  • The rewrite is applied at the four places the module writes cell formulas: Export-Excel (values starting with =), Set-ExcelRange (-Formula, -ArrayFormula, and -Value '=...'), Set-ExcelRow, and Set-ExcelColumn. Both -Calculate sites (Close-ExcelPackage, Export-Excel) register the aliases first.
  • The function table was generated empirically: every candidate function was entered into Excel (Microsoft 365) via COM and the stored XML read back, then cross-checked against the future-function lists used by XlsxWriter and PhpSpreadsheet. Notably NETWORKDAYS.INTL, WORKDAY.INTL, and ISO.CEILING are deliberately excluded — Excel stores them unprefixed despite what some references claim.

Also fixed in passing

Set-ExcelRange -Value '=SUM(1,2)' (a value starting with = is documented to be treated as a formula) actually wrote an empty formula: the code set $PSBoundParameters["Formula"] but then read the never-assigned $Formula variable. Now it works, with an explicit -Formula still taking precedence if both are supplied.

Performance

Compiled once at module load (~1 ms, RegexOptions.Compiled deliberately avoided), with a cheap pre-test so formulas containing no affected function pay ~4 µs each: exports without affected formulas are unchanged (10k rows benchmark: within noise of master), and a pathological 10k-rows-of-CONCAT export adds ~1 s — those cells produced broken files before.

Scope notes

  • Formulas set directly on EPPlus objects ($cell.Formula = ..., as in the issue's original snippet) bypass the module and are unaffected — routing them through Set-ExcelRange -Formula gets the fix.
  • Add-ConditionalFormatting, Add-ExcelDataValidationRule, and table TotalsRowFormula still write raw formula text (same pre-existing behavior); they could adopt Expand-XlFnFormula as a follow-up if desired.

Testing

  • 16 new Pester tests (__tests__/XlFnFormula.tests.ps1) — EPPlus-only, no Excel/COM required, so they run on the Linux/macOS CI. They cover the issue's exact formula, string/sheet-name/structured-ref protection, double-prefix idempotency, UDF names, _xlws functions, case preservation, all four cmdlet paths, -Calculate still computing prefixed functions, and the NoXlFn opt-out.
  • Full local suite: no new failures vs master (294 passed; the 5 failures present are pre-existing environmental ones, byte-identical on master).
  • End-to-end verified against real Excel (Microsoft 365) via COM on both PowerShell 7 and Windows PowerShell 5.1: the issue's repro calculates on open with no #NAME? and clean formula display; legacy-function files are byte-for-byte unaffected; a control file written with $env:NoXlFn set still reproduces the original bug, proving the rewrite is what fixes it.
  • The rewrite engine additionally survived a fuzz run (800 seeded iterations mixing quotes, escapes, brackets, prefixes, operators) with the invariant "output minus inserted prefixes == input" holding in every case.

🤖 Generated with Claude Code

Mike-Crowley and others added 2 commits July 29, 2026 11:27
Formulas using functions added to Excel after the 2007 file format was
defined (IFS, CONCAT, TEXTJOIN, XLOOKUP and many more) showed #NAME?
when a file written by this module was opened in Excel, until each cell
was manually re-entered. Excel stores such functions in the sheet XML
with an "_xlfn." prefix (SORT and FILTER as "_xlfn._xlws.") and expects
the prefix to be present; EPPlus writes formula text verbatim.

Formulas set through Export-Excel, Set-ExcelRange, Set-ExcelRow and
Set-ExcelColumn now have the prefix inserted automatically. String
literals, quoted sheet names, structured table references, already
prefixed names and user-defined names are left untouched, and setting
$env:NoXlFn disables the rewrite. The function list was generated by
entering every candidate function into Excel (Microsoft 365) via COM
and reading back what Excel stored; NETWORKDAYS.INTL, WORKDAY.INTL and
ISO.CEILING are excluded because Excel stores them unprefixed.

Because EPPlus's calculation engine looks functions up by the stored
name, Close-ExcelPackage -Calculate and Export-Excel -Calculate now
register each implemented function under its _xlfn. alias first, so
functions the engine could calculate before (IFNA, RANK.AVG, STDEV.S,
etc.) still calculate after prefixing.

Also fixes Set-ExcelRange -Value: a value beginning with "=" was
documented to be treated as a formula but wrote an empty formula,
because the code set $PSBoundParameters['Formula'] without setting the
$Formula variable it reads back.

The rewrite uses compiled regexes with a cheap pre-test, adding no
measurable cost to exports without affected formulas and ~0.1 ms per
rewritten formula cell.

Fixes dfinke#1728

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add Z.TEST, which was missing from the Excel 2010 group (verified via
Excel COM read-back and the XlsxWriter/PhpSpreadsheet lists), replace a
retired Microsoft support link, correct the comment describing why the
two replacement passes cannot interfere, and drop RegexOptions.Compiled
- it cost ~70ms at module load and measured no faster than the
interpreted engine on these patterns. The NoXlFn test now restores the
environment variable in AfterAll so a failure cannot leak it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Mike-Crowley

Copy link
Copy Markdown
Author

Note on the failing Azure Pipelines checks: same pre-existing pipeline issue as noted on #1741 — every job stops at CI/CI.ps1:113 with Invoke-Pester : A parameter cannot be found that matches parameter name 'OutputFile' (Pester v4 syntax vs the Pester 5+ now on the agents), identically to other open PRs (#1723, #1729). The GitHub Actions workflow is the one that reflects this PR's actual test results once approved to run; the new tests here are EPPlus-only so they run on all three of its platforms.

The ~150-name table in Private/XlFnFormula.ps1 came from entering every
candidate function into a real Excel via COM and reading back what
Excel stored in the worksheet XML. Committing the script makes the list
reproducible and reviewable rather than taken on trust: running it
against the installed Excel reports, for every candidate, the prefix
Excel stores and whether the module's table agrees (currently 171/171
match, including the controls and the deliberately unprefixed
NETWORKDAYS.INTL, WORKDAY.INTL and ISO.CEILING). The output snapshot is
committed alongside, and the table's header comment points to both.

The script requires Excel, so it is not a Pester file and never runs
in CI.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds automatic _xlfn. (and _xlfn._xlws.) prefixing for post-2007 Excel function names when formulas are written via ImportExcel cmdlets, preventing #NAME? errors when opening generated files in Excel (Fixes #1728). It also registers _xlfn.* aliases with EPPlus’ calculation engine so -Calculate continues to work for functions EPPlus already implements, and fixes a Set-ExcelRange -Value '=...' formula-handling bug.

Changes:

  • Introduces Expand-XlFnFormula and applies it across the cmdlet paths that write formulas (Export-Excel, Set-ExcelRange, Set-ExcelRow, Set-ExcelColumn).
  • Registers _xlfn.* function aliases before EPPlus calculation runs in Export-Excel -Calculate and Close-ExcelPackage -Calculate.
  • Adds Pester coverage and supporting test artifacts for the prefixing behavior and opt-out flag.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
Public/Set-ExcelRow.ps1 Uses Expand-XlFnFormula when writing row formulas from =-prefixed values.
Public/Set-ExcelRange.ps1 Fixes -Value '=...' being treated as an empty formula; routes formulas through Expand-XlFnFormula.
Public/Set-ExcelColumn.ps1 Uses Expand-XlFnFormula when writing column formulas from =-prefixed values.
Public/Export-Excel.ps1 Rewrites formula-like cell values via _xlfn expansion and registers aliases before -Calculate.
Public/Close-ExcelPackage.ps1 Registers _xlfn aliases before workbook calculation.
Private/XlFnFormula.ps1 Implements formula expansion and EPPlus function-alias registration.
tests/XlFnFunctionList.txt Captures Excel-vs-module prefix expectations for the function table.
tests/XlFnFormula.tests.ps1 Adds Pester tests validating rewrite behavior, call-site coverage, and -Calculate compatibility.
tests/Get-XlFnFunctionList.ps1 Adds a local-only COM-based regeneration/verifier for the function prefix table.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread Private/XlFnFormula.ps1
FILTER and SORT are rewritten to _xlfn._xlws. names, so if a future
EPPlus implements either, the calculation aliases now cover that form
as well. The EPPlus bundled today implements neither, so this changes
nothing yet.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.

Weird formula assignment

2 participants