@niicojs/excel 0.2.2 → 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,182 @@ 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
+ /**
960
+ * Convert sheet data to an array of JSON objects.
961
+ *
962
+ * @param config - Configuration options
963
+ * @returns Array of objects where keys are field names and values are cell values
964
+ *
965
+ * @example
966
+ * ```typescript
967
+ * // Using first row as headers
968
+ * const data = sheet.toJson();
969
+ *
970
+ * // Using custom field names
971
+ * const data = sheet.toJson({ fields: ['name', 'age', 'city'] });
972
+ *
973
+ * // Starting from a specific row/column
974
+ * const data = sheet.toJson({ startRow: 2, startCol: 1 });
975
+ * ```
976
+ */ toJson(config = {}) {
977
+ const { fields, startRow = 0, startCol = 0, endRow, endCol, stopOnEmptyRow = true, dateHandling = this._workbook.dateHandling } = config;
978
+ // Get the bounds of data in the sheet
979
+ const bounds = this._getDataBounds();
980
+ if (!bounds) {
981
+ return [];
982
+ }
983
+ const effectiveEndRow = endRow ?? bounds.maxRow;
984
+ const effectiveEndCol = endCol ?? bounds.maxCol;
985
+ // Determine field names
986
+ let fieldNames;
987
+ let dataStartRow;
988
+ if (fields) {
989
+ // Use provided field names, data starts at startRow
990
+ fieldNames = fields;
991
+ dataStartRow = startRow;
992
+ } else {
993
+ // Use first row as headers
994
+ fieldNames = [];
995
+ for(let col = startCol; col <= effectiveEndCol; col++){
996
+ const cell = this._cells.get(toAddress(startRow, col));
997
+ const value = cell?.value;
998
+ fieldNames.push(value != null ? String(value) : `column${col}`);
999
+ }
1000
+ dataStartRow = startRow + 1;
1001
+ }
1002
+ // Read data rows
1003
+ const result = [];
1004
+ for(let row = dataStartRow; row <= effectiveEndRow; row++){
1005
+ const obj = {};
1006
+ let hasData = false;
1007
+ for(let colOffset = 0; colOffset < fieldNames.length; colOffset++){
1008
+ const col = startCol + colOffset;
1009
+ const cell = this._cells.get(toAddress(row, col));
1010
+ let value = cell?.value ?? null;
1011
+ if (value instanceof Date) {
1012
+ value = this._serializeDate(value, dateHandling, cell);
1013
+ }
1014
+ if (value !== null) {
1015
+ hasData = true;
1016
+ }
1017
+ const fieldName = fieldNames[colOffset];
1018
+ if (fieldName) {
1019
+ obj[fieldName] = value;
1020
+ }
1021
+ }
1022
+ // Stop on empty row if configured
1023
+ if (stopOnEmptyRow && !hasData) {
1024
+ break;
1025
+ }
1026
+ result.push(obj);
1027
+ }
1028
+ return result;
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
+ }
1039
+ /**
1040
+ * Get the bounds of data in the sheet (min/max row and column with data)
1041
+ */ _getDataBounds() {
1042
+ if (!this._boundsDirty && this._dataBoundsCache) {
1043
+ return this._dataBoundsCache;
1044
+ }
1045
+ if (this._cells.size === 0) {
1046
+ this._dataBoundsCache = null;
1047
+ this._boundsDirty = false;
1048
+ return null;
1049
+ }
1050
+ let minRow = Infinity;
1051
+ let maxRow = -Infinity;
1052
+ let minCol = Infinity;
1053
+ let maxCol = -Infinity;
1054
+ for (const cell of this._cells.values()){
1055
+ if (cell.value !== null) {
1056
+ minRow = Math.min(minRow, cell.row);
1057
+ maxRow = Math.max(maxRow, cell.row);
1058
+ minCol = Math.min(minCol, cell.col);
1059
+ maxCol = Math.max(maxCol, cell.col);
1060
+ }
1061
+ }
1062
+ if (minRow === Infinity) {
1063
+ this._dataBoundsCache = null;
1064
+ this._boundsDirty = false;
1065
+ return null;
1066
+ }
1067
+ this._dataBoundsCache = {
1068
+ minRow,
1069
+ maxRow,
1070
+ minCol,
1071
+ maxCol
1072
+ };
1073
+ this._boundsDirty = false;
1074
+ return this._dataBoundsCache;
1075
+ }
1076
+ /**
826
1077
  * Generate XML for this worksheet
827
1078
  */ toXml() {
828
1079
  // Build sheetData from cells
@@ -834,6 +1085,11 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
834
1085
  }
