@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.cjs CHANGED
@@ -40,8 +40,10 @@ 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}`);
43
45
  const col = letterToCol(match[1].toUpperCase());
44
- const row = parseInt(match[2], 10) - 1; // Convert to 0-based
46
+ const row = rowNumber - 1; // Convert to 0-based
45
47
  return {
46
48
  row,
47
49
  col
@@ -103,6 +105,12 @@ var fflate = require('fflate');
103
105
  }
104
106
  };
105
107
  };
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
+ };
106
114
 
107
115
  // Excel epoch: December 30, 1899 (accounting for the 1900 leap year bug)
108
116
  const EXCEL_EPOCH = new Date(Date.UTC(1899, 11, 30));
@@ -435,12 +443,23 @@ _computedKey = Symbol.iterator;
435
443
  /**
436
444
  * Get all values in the range as a 2D array
437
445
  */ 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;
438
452
  const result = [];
439
453
  for(let r = this._range.start.row; r <= this._range.end.row; r++){
440
454
  const row = [];
441
455
  for(let c = this._range.start.col; c <= this._range.end.col; c++){
442
- const cell = this._worksheet.cell(r, c);
443
- row.push(cell.value);
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
+ }
444
463
  }
445
464
  result.push(row);
446
465
  }
@@ -613,6 +632,11 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
613
632
  this._dirty = false;
614
633
  this._mergedCells = new Set();
615
634
  this._sheetData = [];
635
+ this._columnWidths = new Map();
636
+ this._rowHeights = new Map();
637
+ this._frozenPane = null;
638
+ this._dataBoundsCache = null;
639
+ this._boundsDirty = true;
616
640
  this._workbook = workbook;
617
641
  this._name = name;
618
642
  }
@@ -639,12 +663,49 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
639
663
  const worksheet = findElement(this._xmlNodes, 'worksheet');
640
664
  if (!worksheet) return;
641
665
  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
+ }
642
686
  // Parse sheet data (cells)
643
687
  const sheetData = findElement(worksheetChildren, 'sheetData');
644
688
  if (sheetData) {
645
689
  this._sheetData = getChildren(sheetData, 'sheetData');
646
690
  this._parseSheetData(this._sheetData);
647
691
  }
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
+ }
648
709
  // Parse merged cells
649
710
  const mergeCells = findElement(worksheetChildren, 'mergeCells');
650
711
  if (mergeCells) {
@@ -664,6 +725,11 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
664
725
  */ _parseSheetData(rows) {
665
726
  for (const rowNode of rows){
666
727
  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
+ }
667
733
  const rowChildren = getChildren(rowNode, 'row');
668
734
  for (const cellNode of rowChildren){
669
735
  if (!('c' in cellNode)) continue;
@@ -675,6 +741,7 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
675
741
  this._cells.set(ref, cell);
676
742
  }
677
743
  }
744
+ this._boundsDirty = true;
678
745
  }
679
746
  /**
680
747
  * Parse a cell XML node to CellData
@@ -761,9 +828,17 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
761
828
  if (!cell) {
762
829
  cell = new Cell(this, row, c);
763
830
  this._cells.set(address, cell);
831
+ this._boundsDirty = true;
764
832
  }
765
833
  return cell;
766
834
  }
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
+ }
767
842
  range(startRowOrRange, startCol, endRow, endCol) {
768
843
  let rangeAddr;
769
844
  if (typeof startRowOrRange === 'string') {
@@ -823,6 +898,65 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
823
898
  return this._cells;
824
899
  }
825
900
  /**
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
+ /**
826
960
  * Convert sheet data to an array of JSON objects.
827
961
  *
828
962
  * @param config - Configuration options
@@ -840,7 +974,7 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
840
974
  * const data = sheet.toJson({ startRow: 2, startCol: 1 });
841
975
  * ```
