@niicojs/excel 0.2.5 → 0.2.7

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
@@ -1115,6 +1115,129 @@ const builder = new XMLBuilder(builderOptions);
1115
1115
  }
1116
1116
  }
1117
1117
 
1118
+ /**
1119
+ * Excel built-in number format IDs (0-163 are reserved).
1120
+ * These formats don't need to be defined in the numFmts element.
1121
+ */ const BUILTIN_NUM_FMTS = new Map([
1122
+ [
1123
+ 'General',
1124
+ 0
1125
+ ],
1126
+ [
1127
+ '0',
1128
+ 1
1129
+ ],
1130
+ [
1131
+ '0.00',
1132
+ 2
1133
+ ],
1134
+ [
1135
+ '#,##0',
1136
+ 3
1137
+ ],
1138
+ [
1139
+ '#,##0.00',
1140
+ 4
1141
+ ],
1142
+ [
1143
+ '0%',
1144
+ 9
1145
+ ],
1146
+ [
1147
+ '0.00%',
1148
+ 10
1149
+ ],
1150
+ [
1151
+ '0.00E+00',
1152
+ 11
1153
+ ],
1154
+ [
1155
+ '# ?/?',
1156
+ 12
1157
+ ],
1158
+ [
1159
+ '# ??/??',
1160
+ 13
1161
+ ],
1162
+ [
1163
+ 'mm-dd-yy',
1164
+ 14
1165
+ ],
1166
+ [
1167
+ 'd-mmm-yy',
1168
+ 15
1169
+ ],
1170
+ [
1171
+ 'd-mmm',
1172
+ 16
1173
+ ],
1174
+ [
1175
+ 'mmm-yy',
1176
+ 17
1177
+ ],
1178
+ [
1179
+ 'h:mm AM/PM',
1180
+ 18
1181
+ ],
1182
+ [
1183
+ 'h:mm:ss AM/PM',
1184
+ 19
1185
+ ],
1186
+ [
1187
+ 'h:mm',
1188
+ 20
1189
+ ],
1190
+ [
1191
+ 'h:mm:ss',
1192
+ 21
1193
+ ],
1194
+ [
1195
+ 'm/d/yy h:mm',
1196
+ 22
1197
+ ],
1198
+ [
1199
+ '#,##0 ;(#,##0)',
1200
+ 37
1201
+ ],
1202
+ [
1203
+ '#,##0 ;[Red](#,##0)',
1204
+ 38
1205
+ ],
1206
+ [
1207
+ '#,##0.00;(#,##0.00)',
1208
+ 39
1209
+ ],
1210
+ [
1211
+ '#,##0.00;[Red](#,##0.00)',
1212
+ 40
1213
+ ],
1214
+ [
1215
+ 'mm:ss',
1216
+ 45
1217
+ ],
1218
+ [
1219
+ '[h]:mm:ss',
1220
+ 46
1221
+ ],
1222
+ [
1223
+ 'mmss.0',
1224
+ 47
1225
+ ],
1226
+ [
1227
+ '##0.0E+0',
1228
+ 48
1229
+ ],
1230
+ [
1231
+ '@',
1232
+ 49
1233
+ ]
1234
+ ]);
1235
+ /**
1236
+ * Reverse lookup: built-in format ID -> format code
1237
+ */ const BUILTIN_NUM_FMT_CODES = new Map(Array.from(BUILTIN_NUM_FMTS.entries()).map(([code, id])=>[
1238
+ id,
1239
+ code
1240
+ ]));
1118
1241
  /**
1119
1242
  * Normalize a color to ARGB format (8 hex chars).
1120
1243
  * Accepts: "#RGB", "#RRGGBB", "RGB", "RRGGBB", "AARRGGBB", "#AARRGGBB"
@@ -1305,7 +1428,8 @@ const builder = new XMLBuilder(builderOptions);
1305
1428
  const font = this._fonts[xf.fontId];
1306
1429
  const fill = this._fills[xf.fillId];
1307
1430
  const border = this._borders[xf.borderId];
1308
- const numFmt = this._numFmts.get(xf.numFmtId);
1431
+ // Check custom formats first, then fall back to built-in format codes
1432
+ const numFmt = this._numFmts.get(xf.numFmtId) ?? BUILTIN_NUM_FMT_CODES.get(xf.numFmtId);
1309
1433
  const style = {};
1310
1434
  if (font) {
1311
1435
  if (font.bold) style.bold = true;
@@ -1437,16 +1561,30 @@ const builder = new XMLBuilder(builderOptions);
1437
1561
  return this._borders.length - 1;
1438
1562
  }
1439
1563
  _findOrCreateNumFmt(format) {
1440
- // Check if already exists
1564
+ // Check built-in formats first (IDs 0-163)
1565
+ const builtinId = BUILTIN_NUM_FMTS.get(format);
1566
+ if (builtinId !== undefined) {
1567
+ return builtinId;
1568
+ }
1569
+ // Check if already exists in custom formats
1441
1570
  for (const [id, code] of this._numFmts){
1442
1571
  if (code === format) return id;
1443
1572
  }
1444
- // Create new (custom formats start at 164)
1445
- const id = Math.max(164, ...Array.from(this._numFmts.keys())) + 1;
1573
+ // Create new custom format (IDs 164+)
1574
+ const existingIds = Array.from(this._numFmts.keys());
1575
+ const id = existingIds.length > 0 ? Math.max(...existingIds) + 1 : 164;
1446
1576
  this._numFmts.set(id, format);
1447
1577
  return id;
1448
1578
  }
1449
1579
  /**
1580
+ * Get or create a number format ID for the given format string.
1581
+ * Returns built-in IDs (0-163) for standard formats, or creates custom IDs (164+).
1582
+ * @param format - The number format string (e.g., '0.00', '#,##0', '$#,##0.00')
1583
+ */ getOrCreateNumFmtId(format) {
1584
+ this._dirty = true;
1585
+ return this._findOrCreateNumFmt(format);
1586
+ }
1587
+ /**
1450
1588
  * Check if styles have been modified
1451
1589
  */ get dirty() {
1452
1590
  return this._dirty;
@@ -1626,6 +1764,7 @@ const builder = new XMLBuilder(builderOptions);
1626
1764
  this._columnFields = [];
1627
1765
  this._valueFields = [];
1628
1766
  this._filterFields = [];
1767
+ this._styles = null;
1629
1768
  this._name = name;
1630
1769
  this._cache = cache;
1631
1770
  this._targetSheet = targetSheet;
@@ -1660,6 +1799,13 @@ const builder = new XMLBuilder(builderOptions);
1660
1799
  return this._pivotTableIndex;
1661
1800
  }