835
1086
  rowMap.get(row).push(cell);
836
1087
  }
1088
+ for (const rowIdx of this._rowHeights.keys()){
1089
+ if (!rowMap.has(rowIdx)) {
1090
+ rowMap.set(rowIdx, []);
1091
+ }
1092
+ }
837
1093
  // Sort rows and cells
838
1094
  const sortedRows = Array.from(rowMap.entries()).sort((a, b)=>a[0] - b[0]);
839
1095
  const rowNodes = [];
@@ -844,16 +1100,71 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
844
1100
  const cellNode = this._buildCellNode(cell);
845
1101
  cellNodes.push(cellNode);
846
1102
  }
847
- const rowNode = createElement('row', {
1103
+ const rowAttrs = {
848
1104
  r: String(rowIdx + 1)
849
- }, 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);
850
1112
  rowNodes.push(rowNode);
851
1113
  }
852
1114
  const sheetDataNode = createElement('sheetData', {}, rowNodes);
853
1115
  // Build worksheet structure
854
- const worksheetChildren = [
855
- sheetDataNode
856
- ];
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);
857
1168
  // Add merged cells if any
858
1169
  if (this._mergedCells.size > 0) {
859
1170
  const mergeCellNodes = [];
@@ -1040,6 +1351,40 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
1040
1351
  * Manages the styles (xl/styles.xml)
1041
1352
  */ class Styles {
1042
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
+ /**
1043
1388
  * Parse styles from XML content
1044
1389
  */ static parse(xml) {
1045
1390
  const styles = new Styles();
@@ -1206,6 +1551,10 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
1206
1551
  /**
1207
1552
  * Get a style by index
1208
1553
  */ getStyle(index) {
1554
+ const cached = this._styleObjectCache.get(index);
1555
+ if (cached) return {
1556
+ ...cached
1557
+ };
1209
1558
  const xf = this._cellXfs[index];
1210
1559
  if (!xf) return {};
1211
1560
  const font = this._fonts[xf.fontId];
@@ -1246,13 +1595,16 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
1246
1595
  textRotation: xf.alignment.textRotation
1247
1596
  };
1248
1597
  }
1598
+ this._styleObjectCache.set(index, {
1599
+ ...style
1600
+ });
1249
1601
  return style;
1250
1602
  }
1251
1603
  /**
1252
1604
  * Create a style and return its index
1253
1605
  * Uses caching to deduplicate identical styles
1254
1606
  */ createStyle(style) {
1255
- const key = JSON.stringify(style);
1607
+ const key = this._getStyleKey(style);
1256
1608
  const cached = this._styleCache.get(key);
1257
1609
  if (cached !== undefined) {
1258
1610
  return cached;
@@ -1284,8 +1636,20 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
1284
1636
  const index = this._cellXfs.length;
1285
1637
  this._cellXfs.push(xf);
1286
1638
  this._styleCache.set(key, index);
1639
+ this._styleObjectCache.set(index, {
1640
+ ...style
1641
+ });
1287
1642
  return index;
1288
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
+ }
1289
1653
  _findOrCreateFont(style) {
1290
1654
  const font = {
1291
1655
  bold: style.bold || false,
@@ -1521,6 +1885,7 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
1521
1885
  this._dirty = false;
1522
1886
  // Cache for style deduplication
1523
1887
  this._styleCache = new Map();
1888
+ this._styleObjectCache = new Map();
1524
1889
  }
1525
1890
  }
1526
1891
 
@@ -1532,6 +1897,7 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
1532
1897
  this._columnFields = [];
1533
1898
  this._valueFields = [];
1534
1899
  this._filterFields = [];
1900
+ this._fieldAssignments = new Map();
1535
1901
  this._name = name;
1536
1902
  this._cache = cache;
1537
1903
  this._targetSheet = targetSheet;
@@ -1573,11 +1939,13 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
1573
1939
  if (fieldIndex < 0) {
1574
1940
  throw new Error(`Field not found in source data: ${fieldName}`);
1575
1941
  }
1576
- this._rowFields.push({
1942
+ const assignment = {
1577
1943
  fieldName,
1578
1944
  fieldIndex,
1579
1945
  axis: 'row'
1580
- });
1946
+ };
1947
+ this._rowFields.push(assignment);
1948
+ this._fieldAssignments.set(fieldIndex, assignment);
1581
1949
  return this;
1582
1950
  }
1583
1951
  /**
@@ -1588,11 +1956,13 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
1588
1956
  if (fieldIndex < 0) {
1589
1957
  throw new Error(`Field not found in source data: ${fieldName}`);
1590
1958
  }
1591
- this._columnFields.push({
1959
+ const assignment = {
1592
1960
  fieldName,
1593
1961
  fieldIndex,
1594
1962
  axis: 'column'
1595
- });
1963
+ };
1964
+ this._columnFields.push(assignment);
1965
+ this._fieldAssignments.set(fieldIndex, assignment);
1596
1966
  return this;
1597
1967
  }
1598
1968
  /**
@@ -1606,13 +1976,15 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
1606
1976
  throw new Error(`Field not found in source data: ${fieldName}`);
1607
1977
  }
1608
1978
  const defaultName = `${aggregation.charAt(0).toUpperCase() + aggregation.slice(1)} of ${fieldName}`;
1609
- this._valueFields.push({
1979
+ const assignment = {
1610
1980
  fieldName,
1611
1981
  fieldIndex,
1612
1982
  axis: 'value',
1613
1983
  aggregation,
1614
1984
  displayName: displayName || defaultName
1615
- });
1985
+ };
1986
+ this._valueFields.push(assignment);
1987
+ this._fieldAssignments.set(fieldIndex, assignment);
1616
1988
  return this;
1617
1989
  }
1618
1990
  /**
@@ -1623,11 +1995,44 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
1623
1995
  if (fieldIndex < 0) {
1624
1996
  throw new Error(`Field not found in source data: ${fieldName}`);
1625
1997
  }
1626
- this._filterFields.push({
1998
+ const assignment = {
1627
1999
  fieldName,
1628
2000
  fieldIndex,
1629
2001
  axis: 'filter'
1630
- });
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;
1631
2036
  return this;
1632
2037
  }
1633
2038
  /**
@@ -1792,17 +2197,28 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
1792
2197
  const colField = this._columnFields.find((f)=>f.fieldIndex === fieldIndex);
1793
2198
  const filterField = this._filterFields.find((f)=>f.fieldIndex === fieldIndex);
1794
2199
  const valueField = this._valueFields.find((f)=>f.fieldIndex === fieldIndex);
2200
+ const assignment = this._fieldAssignments.get(fieldIndex);
1795
2201
  if (rowField) {
1796
2202
  attrs.axis = 'axisRow';
1797
2203
  attrs.showAll = '0';
2204
+ if (assignment?.sortOrder) {
2205
+ attrs.sortType = 'ascending';
2206
+ attrs.sortOrder = assignment.sortOrder === 'asc' ? 'ascending' : 'descending';
2207
+ }
1798
2208
  // Add items for shared values
1799
2209
  const cacheField = this._cache.fields[fieldIndex];
1800
2210
  if (cacheField && cacheField.sharedItems.length > 0) {
1801
2211
  const itemNodes = [];
2212
+ const allowedIndexes = this._resolveItemFilter(cacheField.sharedItems, assignment?.filter);
1802
2213
  for(let i = 0; i < cacheField.sharedItems.length; i++){
1803
- itemNodes.push(createElement('item', {
2214
+ const shouldInclude = allowedIndexes.has(i);
2215
+ const itemAttrs = {
1804
2216
  x: String(i)
1805
- }, []));
2217
+ };
2218
+ if (!shouldInclude) {
2219
+ itemAttrs.h = '1';
2220
+ }
2221
+ itemNodes.push(createElement('item', itemAttrs, []));
1806
2222
  }
1807
2223
  // Add default subtotal item
1808
2224
  itemNodes.push(createElement('item', {
@@ -1815,13 +2231,23 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
1815
2231
  } else if (colField) {
1816
2232
  attrs.axis = 'axisCol';
1817
2233
  attrs.showAll = '0';
2234
+ if (assignment?.sortOrder) {
2235
+ attrs.sortType = 'ascending';
2236
+ attrs.sortOrder = assignment.sortOrder === 'asc' ? 'ascending' : 'descending';
2237
+ }
1818
2238
  const cacheField = this._cache.fields[fieldIndex];
1819
2239
  if (cacheField && cacheField.sharedItems.length > 0) {
1820
2240
  const itemNodes = [];
2241
+ const allowedIndexes = this._resolveItemFilter(cacheField.sharedItems, assignment?.filter);
1821
2242
  for(let i = 0; i < cacheField.sharedItems.length; i++){
1822
- itemNodes.push(createElement('item', {
2243
+ const shouldInclude = allowedIndexes.has(i);
2244
+ const itemAttrs = {
1823
2245
  x: String(i)
1824
- }, []));
2246
+ };
2247
+ if (!shouldInclude) {
2248
+ itemAttrs.h = '1';
2249
+ }
2250
+ itemNodes.push(createElement('item', itemAttrs, []));
1825
2251
  }
1826
2252
  itemNodes.push(createElement('item', {
1827
2253
  t: 'default'
@@ -1836,10 +2262,16 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
1836
2262
  const cacheField = this._cache.fields[fieldIndex];
1837
2263
  if (cacheField && cacheField.sharedItems.length > 0) {
1838
2264
  const itemNodes = [];
2265
+ const allowedIndexes = this._resolveItemFilter(cacheField.sharedItems, assignment?.filter);
1839
2266
  for(let i = 0; i < cacheField.sharedItems.length; i++){
1840
- itemNodes.push(createElement('item', {
2267
+ const shouldInclude = allowedIndexes.has(i);
2268
+ const itemAttrs = {
1841
2269
  x: String(i)
1842
- }, []));
2270
+ };
2271
+ if (!shouldInclude) {
2272
+ itemAttrs.h = '1';
2273
+ }
2274
+ itemNodes.push(createElement('item', itemAttrs, []));
1843
2275
  }
1844
2276
  itemNodes.push(createElement('item', {
1845
2277
  t: 'default'
@@ -1856,6 +2288,31 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
1856
2288
  }
1857
2289
  return createElement('pivotField', attrs, children);
1858
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
+ }
1859
2316
  /**
1860
2317
  * Build row items based on unique values in row fields
1861
2318
  */ _buildRowItems() {
@@ -2006,6 +2463,7 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
2006
2463
  this._records = [];
2007
2464
  this._recordCount = 0;
2008
2465
  this._refreshOnLoad = true; // Default to true
2466
+ this._dateGrouping = false;
2009
2467
  this._cacheId = cacheId;
2010
2468
  this._sourceSheet = sourceSheet;
2011
2469
  this._sourceRange = sourceRange;
@@ -2094,6 +2552,8 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
2094
2552
  }
2095
2553
  }
