@niicojs/excel 0.2.4 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -40,10 +40,8 @@ var fflate = require('fflate');
40
40
  if (!match) {
41
41
  throw new Error(`Invalid cell address: ${address}`);
42
42
  }
43
- const rowNumber = +match[2];
44
- if (rowNumber <= 0) throw new Error(`Invalid cell address: ${address}`);
45
43
  const col = letterToCol(match[1].toUpperCase());
46
- const row = rowNumber - 1; // Convert to 0-based
44
+ const row = parseInt(match[2], 10) - 1; // Convert to 0-based
47
45
  return {
48
46
  row,
49
47
  col
@@ -105,12 +103,6 @@ var fflate = require('fflate');
105
103
  }
106
104
  };
107
105
  };
108
- /**
109
- * Checks if an address is within a range
110
- */ const isInRange = (addr, range)=>{
111
- const norm = normalizeRange(range);
112
- return addr.row >= norm.start.row && addr.row <= norm.end.row && addr.col >= norm.start.col && addr.col <= norm.end.col;
113
- };
114
106
 
115
107
  // Excel epoch: December 30, 1899 (accounting for the 1900 leap year bug)
116
108
  const EXCEL_EPOCH = new Date(Date.UTC(1899, 11, 30));
@@ -443,23 +435,12 @@ _computedKey = Symbol.iterator;
443
435
  /**
444
436
  * Get all values in the range as a 2D array
445
437
  */ get values() {
446
- return this.getValues();
447
- }
448
- /**
449
- * Get all values in the range as a 2D array with options
450
- */ getValues(options = {}) {
451
- const { createMissing = true } = options;
452
438
  const result = [];
453
439
  for(let r = this._range.start.row; r <= this._range.end.row; r++){
454
440
  const row = [];
455
441
  for(let c = this._range.start.col; c <= this._range.end.col; c++){
456
- if (createMissing) {
457
- const cell = this._worksheet.cell(r, c);
458
- row.push(cell.value);
459
- } else {
460
- const cell = this._worksheet.getCellIfExists(r, c);
461
- row.push(cell?.value ?? null);
462
- }
442
+ const cell = this._worksheet.cell(r, c);
443
+ row.push(cell.value);
463
444
  }
464
445
  result.push(row);
465
446
  }
@@ -632,11 +613,6 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
632
613
  this._dirty = false;
633
614
  this._mergedCells = new Set();
634
615
  this._sheetData = [];
635
- this._columnWidths = new Map();
636
- this._rowHeights = new Map();
637
- this._frozenPane = null;
638
- this._dataBoundsCache = null;
639
- this._boundsDirty = true;
640
616
  this._workbook = workbook;
641
617
  this._name = name;
642
618
  }
@@ -663,49 +639,12 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
663
639
  const worksheet = findElement(this._xmlNodes, 'worksheet');
664
640
  if (!worksheet) return;
665
641
  const worksheetChildren = getChildren(worksheet, 'worksheet');
666
- // Parse sheet views (freeze panes)
667
- const sheetViews = findElement(worksheetChildren, 'sheetViews');
668
- if (sheetViews) {
669
- const viewChildren = getChildren(sheetViews, 'sheetViews');
670
- const sheetView = findElement(viewChildren, 'sheetView');
671
- if (sheetView) {
672
- const sheetViewChildren = getChildren(sheetView, 'sheetView');
673
- const pane = findElement(sheetViewChildren, 'pane');
674
- if (pane && getAttr(pane, 'state') === 'frozen') {
675
- const xSplit = parseInt(getAttr(pane, 'xSplit') || '0', 10);
676
- const ySplit = parseInt(getAttr(pane, 'ySplit') || '0', 10);
677
- if (xSplit > 0 || ySplit > 0) {
678
- this._frozenPane = {
679
- row: ySplit,
680
- col: xSplit
681
- };
682
- }
683
- }
684
- }
685
- }
686
642
  // Parse sheet data (cells)
687
643
  const sheetData = findElement(worksheetChildren, 'sheetData');
688
644
  if (sheetData) {
689
645
  this._sheetData = getChildren(sheetData, 'sheetData');
690
646
  this._parseSheetData(this._sheetData);
691
647
  }
692
- // Parse column widths
693
- const cols = findElement(worksheetChildren, 'cols');
694
- if (cols) {
695
- const colChildren = getChildren(cols, 'cols');
696
- for (const col of colChildren){
697
- if (!('col' in col)) continue;
698
- const min = parseInt(getAttr(col, 'min') || '0', 10);
699
- const max = parseInt(getAttr(col, 'max') || '0', 10);
700
- const width = parseFloat(getAttr(col, 'width') || '0');
701
- if (!Number.isFinite(width) || width <= 0) continue;
702
- if (min > 0 && max > 0) {
703
- for(let idx = min; idx <= max; idx++){
704
- this._columnWidths.set(idx - 1, width);
705
- }
706
- }
707
- }
708
- }
709
648
  // Parse merged cells
710
649
  const mergeCells = findElement(worksheetChildren, 'mergeCells');