842
976
  */ toJson(config = {}) {
843
- const { fields, startRow = 0, startCol = 0, endRow, endCol, stopOnEmptyRow = true } = config;
977
+ const { fields, startRow = 0, startCol = 0, endRow, endCol, stopOnEmptyRow = true, dateHandling = this._workbook.dateHandling } = config;
844
978
  // Get the bounds of data in the sheet
845
979
  const bounds = this._getDataBounds();
846
980
  if (!bounds) {
@@ -873,7 +1007,10 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
873
1007
  for(let colOffset = 0; colOffset < fieldNames.length; colOffset++){
874
1008
  const col = startCol + colOffset;
875
1009
  const cell = this._cells.get(toAddress(row, col));
876
- const value = cell?.value ?? null;
1010
+ let value = cell?.value ?? null;
1011
+ if (value instanceof Date) {
1012
+ value = this._serializeDate(value, dateHandling, cell);
1013
+ }
877
1014
  if (value !== null) {
878
1015
  hasData = true;
879
1016
  }
@@ -890,10 +1027,24 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
890
1027
  }
891
1028
  return result;
892
1029
  }
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
+ }
893
1039
  /**
894
1040
  * Get the bounds of data in the sheet (min/max row and column with data)
895
1041
  */ _getDataBounds() {
1042
+ if (!this._boundsDirty && this._dataBoundsCache) {
1043
+ return this._dataBoundsCache;
1044
+ }
896
1045
  if (this._cells.size === 0) {
1046
+ this._dataBoundsCache = null;
1047
+ this._boundsDirty = false;
897
1048
  return null;
898
1049
  }
899
1050
  let minRow = Infinity;
@@ -909,14 +1060,18 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
909
1060
  }
910
1061
  }
911
1062
  if (minRow === Infinity) {
1063
+ this._dataBoundsCache = null;
1064
+ this._boundsDirty = false;
912
1065
  return null;
913
1066
  }
914
- return {
1067
+ this._dataBoundsCache = {
915
1068
  minRow,
916
1069
  maxRow,
917
1070
  minCol,
918
1071
  maxCol
919
1072
  };
1073
+ this._boundsDirty = false;
1074
+ return this._dataBoundsCache;
920
1075
  }
921
1076
  /**
922
1077
  * Generate XML for this worksheet
@@ -930,6 +1085,11 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
930
1085
  }
931
1086
  rowMap.get(row).push(cell);
932
1087
  }
1088
+ for (const rowIdx of this._rowHeights.keys()){
1089
+ if (!rowMap.has(rowIdx)) {
1090
+ rowMap.set(rowIdx, []);
1091
+ }
1092
+ }
933
1093
  // Sort rows and cells
934
1094
  const sortedRows = Array.from(rowMap.entries()).sort((a, b)=>a[0] - b[0]);
935
1095
  const rowNodes = [];
@@ -940,16 +1100,71 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
940
1100
  const cellNode = this._buildCellNode(cell);
941
1101
  cellNodes.push(cellNode);
942
1102
  }
943
- const rowNode = createElement('row', {
1103
+ const rowAttrs = {
944
1104
  r: String(rowIdx + 1)
945
- }, cellNodes);
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);
946
1112
  rowNodes.push(rowNode);
947
1113
  }
948
1114
  const sheetDataNode = createElement('sheetData', {}, rowNodes);
949
1115
  // Build worksheet structure
950
- const worksheetChildren = [
951
- sheetDataNode
952
- ];
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);
953
1168
  // Add merged cells if any
954
1169
  if (this._mergedCells.size > 0) {
955
1170
  const mergeCellNodes = [];
@@ -1136,6 +1351,40 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
1136
1351
  * Manages the styles (xl/styles.xml)
1137
1352
  */ class Styles {
1138
1353
  /**
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
+ /**
1139
1388
  * Parse styles from XML content
1140
1389
  */ static parse(xml) {
1141
1390
  const styles = new Styles();
@@ -1302,6 +1551,10 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
1302
1551
  /**
1303
1552
  * Get a style by index
1304
1553
  */ getStyle(index) {
1554
+ const cached = this._styleObjectCache.get(index);
1555
+ if (cached) return {
1556
+ ...cached
1557
+ };
1305
1558
  const xf = this._cellXfs[index];
1306
1559
  if (!xf) return {};
1307
1560
  const font = this._fonts[xf.fontId];
@@ -1342,13 +1595,16 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
1342
1595
  textRotation: xf.alignment.textRotation
1343
1596
  };
1344
1597
  }
1598
+ this._styleObjectCache.set(index, {
1599
+ ...style
1600
+ });
1345
1601
  return style;
1346
1602
  }
