@cj-tech-master/excelts 2.0.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser/excelts.esm.js +78 -17
- package/dist/browser/excelts.esm.js.map +1 -1
- package/dist/browser/excelts.esm.min.js +34 -31
- package/dist/browser/excelts.iife.js +78 -17
- package/dist/browser/excelts.iife.js.map +1 -1
- package/dist/browser/excelts.iife.min.js +32 -29
- package/dist/cjs/utils/datetime.js +7 -156
- package/dist/cjs/xlsx/xform/pivot-table/pivot-cache-definition-xform.js +3 -1
- package/dist/cjs/xlsx/xform/pivot-table/pivot-table-xform.js +91 -6
- package/dist/esm/utils/datetime.js +7 -153
- package/dist/esm/xlsx/xform/pivot-table/pivot-cache-definition-xform.js +3 -1
- package/dist/esm/xlsx/xform/pivot-table/pivot-table-xform.js +91 -6
- package/dist/types/csv/csv-core.d.ts +0 -6
- package/dist/types/stream/xlsx/workbook-reader.d.ts +1 -1
- package/dist/types/stream/xlsx/worksheet-reader.d.ts +1 -1
- package/dist/types/utils/datetime.d.ts +0 -29
- package/package.json +1 -1
|
@@ -7,9 +7,6 @@
|
|
|
7
7
|
*/
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
9
|
exports.DateFormatter = exports.DateParser = void 0;
|
|
10
|
-
exports.mightBeDate = mightBeDate;
|
|
11
|
-
exports.parseDate = parseDate;
|
|
12
|
-
exports.formatDate = formatDate;
|
|
13
10
|
exports.getSupportedFormats = getSupportedFormats;
|
|
14
11
|
// ============================================================================
|
|
15
12
|
// Constants - Pre-computed lookup tables for zero-allocation operations
|
|
@@ -18,7 +15,6 @@ exports.getSupportedFormats = getSupportedFormats;
|
|
|
18
15
|
const PAD2 = Array.from({ length: 60 }, (_, i) => (i < 10 ? `0${i}` : `${i}`));
|
|
19
16
|
// Character codes for fast comparison
|
|
20
17
|
const C_0 = 48;
|
|
21
|
-
const C_9 = 57;
|
|
22
18
|
const C_DASH = 45;
|
|
23
19
|
const C_SLASH = 47;
|
|
24
20
|
const C_COLON = 58;
|
|
@@ -194,158 +190,6 @@ const AUTO_DETECT = [
|
|
|
194
190
|
[29, [parseISOMsOffset]]
|
|
195
191
|
];
|
|
196
192
|
// ============================================================================
|
|
197
|
-
// Public API
|
|
198
|
-
// ============================================================================
|
|
199
|
-
/**
|
|
200
|
-
* Quick pre-filter to reject non-date strings
|
|
201
|
-
*/
|
|
202
|
-
function mightBeDate(s) {
|
|
203
|
-
const len = s.length;
|
|
204
|
-
if (len < 10 || len > 30) {
|
|
205
|
-
return false;
|
|
206
|
-
}
|
|
207
|
-
const c0 = s.charCodeAt(0);
|
|
208
|
-
const c1 = s.charCodeAt(1);
|
|
209
|
-
// Must start with digits
|
|
210
|
-
if (c0 < C_0 || c0 > C_9 || c1 < C_0 || c1 > C_9) {
|
|
211
|
-
return false;
|
|
212
|
-
}
|
|
213
|
-
const c2 = s.charCodeAt(2);
|
|
214
|
-
// Either YYYY format (4 digits) or MM/DD format (separator at pos 2)
|
|
215
|
-
return (c2 >= C_0 && c2 <= C_9) || c2 === C_DASH || c2 === C_SLASH;
|
|
216
|
-
}
|
|
217
|
-
/**
|
|
218
|
-
* Parse a date string
|
|
219
|
-
*
|
|
220
|
-
* @param value - String to parse
|
|
221
|
-
* @param formats - Specific formats to try (optional, auto-detects ISO if omitted)
|
|
222
|
-
* @returns Date object or null
|
|
223
|
-
*
|
|
224
|
-
* @example
|
|
225
|
-
* parseDate("2024-12-26") // auto-detect ISO
|
|
226
|
-
* parseDate("12-26-2024", ["MM-DD-YYYY"]) // explicit US format
|
|
227
|
-
*/
|
|
228
|
-
function parseDate(value, formats) {
|
|
229
|
-
if (!value || typeof value !== "string") {
|
|
230
|
-
return null;
|
|
231
|
-
}
|
|
232
|
-
const s = value.trim();
|
|
233
|
-
if (!s) {
|
|
234
|
-
return null;
|
|
235
|
-
}
|
|
236
|
-
// Explicit formats
|
|
237
|
-
if (formats?.length) {
|
|
238
|
-
for (const fmt of formats) {
|
|
239
|
-
const result = PARSERS[fmt]?.(s);
|
|
240
|
-
if (result) {
|
|
241
|
-
return result;
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
return null;
|
|
245
|
-
}
|
|
246
|
-
// Auto-detect by length
|
|
247
|
-
const len = s.length;
|
|
248
|
-
for (const [l, parsers] of AUTO_DETECT) {
|
|
249
|
-
if (len === l) {
|
|
250
|
-
for (const p of parsers) {
|
|
251
|
-
const result = p(s);
|
|
252
|
-
if (result) {
|
|
253
|
-
return result;
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
return null;
|
|
259
|
-
}
|
|
260
|
-
/**
|
|
261
|
-
* Format a Date to string
|
|
262
|
-
*
|
|
263
|
-
* @param date - Date to format
|
|
264
|
-
* @param format - Format template (optional, defaults to ISO)
|
|
265
|
-
* @param utc - Use UTC time (default: false)
|
|
266
|
-
* @returns Formatted string or empty string if invalid
|
|
267
|
-
*
|
|
268
|
-
* @example
|
|
269
|
-
* formatDate(date) // "2024-12-26T10:30:00.000+08:00"
|
|
270
|
-
* formatDate(date, "YYYY-MM-DD", true) // "2024-12-26"
|
|
271
|
-
*/
|
|
272
|
-
function formatDate(date, format, utc = false) {
|
|
273
|
-
if (!(date instanceof Date)) {
|
|
274
|
-
return "";
|
|
275
|
-
}
|
|
276
|
-
const t = date.getTime();
|
|
277
|
-
if (t !== t) {
|
|
278
|
-
return "";
|
|
279
|
-
} // NaN check (faster than isNaN)
|
|
280
|
-
// Default ISO format - direct string building (faster than toISOString)
|
|
281
|
-
if (!format) {
|
|
282
|
-
if (utc) {
|
|
283
|
-
const y = date.getUTCFullYear();
|
|
284
|
-
const M = date.getUTCMonth() + 1;
|
|
285
|
-
const D = date.getUTCDate();
|
|
286
|
-
const H = date.getUTCHours();
|
|
287
|
-
const m = date.getUTCMinutes();
|
|
288
|
-
const s = date.getUTCSeconds();
|
|
289
|
-
const ms = date.getUTCMilliseconds();
|
|
290
|
-
return `${y}-${PAD2[M]}-${PAD2[D]}T${PAD2[H]}:${PAD2[m]}:${PAD2[s]}.${ms < 10 ? "00" + ms : ms < 100 ? "0" + ms : ms}Z`;
|
|
291
|
-
}
|
|
292
|
-
const y = date.getFullYear();
|
|
293
|
-
const M = date.getMonth() + 1;
|
|
294
|
-
const D = date.getDate();
|
|
295
|
-
const H = date.getHours();
|
|
296
|
-
const m = date.getMinutes();
|
|
297
|
-
const s = date.getSeconds();
|
|
298
|
-
const ms = date.getMilliseconds();
|
|
299
|
-
return `${y}-${PAD2[M]}-${PAD2[D]}T${PAD2[H]}:${PAD2[m]}:${PAD2[s]}.${ms < 10 ? "00" + ms : ms < 100 ? "0" + ms : ms}${tzOffset(date)}`;
|
|
300
|
-
}
|
|
301
|
-
// Fast paths for common formats
|
|
302
|
-
if (format === "YYYY-MM-DD") {
|
|
303
|
-
return utc
|
|
304
|
-
? `${date.getUTCFullYear()}-${PAD2[date.getUTCMonth() + 1]}-${PAD2[date.getUTCDate()]}`
|
|
305
|
-
: `${date.getFullYear()}-${PAD2[date.getMonth() + 1]}-${PAD2[date.getDate()]}`;
|
|
306
|
-
}
|
|
307
|
-
return renderFormat(date, format, utc);
|
|
308
|
-
}
|
|
309
|
-
function tzOffset(d) {
|
|
310
|
-
const off = -d.getTimezoneOffset();
|
|
311
|
-
const sign = off >= 0 ? "+" : "-";
|
|
312
|
-
const h = (Math.abs(off) / 60) | 0; // Bitwise OR faster than Math.floor
|
|
313
|
-
const m = Math.abs(off) % 60;
|
|
314
|
-
return `${sign}${PAD2[h]}:${PAD2[m]}`;
|
|
315
|
-
}
|
|
316
|
-
function renderFormat(d, fmt, utc) {
|
|
317
|
-
// Handle escaped sections [...]
|
|
318
|
-
const esc = [];
|
|
319
|
-
let out = fmt.replace(/\[([^\]]*)\]/g, (_, c) => {
|
|
320
|
-
esc.push(c);
|
|
321
|
-
return `\x00${esc.length - 1}\x00`;
|
|
322
|
-
});
|
|
323
|
-
// Get components once
|
|
324
|
-
const y = utc ? d.getUTCFullYear() : d.getFullYear();
|
|
325
|
-
const M = utc ? d.getUTCMonth() + 1 : d.getMonth() + 1;
|
|
326
|
-
const D = utc ? d.getUTCDate() : d.getDate();
|
|
327
|
-
const H = utc ? d.getUTCHours() : d.getHours();
|
|
328
|
-
const m = utc ? d.getUTCMinutes() : d.getMinutes();
|
|
329
|
-
const s = utc ? d.getUTCSeconds() : d.getSeconds();
|
|
330
|
-
const ms = utc ? d.getUTCMilliseconds() : d.getMilliseconds();
|
|
331
|
-
// Replace tokens
|
|
332
|
-
out = out
|
|
333
|
-
.replace(/YYYY/g, String(y))
|
|
334
|
-
.replace(/SSS/g, ms < 10 ? `00${ms}` : ms < 100 ? `0${ms}` : String(ms))
|
|
335
|
-
.replace(/MM/g, PAD2[M])
|
|
336
|
-
.replace(/DD/g, PAD2[D])
|
|
337
|
-
.replace(/HH/g, PAD2[H])
|
|
338
|
-
.replace(/mm/g, PAD2[m])
|
|
339
|
-
.replace(/ss/g, PAD2[s])
|
|
340
|
-
.replace(/Z/g, utc ? "Z" : tzOffset(d));
|
|
341
|
-
// Restore escaped
|
|
342
|
-
if (esc.length) {
|
|
343
|
-
// oxlint-disable-next-line no-control-regex
|
|
344
|
-
out = out.replace(/\x00(\d+)\x00/g, (_, i) => esc[+i]);
|
|
345
|
-
}
|
|
346
|
-
return out;
|
|
347
|
-
}
|
|
348
|
-
// ============================================================================
|
|
349
193
|
// High-performance batch processors (class-based for state encapsulation)
|
|
350
194
|
// ============================================================================
|
|
351
195
|
/**
|
|
@@ -419,6 +263,13 @@ class DateParser {
|
|
|
419
263
|
}
|
|
420
264
|
}
|
|
421
265
|
exports.DateParser = DateParser;
|
|
266
|
+
function tzOffset(d) {
|
|
267
|
+
const off = -d.getTimezoneOffset();
|
|
268
|
+
const sign = off >= 0 ? "+" : "-";
|
|
269
|
+
const h = (Math.abs(off) / 60) | 0; // Bitwise OR faster than Math.floor
|
|
270
|
+
const m = Math.abs(off) % 60;
|
|
271
|
+
return `${sign}${PAD2[h]}:${PAD2[m]}`;
|
|
272
|
+
}
|
|
422
273
|
/**
|
|
423
274
|
* Optimized date formatter for batch processing
|
|
424
275
|
*
|
|
@@ -46,6 +46,8 @@ class PivotCacheDefinitionXform extends base_xform_1.BaseXform {
|
|
|
46
46
|
*/
|
|
47
47
|
renderNew(xmlStream, model) {
|
|
48
48
|
const { source, cacheFields } = model;
|
|
49
|
+
// Record count = number of data rows (excluding header row)
|
|
50
|
+
const recordCount = source.getSheetValues().slice(2).length;
|
|
49
51
|
xmlStream.openXml(xml_stream_1.XmlStream.StdDocAttributes);
|
|
50
52
|
xmlStream.openNode(this.tag, {
|
|
51
53
|
...PivotCacheDefinitionXform.PIVOT_CACHE_DEFINITION_ATTRIBUTES,
|
|
@@ -56,7 +58,7 @@ class PivotCacheDefinitionXform extends base_xform_1.BaseXform {
|
|
|
56
58
|
createdVersion: "8",
|
|
57
59
|
refreshedVersion: "8",
|
|
58
60
|
minRefreshableVersion: "3",
|
|
59
|
-
recordCount
|
|
61
|
+
recordCount
|
|
60
62
|
});
|
|
61
63
|
xmlStream.openNode("cacheSource", { type: "worksheet" });
|
|
62
64
|
xmlStream.leafNode("worksheetSource", {
|
|
@@ -60,6 +60,22 @@ class PivotTableXform extends base_xform_1.BaseXform {
|
|
|
60
60
|
const { rows, columns, values, cacheFields, cacheId, applyWidthHeightFormats } = model;
|
|
61
61
|
// Generate unique UID for each pivot table to prevent Excel treating them as identical
|
|
62
62
|
const uniqueUid = `{${crypto.randomUUID().toUpperCase()}}`;
|
|
63
|
+
// Build rowItems - need one <i> for each unique value in row fields, plus grand total
|
|
64
|
+
const rowItems = buildRowItems(rows, cacheFields);
|
|
65
|
+
// Build colItems - need one <i> for each unique value in col fields, plus grand total
|
|
66
|
+
const colItems = buildColItems(columns, cacheFields, values.length);
|
|
67
|
+
// Calculate pivot table dimensions
|
|
68
|
+
const rowFieldItemCount = rows.length > 0 ? cacheFields[rows[0]]?.sharedItems?.length || 0 : 0;
|
|
69
|
+
const colFieldItemCount = columns.length > 0 ? cacheFields[columns[0]]?.sharedItems?.length || 0 : 0;
|
|
70
|
+
// Location: A3 is where pivot table starts
|
|
71
|
+
// - firstHeaderRow: 1 (column headers are in first row of pivot table)
|
|
72
|
+
// - firstDataRow: 2 (data starts in second row)
|
|
73
|
+
// - firstDataCol: 1 (data starts in second column, after row labels)
|
|
74
|
+
// Calculate ref based on actual data size
|
|
75
|
+
const endRow = 3 + rowFieldItemCount + 1; // start row + data rows + grand total
|
|
76
|
+
const endCol = 1 + colFieldItemCount + 1; // start col + data cols + grand total
|
|
77
|
+
const endColLetter = String.fromCharCode(64 + endCol);
|
|
78
|
+
const locationRef = `A3:${endColLetter}${endRow}`;
|
|
63
79
|
xmlStream.openXml(xml_stream_1.XmlStream.StdDocAttributes);
|
|
64
80
|
xmlStream.openNode(this.tag, {
|
|
65
81
|
...PivotTableXform.PIVOT_TABLE_ATTRIBUTES,
|
|
@@ -84,23 +100,23 @@ class PivotTableXform extends base_xform_1.BaseXform {
|
|
|
84
100
|
multipleFieldFilters: "0"
|
|
85
101
|
});
|
|
86
102
|
xmlStream.writeXml(`
|
|
87
|
-
<location ref="
|
|
103
|
+
<location ref="${locationRef}" firstHeaderRow="1" firstDataRow="2" firstDataCol="1" />
|
|
88
104
|
<pivotFields count="${cacheFields.length}">
|
|
89
105
|
${renderPivotFields(model)}
|
|
90
106
|
</pivotFields>
|
|
91
107
|
<rowFields count="${rows.length}">
|
|
92
108
|
${rows.map(rowIndex => `<field x="${rowIndex}" />`).join("\n ")}
|
|
93
109
|
</rowFields>
|
|
94
|
-
<rowItems count="
|
|
95
|
-
|
|
110
|
+
<rowItems count="${rowItems.count}">
|
|
111
|
+
${rowItems.xml}
|
|
96
112
|
</rowItems>
|
|
97
113
|
<colFields count="${columns.length === 0 ? 1 : columns.length}">
|
|
98
114
|
${columns.length === 0
|
|
99
115
|
? '<field x="-2" />'
|
|
100
116
|
: columns.map(columnIndex => `<field x="${columnIndex}" />`).join("\n ")}
|
|
101
117
|
</colFields>
|
|
102
|
-
<colItems count="
|
|
103
|
-
|
|
118
|
+
<colItems count="${colItems.count}">
|
|
119
|
+
${colItems.xml}
|
|
104
120
|
</colItems>
|
|
105
121
|
<dataFields count="${values.length}">
|
|
106
122
|
${buildDataFields(cacheFields, values, model.metric)}
|
|
@@ -464,6 +480,70 @@ PivotTableXform.PIVOT_TABLE_ATTRIBUTES = {
|
|
|
464
480
|
"xmlns:xr": "http://schemas.microsoft.com/office/spreadsheetml/2014/revision"
|
|
465
481
|
};
|
|
466
482
|
// Helpers
|
|
483
|
+
/**
|
|
484
|
+
* Build rowItems XML - one item for each unique value in row fields, plus grand total.
|
|
485
|
+
* Each <i> represents a row in the pivot table.
|
|
486
|
+
* - Regular items: <i><x v="index"/></i> where index is the position in sharedItems
|
|
487
|
+
* - Grand total: <i t="grand"><x/></i>
|
|
488
|
+
*/
|
|
489
|
+
function buildRowItems(rows, cacheFields) {
|
|
490
|
+
if (rows.length === 0) {
|
|
491
|
+
// No row fields - just grand total
|
|
492
|
+
return { count: 1, xml: '<i t="grand"><x /></i>' };
|
|
493
|
+
}
|
|
494
|
+
// Get unique values count from the first row field
|
|
495
|
+
const rowFieldIndex = rows[0];
|
|
496
|
+
const sharedItems = cacheFields[rowFieldIndex]?.sharedItems || [];
|
|
497
|
+
const itemCount = sharedItems.length;
|
|
498
|
+
// Build items: one for each unique value + grand total
|
|
499
|
+
const items = [];
|
|
500
|
+
// Regular items - reference each unique value by index
|
|
501
|
+
for (let i = 0; i < itemCount; i++) {
|
|
502
|
+
items.push(`<i><x v="${i}" /></i>`);
|
|
503
|
+
}
|
|
504
|
+
// Grand total row
|
|
505
|
+
items.push('<i t="grand"><x /></i>');
|
|
506
|
+
return {
|
|
507
|
+
count: items.length,
|
|
508
|
+
xml: items.join("\n ")
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Build colItems XML - one item for each unique value in column fields, plus grand total.
|
|
513
|
+
* When there are multiple data fields (values), each column value may have sub-columns.
|
|
514
|
+
*/
|
|
515
|
+
function buildColItems(columns, cacheFields, valueCount) {
|
|
516
|
+
if (columns.length === 0) {
|
|
517
|
+
// No column fields - columns are based on data fields (values)
|
|
518
|
+
if (valueCount > 1) {
|
|
519
|
+
// Multiple values: one column per value + grand total
|
|
520
|
+
const items = [];
|
|
521
|
+
for (let i = 0; i < valueCount; i++) {
|
|
522
|
+
items.push(`<i><x v="${i}" /></i>`);
|
|
523
|
+
}
|
|
524
|
+
items.push('<i t="grand"><x /></i>');
|
|
525
|
+
return { count: items.length, xml: items.join("\n ") };
|
|
526
|
+
}
|
|
527
|
+
// Single value: just grand total
|
|
528
|
+
return { count: 1, xml: '<i t="grand"><x /></i>' };
|
|
529
|
+
}
|
|
530
|
+
// Get unique values count from the first column field
|
|
531
|
+
const colFieldIndex = columns[0];
|
|
532
|
+
const sharedItems = cacheFields[colFieldIndex]?.sharedItems || [];
|
|
533
|
+
const itemCount = sharedItems.length;
|
|
534
|
+
// Build items: one for each unique value + grand total
|
|
535
|
+
const items = [];
|
|
536
|
+
// Regular items - reference each unique value by index
|
|
537
|
+
for (let i = 0; i < itemCount; i++) {
|
|
538
|
+
items.push(`<i><x v="${i}" /></i>`);
|
|
539
|
+
}
|
|
540
|
+
// Grand total column
|
|
541
|
+
items.push('<i t="grand"><x /></i>');
|
|
542
|
+
return {
|
|
543
|
+
count: items.length,
|
|
544
|
+
xml: items.join("\n ")
|
|
545
|
+
};
|
|
546
|
+
}
|
|
467
547
|
/**
|
|
468
548
|
* Build dataField XML elements for all values in the pivot table.
|
|
469
549
|
* Supports multiple values when columns is empty.
|
|
@@ -504,10 +584,15 @@ function renderPivotField(fieldType, sharedItems) {
|
|
|
504
584
|
const defaultAttributes = 'compact="0" outline="0" showAll="0" defaultSubtotal="0"';
|
|
505
585
|
if (fieldType === "row" || fieldType === "column") {
|
|
506
586
|
const axis = fieldType === "row" ? "axisRow" : "axisCol";
|
|
587
|
+
// items = one for each shared item + one default item
|
|
588
|
+
const itemsXml = [
|
|
589
|
+
...sharedItems.map((_item, index) => `<item x="${index}" />`),
|
|
590
|
+
'<item t="default" />' // Required default item for subtotals/grand totals
|
|
591
|
+
].join("\n ");
|
|
507
592
|
return `
|
|
508
593
|
<pivotField axis="${axis}" ${defaultAttributes}>
|
|
509
594
|
<items count="${sharedItems.length + 1}">
|
|
510
|
-
${
|
|
595
|
+
${itemsXml}
|
|
511
596
|
</items>
|
|
512
597
|
</pivotField>
|
|
513
598
|
`;
|
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
const PAD2 = Array.from({ length: 60 }, (_, i) => (i < 10 ? `0${i}` : `${i}`));
|
|
12
12
|
// Character codes for fast comparison
|
|
13
13
|
const C_0 = 48;
|
|
14
|
-
const C_9 = 57;
|
|
15
14
|
const C_DASH = 45;
|
|
16
15
|
const C_SLASH = 47;
|
|
17
16
|
const C_COLON = 58;
|
|
@@ -187,158 +186,6 @@ const AUTO_DETECT = [
|
|
|
187
186
|
[29, [parseISOMsOffset]]
|
|
188
187
|
];
|
|
189
188
|
// ============================================================================
|
|
190
|
-
// Public API
|
|
191
|
-
// ============================================================================
|
|
192
|
-
/**
|
|
193
|
-
* Quick pre-filter to reject non-date strings
|
|
194
|
-
*/
|
|
195
|
-
export function mightBeDate(s) {
|
|
196
|
-
const len = s.length;
|
|
197
|
-
if (len < 10 || len > 30) {
|
|
198
|
-
return false;
|
|
199
|
-
}
|
|
200
|
-
const c0 = s.charCodeAt(0);
|
|
201
|
-
const c1 = s.charCodeAt(1);
|
|
202
|
-
// Must start with digits
|
|
203
|
-
if (c0 < C_0 || c0 > C_9 || c1 < C_0 || c1 > C_9) {
|
|
204
|
-
return false;
|
|
205
|
-
}
|
|
206
|
-
const c2 = s.charCodeAt(2);
|
|
207
|
-
// Either YYYY format (4 digits) or MM/DD format (separator at pos 2)
|
|
208
|
-
return (c2 >= C_0 && c2 <= C_9) || c2 === C_DASH || c2 === C_SLASH;
|
|
209
|
-
}
|
|
210
|
-
/**
|
|
211
|
-
* Parse a date string
|
|
212
|
-
*
|
|
213
|
-
* @param value - String to parse
|
|
214
|
-
* @param formats - Specific formats to try (optional, auto-detects ISO if omitted)
|
|
215
|
-
* @returns Date object or null
|
|
216
|
-
*
|
|
217
|
-
* @example
|
|
218
|
-
* parseDate("2024-12-26") // auto-detect ISO
|
|
219
|
-
* parseDate("12-26-2024", ["MM-DD-YYYY"]) // explicit US format
|
|
220
|
-
*/
|
|
221
|
-
export function parseDate(value, formats) {
|
|
222
|
-
if (!value || typeof value !== "string") {
|
|
223
|
-
return null;
|
|
224
|
-
}
|
|
225
|
-
const s = value.trim();
|
|
226
|
-
if (!s) {
|
|
227
|
-
return null;
|
|
228
|
-
}
|
|
229
|
-
// Explicit formats
|
|
230
|
-
if (formats?.length) {
|
|
231
|
-
for (const fmt of formats) {
|
|
232
|
-
const result = PARSERS[fmt]?.(s);
|
|
233
|
-
if (result) {
|
|
234
|
-
return result;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
return null;
|
|
238
|
-
}
|
|
239
|
-
// Auto-detect by length
|
|
240
|
-
const len = s.length;
|
|
241
|
-
for (const [l, parsers] of AUTO_DETECT) {
|
|
242
|
-
if (len === l) {
|
|
243
|
-
for (const p of parsers) {
|
|
244
|
-
const result = p(s);
|
|
245
|
-
if (result) {
|
|
246
|
-
return result;
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
return null;
|
|
252
|
-
}
|
|
253
|
-
/**
|
|
254
|
-
* Format a Date to string
|
|
255
|
-
*
|
|
256
|
-
* @param date - Date to format
|
|
257
|
-
* @param format - Format template (optional, defaults to ISO)
|
|
258
|
-
* @param utc - Use UTC time (default: false)
|
|
259
|
-
* @returns Formatted string or empty string if invalid
|
|
260
|
-
*
|
|
261
|
-
* @example
|
|
262
|
-
* formatDate(date) // "2024-12-26T10:30:00.000+08:00"
|
|
263
|
-
* formatDate(date, "YYYY-MM-DD", true) // "2024-12-26"
|
|
264
|
-
*/
|
|
265
|
-
export function formatDate(date, format, utc = false) {
|
|
266
|
-
if (!(date instanceof Date)) {
|
|
267
|
-
return "";
|
|
268
|
-
}
|
|
269
|
-
const t = date.getTime();
|
|
270
|
-
if (t !== t) {
|
|
271
|
-
return "";
|
|
272
|
-
} // NaN check (faster than isNaN)
|
|
273
|
-
// Default ISO format - direct string building (faster than toISOString)
|
|
274
|
-
if (!format) {
|
|
275
|
-
if (utc) {
|
|
276
|
-
const y = date.getUTCFullYear();
|
|
277
|
-
const M = date.getUTCMonth() + 1;
|
|
278
|
-
const D = date.getUTCDate();
|
|
279
|
-
const H = date.getUTCHours();
|
|
280
|
-
const m = date.getUTCMinutes();
|
|
281
|
-
const s = date.getUTCSeconds();
|
|
282
|
-
const ms = date.getUTCMilliseconds();
|
|
283
|
-
return `${y}-${PAD2[M]}-${PAD2[D]}T${PAD2[H]}:${PAD2[m]}:${PAD2[s]}.${ms < 10 ? "00" + ms : ms < 100 ? "0" + ms : ms}Z`;
|
|
284
|
-
}
|
|
285
|
-
const y = date.getFullYear();
|
|
286
|
-
const M = date.getMonth() + 1;
|
|
287
|
-
const D = date.getDate();
|
|
288
|
-
const H = date.getHours();
|
|
289
|
-
const m = date.getMinutes();
|
|
290
|
-
const s = date.getSeconds();
|
|
291
|
-
const ms = date.getMilliseconds();
|
|
292
|
-
return `${y}-${PAD2[M]}-${PAD2[D]}T${PAD2[H]}:${PAD2[m]}:${PAD2[s]}.${ms < 10 ? "00" + ms : ms < 100 ? "0" + ms : ms}${tzOffset(date)}`;
|
|
293
|
-
}
|
|
294
|
-
// Fast paths for common formats
|
|
295
|
-
if (format === "YYYY-MM-DD") {
|
|
296
|
-
return utc
|
|
297
|
-
? `${date.getUTCFullYear()}-${PAD2[date.getUTCMonth() + 1]}-${PAD2[date.getUTCDate()]}`
|
|
298
|
-
: `${date.getFullYear()}-${PAD2[date.getMonth() + 1]}-${PAD2[date.getDate()]}`;
|
|
299
|
-
}
|
|
300
|
-
return renderFormat(date, format, utc);
|
|
301
|
-
}
|
|
302
|
-
function tzOffset(d) {
|
|
303
|
-
const off = -d.getTimezoneOffset();
|
|
304
|
-
const sign = off >= 0 ? "+" : "-";
|
|
305
|
-
const h = (Math.abs(off) / 60) | 0; // Bitwise OR faster than Math.floor
|
|
306
|
-
const m = Math.abs(off) % 60;
|
|
307
|
-
return `${sign}${PAD2[h]}:${PAD2[m]}`;
|
|
308
|
-
}
|
|
309
|
-
function renderFormat(d, fmt, utc) {
|
|
310
|
-
// Handle escaped sections [...]
|
|
311
|
-
const esc = [];
|
|
312
|
-
let out = fmt.replace(/\[([^\]]*)\]/g, (_, c) => {
|
|
313
|
-
esc.push(c);
|
|
314
|
-
return `\x00${esc.length - 1}\x00`;
|
|
315
|
-
});
|
|
316
|
-
// Get components once
|
|
317
|
-
const y = utc ? d.getUTCFullYear() : d.getFullYear();
|
|
318
|
-
const M = utc ? d.getUTCMonth() + 1 : d.getMonth() + 1;
|
|
319
|
-
const D = utc ? d.getUTCDate() : d.getDate();
|
|
320
|
-
const H = utc ? d.getUTCHours() : d.getHours();
|
|
321
|
-
const m = utc ? d.getUTCMinutes() : d.getMinutes();
|
|
322
|
-
const s = utc ? d.getUTCSeconds() : d.getSeconds();
|
|
323
|
-
const ms = utc ? d.getUTCMilliseconds() : d.getMilliseconds();
|
|
324
|
-
// Replace tokens
|
|
325
|
-
out = out
|
|
326
|
-
.replace(/YYYY/g, String(y))
|
|
327
|
-
.replace(/SSS/g, ms < 10 ? `00${ms}` : ms < 100 ? `0${ms}` : String(ms))
|
|
328
|
-
.replace(/MM/g, PAD2[M])
|
|
329
|
-
.replace(/DD/g, PAD2[D])
|
|
330
|
-
.replace(/HH/g, PAD2[H])
|
|
331
|
-
.replace(/mm/g, PAD2[m])
|
|
332
|
-
.replace(/ss/g, PAD2[s])
|
|
333
|
-
.replace(/Z/g, utc ? "Z" : tzOffset(d));
|
|
334
|
-
// Restore escaped
|
|
335
|
-
if (esc.length) {
|
|
336
|
-
// oxlint-disable-next-line no-control-regex
|
|
337
|
-
out = out.replace(/\x00(\d+)\x00/g, (_, i) => esc[+i]);
|
|
338
|
-
}
|
|
339
|
-
return out;
|
|
340
|
-
}
|
|
341
|
-
// ============================================================================
|
|
342
189
|
// High-performance batch processors (class-based for state encapsulation)
|
|
343
190
|
// ============================================================================
|
|
344
191
|
/**
|
|
@@ -411,6 +258,13 @@ export class DateParser {
|
|
|
411
258
|
return out;
|
|
412
259
|
}
|
|
413
260
|
}
|
|
261
|
+
function tzOffset(d) {
|
|
262
|
+
const off = -d.getTimezoneOffset();
|
|
263
|
+
const sign = off >= 0 ? "+" : "-";
|
|
264
|
+
const h = (Math.abs(off) / 60) | 0; // Bitwise OR faster than Math.floor
|
|
265
|
+
const m = Math.abs(off) % 60;
|
|
266
|
+
return `${sign}${PAD2[h]}:${PAD2[m]}`;
|
|
267
|
+
}
|
|
414
268
|
/**
|
|
415
269
|
* Optimized date formatter for batch processing
|
|
416
270
|
*
|
|
@@ -43,6 +43,8 @@ class PivotCacheDefinitionXform extends BaseXform {
|
|
|
43
43
|
*/
|
|
44
44
|
renderNew(xmlStream, model) {
|
|
45
45
|
const { source, cacheFields } = model;
|
|
46
|
+
// Record count = number of data rows (excluding header row)
|
|
47
|
+
const recordCount = source.getSheetValues().slice(2).length;
|
|
46
48
|
xmlStream.openXml(XmlStream.StdDocAttributes);
|
|
47
49
|
xmlStream.openNode(this.tag, {
|
|
48
50
|
...PivotCacheDefinitionXform.PIVOT_CACHE_DEFINITION_ATTRIBUTES,
|
|
@@ -53,7 +55,7 @@ class PivotCacheDefinitionXform extends BaseXform {
|
|
|
53
55
|
createdVersion: "8",
|
|
54
56
|
refreshedVersion: "8",
|
|
55
57
|
minRefreshableVersion: "3",
|
|
56
|
-
recordCount
|
|
58
|
+
recordCount
|
|
57
59
|
});
|
|
58
60
|
xmlStream.openNode("cacheSource", { type: "worksheet" });
|
|
59
61
|
xmlStream.leafNode("worksheetSource", {
|
|
@@ -57,6 +57,22 @@ class PivotTableXform extends BaseXform {
|
|
|
57
57
|
const { rows, columns, values, cacheFields, cacheId, applyWidthHeightFormats } = model;
|
|
58
58
|
// Generate unique UID for each pivot table to prevent Excel treating them as identical
|
|
59
59
|
const uniqueUid = `{${crypto.randomUUID().toUpperCase()}}`;
|
|
60
|
+
// Build rowItems - need one <i> for each unique value in row fields, plus grand total
|
|
61
|
+
const rowItems = buildRowItems(rows, cacheFields);
|
|
62
|
+
// Build colItems - need one <i> for each unique value in col fields, plus grand total
|
|
63
|
+
const colItems = buildColItems(columns, cacheFields, values.length);
|
|
64
|
+
// Calculate pivot table dimensions
|
|
65
|
+
const rowFieldItemCount = rows.length > 0 ? cacheFields[rows[0]]?.sharedItems?.length || 0 : 0;
|
|
66
|
+
const colFieldItemCount = columns.length > 0 ? cacheFields[columns[0]]?.sharedItems?.length || 0 : 0;
|
|
67
|
+
// Location: A3 is where pivot table starts
|
|
68
|
+
// - firstHeaderRow: 1 (column headers are in first row of pivot table)
|
|
69
|
+
// - firstDataRow: 2 (data starts in second row)
|
|
70
|
+
// - firstDataCol: 1 (data starts in second column, after row labels)
|
|
71
|
+
// Calculate ref based on actual data size
|
|
72
|
+
const endRow = 3 + rowFieldItemCount + 1; // start row + data rows + grand total
|
|
73
|
+
const endCol = 1 + colFieldItemCount + 1; // start col + data cols + grand total
|
|
74
|
+
const endColLetter = String.fromCharCode(64 + endCol);
|
|
75
|
+
const locationRef = `A3:${endColLetter}${endRow}`;
|
|
60
76
|
xmlStream.openXml(XmlStream.StdDocAttributes);
|
|
61
77
|
xmlStream.openNode(this.tag, {
|
|
62
78
|
...PivotTableXform.PIVOT_TABLE_ATTRIBUTES,
|
|
@@ -81,23 +97,23 @@ class PivotTableXform extends BaseXform {
|
|
|
81
97
|
multipleFieldFilters: "0"
|
|
82
98
|
});
|
|
83
99
|
xmlStream.writeXml(`
|
|
84
|
-
<location ref="
|
|
100
|
+
<location ref="${locationRef}" firstHeaderRow="1" firstDataRow="2" firstDataCol="1" />
|
|
85
101
|
<pivotFields count="${cacheFields.length}">
|
|
86
102
|
${renderPivotFields(model)}
|
|
87
103
|
</pivotFields>
|
|
88
104
|
<rowFields count="${rows.length}">
|
|
89
105
|
${rows.map(rowIndex => `<field x="${rowIndex}" />`).join("\n ")}
|
|
90
106
|
</rowFields>
|
|
91
|
-
<rowItems count="
|
|
92
|
-
|
|
107
|
+
<rowItems count="${rowItems.count}">
|
|
108
|
+
${rowItems.xml}
|
|
93
109
|
</rowItems>
|
|
94
110
|
<colFields count="${columns.length === 0 ? 1 : columns.length}">
|
|
95
111
|
${columns.length === 0
|
|
96
112
|
? '<field x="-2" />'
|
|
97
113
|
: columns.map(columnIndex => `<field x="${columnIndex}" />`).join("\n ")}
|
|
98
114
|
</colFields>
|
|
99
|
-
<colItems count="
|
|
100
|
-
|
|
115
|
+
<colItems count="${colItems.count}">
|
|
116
|
+
${colItems.xml}
|
|
101
117
|
</colItems>
|
|
102
118
|
<dataFields count="${values.length}">
|
|
103
119
|
${buildDataFields(cacheFields, values, model.metric)}
|
|
@@ -460,6 +476,70 @@ PivotTableXform.PIVOT_TABLE_ATTRIBUTES = {
|
|
|
460
476
|
"xmlns:xr": "http://schemas.microsoft.com/office/spreadsheetml/2014/revision"
|
|
461
477
|
};
|
|
462
478
|
// Helpers
|
|
479
|
+
/**
|
|
480
|
+
* Build rowItems XML - one item for each unique value in row fields, plus grand total.
|
|
481
|
+
* Each <i> represents a row in the pivot table.
|
|
482
|
+
* - Regular items: <i><x v="index"/></i> where index is the position in sharedItems
|
|
483
|
+
* - Grand total: <i t="grand"><x/></i>
|
|
484
|
+
*/
|
|
485
|
+
function buildRowItems(rows, cacheFields) {
|
|
486
|
+
if (rows.length === 0) {
|
|
487
|
+
// No row fields - just grand total
|
|
488
|
+
return { count: 1, xml: '<i t="grand"><x /></i>' };
|
|
489
|
+
}
|
|
490
|
+
// Get unique values count from the first row field
|
|
491
|
+
const rowFieldIndex = rows[0];
|
|
492
|
+
const sharedItems = cacheFields[rowFieldIndex]?.sharedItems || [];
|
|
493
|
+
const itemCount = sharedItems.length;
|
|
494
|
+
// Build items: one for each unique value + grand total
|
|
495
|
+
const items = [];
|
|
496
|
+
// Regular items - reference each unique value by index
|
|
497
|
+
for (let i = 0; i < itemCount; i++) {
|
|
498
|
+
items.push(`<i><x v="${i}" /></i>`);
|
|
499
|
+
}
|
|
500
|
+
// Grand total row
|
|
501
|
+
items.push('<i t="grand"><x /></i>');
|
|
502
|
+
return {
|
|
503
|
+
count: items.length,
|
|
504
|
+
xml: items.join("\n ")
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Build colItems XML - one item for each unique value in column fields, plus grand total.
|
|
509
|
+
* When there are multiple data fields (values), each column value may have sub-columns.
|
|
510
|
+
*/
|
|
511
|
+
function buildColItems(columns, cacheFields, valueCount) {
|
|
512
|
+
if (columns.length === 0) {
|
|
513
|
+
// No column fields - columns are based on data fields (values)
|
|
514
|
+
if (valueCount > 1) {
|
|
515
|
+
// Multiple values: one column per value + grand total
|
|
516
|
+
const items = [];
|
|
517
|
+
for (let i = 0; i < valueCount; i++) {
|
|
518
|
+
items.push(`<i><x v="${i}" /></i>`);
|
|
519
|
+
}
|
|
520
|
+
items.push('<i t="grand"><x /></i>');
|
|
521
|
+
return { count: items.length, xml: items.join("\n ") };
|
|
522
|
+
}
|
|
523
|
+
// Single value: just grand total
|
|
524
|
+
return { count: 1, xml: '<i t="grand"><x /></i>' };
|
|
525
|
+
}
|
|
526
|
+
// Get unique values count from the first column field
|
|
527
|
+
const colFieldIndex = columns[0];
|
|
528
|
+
const sharedItems = cacheFields[colFieldIndex]?.sharedItems || [];
|
|
529
|
+
const itemCount = sharedItems.length;
|
|
530
|
+
// Build items: one for each unique value + grand total
|
|
531
|
+
const items = [];
|
|
532
|
+
// Regular items - reference each unique value by index
|
|
533
|
+
for (let i = 0; i < itemCount; i++) {
|
|
534
|
+
items.push(`<i><x v="${i}" /></i>`);
|
|
535
|
+
}
|
|
536
|
+
// Grand total column
|
|
537
|
+
items.push('<i t="grand"><x /></i>');
|
|
538
|
+
return {
|
|
539
|
+
count: items.length,
|
|
540
|
+
xml: items.join("\n ")
|
|
541
|
+
};
|
|
542
|
+
}
|
|
463
543
|
/**
|
|
464
544
|
* Build dataField XML elements for all values in the pivot table.
|
|
465
545
|
* Supports multiple values when columns is empty.
|
|
@@ -500,10 +580,15 @@ function renderPivotField(fieldType, sharedItems) {
|
|
|
500
580
|
const defaultAttributes = 'compact="0" outline="0" showAll="0" defaultSubtotal="0"';
|
|
501
581
|
if (fieldType === "row" || fieldType === "column") {
|
|
502
582
|
const axis = fieldType === "row" ? "axisRow" : "axisCol";
|
|
583
|
+
// items = one for each shared item + one default item
|
|
584
|
+
const itemsXml = [
|
|
585
|
+
...sharedItems.map((_item, index) => `<item x="${index}" />`),
|
|
586
|
+
'<item t="default" />' // Required default item for subtotals/grand totals
|
|
587
|
+
].join("\n ");
|
|
503
588
|
return `
|
|
504
589
|
<pivotField axis="${axis}" ${defaultAttributes}>
|
|
505
590
|
<items count="${sharedItems.length + 1}">
|
|
506
|
-
${
|
|
591
|
+
${itemsXml}
|
|
507
592
|
</items>
|
|
508
593
|
</pivotField>
|
|
509
594
|
`;
|