711
650
  if (mergeCells) {
@@ -725,11 +664,6 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
725
664
  */ _parseSheetData(rows) {
726
665
  for (const rowNode of rows){
727
666
  if (!('row' in rowNode)) continue;
728
- const rowIndex = parseInt(getAttr(rowNode, 'r') || '0', 10) - 1;
729
- const rowHeight = parseFloat(getAttr(rowNode, 'ht') || '0');
730
- if (rowIndex >= 0 && Number.isFinite(rowHeight) && rowHeight > 0) {
731
- this._rowHeights.set(rowIndex, rowHeight);
732
- }
733
667
  const rowChildren = getChildren(rowNode, 'row');
734
668
  for (const cellNode of rowChildren){
735
669
  if (!('c' in cellNode)) continue;
@@ -741,7 +675,6 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
741
675
  this._cells.set(ref, cell);
742
676
  }
743
677
  }
744
- this._boundsDirty = true;
745
678
  }
746
679
  /**
747
680
  * Parse a cell XML node to CellData
@@ -828,17 +761,9 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
828
761
  if (!cell) {
829
762
  cell = new Cell(this, row, c);
830
763
  this._cells.set(address, cell);
831
- this._boundsDirty = true;
832
764
  }
833
765
  return cell;
834
766
  }
835
- /**
836
- * Get an existing cell without creating it.
837
- */ getCellIfExists(rowOrAddress, col) {
838
- const { row, col: c } = parseCellRef(rowOrAddress, col);
839
- const address = toAddress(row, c);
840
- return this._cells.get(address);
841
- }
842
767
  range(startRowOrRange, startCol, endRow, endCol) {
843
768
  let rangeAddr;
844
769
  if (typeof startRowOrRange === 'string') {
@@ -898,65 +823,6 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
898
823
  return this._cells;
899
824
  }
900
825
  /**
901
- * Set a column width (0-based index or column letter)
902
- */ setColumnWidth(col, width) {
903
- if (!Number.isFinite(width) || width <= 0) {
904
- throw new Error('Column width must be a positive number');
905
- }
906
- const colIndex = typeof col === 'number' ? col : letterToCol(col);
907
- if (colIndex < 0) {
908
- throw new Error(`Invalid column: ${col}`);
909
- }
910
- this._columnWidths.set(colIndex, width);
911
- this._dirty = true;
912
- }
913
- /**
914
- * Get a column width if set
915
- */ getColumnWidth(col) {
916
- const colIndex = typeof col === 'number' ? col : letterToCol(col);
917
- return this._columnWidths.get(colIndex);
918
- }
919
- /**
920
- * Set a row height (0-based index)
921
- */ setRowHeight(row, height) {
922
- if (!Number.isFinite(height) || height <= 0) {
923
- throw new Error('Row height must be a positive number');
924
- }
925
- if (row < 0) {
926
- throw new Error('Row index must be >= 0');
927
- }
928
- this._rowHeights.set(row, height);
929
- this._dirty = true;
930
- }
931
- /**
932
- * Get a row height if set
933
- */ getRowHeight(row) {
934
- return this._rowHeights.get(row);
935
- }
936
- /**
937
- * Freeze panes at a given row/column split (counts from top-left)
938
- */ freezePane(rowSplit, colSplit) {
939
- if (rowSplit < 0 || colSplit < 0) {
940
- throw new Error('Freeze pane splits must be >= 0');
941
- }
942
- if (rowSplit === 0 && colSplit === 0) {
943
- this._frozenPane = null;
944
- } else {
945
- this._frozenPane = {
946
- row: rowSplit,
947
- col: colSplit
948
- };
949
- }
950
- this._dirty = true;
951
- }
952
- /**
953
- * Get current frozen pane configuration
954
- */ getFrozenPane() {
955
- return this._frozenPane ? {
956
- ...this._frozenPane
957
- } : null;
958
- }
959
- /**
960
826
  * Convert sheet data to an array of JSON objects.
961
827
  *
962
828
  * @param config - Configuration options
@@ -974,7 +840,7 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
974
840
  * const data = sheet.toJson({ startRow: 2, startCol: 1 });
975
841
  * ```
976
842
  */ toJson(config = {}) {
977
- const { fields, startRow = 0, startCol = 0, endRow, endCol, stopOnEmptyRow = true, dateHandling = this._workbook.dateHandling } = config;
843
+ const { fields, startRow = 0, startCol = 0, endRow, endCol, stopOnEmptyRow = true } = config;
978
844
  // Get the bounds of data in the sheet
979
845
  const bounds = this._getDataBounds();
980
846
  if (!bounds) {
@@ -1007,10 +873,7 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
1007
873
  for(let colOffset = 0; colOffset < fieldNames.length; colOffset++){
1008
874
  const col = startCol + colOffset;
1009
875
  const cell = this._cells.get(toAddress(row, col));
1010
- let value = cell?.value ?? null;
1011
- if (value instanceof Date) {
1012
- value = this._serializeDate(value, dateHandling, cell);
1013
- }
876
+ const value = cell?.value ?? null;
1014
877
  if (value !== null) {
1015
878
  hasData = true;
1016
879
  }
@@ -1027,24 +890,10 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
1027
890
  }
1028
891
  return result;
1029
892
  }
1030
- _serializeDate(value, dateHandling, cell) {
1031
- if (dateHandling === 'excelSerial') {
1032
- return cell?._jsDateToExcel(value) ?? value;
1033
- }
1034
- if (dateHandling === 'isoString') {
1035
- return value.toISOString();
1036
- }
1037
- return value;
1038
- }
1039
893
  /**
1040
894
  * Get the bounds of data in the sheet (min/max row and column with data)
1041
895
  */ _getDataBounds() {
1042
- if (!this._boundsDirty && this._dataBoundsCache) {
1043
- return this._dataBoundsCache;
1044
- }
1045
896
  if (this._cells.size === 0) {
1046
- this._dataBoundsCache = null;
1047
- this._boundsDirty = false;
1048
897
  return null;
1049
898
  }
1050
899
  let minRow = Infinity;
@@ -1060,18 +909,14 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
1060
909
  }
1061
910
  }
1062
911
  if (minRow === Infinity) {
1063
- this._dataBoundsCache = null;
1064
- this._boundsDirty = false;
1065
912
  return null;
1066
913
  }
1067
- this._dataBoundsCache = {
914
+ return {
1068
915
  minRow,
1069
916
  maxRow,
1070
917
  minCol,
1071
918
  maxCol
1072
919
  };
1073
- this._boundsDirty = false;
1074
- return this._dataBoundsCache;
1075
920
  }
1076
921
  /**
1077
922
  * Generate XML for this worksheet
@@ -1085,11 +930,6 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
1085
930
  }
1086
931
  rowMap.get(row).push(cell);
1087
932
  }
1088
- for (const rowIdx of this._rowHeights.keys()){
1089
- if (!rowMap.has(rowIdx)) {
1090
- rowMap.set(rowIdx, []);
1091
- }
1092
- }
1093
933
  // Sort rows and cells
1094
934
  const sortedRows = Array.from(rowMap.entries()).sort((a, b)=>a[0] - b[0]);
1095
935
  const rowNodes = [];
@@ -1100,71 +940,16 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
1100
940
  const cellNode = this._buildCellNode(cell);
1101
941
  cellNodes.push(cellNode);
1102
942
  }
1103
- const rowAttrs = {
943
+ const rowNode = createElement('row', {
1104
944
  r: String(rowIdx + 1)
1105
- };
1106
- const rowHeight = this._rowHeights.get(rowIdx);
1107
- if (rowHeight !== undefined) {
1108
- rowAttrs.ht = String(rowHeight);
1109
- rowAttrs.customHeight = '1';
1110
- }
1111
- const rowNode = createElement('row', rowAttrs, cellNodes);
945
+ }, cellNodes);
1112
946
  rowNodes.push(rowNode);
1113
947
  }
1114
948
  const sheetDataNode = createElement('sheetData', {}, rowNodes);
1115
949
  // Build worksheet structure
1116
- const worksheetChildren = [];
1117
- // Sheet views (freeze panes)
1118
- if (this._frozenPane) {
1119
- const paneAttrs = {
1120
- state: 'frozen'
1121
- };
1122
- const topLeftCell = toAddress(this._frozenPane.row, this._frozenPane.col);
1123
- paneAttrs.topLeftCell = topLeftCell;
1124
- if (this._frozenPane.col > 0) {
1125
- paneAttrs.xSplit = String(this._frozenPane.col);
1126
- }
1127
- if (this._frozenPane.row > 0) {
1128
- paneAttrs.ySplit = String(this._frozenPane.row);
1129
- }
1130
- let activePane = 'bottomRight';
1131
- if (this._frozenPane.row > 0 && this._frozenPane.col === 0) {
1132
- activePane = 'bottomLeft';
1133
- } else if (this._frozenPane.row === 0 && this._frozenPane.col > 0) {
1134
- activePane = 'topRight';
1135
- }
1136
- paneAttrs.activePane = activePane;
1137
- const paneNode = createElement('pane', paneAttrs, []);
1138
- const selectionNode = createElement('selection', {
1139
- pane: activePane,
1140
- activeCell: topLeftCell,
1141
- sqref: topLeftCell
1142
- }, []);
1143
- const sheetViewNode = createElement('sheetView', {
1144
- workbookViewId: '0'
1145
- }, [
1146
- paneNode,
1147
- selectionNode
1148
- ]);
1149
- worksheetChildren.push(createElement('sheetViews', {}, [
1150
- sheetViewNode
1151
- ]));
1152
- }
1153
- // Column widths
1154
- if (this._columnWidths.size > 0) {
1155
- const colNodes = [];
1156
- const entries = Array.from(this._columnWidths.entries()).sort((a, b)=>a[0] - b[0]);
1157
- for (const [colIndex, width] of entries){
1158
- colNodes.push(createElement('col', {
1159
- min: String(colIndex + 1),
1160
- max: String(colIndex + 1),
1161
- width: String(width),
1162
- customWidth: '1'
1163
- }, []));
1164
- }
1165
- worksheetChildren.push(createElement('cols', {}, colNodes));
1166
- }
1167
- worksheetChildren.push(sheetDataNode);
950
+ const worksheetChildren = [
951
+ sheetDataNode
952
+ ];
1168
953
  // Add merged cells if any
1169
954
  if (this._mergedCells.size > 0) {
1170
955
  const mergeCellNodes = [];
@@ -1332,6 +1117,129 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
1332
1117
  }
1333
1118
  }
1334
1119
 
1120
+ /**
1121
+ * Excel built-in number format IDs (0-163 are reserved).
1122
+ * These formats don't need to be defined in the numFmts element.
1123
+ */ const BUILTIN_NUM_FMTS = new Map([
1124
+ [
1125
+ 'General',
1126
+ 0
1127
+ ],
1128
+ [
1129
+ '0',
1130
+ 1
1131
+ ],
1132
+ [
1133
+ '0.00',
1134
+ 2
1135
+ ],
1136
+ [
1137
+ '#,##0',
1138
+ 3
1139
+ ],
1140
+ [
1141
+ '#,##0.00',
1142
+ 4
1143
+ ],
1144
+ [
1145
+ '0%',
1146
+ 9
1147
+ ],
1148
+ [
1149
+ '0.00%',
1150
+ 10
1151
+ ],
1152
+ [
1153
+ '0.00E+00',
1154
+ 11
1155
+ ],
1156
+ [
1157
+ '# ?/?',
1158
+ 12
1159
+ ],
1160
+ [
1161
+ '# ??/??',
1162
+ 13
1163
+ ],
1164
+ [
1165
+ 'mm-dd-yy',
1166
+ 14
1167
+ ],
1168
+ [
1169
+ 'd-mmm-yy',
1170
+ 15
1171
+ ],
1172
+ [
1173
+ 'd-mmm',
1174
+ 16
1175
+ ],
1176
+ [
1177
+ 'mmm-yy',
1178
+ 17
1179
+ ],
1180
+ [
1181
+ 'h:mm AM/PM',
1182
+ 18
1183
+ ],
1184
+ [
1185
+ 'h:mm:ss AM/PM',
1186
+ 19
1187
+ ],
1188
+ [
1189
+ 'h:mm',
1190
+ 20
1191
+ ],
1192
+ [
1193
+ 'h:mm:ss',
1194
+ 21
1195
+ ],
1196
+ [
1197
+ 'm/d/yy h:mm',
1198
+ 22
1199
+ ],
1200
+ [
1201
+ '#,##0 ;(#,##0)',
1202
+ 37
1203
+ ],
1204
+ [
1205
+ '#,##0 ;[Red](#,##0)',
1206
+ 38
1207
+ ],
1208
+ [
1209
+ '#,##0.00;(#,##0.00)',
1210
+ 39
1211
+ ],
1212
+ [
1213
+ '#,##0.00;[Red](#,##0.00)',
1214
+ 40
1215
+ ],
1216
+ [
1217
+ 'mm:ss',
1218
+ 45
1219
+ ],
1220
+ [
1221
+ '[h]:mm:ss',
1222
+ 46
1223
+ ],
1224
+ [
1225
+ 'mmss.0',
1226
+ 47
1227
+ ],
1228
+ [
1229
+ '##0.0E+0',
1230
+ 48
1231
+ ],
1232
+ [
1233
+ '@',
1234
+ 49
1235
+ ]
1236
+ ]);
1237
+ /**
1238
+ * Reverse lookup: built-in format ID -> format code
1239
+ */ const BUILTIN_NUM_FMT_CODES = new Map(Array.from(BUILTIN_NUM_FMTS.entries()).map(([code, id])=>[
1240
+ id,
1241
+ code
1242
+ ]));
1335
1243
  /**
1336
1244
  * Normalize a color to ARGB format (8 hex chars).
1337
1245
  * Accepts: "#RGB", "#RRGGBB", "RGB", "RRGGBB", "AARRGGBB", "#AARRGGBB"
@@ -1351,40 +1259,6 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
1351
1259
  * Manages the styles (xl/styles.xml)
1352
1260
  */ class Styles {
1353
1261
  /**
1354
- * Generate a deterministic cache key for a style object.
1355
- * More efficient than JSON.stringify as it avoids the overhead of
1356
- * full JSON serialization and produces a consistent key regardless
1357
- * of property order.
1358
- */ _getStyleKey(style) {
1359
- // Use a delimiter that won't appear in values
1360
- const SEP = '\x00';
1361
- // Build key from all style properties in a fixed order
1362
- const parts = [
1363
- style.bold ? '1' : '0',
1364
- style.italic ? '1' : '0',
1365
- style.underline === true ? '1' : style.underline === 'single' ? 's' : style.underline === 'double' ? 'd' : '0',
1366
- style.strike ? '1' : '0',
1367
- style.fontSize?.toString() ?? '',
1368
- style.fontName ?? '',
1369
- style.fontColor ?? '',
1370
- style.fill ?? '',
1371
- style.numberFormat ?? ''
1372
- ];
1373
- // Border properties
1374
- if (style.border) {
1375
- parts.push(style.border.top ?? '', style.border.bottom ?? '', style.border.left ?? '', style.border.right ?? '');
1376
- } else {
1377
- parts.push('', '', '', '');
1378
- }
1379
- // Alignment properties
1380
- if (style.alignment) {
1381
- parts.push(style.alignment.horizontal ?? '', style.alignment.vertical ?? '', style.alignment.wrapText ? '1' : '0', style.alignment.textRotation?.toString() ?? '');
1382
- } else {
1383
- parts.push('', '', '0', '');
1384
- }
1385
- return parts.join(SEP);
1386
- }
1387
- /**
1388
1262
  * Parse styles from XML content
1389
1263
  */ static parse(xml) {
1390
1264
  const styles = new Styles();
@@ -1551,16 +1425,13 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
1551
1425
  /**
1552
1426
  * Get a style by index
1553
1427
  */ getStyle(index) {
1554
- const cached = this._styleObjectCache.get(index);
1555
- if (cached) return {
1556
- ...cached
1557
- };
1558
1428
  const xf = this._cellXfs[index];
1559
1429
  if (!xf) return {};
1560
1430
  const font = this._fonts[xf.fontId];
1561
1431
  const fill = this._fills[xf.fillId];
1562
1432
  const border = this._borders[xf.borderId];
1563
- const numFmt = this._numFmts.get(xf.numFmtId);
1433
+ // Check custom formats first, then fall back to built-in format codes
1434
+ const numFmt = this._numFmts.get(xf.numFmtId) ?? BUILTIN_NUM_FMT_CODES.get(xf.numFmtId);
1564
1435
  const style = {};
1565
1436
  if (font) {
1566
1437
  if (font.bold) style.bold = true;
@@ -1595,16 +1466,13 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
1595
1466
  textRotation: xf.alignment.textRotation
1596
1467
  };
1597
1468
  }
1598
- this._styleObjectCache.set(index, {
1599
- ...style
1600
- });
1601
1469
  return style;
1602
1470
  }
1603
1471
  /**
1604
1472
  * Create a style and return its index
1605
1473
  * Uses caching to deduplicate identical styles
1606
1474
  */ createStyle(style) {
1607
- const key = this._getStyleKey(style);
1475
+ const key = JSON.stringify(style);
1608
1476
  const cached = this._styleCache.get(key);
1609
1477
  if (cached !== undefined) {
1610
1478
  return cached;
@@ -1636,20 +1504,8 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
1636
1504
  const index = this._cellXfs.length;
1637
1505
  this._cellXfs.push(xf);
1638
1506
  this._styleCache.set(key, index);
1639
- this._styleObjectCache.set(index, {
1640
- ...style
1641
- });
1642
1507
  return index;
1643
1508
  }
1644
- /**
1645
- * Clone an existing style by index, optionally overriding fields.
1646
- */ cloneStyle(index, overrides = {}) {
1647
- const baseStyle = this.getStyle(index);
1648
- return this.createStyle({
1649
- ...baseStyle,
1650
- ...overrides
1651
- });
1652
- }
1653
1509
  _findOrCreateFont(style) {
1654
1510
  const font = {
1655
1511
  bold: style.bold || false,
@@ -1707,16 +1563,30 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
1707
1563
  return this._borders.length - 1;
1708
1564
  }
1709
1565
  _findOrCreateNumFmt(format) {
1710
- // Check if already exists
1566
+ // Check built-in formats first (IDs 0-163)
1567
+ const builtinId = BUILTIN_NUM_FMTS.get(format);
1568
+ if (builtinId !== undefined) {
1569
+ return builtinId;
1570
+ }
1571
+ // Check if already exists in custom formats
1711
1572
  for (const [id, code] of this._numFmts){
1712
1573
  if (code === format) return id;
1713
1574
  }
1714
- // Create new (custom formats start at 164)
1715
- const id = Math.max(164, ...Array.from(this._numFmts.keys())) + 1;
1575
+ // Create new custom format (IDs 164+)
1576
+ const existingIds = Array.from(this._numFmts.keys());
1577
+ const id = existingIds.length > 0 ? Math.max(...existingIds) + 1 : 164;
1716
1578
  this._numFmts.set(id, format);
1717
1579
  return id;
1718
1580
  }
1719
1581
  /**
1582
+ * Get or create a number format ID for the given format string.
1583
+ * Returns built-in IDs (0-163) for standard formats, or creates custom IDs (164+).
1584
+ * @param format - The number format string (e.g., '0.00', '#,##0', '$#,##0.00')
1585
+ */ getOrCreateNumFmtId(format) {
1586
+ this._dirty = true;
1587
+ return this._findOrCreateNumFmt(format);
1588
+ }
1589
+ /**
1720
1590
  * Check if styles have been modified
1721
1591
  */ get dirty() {
1722
1592
  return this._dirty;
@@ -1885,7 +1755,6 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
1885
1755
  this._dirty = false;
1886
1756
  // Cache for style deduplication
1887
1757
  this._styleCache = new Map();
1888
- this._styleObjectCache = new Map();
1889
1758
  }
1890
1759
  }
1891
1760
 
@@ -1897,7 +1766,7 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
1897
1766
  this._columnFields = [];
1898
1767
  this._valueFields = [];
1899
1768
  this._filterFields = [];
1900
- this._fieldAssignments = new Map();
1769
+ this._styles = null;
1901
1770
  this._name = name;
1902
1771
  this._cache = cache;
1903
1772
  this._targetSheet = targetSheet;
@@ -1932,6 +1801,13 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
1932
1801
  return this._pivotTableIndex;
1933
1802
  }
1934
1803
  /**
1804
+ * Set the styles reference for number format resolution
1805
+ * @internal
1806
+ */ setStyles(styles) {
1807
+ this._styles = styles;
1808
+ return this;
1809
+ }
1810
+ /**
1935
1811
  * Add a field to the row area
1936
1812
  * @param fieldName - Name of the source field (column header)
1937
1813
  */ addRowField(fieldName) {
@@ -1939,13 +1815,11 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
1939
1815
  if (fieldIndex < 0) {
1940
1816
  throw new Error(`Field not found in source data: ${fieldName}`);
1941
1817
  }
1942
- const assignment = {
1818
+ this._rowFields.push({
1943
1819
  fieldName,
1944
1820
  fieldIndex,
1945
1821
  axis: 'row'
1946
- };
1947
- this._rowFields.push(assignment);
1948
- this._fieldAssignments.set(fieldIndex, assignment);
1822
+ });
1949
1823
  return this;
1950
1824
  }
1951
1825
  /**
@@ -1956,13 +1830,11 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
1956
1830
  if (fieldIndex < 0) {
1957
1831
  throw new Error(`Field not found in source data: ${fieldName}`);
1958
1832
  }
1959
- const assignment = {
1833
+ this._columnFields.push({
1960
1834
  fieldName,
1961
1835
  fieldIndex,
1962
1836
  axis: 'column'
1963
- };
1964
- this._columnFields.push(assignment);
1965
- this._fieldAssignments.set(fieldIndex, assignment);
1837
+ });
1966
1838
  return this;
1967
1839
  }
1968
1840
  /**
@@ -1970,21 +1842,21 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
1970
1842
  * @param fieldName - Name of the source field (column header)
1971
1843
  * @param aggregation - Aggregation function (sum, count, average, min, max)
1972
1844
  * @param displayName - Optional display name (defaults to "Sum of FieldName")
1973
- */ addValueField(fieldName, aggregation = 'sum', displayName) {
1845
+ * @param numberFormat - Optional number format (e.g., '$#,##0.00', '0.00%')
1846
+ */ addValueField(fieldName, aggregation = 'sum', displayName, numberFormat) {
1974
1847
  const fieldIndex = this._cache.getFieldIndex(fieldName);
1975
1848
  if (fieldIndex < 0) {
1976
1849
  throw new Error(`Field not found in source data: ${fieldName}`);
1977
1850
  }
1978
1851
  const defaultName = `${aggregation.charAt(0).toUpperCase() + aggregation.slice(1)} of ${fieldName}`;
1979
- const assignment = {
1852
+ this._valueFields.push({
1980
1853
  fieldName,
1981
1854
  fieldIndex,
1982
1855
  axis: 'value',
1983
1856
  aggregation,
1984
- displayName: displayName || defaultName
1985
- };
1986
- this._valueFields.push(assignment);
1987
- this._fieldAssignments.set(fieldIndex, assignment);
1857
+ displayName: displayName || defaultName,
1858
+ numberFormat
1859
+ });
1988
1860
  return this;
1989
1861
  }
1990
1862
  /**
@@ -1995,44 +1867,11 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
1995
1867
  if (fieldIndex < 0) {
1996
1868
  throw new Error(`Field not found in source data: ${fieldName}`);
1997
1869
  }
1998
- const assignment = {
1870
+ this._filterFields.push({
1999
1871
  fieldName,
2000
1872
  fieldIndex,
2001
1873
  axis: 'filter'
2002
- };
2003
- this._filterFields.push(assignment);
2004
- this._fieldAssignments.set(fieldIndex, assignment);
2005
- return this;
2006
- }
2007
- /**
2008
- * Set a sort order for a row/column field
2009
- */ sortField(fieldName, order) {
2010
- const fieldIndex = this._cache.getFieldIndex(fieldName);
2011
- if (fieldIndex < 0) {
2012
- throw new Error(`Field not found in source data: ${fieldName}`);
2013
- }
2014
- const assignment = this._fieldAssignments.get(fieldIndex);
2015
- if (!assignment || assignment.axis !== 'row' && assignment.axis !== 'column') {
2016
- throw new Error(`Field is not assigned to row or column axis: ${fieldName}`);
2017
- }
2018
- assignment.sortOrder = order;
2019
- return this;
2020
- }
2021
- /**
2022
- * Filter items for a field (include or exclude list)
2023
- */ filterField(fieldName, filter) {
2024
- const fieldIndex = this._cache.getFieldIndex(fieldName);
2025
- if (fieldIndex < 0) {
2026
- throw new Error(`Field not found in source data: ${fieldName}`);
2027
- }
2028
- const assignment = this._fieldAssignments.get(fieldIndex);
2029
- if (!assignment) {
2030
- throw new Error(`Field is not assigned to pivot table: ${fieldName}`);
2031
- }
2032
- if (filter.include && filter.exclude) {
2033
- throw new Error('Pivot field filter cannot use both include and exclude');
2034
- }
2035
- assignment.filter = filter;
1874
+ });
2036
1875
  return this;
2037
1876
  }
2038
1877
  /**
@@ -2139,17 +1978,26 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
2139
1978
  }
2140
1979
  // Data fields (values)
2141
1980
  if (this._valueFields.length > 0) {
2142
- const dataFieldNodes = this._valueFields.map((f)=>createElement('dataField', {
1981
+ const dataFieldNodes = this._valueFields.map((f)=>{
1982
+ const attrs = {
2143
1983
  name: f.displayName || f.fieldName,
2144
1984
  fld: String(f.fieldIndex),
2145
1985
  baseField: '0',
2146
1986
  baseItem: '0',
2147
1987
  subtotal: f.aggregation || 'sum'
2148
- }, []));
1988
+ };
1989
+ // Add numFmtId if format specified and styles available
1990
+ if (f.numberFormat && this._styles) {
1991
+ attrs.numFmtId = String(this._styles.getOrCreateNumFmtId(f.numberFormat));
1992
+ }
1993
+ return createElement('dataField', attrs, []);
1994
+ });
2149
1995
  children.push(createElement('dataFields', {
2150
1996
  count: String(dataFieldNodes.length)
2151
1997
  }, dataFieldNodes));
2152
1998
  }
1999
+ // Check if any value field has a number format
2000
+ const hasNumberFormats = this._valueFields.some((f)=>f.numberFormat);
2153
2001
  // Pivot table style
2154
2002
  children.push(createElement('pivotTableStyleInfo', {
2155
2003
  name: 'PivotStyleMedium9',
@@ -2164,7 +2012,7 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
2164
2012
  'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',
2165
2013
  name: this._name,
2166
2014
  cacheId: String(this._cache.cacheId),
2167
- applyNumberFormats: '0',
2015
+ applyNumberFormats: hasNumberFormats ? '1' : '0',
2168
2016
  applyBorderFormats: '0',
2169
2017
  applyFontFormats: '0',
2170
2018
  applyPatternFormats: '0',
@@ -2197,28 +2045,17 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
2197
2045
  const colField = this._columnFields.find((f)=>f.fieldIndex === fieldIndex);
2198
2046
  const filterField = this._filterFields.find((f)=>f.fieldIndex === fieldIndex);
2199
2047
  const valueField = this._valueFields.find((f)=>f.fieldIndex === fieldIndex);
2200
- const assignment = this._fieldAssignments.get(fieldIndex);
2201
2048
  if (rowField) {
2202
2049
  attrs.axis = 'axisRow';
2203
2050
  attrs.showAll = '0';
2204
- if (assignment?.sortOrder) {
2205
- attrs.sortType = 'ascending';
2206
- attrs.sortOrder = assignment.sortOrder === 'asc' ? 'ascending' : 'descending';
2207
- }
2208
2051
  // Add items for shared values
2209
2052
  const cacheField = this._cache.fields[fieldIndex];
2210
2053
  if (cacheField && cacheField.sharedItems.length > 0) {
2211
2054
  const itemNodes = [];
2212
- const allowedIndexes = this._resolveItemFilter(cacheField.sharedItems, assignment?.filter);
2213
2055
  for(let i = 0; i < cacheField.sharedItems.length; i++){
2214
- const shouldInclude = allowedIndexes.has(i);
2215
- const itemAttrs = {
2056
+ itemNodes.push(createElement('item', {
2216
2057
  x: String(i)
2217
- };
2218
- if (!shouldInclude) {
2219
- itemAttrs.h = '1';
2220
- }
2221
- itemNodes.push(createElement('item', itemAttrs, []));
2058
+ }, []));
2222
2059
  }
2223
2060
  // Add default subtotal item
2224
2061
  itemNodes.push(createElement('item', {
@@ -2231,23 +2068,13 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
2231
2068
  } else if (colField) {
2232
2069
  attrs.axis = 'axisCol';
2233
2070
  attrs.showAll = '0';
2234
- if (assignment?.sortOrder) {
2235
- attrs.sortType = 'ascending';
2236
- attrs.sortOrder = assignment.sortOrder === 'asc' ? 'ascending' : 'descending';
2237
- }
2238
2071
  const cacheField = this._cache.fields[fieldIndex];
2239
2072
  if (cacheField && cacheField.sharedItems.length > 0) {
2240
2073
  const itemNodes = [];
2241
- const allowedIndexes = this._resolveItemFilter(cacheField.sharedItems, assignment?.filter);
2242
2074
  for(let i = 0; i < cacheField.sharedItems.length; i++){
2243
- const shouldInclude = allowedIndexes.has(i);
2244
- const itemAttrs = {
2075
+ itemNodes.push(createElement('item', {
2245
2076
  x: String(i)
2246
- };
2247
- if (!shouldInclude) {
2248
- itemAttrs.h = '1';
2249
- }
2250
- itemNodes.push(createElement('item', itemAttrs, []));
2077
+ }, []));
2251
2078
  }
2252
2079
  itemNodes.push(createElement('item', {
2253
2080
  t: 'default'
@@ -2262,16 +2089,10 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
2262
2089
  const cacheField = this._cache.fields[fieldIndex];
2263
2090
  if (cacheField && cacheField.sharedItems.length > 0) {
2264
2091
  const itemNodes = [];
2265
- const allowedIndexes = this._resolveItemFilter(cacheField.sharedItems, assignment?.filter);
2266
2092
  for(let i = 0; i < cacheField.sharedItems.length; i++){
2267
- const shouldInclude = allowedIndexes.has(i);
2268
- const itemAttrs = {
2093
+ itemNodes.push(createElement('item', {
2269
2094
  x: String(i)
2270
- };
2271
- if (!shouldInclude) {
2272
- itemAttrs.h = '1';
2273
- }
2274
- itemNodes.push(createElement('item', itemAttrs, []));
2095
+ }, []));
2275
2096
  }
2276
2097
  itemNodes.push(createElement('item', {
2277
2098
  t: 'default'
@@ -2288,31 +2109,6 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
2288
2109
  }
2289
2110
  return createElement('pivotField', attrs, children);
2290
2111
  }
2291
- _resolveItemFilter(items, filter) {
2292
- const allowed = new Set();
2293
- if (!filter || !filter.include && !filter.exclude) {
2294
- for(let i = 0; i < items.length; i++){
2295
- allowed.add(i);
2296
- }
2297
- return allowed;
2298
- }
2299
- if (filter.include) {
2300
- for(let i = 0; i < items.length; i++){
2301
- if (filter.include.includes(items[i])) {
2302
- allowed.add(i);
2303
- }
2304
- }
2305
- return allowed;
2306
- }
2307
- if (filter.exclude) {
2308
- for(let i = 0; i < items.length; i++){
2309
- if (!filter.exclude.includes(items[i])) {
2310
- allowed.add(i);
2311
- }
2312
- }
2313
- }
2314
- return allowed;
2315
- }
2316
2112
  /**
2317
2113
  * Build row items based on unique values in row fields
2318
2114
  */ _buildRowItems() {
@@ -2463,7 +2259,8 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
2463
2259
  this._records = [];
2464
2260
  this._recordCount = 0;
2465
2261
  this._refreshOnLoad = true; // Default to true
2466
- this._dateGrouping = false;
2262
+ // Optimized lookup: Map<fieldIndex, Map<stringValue, sharedItemsIndex>>
2263
+ this._sharedItemsIndexMap = new Map();
2467
2264
  this._cacheId = cacheId;
2468
2265
  this._sourceSheet = sourceSheet;
2469
2266
  this._sourceRange = sourceRange;
@@ -2524,6 +2321,8 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
2524
2321
  minValue: undefined,
2525
2322
  maxValue: undefined
2526
2323
  }));
