@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
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@niicojs/excel",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "typescript library to manipulate excel files",
|
|
5
|
+
"homepage": "https://github.com/niicojs/excel#readme",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/niicojs/excel/issues"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/niicojs/excel.git"
|
|
12
|
+
},
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"author": "niico",
|
|
15
|
+
"type": "module",
|
|
16
|
+
"main": "./dist/index.cjs",
|
|
17
|
+
"module": "./dist/index.js",
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"import": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"default": "./dist/index.js"
|
|
24
|
+
},
|
|
25
|
+
"require": {
|
|
26
|
+
"types": "./dist/index.d.cts",
|
|
27
|
+
"default": "./dist/index.cjs"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"./package.json": "./package.json"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"src"
|
|
35
|
+
],
|
|
36
|
+
"scripts": {
|
|
37
|
+
"dev": "tsdx dev",
|
|
38
|
+
"build": "tsdx build",
|
|
39
|
+
"test": "tsdx test",
|
|
40
|
+
"test:watch": "tsdx test --watch",
|
|
41
|
+
"lint": "tsdx lint",
|
|
42
|
+
"format": "tsdx format",
|
|
43
|
+
"format:check": "tsdx format --check",
|
|
44
|
+
"typecheck": "tsdx typecheck",
|
|
45
|
+
"prepublishOnly": "bun run build"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"fast-xml-parser": "^5.3.3",
|
|
49
|
+
"fflate": "^0.8.2"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/node": "^25.0.10",
|
|
53
|
+
"bunchee": "^6.9.4",
|
|
54
|
+
"tsdx": "latest",
|
|
55
|
+
"typescript": "^5.7.2",
|
|
56
|
+
"vitest": "^4.0.18"
|
|
57
|
+
},
|
|
58
|
+
"engines": {
|
|
59
|
+
"node": ">=20"
|
|
60
|
+
}
|
|
61
|
+
}
|
package/src/cell.ts
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import type { CellValue, CellType, CellStyle, CellData, ErrorType } from './types';
|
|
2
|
+
import type { Worksheet } from './worksheet';
|
|
3
|
+
import { parseAddress, toAddress } from './utils/address';
|
|
4
|
+
|
|
5
|
+
// Excel epoch: December 30, 1899 (accounting for the 1900 leap year bug)
|
|
6
|
+
const EXCEL_EPOCH = new Date(Date.UTC(1899, 11, 30));
|
|
7
|
+
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
8
|
+
|
|
9
|
+
// Excel error types
|
|
10
|
+
const ERROR_TYPES: Set<string> = new Set([
|
|
11
|
+
'#NULL!',
|
|
12
|
+
'#DIV/0!',
|
|
13
|
+
'#VALUE!',
|
|
14
|
+
'#REF!',
|
|
15
|
+
'#NAME?',
|
|
16
|
+
'#NUM!',
|
|
17
|
+
'#N/A',
|
|
18
|
+
'#GETTING_DATA',
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Represents a single cell in a worksheet
|
|
23
|
+
*/
|
|
24
|
+
export class Cell {
|
|
25
|
+
private _row: number;
|
|
26
|
+
private _col: number;
|
|
27
|
+
private _data: CellData;
|
|
28
|
+
private _worksheet: Worksheet;
|
|
29
|
+
private _dirty = false;
|
|
30
|
+
|
|
31
|
+
constructor(worksheet: Worksheet, row: number, col: number, data?: CellData) {
|
|
32
|
+
this._worksheet = worksheet;
|
|
33
|
+
this._row = row;
|
|
34
|
+
this._col = col;
|
|
35
|
+
this._data = data || {};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get the cell address (e.g., 'A1')
|
|
40
|
+
*/
|
|
41
|
+
get address(): string {
|
|
42
|
+
return toAddress(this._row, this._col);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get the 0-based row index
|
|
47
|
+
*/
|
|
48
|
+
get row(): number {
|
|
49
|
+
return this._row;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get the 0-based column index
|
|
54
|
+
*/
|
|
55
|
+
get col(): number {
|
|
56
|
+
return this._col;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get the cell type
|
|
61
|
+
*/
|
|
62
|
+
get type(): CellType {
|
|
63
|
+
const t = this._data.t;
|
|
64
|
+
if (!t && this._data.v === undefined && !this._data.f) {
|
|
65
|
+
return 'empty';
|
|
66
|
+
}
|
|
67
|
+
switch (t) {
|
|
68
|
+
case 'n':
|
|
69
|
+
return 'number';
|
|
70
|
+
case 's':
|
|
71
|
+
case 'str':
|
|
72
|
+
return 'string';
|
|
73
|
+
case 'b':
|
|
74
|
+
return 'boolean';
|
|
75
|
+
case 'e':
|
|
76
|
+
return 'error';
|
|
77
|
+
case 'd':
|
|
78
|
+
return 'date';
|
|
79
|
+
default:
|
|
80
|
+
// If no type but has value, infer from value
|
|
81
|
+
if (typeof this._data.v === 'number') return 'number';
|
|
82
|
+
if (typeof this._data.v === 'string') return 'string';
|
|
83
|
+
if (typeof this._data.v === 'boolean') return 'boolean';
|
|
84
|
+
return 'empty';
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Get the cell value
|
|
90
|
+
*/
|
|
91
|
+
get value(): CellValue {
|
|
92
|
+
const t = this._data.t;
|
|
93
|
+
const v = this._data.v;
|
|
94
|
+
|
|
95
|
+
if (v === undefined && !this._data.f) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
switch (t) {
|
|
100
|
+
case 'n':
|
|
101
|
+
return typeof v === 'number' ? v : parseFloat(String(v));
|
|
102
|
+
case 's':
|
|
103
|
+
// Shared string reference
|
|
104
|
+
if (typeof v === 'number') {
|
|
105
|
+
return this._worksheet.workbook.sharedStrings.getString(v) ?? '';
|
|
106
|
+
}
|
|
107
|
+
return String(v);
|
|
108
|
+
case 'str':
|
|
109
|
+
// Inline string
|
|
110
|
+
return String(v);
|
|
111
|
+
case 'b':
|
|
112
|
+
return v === 1 || v === '1' || v === true;
|
|
113
|
+
case 'e':
|
|
114
|
+
return { error: String(v) as ErrorType };
|
|
115
|
+
case 'd':
|
|
116
|
+
// ISO 8601 date string
|
|
117
|
+
return new Date(String(v));
|
|
118
|
+
default:
|
|
119
|
+
// No type specified - try to infer
|
|
120
|
+
if (typeof v === 'number') {
|
|
121
|
+
// Check if this might be a date based on number format
|
|
122
|
+
if (this._isDateFormat()) {
|
|
123
|
+
return this._excelDateToJs(v);
|
|
124
|
+
}
|
|
125
|
+
return v;
|
|
126
|
+
}
|
|
127
|
+
if (typeof v === 'string') {
|
|
128
|
+
if (ERROR_TYPES.has(v)) {
|
|
129
|
+
return { error: v as ErrorType };
|
|
130
|
+
}
|
|
131
|
+
return v;
|
|
132
|
+
}
|
|
133
|
+
if (typeof v === 'boolean') return v;
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Set the cell value
|
|
140
|
+
*/
|
|
141
|
+
set value(val: CellValue) {
|
|
142
|
+
this._dirty = true;
|
|
143
|
+
|
|
144
|
+
if (val === null || val === undefined) {
|
|
145
|
+
this._data.v = undefined;
|
|
146
|
+
this._data.t = undefined;
|
|
147
|
+
this._data.f = undefined;
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (typeof val === 'number') {
|
|
152
|
+
this._data.v = val;
|
|
153
|
+
this._data.t = 'n';
|
|
154
|
+
} else if (typeof val === 'string') {
|
|
155
|
+
// Store as shared string
|
|
156
|
+
const index = this._worksheet.workbook.sharedStrings.addString(val);
|
|
157
|
+
this._data.v = index;
|
|
158
|
+
this._data.t = 's';
|
|
159
|
+
} else if (typeof val === 'boolean') {
|
|
160
|
+
this._data.v = val ? 1 : 0;
|
|
161
|
+
this._data.t = 'b';
|
|
162
|
+
} else if (val instanceof Date) {
|
|
163
|
+
// Store as ISO date string with 'd' type
|
|
164
|
+
this._data.v = val.toISOString();
|
|
165
|
+
this._data.t = 'd';
|
|
166
|
+
} else if ('error' in val) {
|
|
167
|
+
this._data.v = val.error;
|
|
168
|
+
this._data.t = 'e';
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Clear formula when setting value directly
|
|
172
|
+
this._data.f = undefined;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Write a 2D array of values starting at this cell
|
|
177
|
+
*/
|
|
178
|
+
set values(data: CellValue[][]) {
|
|
179
|
+
for (let r = 0; r < data.length; r++) {
|
|
180
|
+
const row = data[r];
|
|
181
|
+
for (let c = 0; c < row.length; c++) {
|
|
182
|
+
const cell = this._worksheet.cell(this._row + r, this._col + c);
|
|
183
|
+
cell.value = row[c];
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Get the formula (without leading '=')
|
|
190
|
+
*/
|
|
191
|
+
get formula(): string | undefined {
|
|
192
|
+
return this._data.f;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Set the formula (without leading '=')
|
|
197
|
+
*/
|
|
198
|
+
set formula(f: string | undefined) {
|
|
199
|
+
this._dirty = true;
|
|
200
|
+
if (f === undefined) {
|
|
201
|
+
this._data.f = undefined;
|
|
202
|
+
} else {
|
|
203
|
+
// Remove leading '=' if present
|
|
204
|
+
this._data.f = f.startsWith('=') ? f.slice(1) : f;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Get the formatted text (as displayed in Excel)
|
|
210
|
+
*/
|
|
211
|
+
get text(): string {
|
|
212
|
+
if (this._data.w) {
|
|
213
|
+
return this._data.w;
|
|
214
|
+
}
|
|
215
|
+
const val = this.value;
|
|
216
|
+
if (val === null) return '';
|
|
217
|
+
if (typeof val === 'object' && 'error' in val) return val.error;
|
|
218
|
+
if (val instanceof Date) return val.toISOString().split('T')[0];
|
|
219
|
+
return String(val);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Get the style index
|
|
224
|
+
*/
|
|
225
|
+
get styleIndex(): number | undefined {
|
|
226
|
+
return this._data.s;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Set the style index
|
|
231
|
+
*/
|
|
232
|
+
set styleIndex(index: number | undefined) {
|
|
233
|
+
this._dirty = true;
|
|
234
|
+
this._data.s = index;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Get the cell style
|
|
239
|
+
*/
|
|
240
|
+
get style(): CellStyle {
|
|
241
|
+
if (this._data.s === undefined) {
|
|
242
|
+
return {};
|
|
243
|
+
}
|
|
244
|
+
return this._worksheet.workbook.styles.getStyle(this._data.s);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Set the cell style (merges with existing)
|
|
249
|
+
*/
|
|
250
|
+
set style(style: CellStyle) {
|
|
251
|
+
this._dirty = true;
|
|
252
|
+
const currentStyle = this.style;
|
|
253
|
+
const merged = { ...currentStyle, ...style };
|
|
254
|
+
this._data.s = this._worksheet.workbook.styles.createStyle(merged);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Check if cell has been modified
|
|
259
|
+
*/
|
|
260
|
+
get dirty(): boolean {
|
|
261
|
+
return this._dirty;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Get internal cell data
|
|
266
|
+
*/
|
|
267
|
+
get data(): CellData {
|
|
268
|
+
return this._data;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Check if this cell has a date number format
|
|
273
|
+
*/
|
|
274
|
+
private _isDateFormat(): boolean {
|
|
275
|
+
// TODO: Check actual number format from styles
|
|
276
|
+
// For now, return false - dates should be explicitly typed
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Convert Excel serial date to JavaScript Date
|
|
282
|
+
* Used when reading dates stored as numbers with date formats
|
|
283
|
+
*/
|
|
284
|
+
_excelDateToJs(serial: number): Date {
|
|
285
|
+
// Excel incorrectly considers 1900 a leap year
|
|
286
|
+
// Dates after Feb 28, 1900 need adjustment
|
|
287
|
+
const adjusted = serial > 60 ? serial - 1 : serial;
|
|
288
|
+
const ms = Math.round((adjusted - 1) * MS_PER_DAY);
|
|
289
|
+
return new Date(EXCEL_EPOCH.getTime() + ms);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Convert JavaScript Date to Excel serial date
|
|
294
|
+
* Used when writing dates as numbers for Excel compatibility
|
|
295
|
+
*/
|
|
296
|
+
_jsDateToExcel(date: Date): number {
|
|
297
|
+
const ms = date.getTime() - EXCEL_EPOCH.getTime();
|
|
298
|
+
let serial = ms / MS_PER_DAY + 1;
|
|
299
|
+
// Account for Excel's 1900 leap year bug
|
|
300
|
+
if (serial > 60) {
|
|
301
|
+
serial += 1;
|
|
302
|
+
}
|
|
303
|
+
return serial;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Parse a cell address or row/col to get row and col indices
|
|
309
|
+
*/
|
|
310
|
+
export const parseCellRef = (rowOrAddress: number | string, col?: number): { row: number; col: number } => {
|
|
311
|
+
if (typeof rowOrAddress === 'string') {
|
|
312
|
+
return parseAddress(rowOrAddress);
|
|
313
|
+
}
|
|
314
|
+
if (col === undefined) {
|
|
315
|
+
throw new Error('Column must be provided when row is a number');
|
|
316
|
+
}
|
|
317
|
+
return { row: rowOrAddress, col };
|
|
318
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Main exports
|
|
2
|
+
export { Workbook } from './workbook';
|
|
3
|
+
export { Worksheet } from './worksheet';
|
|
4
|
+
export { Cell } from './cell';
|
|
5
|
+
export { Range } from './range';
|
|
6
|
+
export { SharedStrings } from './shared-strings';
|
|
7
|
+
export { Styles } from './styles';
|
|
8
|
+
export { PivotTable } from './pivot-table';
|
|
9
|
+
export { PivotCache } from './pivot-cache';
|
|
10
|
+
|
|
11
|
+
// Type exports
|
|
12
|
+
export type {
|
|
13
|
+
CellValue,
|
|
14
|
+
CellType,
|
|
15
|
+
CellStyle,
|
|
16
|
+
CellError,
|
|
17
|
+
ErrorType,
|
|
18
|
+
CellAddress,
|
|
19
|
+
RangeAddress,
|
|
20
|
+
BorderStyle,
|
|
21
|
+
BorderType,
|
|
22
|
+
Alignment,
|
|
23
|
+
// Pivot table types
|
|
24
|
+
PivotTableConfig,
|
|
25
|
+
PivotValueConfig,
|
|
26
|
+
AggregationType,
|
|
27
|
+
PivotFieldAxis,
|
|
28
|
+
} from './types';
|
|
29
|
+
|
|
30
|
+
// Utility exports
|
|
31
|
+
export { parseAddress, toAddress, parseRange, toRange } from './utils/address';
|
|
@@ -0,0 +1,268 @@
|
|
|
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 _sourceSheet: string;
|
|
11
|
+
private _sourceRange: string;
|
|
12
|
+
private _fields: PivotCacheField[] = [];
|
|
13
|
+
private _records: CellValue[][] = [];
|
|
14
|
+
private _recordCount = 0;
|
|
15
|
+
private _refreshOnLoad = true; // Default to true
|
|
16
|
+
|
|
17
|
+
constructor(cacheId: number, sourceSheet: string, sourceRange: string) {
|
|
18
|
+
this._cacheId = cacheId;
|
|
19
|
+
this._sourceSheet = sourceSheet;
|
|
20
|
+
this._sourceRange = sourceRange;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get the cache ID
|
|
25
|
+
*/
|
|
26
|
+
get cacheId(): number {
|
|
27
|
+
return this._cacheId;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Set refreshOnLoad option
|
|
32
|
+
*/
|
|
33
|
+
set refreshOnLoad(value: boolean) {
|
|
34
|
+
this._refreshOnLoad = value;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Get refreshOnLoad option
|
|
39
|
+
*/
|
|
40
|
+
get refreshOnLoad(): boolean {
|
|
41
|
+
return this._refreshOnLoad;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Get the source sheet name
|
|
46
|
+
*/
|
|
47
|
+
get sourceSheet(): string {
|
|
48
|
+
return this._sourceSheet;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get the source range
|
|
53
|
+
*/
|
|
54
|
+
get sourceRange(): string {
|
|
55
|
+
return this._sourceRange;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get the full source reference (Sheet!Range)
|
|
60
|
+
*/
|
|
61
|
+
get sourceRef(): string {
|
|
62
|
+
return `${this._sourceSheet}!${this._sourceRange}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get the fields in this cache
|
|
67
|
+
*/
|
|
68
|
+
get fields(): PivotCacheField[] {
|
|
69
|
+
return this._fields;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Get the number of data records
|
|
74
|
+
*/
|
|
75
|
+
get recordCount(): number {
|
|
76
|
+
return this._recordCount;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Build the cache from source data.
|
|
81
|
+
* @param headers - Array of column header names
|
|
82
|
+
* @param data - 2D array of data rows (excluding headers)
|
|
83
|
+
*/
|
|
84
|
+
buildFromData(headers: string[], data: CellValue[][]): void {
|
|
85
|
+
this._recordCount = data.length;
|
|
86
|
+
|
|
87
|
+
// Initialize fields from headers
|
|
88
|
+
this._fields = headers.map((name, index) => ({
|
|
89
|
+
name,
|
|
90
|
+
index,
|
|
91
|
+
isNumeric: true,
|
|
92
|
+
isDate: false,
|
|
93
|
+
sharedItems: [],
|
|
94
|
+
minValue: undefined,
|
|
95
|
+
maxValue: undefined,
|
|
96
|
+
}));
|
|
97
|
+
|
|
98
|
+
// Analyze data to determine field types and collect unique values
|
|
99
|
+
for (const row of data) {
|
|
100
|
+
for (let colIdx = 0; colIdx < row.length && colIdx < this._fields.length; colIdx++) {
|
|
101
|
+
const value = row[colIdx];
|
|
102
|
+
const field = this._fields[colIdx];
|
|
103
|
+
|
|
104
|
+
if (value === null || value === undefined) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (typeof value === 'string') {
|
|
109
|
+
field.isNumeric = false;
|
|
110
|
+
if (!field.sharedItems.includes(value)) {
|
|
111
|
+
field.sharedItems.push(value);
|
|
112
|
+
}
|
|
113
|
+
} else if (typeof value === 'number') {
|
|
114
|
+
if (field.minValue === undefined || value < field.minValue) {
|
|
115
|
+
field.minValue = value;
|
|
116
|
+
}
|
|
117
|
+
if (field.maxValue === undefined || value > field.maxValue) {
|
|
118
|
+
field.maxValue = value;
|
|
119
|
+
}
|
|
120
|
+
} else if (value instanceof Date) {
|
|
121
|
+
field.isDate = true;
|
|
122
|
+
field.isNumeric = false;
|
|
123
|
+
} else if (typeof value === 'boolean') {
|
|
124
|
+
field.isNumeric = false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Store records
|
|
130
|
+
this._records = data;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Get field by name
|
|
135
|
+
*/
|
|
136
|
+
getField(name: string): PivotCacheField | undefined {
|
|
137
|
+
return this._fields.find((f) => f.name === name);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Get field index by name
|
|
142
|
+
*/
|
|
143
|
+
getFieldIndex(name: string): number {
|
|
144
|
+
const field = this._fields.find((f) => f.name === name);
|
|
145
|
+
return field ? field.index : -1;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Generate the pivotCacheDefinition XML
|
|
150
|
+
*/
|
|
151
|
+
toDefinitionXml(recordsRelId: string): string {
|
|
152
|
+
const cacheFieldNodes: XmlNode[] = this._fields.map((field) => {
|
|
153
|
+
const sharedItemsAttrs: Record<string, string> = {};
|
|
154
|
+
const sharedItemChildren: XmlNode[] = [];
|
|
155
|
+
|
|
156
|
+
if (field.sharedItems.length > 0) {
|
|
157
|
+
// String field with shared items - Excel just uses count attribute
|
|
158
|
+
sharedItemsAttrs.count = String(field.sharedItems.length);
|
|
159
|
+
|
|
160
|
+
for (const item of field.sharedItems) {
|
|
161
|
+
sharedItemChildren.push(createElement('s', { v: item }, []));
|
|
162
|
+
}
|
|
163
|
+
} else if (field.isNumeric) {
|
|
164
|
+
// Numeric field - use "0"/"1" for boolean attributes as Excel expects
|
|
165
|
+
sharedItemsAttrs.containsSemiMixedTypes = '0';
|
|
166
|
+
sharedItemsAttrs.containsString = '0';
|
|
167
|
+
sharedItemsAttrs.containsNumber = '1';
|
|
168
|
+
// Check if all values are integers
|
|
169
|
+
if (field.minValue !== undefined && field.maxValue !== undefined) {
|
|
170
|
+
const isInteger = Number.isInteger(field.minValue) && Number.isInteger(field.maxValue);
|
|
171
|
+
if (isInteger) {
|
|
172
|
+
sharedItemsAttrs.containsInteger = '1';
|
|
173
|
+
}
|
|
174
|
+
sharedItemsAttrs.minValue = String(field.minValue);
|
|
175
|
+
sharedItemsAttrs.maxValue = String(field.maxValue);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const sharedItemsNode = createElement('sharedItems', sharedItemsAttrs, sharedItemChildren);
|
|
180
|
+
return createElement('cacheField', { name: field.name, numFmtId: '0' }, [sharedItemsNode]);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const cacheFieldsNode = createElement('cacheFields', { count: String(this._fields.length) }, cacheFieldNodes);
|
|
184
|
+
|
|
185
|
+
const worksheetSourceNode = createElement(
|
|
186
|
+
'worksheetSource',
|
|
187
|
+
{ ref: this._sourceRange, sheet: this._sourceSheet },
|
|
188
|
+
[],
|
|
189
|
+
);
|
|
190
|
+
const cacheSourceNode = createElement('cacheSource', { type: 'worksheet' }, [worksheetSourceNode]);
|
|
191
|
+
|
|
192
|
+
// Build attributes - refreshOnLoad should come early per OOXML schema
|
|
193
|
+
const definitionAttrs: Record<string, string> = {
|
|
194
|
+
xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
|
|
195
|
+
'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',
|
|
196
|
+
'r:id': recordsRelId,
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
// Add refreshOnLoad early in attributes (default is true)
|
|
200
|
+
if (this._refreshOnLoad) {
|
|
201
|
+
definitionAttrs.refreshOnLoad = '1';
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Continue with remaining attributes
|
|
205
|
+
definitionAttrs.refreshedBy = 'User';
|
|
206
|
+
definitionAttrs.refreshedVersion = '8';
|
|
207
|
+
definitionAttrs.minRefreshableVersion = '3';
|
|
208
|
+
definitionAttrs.createdVersion = '8';
|
|
209
|
+
definitionAttrs.recordCount = String(this._recordCount);
|
|
210
|
+
|
|
211
|
+
const definitionNode = createElement('pivotCacheDefinition', definitionAttrs, [cacheSourceNode, cacheFieldsNode]);
|
|
212
|
+
|
|
213
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([definitionNode])}`;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Generate the pivotCacheRecords XML
|
|
218
|
+
*/
|
|
219
|
+
toRecordsXml(): string {
|
|
220
|
+
const recordNodes: XmlNode[] = [];
|
|
221
|
+
|
|
222
|
+
for (const row of this._records) {
|
|
223
|
+
const fieldNodes: XmlNode[] = [];
|
|
224
|
+
|
|
225
|
+
for (let colIdx = 0; colIdx < this._fields.length; colIdx++) {
|
|
226
|
+
const field = this._fields[colIdx];
|
|
227
|
+
const value = colIdx < row.length ? row[colIdx] : null;
|
|
228
|
+
|
|
229
|
+
if (value === null || value === undefined) {
|
|
230
|
+
// Missing value
|
|
231
|
+
fieldNodes.push(createElement('m', {}, []));
|
|
232
|
+
} else if (typeof value === 'string') {
|
|
233
|
+
// String value - use index into sharedItems
|
|
234
|
+
const idx = field.sharedItems.indexOf(value);
|
|
235
|
+
if (idx >= 0) {
|
|
236
|
+
fieldNodes.push(createElement('x', { v: String(idx) }, []));
|
|
237
|
+
} else {
|
|
238
|
+
// Direct string value (shouldn't happen if cache is built correctly)
|
|
239
|
+
fieldNodes.push(createElement('s', { v: value }, []));
|
|
240
|
+
}
|
|
241
|
+
} else if (typeof value === 'number') {
|
|
242
|
+
fieldNodes.push(createElement('n', { v: String(value) }, []));
|
|
243
|
+
} else if (typeof value === 'boolean') {
|
|
244
|
+
fieldNodes.push(createElement('b', { v: value ? '1' : '0' }, []));
|
|
245
|
+
} else if (value instanceof Date) {
|
|
246
|
+
fieldNodes.push(createElement('d', { v: value.toISOString() }, []));
|
|
247
|
+
} else {
|
|
248
|
+
// Unknown type, treat as missing
|
|
249
|
+
fieldNodes.push(createElement('m', {}, []));
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
recordNodes.push(createElement('r', {}, fieldNodes));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const recordsNode = createElement(
|
|
257
|
+
'pivotCacheRecords',
|
|
258
|
+
{
|
|
259
|
+
xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
|
|
260
|
+
'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',
|
|
261
|
+
count: String(this._recordCount),
|
|
262
|
+
},
|
|
263
|
+
recordNodes,
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([recordsNode])}`;
|
|
267
|
+
}
|
|
268
|
+
}
|