1347
1603
  /**
1348
1604
  * Create a style and return its index
1349
1605
  * Uses caching to deduplicate identical styles
1350
1606
  */ createStyle(style) {
1351
- const key = JSON.stringify(style);
1607
+ const key = this._getStyleKey(style);
1352
1608
  const cached = this._styleCache.get(key);
1353
1609
  if (cached !== undefined) {
1354
1610
  return cached;
@@ -1380,8 +1636,20 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
1380
1636
  const index = this._cellXfs.length;
1381
1637
  this._cellXfs.push(xf);
1382
1638
  this._styleCache.set(key, index);
1639
+ this._styleObjectCache.set(index, {
1640
+ ...style
1641
+ });
1383
1642
  return index;
1384
1643
  }
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
+ }
1385
1653
  _findOrCreateFont(style) {
1386
1654
  const font = {
1387
1655
  bold: style.bold || false,
@@ -1617,6 +1885,7 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
1617
1885
  this._dirty = false;
1618
1886
  // Cache for style deduplication
1619
1887
  this._styleCache = new Map();
1888
+ this._styleObjectCache = new Map();
1620
1889
  }
1621
1890
  }
1622
1891
 
@@ -1628,6 +1897,7 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
1628
1897
  this._columnFields = [];
1629
1898
  this._valueFields = [];
1630
1899
  this._filterFields = [];
1900
+ this._fieldAssignments = new Map();
1631
1901
  this._name = name;
1632
1902
  this._cache = cache;
1633
1903
  this._targetSheet = targetSheet;
@@ -1669,11 +1939,13 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
1669
1939
  if (fieldIndex < 0) {
1670
1940
  throw new Error(`Field not found in source data: ${fieldName}`);
1671
1941
  }
1672
- this._rowFields.push({
1942
+ const assignment = {
1673
1943
  fieldName,
1674
1944
  fieldIndex,
1675
1945
  axis: 'row'
1676
- });
1946
+ };
1947
+ this._rowFields.push(assignment);
1948
+ this._fieldAssignments.set(fieldIndex, assignment);
1677
1949
  return this;
1678
1950
  }
