@niicojs/excel 0.2.3 → 0.2.4

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,8 +38,10 @@ 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}`);
41
43
  const col = letterToCol(match[1].toUpperCase());
42
- const row = parseInt(match[2], 10) - 1; // Convert to 0-based
44
+ const row = rowNumber - 1; // Convert to 0-based
43
45
  return {
44
46
  row,
45
47
  col
@@ -101,6 +103,12 @@ import { unzip, strFromU8, zip, strToU8 } from 'fflate';
101
103
  }
102
104
  };
103
105
  };
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
+ };
104
112
 
105
113
  // Excel epoch: December 30, 1899 (accounting for the 1900 leap year bug)
106
114
  const EXCEL_EPOCH = new Date(Date.UTC(1899, 11, 30));
@@ -433,12 +441,23 @@ _computedKey = Symbol.iterator;
433
441
  /**
434
442
  * Get all values in the range as a 2D array
435
443
  */ 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;
436
450
  const result = [];
437
451
  for(let r = this._range.start.row; r <= this._range.end.row; r++){
438
452
  const row = [];
439
453
  for(let c = this._range.start.col; c <= this._range.end.col; c++){
440
- const cell = this._worksheet.cell(r, c);
441
- row.push(cell.value);
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
+ }
442
461
  }
443
462
  result.push(row);
444
463
  }
@@ -611,6 +630,11 @@ const builder = new XMLBuilder(builderOptions);
611
630
  this._dirty = false;
612
631
  this._mergedCells = new Set();
613
632
  this._sheetData = [];
633
+ this._columnWidths = new Map();
634
+ this._rowHeights = new Map();
635
+ this._frozenPane = null;
636
+ this._dataBoundsCache = null;
637
+ this._boundsDirty = true;
614
638
  this._workbook = workbook;
615
639
  this._name = name;
616
640
  }
@@ -637,12 +661,49 @@ const builder = new XMLBuilder(builderOptions);
637
661
  const worksheet = findElement(this._xmlNodes, 'worksheet');
638
662
  if (!worksheet) return;
639
663
  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
+ }
640
684
  // Parse sheet data (cells)
641
685
  const sheetData = findElement(worksheetChildren, 'sheetData');
642
686
  if (sheetData) {
643
687
  this._sheetData = getChildren(sheetData, 'sheetData');
644
688
  this._parseSheetData(this._sheetData);
645
689
  }
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
+ }
646
707
  // Parse merged cells
647
708
  const mergeCells = findElement(worksheetChildren, 'mergeCells');
648
709
  if (mergeCells) {
@@ -662,6 +723,11 @@ const builder = new XMLBuilder(builderOptions);
662
723
  */ _parseSheetData(rows) {
663
724
  for (const rowNode of rows){
664
725
  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
+ }
665
731
  const rowChildren = getChildren(rowNode, 'row');
666
732
  for (const cellNode of rowChildren){
667
733
  if (!('c' in cellNode)) continue;
@@ -673,6 +739,7 @@ const builder = new XMLBuilder(builderOptions);
673
739
  this._cells.set(ref, cell);
674
740
  }
675
741
  }
742
+ this._boundsDirty = true;
676
743
  }
677
744
  /**
678
745
  * Parse a cell XML node to CellData
@@ -759,9 +826,17 @@ const builder = new XMLBuilder(builderOptions);
759
826
  if (!cell) {
760
827
  cell = new Cell(this, row, c);
761
828
  this._cells.set(address, cell);
829
+ this._boundsDirty = true;
762
830
  }
763
831
  return cell;
764
832
  }
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
+ }
765
840
  range(startRowOrRange, startCol, endRow, endCol) {
766
841
  let rangeAddr;
767
842
  if (typeof startRowOrRange === 'string') {
@@ -821,6 +896,65 @@ const builder = new XMLBuilder(builderOptions);
821
896
  return this._cells;
822
897
  }
823
898
  /**
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
+ /**
824
958
  * Convert sheet data to an array of JSON objects.
825
959
  *
826
960
  * @param config - Configuration options
@@ -838,7 +972,7 @@ const builder = new XMLBuilder(builderOptions);
838
972
  * const data = sheet.toJson({ startRow: 2, startCol: 1 });
839
973
  * ```
