fix(xlsx): propagate source numFmt to pivot cacheField

When building a pivot from a source range, resolve each source column's
StyleIndex to its numFmtId and stamp it onto the cacheField. Without
this, a date-formatted column (numFmtId 164, yyyy-mm-dd) rendered in
the pivot as raw OADate serials (45306, 45337, ...) instead of the
intended date format. Reuses ResolveColumnNumFmtIds already used for
DataField.NumberFormatId.
This commit is contained in:
zmworm 2026-04-19 10:24:25 +08:00
parent 56171db77a
commit 4a5b82de37
2 changed files with 30 additions and 10 deletions

View file

@ -361,7 +361,8 @@ internal static partial class PivotTableHelper
string sourceSheetName, string sourceRef,
string[] headers, List<string[]> columnData,
HashSet<int>? axisFieldIndices = null,
List<DateGroupSpec>? dateGroups = null)
List<DateGroupSpec>? dateGroups = null,
uint?[]? columnNumFmtIds = null)
{
var recordCount = columnData.Count > 0 ? columnData[0].Length : 0;
@ -430,11 +431,20 @@ internal static partial class PivotTableHelper
var fieldName = string.IsNullOrEmpty(headers[i]) ? $"Column{i + 1}" : headers[i];
var values = i < columnData.Count ? columnData[i] : Array.Empty<string>();
// R19-1: per-column source numFmtId (date/currency/etc.) to stamp
// on the cacheField so the pivot renders values with the same
// formatting as the source column. Null means "General" and we
// leave the default in place.
uint? srcNumFmtId = (columnNumFmtIds != null && i < columnNumFmtIds.Length)
? columnNumFmtIds[i] : null;
if (derivedByIdx.TryGetValue(i, out var spec))
{
// Derived date group field — synthesized, no records entries.
cacheFields.AppendChild(BuildDateGroupDerivedCacheField(fieldName, spec,
out fieldValueIndex[i]));
var derived = BuildDateGroupDerivedCacheField(fieldName, spec,
out fieldValueIndex[i]);
if (srcNumFmtId.HasValue) derived.NumberFormatId = srcNumFmtId.Value;
cacheFields.AppendChild(derived);
fieldNumeric[i] = false; // records should skip this field
continue;
}
@ -448,8 +458,12 @@ internal static partial class PivotTableHelper
int parIdx = derivedByIdx
.Where(kv => kv.Value.BaseFieldIdx == i)
.Min(kv => kv.Key);
cacheFields.AppendChild(BuildDateGroupBaseCacheField(fieldName, values, parIdx,
out fieldValueIndex[i]));
var baseField = BuildDateGroupBaseCacheField(fieldName, values, parIdx,
out fieldValueIndex[i]);
// Prefer the source column's numFmtId when present; else keep
// the builder's 164u default (yyyy-mm-dd).
if (srcNumFmtId.HasValue) baseField.NumberFormatId = srcNumFmtId.Value;
cacheFields.AppendChild(baseField);
fieldNumeric[i] = false;
continue;
}
@ -458,8 +472,10 @@ internal static partial class PivotTableHelper
// even when their values parse as numeric, so pivotField items
// indices and cache record references stay in sync.
bool forceStringIndexed = axisFieldIndices?.Contains(i) == true;
cacheFields.AppendChild(BuildCacheField(
fieldName, values, out fieldNumeric[i], out fieldValueIndex[i], forceStringIndexed));
var plainField = BuildCacheField(
fieldName, values, out fieldNumeric[i], out fieldValueIndex[i], forceStringIndexed);
if (srcNumFmtId.HasValue) plainField.NumberFormatId = srcNumFmtId.Value;
cacheFields.AppendChild(plainField);
}
cacheDef.AppendChild(cacheFields);

View file

@ -1049,8 +1049,13 @@ internal static partial class PivotTableHelper
foreach (var r in rowFields) axisFieldSet.Add(r);
foreach (var c in colFields) axisFieldSet.Add(c);
foreach (var f in filterFields) axisFieldSet.Add(f);
// R19-1: resolve numFmtIds BEFORE building the cache so date/number
// formats on the source column propagate onto the cacheField's
// numFmtId attribute. Without this, a column styled as "yyyy-mm-dd"
// renders in the pivot as the raw OADate serial (45306, ...).
var columnNumFmtIds = ResolveColumnNumFmtIds(workbookPart, columnStyleIds);
var (cacheDef, fieldNumeric, fieldValueIndex) =
BuildCacheDefinition(sourceSheetName, sourceRef, headers, columnData, axisFieldSet, dateGroups);
BuildCacheDefinition(sourceSheetName, sourceRef, headers, columnData, axisFieldSet, dateGroups, columnNumFmtIds);
cachePart.PivotCacheDefinition = cacheDef;
cachePart.PivotCacheDefinition.Save();
@ -1129,13 +1134,12 @@ internal static partial class PivotTableHelper
}
var style = properties.GetValueOrDefault("style", "PivotStyleLight16");
// Resolve per-column numFmtId from the source StyleIndex so we can stamp
// columnNumFmtIds was resolved above (R19-1) and reused here to stamp
// it onto DataField elements below. Excel uses DataField.NumberFormatId
// as the PRIMARY display driver for pivot values — the cell-level
// StyleIndex alone is not enough; without this, Excel renders pivot
// values as plain General-format numbers even though the rendered cells
// carry the correct style.
var columnNumFmtIds = ResolveColumnNumFmtIds(workbookPart, columnStyleIds);
// Page filters occupy rows ABOVE the pivot body. Ensure position leaves
// enough headroom for filterCount filter rows + 1 blank separator row.