@niicojs/excel 0.3.1 → 0.3.2
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 +20 -20
- package/README.md +585 -585
- package/dist/index.cjs +740 -392
- package/dist/index.d.cts +18 -0
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +740 -392
- package/package.json +3 -3
- package/src/cell.ts +14 -0
- package/src/index.ts +45 -45
- package/src/pivot-cache.ts +300 -300
- package/src/pivot-table.ts +684 -684
- package/src/range.ts +154 -154
- package/src/shared-strings.ts +178 -178
- package/src/styles.ts +819 -819
- package/src/table.ts +386 -386
- package/src/types.ts +16 -10
- package/src/utils/address.ts +121 -121
- package/src/utils/format.ts +356 -0
- package/src/utils/xml.ts +140 -140
- package/src/workbook.ts +1406 -1390
- package/src/worksheet.ts +85 -84
package/src/pivot-cache.ts
CHANGED
|
@@ -1,300 +1,300 @@
|
|
|
1
|
-
import type { PivotCacheField, CellValue } from './types';
|
|
2
|
-
import { createElement, stringifyXml, XmlNode } from './utils/xml';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Manages the pivot cache (definition and records) for a pivot table.
|
|
6
|
-
* The cache stores source data metadata and cached values.
|
|
7
|
-
*/
|
|
8
|
-
export class PivotCache {
|
|
9
|
-
private _cacheId: number;
|
|
10
|
-
private _fileIndex: number;
|
|
11
|
-
private _sourceSheet: string;
|
|
12
|
-
private _sourceRange: string;
|
|
13
|
-
private _fields: PivotCacheField[] = [];
|
|
14
|
-
private _records: CellValue[][] = [];
|
|
15
|
-
private _recordCount = 0;
|
|
16
|
-
private _refreshOnLoad = true; // Default to true
|
|
17
|
-
// Optimized lookup: Map<fieldIndex, Map<stringValue, sharedItemsIndex>>
|
|
18
|
-
private _sharedItemsIndexMap: Map<number, Map<string, number>> = new Map();
|
|
19
|
-
|
|
20
|
-
constructor(cacheId: number, sourceSheet: string, sourceRange: string, fileIndex: number) {
|
|
21
|
-
this._cacheId = cacheId;
|
|
22
|
-
this._fileIndex = fileIndex;
|
|
23
|
-
this._sourceSheet = sourceSheet;
|
|
24
|
-
this._sourceRange = sourceRange;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Get the cache ID
|
|
29
|
-
*/
|
|
30
|
-
get cacheId(): number {
|
|
31
|
-
return this._cacheId;
|
|
32
|
-
}
|
|
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
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Set refreshOnLoad option
|
|
43
|
-
*/
|
|
44
|
-
set refreshOnLoad(value: boolean) {
|
|
45
|
-
this._refreshOnLoad = value;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Get refreshOnLoad option
|
|
50
|
-
*/
|
|
51
|
-
get refreshOnLoad(): boolean {
|
|
52
|
-
return this._refreshOnLoad;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Get the source sheet name
|
|
57
|
-
*/
|
|
58
|
-
get sourceSheet(): string {
|
|
59
|
-
return this._sourceSheet;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Get the source range
|
|
64
|
-
*/
|
|
65
|
-
get sourceRange(): string {
|
|
66
|
-
return this._sourceRange;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Get the full source reference (Sheet!Range)
|
|
71
|
-
*/
|
|
72
|
-
get sourceRef(): string {
|
|
73
|
-
return `${this._sourceSheet}!${this._sourceRange}`;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Get the fields in this cache
|
|
78
|
-
*/
|
|
79
|
-
get fields(): PivotCacheField[] {
|
|
80
|
-
return this._fields;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Get the number of data records
|
|
85
|
-
*/
|
|
86
|
-
get recordCount(): number {
|
|
87
|
-
return this._recordCount;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Build the cache from source data.
|
|
92
|
-
* @param headers - Array of column header names
|
|
93
|
-
* @param data - 2D array of data rows (excluding headers)
|
|
94
|
-
*/
|
|
95
|
-
buildFromData(headers: string[], data: CellValue[][]): void {
|
|
96
|
-
this._recordCount = data.length;
|
|
97
|
-
|
|
98
|
-
// Initialize fields from headers
|
|
99
|
-
this._fields = headers.map((name, index) => ({
|
|
100
|
-
name,
|
|
101
|
-
index,
|
|
102
|
-
isNumeric: true,
|
|
103
|
-
isDate: false,
|
|
104
|
-
sharedItems: [],
|
|
105
|
-
minValue: undefined,
|
|
106
|
-
maxValue: undefined,
|
|
107
|
-
}));
|
|
108
|
-
|
|
109
|
-
// Use Sets for O(1) unique value collection during analysis
|
|
110
|
-
const sharedItemsSets: Set<string>[] = this._fields.map(() => new Set<string>());
|
|
111
|
-
|
|
112
|
-
// Analyze data to determine field types and collect unique values
|
|
113
|
-
for (const row of data) {
|
|
114
|
-
for (let colIdx = 0; colIdx < row.length && colIdx < this._fields.length; colIdx++) {
|
|
115
|
-
const value = row[colIdx];
|
|
116
|
-
const field = this._fields[colIdx];
|
|
117
|
-
|
|
118
|
-
if (value === null || value === undefined) {
|
|
119
|
-
continue;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
if (typeof value === 'string') {
|
|
123
|
-
field.isNumeric = false;
|
|
124
|
-
// O(1) Set.add instead of O(n) Array.includes + push
|
|
125
|
-
sharedItemsSets[colIdx].add(value);
|
|
126
|
-
} else if (typeof value === 'number') {
|
|
127
|
-
if (field.minValue === undefined || value < field.minValue) {
|
|
128
|
-
field.minValue = value;
|
|
129
|
-
}
|
|
130
|
-
if (field.maxValue === undefined || value > field.maxValue) {
|
|
131
|
-
field.maxValue = value;
|
|
132
|
-
}
|
|
133
|
-
} else if (value instanceof Date) {
|
|
134
|
-
field.isDate = true;
|
|
135
|
-
field.isNumeric = false;
|
|
136
|
-
} else if (typeof value === 'boolean') {
|
|
137
|
-
field.isNumeric = false;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Convert Sets to arrays and build reverse index Maps for O(1) lookup during XML generation
|
|
143
|
-
this._sharedItemsIndexMap.clear();
|
|
144
|
-
for (let colIdx = 0; colIdx < this._fields.length; colIdx++) {
|
|
145
|
-
const field = this._fields[colIdx];
|
|
146
|
-
const set = sharedItemsSets[colIdx];
|
|
147
|
-
|
|
148
|
-
// Convert Set to array (maintains insertion order in ES6+)
|
|
149
|
-
field.sharedItems = Array.from(set);
|
|
150
|
-
|
|
151
|
-
// Build reverse lookup Map: value -> index
|
|
152
|
-
if (field.sharedItems.length > 0) {
|
|
153
|
-
const indexMap = new Map<string, number>();
|
|
154
|
-
for (let i = 0; i < field.sharedItems.length; i++) {
|
|
155
|
-
indexMap.set(field.sharedItems[i], i);
|
|
156
|
-
}
|
|
157
|
-
this._sharedItemsIndexMap.set(colIdx, indexMap);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Store records
|
|
162
|
-
this._records = data;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Get field by name
|
|
167
|
-
*/
|
|
168
|
-
getField(name: string): PivotCacheField | undefined {
|
|
169
|
-
return this._fields.find((f) => f.name === name);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Get field index by name
|
|
174
|
-
*/
|
|
175
|
-
getFieldIndex(name: string): number {
|
|
176
|
-
const field = this._fields.find((f) => f.name === name);
|
|
177
|
-
return field ? field.index : -1;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Generate the pivotCacheDefinition XML
|
|
182
|
-
*/
|
|
183
|
-
toDefinitionXml(recordsRelId: string): string {
|
|
184
|
-
const cacheFieldNodes: XmlNode[] = this._fields.map((field) => {
|
|
185
|
-
const sharedItemsAttrs: Record<string, string> = {};
|
|
186
|
-
const sharedItemChildren: XmlNode[] = [];
|
|
187
|
-
|
|
188
|
-
if (field.sharedItems.length > 0) {
|
|
189
|
-
// String field with shared items - Excel just uses count attribute
|
|
190
|
-
sharedItemsAttrs.count = String(field.sharedItems.length);
|
|
191
|
-
|
|
192
|
-
for (const item of field.sharedItems) {
|
|
193
|
-
sharedItemChildren.push(createElement('s', { v: item }, []));
|
|
194
|
-
}
|
|
195
|
-
} else if (field.isNumeric) {
|
|
196
|
-
// Numeric field - use "0"/"1" for boolean attributes as Excel expects
|
|
197
|
-
sharedItemsAttrs.containsSemiMixedTypes = '0';
|
|
198
|
-
sharedItemsAttrs.containsString = '0';
|
|
199
|
-
sharedItemsAttrs.containsNumber = '1';
|
|
200
|
-
// Check if all values are integers
|
|
201
|
-
if (field.minValue !== undefined && field.maxValue !== undefined) {
|
|
202
|
-
const isInteger = Number.isInteger(field.minValue) && Number.isInteger(field.maxValue);
|
|
203
|
-
if (isInteger) {
|
|
204
|
-
sharedItemsAttrs.containsInteger = '1';
|
|
205
|
-
}
|
|
206
|
-
sharedItemsAttrs.minValue = String(field.minValue);
|
|
207
|
-
sharedItemsAttrs.maxValue = String(field.maxValue);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
const sharedItemsNode = createElement('sharedItems', sharedItemsAttrs, sharedItemChildren);
|
|
212
|
-
return createElement('cacheField', { name: field.name, numFmtId: '0' }, [sharedItemsNode]);
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
const cacheFieldsNode = createElement('cacheFields', { count: String(this._fields.length) }, cacheFieldNodes);
|
|
216
|
-
|
|
217
|
-
const worksheetSourceNode = createElement(
|
|
218
|
-
'worksheetSource',
|
|
219
|
-
{ ref: this._sourceRange, sheet: this._sourceSheet },
|
|
220
|
-
[],
|
|
221
|
-
);
|
|
222
|
-
const cacheSourceNode = createElement('cacheSource', { type: 'worksheet' }, [worksheetSourceNode]);
|
|
223
|
-
|
|
224
|
-
// Build attributes - refreshOnLoad should come early per OOXML schema
|
|
225
|
-
const definitionAttrs: Record<string, string> = {
|
|
226
|
-
xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
|
|
227
|
-
'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',
|
|
228
|
-
'r:id': recordsRelId,
|
|
229
|
-
};
|
|
230
|
-
|
|
231
|
-
// Add refreshOnLoad early in attributes (default is true)
|
|
232
|
-
if (this._refreshOnLoad) {
|
|
233
|
-
definitionAttrs.refreshOnLoad = '1';
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// Continue with remaining attributes
|
|
237
|
-
definitionAttrs.refreshedBy = 'User';
|
|
238
|
-
definitionAttrs.refreshedVersion = '8';
|
|
239
|
-
definitionAttrs.minRefreshableVersion = '3';
|
|
240
|
-
definitionAttrs.createdVersion = '8';
|
|
241
|
-
definitionAttrs.recordCount = String(this._recordCount);
|
|
242
|
-
|
|
243
|
-
const definitionNode = createElement('pivotCacheDefinition', definitionAttrs, [cacheSourceNode, cacheFieldsNode]);
|
|
244
|
-
|
|
245
|
-
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([definitionNode])}`;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* Generate the pivotCacheRecords XML
|
|
250
|
-
*/
|
|
251
|
-
toRecordsXml(): string {
|
|
252
|
-
const recordNodes: XmlNode[] = [];
|
|
253
|
-
|
|
254
|
-
for (const row of this._records) {
|
|
255
|
-
const fieldNodes: XmlNode[] = [];
|
|
256
|
-
|
|
257
|
-
for (let colIdx = 0; colIdx < this._fields.length; colIdx++) {
|
|
258
|
-
const value = colIdx < row.length ? row[colIdx] : null;
|
|
259
|
-
|
|
260
|
-
if (value === null || value === undefined) {
|
|
261
|
-
// Missing value
|
|
262
|
-
fieldNodes.push(createElement('m', {}, []));
|
|
263
|
-
} else if (typeof value === 'string') {
|
|
264
|
-
// String value - use index into sharedItems via O(1) Map lookup
|
|
265
|
-
const indexMap = this._sharedItemsIndexMap.get(colIdx);
|
|
266
|
-
const idx = indexMap?.get(value);
|
|
267
|
-
if (idx !== undefined) {
|
|
268
|
-
fieldNodes.push(createElement('x', { v: String(idx) }, []));
|
|
269
|
-
} else {
|
|
270
|
-
// Direct string value (shouldn't happen if cache is built correctly)
|
|
271
|
-
fieldNodes.push(createElement('s', { v: value }, []));
|
|
272
|
-
}
|
|
273
|
-
} else if (typeof value === 'number') {
|
|
274
|
-
fieldNodes.push(createElement('n', { v: String(value) }, []));
|
|
275
|
-
} else if (typeof value === 'boolean') {
|
|
276
|
-
fieldNodes.push(createElement('b', { v: value ? '1' : '0' }, []));
|
|
277
|
-
} else if (value instanceof Date) {
|
|
278
|
-
fieldNodes.push(createElement('d', { v: value.toISOString() }, []));
|
|
279
|
-
} else {
|
|
280
|
-
// Unknown type, treat as missing
|
|
281
|
-
fieldNodes.push(createElement('m', {}, []));
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
recordNodes.push(createElement('r', {}, fieldNodes));
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
const recordsNode = createElement(
|
|
289
|
-
'pivotCacheRecords',
|
|
290
|
-
{
|
|
291
|
-
xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
|
|
292
|
-
'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',
|
|
293
|
-
count: String(this._recordCount),
|
|
294
|
-
},
|
|
295
|
-
recordNodes,
|
|
296
|
-
);
|
|
297
|
-
|
|
298
|
-
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([recordsNode])}`;
|
|
299
|
-
}
|
|
300
|
-
}
|
|
1
|
+
import type { PivotCacheField, CellValue } from './types';
|
|
2
|
+
import { createElement, stringifyXml, XmlNode } from './utils/xml';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Manages the pivot cache (definition and records) for a pivot table.
|
|
6
|
+
* The cache stores source data metadata and cached values.
|
|
7
|
+
*/
|
|
8
|
+
export class PivotCache {
|
|
9
|
+
private _cacheId: number;
|
|
10
|
+
private _fileIndex: number;
|
|
11
|
+
private _sourceSheet: string;
|
|
12
|
+
private _sourceRange: string;
|
|
13
|
+
private _fields: PivotCacheField[] = [];
|
|
14
|
+
private _records: CellValue[][] = [];
|
|
15
|
+
private _recordCount = 0;
|
|
16
|
+
private _refreshOnLoad = true; // Default to true
|
|
17
|
+
// Optimized lookup: Map<fieldIndex, Map<stringValue, sharedItemsIndex>>
|
|
18
|
+
private _sharedItemsIndexMap: Map<number, Map<string, number>> = new Map();
|
|
19
|
+
|
|
20
|
+
constructor(cacheId: number, sourceSheet: string, sourceRange: string, fileIndex: number) {
|
|
21
|
+
this._cacheId = cacheId;
|
|
22
|
+
this._fileIndex = fileIndex;
|
|
23
|
+
this._sourceSheet = sourceSheet;
|
|
24
|
+
this._sourceRange = sourceRange;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get the cache ID
|
|
29
|
+
*/
|
|
30
|
+
get cacheId(): number {
|
|
31
|
+
return this._cacheId;
|
|
32
|
+
}
|
|
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
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Set refreshOnLoad option
|
|
43
|
+
*/
|
|
44
|
+
set refreshOnLoad(value: boolean) {
|
|
45
|
+
this._refreshOnLoad = value;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get refreshOnLoad option
|
|
50
|
+
*/
|
|
51
|
+
get refreshOnLoad(): boolean {
|
|
52
|
+
return this._refreshOnLoad;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Get the source sheet name
|
|
57
|
+
*/
|
|
58
|
+
get sourceSheet(): string {
|
|
59
|
+
return this._sourceSheet;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Get the source range
|
|
64
|
+
*/
|
|
65
|
+
get sourceRange(): string {
|
|
66
|
+
return this._sourceRange;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get the full source reference (Sheet!Range)
|
|
71
|
+
*/
|
|
72
|
+
get sourceRef(): string {
|
|
73
|
+
return `${this._sourceSheet}!${this._sourceRange}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get the fields in this cache
|
|
78
|
+
*/
|
|
79
|
+
get fields(): PivotCacheField[] {
|
|
80
|
+
return this._fields;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Get the number of data records
|
|
85
|
+
*/
|
|
86
|
+
get recordCount(): number {
|
|
87
|
+
return this._recordCount;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Build the cache from source data.
|
|
92
|
+
* @param headers - Array of column header names
|
|
93
|
+
* @param data - 2D array of data rows (excluding headers)
|
|
94
|
+
*/
|
|
95
|
+
buildFromData(headers: string[], data: CellValue[][]): void {
|
|
96
|
+
this._recordCount = data.length;
|
|
97
|
+
|
|
98
|
+
// Initialize fields from headers
|
|
99
|
+
this._fields = headers.map((name, index) => ({
|
|
100
|
+
name,
|
|
101
|
+
index,
|
|
102
|
+
isNumeric: true,
|
|
103
|
+
isDate: false,
|
|
104
|
+
sharedItems: [],
|
|
105
|
+
minValue: undefined,
|
|
106
|
+
maxValue: undefined,
|
|
107
|
+
}));
|
|
108
|
+
|
|
109
|
+
// Use Sets for O(1) unique value collection during analysis
|
|
110
|
+
const sharedItemsSets: Set<string>[] = this._fields.map(() => new Set<string>());
|
|
111
|
+
|
|
112
|
+
// Analyze data to determine field types and collect unique values
|
|
113
|
+
for (const row of data) {
|
|
114
|
+
for (let colIdx = 0; colIdx < row.length && colIdx < this._fields.length; colIdx++) {
|
|
115
|
+
const value = row[colIdx];
|
|
116
|
+
const field = this._fields[colIdx];
|
|
117
|
+
|
|
118
|
+
if (value === null || value === undefined) {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (typeof value === 'string') {
|
|
123
|
+
field.isNumeric = false;
|
|
124
|
+
// O(1) Set.add instead of O(n) Array.includes + push
|
|
125
|
+
sharedItemsSets[colIdx].add(value);
|
|
126
|
+
} else if (typeof value === 'number') {
|
|
127
|
+
if (field.minValue === undefined || value < field.minValue) {
|
|
128
|
+
field.minValue = value;
|
|
129
|
+
}
|
|
130
|
+
if (field.maxValue === undefined || value > field.maxValue) {
|
|
131
|
+
field.maxValue = value;
|
|
132
|
+
}
|
|
133
|
+
} else if (value instanceof Date) {
|
|
134
|
+
field.isDate = true;
|
|
135
|
+
field.isNumeric = false;
|
|
136
|
+
} else if (typeof value === 'boolean') {
|
|
137
|
+
field.isNumeric = false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Convert Sets to arrays and build reverse index Maps for O(1) lookup during XML generation
|
|
143
|
+
this._sharedItemsIndexMap.clear();
|
|
144
|
+
for (let colIdx = 0; colIdx < this._fields.length; colIdx++) {
|
|
145
|
+
const field = this._fields[colIdx];
|
|
146
|
+
const set = sharedItemsSets[colIdx];
|
|
147
|
+
|
|
148
|
+
// Convert Set to array (maintains insertion order in ES6+)
|
|
149
|
+
field.sharedItems = Array.from(set);
|
|
150
|
+
|
|
151
|
+
// Build reverse lookup Map: value -> index
|
|
152
|
+
if (field.sharedItems.length > 0) {
|
|
153
|
+
const indexMap = new Map<string, number>();
|
|
154
|
+
for (let i = 0; i < field.sharedItems.length; i++) {
|
|
155
|
+
indexMap.set(field.sharedItems[i], i);
|
|
156
|
+
}
|
|
157
|
+
this._sharedItemsIndexMap.set(colIdx, indexMap);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Store records
|
|
162
|
+
this._records = data;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Get field by name
|
|
167
|
+
*/
|
|
168
|
+
getField(name: string): PivotCacheField | undefined {
|
|
169
|
+
return this._fields.find((f) => f.name === name);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Get field index by name
|
|
174
|
+
*/
|
|
175
|
+
getFieldIndex(name: string): number {
|
|
176
|
+
const field = this._fields.find((f) => f.name === name);
|
|
177
|
+
return field ? field.index : -1;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Generate the pivotCacheDefinition XML
|
|
182
|
+
*/
|
|
183
|
+
toDefinitionXml(recordsRelId: string): string {
|
|
184
|
+
const cacheFieldNodes: XmlNode[] = this._fields.map((field) => {
|
|
185
|
+
const sharedItemsAttrs: Record<string, string> = {};
|
|
186
|
+
const sharedItemChildren: XmlNode[] = [];
|
|
187
|
+
|
|
188
|
+
if (field.sharedItems.length > 0) {
|
|
189
|
+
// String field with shared items - Excel just uses count attribute
|
|
190
|
+
sharedItemsAttrs.count = String(field.sharedItems.length);
|
|
191
|
+
|
|
192
|
+
for (const item of field.sharedItems) {
|
|
193
|
+
sharedItemChildren.push(createElement('s', { v: item }, []));
|
|
194
|
+
}
|
|
195
|
+
} else if (field.isNumeric) {
|
|
196
|
+
// Numeric field - use "0"/"1" for boolean attributes as Excel expects
|
|
197
|
+
sharedItemsAttrs.containsSemiMixedTypes = '0';
|
|
198
|
+
sharedItemsAttrs.containsString = '0';
|
|
199
|
+
sharedItemsAttrs.containsNumber = '1';
|
|
200
|
+
// Check if all values are integers
|
|
201
|
+
if (field.minValue !== undefined && field.maxValue !== undefined) {
|
|
202
|
+
const isInteger = Number.isInteger(field.minValue) && Number.isInteger(field.maxValue);
|
|
203
|
+
if (isInteger) {
|
|
204
|
+
sharedItemsAttrs.containsInteger = '1';
|
|
205
|
+
}
|
|
206
|
+
sharedItemsAttrs.minValue = String(field.minValue);
|
|
207
|
+
sharedItemsAttrs.maxValue = String(field.maxValue);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const sharedItemsNode = createElement('sharedItems', sharedItemsAttrs, sharedItemChildren);
|
|
212
|
+
return createElement('cacheField', { name: field.name, numFmtId: '0' }, [sharedItemsNode]);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const cacheFieldsNode = createElement('cacheFields', { count: String(this._fields.length) }, cacheFieldNodes);
|
|
216
|
+
|
|
217
|
+
const worksheetSourceNode = createElement(
|
|
218
|
+
'worksheetSource',
|
|
219
|
+
{ ref: this._sourceRange, sheet: this._sourceSheet },
|
|
220
|
+
[],
|
|
221
|
+
);
|
|
222
|
+
const cacheSourceNode = createElement('cacheSource', { type: 'worksheet' }, [worksheetSourceNode]);
|
|
223
|
+
|
|
224
|
+
// Build attributes - refreshOnLoad should come early per OOXML schema
|
|
225
|
+
const definitionAttrs: Record<string, string> = {
|
|
226
|
+
xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
|
|
227
|
+
'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',
|
|
228
|
+
'r:id': recordsRelId,
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
// Add refreshOnLoad early in attributes (default is true)
|
|
232
|
+
if (this._refreshOnLoad) {
|
|
233
|
+
definitionAttrs.refreshOnLoad = '1';
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Continue with remaining attributes
|
|
237
|
+
definitionAttrs.refreshedBy = 'User';
|
|
238
|
+
definitionAttrs.refreshedVersion = '8';
|
|
239
|
+
definitionAttrs.minRefreshableVersion = '3';
|
|
240
|
+
definitionAttrs.createdVersion = '8';
|
|
241
|
+
definitionAttrs.recordCount = String(this._recordCount);
|
|
242
|
+
|
|
243
|
+
const definitionNode = createElement('pivotCacheDefinition', definitionAttrs, [cacheSourceNode, cacheFieldsNode]);
|
|
244
|
+
|
|
245
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([definitionNode])}`;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Generate the pivotCacheRecords XML
|
|
250
|
+
*/
|
|
251
|
+
toRecordsXml(): string {
|
|
252
|
+
const recordNodes: XmlNode[] = [];
|
|
253
|
+
|
|
254
|
+
for (const row of this._records) {
|
|
255
|
+
const fieldNodes: XmlNode[] = [];
|
|
256
|
+
|
|
257
|
+
for (let colIdx = 0; colIdx < this._fields.length; colIdx++) {
|
|
258
|
+
const value = colIdx < row.length ? row[colIdx] : null;
|
|
259
|
+
|
|
260
|
+
if (value === null || value === undefined) {
|
|
261
|
+
// Missing value
|
|
262
|
+
fieldNodes.push(createElement('m', {}, []));
|
|
263
|
+
} else if (typeof value === 'string') {
|
|
264
|
+
// String value - use index into sharedItems via O(1) Map lookup
|
|
265
|
+
const indexMap = this._sharedItemsIndexMap.get(colIdx);
|
|
266
|
+
const idx = indexMap?.get(value);
|
|
267
|
+
if (idx !== undefined) {
|
|
268
|
+
fieldNodes.push(createElement('x', { v: String(idx) }, []));
|
|
269
|
+
} else {
|
|
270
|
+
// Direct string value (shouldn't happen if cache is built correctly)
|
|
271
|
+
fieldNodes.push(createElement('s', { v: value }, []));
|
|
272
|
+
}
|
|
273
|
+
} else if (typeof value === 'number') {
|
|
274
|
+
fieldNodes.push(createElement('n', { v: String(value) }, []));
|
|
275
|
+
} else if (typeof value === 'boolean') {
|
|
276
|
+
fieldNodes.push(createElement('b', { v: value ? '1' : '0' }, []));
|
|
277
|
+
} else if (value instanceof Date) {
|
|
278
|
+
fieldNodes.push(createElement('d', { v: value.toISOString() }, []));
|
|
279
|
+
} else {
|
|
280
|
+
// Unknown type, treat as missing
|
|
281
|
+
fieldNodes.push(createElement('m', {}, []));
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
recordNodes.push(createElement('r', {}, fieldNodes));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const recordsNode = createElement(
|
|
289
|
+
'pivotCacheRecords',
|
|
290
|
+
{
|
|
291
|
+
xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
|
|
292
|
+
'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',
|
|
293
|
+
count: String(this._recordCount),
|
|
294
|
+
},
|
|
295
|
+
recordNodes,
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([recordsNode])}`;
|
|
299
|
+
}
|
|
300
|
+
}
|