@shbernal/ts-xlsx 2.1.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 (225) 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 +5 -4
  7. package/dist/core/autofilter.js +34 -35
  8. package/dist/core/axis-handle.d.ts +10 -0
  9. package/dist/core/axis-handle.js +17 -0
  10. package/dist/core/cell.d.ts +25 -0
  11. package/dist/core/cell.js +9 -3
  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 +5 -1
  16. package/dist/core/column.js +34 -35
  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 +14 -6
  21. package/dist/core/conditional-formatting.js +45 -43
  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 +9 -3
  27. package/dist/core/data-validation.js +21 -18
  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 +14 -10
  39. package/dist/core/image.js +9 -9
  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-index.d.ts +31 -0
  44. package/dist/core/merge-index.js +60 -0
  45. package/dist/core/merge.d.ts +5 -14
  46. package/dist/core/merge.js +18 -27
  47. package/dist/core/page-setup.d.ts +72 -2
  48. package/dist/core/page-setup.js +34 -8
  49. package/dist/core/pivot-table.d.ts +1 -1
  50. package/dist/core/pivot-table.js +67 -53
  51. package/dist/core/protection.js +2 -11
  52. package/dist/core/range.js +34 -30
  53. package/dist/core/row-input.js +2 -2
  54. package/dist/core/row.d.ts +5 -1
  55. package/dist/core/row.js +22 -23
  56. package/dist/core/style.d.ts +134 -75
  57. package/dist/core/style.js +112 -53
  58. package/dist/core/table-style.d.ts +1 -1
  59. package/dist/core/table-style.js +4 -4
  60. package/dist/core/table.d.ts +28 -10
  61. package/dist/core/table.js +54 -39
  62. package/dist/core/theme.js +6 -5
  63. package/dist/core/used-extent.d.ts +44 -0
  64. package/dist/core/used-extent.js +102 -0
  65. package/dist/core/value.d.ts +9 -0
  66. package/dist/core/value.js +36 -34
  67. package/dist/core/workbook-media.d.ts +34 -0
  68. package/dist/core/workbook-media.js +44 -0
  69. package/dist/core/workbook-styles.d.ts +67 -1
  70. package/dist/core/workbook-vba.js +3 -2
  71. package/dist/core/workbook.d.ts +23 -2
  72. package/dist/core/workbook.js +24 -40
  73. package/dist/core/worksheet-comments.d.ts +2 -1
  74. package/dist/core/worksheet-comments.js +20 -13
  75. package/dist/core/worksheet-merges.d.ts +44 -0
  76. package/dist/core/worksheet-merges.js +59 -0
  77. package/dist/core/worksheet-model.d.ts +9 -1
  78. package/dist/core/worksheet-model.js +24 -9
  79. package/dist/core/worksheet.d.ts +72 -12
  80. package/dist/core/worksheet.js +68 -70
  81. package/dist/customui/ribbon.js +6 -7
  82. package/dist/entries/core.d.ts +18 -13
  83. package/dist/entries/core.js +12 -6
  84. package/dist/entries/xlsx.d.ts +1 -1
  85. package/dist/errors.d.ts +64 -0
  86. package/dist/errors.js +22 -0
  87. package/dist/hex.d.ts +8 -0
  88. package/dist/hex.js +3 -0
  89. package/dist/io/csv/delimiter.d.ts +9 -0
  90. package/dist/io/csv/delimiter.js +6 -0
  91. package/dist/io/csv/read.js +11 -4
  92. package/dist/io/csv/write.d.ts +9 -2
  93. package/dist/io/csv/write.js +9 -38
  94. package/dist/io/opc/inflate.js +15 -1
  95. package/dist/io/opc/namespaces.d.ts +16 -0
  96. package/dist/io/opc/namespaces.js +4 -0
  97. package/dist/io/opc/part-paths.d.ts +10 -0
  98. package/dist/io/opc/part-paths.js +16 -1
  99. package/dist/io/opc/read-opc.d.ts +69 -10
  100. package/dist/io/opc/read-opc.js +49 -55
  101. package/dist/io/opc/read-options.d.ts +5 -1
  102. package/dist/io/opc/rels.d.ts +1 -6
  103. package/dist/io/opc/rels.js +1 -4
  104. package/dist/io/opc/sniff-format.d.ts +3 -3
  105. package/dist/io/opc/sniff-format.js +2 -2
  106. package/dist/io/style/xf-style.d.ts +48 -2
  107. package/dist/io/style/xf-style.js +14 -0
  108. package/dist/io/xlsb/formula.d.ts +2 -2
  109. package/dist/io/xlsb/formula.js +49 -34
  110. package/dist/io/xlsb/primitives.js +8 -14
  111. package/dist/io/xlsb/ptg-functions.d.ts +19 -10
  112. package/dist/io/xlsb/ptg-functions.js +385 -596
  113. package/dist/io/xlsb/read-styles.js +50 -83
  114. package/dist/io/xlsb/read-worksheet.d.ts +20 -1
  115. package/dist/io/xlsb/read-worksheet.js +73 -89
  116. package/dist/io/xlsb/read.d.ts +6 -3
  117. package/dist/io/xlsb/read.js +47 -29
  118. package/dist/io/xlsb/record-stream.d.ts +30 -0
  119. package/dist/io/xlsb/record-stream.js +20 -0
  120. package/dist/io/xlsb/record-types.d.ts +1 -5
  121. package/dist/io/xlsb/record-types.js +1 -5
  122. package/dist/io/xlsx/cell-accumulator.d.ts +9 -2
  123. package/dist/io/xlsx/cell-accumulator.js +32 -9
  124. package/dist/io/xlsx/cell-style-resolution.d.ts +25 -0
  125. package/dist/io/xlsx/cell-style-resolution.js +29 -0
  126. package/dist/io/xlsx/cell-value.d.ts +5 -3
  127. package/dist/io/xlsx/cell-value.js +13 -15
  128. package/dist/io/xlsx/color-xml.js +3 -2
  129. package/dist/io/xlsx/column-budget.d.ts +26 -0
  130. package/dist/io/xlsx/column-budget.js +22 -0
  131. package/dist/io/xlsx/comments.d.ts +6 -2
  132. package/dist/io/xlsx/comments.js +36 -31
  133. package/dist/io/xlsx/conditional-formatting.d.ts +9 -5
  134. package/dist/io/xlsx/conditional-formatting.js +51 -46
  135. package/dist/io/xlsx/data-validation.d.ts +9 -7
  136. package/dist/io/xlsx/data-validation.js +38 -35
  137. package/dist/io/xlsx/edit-vba.d.ts +7 -2
  138. package/dist/io/xlsx/edit-vba.js +15 -29
  139. package/dist/io/xlsx/font-xml.d.ts +7 -0
  140. package/dist/io/xlsx/font-xml.js +104 -0
  141. package/dist/io/xlsx/hyperlinks.d.ts +41 -7
  142. package/dist/io/xlsx/hyperlinks.js +40 -28
  143. package/dist/io/xlsx/images.js +24 -25
  144. package/dist/io/xlsx/namespaces.d.ts +16 -0
  145. package/dist/io/xlsx/namespaces.js +7 -0
  146. package/dist/io/xlsx/package-plan.d.ts +23 -3
  147. package/dist/io/xlsx/package-plan.js +66 -25
  148. package/dist/io/xlsx/part-names.d.ts +41 -0
  149. package/dist/io/xlsx/part-names.js +46 -0
  150. package/dist/io/xlsx/pivot.js +8 -7
  151. package/dist/io/xlsx/read-parts.d.ts +59 -0
  152. package/dist/io/xlsx/read-parts.js +252 -0
  153. package/dist/io/xlsx/read-pivot.js +3 -1
  154. package/dist/io/xlsx/read-repair.d.ts +26 -0
  155. package/dist/io/xlsx/read-repair.js +39 -0
  156. package/dist/io/xlsx/{rich-runs.d.ts → read-rich-runs.d.ts} +1 -1
  157. package/dist/io/xlsx/{rich-runs.js → read-rich-runs.js} +7 -3
  158. package/dist/io/xlsx/read-rows.d.ts +18 -5
  159. package/dist/io/xlsx/read-rows.js +50 -37
  160. package/dist/io/xlsx/read-shared-strings.js +5 -3
  161. package/dist/io/xlsx/read-styles.d.ts +1 -6
  162. package/dist/io/xlsx/read-styles.js +65 -109
  163. package/dist/io/xlsx/read-workbook-xml.d.ts +36 -0
  164. package/dist/io/xlsx/read-workbook-xml.js +170 -0
  165. package/dist/io/xlsx/read-worksheet.d.ts +11 -1
  166. package/dist/io/xlsx/read-worksheet.js +51 -193
  167. package/dist/io/xlsx/read.d.ts +5 -13
  168. package/dist/io/xlsx/read.js +111 -436
  169. package/dist/io/xlsx/rich-text.js +1 -1
  170. package/dist/io/xlsx/row-position.d.ts +18 -0
  171. package/dist/io/xlsx/row-position.js +11 -0
  172. package/dist/io/xlsx/row-xml.d.ts +128 -0
  173. package/dist/io/xlsx/row-xml.js +203 -0
  174. package/dist/io/xlsx/shared-formulas.js +2 -2
  175. package/dist/io/xlsx/sheet-properties.d.ts +10 -1
  176. package/dist/io/xlsx/sheet-properties.js +166 -37
  177. package/dist/io/xlsx/style-elements.d.ts +43 -0
  178. package/dist/io/xlsx/style-elements.js +156 -0
  179. package/dist/io/xlsx/styles.d.ts +12 -3
  180. package/dist/io/xlsx/styles.js +5 -204
  181. package/dist/io/xlsx/tables.js +11 -14
  182. package/dist/io/xlsx/theme-xml.d.ts +26 -4
  183. package/dist/io/xlsx/theme-xml.js +107 -69
  184. package/dist/io/xlsx/threaded-comments.d.ts +8 -10
  185. package/dist/io/xlsx/threaded-comments.js +3 -1
  186. package/dist/io/xlsx/workbook-xml.d.ts +54 -4
  187. package/dist/io/xlsx/workbook-xml.js +84 -57
  188. package/dist/io/xlsx/worksheet-xml.d.ts +23 -70
  189. package/dist/io/xlsx/worksheet-xml.js +59 -242
  190. package/dist/io/xlsx/write-stream.d.ts +71 -16
  191. package/dist/io/xlsx/write-stream.js +79 -41
  192. package/dist/io/xlsx/write.d.ts +6 -2
  193. package/dist/io/xlsx/write.js +217 -153
  194. package/dist/rel-type.d.ts +16 -0
  195. package/dist/rel-type.js +6 -0
  196. package/dist/token-set.d.ts +28 -0
  197. package/dist/token-set.js +7 -0
  198. package/dist/vba/bytes.d.ts +14 -5
  199. package/dist/vba/bytes.js +16 -6
  200. package/dist/vba/cfb-format.d.ts +32 -0
  201. package/dist/vba/cfb-format.js +13 -0
  202. package/dist/vba/cfb-writer.d.ts +9 -0
  203. package/dist/vba/cfb-writer.js +28 -25
  204. package/dist/vba/cfb.d.ts +13 -1
  205. package/dist/vba/cfb.js +73 -57
  206. package/dist/vba/codepage.js +2 -1
  207. package/dist/vba/errors.d.ts +7 -0
  208. package/dist/vba/ms-ovba.js +79 -23
  209. package/dist/vba/project-editor.js +35 -40
  210. package/dist/vba/project.d.ts +11 -1
  211. package/dist/vba/project.js +40 -21
  212. package/dist/vba/vba-encoding.js +5 -6
  213. package/dist/xml/xml-attrs.d.ts +37 -0
  214. package/dist/xml/xml-attrs.js +30 -0
  215. package/dist/xml/xml-chars.d.ts +40 -0
  216. package/dist/xml/xml-chars.js +12 -0
  217. package/dist/xml/xml-namespaces.d.ts +31 -0
  218. package/dist/xml/xml-namespaces.js +50 -0
  219. package/dist/xml/xml-read.d.ts +128 -81
  220. package/dist/xml/xml-read.js +161 -178
  221. package/dist/xml/xml-scan.d.ts +78 -0
  222. package/dist/xml/xml-scan.js +180 -0
  223. package/dist/xml/xml.d.ts +29 -28
  224. package/dist/xml/xml.js +24 -24
  225. package/package.json +13 -9