1662
1801
  /**
1802
+ * Set the styles reference for number format resolution
1803
+ * @internal
1804
+ */ setStyles(styles) {
1805
+ this._styles = styles;
1806
+ return this;
1807
+ }
1808
+ /**
1663
1809
  * Add a field to the row area
1664
1810
  * @param fieldName - Name of the source field (column header)
1665
1811
  */ addRowField(fieldName) {
@@ -1689,23 +1835,40 @@ const builder = new XMLBuilder(builderOptions);
1689
1835
  });
1690
1836
  return this;
1691
1837
  }
1692
- /**
1693
- * Add a field to the values area with aggregation
1694
- * @param fieldName - Name of the source field (column header)
1695
- * @param aggregation - Aggregation function (sum, count, average, min, max)
1696
- * @param displayName - Optional display name (defaults to "Sum of FieldName")
1697
- */ addValueField(fieldName, aggregation = 'sum', displayName) {
1838
+ addValueField(fieldNameOrConfig, aggregation = 'sum', displayName, numberFormat) {
1839
+ // Normalize arguments to a common form
1840
+ let fieldName;
1841
+ let agg;
1842
+ let name;
1843
+ let format;
1844
+ if (typeof fieldNameOrConfig === 'object') {
1845
+ fieldName = fieldNameOrConfig.field;
1846
+ agg = fieldNameOrConfig.aggregation ?? 'sum';
1847
+ name = fieldNameOrConfig.name;
1848
+ format = fieldNameOrConfig.numberFormat;
1849
+ } else {
1850
+ fieldName = fieldNameOrConfig;
1851
+ agg = aggregation;
1852
+ name = displayName;
1853
+ format = numberFormat;
1854
+ }
1698
1855
  const fieldIndex = this._cache.getFieldIndex(fieldName);
1699
1856
  if (fieldIndex < 0) {
1700
1857
  throw new Error(`Field not found in source data: ${fieldName}`);
1701
1858
  }
1702
- const defaultName = `${aggregation.charAt(0).toUpperCase() + aggregation.slice(1)} of ${fieldName}`;
1859
+ const defaultName = `${agg.charAt(0).toUpperCase() + agg.slice(1)} of ${fieldName}`;
1860
+ // Resolve numFmtId immediately if format is provided and styles are available
1861
+ let numFmtId;
1862
+ if (format && this._styles) {
1863
+ numFmtId = this._styles.getOrCreateNumFmtId(format);
1864
+ }
1703
1865
  this._valueFields.push({
1704
1866
  fieldName,
1705
1867
  fieldIndex,
1706
1868
  axis: 'value',
1707
- aggregation,
1708
- displayName: displayName || defaultName
1869
+ aggregation: agg,
1870
+ displayName: name || defaultName,
1871
+ numFmtId
1709
1872
  });
1710
1873
  return this;
1711
1874
  }
@@ -1828,17 +1991,26 @@ const builder = new XMLBuilder(builderOptions);
1828
1991
  }
