@shbernal/ts-xlsx 3.0.0 → 3.1.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.
Files changed (213) hide show
  1. package/README.md +4 -1
  2. package/dist/bytes.d.ts +7 -0
  3. package/dist/bytes.js +22 -0
  4. package/dist/core/address.d.ts +60 -2
  5. package/dist/core/address.js +40 -24
  6. package/dist/core/autofilter.d.ts +4 -3
  7. package/dist/core/autofilter.js +31 -30
  8. package/dist/core/axis-handle.d.ts +2 -0
  9. package/dist/core/axis-handle.js +5 -2
  10. package/dist/core/cell.d.ts +4 -3
  11. package/dist/core/cell.js +4 -6
  12. package/dist/core/clone.d.ts +29 -0
  13. package/dist/core/clone.js +24 -0
  14. package/dist/core/color-resolution.js +2 -4
  15. package/dist/core/column.d.ts +1 -0
  16. package/dist/core/column.js +3 -0
  17. package/dist/core/comment-thread.js +2 -1
  18. package/dist/core/conditional-formatting-overlay.d.ts +2 -1
  19. package/dist/core/conditional-formatting-overlay.js +2 -2
  20. package/dist/core/conditional-formatting.d.ts +9 -1
  21. package/dist/core/conditional-formatting.js +34 -18
  22. package/dist/core/containers.d.ts +12 -0
  23. package/dist/core/containers.js +5 -0
  24. package/dist/core/data-validation-overlay.d.ts +2 -1
  25. package/dist/core/data-validation-overlay.js +4 -4
  26. package/dist/core/data-validation.d.ts +6 -0
  27. package/dist/core/data-validation.js +15 -4
  28. package/dist/core/date-format.d.ts +20 -0
  29. package/dist/core/date-format.js +160 -0
  30. package/dist/core/date.d.ts +43 -8
  31. package/dist/core/date.js +18 -2
  32. package/dist/core/formula.d.ts +26 -0
  33. package/dist/core/formula.js +20 -4
  34. package/dist/core/grid-edits.d.ts +2 -3
  35. package/dist/core/grid-edits.js +69 -70
  36. package/dist/core/grid-shift.d.ts +58 -9
  37. package/dist/core/grid-shift.js +26 -4
  38. package/dist/core/image.d.ts +13 -9
  39. package/dist/core/image.js +7 -5
  40. package/dist/core/internal.d.ts +8 -2
  41. package/dist/core/limits.d.ts +23 -0
  42. package/dist/core/limits.js +4 -0
  43. package/dist/core/merge.d.ts +5 -4
  44. package/dist/core/merge.js +18 -19
  45. package/dist/core/pivot-table.js +63 -45
  46. package/dist/core/protection.js +2 -11
  47. package/dist/core/range.js +34 -30
  48. package/dist/core/row-input.js +2 -2
  49. package/dist/core/row.d.ts +1 -0
  50. package/dist/core/row.js +3 -0
  51. package/dist/core/style.d.ts +85 -70
  52. package/dist/core/style.js +92 -28
  53. package/dist/core/table-style.d.ts +1 -1
  54. package/dist/core/table-style.js +4 -4
  55. package/dist/core/table.d.ts +27 -9
  56. package/dist/core/table.js +51 -34
  57. package/dist/core/theme.js +6 -5
  58. package/dist/core/value.d.ts +9 -0
  59. package/dist/core/value.js +36 -34
  60. package/dist/core/workbook-media.d.ts +34 -0
  61. package/dist/core/workbook-media.js +44 -0
  62. package/dist/core/workbook-styles.d.ts +67 -1
  63. package/dist/core/workbook-vba.js +3 -2
  64. package/dist/core/workbook.d.ts +23 -2
  65. package/dist/core/workbook.js +24 -40
  66. package/dist/core/worksheet-comments.d.ts +2 -1
  67. package/dist/core/worksheet-comments.js +20 -13
  68. package/dist/core/worksheet-merges.d.ts +44 -0
  69. package/dist/core/worksheet-merges.js +59 -0
  70. package/dist/core/worksheet-model.d.ts +9 -1
  71. package/dist/core/worksheet-model.js +24 -9
  72. package/dist/core/worksheet.d.ts +55 -6
  73. package/dist/core/worksheet.js +36 -44
  74. package/dist/customui/ribbon.js +2 -1
  75. package/dist/entries/core.d.ts +18 -13
  76. package/dist/entries/core.js +12 -6
  77. package/dist/entries/xlsx.d.ts +1 -1
  78. package/dist/errors.d.ts +64 -0
  79. package/dist/errors.js +22 -0
  80. package/dist/hex.d.ts +8 -0
  81. package/dist/hex.js +3 -0
  82. package/dist/io/csv/delimiter.d.ts +9 -0
  83. package/dist/io/csv/delimiter.js +6 -0
  84. package/dist/io/csv/read.js +11 -4
  85. package/dist/io/csv/write.d.ts +9 -2
  86. package/dist/io/csv/write.js +9 -38
  87. package/dist/io/opc/inflate.js +14 -0
  88. package/dist/io/opc/namespaces.d.ts +16 -0
  89. package/dist/io/opc/namespaces.js +4 -0
  90. package/dist/io/opc/part-paths.d.ts +9 -0
  91. package/dist/io/opc/part-paths.js +1 -1
  92. package/dist/io/opc/read-opc.d.ts +54 -14
  93. package/dist/io/opc/read-opc.js +42 -20
  94. package/dist/io/opc/read-options.d.ts +5 -1
  95. package/dist/io/opc/sniff-format.d.ts +3 -3
  96. package/dist/io/opc/sniff-format.js +2 -2
  97. package/dist/io/style/xf-style.d.ts +23 -1
  98. package/dist/io/style/xf-style.js +8 -0
  99. package/dist/io/xlsb/formula.d.ts +2 -2
  100. package/dist/io/xlsb/formula.js +49 -34
  101. package/dist/io/xlsb/primitives.js +8 -14
  102. package/dist/io/xlsb/ptg-functions.d.ts +19 -10
  103. package/dist/io/xlsb/ptg-functions.js +385 -596
  104. package/dist/io/xlsb/read-styles.js +45 -81
  105. package/dist/io/xlsb/read-worksheet.d.ts +20 -1
  106. package/dist/io/xlsb/read-worksheet.js +73 -89
  107. package/dist/io/xlsb/read.d.ts +6 -3
  108. package/dist/io/xlsb/read.js +44 -23
  109. package/dist/io/xlsb/record-stream.d.ts +30 -0
  110. package/dist/io/xlsb/record-stream.js +20 -0
  111. package/dist/io/xlsb/record-types.d.ts +1 -5
  112. package/dist/io/xlsb/record-types.js +1 -5
  113. package/dist/io/xlsx/cell-accumulator.d.ts +8 -1
  114. package/dist/io/xlsx/cell-accumulator.js +32 -9
  115. package/dist/io/xlsx/cell-style-resolution.d.ts +25 -0
  116. package/dist/io/xlsx/cell-style-resolution.js +29 -0
  117. package/dist/io/xlsx/cell-value.d.ts +5 -3
  118. package/dist/io/xlsx/cell-value.js +13 -15
  119. package/dist/io/xlsx/color-xml.js +3 -2
  120. package/dist/io/xlsx/column-budget.d.ts +26 -0
  121. package/dist/io/xlsx/column-budget.js +22 -0
  122. package/dist/io/xlsx/comments.d.ts +6 -2
  123. package/dist/io/xlsx/comments.js +36 -32
  124. package/dist/io/xlsx/conditional-formatting.d.ts +6 -3
  125. package/dist/io/xlsx/conditional-formatting.js +30 -26
  126. package/dist/io/xlsx/data-validation.js +29 -27
  127. package/dist/io/xlsx/edit-vba.d.ts +7 -2
  128. package/dist/io/xlsx/edit-vba.js +14 -21
  129. package/dist/io/xlsx/font-xml.d.ts +7 -0
  130. package/dist/io/xlsx/font-xml.js +104 -0
  131. package/dist/io/xlsx/hyperlinks.d.ts +37 -5
  132. package/dist/io/xlsx/hyperlinks.js +22 -13
  133. package/dist/io/xlsx/images.js +11 -13
  134. package/dist/io/xlsx/namespaces.d.ts +16 -0
  135. package/dist/io/xlsx/namespaces.js +7 -0
  136. package/dist/io/xlsx/package-plan.d.ts +14 -2
  137. package/dist/io/xlsx/package-plan.js +39 -11
  138. package/dist/io/xlsx/pivot.js +8 -7
  139. package/dist/io/xlsx/read-parts.d.ts +59 -0
  140. package/dist/io/xlsx/read-parts.js +252 -0
  141. package/dist/io/xlsx/read-pivot.js +2 -1
  142. package/dist/io/xlsx/read-repair.d.ts +26 -0
  143. package/dist/io/xlsx/read-repair.js +39 -0
  144. package/dist/io/xlsx/{rich-runs.js → read-rich-runs.js} +6 -3
  145. package/dist/io/xlsx/read-rows.d.ts +18 -5
  146. package/dist/io/xlsx/read-rows.js +45 -31
  147. package/dist/io/xlsx/read-shared-strings.js +3 -2
  148. package/dist/io/xlsx/read-styles.d.ts +1 -6
  149. package/dist/io/xlsx/read-styles.js +6 -71
  150. package/dist/io/xlsx/read-workbook-xml.d.ts +36 -0
  151. package/dist/io/xlsx/read-workbook-xml.js +170 -0
  152. package/dist/io/xlsx/read-worksheet.d.ts +2 -1
  153. package/dist/io/xlsx/read-worksheet.js +35 -164
  154. package/dist/io/xlsx/read.d.ts +5 -12
  155. package/dist/io/xlsx/read.js +73 -406
  156. package/dist/io/xlsx/rich-text.js +1 -1
  157. package/dist/io/xlsx/row-position.d.ts +18 -0
  158. package/dist/io/xlsx/row-position.js +11 -0
  159. package/dist/io/xlsx/row-xml.d.ts +128 -0
  160. package/dist/io/xlsx/row-xml.js +203 -0
  161. package/dist/io/xlsx/shared-formulas.js +2 -2
  162. package/dist/io/xlsx/sheet-properties.d.ts +10 -1
  163. package/dist/io/xlsx/sheet-properties.js +145 -9
  164. package/dist/io/xlsx/style-elements.d.ts +43 -0
  165. package/dist/io/xlsx/style-elements.js +156 -0
  166. package/dist/io/xlsx/styles.d.ts +12 -3
  167. package/dist/io/xlsx/styles.js +5 -207
  168. package/dist/io/xlsx/tables.js +10 -14
  169. package/dist/io/xlsx/theme-xml.d.ts +26 -4
  170. package/dist/io/xlsx/theme-xml.js +107 -69
  171. package/dist/io/xlsx/threaded-comments.d.ts +8 -10
  172. package/dist/io/xlsx/threaded-comments.js +2 -1
  173. package/dist/io/xlsx/workbook-xml.d.ts +54 -4
  174. package/dist/io/xlsx/workbook-xml.js +60 -34
  175. package/dist/io/xlsx/worksheet-xml.d.ts +23 -70
  176. package/dist/io/xlsx/worksheet-xml.js +48 -229
  177. package/dist/io/xlsx/write-stream.d.ts +65 -16
  178. package/dist/io/xlsx/write-stream.js +50 -20
  179. package/dist/io/xlsx/write.d.ts +1 -2
  180. package/dist/io/xlsx/write.js +114 -74
  181. package/dist/rel-type.d.ts +16 -0
  182. package/dist/rel-type.js +6 -0
  183. package/dist/token-set.d.ts +13 -0
  184. package/dist/token-set.js +4 -1
  185. package/dist/vba/bytes.d.ts +14 -5
  186. package/dist/vba/bytes.js +16 -6
  187. package/dist/vba/cfb-format.d.ts +8 -0
  188. package/dist/vba/cfb-format.js +1 -0
  189. package/dist/vba/cfb-writer.d.ts +9 -0
  190. package/dist/vba/cfb-writer.js +12 -9
  191. package/dist/vba/cfb.d.ts +13 -1
  192. package/dist/vba/cfb.js +73 -51
  193. package/dist/vba/codepage.js +2 -1
  194. package/dist/vba/errors.d.ts +7 -0
  195. package/dist/vba/ms-ovba.js +51 -16
  196. package/dist/vba/project-editor.js +35 -40
  197. package/dist/vba/project.d.ts +11 -1
  198. package/dist/vba/project.js +27 -8
  199. package/dist/vba/vba-encoding.js +4 -5
  200. package/dist/xml/xml-attrs.d.ts +37 -0
  201. package/dist/xml/xml-attrs.js +30 -0
  202. package/dist/xml/xml-chars.d.ts +40 -0
  203. package/dist/xml/xml-chars.js +12 -0
  204. package/dist/xml/xml-namespaces.d.ts +31 -0
  205. package/dist/xml/xml-namespaces.js +50 -0
  206. package/dist/xml/xml-read.d.ts +72 -10
  207. package/dist/xml/xml-read.js +94 -18
  208. package/dist/xml/xml-scan.d.ts +15 -37
  209. package/dist/xml/xml-scan.js +12 -40
  210. package/dist/xml/xml.d.ts +28 -27
  211. package/dist/xml/xml.js +24 -24
  212. package/package.json +12 -7
  213. /package/dist/io/xlsx/{rich-runs.d.ts → read-rich-runs.d.ts} +0 -0