2324
+ // Use Sets for O(1) unique value collection during analysis
2325
+ const sharedItemsSets = this._fields.map(()=>new Set());
2527
2326
  // Analyze data to determine field types and collect unique values
2528
2327
  for (const row of data){
2529
2328
  for(let colIdx = 0; colIdx < row.length && colIdx < this._fields.length; colIdx++){
@@ -2534,9 +2333,8 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
2534
2333
  }
2535
2334
  if (typeof value === 'string') {
2536
2335
  field.isNumeric = false;
2537
- if (!field.sharedItems.includes(value)) {
2538
- field.sharedItems.push(value);
2539
- }
2336
+ // O(1) Set.add instead of O(n) Array.includes + push
2337
+ sharedItemsSets[colIdx].add(value);
2540
2338
  } else if (typeof value === 'number') {
2541
2339
  if (field.minValue === undefined || value < field.minValue) {
2542
2340
  field.minValue = value;
@@ -2552,8 +2350,22 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
2552
2350
  }
2553
2351
  }
2554
2352
  }
2555
- // Enable date grouping flag if any date field exists
2556
- this._dateGrouping = this._fields.some((field)=>field.isDate);
2353
+ // Convert Sets to arrays and build reverse index Maps for O(1) lookup during XML generation
2354
+ this._sharedItemsIndexMap.clear();
2355
+ for(let colIdx = 0; colIdx < this._fields.length; colIdx++){
2356
+ const field = this._fields[colIdx];
2357
+ const set = sharedItemsSets[colIdx];
2358
+ // Convert Set to array (maintains insertion order in ES6+)
2359
+ field.sharedItems = Array.from(set);
2360
+ // Build reverse lookup Map: value -> index
2361
+ if (field.sharedItems.length > 0) {
2362
+ const indexMap = new Map();
2363
+ for(let i = 0; i < field.sharedItems.length; i++){
2364
+ indexMap.set(field.sharedItems[i], i);
2365
+ }
2366
+ this._sharedItemsIndexMap.set(colIdx, indexMap);
2367
+ }
2368
+ }
2557
2369
  // Store records
