@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.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,182 @@ 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
+ /**
958
+ * Convert sheet data to an array of JSON objects.
959
+ *
960
+ * @param config - Configuration options
961
+ * @returns Array of objects where keys are field names and values are cell values
962
+ *
963
+ * @example
964
+ * ```typescript
965
+ * // Using first row as headers
966
+ * const data = sheet.toJson();
967
+ *
968
+ * // Using custom field names
969
+ * const data = sheet.toJson({ fields: ['name', 'age', 'city'] });
970
+ *
971
+ * // Starting from a specific row/column
972
+ * const data = sheet.toJson({ startRow: 2, startCol: 1 });
973
+ * ```
974
+ */ toJson(config = {}) {
975
+ const { fields, startRow = 0, startCol = 0, endRow, endCol, stopOnEmptyRow = true, dateHandling = this._workbook.dateHandling } = config;
976
+ // Get the bounds of data in the sheet
977
+ const bounds = this._getDataBounds();
978
+ if (!bounds) {
979
+ return [];
980
+ }
981
+ const effectiveEndRow = endRow ?? bounds.maxRow;
982
+ const effectiveEndCol = endCol ?? bounds.maxCol;
983
+ // Determine field names
984
+ let fieldNames;
985
+ let dataStartRow;
986
+ if (fields) {
987
+ // Use provided field names, data starts at startRow
988
+ fieldNames = fields;
989
+ dataStartRow = startRow;
990
+ } else {
991
+ // Use first row as headers
992
+ fieldNames = [];
993
+ for(let col = startCol; col <= effectiveEndCol; col++){
994
+ const cell = this._cells.get(toAddress(startRow, col));
995
+ const value = cell?.value;
996
+ fieldNames.push(value != null ? String(value) : `column${col}`);
997
+ }
998
+ dataStartRow = startRow + 1;
999
+ }
1000
+ // Read data rows
1001
+ const result = [];
1002
+ for(let row = dataStartRow; row <= effectiveEndRow; row++){
1003
+ const obj = {};
1004
+ let hasData = false;
1005
+ for(let colOffset = 0; colOffset < fieldNames.length; colOffset++){
1006
+ const col = startCol + colOffset;
1007
+ 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
+ }
1012
+ if (value !== null) {
1013
+ hasData = true;
1014
+ }
1015
+ const fieldName = fieldNames[colOffset];
1016
+ if (fieldName) {
1017
+ obj[fieldName] = value;
1018
+ }
1019
+ }
1020
+ // Stop on empty row if configured
1021
+ if (stopOnEmptyRow && !hasData) {
1022
+ break;
1023
+ }
1024
+ result.push(obj);
1025
+ }
1026
+ return result;
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
+ }
1037
+ /**
1038
+ * Get the bounds of data in the sheet (min/max row and column with data)
1039
+ */ _getDataBounds() {
1040
+ if (!this._boundsDirty && this._dataBoundsCache) {
1041
+ return this._dataBoundsCache;
1042
+ }
1043
+ if (this._cells.size === 0) {
1044
+ this._dataBoundsCache = null;
1045
+ this._boundsDirty = false;
1046
+ return null;
1047
+ }
1048
+ let minRow = Infinity;
1049
+ let maxRow = -Infinity;
1050
+ let minCol = Infinity;
1051
+ let maxCol = -Infinity;
1052
+ for (const cell of this._cells.values()){
1053
+ if (cell.value !== null) {
1054
+ minRow = Math.min(minRow, cell.row);
1055
+ maxRow = Math.max(maxRow, cell.row);
1056
+ minCol = Math.min(minCol, cell.col);
1057
+ maxCol = Math.max(maxCol, cell.col);
1058
+ }
1059
+ }
1060
+ if (minRow === Infinity) {
1061
+ this._dataBoundsCache = null;
1062
+ this._boundsDirty = false;
1063
+ return null;
1064
+ }
1065
+ this._dataBoundsCache = {
1066
+ minRow,
1067
+ maxRow,
1068
+ minCol,
1069
+ maxCol
1070
+ };
1071
+ this._boundsDirty = false;
1072
+ return this._dataBoundsCache;
1073
+ }
1074
+ /**
824
1075
  * Generate XML for this worksheet
825
1076
  */ toXml() {
826
1077
  // Build sheetData from cells
@@ -832,6 +1083,11 @@ const builder = new XMLBuilder(builderOptions);
832
1083
  }
833
1084
  rowMap.get(row).push(cell);
834
1085
  }
1086
+ for (const rowIdx of this._rowHeights.keys()){
1087
+ if (!rowMap.has(rowIdx)) {
1088
+ rowMap.set(rowIdx, []);
1089
+ }
1090
+ }
835
1091
  // Sort rows and cells