2096
2554
  }
2555
+ // Enable date grouping flag if any date field exists
2556
+ this._dateGrouping = this._fields.some((field)=>field.isDate);
2097
2557
  // Store records
2098
2558
  this._records = data;
2099
2559
  }
@@ -2122,6 +2582,8 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
2122
2582
  v: item
2123
2583
  }, []));
2124
2584
  }
2585
+ } else if (field.isDate) {
2586
+ sharedItemsAttrs.containsDate = '1';
2125
2587
  } else if (field.isNumeric) {
2126
2588
  // Numeric field - use "0"/"1" for boolean attributes as Excel expects
2127
2589
  sharedItemsAttrs.containsSemiMixedTypes = '0';
@@ -2152,9 +2614,13 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
2152
2614
  ref: this._sourceRange,
2153
2615
  sheet: this._sourceSheet
2154
2616
  }, []);
2155
- const cacheSourceNode = createElement('cacheSource', {
2617
+ const cacheSourceAttrs = {
2156
2618
  type: 'worksheet'
2157
- }, [
2619
+ };
2620
+ if (this._dateGrouping) {
2621
+ cacheSourceAttrs.grouping = '1';
2622
+ }
2623
+ const cacheSourceNode = createElement('cacheSource', cacheSourceAttrs, [
2158
2624
  worksheetSourceNode
2159
2625
  ]);
2160
2626
  // Build attributes - refreshOnLoad should come early per OOXML schema
@@ -2300,6 +2766,8 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
2300
2766
  this._pivotTables = [];
2301
2767
  this._pivotCaches = [];
2302
2768
  this._nextCacheId = 0;
2769
+ // Date serialization handling
2770
+ this._dateHandling = 'jsDate';
2303
2771
  this._sharedStrings = new SharedStrings();
2304
2772
  this._styles = Styles.createDefault();
2305
2773
  }
@@ -2364,6 +2832,16 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
2364
2832
  return this._styles;
2365
2833
  }
2366
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
+ /**
2367
2845
  * Get a worksheet by name or index
2368
2846
  */ sheet(nameOrIndex) {
2369
2847
  let def;
@@ -3051,6 +3529,10 @@ exports.SharedStrings = SharedStrings;
3051
3529
  exports.Styles = Styles;
3052
3530
  exports.Workbook = Workbook;
3053
3531
  exports.Worksheet = Worksheet;
3532
+ exports.colToLetter = colToLetter;
3533
+ exports.isInRange = isInRange;
3534
+ exports.letterToCol = letterToCol;
3535
+ exports.normalizeRange = normalizeRange;
3054
3536
  exports.parseAddress = parseAddress;
3055
3537
  exports.parseRange = parseRange;
3056
3538
  exports.toAddress = toAddress;