@niicojs/excel 0.3.0 → 0.3.2

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 CHANGED
@@ -106,6 +106,334 @@ var fflate = require('fflate');
106
106
  };
107
107
  };
108
108
 
109
+ const DEFAULT_LOCALE = 'fr-FR';
110
+ const formatPartsCache = new Map();
111
+ const getLocaleNumberInfo = (locale)=>{
112
+ const cacheKey = `num-info:${locale}`;
113
+ const cached = formatPartsCache.get(cacheKey);
114
+ if (cached) {
115
+ const decimal = cached.find((p)=>p.type === 'decimal')?.value ?? ',';
116
+ const group = cached.find((p)=>p.type === 'group')?.value ?? ' ';
117
+ return {
118
+ decimal,
119
+ group
120
+ };
121
+ }
122
+ const parts = new Intl.NumberFormat(locale).formatToParts(12345.6);
123
+ formatPartsCache.set(cacheKey, parts);
124
+ const decimal = parts.find((p)=>p.type === 'decimal')?.value ?? ',';
125
+ const group = parts.find((p)=>p.type === 'group')?.value ?? ' ';
126
+ return {
127
+ decimal,
128
+ group
129
+ };
130
+ };
131
+ const normalizeSpaces = (value)=>{
132
+ return value.replace(/[\u00a0\u202f]/g, ' ');
133
+ };
134
+ const splitFormatSections = (format)=>{
135
+ const sections = [];
136
+ let current = '';
137
+ let inQuote = false;
138
+ for(let i = 0; i < format.length; i++){
139
+ const ch = format[i];
140
+ if (ch === '"') {
141
+ inQuote = !inQuote;
142
+ current += ch;
143
+ continue;
144
+ }
145
+ if (ch === ';' && !inQuote) {
146
+ sections.push(current);
147
+ current = '';
148
+ continue;
149
+ }
150
+ current += ch;
151
+ }
152
+ sections.push(current);
153
+ return sections;
154
+ };
155
+ const extractFormatLiterals = (format)=>{
156
+ let prefix = '';
157
+ let suffix = '';
158
+ let cleaned = '';
159
+ let inQuote = false;
160
+ let sawPlaceholder = false;
161
+ for(let i = 0; i < format.length; i++){
162
+ const ch = format[i];
163
+ if (ch === '"') {
164
+ inQuote = !inQuote;
165
+ continue;
166
+ }
167
+ if (inQuote) {
168
+ if (!sawPlaceholder) {
169
+ prefix += ch;
170
+ } else {
171
+ suffix += ch;
172
+ }
173
+ continue;
174
+ }
175
+ if (ch === '\\' && i + 1 < format.length) {
176
+ const escaped = format[i + 1];
177
+ if (!sawPlaceholder) {
178
+ prefix += escaped;
179
+ } else {
180
+ suffix += escaped;
181
+ }
182
+ i++;
183
+ continue;
184
+ }
185
+ if (ch === '_' || ch === '*') {
186
+ if (i + 1 < format.length) {
187
+ i++;
188
+ }
189
+ continue;
190
+ }
191
+ if (ch === '[') {
192
+ const end = format.indexOf(']', i + 1);
193
+ if (end !== -1) {
194
+ const content = format.slice(i + 1, end);
195
+ const currencyMatch = content.match(/[$€]/);
196
+ if (currencyMatch) {
197
+ if (!sawPlaceholder) {
198
+ prefix += currencyMatch[0];
199
+ } else {
200
+ suffix += currencyMatch[0];
201
+ }
202
+ }
203
+ i = end;
204
+ continue;
205
+ }
206
+ }
207
+ if (ch === '%') {
208
+ if (!sawPlaceholder) {
209
+ prefix += ch;
210
+ } else {
211
+ suffix += ch;
212
+ }
213
+ continue;
214
+ }
215
+ if (ch === '0' || ch === '#' || ch === '?' || ch === '.' || ch === ',') {
216
+ sawPlaceholder = true;
217
+ cleaned += ch;
218
+ continue;
219
+ }
220
+ if (!sawPlaceholder) {
221
+ prefix += ch;
222
+ } else {
223
+ suffix += ch;
224
+ }
225
+ }
226
+ return {
227
+ cleaned,
228
+ prefix,
229
+ suffix
230
+ };
231
+ };
232
+ const parseNumberFormat = (format)=>{
233
+ const trimmed = format.trim();
234
+ if (!trimmed) return null;
235
+ const { cleaned, prefix, suffix } = extractFormatLiterals(trimmed);
236
+ const lower = cleaned.toLowerCase();
237
+ if (!/[0#?]/.test(lower)) return null;
238
+ const percent = /%/.test(trimmed);
239
+ const section = lower;
240
+ const lastDot = section.lastIndexOf('.');
241
+ const lastComma = section.lastIndexOf(',');
242
+ let decimalSeparator = null;
243
+ if (lastDot >= 0 && lastComma >= 0) {
244
+ decimalSeparator = lastDot > lastComma ? '.' : ',';
245
+ } else if (lastDot >= 0 || lastComma >= 0) {
246
+ const candidate = lastDot >= 0 ? '.' : ',';
247
+ const index = lastDot >= 0 ? lastDot : lastComma;
248
+ const fractionSection = section.slice(index + 1);
249
+ const fractionDigitsCandidate = fractionSection.replace(/[^0#?]/g, '').length;
250
+ if (fractionDigitsCandidate > 0) {
251
+ if (fractionDigitsCandidate === 3) {
252
+ decimalSeparator = null;
253
+ } else {
254
+ decimalSeparator = candidate;
255
+ }
256
+ }
257
+ }
258
+ let decimalIndex = decimalSeparator === '.' ? lastDot : decimalSeparator === ',' ? lastComma : -1;
259
+ if (decimalIndex >= 0) {
260
+ const fractionSection = section.slice(decimalIndex + 1);
261
+ if (!/[0#?]/.test(fractionSection)) {
262
+ decimalIndex = -1;
263
+ }
264
+ }
265
+ const decimalSection = decimalIndex >= 0 ? section.slice(decimalIndex + 1) : '';
266
+ const fractionDigits = decimalSection.replace(/[^0#?]/g, '').length;
267
+ const integerSection = decimalIndex >= 0 ? section.slice(0, decimalIndex) : section;
268
+ const useGrouping = /[,.\s\u00a0\u202f]/.test(integerSection) && integerSection.length > 0;
269
+ return {
270
+ fractionDigits,
271
+ percent,
272
+ useGrouping,
273
+ literalPrefix: prefix || undefined,
274
+ literalSuffix: suffix || undefined
275
+ };
276
+ };
277
+ const formatNumber = (value, info, locale)=>{
278
+ const adjusted = info.percent ? value * 100 : value;
279
+ const { decimal, group } = getLocaleNumberInfo(locale);
280
+ const absValue = Math.abs(adjusted);
281
+ const fixed = absValue.toFixed(info.fractionDigits);
282
+ const [integerPart, fractionPart] = fixed.split('.');
283
+ const grouped = info.useGrouping ? integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, group) : integerPart;
284
+ const fraction = info.fractionDigits > 0 ? `${decimal}${fractionPart}` : '';
285
+ const signed = adjusted < 0 ? '-' : '';
286
+ let result = `${signed}${grouped}${fraction}`;
287
+ if (info.literalPrefix) {
288
+ result = `${info.literalPrefix}${result}`;
289
+ }
290
+ if (info.literalSuffix) {
291
+ result = `${result}${info.literalSuffix}`;
292
+ }
293
+ return normalizeSpaces(result);
294
+ };
295
+ const padNumber = (value, length)=>{
296
+ const str = String(value);
297
+ return str.length >= length ? str : `${'0'.repeat(length - str.length)}${str}`;
298
+ };
299
+ const formatDatePart = (value, token, locale)=>{
300
+ switch(token){
301
+ case 'yyyy':
302
+ return String(value.getFullYear());
303
+ case 'yy':
304
+ return padNumber(value.getFullYear() % 100, 2);
305
+ case 'mmmm':
306
+ return value.toLocaleString(locale, {
307
+ month: 'long'
308
+ });
309
+ case 'mmm':
310
+ return value.toLocaleString(locale, {
311
+ month: 'short'
312
+ });
313
+ case 'mm':
314
+ return padNumber(value.getMonth() + 1, 2);
315
+ case 'm':
316
+ return String(value.getMonth() + 1);
317
+ case 'dd':
318
+ return padNumber(value.getDate(), 2);
319
+ case 'd':
320
+ return String(value.getDate());
321
+ case 'hh':
322
+ {
323
+ const hours = value.getHours();
324
+ return padNumber(hours, 2);
325
+ }
326
+ case 'h':
327
+ return String(value.getHours());
328
+ case 'min2':
329
+ return padNumber(value.getMinutes(), 2);
330
+ case 'min1':
331
+ return String(value.getMinutes());
332
+ case 'ss':
333
+ return padNumber(value.getSeconds(), 2);
334
+ case 's':
335
+ return String(value.getSeconds());
336
+ default:
337
+ return token;
338
+ }
339
+ };
340
+ const tokenizeDateFormat = (format)=>{
341
+ const tokens = [];
342
+ let i = 0;
343
+ while(i < format.length){
344
+ const ch = format[i];
345
+ if (ch === '"') {
346
+ let literal = '';
347
+ i++;
348
+ while(i < format.length && format[i] !== '"'){
349
+ literal += format[i];
350
+ i++;
351
+ }
352
+ i++;
353
+ if (literal) tokens.push(literal);
354
+ continue;
355
+ }
356
+ if (ch === '\\' && i + 1 < format.length) {
357
+ tokens.push(format[i + 1]);
358
+ i += 2;
359
+ continue;
360
+ }
361
+ if (ch === '[') {
362
+ const end = format.indexOf(']', i + 1);
363
+ if (end !== -1) {
364
+ i = end + 1;
365
+ continue;
366
+ }
367
+ }
368
+ const lower = format.slice(i).toLowerCase();
369
+ const match = [
370
+ 'yyyy',
371
+ 'yy',
372
+ 'mmmm',
373
+ 'mmm',
374
+ 'mm',
375
+ 'm',
376
+ 'dd',
377
+ 'd',
378
+ 'hh',
379
+ 'h',
380
+ 'ss',
381
+ 's'
382
+ ].find((t)=>lower.startsWith(t));
383
+ if (match) {
384
+ if (match === 'm' || match === 'mm') {
385
+ let j = i - 1;
386
+ let previousChar = '';
387
+ while(j >= 0 && previousChar === ''){
388
+ const candidate = format[j];
389
+ if (candidate && candidate !== ' ') {
390
+ previousChar = candidate;
391
+ }
392
+ j--;
393
+ }
394
+ const isMinute = previousChar === 'h' || previousChar === 'H' || previousChar === ':';
395
+ if (isMinute) {
396
+ tokens.push(match === 'mm' ? 'min2' : 'min1');
397
+ i += match.length;
398
+ continue;
399
+ }
400
+ }
401
+ tokens.push(match);
402
+ i += match.length;
403
+ continue;
404
+ }
405
+ tokens.push(ch);
406
+ i++;
407
+ }
408
+ return tokens;
409
+ };
410
+ const isDateFormat = (format)=>{
411
+ const lowered = format.toLowerCase();
412
+ return /[ymdhss]/.test(lowered);
413
+ };
414
+ const formatDate = (value, format, locale)=>{
415
+ const tokens = tokenizeDateFormat(format);
416
+ return tokens.map((token)=>formatDatePart(value, token, locale)).join('');
417
+ };
418
+ const formatCellValue = (value, style, locale)=>{
419
+ const numberFormat = style?.numberFormat;
420
+ if (!numberFormat) return null;
421
+ const normalizedLocale = locale || DEFAULT_LOCALE;
422
+ const sections = splitFormatSections(numberFormat);
423
+ const hasNegativeSection = sections.length > 1;
424
+ const section = value instanceof Date ? sections[0] : value < 0 ? sections[1] ?? sections[0] : sections[0];
425
+ if (value instanceof Date && isDateFormat(section)) {
426
+ return formatDate(value, section, normalizedLocale);
427
+ }
428
+ if (typeof value === 'number') {
429
+ const info = parseNumberFormat(section);
430
+ if (!info) return null;
431
+ const numericValue = value < 0 && hasNegativeSection ? Math.abs(value) : value;
432
+ return formatNumber(numericValue, info, normalizedLocale);
433
+ }
434
+ return null;
435
+ };
436
+
109
437
  // Excel epoch: December 30, 1899 (accounting for the 1900 leap year bug)
110
438
  const EXCEL_EPOCH = new Date(Date.UTC(1899, 11, 30));
111
439
  const MS_PER_DAY = 24 * 60 * 60 * 1000;
@@ -306,12 +634,21 @@ const ERROR_TYPES = new Set([
306
634
  /**
307
635
  * Get the formatted text (as displayed in Excel)
308
636
  */ get text() {
637
+ return this.textWithLocale();
638
+ }
639
+ /**
640
+ * Get the formatted text using a specific locale
641
+ */ textWithLocale(locale) {
309
642
  if (this._data.w) {
310
643
  return this._data.w;
311
644
  }
312
645
  const val = this.value;
313
646
  if (val === null) return '';
314
647
  if (typeof val === 'object' && 'error' in val) return val.error;
648
+ if (val instanceof Date || typeof val === 'number') {
649
+ const formatted = formatCellValue(val, this.style, locale ?? this._worksheet.workbook.locale);
650
+ if (formatted !== null) return formatted;
651
+ }
315
652
  if (val instanceof Date) return val.toISOString().split('T')[0];
316
653
  return String(val);
317
654
  }
@@ -933,8 +1270,8 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
933
1270
  }
934
1271
  }
935
1272
 
936
- /**
937
- * Represents a worksheet in a workbook
1273
+ /**
1274
+ * Represents a worksheet in a workbook
938
1275
  */ class Worksheet {
939
1276
  constructor(workbook, name){
940
1277
  this._cells = new Map();
@@ -956,24 +1293,24 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
956
1293
  this._workbook = workbook;
957
1294
  this._name = name;
958
1295
  }
959
- /**
960
- * Get the workbook this sheet belongs to
1296
+ /**
1297
+ * Get the workbook this sheet belongs to
961
1298
  */ get workbook() {
962
1299
  return this._workbook;
963
1300
  }
964
- /**
965
- * Get the sheet name
1301
+ /**
1302
+ * Get the sheet name
966
1303
  */ get name() {
967
1304
  return this._name;
968
1305
  }
969
- /**
970
- * Set the sheet name
1306
+ /**
1307
+ * Set the sheet name
971
1308
  */ set name(value) {
972
1309
  this._name = value;
973
1310
  this._dirty = true;
974
1311
  }
975
- /**
976
- * Parse worksheet XML content
1312
+ /**
1313
+ * Parse worksheet XML content
977
1314
  */ parse(xml) {
978
1315
  this._xmlNodes = parseXml(xml);
979
1316
  this._preserveXml = true;
@@ -1037,8 +1374,8 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
1037
1374
  }
1038
1375
  }
1039
1376
  }
1040
- /**
1041
- * Parse the sheetData element to extract cells
1377
+ /**
1378
+ * Parse the sheetData element to extract cells
1042
1379
  */ _parseSheetData(rows) {
1043
1380
  for (const rowNode of rows){
1044
1381
  if (!('row' in rowNode)) continue;
@@ -1060,8 +1397,8 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
1060
1397
  }
1061
1398
  this._boundsDirty = true;
1062
1399
  }
1063
- /**
1064
- * Parse a cell XML node to CellData
1400
+ /**
1401
+ * Parse a cell XML node to CellData
1065
1402
  */ _parseCellNode(node) {
1066
1403
  const data = {};
1067
1404
  // Type attribute
@@ -1136,8 +1473,8 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
1136
1473
  }
1137
1474
  return data;
1138
1475
  }
1139
- /**
1140
- * Get a cell by address or row/col
1476
+ /**
1477
+ * Get a cell by address or row/col
1141
1478
  */ cell(rowOrAddress, col) {
1142
1479
  const { row, col: c } = parseCellRef(rowOrAddress, col);
1143
1480
  const address = toAddress(row, c);
@@ -1149,8 +1486,8 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
1149
1486
  }
1150
1487
  return cell;
1151
1488
  }
1152
- /**
1153
- * Get an existing cell without creating it.
1489
+ /**
1490
+ * Get an existing cell without creating it.
1154
1491
  */ getCellIfExists(rowOrAddress, col) {
1155
1492
  const { row, col: c } = parseCellRef(rowOrAddress, col);
1156
1493
  const address = toAddress(row, c);
@@ -1177,8 +1514,8 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
1177
1514
  }
1178
1515
  return new Range(this, rangeAddr);
1179
1516
  }
1180
- /**
1181
- * Merge cells in the given range
1517
+ /**
1518
+ * Merge cells in the given range
1182
1519
  */ mergeCells(rangeOrStart, end) {
1183
1520
  let rangeStr;
1184
1521
  if (end) {
@@ -1189,19 +1526,19 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
1189
1526
  this._mergedCells.add(rangeStr);
1190
1527
  this._dirty = true;
1191
1528
  }
1192
- /**
1193
- * Unmerge cells in the given range
1529
+ /**
1530
+ * Unmerge cells in the given range
1194
1531
  */ unmergeCells(rangeStr) {
1195
1532
  this._mergedCells.delete(rangeStr);
1196
1533
  this._dirty = true;
1197
1534
  }
1198
- /**
1199
- * Get all merged cell ranges
1535
+ /**
1536
+ * Get all merged cell ranges
1200
1537
  */ get mergedCells() {
1201
1538
  return Array.from(this._mergedCells);
1202
1539
  }
1203
- /**
1204
- * Check if the worksheet has been modified
1540
+ /**
1541
+ * Check if the worksheet has been modified
1205
1542
  */ get dirty() {
1206
1543
  if (this._dirty) return true;
1207
1544
  for (const cell of this._cells.values()){
@@ -1209,13 +1546,13 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
1209
1546
  }
1210
1547
  return false;
1211
1548
  }
1212
- /**
1213
- * Get all cells in the worksheet
1549
+ /**
1550
+ * Get all cells in the worksheet
1214
1551
  */ get cells() {
1215
1552
  return this._cells;
1216
1553
  }
1217
- /**
1218
- * Set a column width (0-based index or column letter)
1554
+ /**
1555
+ * Set a column width (0-based index or column letter)
1219
1556
  */ setColumnWidth(col, width) {
1220
1557
  if (!Number.isFinite(width) || width <= 0) {
1221
1558
  throw new Error('Column width must be a positive number');
@@ -1228,14 +1565,14 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
1228
1565
  this._colsDirty = true;
1229
1566
  this._dirty = true;
1230
1567
  }
1231
- /**
1232
- * Get a column width if set
1568
+ /**
1569
+ * Get a column width if set
1233
1570
  */ getColumnWidth(col) {
1234
1571
  const colIndex = typeof col === 'number' ? col : letterToCol(col);
1235
1572
  return this._columnWidths.get(colIndex);
1236
1573
  }
1237
- /**
1238
- * Set a row height (0-based index)
1574
+ /**
1575
+ * Set a row height (0-based index)
1239
1576
  */ setRowHeight(row, height) {
1240
1577
  if (!Number.isFinite(height) || height <= 0) {
1241
1578
  throw new Error('Row height must be a positive number');
@@ -1247,13 +1584,13 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
1247
1584
  this._colsDirty = true;
1248
1585
  this._dirty = true;
1249
1586
  }
1250
- /**
1251
- * Get a row height if set
1587
+ /**
1588
+ * Get a row height if set
1252
1589
  */ getRowHeight(row) {
1253
1590
  return this._rowHeights.get(row);
1254
1591
  }
1255
- /**
1256
- * Freeze panes at a given row/column split (counts from top-left)
1592
+ /**
1593
+ * Freeze panes at a given row/column split (counts from top-left)
1257
1594
  */ freezePane(rowSplit, colSplit) {
1258
1595
  if (rowSplit < 0 || colSplit < 0) {
1259
1596
  throw new Error('Freeze pane splits must be >= 0');
@@ -1269,68 +1606,68 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
1269
1606
  this._sheetViewsDirty = true;
1270
1607
  this._dirty = true;
1271
1608
  }
1272
- /**
1273
- * Get current frozen pane configuration
1609
+ /**
1610
+ * Get current frozen pane configuration
1274
1611
  */ getFrozenPane() {
1275
1612
  return this._frozenPane ? {
1276
1613
  ...this._frozenPane
1277
1614
  } : null;
1278
1615
  }
1279
- /**
1280
- * Get all tables in the worksheet
1616
+ /**
1617
+ * Get all tables in the worksheet
1281
1618
  */ get tables() {
1282
1619
  return [
1283
1620
  ...this._tables
1284
1621
  ];
1285
1622
  }
1286
- /**
1287
- * Get column width entries
1288
- * @internal
1623
+ /**
1624
+ * Get column width entries
1625
+ * @internal
1289
1626
  */ getColumnWidths() {
1290
1627
  return new Map(this._columnWidths);
1291
1628
  }
1292
- /**
1293
- * Get row height entries
1294
- * @internal
1629
+ /**
1630
+ * Get row height entries
1631
+ * @internal
1295
1632
  */ getRowHeights() {
1296
1633
  return new Map(this._rowHeights);
1297
1634
  }
1298
- /**
1299
- * Set table relationship IDs for tableParts generation.
1300
- * @internal
1635
+ /**
1636
+ * Set table relationship IDs for tableParts generation.
1637
+ * @internal
1301
1638
  */ setTableRelIds(ids) {
1302
1639
  this._tableRelIds = ids ? [
1303
1640
  ...ids
1304
1641
  ] : null;
1305
1642
  this._tablePartsDirty = true;
1306
1643
  }
1307
- /**
1308
- * Create an Excel Table (ListObject) from a data range.
1309
- *
1310
- * Tables provide structured data features like auto-filter, banded styling,
1311
- * and total row with aggregation functions.
1312
- *
1313
- * @param config - Table configuration
1314
- * @returns Table instance for method chaining
1315
- *
1316
- * @example
1317
- * ```typescript
1318
- * // Create a table with default styling
1319
- * const table = sheet.createTable({
1320
- * name: 'SalesData',
1321
- * range: 'A1:D10',
1322
- * });
1323
- *
1324
- * // Create a table with total row
1325
- * const table = sheet.createTable({
1326
- * name: 'SalesData',
1327
- * range: 'A1:D10',
1328
- * totalRow: true,
1329
- * style: { name: 'TableStyleMedium2' }
1330
- * });
1331
- *
1332
- * table.setTotalFunction('Sales', 'sum');
1333
- * ```
1644
+ /**
1645
+ * Create an Excel Table (ListObject) from a data range.
1646
+ *
1647
+ * Tables provide structured data features like auto-filter, banded styling,
1648
+ * and total row with aggregation functions.
1649
+ *
1650
+ * @param config - Table configuration
1651
+ * @returns Table instance for method chaining
1652
+ *
1653
+ * @example
1654
+ * ```typescript
1655
+ * // Create a table with default styling
1656
+ * const table = sheet.createTable({
1657
+ * name: 'SalesData',
1658
+ * range: 'A1:D10',
1659
+ * });
1660
+ *
1661
+ * // Create a table with total row
1662
+ * const table = sheet.createTable({
1663
+ * name: 'SalesData',
1664
+ * range: 'A1:D10',
1665
+ * totalRow: true,
1666
+ * style: { name: 'TableStyleMedium2' }
1667
+ * });
1668
+ *
1669
+ * table.setTotalFunction('Sales', 'sum');
1670
+ * ```
1334
1671
  */ createTable(config) {
1335
1672
  // Validate table name is unique within the workbook
1336
1673
  for (const sheet of this._workbook.sheetNames){
@@ -1353,25 +1690,25 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
1353
1690
  this._dirty = true;
1354
1691
  return table;
1355
1692
  }
1356
- /**
1693
+ /**
1357
1694
  * Convert sheet data to an array of JSON objects.
1358
- *
1695
+ *
1359
1696
  * @param config - Configuration options
1360
1697
  * @returns Array of objects where keys are field names and values are cell values
1361
1698
  *
1362
1699
  * @example
1363
- * ```typescript
1364
- * // Using first row as headers
1365
- * const data = sheet.toJson();
1366
- *
1367
- * // Using custom field names
1368
- * const data = sheet.toJson({ fields: ['name', 'age', 'city'] });
1369
- *
1370
- * // Starting from a specific row/column
1371
- * const data = sheet.toJson({ startRow: 2, startCol: 1 });
1372
- * ```
1700
+ * ```typescript
1701
+ * // Using first row as headers
1702
+ * const data = sheet.toJson();
1703
+ *
1704
+ * // Using custom field names
1705
+ * const data = sheet.toJson({ fields: ['name', 'age', 'city'] });
1706
+ *
1707
+ * // Starting from a specific row/column
1708
+ * const data = sheet.toJson({ startRow: 2, startCol: 1 });
1709
+ * ```
1373
1710
  */ toJson(config = {}) {
1374
- const { fields, startRow = 0, startCol = 0, endRow, endCol, stopOnEmptyRow = true, dateHandling = this._workbook.dateHandling } = config;
1711
+ const { fields, startRow = 0, startCol = 0, endRow, endCol, stopOnEmptyRow = true, dateHandling = this._workbook.dateHandling, asText = false, locale } = config;
1375
1712
  // Get the bounds of data in the sheet
1376
1713
  const bounds = this._getDataBounds();
1377
1714
  if (!bounds) {
@@ -1404,12 +1741,21 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
1404
1741
  for(let colOffset = 0; colOffset < fieldNames.length; colOffset++){
1405
1742
  const col = startCol + colOffset;
1406
1743
  const cell = this._cells.get(toAddress(row, col));
1407
- let value = cell?.value ?? null;
1408
- if (value instanceof Date) {
1409
- value = this._serializeDate(value, dateHandling, cell);
1410
- }
1411
- if (value !== null) {
1412
- hasData = true;
1744
+ let value;
1745
+ if (asText) {
1746
+ // Return formatted text instead of raw value
1747
+ value = cell?.textWithLocale(locale) ?? '';
1748
+ if (value !== '') {
1749
+ hasData = true;
1750
+ }
1751
+ } else {
1752
+ value = cell?.value ?? null;
1753
+ if (value instanceof Date) {
1754
+ value = this._serializeDate(value, dateHandling, cell);
1755
+ }
1756
+ if (value !== null) {
1757
+ hasData = true;
1758
+ }
1413
1759
  }
1414
1760
  const fieldName = fieldNames[colOffset];
1415
1761
  if (fieldName) {
@@ -1433,8 +1779,8 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
1433
1779
  }
1434
1780
  return value;
1435
1781
  }
1436
- /**
1437
- * Get the bounds of data in the sheet (min/max row and column with data)
1782
+ /**
1783
+ * Get the bounds of data in the sheet (min/max row and column with data)
1438
1784
  */ _getDataBounds() {
1439
1785
  if (!this._boundsDirty && this._dataBoundsCache) {
1440
1786
  return this._dataBoundsCache;
@@ -1470,8 +1816,8 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
1470
1816
  this._boundsDirty = false;
1471
1817
  return this._dataBoundsCache;
1472
1818
  }
1473
- /**
1474
- * Generate XML for this worksheet
1819
+ /**
1820
+ * Generate XML for this worksheet
1475
1821
  */ toXml() {
1476
1822
  const preserved = this._preserveXml && this._xmlNodes ? this._buildPreservedWorksheet() : null;
1477
1823
  // Build sheetData from cells
@@ -1706,8 +2052,8 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
1706
2052
  }
1707
2053
  return worksheet;
1708
2054
  }
1709
- /**
1710
- * Build a cell XML node from a Cell object
2055
+ /**
2056
+ * Build a cell XML node from a Cell object
1711
2057
  */ _buildCellNode(cell) {
1712
2058
  const data = cell.data;
1713
2059
  const attrs = {
@@ -3586,6 +3932,7 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
3586
3932
  this._nextTableId = 1;
3587
3933
  // Date serialization handling
3588
3934
  this._dateHandling = 'jsDate';
3935
+ this._locale = 'fr-FR';
3589
3936
  this._sharedStrings = new SharedStrings();
3590
3937
  this._styles = Styles.createDefault();
3591
3938
  }
@@ -3660,6 +4007,16 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
3660
4007
  this._dateHandling = value;
3661
4008
  }
3662
4009
  /**
4010
+ * Get the workbook locale for formatting.
4011
+ */ get locale() {
4012
+ return this._locale;
4013
+ }
4014
+ /**
4015
+ * Set the workbook locale for formatting.
4016
+ */ set locale(value) {
4017
+ this._locale = value;
4018
+ }
4019
+ /**
3663
4020
  * Get the next unique table ID for this workbook.
3664
4021
  * Table IDs must be unique across all worksheets.
3665
4022
  * @internal