840
974
  */ toJson(config = {}) {
841
- const { fields, startRow = 0, startCol = 0, endRow, endCol, stopOnEmptyRow = true } = config;
975
+ const { fields, startRow = 0, startCol = 0, endRow, endCol, stopOnEmptyRow = true, dateHandling = this._workbook.dateHandling } = config;
842
976
  // Get the bounds of data in the sheet
843
977
  const bounds = this._getDataBounds();
844
978
  if (!bounds) {
@@ -871,7 +1005,10 @@ const builder = new XMLBuilder(builderOptions);
871
1005
  for(let colOffset = 0; colOffset < fieldNames.length; colOffset++){
872
1006
  const col = startCol + colOffset;
873
1007
  const cell = this._cells.get(toAddress(row, col));
874
- const value = cell?.value ?? null;
1008
+ let value = cell?.value ?? null;
1009
+ if (value instanceof Date) {
1010
+ value = this._serializeDate(value, dateHandling, cell);
1011
+ }
875
1012
  if (value !== null) {
876
1013
  hasData = true;
877
1014
  }
@@ -888,10 +1025,24 @@ const builder = new XMLBuilder(builderOptions);
888
1025
  }
889
1026
  return result;
890
1027
  }
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
+ }
891
1037
  /**
892
1038
  * Get the bounds of data in the sheet (min/max row and column with data)
893
1039
  */ _getDataBounds() {
1040
+ if (!this._boundsDirty && this._dataBoundsCache) {
1041
+ return this._dataBoundsCache;
1042
+ }
894
1043
  if (this._cells.size === 0) {
1044
+ this._dataBoundsCache = null;
1045
+ this._boundsDirty = false;
895
1046
  return null;
896
1047
  }
897
1048
  let minRow = Infinity;
@@ -907,14 +1058,18 @@ const builder = new XMLBuilder(builderOptions);
907
1058
  }
908
1059
  }
909
1060
  if (minRow === Infinity) {
1061
+ this._dataBoundsCache = null;
1062
+ this._boundsDirty = false;
910
1063
  return null;
911
1064
  }
912
- return {
1065
+ this._dataBoundsCache = {
913
1066
  minRow,
914
1067
  maxRow,
915
1068
  minCol,
916
1069
  maxCol
917
1070
  };
1071
+ this._boundsDirty = false;
1072
+ return this._dataBoundsCache;
918
1073
  }
919
1074
  /**
920
1075
  * Generate XML for this worksheet
@@ -928,6 +1083,11 @@ const builder = new XMLBuilder(builderOptions);
928
1083
  }
929
1084
  rowMap.get(row).push(cell);
930
1085
  }
1086
+ for (const rowIdx of this._rowHeights.keys()){
1087
+ if (!rowMap.has(rowIdx)) {
1088
+ rowMap.set(rowIdx, []);
1089
+ }
1090
+ }
931
1091
  // Sort rows and cells
932
1092
  const sortedRows = Array.from(rowMap.entries()).sort((a, b)=>a[0] - b[0]);
933
1093
  const rowNodes = [];
@@ -938,16 +1098,71 @@ const builder = new XMLBuilder(builderOptions);
938
1098
  const cellNode = this._buildCellNode(cell);
939
1099
  cellNodes.push(cellNode);
940
1100
  }
941
- const rowNode = createElement('row', {
1101
+ const rowAttrs = {
942
1102
  r: String(rowIdx + 1)
943
- }, cellNodes);
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);
944
1110
  rowNodes.push(rowNode);
945
1111
  }
946
1112
  const sheetDataNode = createElement('sheetData', {}, rowNodes);
947
1113
  // Build worksheet structure
948
- const worksheetChildren = [
949
- sheetDataNode
950
- ];
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);
951
1166
  // Add merged cells if any
952
1167
  if (this._mergedCells.size > 0) {
953
1168
  const mergeCellNodes = [];
@@ -1134,6 +1349,40 @@ const builder = new XMLBuilder(builderOptions);
1134
1349
  * Manages the styles (xl/styles.xml)
1135
1350
  */ class Styles {
1136
1351
  /**
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
+ /**
1137
1386
  * Parse styles from XML content
1138
1387
  */ static parse(xml) {
1139
1388
  const styles = new Styles();
@@ -1300,6 +1549,10 @@ const builder = new XMLBuilder(builderOptions);
1300
1549
  /**
1301
1550
  * Get a style by index
1302
1551
  */ getStyle(index) {
1552
+ const cached = this._styleObjectCache.get(index);
1553
+ if (cached) return {
1554
+ ...cached
1555
+ };
1303
1556
  const xf = this._cellXfs[index];
1304
1557
  if (!xf) return {};
1305
1558
  const font = this._fonts[xf.fontId];
@@ -1340,13 +1593,16 @@ const builder = new XMLBuilder(builderOptions);
1340
1593
  textRotation: xf.alignment.textRotation
1341
1594
  };
1342
1595
  }
1596
+ this._styleObjectCache.set(index, {
1597
+ ...style
1598
+ });
1343
1599
  return style;
1344
1600
  }
