@cj-tech-master/excelts 1.4.2 → 1.4.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.
@@ -4,30 +4,140 @@
4
4
  */
5
5
  import { Workbook } from "../doc/workbook.js";
6
6
  import { colCache } from "./col-cache.js";
7
- import { dateToExcel } from "./utils.js";
8
7
  import { format as cellFormat } from "./cell-format.js";
8
+ /**
9
+ * Convert a Date object back to Excel serial number without timezone issues.
10
+ * This reverses the excelToDate conversion exactly.
11
+ * excelToDate uses: new Date(Math.round((v - 25569) * 24 * 3600 * 1000))
12
+ * So we reverse it: (date.getTime() / (24 * 3600 * 1000)) + 25569
13
+ */
14
+ function dateToExcelSerial(d) {
15
+ return d.getTime() / (24 * 3600 * 1000) + 25569;
16
+ }
17
+ /**
18
+ * Check if format is a pure time format (no date components like y, m for month, d)
19
+ * Time formats only contain: h, m (minutes in time context), s, AM/PM
20
+ * Excludes elapsed time formats like [h]:mm:ss which should keep full serial number
21
+ */
22
+ function isTimeOnlyFormat(fmt) {
23
+ // Remove quoted strings first
24
+ const cleaned = fmt.replace(/"[^"]*"/g, "");
25
+ // Elapsed time formats [h], [m], [s] should NOT be treated as time-only
26
+ // They need the full serial number to calculate total hours/minutes/seconds
27
+ if (/\[[hms]\]/i.test(cleaned)) {
28
+ return false;
29
+ }
30
+ // Remove color codes and conditions (but we already checked for [h], [m], [s])
31
+ const withoutBrackets = cleaned.replace(/\[[^\]]*\]/g, "");
32
+ // Check if it has time components (h, s, or AM/PM)
33
+ const hasTimeComponents = /[hs]/i.test(withoutBrackets) || /AM\/PM|A\/P/i.test(withoutBrackets);
34
+ // Check if it has date components (y, d, or m not adjacent to h/s which would make it minutes)
35
+ // In Excel: "m" after "h" or before "s" is minutes, otherwise it's month
36
+ const hasDateComponents = /[yd]/i.test(withoutBrackets);
37
+ // If it has time but no date components, it's a time-only format
38
+ // Also check for standalone 'm' that's not minutes (not near h or s)
39
+ if (hasDateComponents) {
40
+ return false;
41
+ }
42
+ // Check for month 'm' - if 'm' exists but not in h:m or m:s context, it's a date format
43
+ if (/m/i.test(withoutBrackets) && !hasTimeComponents) {
44
+ return false;
45
+ }
46
+ return hasTimeComponents;
47
+ }
48
+ /**
49
+ * Check if format is a date format (contains y, d, or month-m)
50
+ * Used to determine if dateFormat override should be applied
51
+ */
52
+ function isDateFormat(fmt) {
53
+ // Remove quoted strings first
54
+ const cleaned = fmt.replace(/"[^"]*"/g, "");
55
+ // Elapsed time formats [h], [m], [s] are NOT date formats
56
+ if (/\[[hms]\]/i.test(cleaned)) {
57
+ return false;
58
+ }
59
+ // Remove color codes and conditions
60
+ const withoutBrackets = cleaned.replace(/\[[^\]]*\]/g, "");
61
+ // Check for year or day components
62
+ if (/[yd]/i.test(withoutBrackets)) {
63
+ return true;
64
+ }
65
+ // Check for month 'm' - only if it's NOT in time context (not near h or s)
66
+ // In Excel: "m" after "h" or before "s" is minutes, otherwise it's month
67
+ if (/m/i.test(withoutBrackets)) {
68
+ const hasTimeComponents = /[hs]/i.test(withoutBrackets) || /AM\/PM|A\/P/i.test(withoutBrackets);
69
+ // If no time components, 'm' is month
70
+ if (!hasTimeComponents) {
71
+ return true;
72
+ }
73
+ // If has time components, need to check if 'm' is month or minutes
74
+ // Simplified: if format has both date-like and time-like patterns, consider it a date format
75
+ // e.g., "m/d/yy h:mm" - has 'm' as month and 'mm' as minutes
76
+ }
77
+ return false;
78
+ }
79
+ /**
80
+ * Format a value (Date, number, boolean, string) according to the given format
81
+ * Handles timezone-independent conversion for Date objects
82
+ * @param value - The value to format
83
+ * @param fmt - The format string to use
84
+ * @param dateFormat - Optional override format for date values (not applied to time or elapsed time formats)
85
+ */
86
+ function formatValue(value, fmt, dateFormat) {
87
+ // Date object - convert back to Excel serial number
88
+ if (value instanceof Date) {
89
+ let serial = dateToExcelSerial(value);
90
+ // For time-only formats, use only the fractional part (time portion)
91
+ if (isTimeOnlyFormat(fmt)) {
92
+ serial = serial % 1;
93
+ if (serial < 0) {
94
+ serial += 1;
95
+ }
96
+ return cellFormat(fmt, serial);
97
+ }
98
+ // Only apply dateFormat override to actual date formats
99
+ // (not elapsed time formats like [h]:mm:ss)
100
+ const actualFmt = dateFormat && isDateFormat(fmt) ? dateFormat : fmt;
101
+ return cellFormat(actualFmt, serial);
102
+ }
103
+ // Number/Boolean/String - let cellFormat handle it
104
+ return cellFormat(fmt, value);
105
+ }
9
106
  /**
10
107
  * Get formatted display text for a cell value
11
108
  * Returns the value formatted according to the cell's numFmt
12
- * This matches Excel's display exactly
109
+ * This matches Excel's display exactly (timezone-independent)
110
+ * @param cell - The cell to get display text for
111
+ * @param dateFormat - Optional override format for date values
13
112
  */