1679
1951
  /**
@@ -1684,11 +1956,13 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
1684
1956
  if (fieldIndex < 0) {
1685
1957
  throw new Error(`Field not found in source data: ${fieldName}`);
1686
1958
  }
1687
- this._columnFields.push({
1959
+ const assignment = {
1688
1960
  fieldName,
1689
1961
  fieldIndex,
1690
1962
  axis: 'column'
1691
- });
1963
+ };
1964
+ this._columnFields.push(assignment);
1965
+ this._fieldAssignments.set(fieldIndex, assignment);
1692
1966
  return this;
1693
1967
  }
1694
1968
  /**
@@ -1702,13 +1976,15 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
1702
1976
  throw new Error(`Field not found in source data: ${fieldName}`);
1703
1977
  }
1704
1978
  const defaultName = `${aggregation.charAt(0).toUpperCase() + aggregation.slice(1)} of ${fieldName}`;
1705
- this._valueFields.push({
1979
+ const assignment = {
1706
1980
  fieldName,
1707
1981
  fieldIndex,
1708
1982
  axis: 'value',
1709
1983
  aggregation,
1710
1984
  displayName: displayName || defaultName
1711
- });
1985
+ };
1986
+ this._valueFields.push(assignment);
1987
+ this._fieldAssignments.set(fieldIndex, assignment);
1712
1988
  return this;
1713
1989
  }
1714
1990
  /**
@@ -1719,11 +1995,44 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
1719
1995
  if (fieldIndex < 0) {
1720
1996
  throw new Error(`Field not found in source data: ${fieldName}`);
1721
1997
  }
1722
- this._filterFields.push({
1998
+ const assignment = {
1723
1999
  fieldName,
1724
2000
  fieldIndex,
1725
2001
  axis: 'filter'
1726
- });
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;
1727
2036
  return this;
1728
2037
  }
1729
2038
  /**
@@ -1888,17 +2197,28 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
1888
2197
  const colField = this._columnFields.find((f)=>f.fieldIndex === fieldIndex);
1889
2198
  const filterField = this._filterFields.find((f)=>f.fieldIndex === fieldIndex);
1890
2199
  const valueField = this._valueFields.find((f)=>f.fieldIndex === fieldIndex);
2200
+ const assignment = this._fieldAssignments.get(fieldIndex);
1891
2201
  if (rowField) {
1892
2202
  attrs.axis = 'axisRow';
1893
2203
  attrs.showAll = '0';
2204
+ if (assignment?.sortOrder) {
2205
+ attrs.sortType = 'ascending';
2206
+ attrs.sortOrder = assignment.sortOrder === 'asc' ? 'ascending' : 'descending';
2207
+ }
1894
2208
  // Add items for shared values
1895
2209
  const cacheField = this._cache.fields[fieldIndex];
1896
2210
  if (cacheField && cacheField.sharedItems.length > 0) {
1897
2211
  const itemNodes = [];
2212
+ const allowedIndexes = this._resolveItemFilter(cacheField.sharedItems, assignment?.filter);
1898
2213
  for(let i = 0; i < cacheField.sharedItems.length; i++){
1899
- itemNodes.push(createElement('item', {
2214
+ const shouldInclude = allowedIndexes.has(i);
2215
+ const itemAttrs = {
1900
2216
  x: String(i)
1901
- }, []));
2217
+ };
2218
+ if (!shouldInclude) {
2219
+ itemAttrs.h = '1';
2220
+ }
2221
+ itemNodes.push(createElement('item', itemAttrs, []));
1902
2222
  }
1903
2223
  // Add default subtotal item
1904
2224
  itemNodes.push(createElement('item', {
@@ -1911,13 +2231,23 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
1911
2231
  } else if (colField) {
1912
2232
  attrs.axis = 'axisCol';
1913
2233
  attrs.showAll = '0';
2234
+ if (assignment?.sortOrder) {
2235
+ attrs.sortType = 'ascending';
2236
+ attrs.sortOrder = assignment.sortOrder === 'asc' ? 'ascending' : 'descending';
2237
+ }
1914
2238
  const cacheField = this._cache.fields[fieldIndex];
1915
2239
  if (cacheField && cacheField.sharedItems.length > 0) {
1916
2240
  const itemNodes = [];
2241
+ const allowedIndexes = this._resolveItemFilter(cacheField.sharedItems, assignment?.filter);
1917
2242
  for(let i = 0; i < cacheField.sharedItems.length; i++){
1918
- itemNodes.push(createElement('item', {
2243
+ const shouldInclude = allowedIndexes.has(i);
2244
+ const itemAttrs = {
1919
2245
  x: String(i)
1920
- }, []));
2246
+ };
2247
+ if (!shouldInclude) {
2248
+ itemAttrs.h = '1';
2249
+ }
2250
+ itemNodes.push(createElement('item', itemAttrs, []));
1921
2251
  }
1922
2252
  itemNodes.push(createElement('item', {
1923
2253
  t: 'default'
@@ -1932,10 +2262,16 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
1932
2262
  const cacheField = this._cache.fields[fieldIndex];
1933
2263
  if (cacheField && cacheField.sharedItems.length > 0) {
1934
2264
  const itemNodes = [];
2265
+ const allowedIndexes = this._resolveItemFilter(cacheField.sharedItems, assignment?.filter);
1935
2266
  for(let i = 0; i < cacheField.sharedItems.length; i++){
1936
- itemNodes.push(createElement('item', {
2267
+ const shouldInclude = allowedIndexes.has(i);
2268
+ const itemAttrs = {
1937
2269
  x: String(i)
1938
- }, []));
2270
+ };
2271
+ if (!shouldInclude) {
2272
+ itemAttrs.h = '1';
2273
+ }
2274
+ itemNodes.push(createElement('item', itemAttrs, []));
1939
2275
  }
1940
2276
  itemNodes.push(createElement('item', {
1941
2277
  t: 'default'
@@ -1952,6 +2288,31 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
1952
2288
  }
1953
2289
  return createElement('pivotField', attrs, children);
1954
2290
  }
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
+ }
1955
2316
  /**
1956
2317
  * Build row items based on unique values in row fields
1957
2318
  */ _buildRowItems() {
@@ -2102,6 +2463,7 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
2102
2463
  this._records = [];
2103
2464
  this._recordCount = 0;
2104
2465
  this._refreshOnLoad = true; // Default to true
2466
+ this._dateGrouping = false;
2105
2467
  this._cacheId = cacheId;
2106
2468
  this._sourceSheet = sourceSheet;
2107
2469
  this._sourceRange = sourceRange;
@@ -2190,6 +2552,8 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
2190
2552
  }