1345
1601
  /**
1346
1602
  * Create a style and return its index
1347
1603
  * Uses caching to deduplicate identical styles
1348
1604
  */ createStyle(style) {
1349
- const key = JSON.stringify(style);
1605
+ const key = this._getStyleKey(style);
1350
1606
  const cached = this._styleCache.get(key);
1351
1607
  if (cached !== undefined) {
1352
1608
  return cached;
@@ -1378,8 +1634,20 @@ const builder = new XMLBuilder(builderOptions);
1378
1634
  const index = this._cellXfs.length;
1379
1635
  this._cellXfs.push(xf);
1380
1636
  this._styleCache.set(key, index);
1637
+ this._styleObjectCache.set(index, {
1638
+ ...style
1639
+ });
1381
1640
  return index;
1382
1641
  }
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
+ }
1383
1651
  _findOrCreateFont(style) {
1384
1652
  const font = {
1385
1653
  bold: style.bold || false,
@@ -1615,6 +1883,7 @@ const builder = new XMLBuilder(builderOptions);
1615
1883
  this._dirty = false;
1616
1884
  // Cache for style deduplication
1617
1885
  this._styleCache = new Map();
1886
+ this._styleObjectCache = new Map();
1618
1887
  }
1619
1888
  }
1620
1889
 
@@ -1626,6 +1895,7 @@ const builder = new XMLBuilder(builderOptions);
1626
1895
  this._columnFields = [];
1627
1896
  this._valueFields = [];
1628
1897
  this._filterFields = [];
1898
+ this._fieldAssignments = new Map();
1629
1899
  this._name = name;
1630
1900
  this._cache = cache;
1631
1901
  this._targetSheet = targetSheet;
@@ -1667,11 +1937,13 @@ const builder = new XMLBuilder(builderOptions);
1667
1937
  if (fieldIndex < 0) {
1668
1938
  throw new Error(`Field not found in source data: ${fieldName}`);
1669
1939
  }
1670
- this._rowFields.push({
1940
+ const assignment = {
1671
1941
  fieldName,
1672
1942
  fieldIndex,
1673
1943
  axis: 'row'
1674
- });
1944
+ };
1945
+ this._rowFields.push(assignment);
1946
+ this._fieldAssignments.set(fieldIndex, assignment);
1675
1947
  return this;
1676
1948
  }
