@niicojs/excel 0.1.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/LICENSE +21 -0
- package/README.md +208 -0
- package/dist/index.cjs +2894 -0
- package/dist/index.d.cts +745 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.ts +745 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2881 -0
- package/package.json +61 -0
- package/src/cell.ts +318 -0
- package/src/index.ts +31 -0
- package/src/pivot-cache.ts +268 -0
- package/src/pivot-table.ts +523 -0
- package/src/range.ts +141 -0
- package/src/shared-strings.ts +129 -0
- package/src/styles.ts +588 -0
- package/src/types.ts +165 -0
- package/src/utils/address.ts +118 -0
- package/src/utils/xml.ts +147 -0
- package/src/utils/zip.ts +61 -0
- package/src/workbook.ts +845 -0
- package/src/worksheet.ts +372 -0
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
import type { AggregationType, PivotFieldAxis } from './types';
|
|
2
|
+
import { PivotCache } from './pivot-cache';
|
|
3
|
+
import { createElement, stringifyXml, XmlNode } from './utils/xml';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Internal structure for tracking field assignments
|
|
7
|
+
*/
|
|
8
|
+
interface FieldAssignment {
|
|
9
|
+
fieldName: string;
|
|
10
|
+
fieldIndex: number;
|
|
11
|
+
axis: PivotFieldAxis;
|
|
12
|
+
aggregation?: AggregationType;
|
|
13
|
+
displayName?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Represents an Excel pivot table with a fluent API for configuration.
|
|
18
|
+
*/
|
|
19
|
+
export class PivotTable {
|
|
20
|
+
private _name: string;
|
|
21
|
+
private _cache: PivotCache;
|
|
22
|
+
private _targetSheet: string;
|
|
23
|
+
private _targetCell: string;
|
|
24
|
+
private _targetRow: number;
|
|
25
|
+
private _targetCol: number;
|
|
26
|
+
|
|
27
|
+
private _rowFields: FieldAssignment[] = [];
|
|
28
|
+
private _columnFields: FieldAssignment[] = [];
|
|
29
|
+
private _valueFields: FieldAssignment[] = [];
|
|
30
|
+
private _filterFields: FieldAssignment[] = [];
|
|
31
|
+
|
|
32
|
+
private _pivotTableIndex: number;
|
|
33
|
+
|
|
34
|
+
constructor(
|
|
35
|
+
name: string,
|
|
36
|
+
cache: PivotCache,
|
|
37
|
+
targetSheet: string,
|
|
38
|
+
targetCell: string,
|
|
39
|
+
targetRow: number,
|
|
40
|
+
targetCol: number,
|
|
41
|
+
pivotTableIndex: number,
|
|
42
|
+
) {
|
|
43
|
+
this._name = name;
|
|
44
|
+
this._cache = cache;
|
|
45
|
+
this._targetSheet = targetSheet;
|
|
46
|
+
this._targetCell = targetCell;
|
|
47
|
+
this._targetRow = targetRow;
|
|
48
|
+
this._targetCol = targetCol;
|
|
49
|
+
this._pivotTableIndex = pivotTableIndex;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get the pivot table name
|
|
54
|
+
*/
|
|
55
|
+
get name(): string {
|
|
56
|
+
return this._name;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get the target sheet name
|
|
61
|
+
*/
|
|
62
|
+
get targetSheet(): string {
|
|
63
|
+
return this._targetSheet;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get the target cell address
|
|
68
|
+
*/
|
|
69
|
+
get targetCell(): string {
|
|
70
|
+
return this._targetCell;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Get the pivot cache
|
|
75
|
+
*/
|
|
76
|
+
get cache(): PivotCache {
|
|
77
|
+
return this._cache;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Get the pivot table index (for file naming)
|
|
82
|
+
*/
|
|
83
|
+
get index(): number {
|
|
84
|
+
return this._pivotTableIndex;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Add a field to the row area
|
|
89
|
+
* @param fieldName - Name of the source field (column header)
|
|
90
|
+
*/
|
|
91
|
+
addRowField(fieldName: string): this {
|
|
92
|
+
const fieldIndex = this._cache.getFieldIndex(fieldName);
|
|
93
|
+
if (fieldIndex < 0) {
|
|
94
|
+
throw new Error(`Field not found in source data: ${fieldName}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
this._rowFields.push({
|
|
98
|
+
fieldName,
|
|
99
|
+
fieldIndex,
|
|
100
|
+
axis: 'row',
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
return this;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Add a field to the column area
|
|
108
|
+
* @param fieldName - Name of the source field (column header)
|
|
109
|
+
*/
|
|
110
|
+
addColumnField(fieldName: string): this {
|
|
111
|
+
const fieldIndex = this._cache.getFieldIndex(fieldName);
|
|
112
|
+
if (fieldIndex < 0) {
|
|
113
|
+
throw new Error(`Field not found in source data: ${fieldName}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
this._columnFields.push({
|
|
117
|
+
fieldName,
|
|
118
|
+
fieldIndex,
|
|
119
|
+
axis: 'column',
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
return this;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Add a field to the values area with aggregation
|
|
127
|
+
* @param fieldName - Name of the source field (column header)
|
|
128
|
+
* @param aggregation - Aggregation function (sum, count, average, min, max)
|
|
129
|
+
* @param displayName - Optional display name (defaults to "Sum of FieldName")
|
|
130
|
+
*/
|
|
131
|
+
addValueField(fieldName: string, aggregation: AggregationType = 'sum', displayName?: string): this {
|
|
132
|
+
const fieldIndex = this._cache.getFieldIndex(fieldName);
|
|
133
|
+
if (fieldIndex < 0) {
|
|
134
|
+
throw new Error(`Field not found in source data: ${fieldName}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const defaultName = `${aggregation.charAt(0).toUpperCase() + aggregation.slice(1)} of ${fieldName}`;
|
|
138
|
+
|
|
139
|
+
this._valueFields.push({
|
|
140
|
+
fieldName,
|
|
141
|
+
fieldIndex,
|
|
142
|
+
axis: 'value',
|
|
143
|
+
aggregation,
|
|
144
|
+
displayName: displayName || defaultName,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
return this;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Add a field to the filter (page) area
|
|
152
|
+
* @param fieldName - Name of the source field (column header)
|
|
153
|
+
*/
|
|
154
|
+
addFilterField(fieldName: string): this {
|
|
155
|
+
const fieldIndex = this._cache.getFieldIndex(fieldName);
|
|
156
|
+
if (fieldIndex < 0) {
|
|
157
|
+
throw new Error(`Field not found in source data: ${fieldName}`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
this._filterFields.push({
|
|
161
|
+
fieldName,
|
|
162
|
+
fieldIndex,
|
|
163
|
+
axis: 'filter',
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
return this;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Generate the pivotTableDefinition XML
|
|
171
|
+
*/
|
|
172
|
+
toXml(): string {
|
|
173
|
+
const children: XmlNode[] = [];
|
|
174
|
+
|
|
175
|
+
// Calculate location (estimate based on fields)
|
|
176
|
+
const locationRef = this._calculateLocationRef();
|
|
177
|
+
|
|
178
|
+
// Calculate first data row/col offsets (1-based, relative to pivot table)
|
|
179
|
+
// firstHeaderRow: row offset of column headers (usually 1)
|
|
180
|
+
// firstDataRow: row offset where data starts (after filters and column headers)
|
|
181
|
+
// firstDataCol: column offset where data starts (after row labels)
|
|
182
|
+
const filterRowCount = this._filterFields.length > 0 ? this._filterFields.length + 1 : 0;
|
|
183
|
+
const headerRows = this._columnFields.length > 0 ? 1 : 0;
|
|
184
|
+
const firstDataRow = filterRowCount + headerRows + 1;
|
|
185
|
+
const firstDataCol = this._rowFields.length > 0 ? this._rowFields.length : 1;
|
|
186
|
+
|
|
187
|
+
const locationNode = createElement(
|
|
188
|
+
'location',
|
|
189
|
+
{
|
|
190
|
+
ref: locationRef,
|
|
191
|
+
firstHeaderRow: String(filterRowCount + 1),
|
|
192
|
+
firstDataRow: String(firstDataRow),
|
|
193
|
+
firstDataCol: String(firstDataCol),
|
|
194
|
+
},
|
|
195
|
+
[],
|
|
196
|
+
);
|
|
197
|
+
children.push(locationNode);
|
|
198
|
+
|
|
199
|
+
// Build pivotFields (one per source field)
|
|
200
|
+
const pivotFieldNodes: XmlNode[] = [];
|
|
201
|
+
for (const cacheField of this._cache.fields) {
|
|
202
|
+
const fieldNode = this._buildPivotFieldNode(cacheField.index);
|
|
203
|
+
pivotFieldNodes.push(fieldNode);
|
|
204
|
+
}
|
|
205
|
+
children.push(createElement('pivotFields', { count: String(pivotFieldNodes.length) }, pivotFieldNodes));
|
|
206
|
+
|
|
207
|
+
// Row fields
|
|
208
|
+
if (this._rowFields.length > 0) {
|
|
209
|
+
const rowFieldNodes = this._rowFields.map((f) => createElement('field', { x: String(f.fieldIndex) }, []));
|
|
210
|
+
children.push(createElement('rowFields', { count: String(rowFieldNodes.length) }, rowFieldNodes));
|
|
211
|
+
|
|
212
|
+
// Row items
|
|
213
|
+
const rowItemNodes = this._buildRowItems();
|
|
214
|
+
children.push(createElement('rowItems', { count: String(rowItemNodes.length) }, rowItemNodes));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Column fields
|
|
218
|
+
if (this._columnFields.length > 0) {
|
|
219
|
+
const colFieldNodes = this._columnFields.map((f) => createElement('field', { x: String(f.fieldIndex) }, []));
|
|
220
|
+
// If we have multiple value fields, add -2 to indicate where "Values" header goes
|
|
221
|
+
if (this._valueFields.length > 1) {
|
|
222
|
+
colFieldNodes.push(createElement('field', { x: '-2' }, []));
|
|
223
|
+
}
|
|
224
|
+
children.push(createElement('colFields', { count: String(colFieldNodes.length) }, colFieldNodes));
|
|
225
|
+
|
|
226
|
+
// Column items - need to account for multiple value fields
|
|
227
|
+
const colItemNodes = this._buildColItems();
|
|
228
|
+
children.push(createElement('colItems', { count: String(colItemNodes.length) }, colItemNodes));
|
|
229
|
+
} else if (this._valueFields.length > 1) {
|
|
230
|
+
// If no column fields but we have multiple values, need colFields with -2 (data field indicator)
|
|
231
|
+
children.push(createElement('colFields', { count: '1' }, [createElement('field', { x: '-2' }, [])]));
|
|
232
|
+
|
|
233
|
+
// Column items for each value field
|
|
234
|
+
const colItemNodes: XmlNode[] = [];
|
|
235
|
+
for (let i = 0; i < this._valueFields.length; i++) {
|
|
236
|
+
colItemNodes.push(createElement('i', {}, [createElement('x', i === 0 ? {} : { v: String(i) }, [])]));
|
|
237
|
+
}
|
|
238
|
+
children.push(createElement('colItems', { count: String(colItemNodes.length) }, colItemNodes));
|
|
239
|
+
} else if (this._valueFields.length === 1) {
|
|
240
|
+
// Single value field - just add a single column item
|
|
241
|
+
children.push(createElement('colItems', { count: '1' }, [createElement('i', {}, [])]));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Page (filter) fields
|
|
245
|
+
if (this._filterFields.length > 0) {
|
|
246
|
+
const pageFieldNodes = this._filterFields.map((f) =>
|
|
247
|
+
createElement('pageField', { fld: String(f.fieldIndex), hier: '-1' }, []),
|
|
248
|
+
);
|
|
249
|
+
children.push(createElement('pageFields', { count: String(pageFieldNodes.length) }, pageFieldNodes));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Data fields (values)
|
|
253
|
+
if (this._valueFields.length > 0) {
|
|
254
|
+
const dataFieldNodes = this._valueFields.map((f) =>
|
|
255
|
+
createElement(
|
|
256
|
+
'dataField',
|
|
257
|
+
{
|
|
258
|
+
name: f.displayName || f.fieldName,
|
|
259
|
+
fld: String(f.fieldIndex),
|
|
260
|
+
baseField: '0',
|
|
261
|
+
baseItem: '0',
|
|
262
|
+
subtotal: f.aggregation || 'sum',
|
|
263
|
+
},
|
|
264
|
+
[],
|
|
265
|
+
),
|
|
266
|
+
);
|
|
267
|
+
children.push(createElement('dataFields', { count: String(dataFieldNodes.length) }, dataFieldNodes));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Pivot table style
|
|
271
|
+
children.push(
|
|
272
|
+
createElement(
|
|
273
|
+
'pivotTableStyleInfo',
|
|
274
|
+
{
|
|
275
|
+
name: 'PivotStyleMedium9',
|
|
276
|
+
showRowHeaders: '1',
|
|
277
|
+
showColHeaders: '1',
|
|
278
|
+
showRowStripes: '0',
|
|
279
|
+
showColStripes: '0',
|
|
280
|
+
showLastColumn: '1',
|
|
281
|
+
},
|
|
282
|
+
[],
|
|
283
|
+
),
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
const pivotTableNode = createElement(
|
|
287
|
+
'pivotTableDefinition',
|
|
288
|
+
{
|
|
289
|
+
xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
|
|
290
|
+
'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',
|
|
291
|
+
name: this._name,
|
|
292
|
+
cacheId: String(this._cache.cacheId),
|
|
293
|
+
applyNumberFormats: '0',
|
|
294
|
+
applyBorderFormats: '0',
|
|
295
|
+
applyFontFormats: '0',
|
|
296
|
+
applyPatternFormats: '0',
|
|
297
|
+
applyAlignmentFormats: '0',
|
|
298
|
+
applyWidthHeightFormats: '1',
|
|
299
|
+
dataCaption: 'Values',
|
|
300
|
+
updatedVersion: '8',
|
|
301
|
+
minRefreshableVersion: '3',
|
|
302
|
+
useAutoFormatting: '1',
|
|
303
|
+
rowGrandTotals: '1',
|
|
304
|
+
colGrandTotals: '1',
|
|
305
|
+
itemPrintTitles: '1',
|
|
306
|
+
createdVersion: '8',
|
|
307
|
+
indent: '0',
|
|
308
|
+
outline: '1',
|
|
309
|
+
outlineData: '1',
|
|
310
|
+
multipleFieldFilters: '0',
|
|
311
|
+
},
|
|
312
|
+
children,
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([pivotTableNode])}`;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Build a pivotField node for a given field index
|
|
320
|
+
*/
|
|
321
|
+
private _buildPivotFieldNode(fieldIndex: number): XmlNode {
|
|
322
|
+
const attrs: Record<string, string> = {};
|
|
323
|
+
const children: XmlNode[] = [];
|
|
324
|
+
|
|
325
|
+
// Check if this field is assigned to an axis
|
|
326
|
+
const rowField = this._rowFields.find((f) => f.fieldIndex === fieldIndex);
|
|
327
|
+
const colField = this._columnFields.find((f) => f.fieldIndex === fieldIndex);
|
|
328
|
+
const filterField = this._filterFields.find((f) => f.fieldIndex === fieldIndex);
|
|
329
|
+
const valueField = this._valueFields.find((f) => f.fieldIndex === fieldIndex);
|
|
330
|
+
|
|
331
|
+
if (rowField) {
|
|
332
|
+
attrs.axis = 'axisRow';
|
|
333
|
+
attrs.showAll = '0';
|
|
334
|
+
// Add items for shared values
|
|
335
|
+
const cacheField = this._cache.fields[fieldIndex];
|
|
336
|
+
if (cacheField && cacheField.sharedItems.length > 0) {
|
|
337
|
+
const itemNodes: XmlNode[] = [];
|
|
338
|
+
for (let i = 0; i < cacheField.sharedItems.length; i++) {
|
|
339
|
+
itemNodes.push(createElement('item', { x: String(i) }, []));
|
|
340
|
+
}
|
|
341
|
+
// Add default subtotal item
|
|
342
|
+
itemNodes.push(createElement('item', { t: 'default' }, []));
|
|
343
|
+
children.push(createElement('items', { count: String(itemNodes.length) }, itemNodes));
|
|
344
|
+
}
|
|
345
|
+
} else if (colField) {
|
|
346
|
+
attrs.axis = 'axisCol';
|
|
347
|
+
attrs.showAll = '0';
|
|
348
|
+
const cacheField = this._cache.fields[fieldIndex];
|
|
349
|
+
if (cacheField && cacheField.sharedItems.length > 0) {
|
|
350
|
+
const itemNodes: XmlNode[] = [];
|
|
351
|
+
for (let i = 0; i < cacheField.sharedItems.length; i++) {
|
|
352
|
+
itemNodes.push(createElement('item', { x: String(i) }, []));
|
|
353
|
+
}
|
|
354
|
+
itemNodes.push(createElement('item', { t: 'default' }, []));
|
|
355
|
+
children.push(createElement('items', { count: String(itemNodes.length) }, itemNodes));
|
|
356
|
+
}
|
|
357
|
+
} else if (filterField) {
|
|
358
|
+
attrs.axis = 'axisPage';
|
|
359
|
+
attrs.showAll = '0';
|
|
360
|
+
const cacheField = this._cache.fields[fieldIndex];
|
|
361
|
+
if (cacheField && cacheField.sharedItems.length > 0) {
|
|
362
|
+
const itemNodes: XmlNode[] = [];
|
|
363
|
+
for (let i = 0; i < cacheField.sharedItems.length; i++) {
|
|
364
|
+
itemNodes.push(createElement('item', { x: String(i) }, []));
|
|
365
|
+
}
|
|
366
|
+
itemNodes.push(createElement('item', { t: 'default' }, []));
|
|
367
|
+
children.push(createElement('items', { count: String(itemNodes.length) }, itemNodes));
|
|
368
|
+
}
|
|
369
|
+
} else if (valueField) {
|
|
370
|
+
attrs.dataField = '1';
|
|
371
|
+
attrs.showAll = '0';
|
|
372
|
+
} else {
|
|
373
|
+
attrs.showAll = '0';
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return createElement('pivotField', attrs, children);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Build row items based on unique values in row fields
|
|
381
|
+
*/
|
|
382
|
+
private _buildRowItems(): XmlNode[] {
|
|
383
|
+
const items: XmlNode[] = [];
|
|
384
|
+
|
|
385
|
+
if (this._rowFields.length === 0) return items;
|
|
386
|
+
|
|
387
|
+
// Get unique values from first row field
|
|
388
|
+
const firstRowField = this._rowFields[0];
|
|
389
|
+
const cacheField = this._cache.fields[firstRowField.fieldIndex];
|
|
390
|
+
|
|
391
|
+
if (cacheField && cacheField.sharedItems.length > 0) {
|
|
392
|
+
for (let i = 0; i < cacheField.sharedItems.length; i++) {
|
|
393
|
+
items.push(createElement('i', {}, [createElement('x', i === 0 ? {} : { v: String(i) }, [])]));
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Add grand total row
|
|
398
|
+
items.push(createElement('i', { t: 'grand' }, [createElement('x', {}, [])]));
|
|
399
|
+
|
|
400
|
+
return items;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Build column items based on unique values in column fields
|
|
405
|
+
*/
|
|
406
|
+
private _buildColItems(): XmlNode[] {
|
|
407
|
+
const items: XmlNode[] = [];
|
|
408
|
+
|
|
409
|
+
if (this._columnFields.length === 0) return items;
|
|
410
|
+
|
|
411
|
+
// Get unique values from first column field
|
|
412
|
+
const firstColField = this._columnFields[0];
|
|
413
|
+
const cacheField = this._cache.fields[firstColField.fieldIndex];
|
|
414
|
+
|
|
415
|
+
if (cacheField && cacheField.sharedItems.length > 0) {
|
|
416
|
+
if (this._valueFields.length > 1) {
|
|
417
|
+
// Multiple value fields - need nested items for each column value + value field combination
|
|
418
|
+
for (let colIdx = 0; colIdx < cacheField.sharedItems.length; colIdx++) {
|
|
419
|
+
for (let valIdx = 0; valIdx < this._valueFields.length; valIdx++) {
|
|
420
|
+
const xNodes: XmlNode[] = [
|
|
421
|
+
createElement('x', colIdx === 0 ? {} : { v: String(colIdx) }, []),
|
|
422
|
+
createElement('x', valIdx === 0 ? {} : { v: String(valIdx) }, []),
|
|
423
|
+
];
|
|
424
|
+
items.push(createElement('i', {}, xNodes));
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
} else {
|
|
428
|
+
// Single value field - simple column items
|
|
429
|
+
for (let i = 0; i < cacheField.sharedItems.length; i++) {
|
|
430
|
+
items.push(createElement('i', {}, [createElement('x', i === 0 ? {} : { v: String(i) }, [])]));
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Add grand total column(s)
|
|
436
|
+
if (this._valueFields.length > 1) {
|
|
437
|
+
// Grand total for each value field
|
|
438
|
+
for (let valIdx = 0; valIdx < this._valueFields.length; valIdx++) {
|
|
439
|
+
const xNodes: XmlNode[] = [
|
|
440
|
+
createElement('x', {}, []),
|
|
441
|
+
createElement('x', valIdx === 0 ? {} : { v: String(valIdx) }, []),
|
|
442
|
+
];
|
|
443
|
+
items.push(createElement('i', { t: 'grand' }, xNodes));
|
|
444
|
+
}
|
|
445
|
+
} else {
|
|
446
|
+
items.push(createElement('i', { t: 'grand' }, [createElement('x', {}, [])]));
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return items;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Calculate the location reference for the pivot table output
|
|
454
|
+
*/
|
|
455
|
+
private _calculateLocationRef(): string {
|
|
456
|
+
// Estimate output size based on fields
|
|
457
|
+
const numRows = this._estimateRowCount();
|
|
458
|
+
const numCols = this._estimateColCount();
|
|
459
|
+
|
|
460
|
+
const startRow = this._targetRow;
|
|
461
|
+
const startCol = this._targetCol;
|
|
462
|
+
const endRow = startRow + numRows - 1;
|
|
463
|
+
const endCol = startCol + numCols - 1;
|
|
464
|
+
|
|
465
|
+
return `${this._colToLetter(startCol)}${startRow}:${this._colToLetter(endCol)}${endRow}`;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Estimate number of rows in pivot table output
|
|
470
|
+
*/
|
|
471
|
+
private _estimateRowCount(): number {
|
|
472
|
+
let count = 1; // Header row
|
|
473
|
+
|
|
474
|
+
// Add filter area rows
|
|
475
|
+
count += this._filterFields.length;
|
|
476
|
+
|
|
477
|
+
// Add row labels (unique values in row fields)
|
|
478
|
+
if (this._rowFields.length > 0) {
|
|
479
|
+
const firstRowField = this._rowFields[0];
|
|
480
|
+
const cacheField = this._cache.fields[firstRowField.fieldIndex];
|
|
481
|
+
count += (cacheField?.sharedItems.length || 1) + 1; // +1 for grand total
|
|
482
|
+
} else {
|
|
483
|
+
count += 1; // At least one data row
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return Math.max(count, 3);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Estimate number of columns in pivot table output
|
|
491
|
+
*/
|
|
492
|
+
private _estimateColCount(): number {
|
|
493
|
+
let count = 0;
|
|
494
|
+
|
|
495
|
+
// Row label columns
|
|
496
|
+
count += Math.max(this._rowFields.length, 1);
|
|
497
|
+
|
|
498
|
+
// Column labels (unique values in column fields)
|
|
499
|
+
if (this._columnFields.length > 0) {
|
|
500
|
+
const firstColField = this._columnFields[0];
|
|
501
|
+
const cacheField = this._cache.fields[firstColField.fieldIndex];
|
|
502
|
+
count += (cacheField?.sharedItems.length || 1) + 1; // +1 for grand total
|
|
503
|
+
} else {
|
|
504
|
+
// Value columns
|
|
505
|
+
count += Math.max(this._valueFields.length, 1);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
return Math.max(count, 2);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Convert 0-based column index to letter (A, B, ..., Z, AA, etc.)
|
|
513
|
+
*/
|
|
514
|
+
private _colToLetter(col: number): string {
|
|
515
|
+
let result = '';
|
|
516
|
+
let n = col;
|
|
517
|
+
while (n >= 0) {
|
|
518
|
+
result = String.fromCharCode((n % 26) + 65) + result;
|
|
519
|
+
n = Math.floor(n / 26) - 1;
|
|
520
|
+
}
|
|
521
|
+
return result;
|
|
522
|
+
}
|
|
523
|
+
}
|
package/src/range.ts
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import type { CellValue, CellStyle, RangeAddress } from './types';
|
|
2
|
+
import type { Worksheet } from './worksheet';
|
|
3
|
+
import { toAddress, normalizeRange } from './utils/address';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Represents a range of cells in a worksheet
|
|
7
|
+
*/
|
|
8
|
+
export class Range {
|
|
9
|
+
private _worksheet: Worksheet;
|
|
10
|
+
private _range: RangeAddress;
|
|
11
|
+
|
|
12
|
+
constructor(worksheet: Worksheet, range: RangeAddress) {
|
|
13
|
+
this._worksheet = worksheet;
|
|
14
|
+
this._range = normalizeRange(range);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Get the range address as a string
|
|
19
|
+
*/
|
|
20
|
+
get address(): string {
|
|
21
|
+
const start = toAddress(this._range.start.row, this._range.start.col);
|
|
22
|
+
const end = toAddress(this._range.end.row, this._range.end.col);
|
|
23
|
+
if (start === end) return start;
|
|
24
|
+
return `${start}:${end}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get the number of rows in the range
|
|
29
|
+
*/
|
|
30
|
+
get rowCount(): number {
|
|
31
|
+
return this._range.end.row - this._range.start.row + 1;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Get the number of columns in the range
|
|
36
|
+
*/
|
|
37
|
+
get colCount(): number {
|
|
38
|
+
return this._range.end.col - this._range.start.col + 1;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get all values in the range as a 2D array
|
|
43
|
+
*/
|
|
44
|
+
get values(): CellValue[][] {
|
|
45
|
+
const result: CellValue[][] = [];
|
|
46
|
+
for (let r = this._range.start.row; r <= this._range.end.row; r++) {
|
|
47
|
+
const row: CellValue[] = [];
|
|
48
|
+
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);
|
|
51
|
+
}
|
|
52
|
+
result.push(row);
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Set values in the range from a 2D array
|
|
59
|
+
*/
|
|
60
|
+
set values(data: CellValue[][]) {
|
|
61
|
+
for (let r = 0; r < data.length && r < this.rowCount; r++) {
|
|
62
|
+
const row = data[r];
|
|
63
|
+
for (let c = 0; c < row.length && c < this.colCount; c++) {
|
|
64
|
+
const cell = this._worksheet.cell(this._range.start.row + r, this._range.start.col + c);
|
|
65
|
+
cell.value = row[c];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Get all formulas in the range as a 2D array
|
|
72
|
+
*/
|
|
73
|
+
get formulas(): (string | undefined)[][] {
|
|
74
|
+
const result: (string | undefined)[][] = [];
|
|
75
|
+
for (let r = this._range.start.row; r <= this._range.end.row; r++) {
|
|
76
|
+
const row: (string | undefined)[] = [];
|
|
77
|
+
for (let c = this._range.start.col; c <= this._range.end.col; c++) {
|
|
78
|
+
const cell = this._worksheet.cell(r, c);
|
|
79
|
+
row.push(cell.formula);
|
|
80
|
+
}
|
|
81
|
+
result.push(row);
|
|
82
|
+
}
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Set formulas in the range from a 2D array
|
|
88
|
+
*/
|
|
89
|
+
set formulas(data: (string | undefined)[][]) {
|
|
90
|
+
for (let r = 0; r < data.length && r < this.rowCount; r++) {
|
|
91
|
+
const row = data[r];
|
|
92
|
+
for (let c = 0; c < row.length && c < this.colCount; c++) {
|
|
93
|
+
const cell = this._worksheet.cell(this._range.start.row + r, this._range.start.col + c);
|
|
94
|
+
cell.formula = row[c];
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get the style of the top-left cell
|
|
101
|
+
*/
|
|
102
|
+
get style(): CellStyle {
|
|
103
|
+
return this._worksheet.cell(this._range.start.row, this._range.start.col).style;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Set style for all cells in the range
|
|
108
|
+
*/
|
|
109
|
+
set style(style: CellStyle) {
|
|
110
|
+
for (let r = this._range.start.row; r <= this._range.end.row; r++) {
|
|
111
|
+
for (let c = this._range.start.col; c <= this._range.end.col; c++) {
|
|
112
|
+
const cell = this._worksheet.cell(r, c);
|
|
113
|
+
cell.style = style;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Iterate over all cells in the range
|
|
120
|
+
*/
|
|
121
|
+
*[Symbol.iterator]() {
|
|
122
|
+
for (let r = this._range.start.row; r <= this._range.end.row; r++) {
|
|
123
|
+
for (let c = this._range.start.col; c <= this._range.end.col; c++) {
|
|
124
|
+
yield this._worksheet.cell(r, c);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Iterate over cells row by row
|
|
131
|
+
*/
|
|
132
|
+
*rows() {
|
|
133
|
+
for (let r = this._range.start.row; r <= this._range.end.row; r++) {
|
|
134
|
+
const row = [];
|
|
135
|
+
for (let c = this._range.start.col; c <= this._range.end.col; c++) {
|
|
136
|
+
row.push(this._worksheet.cell(r, c));
|
|
137
|
+
}
|
|
138
|
+
yield row;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|