@niicojs/excel 0.2.7 → 0.3.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@niicojs/excel",
3
- "version": "0.2.7",
3
+ "version": "0.3.0",
4
4
  "description": "typescript library to manipulate excel files",
5
5
  "homepage": "https://github.com/niicojs/excel#readme",
6
6
  "bugs": {
package/src/index.ts CHANGED
@@ -7,6 +7,8 @@ export { SharedStrings } from './shared-strings';
7
7
  export { Styles } from './styles';
8
8
  export { PivotTable } from './pivot-table';
9
9
  export { PivotCache } from './pivot-cache';
10
+ export { Table } from './table';
11
+ export { parseAddress, toAddress, parseRange, toRange } from './utils/address';
10
12
 
11
13
  // Type exports
12
14
  export type {
@@ -20,11 +22,18 @@ export type {
20
22
  BorderStyle,
21
23
  BorderType,
22
24
  Alignment,
25
+ DateHandling,
23
26
  // Pivot table types
24
27
  PivotTableConfig,
25
28
  PivotValueConfig,
26
29
  AggregationType,
27
30
  PivotFieldAxis,
31
+ PivotSortOrder,
32
+ PivotFieldFilter,
33
+ // Table types
34
+ TableConfig,
35
+ TableStyleConfig,
36
+ TableTotalFunction,
28
37
  // Sheet from data types
29
38
  SheetFromDataConfig,
30
39
  ColumnConfig,
@@ -34,4 +43,3 @@ export type {
34
43
  } from './types';
35
44
 
36
45
  // Utility exports
37
- export { parseAddress, toAddress, parseRange, toRange } from './utils/address';
@@ -7,6 +7,7 @@ import { createElement, stringifyXml, XmlNode } from './utils/xml';
7
7
  */
8
8
  export class PivotCache {
9
9
  private _cacheId: number;
10
+ private _fileIndex: number;
10
11
  private _sourceSheet: string;
11
12
  private _sourceRange: string;
12
13
  private _fields: PivotCacheField[] = [];
@@ -16,8 +17,9 @@ export class PivotCache {
16
17
  // Optimized lookup: Map<fieldIndex, Map<stringValue, sharedItemsIndex>>
17
18
  private _sharedItemsIndexMap: Map<number, Map<string, number>> = new Map();
18
19
 
19
- constructor(cacheId: number, sourceSheet: string, sourceRange: string) {
20
+ constructor(cacheId: number, sourceSheet: string, sourceRange: string, fileIndex: number) {
20
21
  this._cacheId = cacheId;
22
+ this._fileIndex = fileIndex;
21
23
  this._sourceSheet = sourceSheet;
22
24
  this._sourceRange = sourceRange;
23
25
  }
@@ -29,6 +31,13 @@ export class PivotCache {
29
31
  return this._cacheId;
30
32
  }
31
33
 
34
+ /**
35
+ * Get the file index for this cache (used for file naming).
36
+ */
37
+ get fileIndex(): number {
38
+ return this._fileIndex;
39
+ }
40
+
32
41
  /**
33
42
  * Set refreshOnLoad option
34
43
  */
@@ -1,4 +1,4 @@
1
- import type { AggregationType, PivotFieldAxis, PivotValueConfig } from './types';
1
+ import type { AggregationType, PivotFieldAxis, PivotFieldFilter, PivotSortOrder, PivotValueConfig } from './types';
2
2
  import type { Styles } from './styles';
3
3
  import { PivotCache } from './pivot-cache';
4
4
  import { createElement, stringifyXml, XmlNode } from './utils/xml';
@@ -13,6 +13,8 @@ interface FieldAssignment {
13
13
  aggregation?: AggregationType;
14
14
  displayName?: string;
15
15
  numFmtId?: number;
16
+ sortOrder?: PivotSortOrder;
17
+ filter?: PivotFieldFilter;
16
18
  }
17
19
 
18
20
  /**
@@ -30,8 +32,10 @@ export class PivotTable {
30
32
  private _columnFields: FieldAssignment[] = [];
31
33
  private _valueFields: FieldAssignment[] = [];
32
34
  private _filterFields: FieldAssignment[] = [];
35
+ private _fieldAssignments: Map<number, FieldAssignment> = new Map();
33
36
 
34
37
  private _pivotTableIndex: number;
38
+ private _cacheFileIndex: number;
35
39
  private _styles: Styles | null = null;
36
40
 
37
41
  constructor(
@@ -42,6 +46,7 @@ export class PivotTable {
42
46
  targetRow: number,
43
47
  targetCol: number,
44
48
  pivotTableIndex: number,
49
+ cacheFileIndex: number,
45
50
  ) {
46
51
  this._name = name;
47
52
  this._cache = cache;
@@ -50,6 +55,7 @@ export class PivotTable {
50
55
  this._targetRow = targetRow;
51
56
  this._targetCol = targetCol;
52
57
  this._pivotTableIndex = pivotTableIndex;
58
+ this._cacheFileIndex = cacheFileIndex;
53
59
  }
54
60
 
55
61
  /**
@@ -87,6 +93,14 @@ export class PivotTable {
87
93
  return this._pivotTableIndex;
88
94
  }
89
95
 
96
+ /**
97
+ * Get the pivot cache file index used for rels.
98
+ * @internal
99
+ */
100
+ get cacheFileIndex(): number {
101
+ return this._cacheFileIndex;
102
+ }
103
+
90
104
  /**
91
105
  * Set the styles reference for number format resolution
92
106
  * @internal
@@ -106,11 +120,13 @@ export class PivotTable {
106
120
  throw new Error(`Field not found in source data: ${fieldName}`);
107
121
  }
108
122
 
109
- this._rowFields.push({
123
+ const assignment: FieldAssignment = {
110
124
  fieldName,
111
125
  fieldIndex,
112
126
  axis: 'row',
113
- });
127
+ };
128
+ this._rowFields.push(assignment);
129
+ this._fieldAssignments.set(fieldIndex, assignment);
114
130
 
115
131
  return this;
116
132
  }
@@ -125,11 +141,13 @@ export class PivotTable {
125
141
  throw new Error(`Field not found in source data: ${fieldName}`);
126
142
  }
127
143
 
128
- this._columnFields.push({
144
+ const assignment: FieldAssignment = {
129
145
  fieldName,
130
146
  fieldIndex,
131
147
  axis: 'column',
132
- });
148
+ };
149
+ this._columnFields.push(assignment);
150
+ this._fieldAssignments.set(fieldIndex, assignment);
133
151
 
134
152
  return this;
135
153
  }
@@ -149,12 +167,7 @@ export class PivotTable {
149
167
  * pivot.addValueField({ field: 'Sales', aggregation: 'sum', name: 'Total Sales', numberFormat: '$#,##0.00' });
150
168
  */
151
169
  addValueField(config: PivotValueConfig): this;
152
- addValueField(
153
- fieldName: string,
154
- aggregation?: AggregationType,
155
- displayName?: string,
156
- numberFormat?: string,
157
- ): this;
170
+ addValueField(fieldName: string, aggregation?: AggregationType, displayName?: string, numberFormat?: string): this;
158
171
  addValueField(
159
172
  fieldNameOrConfig: string | PivotValueConfig,
160
173
  aggregation: AggregationType = 'sum',
@@ -192,14 +205,16 @@ export class PivotTable {
192
205
  numFmtId = this._styles.getOrCreateNumFmtId(format);
193
206
  }
194
207
 
195
- this._valueFields.push({
208
+ const assignment: FieldAssignment = {
196
209
  fieldName,
197
210
  fieldIndex,
198
211
  axis: 'value',
199
212
  aggregation: agg,
200
213
  displayName: name || defaultName,
201
214
  numFmtId,
202
- });
215
+ };
216
+ this._valueFields.push(assignment);
217
+ this._fieldAssignments.set(fieldIndex, assignment);
203
218
 
204
219
  return this;
205
220
  }
@@ -214,12 +229,61 @@ export class PivotTable {
214
229
  throw new Error(`Field not found in source data: ${fieldName}`);
215
230
  }
216
231
 
217
- this._filterFields.push({
232
+ const assignment: FieldAssignment = {
218
233
  fieldName,
219
234
  fieldIndex,
220
235
  axis: 'filter',
221
- });
236
+ };
237
+ this._filterFields.push(assignment);
238
+ this._fieldAssignments.set(fieldIndex, assignment);
239
+
240
+ return this;
241
+ }
242
+
243
+ /**
244
+ * Set a sort order for a row or column field
245
+ * @param fieldName - Name of the field to sort
246
+ * @param order - Sort order ('asc' or 'desc')
247
+ */
248
+ sortField(fieldName: string, order: PivotSortOrder): this {
249
+ const fieldIndex = this._cache.getFieldIndex(fieldName);
250
+ if (fieldIndex < 0) {
251
+ throw new Error(`Field not found in source data: ${fieldName}`);
252
+ }
253
+
254
+ const assignment = this._fieldAssignments.get(fieldIndex);
255
+ if (!assignment) {
256
+ throw new Error(`Field is not assigned to pivot table: ${fieldName}`);
257
+ }
258
+ if (assignment.axis !== 'row' && assignment.axis !== 'column') {
259
+ throw new Error(`Sort is only supported for row or column fields: ${fieldName}`);
260
+ }
222
261
 
262
+ assignment.sortOrder = order;
263
+ return this;
264
+ }
265
+
266
+ /**
267
+ * Filter items for a row, column, or filter field
268
+ * @param fieldName - Name of the field to filter
269
+ * @param filter - Filter configuration with include or exclude list
270
+ */
271
+ filterField(fieldName: string, filter: PivotFieldFilter): this {
272
+ const fieldIndex = this._cache.getFieldIndex(fieldName);
273
+ if (fieldIndex < 0) {
274
+ throw new Error(`Field not found in source data: ${fieldName}`);
275
+ }
276
+
277
+ const assignment = this._fieldAssignments.get(fieldIndex);
278
+ if (!assignment) {
279
+ throw new Error(`Field is not assigned to pivot table: ${fieldName}`);
280
+ }
281
+
282
+ if (filter.include && filter.exclude) {
283
+ throw new Error('Cannot use both include and exclude in the same filter');
284
+ }
285
+
286
+ assignment.filter = filter;
223
287
  return this;
224
288
  }
225
289
 
@@ -391,30 +455,36 @@ export class PivotTable {
391
455
  const filterField = this._filterFields.find((f) => f.fieldIndex === fieldIndex);
392
456
  const valueField = this._valueFields.find((f) => f.fieldIndex === fieldIndex);
393
457
 
458
+ // Get the assignment to check for sort/filter options
459
+ const assignment = rowField || colField || filterField;
460
+
394
461
  if (rowField) {
395
462
  attrs.axis = 'axisRow';
396
463
  attrs.showAll = '0';
464
+
465
+ // Add sort order if specified
466
+ if (rowField.sortOrder) {
467
+ attrs.sortType = rowField.sortOrder === 'asc' ? 'ascending' : 'descending';
468
+ }
469
+
397
470
  // Add items for shared values
398
471
  const cacheField = this._cache.fields[fieldIndex];
399
472
  if (cacheField && cacheField.sharedItems.length > 0) {
400
- const itemNodes: XmlNode[] = [];
401
- for (let i = 0; i < cacheField.sharedItems.length; i++) {
402
- itemNodes.push(createElement('item', { x: String(i) }, []));
403
- }
404
- // Add default subtotal item
405
- itemNodes.push(createElement('item', { t: 'default' }, []));
473
+ const itemNodes = this._buildItemNodes(cacheField.sharedItems, assignment?.filter);
406
474
  children.push(createElement('items', { count: String(itemNodes.length) }, itemNodes));
407
475
  }
408
476
  } else if (colField) {
409
477
  attrs.axis = 'axisCol';
410
478
  attrs.showAll = '0';
479
+
480
+ // Add sort order if specified
481
+ if (colField.sortOrder) {
482
+ attrs.sortType = colField.sortOrder === 'asc' ? 'ascending' : 'descending';
483
+ }
484
+
411
485
  const cacheField = this._cache.fields[fieldIndex];
412
486
  if (cacheField && cacheField.sharedItems.length > 0) {
413
- const itemNodes: XmlNode[] = [];
414
- for (let i = 0; i < cacheField.sharedItems.length; i++) {
415
- itemNodes.push(createElement('item', { x: String(i) }, []));
416
- }
417
- itemNodes.push(createElement('item', { t: 'default' }, []));
487
+ const itemNodes = this._buildItemNodes(cacheField.sharedItems, assignment?.filter);
418
488
  children.push(createElement('items', { count: String(itemNodes.length) }, itemNodes));
419
489
  }
420
490
  } else if (filterField) {
@@ -422,11 +492,7 @@ export class PivotTable {
422
492
  attrs.showAll = '0';
423
493
  const cacheField = this._cache.fields[fieldIndex];
424
494
  if (cacheField && cacheField.sharedItems.length > 0) {
425
- const itemNodes: XmlNode[] = [];
426
- for (let i = 0; i < cacheField.sharedItems.length; i++) {
427
- itemNodes.push(createElement('item', { x: String(i) }, []));
428
- }
429
- itemNodes.push(createElement('item', { t: 'default' }, []));
495
+ const itemNodes = this._buildItemNodes(cacheField.sharedItems, assignment?.filter);
430
496
  children.push(createElement('items', { count: String(itemNodes.length) }, itemNodes));
431
497
  }
432
498
  } else if (valueField) {
@@ -439,6 +505,38 @@ export class PivotTable {
439
505
  return createElement('pivotField', attrs, children);
440
506
  }
441
507
 
508
+ /**
509
+ * Build item nodes for a pivot field, with optional filtering
510
+ */
511
+ private _buildItemNodes(sharedItems: string[], filter?: PivotFieldFilter): XmlNode[] {
512
+ const itemNodes: XmlNode[] = [];
513
+
514
+ for (let i = 0; i < sharedItems.length; i++) {
515
+ const itemValue = sharedItems[i];
516
+ const itemAttrs: Record<string, string> = { x: String(i) };
517
+
518
+ // Check if this item should be hidden
519
+ if (filter) {
520
+ let hidden = false;
521
+ if (filter.exclude && filter.exclude.includes(itemValue)) {
522
+ hidden = true;
523
+ } else if (filter.include && !filter.include.includes(itemValue)) {
524
+ hidden = true;
525
+ }
526
+ if (hidden) {
527
+ itemAttrs.h = '1';
528
+ }
529
+ }
530
+
531
+ itemNodes.push(createElement('item', itemAttrs, []));
532
+ }
533
+
534
+ // Add default subtotal item
535
+ itemNodes.push(createElement('item', { t: 'default' }, []));
536
+
537
+ return itemNodes;
538
+ }
539
+
442
540
  /**
443
541
  * Build row items based on unique values in row fields
444
542
  */
package/src/range.ts CHANGED
@@ -42,12 +42,25 @@ export class Range {
42
42
  * Get all values in the range as a 2D array
43
43
  */
44
44
  get values(): CellValue[][] {
45
+ return this.getValues();
46
+ }
47
+
48
+ /**
49
+ * Get all values in the range as a 2D array with options
50
+ */
51
+ getValues(options: { createMissing?: boolean } = {}): CellValue[][] {
52
+ const { createMissing = true } = options;
45
53
  const result: CellValue[][] = [];
46
54
  for (let r = this._range.start.row; r <= this._range.end.row; r++) {
47
55
  const row: CellValue[] = [];
48
56
  for (let c = this._range.start.col; c <= this._range.end.col; c++) {
49
- const cell = this._worksheet.cell(r, c);
50
- row.push(cell.value);
57
+ if (createMissing) {
58
+ const cell = this._worksheet.cell(r, c);
59
+ row.push(cell.value);
60
+ } else {
61
+ const cell = this._worksheet.getCellIfExists(r, c);
62
+ row.push(cell?.value ?? null);
63
+ }
51
64
  }
52
65
  result.push(row);
53
66
  }
@@ -1,13 +1,23 @@
1
- import { parseXml, findElement, getChildren, XmlNode, stringifyXml, createElement, createText } from './utils/xml';
1
+ import {
2
+ parseXml,
3
+ findElement,
4
+ getChildren,
5
+ getAttr,
6
+ XmlNode,
7
+ stringifyXml,
8
+ createElement,
9
+ createText,
10
+ } from './utils/xml';
2
11
 
3
12
  /**
4
13
  * Manages the shared strings table from xl/sharedStrings.xml
5
14
  * Excel stores strings in a shared table to reduce file size
6
15
  */
7
16
  export class SharedStrings {
8
- private strings: string[] = [];
17
+ private entries: SharedStringEntry[] = [];
9
18
  private stringToIndex: Map<string, number> = new Map();
10
19
  private _dirty = false;
20
+ private _totalCount = 0;
11
21
 
12
22
  /**
13
23
  * Parse shared strings from XML content
@@ -18,16 +28,28 @@ export class SharedStrings {
18
28
  const sst = findElement(parsed, 'sst');
19
29
  if (!sst) return ss;
20
30
 
31
+ const countAttr = getAttr(sst, 'count');
32
+ if (countAttr) {
33
+ const total = parseInt(countAttr, 10);
34
+ if (Number.isFinite(total) && total >= 0) {
35
+ ss._totalCount = total;
36
+ }
37
+ }
38
+
21
39
  const children = getChildren(sst, 'sst');
22
40
  for (const child of children) {
23
41
  if ('si' in child) {
24
42
  const siChildren = getChildren(child, 'si');
25
43
  const text = ss.extractText(siChildren);
26
- ss.strings.push(text);
27
- ss.stringToIndex.set(text, ss.strings.length - 1);
44
+ ss.entries.push({ text, node: child });
45
+ ss.stringToIndex.set(text, ss.entries.length - 1);
28
46
  }
29
47
  }
30
48
 
49
+ if (ss._totalCount === 0 && ss.entries.length > 0) {
50
+ ss._totalCount = ss.entries.length;
51
+ }
52
+
31
53
  return ss;
32
54
  }
33
55
 
@@ -68,7 +90,7 @@ export class SharedStrings {
68
90
  * Get a string by index
69
91
  */
70
92
  getString(index: number): string | undefined {
71
- return this.strings[index];
93
+ return this.entries[index]?.text;
72
94
  }
73
95
 
74
96
  /**
@@ -78,11 +100,18 @@ export class SharedStrings {
78
100
  addString(str: string): number {
79
101
  const existing = this.stringToIndex.get(str);
80
102
  if (existing !== undefined) {
103
+ this._totalCount++;
104
+ this._dirty = true;
81
105
  return existing;
82
106
  }
83
- const index = this.strings.length;
84
- this.strings.push(str);
107
+ const index = this.entries.length;
108
+ const tElement = createElement('t', str.startsWith(' ') || str.endsWith(' ') ? { 'xml:space': 'preserve' } : {}, [
109
+ createText(str),
110
+ ]);
111
+ const siElement = createElement('si', {}, [tElement]);
112
+ this.entries.push({ text: str, node: siElement });
85
113
  this.stringToIndex.set(str, index);
114
+ this._totalCount++;
86
115
  this._dirty = true;
87
116
  return index;
88
117
  }
@@ -98,7 +127,14 @@ export class SharedStrings {
98
127
  * Get the count of strings
99
128
  */
100
129
  get count(): number {
101
- return this.strings.length;
130
+ return this.entries.length;
131
+ }
132
+
133
+ /**
134
+ * Get total usage count of shared strings
135
+ */
136
+ get totalCount(): number {
137
+ return Math.max(this._totalCount, this.entries.length);
102
138
  }
103
139
 
104
140
  /**
@@ -106,20 +142,28 @@ export class SharedStrings {
106
142
  */
107
143
  toXml(): string {
108
144
  const siElements: XmlNode[] = [];
109
- for (const str of this.strings) {
110
- const tElement = createElement('t', str.startsWith(' ') || str.endsWith(' ') ? { 'xml:space': 'preserve' } : {}, [
111
- createText(str),
112
- ]);
113
- const siElement = createElement('si', {}, [tElement]);
114
- siElements.push(siElement);
145
+ for (const entry of this.entries) {
146
+ if (entry.node) {
147
+ siElements.push(entry.node);
148
+ } else {
149
+ const str = entry.text;
150
+ const tElement = createElement(
151
+ 't',
152
+ str.startsWith(' ') || str.endsWith(' ') ? { 'xml:space': 'preserve' } : {},
153
+ [createText(str)],
154
+ );
155
+ const siElement = createElement('si', {}, [tElement]);
156
+ siElements.push(siElement);
157
+ }
115
158
  }
116
159
 
160
+ const totalCount = Math.max(this._totalCount, this.entries.length);
117
161
  const sst = createElement(
118
162
  'sst',
119
163
  {
120
164
  xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
121
- count: String(this.strings.length),
122
- uniqueCount: String(this.strings.length),
165
+ count: String(totalCount),
166
+ uniqueCount: String(this.entries.length),
123
167
  },
124
168
  siElements,
125
169
  );
@@ -127,3 +171,8 @@ export class SharedStrings {
127
171
  return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([sst])}`;
128
172
  }
129
173
  }
174
+
175
+ interface SharedStringEntry {
176
+ text: string;
177
+ node?: XmlNode;
178
+ }