1677
1949
  /**
@@ -1682,11 +1954,13 @@ const builder = new XMLBuilder(builderOptions);
1682
1954
  if (fieldIndex < 0) {
1683
1955
  throw new Error(`Field not found in source data: ${fieldName}`);
1684
1956
  }
1685
- this._columnFields.push({
1957
+ const assignment = {
1686
1958
  fieldName,
1687
1959
  fieldIndex,
1688
1960
  axis: 'column'
1689
- });
1961
+ };
1962
+ this._columnFields.push(assignment);
1963
+ this._fieldAssignments.set(fieldIndex, assignment);
1690
1964
  return this;
1691
1965
  }
1692
1966
  /**
@@ -1700,13 +1974,15 @@ const builder = new XMLBuilder(builderOptions);
1700
1974
  throw new Error(`Field not found in source data: ${fieldName}`);
1701
1975
  }
1702
1976
  const defaultName = `${aggregation.charAt(0).toUpperCase() + aggregation.slice(1)} of ${fieldName}`;
1703
- this._valueFields.push({
1977
+ const assignment = {
1704
1978
  fieldName,
1705
1979
  fieldIndex,
1706
1980
  axis: 'value',
1707
1981
  aggregation,
1708
1982
  displayName: displayName || defaultName
1709
- });
1983
+ };
1984
+ this._valueFields.push(assignment);
1985
+ this._fieldAssignments.set(fieldIndex, assignment);
1710
1986
  return this;
1711
1987
  }
1712
1988
  /**
@@ -1717,11 +1993,44 @@ const builder = new XMLBuilder(builderOptions);
1717
1993
  if (fieldIndex < 0) {
1718
1994
  throw new Error(`Field not found in source data: ${fieldName}`);
1719
1995
  }
1720
- this._filterFields.push({
1996
+ const assignment = {
1721
1997
  fieldName,
1722
1998
  fieldIndex,
1723
1999
  axis: 'filter'
1724
- });
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;
1725
2034
  return this;
1726
2035
  }
1727
2036
  /**
@@ -1886,17 +2195,28 @@ const builder = new XMLBuilder(builderOptions);
1886
2195
  const colField = this._columnFields.find((f)=>f.fieldIndex === fieldIndex);
1887
2196
  const filterField = this._filterFields.find((f)=>f.fieldIndex === fieldIndex);
1888
2197
  const valueField = this._valueFields.find((f)=>f.fieldIndex === fieldIndex);
2198
+ const assignment = this._fieldAssignments.get(fieldIndex);
1889
2199
  if (rowField) {
1890
2200
  attrs.axis = 'axisRow';
1891
2201
  attrs.showAll = '0';
2202
+ if (assignment?.sortOrder) {
2203
+ attrs.sortType = 'ascending';
2204
+ attrs.sortOrder = assignment.sortOrder === 'asc' ? 'ascending' : 'descending';
2205
+ }
1892
2206
  // Add items for shared values
1893
2207
  const cacheField = this._cache.fields[fieldIndex];
1894
2208
  if (cacheField && cacheField.sharedItems.length > 0) {
1895
2209
  const itemNodes = [];
2210
+ const allowedIndexes = this._resolveItemFilter(cacheField.sharedItems, assignment?.filter);
1896
2211
  for(let i = 0; i < cacheField.sharedItems.length; i++){
1897
- itemNodes.push(createElement('item', {
2212
+ const shouldInclude = allowedIndexes.has(i);
2213
+ const itemAttrs = {
1898
2214
  x: String(i)
1899
- }, []));
2215
+ };
2216
+ if (!shouldInclude) {
2217
+ itemAttrs.h = '1';
2218
+ }
2219
+ itemNodes.push(createElement('item', itemAttrs, []));
1900
2220
  }
1901
2221
  // Add default subtotal item
1902
2222
  itemNodes.push(createElement('item', {
@@ -1909,13 +2229,23 @@ const builder = new XMLBuilder(builderOptions);
1909
2229
  } else if (colField) {
1910
2230
  attrs.axis = 'axisCol';
1911
2231
  attrs.showAll = '0';
2232
+ if (assignment?.sortOrder) {
2233
+ attrs.sortType = 'ascending';
2234
+ attrs.sortOrder = assignment.sortOrder === 'asc' ? 'ascending' : 'descending';
2235
+ }
1912
2236
  const cacheField = this._cache.fields[fieldIndex];
1913
2237
  if (cacheField && cacheField.sharedItems.length > 0) {
1914
2238
  const itemNodes = [];
2239
+ const allowedIndexes = this._resolveItemFilter(cacheField.sharedItems, assignment?.filter);
1915
2240
  for(let i = 0; i < cacheField.sharedItems.length; i++){
1916
- itemNodes.push(createElement('item', {
2241
+ const shouldInclude = allowedIndexes.has(i);
2242
+ const itemAttrs = {
1917
2243
  x: String(i)
1918
- }, []));
2244
+ };
2245
+ if (!shouldInclude) {
2246
+ itemAttrs.h = '1';
2247
+ }
2248
+ itemNodes.push(createElement('item', itemAttrs, []));
1919
2249
  }
1920
2250
  itemNodes.push(createElement('item', {
1921
2251
  t: 'default'
@@ -1930,10 +2260,16 @@ const builder = new XMLBuilder(builderOptions);
1930
2260
  const cacheField = this._cache.fields[fieldIndex];
1931
2261
  if (cacheField && cacheField.sharedItems.length > 0) {
1932
2262
  const itemNodes = [];
2263
+ const allowedIndexes = this._resolveItemFilter(cacheField.sharedItems, assignment?.filter);
1933
2264
  for(let i = 0; i < cacheField.sharedItems.length; i++){
1934
- itemNodes.push(createElement('item', {
2265
+ const shouldInclude = allowedIndexes.has(i);
2266
+ const itemAttrs = {
1935
2267
  x: String(i)
1936
- }, []));
2268
+ };
2269
+ if (!shouldInclude) {
2270
+ itemAttrs.h = '1';
2271
+ }
2272
+ itemNodes.push(createElement('item', itemAttrs, []));
1937
2273
  }
1938
2274
  itemNodes.push(createElement('item', {
1939
2275
  t: 'default'
@@ -1950,6 +2286,31 @@ const builder = new XMLBuilder(builderOptions);
1950
2286
  }
1951
2287
  return createElement('pivotField', attrs, children);
1952
2288
  }
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
+ }
1953
2314
  /**
1954
2315
  * Build row items based on unique values in row fields
1955
2316
  */ _buildRowItems() {
@@ -2100,6 +2461,7 @@ const builder = new XMLBuilder(builderOptions);
2100
2461
  this._records = [];
2101
2462
  this._recordCount = 0;
2102
2463
  this._refreshOnLoad = true; // Default to true
2464
+ this._dateGrouping = false;
2103
2465
  this._cacheId = cacheId;
2104
2466
  this._sourceSheet = sourceSheet;
2105
2467
  this._sourceRange = sourceRange;
@@ -2188,6 +2550,8 @@ const builder = new XMLBuilder(builderOptions);
2188
2550
  }
