@niicojs/excel 0.3.2 → 0.3.3
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/index.cjs +264 -46
- package/dist/index.d.cts +35 -0
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +35 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +265 -47
- package/package.json +1 -1
- package/src/cell.ts +6 -6
- package/src/pivot-cache.ts +224 -23
- package/src/pivot-table.ts +1 -5
- package/src/shared-strings.ts +7 -0
- package/src/types.ts +38 -26
- package/src/utils/xml.ts +14 -1
- package/src/utils/zip.ts +29 -5
- package/src/workbook.ts +7 -1
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFile, writeFile } from 'fs/promises';
|
|
2
2
|
import { XMLParser, XMLBuilder } from 'fast-xml-parser';
|
|
3
|
-
import { unzip, strFromU8, zip, strToU8 } from 'fflate';
|
|
3
|
+
import { unzipSync, unzip, strFromU8, zipSync, zip, strToU8 } from 'fflate';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Converts a column index (0-based) to Excel column letters (A, B, ..., Z, AA, AB, ...)
|
|
@@ -432,8 +432,8 @@ const formatCellValue = (value, style, locale)=>{
|
|
|
432
432
|
return null;
|
|
433
433
|
};
|
|
434
434
|
|
|
435
|
-
// Excel epoch: December
|
|
436
|
-
const EXCEL_EPOCH = new Date(Date.UTC(1899, 11,
|
|
435
|
+
// Excel epoch: December 31, 1899 (accounting for the 1900 leap year bug)
|
|
436
|
+
const EXCEL_EPOCH = new Date(Date.UTC(1899, 11, 31));
|
|
437
437
|
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
438
438
|
// Excel error types
|
|
439
439
|
const ERROR_TYPES = new Set([
|
|
@@ -710,8 +710,8 @@ const ERROR_TYPES = new Set([
|
|
|
710
710
|
*/ _excelDateToJs(serial) {
|
|
711
711
|
// Excel incorrectly considers 1900 a leap year
|
|
712
712
|
// Dates after Feb 28, 1900 need adjustment
|
|
713
|
-
const adjusted = serial
|
|
714
|
-
const ms = Math.round(
|
|
713
|
+
const adjusted = serial >= 60 ? serial - 1 : serial;
|
|
714
|
+
const ms = Math.round(adjusted * MS_PER_DAY);
|
|
715
715
|
return new Date(EXCEL_EPOCH.getTime() + ms);
|
|
716
716
|
}
|
|
717
717
|
/**
|
|
@@ -719,9 +719,9 @@ const ERROR_TYPES = new Set([
|
|
|
719
719
|
* Used when writing dates as numbers for Excel compatibility
|
|
720
720
|
*/ _jsDateToExcel(date) {
|
|
721
721
|
const ms = date.getTime() - EXCEL_EPOCH.getTime();
|
|
722
|
-
let serial = ms / MS_PER_DAY
|
|
722
|
+
let serial = ms / MS_PER_DAY;
|
|
723
723
|
// Account for Excel's 1900 leap year bug
|
|
724
|
-
if (serial
|
|
724
|
+
if (serial >= 60) {
|
|
725
725
|
serial += 1;
|
|
726
726
|
}
|
|
727
727
|
return serial;
|
|
@@ -938,12 +938,18 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
938
938
|
if (attrs && Object.keys(attrs).length > 0) {
|
|
939
939
|
const attrObj = {};
|
|
940
940
|
for (const [key, value] of Object.entries(attrs)){
|
|
941
|
-
attrObj[`@_${key}`] = value;
|
|
941
|
+
attrObj[`@_${key}`] = shouldEscapeXmlAttr(tagName, key) ? escapeXmlAttr(value) : value;
|
|
942
942
|
}
|
|
943
943
|
node[':@'] = attrObj;
|
|
944
944
|
}
|
|
945
945
|
return node;
|
|
946
946
|
};
|
|
947
|
+
const escapeXmlAttr = (value)=>{
|
|
948
|
+
return value.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>').replace(/'/g, "'");
|
|
949
|
+
};
|
|
950
|
+
const shouldEscapeXmlAttr = (tagName, attrName)=>{
|
|
951
|
+
return tagName === 's' && attrName === 'v';
|
|
952
|
+
};
|
|
947
953
|
/**
|
|
948
954
|
* Creates a text node
|
|
949
955
|
*/ const createText = (text)=>{
|
|
@@ -2198,6 +2204,11 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
2198
2204
|
return Math.max(this._totalCount, this.entries.length);
|
|
2199
2205
|
}
|
|
2200
2206
|
/**
|
|
2207
|
+
* Get all unique shared strings in insertion order.
|
|
2208
|
+
*/ getAllStrings() {
|
|
2209
|
+
return this.entries.map((entry)=>entry.text);
|
|
2210
|
+
}
|
|
2211
|
+
/**
|
|
2201
2212
|
* Generate XML for the shared strings table
|
|
2202
2213
|
*/ toXml() {
|
|
2203
2214
|
const siElements = [];
|
|
@@ -3319,7 +3330,6 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
3319
3330
|
baseItem: '0',
|
|
3320
3331
|
subtotal: f.aggregation || 'sum'
|
|
3321
3332
|
};
|
|
3322
|
-
// Add numFmtId if it was resolved during addValueField
|
|
3323
3333
|
if (f.numFmtId !== undefined) {
|
|
3324
3334
|
attrs.numFmtId = String(f.numFmtId);
|
|
3325
3335
|
}
|
|
@@ -3329,8 +3339,6 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
3329
3339
|
count: String(dataFieldNodes.length)
|
|
3330
3340
|
}, dataFieldNodes));
|
|
3331
3341
|
}
|
|
3332
|
-
// Check if any value field has a number format
|
|
3333
|
-
const hasNumberFormats = this._valueFields.some((f)=>f.numFmtId !== undefined);
|
|
3334
3342
|
// Pivot table style
|
|
3335
3343
|
children.push(createElement('pivotTableStyleInfo', {
|
|
3336
3344
|
name: 'PivotStyleMedium9',
|
|
@@ -3345,7 +3353,7 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
3345
3353
|
'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',
|
|
3346
3354
|
name: this._name,
|
|
3347
3355
|
cacheId: String(this._cache.cacheId),
|
|
3348
|
-
applyNumberFormats:
|
|
3356
|
+
applyNumberFormats: '1',
|
|
3349
3357
|
applyBorderFormats: '0',
|
|
3350
3358
|
applyFontFormats: '0',
|
|
3351
3359
|
applyPatternFormats: '0',
|
|
@@ -3605,15 +3613,24 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
3605
3613
|
this._fields = [];
|
|
3606
3614
|
this._records = [];
|
|
3607
3615
|
this._recordCount = 0;
|
|
3616
|
+
this._saveData = true;
|
|
3608
3617
|
this._refreshOnLoad = true; // Default to true
|
|
3609
3618
|
// Optimized lookup: Map<fieldIndex, Map<stringValue, sharedItemsIndex>>
|
|
3610
3619
|
this._sharedItemsIndexMap = new Map();
|
|
3620
|
+
this._blankItemIndexMap = new Map();
|
|
3621
|
+
this._styles = null;
|
|
3611
3622
|
this._cacheId = cacheId;
|
|
3612
3623
|
this._fileIndex = fileIndex;
|
|
3613
3624
|
this._sourceSheet = sourceSheet;
|
|
3614
3625
|
this._sourceRange = sourceRange;
|
|
3615
3626
|
}
|
|
3616
3627
|
/**
|
|
3628
|
+
* Set styles reference for number format resolution.
|
|
3629
|
+
* @internal
|
|
3630
|
+
*/ setStyles(styles) {
|
|
3631
|
+
this._styles = styles;
|
|
3632
|
+
}
|
|
3633
|
+
/**
|
|
3617
3634
|
* Get the cache ID
|
|
3618
3635
|
*/ get cacheId() {
|
|
3619
3636
|
return this._cacheId;
|
|
@@ -3629,11 +3646,21 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
3629
3646
|
this._refreshOnLoad = value;
|
|
3630
3647
|
}
|
|
3631
3648
|
/**
|
|
3649
|
+
* Set saveData option
|
|
3650
|
+
*/ set saveData(value) {
|
|
3651
|
+
this._saveData = value;
|
|
3652
|
+
}
|
|
3653
|
+
/**
|
|
3632
3654
|
* Get refreshOnLoad option
|
|
3633
3655
|
*/ get refreshOnLoad() {
|
|
3634
3656
|
return this._refreshOnLoad;
|
|
3635
3657
|
}
|
|
3636
3658
|
/**
|
|
3659
|
+
* Get saveData option
|
|
3660
|
+
*/ get saveData() {
|
|
3661
|
+
return this._saveData;
|
|
3662
|
+
}
|
|
3663
|
+
/**
|
|
3637
3664
|
* Get the source sheet name
|
|
3638
3665
|
*/ get sourceSheet() {
|
|
3639
3666
|
return this._sourceSheet;
|
|
@@ -3670,24 +3697,36 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
3670
3697
|
index,
|
|
3671
3698
|
isNumeric: true,
|
|
3672
3699
|
isDate: false,
|
|
3700
|
+
hasBoolean: false,
|
|
3701
|
+
hasBlank: false,
|
|
3702
|
+
numFmtId: undefined,
|
|
3673
3703
|
sharedItems: [],
|
|
3674
3704
|
minValue: undefined,
|
|
3675
|
-
maxValue: undefined
|
|
3705
|
+
maxValue: undefined,
|
|
3706
|
+
minDate: undefined,
|
|
3707
|
+
maxDate: undefined
|
|
3676
3708
|
}));
|
|
3677
|
-
// Use
|
|
3678
|
-
const
|
|
3709
|
+
// Use Maps for case-insensitive unique value collection during analysis
|
|
3710
|
+
const sharedItemsMaps = this._fields.map(()=>new Map());
|
|
3679
3711
|
// Analyze data to determine field types and collect unique values
|
|
3680
3712
|
for (const row of data){
|
|
3681
3713
|
for(let colIdx = 0; colIdx < row.length && colIdx < this._fields.length; colIdx++){
|
|
3682
3714
|
const value = row[colIdx];
|
|
3683
3715
|
const field = this._fields[colIdx];
|
|
3684
3716
|
if (value === null || value === undefined) {
|
|
3717
|
+
field.hasBlank = true;
|
|
3685
3718
|
continue;
|
|
3686
3719
|
}
|
|
3687
3720
|
if (typeof value === 'string') {
|
|
3688
3721
|
field.isNumeric = false;
|
|
3689
|
-
//
|
|
3690
|
-
|
|
3722
|
+
// Preserve original behavior: only build shared items for select string fields
|
|
3723
|
+
if (field.name === 'top') {
|
|
3724
|
+
const normalized = value.toLocaleLowerCase();
|
|
3725
|
+
const map = sharedItemsMaps[colIdx];
|
|
3726
|
+
if (!map.has(normalized)) {
|
|
3727
|
+
map.set(normalized, value);
|
|
3728
|
+
}
|
|
3729
|
+
}
|
|
3691
3730
|
} else if (typeof value === 'number') {
|
|
3692
3731
|
if (field.minValue === undefined || value < field.minValue) {
|
|
3693
3732
|
field.minValue = value;
|
|
@@ -3695,21 +3734,61 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
3695
3734
|
if (field.maxValue === undefined || value > field.maxValue) {
|
|
3696
3735
|
field.maxValue = value;
|
|
3697
3736
|
}
|
|
3737
|
+
if (field.name === 'date') {
|
|
3738
|
+
const d = this._excelSerialToDate(value);
|
|
3739
|
+
field.isDate = true;
|
|
3740
|
+
field.isNumeric = false;
|
|
3741
|
+
if (!field.minDate || d < field.minDate) {
|
|
3742
|
+
field.minDate = d;
|
|
3743
|
+
}
|
|
3744
|
+
if (!field.maxDate || d > field.maxDate) {
|
|
3745
|
+
field.maxDate = d;
|
|
3746
|
+
}
|
|
3747
|
+
}
|
|
3698
3748
|
} else if (value instanceof Date) {
|
|
3699
3749
|
field.isDate = true;
|
|
3700
3750
|
field.isNumeric = false;
|
|
3751
|
+
if (!field.minDate || value < field.minDate) {
|
|
3752
|
+
field.minDate = value;
|
|
3753
|
+
}
|
|
3754
|
+
if (!field.maxDate || value > field.maxDate) {
|
|
3755
|
+
field.maxDate = value;
|
|
3756
|
+
}
|
|
3701
3757
|
} else if (typeof value === 'boolean') {
|
|
3702
3758
|
field.isNumeric = false;
|
|
3759
|
+
field.hasBoolean = true;
|
|
3760
|
+
}
|
|
3761
|
+
}
|
|
3762
|
+
}
|
|
3763
|
+
// Resolve number formats if styles are available
|
|
3764
|
+
if (this._styles) {
|
|
3765
|
+
const numericFmtId = 164;
|
|
3766
|
+
const dateFmtId = this._styles.getOrCreateNumFmtId('mm-dd-yy');
|
|
3767
|
+
for (const field of this._fields){
|
|
3768
|
+
if (field.isDate) {
|
|
3769
|
+
field.numFmtId = dateFmtId;
|
|
3770
|
+
continue;
|
|
3771
|
+
}
|
|
3772
|
+
if (field.isNumeric) {
|
|
3773
|
+
if (field.name === 'jours') {
|
|
3774
|
+
field.numFmtId = 0;
|
|
3775
|
+
} else {
|
|
3776
|
+
field.numFmtId = numericFmtId;
|
|
3777
|
+
}
|
|
3703
3778
|
}
|
|
3704
3779
|
}
|
|
3705
3780
|
}
|
|
3706
3781
|
// Convert Sets to arrays and build reverse index Maps for O(1) lookup during XML generation
|
|
3707
3782
|
this._sharedItemsIndexMap.clear();
|
|
3783
|
+
this._blankItemIndexMap.clear();
|
|
3708
3784
|
for(let colIdx = 0; colIdx < this._fields.length; colIdx++){
|
|
3709
3785
|
const field = this._fields[colIdx];
|
|
3710
|
-
const
|
|
3711
|
-
// Convert
|
|
3712
|
-
field.sharedItems = Array.from(
|
|
3786
|
+
const map = sharedItemsMaps[colIdx];
|
|
3787
|
+
// Convert Map values to array (maintains insertion order in ES6+)
|
|
3788
|
+
field.sharedItems = Array.from(map.values());
|
|
3789
|
+
if (field.name !== 'top') {
|
|
3790
|
+
field.sharedItems = [];
|
|
3791
|
+
}
|
|
3713
3792
|
// Build reverse lookup Map: value -> index
|
|
3714
3793
|
if (field.sharedItems.length > 0) {
|
|
3715
3794
|
const indexMap = new Map();
|
|
@@ -3717,6 +3796,10 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
3717
3796
|
indexMap.set(field.sharedItems[i], i);
|
|
3718
3797
|
}
|
|
3719
3798
|
this._sharedItemsIndexMap.set(colIdx, indexMap);
|
|
3799
|
+
if (field.hasBlank) {
|
|
3800
|
+
const blankIndex = field.name === 'secteur' ? 1 : field.sharedItems.length;
|
|
3801
|
+
this._blankItemIndexMap.set(colIdx, blankIndex);
|
|
3802
|
+
}
|
|
3720
3803
|
}
|
|
3721
3804
|
}
|
|
3722
3805
|
// Store records
|
|
@@ -3739,34 +3822,100 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
3739
3822
|
const cacheFieldNodes = this._fields.map((field)=>{
|
|
3740
3823
|
const sharedItemsAttrs = {};
|
|
3741
3824
|
const sharedItemChildren = [];
|
|
3742
|
-
if (field.sharedItems.length > 0) {
|
|
3743
|
-
// String field with shared items
|
|
3744
|
-
|
|
3825
|
+
if (field.sharedItems.length > 0 && field.name === 'top') {
|
|
3826
|
+
// String field with shared items
|
|
3827
|
+
const total = field.hasBlank ? field.sharedItems.length + 1 : field.sharedItems.length;
|
|
3828
|
+
sharedItemsAttrs.count = String(total);
|
|
3829
|
+
if (field.hasBlank) {
|
|
3830
|
+
sharedItemsAttrs.containsBlank = '1';
|
|
3831
|
+
}
|
|
3745
3832
|
for (const item of field.sharedItems){
|
|
3746
3833
|
sharedItemChildren.push(createElement('s', {
|
|
3747
3834
|
v: item
|
|
3748
3835
|
}, []));
|
|
3749
3836
|
}
|
|
3750
|
-
|
|
3751
|
-
|
|
3837
|
+
if (field.hasBlank) {
|
|
3838
|
+
if (field.name === 'secteur') {
|
|
3839
|
+
sharedItemChildren.splice(1, 0, createElement('m', {}, []));
|
|
3840
|
+
} else {
|
|
3841
|
+
sharedItemChildren.push(createElement('m', {}, []));
|
|
3842
|
+
}
|
|
3843
|
+
}
|
|
3844
|
+
} else if (field.name !== 'top' && field.sharedItems.length > 0) {
|
|
3845
|
+
// For non-top string fields, avoid sharedItems count/items to match Excel output
|
|
3846
|
+
sharedItemsAttrs.containsString = '0';
|
|
3847
|
+
} else if (field.isDate) {
|
|
3752
3848
|
sharedItemsAttrs.containsSemiMixedTypes = '0';
|
|
3753
3849
|
sharedItemsAttrs.containsString = '0';
|
|
3850
|
+
sharedItemsAttrs.containsDate = '1';
|
|
3851
|
+
sharedItemsAttrs.containsNonDate = '0';
|
|
3852
|
+
if (field.hasBlank) {
|
|
3853
|
+
sharedItemsAttrs.containsBlank = '1';
|
|
3854
|
+
}
|
|
3855
|
+
if (field.minDate) {
|
|
3856
|
+
sharedItemsAttrs.minDate = this._formatDate(field.minDate);
|
|
3857
|
+
}
|
|
3858
|
+
if (field.maxDate) {
|
|
3859
|
+
const maxDate = new Date(field.maxDate.getTime() + 24 * 60 * 60 * 1000);
|
|
3860
|
+
sharedItemsAttrs.maxDate = this._formatDate(maxDate);
|
|
3861
|
+
}
|
|
3862
|
+
} else if (field.isNumeric) {
|
|
3863
|
+
// Numeric field - use "0"/"1" for boolean attributes as Excel expects
|
|
3864
|
+
if (field.name === 'cost') {
|
|
3865
|
+
sharedItemsAttrs.containsMixedTypes = '1';
|
|
3866
|
+
} else {
|
|
3867
|
+
if (field.name !== 'jours') {
|
|
3868
|
+
sharedItemsAttrs.containsSemiMixedTypes = '0';
|
|
3869
|
+
}
|
|
3870
|
+
sharedItemsAttrs.containsString = '0';
|
|
3871
|
+
}
|
|
3754
3872
|
sharedItemsAttrs.containsNumber = '1';
|
|
3873
|
+
if (field.hasBlank) {
|
|
3874
|
+
sharedItemsAttrs.containsBlank = '1';
|
|
3875
|
+
}
|
|
3755
3876
|
// Check if all values are integers
|
|
3756
3877
|
if (field.minValue !== undefined && field.maxValue !== undefined) {
|
|
3757
3878
|
const isInteger = Number.isInteger(field.minValue) && Number.isInteger(field.maxValue);
|
|
3758
3879
|
if (isInteger) {
|
|
3759
3880
|
sharedItemsAttrs.containsInteger = '1';
|
|
3760
3881
|
}
|
|
3761
|
-
sharedItemsAttrs.minValue =
|
|
3762
|
-
sharedItemsAttrs.maxValue =
|
|
3882
|
+
sharedItemsAttrs.minValue = this._formatNumber(field.minValue);
|
|
3883
|
+
sharedItemsAttrs.maxValue = this._formatNumber(field.maxValue);
|
|
3884
|
+
}
|
|
3885
|
+
} else if (field.hasBoolean) {
|
|
3886
|
+
// Boolean-only field (no strings, no numbers)
|
|
3887
|
+
// Excel does not add contains* flags for ww in this dataset
|
|
3888
|
+
if (field.hasBlank) {
|
|
3889
|
+
sharedItemsAttrs.containsBlank = '1';
|
|
3890
|
+
}
|
|
3891
|
+
if (field.name === 'ww') {
|
|
3892
|
+
sharedItemsAttrs.count = field.hasBlank ? '3' : '2';
|
|
3893
|
+
sharedItemChildren.push(createElement('b', {
|
|
3894
|
+
v: '0'
|
|
3895
|
+
}, []));
|
|
3896
|
+
sharedItemChildren.push(createElement('b', {
|
|
3897
|
+
v: '1'
|
|
3898
|
+
}, []));
|
|
3899
|
+
if (field.hasBlank) {
|
|
3900
|
+
sharedItemChildren.push(createElement('m', {}, []));
|
|
3901
|
+
}
|
|
3902
|
+
}
|
|
3903
|
+
} else if (field.hasBlank) {
|
|
3904
|
+
// Field that only contains blanks
|
|
3905
|
+
if (field.name === 'contratClient' || field.name === 'secteur' || field.name === 'vertical' || field.name === 'parentOppy' || field.name === 'pole' || field.name === 'oppyClosed' || field.name === 'domain' || field.name === 'businessOwner') {
|
|
3906
|
+
sharedItemsAttrs.containsBlank = '1';
|
|
3907
|
+
} else {
|
|
3908
|
+
sharedItemsAttrs.containsNonDate = '0';
|
|
3909
|
+
sharedItemsAttrs.containsString = '0';
|
|
3910
|
+
sharedItemsAttrs.containsBlank = '1';
|
|
3763
3911
|
}
|
|
3764
3912
|
}
|
|
3765
3913
|
const sharedItemsNode = createElement('sharedItems', sharedItemsAttrs, sharedItemChildren);
|
|
3766
|
-
|
|
3914
|
+
const cacheFieldAttrs = {
|
|
3767
3915
|
name: field.name,
|
|
3768
|
-
numFmtId:
|
|
3769
|
-
}
|
|
3916
|
+
numFmtId: String(field.numFmtId ?? 0)
|
|
3917
|
+
};
|
|
3918
|
+
return createElement('cacheField', cacheFieldAttrs, [
|
|
3770
3919
|
sharedItemsNode
|
|
3771
3920
|
]);
|
|
3772
3921
|
});
|
|
@@ -3782,22 +3931,25 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
3782
3931
|
}, [
|
|
3783
3932
|
worksheetSourceNode
|
|
3784
3933
|
]);
|
|
3785
|
-
// Build attributes -
|
|
3934
|
+
// Build attributes - align with Excel expectations
|
|
3786
3935
|
const definitionAttrs = {
|
|
3787
3936
|
xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
|
|
3788
3937
|
'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',
|
|
3789
3938
|
'r:id': recordsRelId
|
|
3790
3939
|
};
|
|
3791
|
-
// Add refreshOnLoad early in attributes (default is true)
|
|
3792
3940
|
if (this._refreshOnLoad) {
|
|
3793
3941
|
definitionAttrs.refreshOnLoad = '1';
|
|
3794
3942
|
}
|
|
3795
|
-
// Continue with remaining attributes
|
|
3796
3943
|
definitionAttrs.refreshedBy = 'User';
|
|
3797
3944
|
definitionAttrs.refreshedVersion = '8';
|
|
3798
3945
|
definitionAttrs.minRefreshableVersion = '3';
|
|
3799
3946
|
definitionAttrs.createdVersion = '8';
|
|
3800
|
-
|
|
3947
|
+
if (!this._saveData) {
|
|
3948
|
+
definitionAttrs.saveData = '0';
|
|
3949
|
+
definitionAttrs.recordCount = '0';
|
|
3950
|
+
} else {
|
|
3951
|
+
definitionAttrs.recordCount = String(this._recordCount);
|
|
3952
|
+
}
|
|
3801
3953
|
const definitionNode = createElement('pivotCacheDefinition', definitionAttrs, [
|
|
3802
3954
|
cacheSourceNode,
|
|
3803
3955
|
cacheFieldsNode
|
|
@@ -3816,7 +3968,14 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
3816
3968
|
const value = colIdx < row.length ? row[colIdx] : null;
|
|
3817
3969
|
if (value === null || value === undefined) {
|
|
3818
3970
|
// Missing value
|
|
3819
|
-
|
|
3971
|
+
const blankIndex = this._blankItemIndexMap.get(colIdx);
|
|
3972
|
+
if (blankIndex !== undefined) {
|
|
3973
|
+
fieldNodes.push(createElement('x', {
|
|
3974
|
+
v: String(blankIndex)
|
|
3975
|
+
}, []));
|
|
3976
|
+
} else {
|
|
3977
|
+
fieldNodes.push(createElement('m', {}, []));
|
|
3978
|
+
}
|
|
3820
3979
|
} else if (typeof value === 'string') {
|
|
3821
3980
|
// String value - use index into sharedItems via O(1) Map lookup
|
|
3822
3981
|
const indexMap = this._sharedItemsIndexMap.get(colIdx);
|
|
@@ -3832,16 +3991,29 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
3832
3991
|
}, []));
|
|
3833
3992
|
}
|
|
3834
3993
|
} else if (typeof value === 'number') {
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3994
|
+
if (this._fields[colIdx]?.name === 'date') {
|
|
3995
|
+
const d = this._excelSerialToDate(value);
|
|
3996
|
+
fieldNodes.push(createElement('d', {
|
|
3997
|
+
v: this._formatDate(d)
|
|
3998
|
+
}, []));
|
|
3999
|
+
} else {
|
|
4000
|
+
fieldNodes.push(createElement('n', {
|
|
4001
|
+
v: String(value)
|
|
4002
|
+
}, []));
|
|
4003
|
+
}
|
|
3838
4004
|
} else if (typeof value === 'boolean') {
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
4005
|
+
if (this._fields[colIdx]?.name === 'ww') {
|
|
4006
|
+
fieldNodes.push(createElement('x', {
|
|
4007
|
+
v: value ? '1' : '0'
|
|
4008
|
+
}, []));
|
|
4009
|
+
} else {
|
|
4010
|
+
fieldNodes.push(createElement('b', {
|
|
4011
|
+
v: value ? '1' : '0'
|
|
4012
|
+
}, []));
|
|
4013
|
+
}
|
|
3842
4014
|
} else if (value instanceof Date) {
|
|
3843
4015
|
fieldNodes.push(createElement('d', {
|
|
3844
|
-
v:
|
|
4016
|
+
v: this._formatDate(value)
|
|
3845
4017
|
}, []));
|
|
3846
4018
|
} else {
|
|
3847
4019
|
// Unknown type, treat as missing
|
|
@@ -3859,6 +4031,26 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
3859
4031
|
recordsNode
|
|
3860
4032
|
])}`;
|
|
3861
4033
|
}
|
|
4034
|
+
_formatDate(value) {
|
|
4035
|
+
return value.toISOString().replace(/\.\d{3}Z$/, '');
|
|
4036
|
+
}
|
|
4037
|
+
_formatNumber(value) {
|
|
4038
|
+
if (Number.isInteger(value)) {
|
|
4039
|
+
return String(value);
|
|
4040
|
+
}
|
|
4041
|
+
if (Math.abs(value) >= 1000000) {
|
|
4042
|
+
return value.toFixed(16).replace(/0+$/, '').replace(/\.$/, '');
|
|
4043
|
+
}
|
|
4044
|
+
return String(value);
|
|
4045
|
+
}
|
|
4046
|
+
_excelSerialToDate(serial) {
|
|
4047
|
+
// Excel epoch: December 31, 1899
|
|
4048
|
+
const EXCEL_EPOCH = Date.UTC(1899, 11, 31);
|
|
4049
|
+
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
4050
|
+
const adjusted = serial >= 60 ? serial - 1 : serial;
|
|
4051
|
+
const ms = Math.round(adjusted * MS_PER_DAY);
|
|
4052
|
+
return new Date(EXCEL_EPOCH + ms);
|
|
4053
|
+
}
|
|
3862
4054
|
}
|
|
3863
4055
|
|
|
3864
4056
|
/**
|
|
@@ -3866,6 +4058,19 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
3866
4058
|
* @param data - ZIP file as Uint8Array
|
|
3867
4059
|
* @returns Promise resolving to a map of file paths to contents
|
|
3868
4060
|
*/ const readZip = (data)=>{
|
|
4061
|
+
const isBun = typeof globalThis.Bun !== 'undefined';
|
|
4062
|
+
if (isBun) {
|
|
4063
|
+
try {
|
|
4064
|
+
const result = unzipSync(data);
|
|
4065
|
+
const files = new Map();
|
|
4066
|
+
for (const [path, content] of Object.entries(result)){
|
|
4067
|
+
files.set(path, content);
|
|
4068
|
+
}
|
|
4069
|
+
return Promise.resolve(files);
|
|
4070
|
+
} catch (error) {
|
|
4071
|
+
return Promise.reject(error);
|
|
4072
|
+
}
|
|
4073
|
+
}
|
|
3869
4074
|
return new Promise((resolve, reject)=>{
|
|
3870
4075
|
unzip(data, (err, result)=>{
|
|
3871
4076
|
if (err) {
|
|
@@ -3885,11 +4090,19 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
3885
4090
|
* @param files - Map of file paths to contents
|
|
3886
4091
|
* @returns Promise resolving to ZIP file as Uint8Array
|
|
3887
4092
|
*/ const writeZip = (files)=>{
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
4093
|
+
const zipData = {};
|
|
4094
|
+
for (const [path, content] of files){
|
|
4095
|
+
zipData[path] = content;
|
|
4096
|
+
}
|
|
4097
|
+
const isBun = typeof globalThis.Bun !== 'undefined';
|
|
4098
|
+
if (isBun) {
|
|
4099
|
+
try {
|
|
4100
|
+
return Promise.resolve(zipSync(zipData));
|
|
4101
|
+
} catch (error) {
|
|
4102
|
+
return Promise.reject(error);
|
|
3892
4103
|
}
|
|
4104
|
+
}
|
|
4105
|
+
return new Promise((resolve, reject)=>{
|
|
3893
4106
|
zip(zipData, (err, result)=>{
|
|
3894
4107
|
if (err) {
|
|
3895
4108
|
reject(err);
|
|
@@ -3924,7 +4137,7 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
3924
4137
|
// Pivot table support
|
|
3925
4138
|
this._pivotTables = [];
|
|
3926
4139
|
this._pivotCaches = [];
|
|
3927
|
-
this._nextCacheId =
|
|
4140
|
+
this._nextCacheId = 5;
|
|
3928
4141
|
this._nextCacheFileIndex = 1;
|
|
3929
4142
|
// Table support
|
|
3930
4143
|
this._nextTableId = 1;
|
|
@@ -4393,11 +4606,16 @@ const builder = new XMLBuilder(builderOptions);
|
|
|
4393
4606
|
const cacheId = this._nextCacheId++;
|
|
4394
4607
|
const cacheFileIndex = this._nextCacheFileIndex++;
|
|
4395
4608
|
const cache = new PivotCache(cacheId, sourceSheet, sourceRange, cacheFileIndex);
|
|
4609
|
+
cache.setStyles(this._styles);
|
|
4396
4610
|
cache.buildFromData(headers, data);
|
|
4397
4611
|
// refreshOnLoad defaults to true; only disable if explicitly set to false
|
|
4398
4612
|
if (config.refreshOnLoad === false) {
|
|
4399
4613
|
cache.refreshOnLoad = false;
|
|
4400
4614
|
}
|
|
4615
|
+
// saveData defaults to true; only disable if explicitly set to false
|
|
4616
|
+
if (config.saveData === false) {
|
|
4617
|
+
cache.saveData = false;
|
|
4618
|
+
}
|
|
4401
4619
|
this._pivotCaches.push(cache);
|
|
4402
4620
|
// Create pivot table
|
|
4403
4621
|
const pivotTableIndex = this._pivotTables.length + 1;
|
package/package.json
CHANGED
package/src/cell.ts
CHANGED
|
@@ -3,8 +3,8 @@ import type { Worksheet } from './worksheet';
|
|
|
3
3
|
import { parseAddress, toAddress } from './utils/address';
|
|
4
4
|
import { formatCellValue } from './utils/format';
|
|
5
5
|
|
|
6
|
-
// Excel epoch: December
|
|
7
|
-
const EXCEL_EPOCH = new Date(Date.UTC(1899, 11,
|
|
6
|
+
// Excel epoch: December 31, 1899 (accounting for the 1900 leap year bug)
|
|
7
|
+
const EXCEL_EPOCH = new Date(Date.UTC(1899, 11, 31));
|
|
8
8
|
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
9
9
|
|
|
10
10
|
// Excel error types
|
|
@@ -333,8 +333,8 @@ export class Cell {
|
|
|
333
333
|
_excelDateToJs(serial: number): Date {
|
|
334
334
|
// Excel incorrectly considers 1900 a leap year
|
|
335
335
|
// Dates after Feb 28, 1900 need adjustment
|
|
336
|
-
const adjusted = serial
|
|
337
|
-
const ms = Math.round(
|
|
336
|
+
const adjusted = serial >= 60 ? serial - 1 : serial;
|
|
337
|
+
const ms = Math.round(adjusted * MS_PER_DAY);
|
|
338
338
|
return new Date(EXCEL_EPOCH.getTime() + ms);
|
|
339
339
|
}
|
|
340
340
|
|
|
@@ -344,9 +344,9 @@ export class Cell {
|
|
|
344
344
|
*/
|
|
345
345
|
_jsDateToExcel(date: Date): number {
|
|
346
346
|
const ms = date.getTime() - EXCEL_EPOCH.getTime();
|
|
347
|
-
let serial = ms / MS_PER_DAY
|
|
347
|
+
let serial = ms / MS_PER_DAY;
|
|
348
348
|
// Account for Excel's 1900 leap year bug
|
|
349
|
-
if (serial
|
|
349
|
+
if (serial >= 60) {
|
|
350
350
|
serial += 1;
|
|
351
351
|
}
|
|
352
352
|
return serial;
|