836
1092
  const sortedRows = Array.from(rowMap.entries()).sort((a, b)=>a[0] - b[0]);
837
1093
  const rowNodes = [];
@@ -842,16 +1098,71 @@ const builder = new XMLBuilder(builderOptions);
842
1098
  const cellNode = this._buildCellNode(cell);
843
1099
  cellNodes.push(cellNode);
844
1100
  }
845
- const rowNode = createElement('row', {
1101
+ const rowAttrs = {
846
1102
  r: String(rowIdx + 1)
847
- }, 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);
848
1110
  rowNodes.push(rowNode);
849
1111
  }
850
1112
  const sheetDataNode = createElement('sheetData', {}, rowNodes);
851
1113
  // Build worksheet structure
852
- const worksheetChildren = [
853
- sheetDataNode
854
- ];
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);
855
1166
  // Add merged cells if any
856
1167
  if (this._mergedCells.size > 0) {
857
1168
  const mergeCellNodes = [];
@@ -1038,6 +1349,40 @@ const builder = new XMLBuilder(builderOptions);
1038
1349
  * Manages the styles (xl/styles.xml)
1039
1350
  */ class Styles {
1040
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
+ /**
1041
1386
  * Parse styles from XML content
1042
1387
  */ static parse(xml) {
1043
1388
  const styles = new Styles();
@@ -1204,6 +1549,10 @@ const builder = new XMLBuilder(builderOptions);
1204
1549
  /**
1205
1550
  * Get a style by index
1206
1551
  */ getStyle(index) {
1552
+ const cached = this._styleObjectCache.get(index);
1553
+ if (cached) return {
1554
+ ...cached
1555
+ };
1207
1556
  const xf = this._cellXfs[index];
1208
1557
  if (!xf) return {};
1209
1558
  const font = this._fonts[xf.fontId];
@@ -1244,13 +1593,16 @@ const builder = new XMLBuilder(builderOptions);
1244
1593
  textRotation: xf.alignment.textRotation
1245
1594
  };
1246
1595
  }
1596
+ this._styleObjectCache.set(index, {
1597
+ ...style
1598
+ });
1247
1599
  return style;
1248
1600
  }
1249
1601
  /**
1250
1602
  * Create a style and return its index
1251
1603
  * Uses caching to deduplicate identical styles
1252
1604
  */ createStyle(style) {
1253
- const key = JSON.stringify(style);
1605
+ const key = this._getStyleKey(style);
1254
1606
  const cached = this._styleCache.get(key);
1255
1607
  if (cached !== undefined) {
1256
1608
  return cached;
@@ -1282,8 +1634,20 @@ const builder = new XMLBuilder(builderOptions);
1282
1634
  const index = this._cellXfs.length;
1283
1635
  this._cellXfs.push(xf);
1284
1636
  this._styleCache.set(key, index);
1637
+ this._styleObjectCache.set(index, {
1638
+ ...style
1639
+ });
1285
1640
  return index;
1286
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
+ }
1287
1651
  _findOrCreateFont(style) {
1288
1652
  const font = {
1289
1653
  bold: style.bold || false,
@@ -1519,6 +1883,7 @@ const builder = new XMLBuilder(builderOptions);
1519
1883
  this._dirty = false;
1520
1884
  // Cache for style deduplication
1521
1885
  this._styleCache = new Map();
1886
+ this._styleObjectCache = new Map();
1522
1887
  }
1523
1888
  }
1524
1889
 
@@ -1530,6 +1895,7 @@ const builder = new XMLBuilder(builderOptions);
1530
1895
  this._columnFields = [];
1531
1896
  this._valueFields = [];
1532
1897
  this._filterFields = [];
1898
+ this._fieldAssignments = new Map();
1533
1899
  this._name = name;
1534
1900
  this._cache = cache;
1535
1901
  this._targetSheet = targetSheet;
@@ -1571,11 +1937,13 @@ const builder = new XMLBuilder(builderOptions);
1571
1937
  if (fieldIndex < 0) {
1572
1938
  throw new Error(`Field not found in source data: ${fieldName}`);
1573
1939
  }
1574
- this._rowFields.push({
1940
+ const assignment = {
1575
1941
  fieldName,
1576
1942
  fieldIndex,
1577
1943
  axis: 'row'
1578
- });
1944
+ };
1945
+ this._rowFields.push(assignment);
1946
+ this._fieldAssignments.set(fieldIndex, assignment);
1579
1947
  return this;
1580
1948
  }
1581
1949
  /**
@@ -1586,11 +1954,13 @@ const builder = new XMLBuilder(builderOptions);
1586
1954
  if (fieldIndex < 0) {
1587
1955
  throw new Error(`Field not found in source data: ${fieldName}`);
1588
1956
  }
1589
- this._columnFields.push({
1957
+ const assignment = {
1590
1958
  fieldName,
1591
1959
  fieldIndex,
1592
1960
  axis: 'column'
1593
- });
1961
+ };
1962
+ this._columnFields.push(assignment);
1963
+ this._fieldAssignments.set(fieldIndex, assignment);
1594
1964
  return this;
1595
1965
  }
1596
1966
  /**
@@ -1604,13 +1974,15 @@ const builder = new XMLBuilder(builderOptions);
1604
1974
  throw new Error(`Field not found in source data: ${fieldName}`);
1605
1975
  }
1606
1976
  const defaultName = `${aggregation.charAt(0).toUpperCase() + aggregation.slice(1)} of ${fieldName}`;
1607
- this._valueFields.push({
1977
+ const assignment = {
1608
1978
  fieldName,
1609
1979
  fieldIndex,
1610
1980
  axis: 'value',
1611
1981
  aggregation,
1612
1982
  displayName: displayName || defaultName
1613
- });
1983
+ };
1984
+ this._valueFields.push(assignment);
1985
+ this._fieldAssignments.set(fieldIndex, assignment);
1614
1986
  return this;
1615
1987
  }
1616
1988
  /**
@@ -1621,11 +1993,44 @@ const builder = new XMLBuilder(builderOptions);
1621
1993
  if (fieldIndex < 0) {
1622
1994
  throw new Error(`Field not found in source data: ${fieldName}`);
1623
1995
  }
1624
- this._filterFields.push({
1996
+ const assignment = {
1625
1997
  fieldName,
1626
1998
  fieldIndex,
1627
1999
  axis: 'filter'
1628
- });
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;
1629
2034
  return this;
1630
2035
  }
1631
2036
  /**
@@ -1790,17 +2195,28 @@ const builder = new XMLBuilder(builderOptions);
1790
2195
  const colField = this._columnFields.find((f)=>f.fieldIndex === fieldIndex);
1791
2196
  const filterField = this._filterFields.find((f)=>f.fieldIndex === fieldIndex);
1792
2197
  const valueField = this._valueFields.find((f)=>f.fieldIndex === fieldIndex);
2198
+ const assignment = this._fieldAssignments.get(fieldIndex);
1793
2199
  if (rowField) {
1794
2200
  attrs.axis = 'axisRow';
1795
2201
  attrs.showAll = '0';
2202
+ if (assignment?.sortOrder) {
2203
+ attrs.sortType = 'ascending';
2204
+ attrs.sortOrder = assignment.sortOrder === 'asc' ? 'ascending' : 'descending';
2205
+ }
1796
2206
  // Add items for shared values
1797
2207
  const cacheField = this._cache.fields[fieldIndex];
1798
2208
  if (cacheField && cacheField.sharedItems.length > 0) {
1799
2209
  const itemNodes = [];
2210
+ const allowedIndexes = this._resolveItemFilter(cacheField.sharedItems, assignment?.filter);
1800
2211
  for(let i = 0; i < cacheField.sharedItems.length; i++){
1801
- itemNodes.push(createElement('item', {
2212
+ const shouldInclude = allowedIndexes.has(i);
2213
+ const itemAttrs = {
1802
2214
  x: String(i)
1803
- }, []));
2215
+ };
2216
+ if (!shouldInclude) {
2217
+ itemAttrs.h = '1';
2218
+ }
2219
+ itemNodes.push(createElement('item', itemAttrs, []));
1804
2220
  }
1805
2221
  // Add default subtotal item
1806
2222
  itemNodes.push(createElement('item', {
@@ -1813,13 +2229,23 @@ const builder = new XMLBuilder(builderOptions);
1813
2229
  } else if (colField) {
1814
2230
  attrs.axis = 'axisCol';
1815
2231
  attrs.showAll = '0';
2232
+ if (assignment?.sortOrder) {
2233
+ attrs.sortType = 'ascending';
2234
+ attrs.sortOrder = assignment.sortOrder === 'asc' ? 'ascending' : 'descending';
2235
+ }
1816
2236
  const cacheField = this._cache.fields[fieldIndex];
1817
2237
  if (cacheField && cacheField.sharedItems.length > 0) {
1818
2238
  const itemNodes = [];
2239
+ const allowedIndexes = this._resolveItemFilter(cacheField.sharedItems, assignment?.filter);
1819
2240
  for(let i = 0; i < cacheField.sharedItems.length; i++){
1820
- itemNodes.push(createElement('item', {
2241
+ const shouldInclude = allowedIndexes.has(i);
2242
+ const itemAttrs = {
1821
2243
  x: String(i)
1822
- }, []));
2244
+ };
2245
+ if (!shouldInclude) {
2246
+ itemAttrs.h = '1';
2247
+ }
2248
+ itemNodes.push(createElement('item', itemAttrs, []));
1823
2249
  }
1824
2250
  itemNodes.push(createElement('item', {
1825
2251
  t: 'default'
@@ -1834,10 +2260,16 @@ const builder = new XMLBuilder(builderOptions);
1834
2260
  const cacheField = this._cache.fields[fieldIndex];
1835
2261
  if (cacheField && cacheField.sharedItems.length > 0) {
1836
2262
  const itemNodes = [];
2263
+ const allowedIndexes = this._resolveItemFilter(cacheField.sharedItems, assignment?.filter);
1837
2264
  for(let i = 0; i < cacheField.sharedItems.length; i++){
1838
- itemNodes.push(createElement('item', {
2265
+ const shouldInclude = allowedIndexes.has(i);
2266
+ const itemAttrs = {
1839
2267
  x: String(i)
1840
- }, []));
2268
+ };
2269
+ if (!shouldInclude) {
2270
+ itemAttrs.h = '1';
2271
+ }
2272
+ itemNodes.push(createElement('item', itemAttrs, []));
1841
2273
  }
1842
2274
  itemNodes.push(createElement('item', {
1843
2275
  t: 'default'
@@ -1854,6 +2286,31 @@ const builder = new XMLBuilder(builderOptions);
1854
2286
  }
1855
2287
  return createElement('pivotField', attrs, children);
1856
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
+ }
1857
2314
  /**
1858
2315
  * Build row items based on unique values in row fields
1859
2316
  */ _buildRowItems() {
@@ -2004,6 +2461,7 @@ const builder = new XMLBuilder(builderOptions);
2004
2461
  this._records = [];
2005
2462
  this._recordCount = 0;
2006
2463
  this._refreshOnLoad = true; // Default to true
2464
+ this._dateGrouping = false;
2007
2465
  this._cacheId = cacheId;
2008
2466
  this._sourceSheet = sourceSheet;
2009
2467
  this._sourceRange = sourceRange;
@@ -2092,6 +2550,8 @@ const builder = new XMLBuilder(builderOptions);
2092
2550
  }
2093
2551
  }