1829
1992
  // Data fields (values)
1830
1993
  if (this._valueFields.length > 0) {
1831
- const dataFieldNodes = this._valueFields.map((f)=>createElement('dataField', {
1994
+ const dataFieldNodes = this._valueFields.map((f)=>{
1995
+ const attrs = {
1832
1996
  name: f.displayName || f.fieldName,
1833
1997
  fld: String(f.fieldIndex),
1834
1998
  baseField: '0',
1835
1999
  baseItem: '0',
1836
2000
  subtotal: f.aggregation || 'sum'
1837
- }, []));
2001
+ };
2002
+ // Add numFmtId if it was resolved during addValueField
2003
+ if (f.numFmtId !== undefined) {
2004
+ attrs.numFmtId = String(f.numFmtId);
2005
+ }
2006
+ return createElement('dataField', attrs, []);
2007
+ });
1838
2008
  children.push(createElement('dataFields', {
1839
2009
  count: String(dataFieldNodes.length)
1840
2010
  }, dataFieldNodes));
1841
2011
  }
2012
+ // Check if any value field has a number format
2013
+ const hasNumberFormats = this._valueFields.some((f)=>f.numFmtId !== undefined);
1842
2014
  // Pivot table style
1843
2015
  children.push(createElement('pivotTableStyleInfo', {
1844
2016
  name: 'PivotStyleMedium9',
@@ -1853,7 +2025,7 @@ const builder = new XMLBuilder(builderOptions);
1853
2025
  'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',
1854
2026
  name: this._name,
1855
2027
  cacheId: String(this._cache.cacheId),
1856
- applyNumberFormats: '0',
2028
+ applyNumberFormats: hasNumberFormats ? '1' : '0',
1857
2029
  applyBorderFormats: '0',
1858
2030
  applyFontFormats: '0',
1859
2031
  applyPatternFormats: '0',
@@ -2787,6 +2959,8 @@ const builder = new XMLBuilder(builderOptions);
2787
2959
  // Create pivot table
2788
2960
  const pivotTableIndex = this._pivotTables.length + 1;
2789
2961
  const pivotTable = new PivotTable(config.name, cache, targetSheet, targetCell, targetAddr.row + 1, targetAddr.col, pivotTableIndex);
2962
+ // Set styles reference for number format resolution
2963
+ pivotTable.setStyles(this._styles);
2790
2964
  this._pivotTables.push(pivotTable);
2791
2965
  return pivotTable;
2792
2966
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@niicojs/excel",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "description": "typescript library to manipulate excel files",
5
5
  "homepage": "https://github.com/niicojs/excel#readme",
6
6
  "bugs": {
@@ -1,4 +1,5 @@
1
- import type { AggregationType, PivotFieldAxis } from './types';
1
+ import type { AggregationType, PivotFieldAxis, PivotValueConfig } from './types';
2
+ import type { Styles } from './styles';
2
3
  import { PivotCache } from './pivot-cache';
3
4
  import { createElement, stringifyXml, XmlNode } from './utils/xml';
4
5
 
@@ -11,6 +12,7 @@ interface FieldAssignment {
11
12
  axis: PivotFieldAxis;
12
13
  aggregation?: AggregationType;
13
14
  displayName?: string;
15
+ numFmtId?: number;
14
16
  }
15
17
 
16
18
  /**
@@ -30,6 +32,7 @@ export class PivotTable {
30
32
  private _filterFields: FieldAssignment[] = [];
31
33
 
32
34
  private _pivotTableIndex: number;
35
+ private _styles: Styles | null = null;
33
36
 
34
37
  constructor(
35
38
  name: string,
@@ -84,6 +87,15 @@ export class PivotTable {
84
87
  return this._pivotTableIndex;
85
88
  }
86
89
 
90
+ /**
91
+ * Set the styles reference for number format resolution
92
+ * @internal
93
+ */
94
+ setStyles(styles: Styles): this {
95
+ this._styles = styles;
96
+ return this;
97
+ }
98
+
87
99
  /**
88
100
  * Add a field to the row area
89
101
  * @param fieldName - Name of the source field (column header)
@@ -123,25 +135,70 @@ export class PivotTable {
123
135
  }
124
136
 
125
137
  /**
126
- * Add a field to the values area with aggregation
127
- * @param fieldName - Name of the source field (column header)
128
- * @param aggregation - Aggregation function (sum, count, average, min, max)
129
- * @param displayName - Optional display name (defaults to "Sum of FieldName")
138
+ * Add a field to the values area with aggregation.
139
+ *
140
+ * Supports two call signatures:
141
+ * - Positional: `addValueField(fieldName, aggregation?, displayName?, numberFormat?)`
142
+ * - Object: `addValueField({ field, aggregation?, name?, numberFormat? })`
143
+ *
144
+ * @example
145
+ * // Positional arguments
146
+ * pivot.addValueField('Sales', 'sum', 'Total Sales', '$#,##0.00');
147
+ *
148
+ * // Object form
149
+ * pivot.addValueField({ field: 'Sales', aggregation: 'sum', name: 'Total Sales', numberFormat: '$#,##0.00' });
130
150
  */
131
- addValueField(fieldName: string, aggregation: AggregationType = 'sum', displayName?: string): this {
151
+ addValueField(config: PivotValueConfig): this;
152
+ addValueField(
153
+ fieldName: string,
154
+ aggregation?: AggregationType,
155
+ displayName?: string,
156
+ numberFormat?: string,
157
+ ): this;
158
+ addValueField(
159
+ fieldNameOrConfig: string | PivotValueConfig,
160
+ aggregation: AggregationType = 'sum',
161
+ displayName?: string,
162
+ numberFormat?: string,
163
+ ): this {
164
+ // Normalize arguments to a common form
165
+ let fieldName: string;
166
+ let agg: AggregationType;
167
+ let name: string | undefined;
168
+ let format: string | undefined;
169
+
170
+ if (typeof fieldNameOrConfig === 'object') {
171
+ fieldName = fieldNameOrConfig.field;
172
+ agg = fieldNameOrConfig.aggregation ?? 'sum';
173
+ name = fieldNameOrConfig.name;
174
+ format = fieldNameOrConfig.numberFormat;
175
+ } else {
176
+ fieldName = fieldNameOrConfig;
177
+ agg = aggregation;
178
+ name = displayName;
179
+ format = numberFormat;
180
+ }
181
+
132
182
  const fieldIndex = this._cache.getFieldIndex(fieldName);
133
183
  if (fieldIndex < 0) {
134
184
  throw new Error(`Field not found in source data: ${fieldName}`);
135
185
  }
136
186
 
137
- const defaultName = `${aggregation.charAt(0).toUpperCase() + aggregation.slice(1)} of ${fieldName}`;
187
+ const defaultName = `${agg.charAt(0).toUpperCase() + agg.slice(1)} of ${fieldName}`;
188
+
189
+ // Resolve numFmtId immediately if format is provided and styles are available
190
+ let numFmtId: number | undefined;
191
+ if (format && this._styles) {
192
+ numFmtId = this._styles.getOrCreateNumFmtId(format);
193
+ }
138
194
 
139
195
  this._valueFields.push({
140
196
  fieldName,
141
197
  fieldIndex,
142
198
  axis: 'value',
143
- aggregation,
144
- displayName: displayName || defaultName,
199
+ aggregation: agg,
200
+ displayName: name || defaultName,
201
+ numFmtId,
145
202
  });
146
203
 
147
204
  return this;
@@ -251,22 +308,28 @@ export class PivotTable {
251
308
 
252
309
  // Data fields (values)
253
310
  if (this._valueFields.length > 0) {
254
- const dataFieldNodes = this._valueFields.map((f) =>
255
- createElement(
256
- 'dataField',
257
- {
258
- name: f.displayName || f.fieldName,
259
- fld: String(f.fieldIndex),
260
- baseField: '0',
261
- baseItem: '0',
262
- subtotal: f.aggregation || 'sum',
263
- },
264
- [],
265
- ),
266
- );
311
+ const dataFieldNodes = this._valueFields.map((f) => {
312
+ const attrs: Record<string, string> = {
313
+ name: f.displayName || f.fieldName,
314
+ fld: String(f.fieldIndex),
315
+ baseField: '0',
316
+ baseItem: '0',
317
+ subtotal: f.aggregation || 'sum',
318
+ };
319
+
320
+ // Add numFmtId if it was resolved during addValueField
321
+ if (f.numFmtId !== undefined) {
322
+ attrs.numFmtId = String(f.numFmtId);
323
+ }
324
+
325
+ return createElement('dataField', attrs, []);
326
+ });
267
327
  children.push(createElement('dataFields', { count: String(dataFieldNodes.length) }, dataFieldNodes));
268
328
  }
269
329
 
330
+ // Check if any value field has a number format
331
+ const hasNumberFormats = this._valueFields.some((f) => f.numFmtId !== undefined);
332
+
270
333
  // Pivot table style
271
334
  children.push(
272
335
  createElement(
@@ -290,7 +353,7 @@ export class PivotTable {
290
353
  'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',
291
354
  name: this._name,
292
355
  cacheId: String(this._cache.cacheId),
293
- applyNumberFormats: '0',
356
+ applyNumberFormats: hasNumberFormats ? '1' : '0',
294
357
  applyBorderFormats: '0',
295
358
  applyFontFormats: '0',
296
359
  applyPatternFormats: '0',
package/src/styles.ts CHANGED
@@ -1,6 +1,48 @@
1
1
  import type { CellStyle, BorderType } from './types';
2
2
  import { parseXml, findElement, getChildren, getAttr, XmlNode, stringifyXml, createElement } from './utils/xml';
3
3
 
4
+ /**
5
+ * Excel built-in number format IDs (0-163 are reserved).
6
+ * These formats don't need to be defined in the numFmts element.
7
+ */
8
+ const BUILTIN_NUM_FMTS: Map<string, number> = new Map([
9
+ ['General', 0],
10
+ ['0', 1],
11
+ ['0.00', 2],
12
+ ['#,##0', 3],
13
+ ['#,##0.00', 4],
14
+ ['0%', 9],
15
+ ['0.00%', 10],
16
+ ['0.00E+00', 11],
17
+ ['# ?/?', 12],
18
+ ['# ??/??', 13],
19
+ ['mm-dd-yy', 14],
20
+ ['d-mmm-yy', 15],
21
+ ['d-mmm', 16],
22
+ ['mmm-yy', 17],
23
+ ['h:mm AM/PM', 18],
24
+ ['h:mm:ss AM/PM', 19],
25
+ ['h:mm', 20],
26
+ ['h:mm:ss', 21],
27
+ ['m/d/yy h:mm', 22],
28
+ ['#,##0 ;(#,##0)', 37],
29
+ ['#,##0 ;[Red](#,##0)', 38],
30
+ ['#,##0.00;(#,##0.00)', 39],
31
+ ['#,##0.00;[Red](#,##0.00)', 40],
32
+ ['mm:ss', 45],
33
+ ['[h]:mm:ss', 46],
34
+ ['mmss.0', 47],
35
+ ['##0.0E+0', 48],
36
+ ['@', 49],
37
+ ]);
38
+
39
+ /**
40
+ * Reverse lookup: built-in format ID -> format code
41
+ */
42
+ const BUILTIN_NUM_FMT_CODES: Map<number, string> = new Map(
43
+ Array.from(BUILTIN_NUM_FMTS.entries()).map(([code, id]) => [id, code]),
44
+ );
45
+
4
46
  /**
5
47
  * Normalize a color to ARGB format (8 hex chars).
6
48
  * Accepts: "#RGB", "#RRGGBB", "RGB", "RRGGBB", "AARRGGBB", "#AARRGGBB"
@@ -234,7 +276,8 @@ export class Styles {
234
276
  const font = this._fonts[xf.fontId];
235
277
  const fill = this._fills[xf.fillId];
236
278
  const border = this._borders[xf.borderId];
237
- const numFmt = this._numFmts.get(xf.numFmtId);
279
+ // Check custom formats first, then fall back to built-in format codes
280
+ const numFmt = this._numFmts.get(xf.numFmtId) ?? BUILTIN_NUM_FMT_CODES.get(xf.numFmtId);
238
281
 
239
282
  const style: CellStyle = {};
240
283
 
@@ -403,17 +446,34 @@ export class Styles {
403
446
  }
404
447
 
405
448
  private _findOrCreateNumFmt(format: string): number {
406
- // Check if already exists
449
+ // Check built-in formats first (IDs 0-163)
450
+ const builtinId = BUILTIN_NUM_FMTS.get(format);
451
+ if (builtinId !== undefined) {
452
+ return builtinId;
453
+ }
454
+
455
+ // Check if already exists in custom formats
407
456
  for (const [id, code] of this._numFmts) {
408
457
  if (code === format) return id;
409
458
  }
410
459
 
411
- // Create new (custom formats start at 164)
412
- const id = Math.max(164, ...Array.from(this._numFmts.keys())) + 1;
460
+ // Create new custom format (IDs 164+)
461
+ const existingIds = Array.from(this._numFmts.keys());
462
+ const id = existingIds.length > 0 ? Math.max(...existingIds) + 1 : 164;
413
463
  this._numFmts.set(id, format);
414
464
  return id;
415
465
  }
416
466
 
467
+ /**
468
+ * Get or create a number format ID for the given format string.
469
+ * Returns built-in IDs (0-163) for standard formats, or creates custom IDs (164+).
470
+ * @param format - The number format string (e.g., '0.00', '#,##0', '$#,##0.00')
471
+ */
472
+ getOrCreateNumFmtId(format: string): number {
473
+ this._dirty = true;
474
+ return this._findOrCreateNumFmt(format);
475
+ }
476
+
417
477
  /**
418
478
  * Check if styles have been modified
419
479
  */
package/src/types.ts CHANGED
@@ -119,10 +119,12 @@ export type AggregationType = 'sum' | 'count' | 'average' | 'min' | 'max';
119
119
  export interface PivotValueConfig {
120
120
  /** Source field name (column header) */
121
121
  field: string;
122
- /** Aggregation function */
123
- aggregation: AggregationType;
122
+ /** Aggregation function (default: 'sum') */
123
+ aggregation?: AggregationType;
124
124
  /** Display name (e.g., "Sum of Sales") */
125
125
  name?: string;
126
+ /** Number format (e.g., '$#,##0.00', '0.00%') */
127
+ numberFormat?: string;
126
128
  }
127
129
 
128
130
  /**
package/src/workbook.ts CHANGED
@@ -493,6 +493,10 @@ export class Workbook {
493
493
  targetAddr.col,
494
494
  pivotTableIndex,
495
495
  );
496
+
497
+ // Set styles reference for number format resolution
498
+ pivotTable.setStyles(this._styles);
499
+
496
500
  this._pivotTables.push(pivotTable);
497
501
 
498
502
  return pivotTable;