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