2558
2370
  this._records = data;
2559
2371
  }
@@ -2582,8 +2394,6 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
2582
2394
  v: item
2583
2395
  }, []));
2584
2396
  }
2585
- } else if (field.isDate) {
2586
- sharedItemsAttrs.containsDate = '1';
2587
2397
  } else if (field.isNumeric) {
2588
2398
  // Numeric field - use "0"/"1" for boolean attributes as Excel expects
2589
2399
  sharedItemsAttrs.containsSemiMixedTypes = '0';
@@ -2614,13 +2424,9 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
2614
2424
  ref: this._sourceRange,
2615
2425
  sheet: this._sourceSheet
2616
2426
  }, []);
2617
- const cacheSourceAttrs = {
2427
+ const cacheSourceNode = createElement('cacheSource', {
2618
2428
  type: 'worksheet'
2619
- };
2620
- if (this._dateGrouping) {
2621
- cacheSourceAttrs.grouping = '1';
2622
- }
2623
- const cacheSourceNode = createElement('cacheSource', cacheSourceAttrs, [
2429
+ }, [
2624
2430
  worksheetSourceNode
2625
2431
  ]);
2626
2432
  // Build attributes - refreshOnLoad should come early per OOXML schema
@@ -2654,15 +2460,15 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
2654
2460
  for (const row of this._records){
2655
2461
  const fieldNodes = [];
2656
2462
  for(let colIdx = 0; colIdx < this._fields.length; colIdx++){
2657
- const field = this._fields[colIdx];
2658
2463
  const value = colIdx < row.length ? row[colIdx] : null;
2659
2464
  if (value === null || value === undefined) {
2660
2465
  // Missing value
2661
2466
  fieldNodes.push(createElement('m', {}, []));
2662
2467
  } else if (typeof value === 'string') {
2663
- // String value - use index into sharedItems
2664
- const idx = field.sharedItems.indexOf(value);
2665
- if (idx >= 0) {
2468
+ // String value - use index into sharedItems via O(1) Map lookup
2469
+ const indexMap = this._sharedItemsIndexMap.get(colIdx);
2470
+ const idx = indexMap?.get(value);
2471
+ if (idx !== undefined) {
2666
2472
  fieldNodes.push(createElement('x', {
2667
2473
  v: String(idx)
2668
2474
  }, []));
@@ -2766,8 +2572,6 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
2766
2572
  this._pivotTables = [];
2767
2573
  this._pivotCaches = [];
2768
2574
  this._nextCacheId = 0;
2769
- // Date serialization handling
2770
- this._dateHandling = 'jsDate';
2771
2575
  this._sharedStrings = new SharedStrings();
2772
2576
  this._styles = Styles.createDefault();
2773
2577
  }
@@ -2832,16 +2636,6 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
2832
2636
  return this._styles;
2833
2637
  }
2834
2638
  /**
2835
- * Get the workbook date handling strategy.
2836
- */ get dateHandling() {
2837
- return this._dateHandling;
2838
- }
2839
- /**
2840
- * Set the workbook date handling strategy.
2841
- */ set dateHandling(value) {
2842
- this._dateHandling = value;
2843
- }
2844
- /**
2845
2639
  * Get a worksheet by name or index
2846
2640
  */ sheet(nameOrIndex) {
2847
2641
  let def;
@@ -3152,6 +2946,8 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
3152
2946
  // Create pivot table
3153
2947
  const pivotTableIndex = this._pivotTables.length + 1;
3154
2948
  const pivotTable = new PivotTable(config.name, cache, targetSheet, targetCell, targetAddr.row + 1, targetAddr.col, pivotTableIndex);
2949
+ // Set styles reference for number format resolution
2950
+ pivotTable.setStyles(this._styles);
3155
2951
  this._pivotTables.push(pivotTable);
3156
2952
  return pivotTable;
3157
2953
  }
@@ -3529,10 +3325,6 @@ exports.SharedStrings = SharedStrings;
3529
3325
  exports.Styles = Styles;
3530
3326
  exports.Workbook = Workbook;
3531
3327
  exports.Worksheet = Worksheet;
3532
- exports.colToLetter = colToLetter;
3533
- exports.isInRange = isInRange;
3534
- exports.letterToCol = letterToCol;
3535
- exports.normalizeRange = normalizeRange;
3536
3328
  exports.parseAddress = parseAddress;
3537
3329
  exports.parseRange = parseRange;
3538
3330
  exports.toAddress = toAddress;