14
- function getCellDisplayText(cell) {
113
+ function getCellDisplayText(cell, dateFormat) {
15
114
  const value = cell.value;
16
115
  const fmt = cell.numFmt || "General";
17
116
  // Null/undefined
18
117
  if (value == null) {
19
118
  return "";
20
119
  }
21
- // Date object - convert to Excel serial number
22
- if (value instanceof Date) {
23
- const serial = dateToExcel(value, false);
24
- return cellFormat(fmt, serial);
120
+ // Date/Number/Boolean/String - format directly
121
+ if (value instanceof Date ||
122
+ typeof value === "number" ||
123
+ typeof value === "boolean" ||
124
+ typeof value === "string") {
125
+ return formatValue(value, fmt, dateFormat);
25
126
  }
26
- // Number/Boolean/String - let cellFormat handle it
27
- if (typeof value === "number" || typeof value === "boolean" || typeof value === "string") {
28
- return cellFormat(fmt, value);
127
+ // Formula type - use the result value
128
+ if (typeof value === "object" && "formula" in value) {
129
+ const result = value.result;
130
+ if (result == null) {
131
+ return "";
132
+ }
133
+ if (result instanceof Date ||
134
+ typeof result === "number" ||
135
+ typeof result === "boolean" ||
136
+ typeof result === "string") {
137
+ return formatValue(result, fmt, dateFormat);
138
+ }
29
139
  }
30
- // Fallback to cell.text for other types (rich text, hyperlink, error, formula, etc.)
140
+ // Fallback to cell.text for other types (rich text, hyperlink, error, etc.)
31
141
  return cell.text;
32
142
  }
33
143
  // =============================================================================
@@ -263,7 +373,7 @@ export function sheetToJson(worksheet, opts) {
263
373
  let isEmpty = true;
264
374
  for (let col = startCol; col <= endCol; col++) {
265
375
  const cell = worksheet.getCell(row, col);
266
- const val = o.raw === false ? getCellDisplayText(cell).trim() : cell.value;
376
+ const val = o.raw === false ? getCellDisplayText(cell, o.dateFormat).trim() : cell.value;
267
377
  if (val != null && val !== "") {
268
378
  rowData[col - startCol] = val;
269
379
  isEmpty = false;
@@ -291,7 +401,7 @@ export function sheetToJson(worksheet, opts) {
291
401
  let isEmpty = true;
292
402
  for (let col = startCol; col <= endCol; col++) {
293
403
  const cell = worksheet.getCell(row, col);
294
- const val = o.raw === false ? getCellDisplayText(cell).trim() : cell.value;
404
+ const val = o.raw === false ? getCellDisplayText(cell, o.dateFormat).trim() : cell.value;
295
405
  const key = encodeCol(col - 1); // 0-indexed for encodeCol
296
406
  if (val != null && val !== "") {
297
407
  rowData[key] = val;
@@ -318,7 +428,7 @@ export function sheetToJson(worksheet, opts) {
318
428
  const colIdx = col - startCol;
319
429
  const key = headerOpt[colIdx] ?? `__EMPTY_${colIdx}`;
320
430
  const cell = worksheet.getCell(row, col);
321
- const val = o.raw === false ? getCellDisplayText(cell).trim() : cell.value;
431
+ const val = o.raw === false ? getCellDisplayText(cell, o.dateFormat).trim() : cell.value;
322
432
  if (val != null && val !== "") {
323
433
  rowData[key] = val;
324
434
  isEmpty = false;
@@ -360,7 +470,7 @@ export function sheetToJson(worksheet, opts) {
360
470
  let isEmpty = true;
361
471
  for (let col = startCol; col <= endCol; col++) {
362
472
  const cell = worksheet.getCell(row, col);
363
- const val = o.raw === false ? getCellDisplayText(cell).trim() : cell.value;
473
+ const val = o.raw === false ? getCellDisplayText(cell, o.dateFormat).trim() : cell.value;
364
474
  const key = headers[col - startCol];
365
475
  if (val != null && val !== "") {
366
476
  rowData[key] = val;
@@ -73,7 +73,7 @@ export interface JSON2SheetOpts {
73
73
  /** Use specified field order (default Object.keys) */
74
74
  header?: string[];
75
75
  /** Use specified date format in string output */
76
- dateNF?: string;
76
+ dateFormat?: string;
77
77
  /** Store dates as type d (default is n) */
78
78
  cellDates?: boolean;
79
79
  /** If true, do not include header row in output */
@@ -117,6 +117,12 @@ export interface Sheet2JSONOpts {
117
117
  defval?: CellValue;
118
118
  /** Include blank lines in the output */
119
119
  blankrows?: boolean;
120
+ /**
121
+ * Override format for date values (not applied to time-only formats).
122
+ * When provided, Date values will be formatted using this format string.
123
+ * @example "dd/mm/yyyy" or "yyyy-mm-dd"
124
+ */
125
+ dateFormat?: string;
120
126
  }
121
127
  /**
122
128
  * Convert worksheet to JSON array (xlsx compatible)
@@ -164,7 +170,7 @@ export interface AOA2SheetOpts {
164
170
  /** Use specified cell as starting point */
165
171
  origin?: Origin;
166
172
  /** Use specified date format in string output */
167
- dateNF?: string;
173
+ dateFormat?: string;
168
174
  /** Store dates as type d (default is n) */
169
175
  cellDates?: boolean;
170
176
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cj-tech-master/excelts",
3
- "version": "1.4.2",
3
+ "version": "1.4.3",
4
4
  "description": "TypeScript Excel Workbook Manager - Read and Write xlsx and csv Files.",
5
5
  "private": false,
6
6
  "publishConfig": {