@@ -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;
@@ -1,16 +1,16 @@
1
- import { decodeRange, encodeAddress, tryDecodeCellRef } from './address.js';
1
+ import { boundedRect, decodeRange, encodeAddress, encodeRect, tryDecodeCellRef } from './address.js';
2
2
  import { shiftAutoFilter } from './autofilter.js';
3
3
  import { Cell, copyCellContent } from './cell.js';
4
4
  import { replaceContents } from './containers.js';
5
- import { isDeletedSpan, shiftIndex } from './grid-shift.js';
5
+ import { isDeletedSpan, shiftIndex, shiftRect } from './grid-shift.js';
6
6
  import { isOneCellAnchor } from './image.js';
7
+ import { positionalPlacements } from './row-input.js';
7
8
  import { isSharedFormulaValue } from './value.js';
8
9
  export class GridEdits {
9
10
  #rows;
10
11
  #rowProperties;
11
12
  #columns;
12
13
  #merges;
13
- #mergeRects;
14
14
  #tables;
15
15
  #images;
16
16
  #dataValidations;
@@ -22,7 +22,6 @@ export class GridEdits {
22
22
  this.#rowProperties = storage.rowProperties;
23
23
  this.#columns = storage.columns;
24
24
  this.#merges = storage.merges;
25
- this.#mergeRects = storage.mergeRects;
26
25
  this.#tables = storage.tables;
27
26
  this.#images = storage.images;
28
27
  this.#dataValidations = storage.dataValidations;
@@ -31,13 +30,15 @@ export class GridEdits {
31
30
  this.#autoFilter = storage.autoFilter;
32
31
  }
33
32
  spliceRows(start, count, inserted) {
34
- const delta = inserted.length - count;
33
+ const splice = { axis: 'row', start, count, delta: inserted.length - count };
35
34
  const shifted = new Map();
36
35
  for (const [row, cols] of this.#rows) {
37
36
  if (row < start)
38
37
  shifted.set(row, cols);
39
- else if (row >= start + count)
40
- shifted.set(row + delta, this.#relocateRow(cols, row + delta));
38
+ else if (row >= start + count) {
39
+ const dest = shiftIndex(row, splice);
40
+ shifted.set(dest, this.#relocateRow(cols, dest));
41
+ }
41
42
  }
42
43
  inserted.forEach((cols, i) => {
43
44
  shifted.set(start + i, this.#relocateRow(cols, start + i));
@@ -45,15 +46,16 @@ export class GridEdits {
45
46
  this.#rows.clear();
46
47
  for (const [row, cols] of shifted)
47
48
  this.#rows.set(row, cols);
48
- this.#shiftLineProperties(this.#rowProperties, start, count, delta);
49
- this.#shiftMerges('row', start, count, delta);
50
- this.#shiftTables('row', start, count, delta);
51
- this.#shiftImages('row', start, count, delta);
52
- this.#reanchorSharedFormulas('row', start, count, delta);
53
- this.#shiftRangeBoundOverlays('row', start, count, delta);
49
+ this.#shiftLineProperties(this.#rowProperties, splice);
50
+ this.#shiftMerges(splice);
51
+ this.#shiftTables(splice);
52
+ this.#shiftImages(splice);
53
+ this.#reanchorSharedFormulas(splice);
54
+ this.#shiftRangeBoundOverlays(splice);
54
55
  }
55
56
  spliceColumns(start, count, inserts) {
56
- const delta = inserts.length - count;
57
+ const splice = { axis: 'col', start, count, delta: inserts.length - count };
58
+ const shiftedRows = new Map();
57
59
  for (const [row, cols] of this.#rows) {
58
60
  const shifted = new Map();
59
61
  for (const [col, cell] of cols) {
@@ -61,37 +63,42 @@ export class GridEdits {
61
63
  shifted.set(col, cell);
62
64
  }
63
65
  else if (col >= start + count) {
64
- const dest = col + delta;
66
+ const dest = shiftIndex(col, splice);
65
67
  const moved = new Cell(row, dest);
66
68
  copyCellContent(cell, moved);
67
69
  shifted.set(dest, moved);
68
70
  }
69
71
  }
70
- inserts.forEach((values, i) => {
71
- const value = values[row - 1];
72
- if (value !== undefined) {
73
- const cell = new Cell(row, start + i);
74
- cell.value = value;
75
- shifted.set(start + i, cell);
76
- }
77
- });
78
- this.#rows.set(row, shifted);
72
+ shiftedRows.set(row, shifted);
79
73
  }
80
- this.#shiftLineProperties(this.#columns, start, count, delta);
81
- this.#shiftMerges('col', start, count, delta);
82
- this.#shiftTables('col', start, count, delta);
83
- this.#shiftImages('col', start, count, delta);
84
- this.#reanchorSharedFormulas('col', start, count, delta);
85
- this.#shiftRangeBoundOverlays('col', start, count, delta);
74
+ inserts.forEach((values, i) => {
75
+ for (const [row, value] of positionalPlacements(values)) {
76
+ const cell = new Cell(row, start + i);
77
+ cell.value = value;
78
+ let cols = shiftedRows.get(row);
79
+ if (cols === undefined) {
80
+ cols = new Map();
81
+ shiftedRows.set(row, cols);
82
+ }
83
+ cols.set(start + i, cell);
84
+ }
85
+ });
86
+ for (const [row, cols] of shiftedRows)
87
+ this.#rows.set(row, cols);
88
+ this.#shiftLineProperties(this.#columns, splice);
89
+ this.#shiftMerges(splice);
90
+ this.#shiftTables(splice);
91
+ this.#shiftImages(splice);
92
+ this.#reanchorSharedFormulas(splice);
93
+ this.#shiftRangeBoundOverlays(splice);
86
94
  }
87
- #shiftRangeBoundOverlays(axis, start, count, delta) {
88
- this.#dataValidations.shift(axis, start, count, delta);
89
- this.#conditionalFormattings.shift(axis, start, count, delta);
90
- this.#comments.shift(axis, start, count, delta);
95
+ #shiftRangeBoundOverlays(splice) {
96
+ this.#dataValidations.shift(splice);
97
+ this.#conditionalFormattings.shift(splice);
98
+ this.#comments.shift(splice);
91
99
  const filter = this.#autoFilter.get();
92
- if (filter !== undefined) {
93
- this.#autoFilter.set(shiftAutoFilter(filter, axis, start, count, delta));
94
- }
100
+ if (filter !== undefined)
101
+ this.#autoFilter.set(shiftAutoFilter(filter, splice));
95
102
  }
96
103
  #relocateRow(cols, destRow) {
97
104
  const moved = new Map();
@@ -107,7 +114,7 @@ export class GridEdits {
107
114
  }
108
115
  return moved;
109
116
  }
110
- #reanchorSharedFormulas(axis, start, count, delta) {
117
+ #reanchorSharedFormulas(splice) {
111
118
  for (const cols of this.#rows.values()) {
112
119
  for (const cell of cols.values()) {
113
120
  const value = cell.value;
@@ -116,9 +123,9 @@ export class GridEdits {
116
123
  const master = tryDecodeCellRef(value.sharedFormula);
117
124
  if (master === undefined)
118
125
  continue;
119
- const anchored = axis === 'row'
120
- ? encodeAddress(master.col, shiftIndex(master.row, start, count, delta, 'row'))
121
- : encodeAddress(shiftIndex(master.col, start, count, delta, 'col'), master.row);
126
+ const anchored = splice.axis === 'row'
127
+ ? encodeAddress(master.col, shiftIndex(master.row, splice))
128
+ : encodeAddress(shiftIndex(master.col, splice), master.row);
122
129
  if (anchored === value.sharedFormula)
123
130
  continue;
124
131
  const reanchored = { ...value, sharedFormula: anchored };
@@ -126,59 +133,51 @@ export class GridEdits {
126
133
  }
127
134
  }
128
135
  }
129
- #shiftLineProperties(map, start, count, delta) {
136
+ #shiftLineProperties(map, splice) {
130
137
  const shifted = new Map();
131
138
  for (const [index, value] of map) {
132
- if (index < start)
133
- shifted.set(index, value);
134
- else if (index >= start + count)
135
- shifted.set(index + delta, value);
139
+ if (isDeletedSpan(index, index, splice))
140
+ continue;
141
+ shifted.set(shiftIndex(index, splice), value);
136
142
  }
137
143
  map.clear();
138
144
  for (const [index, value] of shifted)
139
145
  map.set(index, value);
140
146
  }
141
- #shiftMerges(axis, start, count, delta) {
142
- const shift = (v) => shiftIndex(v, start, count, delta, axis);
147
+ #shiftMerges(splice) {
143
148
  const merges = [];
144
149
  const rects = [];
145
- for (const range of this.#merges) {
146
- const { top, left, bottom, right } = decodeRange(range);
147
- if (top === undefined || left === undefined || bottom === undefined || right === undefined) {
150
+ for (const range of this.#merges.ranges) {
151
+ const decoded = boundedRect(decodeRange(range));
152
+ if (decoded === undefined) {
148
153
  merges.push(range);
149
154
  continue;
150
155
  }
151
- const [lo, hi] = axis === 'row' ? [top, bottom] : [left, right];
152
- if (isDeletedSpan(lo, hi, start, count))
156
+ const rect = shiftRect(decoded, splice);
157
+ if (rect === undefined)
153
158
  continue;
154
- const rect = axis === 'row'
155
- ? { top: shift(top), left, bottom: shift(bottom), right }
156
- : { top, left: shift(left), bottom, right: shift(right) };
157
159
  rects.push(rect);
158
- merges.push(`${encodeAddress(rect.left, rect.top)}:${encodeAddress(rect.right, rect.bottom)}`);
160
+ merges.push(encodeRect(rect));
159
161
  }
160
- replaceContents(this.#merges, merges);
161
- replaceContents(this.#mergeRects, rects);
162
+ this.#merges.replaceAll(merges, rects);
162
163
  }
163
- #shiftTables(axis, start, count, delta) {
164
- const survivors = this.#tables.filter((table) => axis === 'row'
165
- ? table.shiftRows(start, count, delta)
166
- : table.shiftColumns(start, count, delta));
164
+ #shiftTables(splice) {
165
+ const survivors = this.#tables.filter((table) => splice.axis === 'row' ? table.shiftRows(splice) : table.shiftColumns(splice));
167
166
  replaceContents(this.#tables, survivors);
168
167
  }
169
- #shiftImages(axis, start, count, delta) {
170
- const shiftPoint = (point) => {
171
- const zeroBased = axis === 'row' ? point.row : point.col;
172
- const shifted = shiftIndex(zeroBased + 1, start, count, delta, axis) - 1;
168
+ #shiftImages(splice) {
169
+ const shiftAnchor = (point) => {
170
+ const zeroBased = splice.axis === 'row' ? point.row : point.col;
171
+ const shifted = shiftIndex(zeroBased + 1, splice) - 1;
173
172
  if (shifted === zeroBased)
174
173
  return point;
175
- return axis === 'row' ? { ...point, row: shifted } : { ...point, col: shifted };
174
+ return splice.axis === 'row' ? { ...point, row: shifted } : { ...point, col: shifted };
176
175
  };
177
176
  const moved = this.#images.map((image) => {
178
- const from = shiftPoint(image.anchor.from);
177
+ const from = shiftAnchor(image.anchor.from);
179
178
  const anchor = isOneCellAnchor(image.anchor)
180
179
  ? { ...image.anchor, from }
181
- : { ...image.anchor, from, to: shiftPoint(image.anchor.to) };
180
+ : { ...image.anchor, from, to: shiftAnchor(image.anchor.to) };
182
181
  return { imageId: image.imageId, anchor };
183
182
  });
184
183
  replaceContents(this.#images, moved);