@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.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 30, 1899 (accounting for the 1900 leap year bug)
436
- const EXCEL_EPOCH = new Date(Date.UTC(1899, 11, 30));
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 > 60 ? serial - 1 : serial;
714
- const ms = Math.round((adjusted - 1) * MS_PER_DAY);
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 + 1;
722
+ let serial = ms / MS_PER_DAY;
723
723
  // Account for Excel's 1900 leap year bug
724
- if (serial > 60) {
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, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/&apos;/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: hasNumberFormats ? '1' : '0',
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 Sets for O(1) unique value collection during analysis
3678
- const sharedItemsSets = this._fields.map(()=>new Set());
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
- // O(1) Set.add instead of O(n) Array.includes + push
3690
- sharedItemsSets[colIdx].add(value);
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 set = sharedItemsSets[colIdx];
3711
- // Convert Set to array (maintains insertion order in ES6+)
3712
- field.sharedItems = Array.from(set);
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 - Excel just uses count attribute
3744
- sharedItemsAttrs.count = String(field.sharedItems.length);
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
- } else if (field.isNumeric) {
3751
- // Numeric field - use "0"/"1" for boolean attributes as Excel expects
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 = String(field.minValue);
3762
- sharedItemsAttrs.maxValue = String(field.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
- return createElement('cacheField', {
3914
+ const cacheFieldAttrs = {
3767
3915
  name: field.name,
3768
- numFmtId: '0'
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 - refreshOnLoad should come early per OOXML schema
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
- definitionAttrs.recordCount = String(this._recordCount);
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
- fieldNodes.push(createElement('m', {}, []));
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
- fieldNodes.push(createElement('n', {
3836
- v: String(value)
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
- fieldNodes.push(createElement('b', {
3840
- v: value ? '1' : '0'
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: value.toISOString()
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
- return new Promise((resolve, reject)=>{
3889
- const zipData = {};
3890
- for (const [path, content] of files){
3891
- zipData[path] = content;
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 = 0;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@niicojs/excel",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "typescript library to manipulate excel files",
5
5
  "homepage": "https://github.com/niicojs/excel#readme",
6
6
  "bugs": {
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 30, 1899 (accounting for the 1900 leap year bug)
7
- const EXCEL_EPOCH = new Date(Date.UTC(1899, 11, 30));
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 > 60 ? serial - 1 : serial;
337
- const ms = Math.round((adjusted - 1) * MS_PER_DAY);
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 + 1;
347
+ let serial = ms / MS_PER_DAY;
348
348
  // Account for Excel's 1900 leap year bug
349
- if (serial > 60) {
349
+ if (serial >= 60) {
350
350
  serial += 1;
351
351
  }
352
352
  return serial;