@@ -1,4 +1,6 @@
1
1
  import { tokenSet } from '../token-set.js';
2
+ import { cloneWith } from './clone.js';
3
+ import { cloneBorder, cloneFill, cloneFont } from './style.js';
2
4
  export const isCfValueObjectType = tokenSet({
3
5
  num: true,
4
6
  percent: true,
@@ -75,25 +77,39 @@ export const isIconSetType = tokenSet({
75
77
  export function cloneConditionalFormatting(cf) {
76
78
  return { ref: cf.ref, rules: cf.rules.map(cloneRule) };
77
79
  }
80
+ const RULE_CLONE = {
81
+ type: 'value',
82
+ priority: 'value',
83
+ stopIfTrue: 'value',
84
+ operator: 'value',
85
+ formulae: 'values',
86
+ text: 'value',
87
+ style: cloneStyle,
88
+ dxfId: 'value',
89
+ cfvo: 'records',
90
+ color: 'record',
91
+ colors: 'records',
92
+ gradient: 'value',
93
+ negativeFillColor: 'record',
94
+ axisColor: 'record',
95
+ iconSet: 'value',
96
+ rank: 'value',
97
+ percent: 'value',
98
+ bottom: 'value',
99
+ aboveAverage: 'value',
100
+ equalAverage: 'value',
101
+ stdDev: 'value',
102
+ timePeriod: 'value',
103
+ };
78
104
  function cloneRule(rule) {
79
- return {
80
- ...rule,
81
- ...(rule.formulae !== undefined ? { formulae: [...rule.formulae] } : {}),
82
- ...(rule.cfvo !== undefined ? { cfvo: rule.cfvo.map((v) => ({ ...v })) } : {}),
83
- ...(rule.color !== undefined ? { color: { ...rule.color } } : {}),
84
- ...(rule.negativeFillColor !== undefined
85
- ? { negativeFillColor: { ...rule.negativeFillColor } }
86
- : {}),
87
- ...(rule.axisColor !== undefined ? { axisColor: { ...rule.axisColor } } : {}),
88
- ...(rule.colors !== undefined ? { colors: rule.colors.map((c) => ({ ...c })) } : {}),
89
- ...(rule.style !== undefined ? { style: cloneStyle(rule.style) } : {}),
90
- };
105
+ return cloneWith(rule, RULE_CLONE);
91
106
  }
107
+ const STYLE_CLONE = {
108
+ font: cloneFont,
109
+ fill: cloneFill,
110
+ border: cloneBorder,
111
+ numFmt: 'value',
112
+ };
92
113
  function cloneStyle(style) {
93
- return {
94
- ...style,
95
- ...(style.font !== undefined ? { font: { ...style.font } } : {}),
96
- ...(style.fill !== undefined ? { fill: { ...style.fill } } : {}),
97
- ...(style.border !== undefined ? { border: { ...style.border } } : {}),
98
- };
114
+ return cloneWith(style, STYLE_CLONE);
99
115
  }
@@ -1,2 +1,14 @@
1
1
  export declare function overwrite<T extends object>(target: T, source: T): void;
2
2
  export declare function replaceContents<T>(array: T[], next: readonly T[]): void;
3
+ /**
4
+ * Copy one key from `source` onto `target`, leaving `target` untouched when `source` omits it.
5
+ *
6
+ * One key at a time is the whole point, and the reason is a limit of the checker rather than a
7
+ * preference: over a union key the compiler cannot correlate `source[key]`'s type with `target[key]`'s,
8
+ * so a version taking the union needs a cast, and that cast is the one place a facet could be written
9
+ * into the wrong slot with nothing to notice. Bound to a single member here, the assignment is checked.
10
+ *
11
+ * Generic over the object as well as the key because these two lines had been written out once for a
12
+ * cell's formatting and once for a cell's content, each under its own paragraph making this point.
13
+ */
14
+ export declare function copyKeyIfPresent<T extends object, K extends keyof T>(target: T, source: Readonly<T>, key: K): void;
@@ -8,3 +8,8 @@ export function replaceContents(array, next) {
8
8
  for (const item of next)
9
9
  array.push(item);
10
10
  }
11
+ export function copyKeyIfPresent(target, source, key) {
12
+ const value = source[key];
13
+ if (value !== undefined)
14
+ target[key] = value;
15
+ }
@@ -1,4 +1,5 @@
1
1
  import { type DataValidation, type DataValidationEntry } from './data-validation.ts';
2
+ import type { AxisSplice } from './grid-shift.ts';
2
3
  export declare class DataValidationOverlay {
3
4
  #private;
4
5
  /**
@@ -33,7 +34,7 @@ export declare class DataValidationOverlay {
33
34
  * attached to rather than on whatever moved into their place. A rule whose every target area fell
34
35
  * inside a deleted span is dropped with them.
35
36
  */
36
- shift(axis: 'row' | 'col', start: number, count: number, delta: number): void;
37
+ shift(splice: AxisSplice): void;
37
38
  /** Drop every validation, leaving the overlay empty. */
38
39
  clear(): void;
39
40
  }
@@ -1,4 +1,4 @@
1
- import { AuthoringError } from '../errors.js';
1
+ import { AuthoringError, quoted } from '../errors.js';
2
2
  import { replaceContents } from './containers.js';
3
3
  import { cloneDataValidation, } from './data-validation.js';
4
4
  import { decodeSqrefRects, shiftSqref } from './merge.js';
@@ -8,7 +8,7 @@ export class DataValidationOverlay {
8
8
  add(sqref, rule, options = {}) {
9
9
  const rects = decodeSqrefRects(sqref);
10
10
  if (rects.length === 0) {
11
- throw new AuthoringError(`data validation range "${sqref}" names no cells`);
11
+ throw new AuthoringError(`data validation range ${quoted(sqref)} names no cells`);
12
12
  }
13
13
  const stored = cloneDataValidation(rule);
14
14
  const entry = { sqref, rule: stored };
@@ -30,11 +30,11 @@ export class DataValidationOverlay {
30
30
  }
31
31
  return undefined;
32
32
  }
33
- shift(axis, start, count, delta) {
33
+ shift(splice) {
34
34
  const entries = [];
35
35
  const rects = [];
36
36
  for (const entry of this.#entries) {
37
- const sqref = shiftSqref(entry.sqref, axis, start, count, delta);
37
+ const sqref = shiftSqref(entry.sqref, splice);
38
38
  if (sqref === undefined)
39
39
  continue;
40
40
  entries.push({ ...entry, sqref });
@@ -1,3 +1,5 @@
1
+ import { type ClonePlan } from './clone.ts';
2
+ import type { AssertNever } from './internal.ts';
1
3
  /** The kind of constraint a validation enforces. `list` is a dropdown; `custom` is an arbitrary
2
4
  * boolean formula; `none` constrains nothing and exists only to carry the rule's messages; the rest
3
5
  * bound a typed value (`whole`/`decimal`/`date`/`time`/`textLength`). */
@@ -42,4 +44,8 @@ export interface DataValidationEntry {
42
44
  }
43
45
  /** A defensive copy of a rule, so a stored validation never aliases the caller's object (nor its
44
46
  * `formulae` array). */
47
+ declare const RULE_CLONE: ClonePlan<DataValidation>;
48
+ /** The proof that {@link RULE_CLONE} names every field of the rule. */
49
+ export type EveryDataValidationFieldIsCloned = AssertNever<Exclude<keyof Required<DataValidation>, keyof typeof RULE_CLONE>>;
45
50
  export declare function cloneDataValidation(rule: DataValidation): DataValidation;
51
+ export {};
@@ -1,4 +1,5 @@
1
1
  import { tokenSet } from '../token-set.js';
2
+ import { cloneWith } from './clone.js';
2
3
  export const isDataValidationType = tokenSet({
3
4
  none: true,
4
5
  list: true,
@@ -24,9 +25,19 @@ export const isDataValidationErrorStyle = tokenSet({
24
25
  warning: true,
25
26
  information: true,
26
27
  });
28
+ const RULE_CLONE = {
29
+ type: 'value',
30
+ operator: 'value',
31
+ formulae: 'values',
32
+ allowBlank: 'value',
33
+ showInputMessage: 'value',
34
+ showErrorMessage: 'value',
35
+ errorStyle: 'value',
36
+ error: 'value',
37
+ errorTitle: 'value',
38
+ prompt: 'value',
39
+ promptTitle: 'value',
40
+ };
27
41
  export function cloneDataValidation(rule) {
28
- return {
29
- ...rule,
30
- ...(rule.formulae !== undefined ? { formulae: [...rule.formulae] } : {}),
31
- };
42
+ return cloneWith(rule, RULE_CLONE);
32
43
  }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Render a `Date` through an Excel number-format code: `yyyy-mm-dd`, `d mmm yy`, `hh:mm:ss`.
3
+ *
4
+ * The one vocabulary this library speaks for dates. `Cell.numFmt` holds a code, `DEFAULT_DATE_NUMFMT`
5
+ * is one, `isDateFormat` classifies one, and the CSV writer's `dateFormat` used to be a moment.js
6
+ * token set instead -- case-sensitive, with `mm` meaning minutes and the month spelled `MM` -- so
7
+ * `writeCsv(wb, {dateFormat: cell.numFmt})`, the obvious thing to write, rendered every date cell as
8
+ * `2024-45-dd` with no throw and no warning. A library with two public date-format properties
9
+ * speaking different languages has no defensible answer to "which one is this".
10
+ *
11
+ * Codes are case-insensitive, as Excel's are. What is *not* covered, deliberately: the elapsed-time
12
+ * forms (`[h]`, `[mm]`), which measure a duration rather than name a moment, and the locale directive
13
+ * (`[$-409]`), which is dropped rather than honoured -- month and weekday names render in English.
14
+ * Both are recorded in ADR 0041.
15
+ *
16
+ * @param utc read the date's fields in UTC rather than in the runner's local time. A serial carries
17
+ * no timezone, so a `Date` that came from one is UTC wall-clock and this should be `true`; a `Date`
18
+ * a caller assigned may mean either.
19
+ */
20
+ export declare function formatSerialDate(date: Date, code: string, utc: boolean): string;
@@ -0,0 +1,160 @@
1
+ const MONTH_NAMES = [
2
+ 'January',
3
+ 'February',
4
+ 'March',
5
+ 'April',
6
+ 'May',
7
+ 'June',
8
+ 'July',
9
+ 'August',
10
+ 'September',
11
+ 'October',
12
+ 'November',
13
+ 'December',
14
+ ];
15
+ const WEEKDAY_NAMES = [
16
+ 'Sunday',
17
+ 'Monday',
18
+ 'Tuesday',
19
+ 'Wednesday',
20
+ 'Thursday',
21
+ 'Friday',
22
+ 'Saturday',
23
+ ];
24
+ function formatRuns(code) {
25
+ const runs = [];
26
+ const literal = (text) => {
27
+ if (text !== '')
28
+ runs.push({ kind: 'literal', text, count: text.length });
29
+ };
30
+ for (let i = 0; i < code.length; i++) {
31
+ const ch = code[i];
32
+ if (ch === ';')
33
+ break;
34
+ if (ch === '[') {
35
+ const end = code.indexOf(']', i);
36
+ i = end === -1 ? code.length : end;
37
+ continue;
38
+ }
39
+ if (ch === '"') {
40
+ const end = code.indexOf('"', i + 1);
41
+ literal(code.slice(i + 1, end === -1 ? code.length : end));
42
+ i = end === -1 ? code.length : end;
43
+ continue;
44
+ }
45
+ if (ch === '\\') {
46
+ literal(code[i + 1] ?? '');
47
+ i += 1;
48
+ continue;
49
+ }
50
+ const fraction = /^\.0+/.exec(code.slice(i));
51
+ if (fraction !== null && runs.at(-1)?.text === 's') {
52
+ runs.push({ kind: 'placeholder', text: '.0', count: fraction[0].length - 1 });
53
+ i += fraction[0].length - 1;
54
+ continue;
55
+ }
56
+ const lower = ch.toLowerCase();
57
+ if (lower === 'y' || lower === 'm' || lower === 'd' || lower === 'h' || lower === 's') {
58
+ let count = 1;
59
+ while (code[i + count]?.toLowerCase() === lower)
60
+ count += 1;
61
+ runs.push({ kind: 'placeholder', text: lower, count });
62
+ i += count - 1;
63
+ continue;
64
+ }
65
+ const meridiem = /^(AM\/PM|A\/P)/i.exec(code.slice(i));
66
+ if (meridiem !== null) {
67
+ runs.push({ kind: 'placeholder', text: 'am/pm', count: meridiem[0].length });
68
+ i += meridiem[0].length - 1;
69
+ continue;
70
+ }
71
+ literal(ch);
72
+ }
73
+ return runs;
74
+ }
75
+ function isMinuteRun(runs, index) {
76
+ for (let i = index - 1; i >= 0; i--) {
77
+ const run = runs[i];
78
+ if (run.kind === 'literal')
79
+ continue;
80
+ if (run.text === 'h')
81
+ return true;
82
+ break;
83
+ }
84
+ for (let i = index + 1; i < runs.length; i++) {
85
+ const run = runs[i];
86
+ if (run.kind === 'literal')
87
+ continue;
88
+ return run.text === 's';
89
+ }
90
+ return false;
91
+ }
92
+ export function formatSerialDate(date, code, utc) {
93
+ if (Number.isNaN(date.getTime()))
94
+ return '';
95
+ const runs = formatRuns(code);
96
+ const year = utc ? date.getUTCFullYear() : date.getFullYear();
97
+ const month = (utc ? date.getUTCMonth() : date.getMonth()) + 1;
98
+ const day = utc ? date.getUTCDate() : date.getDate();
99
+ const weekday = utc ? date.getUTCDay() : date.getDay();
100
+ const hour24 = utc ? date.getUTCHours() : date.getHours();
101
+ const minute = utc ? date.getUTCMinutes() : date.getMinutes();
102
+ const second = utc ? date.getUTCSeconds() : date.getSeconds();
103
+ const twelveHour = runs.some((run) => run.kind === 'placeholder' && run.text === 'am/pm');
104
+ const hour = twelveHour ? hour24 % 12 || 12 : hour24;
105
+ const pad = (value, width) => String(value).padStart(width, '0');
106
+ let out = '';
107
+ for (const [index, run] of runs.entries()) {
108
+ if (run.kind === 'literal') {
109
+ out += run.text;
110
+ continue;
111
+ }
112
+ switch (run.text) {
113
+ case 'y':
114
+ out += run.count <= 2 ? pad(year % 100, 2) : String(year);
115
+ break;
116
+ case 'm':
117
+ if (isMinuteRun(runs, index)) {
118
+ out += run.count >= 2 ? pad(minute, 2) : String(minute);
119
+ }
120
+ else if (run.count >= 5) {
121
+ out += MONTH_NAMES[month - 1][0];
122
+ }
123
+ else if (run.count === 4) {
124
+ out += MONTH_NAMES[month - 1];
125
+ }
126
+ else if (run.count === 3) {
127
+ out += MONTH_NAMES[month - 1].slice(0, 3);
128
+ }
129
+ else {
130
+ out += run.count === 2 ? pad(month, 2) : String(month);
131
+ }
132
+ break;
133
+ case 'd':
134
+ if (run.count >= 4)
135
+ out += WEEKDAY_NAMES[weekday];
136
+ else if (run.count === 3)
137
+ out += WEEKDAY_NAMES[weekday].slice(0, 3);
138
+ else
139
+ out += run.count === 2 ? pad(day, 2) : String(day);
140
+ break;
141
+ case 'h':
142
+ out += run.count >= 2 ? pad(hour, 2) : String(hour);
143
+ break;
144
+ case 's':
145
+ out += run.count >= 2 ? pad(second, 2) : String(second);
146
+ break;
147
+ case '.0': {
148
+ const digits = pad(utc ? date.getUTCMilliseconds() : date.getMilliseconds(), 3);
149
+ out += `.${(digits + '000').slice(0, run.count)}`;
150
+ break;
151
+ }
152
+ case 'am/pm':
153
+ out += hour24 < 12 ? 'AM' : 'PM';
154
+ break;
155
+ default:
156
+ break;
157
+ }
158
+ }
159
+ return out;
160
+ }
@@ -1,20 +1,40 @@
1
+ /**
2
+ * Which date system a workbook counts its serials in: the `date1904` flag of `<workbookPr>`.
3
+ *
4
+ * Carried as the epoch year rather than as a boolean because it is what every conversion here
5
+ * actually needs, and a boolean would mean a `? 1904 : 1900` at each of them: five chances to write
6
+ * the ternary backwards, on a value whose wrongness is silent by construction (a date read under the
7
+ * wrong system is still a perfectly good date, four years off).
8
+ */
9
+ export type DateEpoch = 1900 | 1904;
1
10
  /**
2
11
  * The number format applied to a `Date` cell that carries no explicit format of its own,
3
12
  * so the value renders, and reads back, as a date rather than a bare serial number.
4
13
  */
5
14
  export declare const DEFAULT_DATE_NUMFMT = "yyyy-mm-dd";
6
15
  /**
7
- * Convert a JS `Date` to its 1900-system Excel serial, reproducing the phantom-leap-day
8
- * quirk so the value renders on the calendar date Excel would show. Fractional serials
9
- * carry the time of day. The caller must reject a non-finite (invalid) date before here.
16
+ * Convert a JS `Date` to its Excel serial under the workbook's date system, reproducing the
17
+ * phantom-leap-day quirk (1900 only) so the value renders on the calendar date Excel would show.
18
+ * Fractional serials carry the time of day. The caller must reject a non-finite (invalid) date
19
+ * before here.
20
+ */
21
+ export declare function dateToSerial(date: Date, epoch: DateEpoch): number;
22
+ /**
23
+ * Convert an Excel serial back to a UTC `Date` under the workbook's date system, accounting in the
24
+ * 1900 system for the phantom 1900-02-29 so serial 1 reads as 1900-01-01 (not 1899-12-31) and
25
+ * consecutive serials map to consecutive days.
10
26
  */
11
- export declare function dateToSerial(date: Date): number;
27
+ export declare function serialToDate(serial: number, epoch: DateEpoch): Date;
12
28
  /**
13
- * Convert a 1900-system Excel serial back to a UTC `Date`, accounting for the phantom
14
- * 1900-02-29 so serial 1 reads as 1900-01-01 (not 1899-12-31) and consecutive serials
15
- * map to consecutive days.
29
+ * Parse a date-bearing text field (a Strict-mode `t="d"` cell value, a core-property timestamp) to a
30
+ * `Date`, or `null` when it carries nothing parseable.
31
+ *
32
+ * The `null` is the point. `new Date('not-a-date')` is a `Date` whose time is `NaN`: it satisfies
33
+ * `instanceof Date` and every guard downstream, survives into the model, and reaches serialisation,
34
+ * where it writes back as the literal string `Invalid Date` and loses the value for good. Dropping it
35
+ * at the boundary is the only reading that cannot lie.
16
36
  */
17
- export declare function serialToDate(serial: number): Date;
37
+ export declare function parseDateText(text: string): Date | null;
18
38
  /**
19
39
  * Whether a number-format code renders its value as a date or time. A format is a date
20
40
  * format when, once its non-formatting sections are removed (bracketed color/locale/
@@ -23,3 +43,18 @@ export declare function serialToDate(serial: number): Date;
23
43
  * while `yyyy-mm-dd`, `dd/mm/yyyy`, and `[$-409]mmmm d, yyyy` are.
24
44
  */
25
45
  export declare function isDateFormat(code: string): boolean;
46
+ /**
47
+ * Surface a number stored under a date format as a `Date`, and leave everything else alone.
48
+ *
49
+ * OOXML has no date type: a date is a number plus a number format that renders it as one, so this
50
+ * one test is what separates `45000` from `2023-03-15` across every reader. Only a plain number
51
+ * qualifies; a string, a boolean or a formula result of another kind under a date format keeps its
52
+ * own kind.
53
+ *
54
+ * One function rather than three copies of the test, because the corpus asserts the rule is identical
55
+ * across serialisations: the `.xlsb` reader, the `.xlsx` cell decoder and the cached formula-result
56
+ * decoder must all answer the same, and three spellings of one rule are three chances to disagree.
57
+ * The workbook's {@link DateEpoch} rides along for exactly that reason: it is the one input to this
58
+ * test that is not the cell's, and the three used to agree on the wrong answer for a 1904 file.
59
+ */
60
+ export declare function coerceDateSerial<T>(value: T, numFmt: string | undefined, epoch: DateEpoch): T | Date;
package/dist/core/date.js CHANGED
@@ -1,15 +1,26 @@
1
1
  const MS_PER_DAY = 86_400_000;
2
2
  const EPOCH_1900_UTC = Date.UTC(1899, 11, 30);
3
3
  const PHANTOM_SERIAL = 60;
4
+ const EPOCH_1904_UTC = Date.UTC(1904, 0, 1);
4
5
  export const DEFAULT_DATE_NUMFMT = 'yyyy-mm-dd';
5
- export function dateToSerial(date) {
6
+ export function dateToSerial(date, epoch) {
7
+ if (epoch === 1904)
8
+ return (date.getTime() - EPOCH_1904_UTC) / MS_PER_DAY;
6
9
  const days = (date.getTime() - EPOCH_1900_UTC) / MS_PER_DAY;
7
10
  return days <= PHANTOM_SERIAL ? days - 1 : days;
8
11
  }
9
- export function serialToDate(serial) {
12
+ export function serialToDate(serial, epoch) {
13
+ if (epoch === 1904)
14
+ return new Date(EPOCH_1904_UTC + serial * MS_PER_DAY);
10
15
  const dayOffset = serial < PHANTOM_SERIAL ? serial + 1 : serial;
11
16
  return new Date(EPOCH_1900_UTC + dayOffset * MS_PER_DAY);
12
17
  }
18
+ export function parseDateText(text) {
19
+ if (text === '')
20
+ return null;
21
+ const date = new Date(text);
22
+ return Number.isNaN(date.getTime()) ? null : date;
23
+ }
13
24
  export function isDateFormat(code) {
14
25
  const stripped = code
15
26
  .replace(/\[[^\]]*\]/g, '')
@@ -17,3 +28,8 @@ export function isDateFormat(code) {
17
28
  .replace(/\\./g, '');
18
29
  return /[ymdhs]/i.test(stripped);
19
30
  }
31
+ export function coerceDateSerial(value, numFmt, epoch) {
32
+ return typeof value === 'number' && numFmt !== undefined && isDateFormat(numFmt)
33
+ ? serialToDate(value, epoch)
34
+ : value;
35
+ }
@@ -47,3 +47,29 @@ export declare function mangleFormula(formula: string): string;
47
47
  * sheet name is untouched. Function names and defined names carry no row digits, so they pass through.
48
48
  */
49
49
  export declare function translateFormula(formula: string, colDelta: number, rowDelta: number): string;
50
+ /**
51
+ * A number as it appears *inside formula text*, which is not the same serialisation as an
52
+ * attribute's.
53
+ *
54
+ * Two divergences, both found as drift rather than designed. The exponent's case: Excel writes
55
+ * `1E+21` where JavaScript writes `1e+21`, so the same literal read back through the two codecs
56
+ * produced two different formula strings depending on which file it came from -- exactly the
57
+ * asymmetry `cell-value.ts` and `cell-accumulator.ts` were extracted to prevent. And the finiteness
58
+ * guard: the BIFF12 decoder had a private copy of this function with none, so a `PtgNum` whose eight
59
+ * bytes decode to an infinity produced the formula text `INFINITY`, which the writer then escaped as
60
+ * ordinary text into a package Excel reports as damaged.
61
+ *
62
+ * Here rather than in `xml/xml.ts` beside the attribute form, because formula text is not XML: the
63
+ * BIFF12 codec produces it too, and reaching the XML serialiser for it put the whole write half of
64
+ * that module into the closure of an entry that has no XML in it.
65
+ *
66
+ * @throws {AuthoringError} when the value is not finite.
67
+ */
68
+ export declare function formulaNumberLiteral(value: number): string;
69
+ /**
70
+ * Render a formula operand for serialisation: a number becomes its literal, a string is stripped of
71
+ * the single optional leading '=' an author may write (OOXML stores the expression without it, e.g.
72
+ * `=A1>0` on disk is `A1>0`). The result is unescaped: the caller escapes it for its target, whether
73
+ * that is element text or an attribute value.
74
+ */
75
+ export declare function stripFormulaEquals(value: string | number): string;
@@ -1,5 +1,7 @@
1
- import { columnToNumber, numberToColumn } from './address.js';
1
+ import { assertWritableNumber } from '../errors.js';
2
+ import { MAX_COLUMN, MAX_ROW, numberToColumn, tryColumnToNumber } from './address.js';
2
3
  import { MODERN_FUNCTIONS } from './modern-functions.js';
4
+ import { REF_ERROR } from './value.js';
3
5
  const XLFN = '_xlfn.';
4
6
  const XLPM = '_xlpm.';
5
7
  export function quoteSheetName(name, last) {
@@ -195,8 +197,22 @@ export function translateFormula(formula, colDelta, rowDelta) {
195
197
  if (colDelta === 0 && rowDelta === 0)
196
198
  return formula;
197
199
  return scanFormula(formula, (code) => code.replace(CELL_REFERENCE, (_match, colAbs, colLetters, rowAbs, rowDigits) => {
198
- const col = colAbs === '$' ? colLetters : numberToColumn(columnToNumber(colLetters) + colDelta);
199
- const row = rowAbs === '$' ? rowDigits : String(Number(rowDigits) + rowDelta);
200
- return `${colAbs}${col}${rowAbs}${row}`;
200
+ const decoded = tryColumnToNumber(colLetters);
201
+ if (decoded === undefined)
202
+ return REF_ERROR;
203
+ const col = colAbs === '$' ? decoded : decoded + colDelta;
204
+ const row = rowAbs === '$' ? Number(rowDigits) : Number(rowDigits) + rowDelta;
205
+ if (col < 1 || col > MAX_COLUMN || row < 1 || row > MAX_ROW)
206
+ return REF_ERROR;
207
+ return `${colAbs}${numberToColumn(col)}${rowAbs}${row}`;
201
208
  }));
202
209
  }
210
+ export function formulaNumberLiteral(value) {
211
+ assertWritableNumber(value);
212
+ return String(value).toUpperCase();
213
+ }
214
+ export function stripFormulaEquals(value) {
215
+ if (typeof value === 'number')
216
+ return formulaNumberLiteral(value);
217
+ return value.startsWith('=') ? value.slice(1) : value;
218
+ }
@@ -3,10 +3,10 @@ import { Cell } from './cell.ts';
3
3
  import type { ConditionalFormattingOverlay } from './conditional-formatting-overlay.ts';
4
4
  import type { DataValidationOverlay } from './data-validation-overlay.ts';
5
5
  import { type AnchoredImage } from './image.ts';
6
- import type { MergeRect } from './merge.ts';
7
6
  import type { Table } from './table.ts';
8
7
  import { type CellValue } from './value.ts';
9
8
  import type { WorksheetComments } from './worksheet-comments.ts';
9
+ import type { WorksheetMerges } from './worksheet-merges.ts';
10
10
  import type { ColumnProperties, RowProperties } from './worksheet.ts';
11
11
  /**
12
12
  * The sheet's autofilter, reached as a slot rather than held by reference like the containers beside
@@ -20,8 +20,7 @@ interface GridStorage {
20
20
  readonly rows: Map<number, Map<number, Cell>>;
21
21
  readonly rowProperties: Map<number, RowProperties>;
22
22
  readonly columns: Map<number, ColumnProperties>;
23
- readonly merges: string[];
24
- readonly mergeRects: MergeRect[];
23
+ readonly merges: WorksheetMerges;
25
24
  readonly tables: Table[];
26
25
  readonly images: AnchoredImage[];
27
26
  readonly dataValidations: DataValidationOverlay;