@niicojs/excel 0.2.5 → 0.2.6

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) {
@@ -1694,7 +1840,8 @@ const builder = new XMLBuilder(builderOptions);
1694
1840
  * @param fieldName - Name of the source field (column header)
1695
1841
  * @param aggregation - Aggregation function (sum, count, average, min, max)
1696
1842
  * @param displayName - Optional display name (defaults to "Sum of FieldName")
1697
- */ addValueField(fieldName, aggregation = 'sum', displayName) {
1843
+ * @param numberFormat - Optional number format (e.g., '$#,##0.00', '0.00%')
1844
+ */ addValueField(fieldName, aggregation = 'sum', displayName, numberFormat) {
1698
1845
  const fieldIndex = this._cache.getFieldIndex(fieldName);
1699
1846
  if (fieldIndex < 0) {
1700
1847
  throw new Error(`Field not found in source data: ${fieldName}`);
@@ -1705,7 +1852,8 @@ const builder = new XMLBuilder(builderOptions);
1705
1852
  fieldIndex,
1706
1853
  axis: 'value',
1707
1854
  aggregation,
1708
- displayName: displayName || defaultName
1855
+ displayName: displayName || defaultName,
1856
+ numberFormat
1709
1857
  });
1710
1858
  return this;
1711
1859
  }
@@ -1828,17 +1976,26 @@ const builder = new XMLBuilder(builderOptions);
1828
1976
  }
1829
1977
  // Data fields (values)
1830
1978
  if (this._valueFields.length > 0) {
1831
- const dataFieldNodes = this._valueFields.map((f)=>createElement('dataField', {
1979
+ const dataFieldNodes = this._valueFields.map((f)=>{
1980
+ const attrs = {
1832
1981
  name: f.displayName || f.fieldName,
1833
1982
  fld: String(f.fieldIndex),
1834
1983
  baseField: '0',
1835
1984
  baseItem: '0',
1836
1985
  subtotal: f.aggregation || 'sum'
1837
- }, []));
1986
+ };
1987
+ // Add numFmtId if format specified and styles available
1988
+ if (f.numberFormat && this._styles) {
1989
+ attrs.numFmtId = String(this._styles.getOrCreateNumFmtId(f.numberFormat));
1990
+ }
1991
+ return createElement('dataField', attrs, []);
1992
+ });
1838
1993
  children.push(createElement('dataFields', {
1839
1994
  count: String(dataFieldNodes.length)
1840
1995
  }, dataFieldNodes));
1841
1996
  }
1997
+ // Check if any value field has a number format
1998
+ const hasNumberFormats = this._valueFields.some((f)=>f.numberFormat);
1842
1999
  // Pivot table style
1843
2000
  children.push(createElement('pivotTableStyleInfo', {
1844
2001
  name: 'PivotStyleMedium9',
@@ -1853,7 +2010,7 @@ const builder = new XMLBuilder(builderOptions);
1853
2010
  'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',
1854
2011
  name: this._name,
1855
2012
  cacheId: String(this._cache.cacheId),
1856
- applyNumberFormats: '0',
2013
+ applyNumberFormats: hasNumberFormats ? '1' : '0',
1857
2014
  applyBorderFormats: '0',
1858
2015
  applyFontFormats: '0',
1859
2016
  applyPatternFormats: '0',
@@ -2787,6 +2944,8 @@ const builder = new XMLBuilder(builderOptions);
2787
2944
  // Create pivot table
2788
2945
  const pivotTableIndex = this._pivotTables.length + 1;
2789
2946
  const pivotTable = new PivotTable(config.name, cache, targetSheet, targetCell, targetAddr.row + 1, targetAddr.col, pivotTableIndex);
2947
+ // Set styles reference for number format resolution
2948
+ pivotTable.setStyles(this._styles);
2790
2949
  this._pivotTables.push(pivotTable);
2791
2950
  return pivotTable;
2792
2951
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@niicojs/excel",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
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
1
  import type { AggregationType, PivotFieldAxis } 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
+ numberFormat?: string;
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)
@@ -127,8 +139,14 @@ export class PivotTable {
127
139
  * @param fieldName - Name of the source field (column header)
128
140
  * @param aggregation - Aggregation function (sum, count, average, min, max)
129
141
  * @param displayName - Optional display name (defaults to "Sum of FieldName")
142
+ * @param numberFormat - Optional number format (e.g., '$#,##0.00', '0.00%')
130
143
  */
131
- addValueField(fieldName: string, aggregation: AggregationType = 'sum', displayName?: string): this {
144
+ addValueField(
145
+ fieldName: string,
146
+ aggregation: AggregationType = 'sum',
147
+ displayName?: string,
148
+ numberFormat?: string,
149
+ ): this {
132
150
  const fieldIndex = this._cache.getFieldIndex(fieldName);
133
151
  if (fieldIndex < 0) {
134
152
  throw new Error(`Field not found in source data: ${fieldName}`);
@@ -142,6 +160,7 @@ export class PivotTable {
142
160
  axis: 'value',
143
161
  aggregation,
144
162
  displayName: displayName || defaultName,
163
+ numberFormat,
145
164
  });
146
165
 
147
166
  return this;
@@ -251,22 +270,28 @@ export class PivotTable {
251
270
 
252
271
  // Data fields (values)
253
272
  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
- );
273
+ const dataFieldNodes = this._valueFields.map((f) => {
274
+ const attrs: Record<string, string> = {
275
+ name: f.displayName || f.fieldName,
276
+ fld: String(f.fieldIndex),
277
+ baseField: '0',
278
+ baseItem: '0',
279
+ subtotal: f.aggregation || 'sum',
280
+ };
281
+
282
+ // Add numFmtId if format specified and styles available
283
+ if (f.numberFormat && this._styles) {
284
+ attrs.numFmtId = String(this._styles.getOrCreateNumFmtId(f.numberFormat));
285
+ }
286
+
287
+ return createElement('dataField', attrs, []);
288
+ });
267
289
  children.push(createElement('dataFields', { count: String(dataFieldNodes.length) }, dataFieldNodes));
268
290
  }
269
291
 
292
+ // Check if any value field has a number format
293
+ const hasNumberFormats = this._valueFields.some((f) => f.numberFormat);
294
+
270
295
  // Pivot table style
271
296
  children.push(
272
297
  createElement(
@@ -290,7 +315,7 @@ export class PivotTable {
290
315
  'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',
291
316
  name: this._name,
292
317
  cacheId: String(this._cache.cacheId),
293
- applyNumberFormats: '0',
318
+ applyNumberFormats: hasNumberFormats ? '1' : '0',
294
319
  applyBorderFormats: '0',
295
320
  applyFontFormats: '0',
296
321
  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/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;