2191
2553
  }
2192
2554
  }
2555
+ // Enable date grouping flag if any date field exists
2556
+ this._dateGrouping = this._fields.some((field)=>field.isDate);
2193
2557
  // Store records
2194
2558
  this._records = data;
2195
2559
  }
@@ -2218,6 +2582,8 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
2218
2582
  v: item
2219
2583
  }, []));
2220
2584
  }
2585
+ } else if (field.isDate) {
2586
+ sharedItemsAttrs.containsDate = '1';
2221
2587
  } else if (field.isNumeric) {
2222
2588
  // Numeric field - use "0"/"1" for boolean attributes as Excel expects
2223
2589
  sharedItemsAttrs.containsSemiMixedTypes = '0';
@@ -2248,9 +2614,13 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
2248
2614
  ref: this._sourceRange,
2249
2615
  sheet: this._sourceSheet
2250
2616
  }, []);
2251
- const cacheSourceNode = createElement('cacheSource', {
2617
+ const cacheSourceAttrs = {
2252
2618
  type: 'worksheet'
2253
- }, [
2619
+ };
2620
+ if (this._dateGrouping) {
2621
+ cacheSourceAttrs.grouping = '1';
2622
+ }
2623
+ const cacheSourceNode = createElement('cacheSource', cacheSourceAttrs, [
2254
2624
  worksheetSourceNode
2255
2625
  ]);
2256
2626
  // Build attributes - refreshOnLoad should come early per OOXML schema
@@ -2396,6 +2766,8 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
2396
2766
  this._pivotTables = [];
2397
2767
  this._pivotCaches = [];
2398
2768
  this._nextCacheId = 0;
2769
+ // Date serialization handling
2770
+ this._dateHandling = 'jsDate';
2399
2771
  this._sharedStrings = new SharedStrings();
2400
2772
  this._styles = Styles.createDefault();
2401
2773
  }
@@ -2460,6 +2832,16 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
2460
2832
  return this._styles;
2461
2833
  }
2462
2834
  /**
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
+ /**
2463
2845
  * Get a worksheet by name or index
2464
2846
  */ sheet(nameOrIndex) {
2465
2847
  let def;
@@ -3147,6 +3529,10 @@ exports.SharedStrings = SharedStrings;
3147
3529
  exports.Styles = Styles;
3148
3530
  exports.Workbook = Workbook;
3149
3531
  exports.Worksheet = Worksheet;
3532
+ exports.colToLetter = colToLetter;
3533
+ exports.isInRange = isInRange;
3534
+ exports.letterToCol = letterToCol;
3535
+ exports.normalizeRange = normalizeRange;
3150
3536
  exports.parseAddress = parseAddress;
3151
3537
  exports.parseRange = parseRange;
3152
3538
  exports.toAddress = toAddress;