2189
2551
  }
2190
2552
  }
2553
+ // Enable date grouping flag if any date field exists
2554
+ this._dateGrouping = this._fields.some((field)=>field.isDate);
2191
2555
  // Store records
2192
2556
  this._records = data;
2193
2557
  }
@@ -2216,6 +2580,8 @@ const builder = new XMLBuilder(builderOptions);
2216
2580
  v: item
2217
2581
  }, []));
2218
2582
  }
2583
+ } else if (field.isDate) {
2584
+ sharedItemsAttrs.containsDate = '1';
2219
2585
  } else if (field.isNumeric) {
2220
2586
  // Numeric field - use "0"/"1" for boolean attributes as Excel expects
2221
2587
  sharedItemsAttrs.containsSemiMixedTypes = '0';
@@ -2246,9 +2612,13 @@ const builder = new XMLBuilder(builderOptions);
2246
2612
  ref: this._sourceRange,
2247
2613
  sheet: this._sourceSheet
2248
2614
  }, []);
2249
- const cacheSourceNode = createElement('cacheSource', {
2615
+ const cacheSourceAttrs = {
2250
2616
  type: 'worksheet'
2251
- }, [
2617
+ };
2618
+ if (this._dateGrouping) {
2619
+ cacheSourceAttrs.grouping = '1';
2620
+ }
2621
+ const cacheSourceNode = createElement('cacheSource', cacheSourceAttrs, [
2252
2622
  worksheetSourceNode
2253
2623
  ]);
2254
2624
  // Build attributes - refreshOnLoad should come early per OOXML schema
@@ -2394,6 +2764,8 @@ const builder = new XMLBuilder(builderOptions);
2394
2764
  this._pivotTables = [];
2395
2765
  this._pivotCaches = [];
2396
2766
  this._nextCacheId = 0;
2767
+ // Date serialization handling
2768
+ this._dateHandling = 'jsDate';
2397
2769
  this._sharedStrings = new SharedStrings();
2398
2770
  this._styles = Styles.createDefault();
2399
2771
  }
@@ -2458,6 +2830,16 @@ const builder = new XMLBuilder(builderOptions);
2458
2830
  return this._styles;
2459
2831
  }
2460
2832
  /**
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
+ /**
2461
2843
  * Get a worksheet by name or index
2462
2844
  */ sheet(nameOrIndex) {
2463
2845
  let def;
@@ -3137,4 +3519,4 @@ const builder = new XMLBuilder(builderOptions);
3137
3519
  }
3138
3520
  }
3139
3521
 
3140
- export { Cell, PivotCache, PivotTable, Range, SharedStrings, Styles, Workbook, Worksheet, parseAddress, parseRange, toAddress, toRange };
3522
+ export { Cell, PivotCache, PivotTable, Range, SharedStrings, Styles, Workbook, Worksheet, colToLetter, isInRange, letterToCol, normalizeRange, parseAddress, parseRange, toAddress, toRange };