2094
2552
  }
2553
+ // Enable date grouping flag if any date field exists
2554
+ this._dateGrouping = this._fields.some((field)=>field.isDate);
2095
2555
  // Store records
2096
2556
  this._records = data;
2097
2557
  }
@@ -2120,6 +2580,8 @@ const builder = new XMLBuilder(builderOptions);
2120
2580
  v: item
2121
2581
  }, []));
2122
2582
  }
2583
+ } else if (field.isDate) {
2584
+ sharedItemsAttrs.containsDate = '1';
2123
2585
  } else if (field.isNumeric) {
2124
2586
  // Numeric field - use "0"/"1" for boolean attributes as Excel expects
2125
2587
  sharedItemsAttrs.containsSemiMixedTypes = '0';
@@ -2150,9 +2612,13 @@ const builder = new XMLBuilder(builderOptions);
2150
2612
  ref: this._sourceRange,
2151
2613
  sheet: this._sourceSheet
2152
2614
  }, []);
2153
- const cacheSourceNode = createElement('cacheSource', {
2615
+ const cacheSourceAttrs = {
2154
2616
  type: 'worksheet'
2155
- }, [
2617
+ };
2618
+ if (this._dateGrouping) {
2619
+ cacheSourceAttrs.grouping = '1';
2620
+ }
2621
+ const cacheSourceNode = createElement('cacheSource', cacheSourceAttrs, [
2156
2622
  worksheetSourceNode
2157
2623
  ]);
2158
2624
  // Build attributes - refreshOnLoad should come early per OOXML schema
@@ -2298,6 +2764,8 @@ const builder = new XMLBuilder(builderOptions);
2298
2764
  this._pivotTables = [];
2299
2765
  this._pivotCaches = [];
2300
2766
  this._nextCacheId = 0;
2767
+ // Date serialization handling
2768
+ this._dateHandling = 'jsDate';
2301
2769
  this._sharedStrings = new SharedStrings();
2302
2770
  this._styles = Styles.createDefault();
2303
2771
  }
@@ -2362,6 +2830,16 @@ const builder = new XMLBuilder(builderOptions);
2362
2830
  return this._styles;
2363
2831
  }
2364
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
+ /**
2365
2843
  * Get a worksheet by name or index
2366
2844
  */ sheet(nameOrIndex) {
2367
2845
  let def;
@@ -3041,4 +3519,4 @@ const builder = new XMLBuilder(builderOptions);
3041
3519
  }
3042
3520
  }
3043
3521
 
3044
- 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 };