@niicojs/excel 0.1.0 → 0.2.0

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
@@ -150,6 +150,10 @@ const ERROR_TYPES = new Set([
150
150
  }
151
151
  switch(t){
152
152
  case 'n':
153
+ // Check if this is actually a date stored as number
154
+ if (this._isDateFormat()) {
155
+ return 'date';
156
+ }
153
157
  return 'number';
154
158
  case 's':
155
159
  case 'str':
@@ -162,7 +166,12 @@ const ERROR_TYPES = new Set([
162
166
  return 'date';
163
167
  default:
164
168
  // If no type but has value, infer from value
165
- if (typeof this._data.v === 'number') return 'number';
169
+ if (typeof this._data.v === 'number') {
170
+ if (this._isDateFormat()) {
171
+ return 'date';
172
+ }
173
+ return 'number';
174
+ }
166
175
  if (typeof this._data.v === 'string') return 'string';
167
176
  if (typeof this._data.v === 'boolean') return 'boolean';
168
177
  return 'empty';
@@ -178,7 +187,14 @@ const ERROR_TYPES = new Set([
178
187
  }
179
188
  switch(t){
180
189
  case 'n':
181
- return typeof v === 'number' ? v : parseFloat(String(v));
190
+ {
191
+ const numVal = typeof v === 'number' ? v : parseFloat(String(v));
192
+ // Check if this is actually a date stored as number
193
+ if (this._isDateFormat()) {
194
+ return this._excelDateToJs(numVal);
195
+ }
196
+ return numVal;
197
+ }
182
198
  case 's':
183
199
  // Shared string reference
184
200
  if (typeof v === 'number') {
@@ -240,9 +256,15 @@ const ERROR_TYPES = new Set([
240
256
  this._data.v = val ? 1 : 0;
241
257
  this._data.t = 'b';
242
258
  } else if (val instanceof Date) {
243
- // Store as ISO date string with 'd' type
244
- this._data.v = val.toISOString();
245
- this._data.t = 'd';
259
+ // Store as Excel serial number with date format for maximum compatibility
260
+ this._data.v = this._jsDateToExcel(val);
261
+ this._data.t = 'n';
262
+ // Apply a default date format if no style is set
263
+ if (this._data.s === undefined) {
264
+ this._data.s = this._worksheet.workbook.styles.createStyle({
265
+ numberFormat: 'yyyy-mm-dd'
266
+ });
267
+ }
246
268
  } else if ('error' in val) {
247
269
  this._data.v = val.error;
248
270
  this._data.t = 'e';
@@ -332,9 +354,16 @@ const ERROR_TYPES = new Set([
332
354
  /**
333
355
  * Check if this cell has a date number format
334
356
  */ _isDateFormat() {
335
- // TODO: Check actual number format from styles
336
- // For now, return false - dates should be explicitly typed
337
- return false;
357
+ if (this._data.s === undefined) {
358
+ return false;
359
+ }
360
+ const style = this._worksheet.workbook.styles.getStyle(this._data.s);
361
+ if (!style.numberFormat) {
362
+ return false;
363
+ }
364
+ // Common date format patterns
365
+ const fmt = style.numberFormat.toLowerCase();
366
+ return fmt.includes('y') || fmt.includes('m') || fmt.includes('d') || fmt.includes('h') || fmt.includes('s') || fmt === 'general date' || fmt === 'short date' || fmt === 'long date';
338
367
  }
339
368
  /**
340
369
  * Convert Excel serial date to JavaScript Date
@@ -2465,6 +2494,110 @@ const builder = new XMLBuilder(builderOptions);
2465
2494
  return copy;
2466
2495
  }
2467
2496
  /**
2497
+ * Create a new worksheet from an array of objects.
2498
+ *
2499
+ * The first row contains headers (object keys or custom column headers),
2500
+ * and subsequent rows contain the object values.
2501
+ *
2502
+ * @param config - Configuration for the sheet creation
2503
+ * @returns The created Worksheet
2504
+ *
2505
+ * @example
2506
+ * ```typescript
2507
+ * const data = [
2508
+ * { name: 'Alice', age: 30, city: 'Paris' },
2509
+ * { name: 'Bob', age: 25, city: 'London' },
2510
+ * { name: 'Charlie', age: 35, city: 'Berlin' },
2511
+ * ];
2512
+ *
2513
+ * // Simple usage - all object keys become columns
2514
+ * const sheet = wb.addSheetFromData({
2515
+ * name: 'People',
2516
+ * data: data,
2517
+ * });
2518
+ *
2519
+ * // With custom column configuration
2520
+ * const sheet2 = wb.addSheetFromData({
2521
+ * name: 'People Custom',
2522
+ * data: data,
2523
+ * columns: [
2524
+ * { key: 'name', header: 'Full Name' },
2525
+ * { key: 'age', header: 'Age (years)' },
2526
+ * ],
2527
+ * });
2528
+ * ```
2529
+ */ addSheetFromData(config) {
2530
+ const { name, data, columns, headerStyle = true, startCell = 'A1' } = config;
2531
+ if (data.length === 0) {
2532
+ // Create empty sheet if no data
2533
+ return this.addSheet(name);
2534
+ }
2535
+ // Create the new sheet
2536
+ const sheet = this.addSheet(name);
2537
+ // Parse start cell
2538
+ const startAddr = parseAddress(startCell);
2539
+ let startRow = startAddr.row;
2540
+ const startCol = startAddr.col;
2541
+ // Determine columns to use
2542
+ const columnConfigs = columns ?? this._inferColumns(data[0]);
2543
+ // Write header row
2544
+ for(let colIdx = 0; colIdx < columnConfigs.length; colIdx++){
2545
+ const colConfig = columnConfigs[colIdx];
2546
+ const headerText = colConfig.header ?? String(colConfig.key);
2547
+ const cell = sheet.cell(startRow, startCol + colIdx);
2548
+ cell.value = headerText;
2549
+ // Apply header style if enabled
2550
+ if (headerStyle) {
2551
+ cell.style = {
2552
+ bold: true
2553
+ };
2554
+ }
2555
+ }
2556
+ // Move to data rows
2557
+ startRow++;
2558
+ // Write data rows
2559
+ for(let rowIdx = 0; rowIdx < data.length; rowIdx++){
2560
+ const rowData = data[rowIdx];
2561
+ for(let colIdx = 0; colIdx < columnConfigs.length; colIdx++){
2562
+ const colConfig = columnConfigs[colIdx];
2563
+ const value = rowData[colConfig.key];
2564
+ const cell = sheet.cell(startRow + rowIdx, startCol + colIdx);
2565
+ // Convert value to CellValue
2566
+ cell.value = this._toCellValue(value);
2567
+ // Apply column style if defined
2568
+ if (colConfig.style) {
2569
+ cell.style = colConfig.style;
2570
+ }
2571
+ }
2572
+ }
2573
+ return sheet;
2574
+ }
2575
+ /**
2576
+ * Infer column configuration from the first data object
2577
+ */ _inferColumns(sample) {
2578
+ return Object.keys(sample).map((key)=>({
2579
+ key
2580
+ }));
2581
+ }
2582
+ /**
2583
+ * Convert an unknown value to a CellValue
2584
+ */ _toCellValue(value) {
2585
+ if (value === null || value === undefined) {
2586
+ return null;
2587
+ }
2588
+ if (typeof value === 'number' || typeof value === 'string' || typeof value === 'boolean') {
2589
+ return value;
2590
+ }
2591
+ if (value instanceof Date) {
2592
+ return value;
2593
+ }
2594
+ if (typeof value === 'object' && 'error' in value) {
2595
+ return value;
2596
+ }
2597
+ // Convert other types to string
2598
+ return String(value);
2599
+ }
2600
+ /**
2468
2601
  * Create a pivot table from source data.
2469
2602
  *
2470
2603
  * @param config - Pivot table configuration
@@ -2675,7 +2808,8 @@ const builder = new XMLBuilder(builderOptions);
2675
2808
  Type: rel.type,
2676
2809
  Target: rel.target
2677
2810
  }, []));
2678
- let nextRelId = this._relationships.length + 1;
2811
+ // Calculate next available relationship ID based on existing max ID
2812
+ let nextRelId = Math.max(0, ...this._relationships.map((r)=>parseInt(r.id.replace('rId', ''), 10) || 0)) + 1;
2679
2813
  // Add shared strings relationship if needed
2680
2814
  if (this._sharedStrings.count > 0) {
2681
2815
  const hasSharedStrings = this._relationships.some((r)=>r.type === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings');
package/package.json CHANGED
@@ -1,17 +1,21 @@
1
1
  {
2
2
  "name": "@niicojs/excel",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "typescript library to manipulate excel files",
5
5
  "homepage": "https://github.com/niicojs/excel#readme",
6
6
  "bugs": {
7
7
  "url": "https://github.com/niicojs/excel/issues"
8
8
  },
9
+ "license": "MIT",
10
+ "author": "niico",
9
11
  "repository": {
10
12
  "type": "git",
11
13
  "url": "git+https://github.com/niicojs/excel.git"
12
14
  },
13
- "license": "MIT",
14
- "author": "niico",
15
+ "files": [
16
+ "dist",
17
+ "src"
18
+ ],
15
19
  "type": "module",
16
20
  "main": "./dist/index.cjs",
17
21
  "module": "./dist/index.js",
@@ -29,10 +33,6 @@
29
33
  },
30
34
  "./package.json": "./package.json"
31
35
  },
32
- "files": [
33
- "dist",
34
- "src"
35
- ],
36
36
  "scripts": {
37
37
  "dev": "tsdx dev",
38
38
  "build": "tsdx build",
package/src/cell.ts CHANGED
@@ -66,6 +66,10 @@ export class Cell {
66
66
  }
67
67
  switch (t) {
68
68
  case 'n':
69
+ // Check if this is actually a date stored as number
70
+ if (this._isDateFormat()) {
71
+ return 'date';
72
+ }
69
73
  return 'number';
70
74
  case 's':
71
75
  case 'str':
@@ -78,7 +82,12 @@ export class Cell {
78
82
  return 'date';
79
83
  default:
80
84
  // If no type but has value, infer from value
81
- if (typeof this._data.v === 'number') return 'number';
85
+ if (typeof this._data.v === 'number') {
86
+ if (this._isDateFormat()) {
87
+ return 'date';
88
+ }
89
+ return 'number';
90
+ }
82
91
  if (typeof this._data.v === 'string') return 'string';
83
92
  if (typeof this._data.v === 'boolean') return 'boolean';
84
93
  return 'empty';
@@ -97,8 +106,14 @@ export class Cell {
97
106
  }
98
107
 
99
108
  switch (t) {
100
- case 'n':
101
- return typeof v === 'number' ? v : parseFloat(String(v));
109
+ case 'n': {
110
+ const numVal = typeof v === 'number' ? v : parseFloat(String(v));
111
+ // Check if this is actually a date stored as number
112
+ if (this._isDateFormat()) {
113
+ return this._excelDateToJs(numVal);
114
+ }
115
+ return numVal;
116
+ }
102
117
  case 's':
103
118
  // Shared string reference
104
119
  if (typeof v === 'number') {
@@ -160,9 +175,13 @@ export class Cell {
160
175
  this._data.v = val ? 1 : 0;
161
176
  this._data.t = 'b';
162
177
  } else if (val instanceof Date) {
163
- // Store as ISO date string with 'd' type
164
- this._data.v = val.toISOString();
165
- this._data.t = 'd';
178
+ // Store as Excel serial number with date format for maximum compatibility
179
+ this._data.v = this._jsDateToExcel(val);
180
+ this._data.t = 'n';
181
+ // Apply a default date format if no style is set
182
+ if (this._data.s === undefined) {
183
+ this._data.s = this._worksheet.workbook.styles.createStyle({ numberFormat: 'yyyy-mm-dd' });
184
+ }
166
185
  } else if ('error' in val) {
167
186
  this._data.v = val.error;
168
187
  this._data.t = 'e';
@@ -272,9 +291,25 @@ export class Cell {
272
291
  * Check if this cell has a date number format
273
292
  */
274
293
  private _isDateFormat(): boolean {
275
- // TODO: Check actual number format from styles
276
- // For now, return false - dates should be explicitly typed
277
- return false;
294
+ if (this._data.s === undefined) {
295
+ return false;
296
+ }
297
+ const style = this._worksheet.workbook.styles.getStyle(this._data.s);
298
+ if (!style.numberFormat) {
299
+ return false;
300
+ }
301
+ // Common date format patterns
302
+ const fmt = style.numberFormat.toLowerCase();
303
+ return (
304
+ fmt.includes('y') ||
305
+ fmt.includes('m') ||
306
+ fmt.includes('d') ||
307
+ fmt.includes('h') ||
308
+ fmt.includes('s') ||
309
+ fmt === 'general date' ||
310
+ fmt === 'short date' ||
311
+ fmt === 'long date'
312
+ );
278
313
  }
279
314
 
280
315
  /**
package/src/index.ts CHANGED
@@ -25,6 +25,9 @@ export type {
25
25
  PivotValueConfig,
26
26
  AggregationType,
27
27
  PivotFieldAxis,
28
+ // Sheet from data types
29
+ SheetFromDataConfig,
30
+ ColumnConfig,
28
31
  } from './types';
29
32
 
30
33
  // Utility exports
package/src/types.ts CHANGED
@@ -163,3 +163,31 @@ export interface PivotCacheField {
163
163
  * Pivot field axis assignment
164
164
  */
165
165
  export type PivotFieldAxis = 'row' | 'column' | 'filter' | 'value';
166
+
167
+ /**
168
+ * Configuration for creating a sheet from an array of objects
169
+ */
170
+ export interface SheetFromDataConfig<T extends object = Record<string, unknown>> {
171
+ /** Name of the sheet to create */
172
+ name: string;
173
+ /** Array of objects with the same structure */
174
+ data: T[];
175
+ /** Column definitions (optional - defaults to all keys from first object) */
176
+ columns?: ColumnConfig<T>[];
177
+ /** Apply header styling (bold text) (default: true) */
178
+ headerStyle?: boolean;
179
+ /** Starting cell address (default: 'A1') */
180
+ startCell?: string;
181
+ }
182
+
183
+ /**
184
+ * Column configuration for sheet data
185
+ */
186
+ export interface ColumnConfig<T = Record<string, unknown>> {
187
+ /** Key from the object to use for this column */
188
+ key: keyof T;
189
+ /** Header text (optional - defaults to key name) */
190
+ header?: string;
191
+ /** Cell style for data cells in this column */
192
+ style?: CellStyle;
193
+ }
package/src/workbook.ts CHANGED
@@ -1,5 +1,12 @@
1
1
  import { readFile, writeFile } from 'fs/promises';
2
- import type { SheetDefinition, Relationship, PivotTableConfig, CellValue } from './types';
2
+ import type {
3
+ SheetDefinition,
4
+ Relationship,
5
+ PivotTableConfig,
6
+ CellValue,
7
+ SheetFromDataConfig,
8
+ ColumnConfig,
9
+ } from './types';
3
10
  import { Worksheet } from './worksheet';
4
11
  import { SharedStrings } from './shared-strings';
5
12
  import { Styles } from './styles';
@@ -276,6 +283,126 @@ export class Workbook {
276
283
  return copy;
277
284
  }
278
285
 
286
+ /**
287
+ * Create a new worksheet from an array of objects.
288
+ *
289
+ * The first row contains headers (object keys or custom column headers),
290
+ * and subsequent rows contain the object values.
291
+ *
292
+ * @param config - Configuration for the sheet creation
293
+ * @returns The created Worksheet
294
+ *
295
+ * @example
296
+ * ```typescript
297
+ * const data = [
298
+ * { name: 'Alice', age: 30, city: 'Paris' },
299
+ * { name: 'Bob', age: 25, city: 'London' },
300
+ * { name: 'Charlie', age: 35, city: 'Berlin' },
301
+ * ];
302
+ *
303
+ * // Simple usage - all object keys become columns
304
+ * const sheet = wb.addSheetFromData({
305
+ * name: 'People',
306
+ * data: data,
307
+ * });
308
+ *
309
+ * // With custom column configuration
310
+ * const sheet2 = wb.addSheetFromData({
311
+ * name: 'People Custom',
312
+ * data: data,
313
+ * columns: [
314
+ * { key: 'name', header: 'Full Name' },
315
+ * { key: 'age', header: 'Age (years)' },
316
+ * ],
317
+ * });
318
+ * ```
319
+ */
320
+ addSheetFromData<T extends object>(config: SheetFromDataConfig<T>): Worksheet {
321
+ const { name, data, columns, headerStyle = true, startCell = 'A1' } = config;
322
+
323
+ if (data.length === 0) {
324
+ // Create empty sheet if no data
325
+ return this.addSheet(name);
326
+ }
327
+
328
+ // Create the new sheet
329
+ const sheet = this.addSheet(name);
330
+
331
+ // Parse start cell
332
+ const startAddr = parseAddress(startCell);
333
+ let startRow = startAddr.row;
334
+ const startCol = startAddr.col;
335
+
336
+ // Determine columns to use
337
+ const columnConfigs: ColumnConfig<T>[] = columns ?? this._inferColumns(data[0]);
338
+
339
+ // Write header row
340
+ for (let colIdx = 0; colIdx < columnConfigs.length; colIdx++) {
341
+ const colConfig = columnConfigs[colIdx];
342
+ const headerText = colConfig.header ?? String(colConfig.key);
343
+ const cell = sheet.cell(startRow, startCol + colIdx);
344
+ cell.value = headerText;
345
+
346
+ // Apply header style if enabled
347
+ if (headerStyle) {
348
+ cell.style = { bold: true };
349
+ }
350
+ }
351
+
352
+ // Move to data rows
353
+ startRow++;
354
+
355
+ // Write data rows
356
+ for (let rowIdx = 0; rowIdx < data.length; rowIdx++) {
357
+ const rowData = data[rowIdx];
358
+
359
+ for (let colIdx = 0; colIdx < columnConfigs.length; colIdx++) {
360
+ const colConfig = columnConfigs[colIdx];
361
+ const value = rowData[colConfig.key];
362
+ const cell = sheet.cell(startRow + rowIdx, startCol + colIdx);
363
+
364
+ // Convert value to CellValue
365
+ cell.value = this._toCellValue(value);
366
+
367
+ // Apply column style if defined
368
+ if (colConfig.style) {
369
+ cell.style = colConfig.style;
370
+ }
371
+ }
372
+ }
373
+
374
+ return sheet;
375
+ }
376
+
377
+ /**
378
+ * Infer column configuration from the first data object
379
+ */
380
+ private _inferColumns<T extends object>(sample: T): ColumnConfig<T>[] {
381
+ return (Object.keys(sample) as (keyof T)[]).map((key) => ({
382
+ key,
383
+ }));
384
+ }
385
+
386
+ /**
387
+ * Convert an unknown value to a CellValue
388
+ */
389
+ private _toCellValue(value: unknown): CellValue {
390
+ if (value === null || value === undefined) {
391
+ return null;
392
+ }
393
+ if (typeof value === 'number' || typeof value === 'string' || typeof value === 'boolean') {
394
+ return value;
395
+ }
396
+ if (value instanceof Date) {
397
+ return value;
398
+ }
399
+ if (typeof value === 'object' && 'error' in value) {
400
+ return value as CellValue;
401
+ }
402
+ // Convert other types to string
403
+ return String(value);
404
+ }
405
+
279
406
  /**
280
407
  * Create a pivot table from source data.
281
408
  *
@@ -521,7 +648,8 @@ export class Workbook {
521
648
  createElement('Relationship', { Id: rel.id, Type: rel.type, Target: rel.target }, []),
522
649
  );
523
650
 
524
- let nextRelId = this._relationships.length + 1;
651
+ // Calculate next available relationship ID based on existing max ID
652
+ let nextRelId = Math.max(0, ...this._relationships.map((r) => parseInt(r.id.replace('rId', ''), 10) || 0)) + 1;
525
653
 
526
654
  // Add shared strings relationship if needed
527
655
  if (this._sharedStrings.count > 0) {