@pixerate/editor 0.23.0 → 0.25.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 (187) hide show
  1. package/README.md +167 -2
  2. package/THIRD_PARTY_NOTICES.md +80 -0
  3. package/dist/agent/index.cjs +10 -8
  4. package/dist/agent/index.cjs.map +1 -1
  5. package/dist/agent/index.d.cts +4 -3
  6. package/dist/agent/index.d.ts +4 -3
  7. package/dist/agent/index.js +4 -2
  8. package/dist/agent/index.js.map +1 -1
  9. package/dist/canvas/index.cjs +4437 -411
  10. package/dist/canvas/index.cjs.map +1 -1
  11. package/dist/canvas/index.d.cts +934 -4
  12. package/dist/canvas/index.d.ts +934 -4
  13. package/dist/canvas/index.js +4309 -283
  14. package/dist/canvas/index.js.map +1 -1
  15. package/dist/chunk-4BVP64JJ.cjs +1406 -0
  16. package/dist/chunk-4BVP64JJ.cjs.map +1 -0
  17. package/dist/chunk-664LQYFJ.js +115 -0
  18. package/dist/chunk-664LQYFJ.js.map +1 -0
  19. package/dist/chunk-7WISYZFO.cjs +740 -0
  20. package/dist/chunk-7WISYZFO.cjs.map +1 -0
  21. package/dist/chunk-AL6JMGWW.cjs +783 -0
  22. package/dist/chunk-AL6JMGWW.cjs.map +1 -0
  23. package/dist/chunk-BVP3TSLH.cjs +69 -0
  24. package/dist/chunk-BVP3TSLH.cjs.map +1 -0
  25. package/dist/{chunk-RIKBJLS2.cjs → chunk-C7P22VIP.cjs} +2001 -279
  26. package/dist/chunk-C7P22VIP.cjs.map +1 -0
  27. package/dist/chunk-D6FH2YAX.cjs +131 -0
  28. package/dist/chunk-D6FH2YAX.cjs.map +1 -0
  29. package/dist/chunk-ECCCYKGL.js +740 -0
  30. package/dist/chunk-ECCCYKGL.js.map +1 -0
  31. package/dist/{chunk-4SUNJZWB.cjs → chunk-FD4LPFAE.cjs} +48 -200
  32. package/dist/chunk-FD4LPFAE.cjs.map +1 -0
  33. package/dist/{chunk-QBJ22YZR.js → chunk-FH6F67DN.js} +1999 -277
  34. package/dist/chunk-FH6F67DN.js.map +1 -0
  35. package/dist/{chunk-OYXSKQFR.js → chunk-FPENXAEJ.js} +35 -187
  36. package/dist/chunk-FPENXAEJ.js.map +1 -0
  37. package/dist/chunk-HLIMTC6N.js +1 -0
  38. package/dist/chunk-HLIMTC6N.js.map +1 -0
  39. package/dist/chunk-HRMEDT2L.js +1406 -0
  40. package/dist/chunk-HRMEDT2L.js.map +1 -0
  41. package/dist/{chunk-QGHGJ56O.js → chunk-IXHSYOVY.js} +2518 -122
  42. package/dist/chunk-IXHSYOVY.js.map +1 -0
  43. package/dist/chunk-MKA3AHLV.cjs +256 -0
  44. package/dist/chunk-MKA3AHLV.cjs.map +1 -0
  45. package/dist/chunk-NAXVVS5I.js +783 -0
  46. package/dist/chunk-NAXVVS5I.js.map +1 -0
  47. package/dist/chunk-O5RYH2PG.js +69 -0
  48. package/dist/chunk-O5RYH2PG.js.map +1 -0
  49. package/dist/chunk-OXSV64JX.js +689 -0
  50. package/dist/chunk-OXSV64JX.js.map +1 -0
  51. package/dist/chunk-P3RQ5UJE.cjs +689 -0
  52. package/dist/chunk-P3RQ5UJE.cjs.map +1 -0
  53. package/dist/chunk-P6ODPZ4W.js +256 -0
  54. package/dist/chunk-P6ODPZ4W.js.map +1 -0
  55. package/dist/chunk-PL4JB3BF.cjs +1 -0
  56. package/dist/chunk-PL4JB3BF.cjs.map +1 -0
  57. package/dist/{chunk-GVSMRPHB.cjs → chunk-PO6A6QQH.cjs} +2607 -211
  58. package/dist/chunk-PO6A6QQH.cjs.map +1 -0
  59. package/dist/{chunk-JJDLPEWL.js → chunk-QGYDWPRM.js} +2 -2
  60. package/dist/chunk-RR7F5GUM.js +5726 -0
  61. package/dist/chunk-RR7F5GUM.js.map +1 -0
  62. package/dist/chunk-TNNJXF46.cjs +115 -0
  63. package/dist/chunk-TNNJXF46.cjs.map +1 -0
  64. package/dist/chunk-U5HZSFCS.js +131 -0
  65. package/dist/chunk-U5HZSFCS.js.map +1 -0
  66. package/dist/{chunk-MJZBNWG7.js → chunk-UAKDG62Z.js} +309 -581
  67. package/dist/chunk-UAKDG62Z.js.map +1 -0
  68. package/dist/{chunk-CL37JGFW.cjs → chunk-V3CHHWOE.cjs} +4 -4
  69. package/dist/{chunk-CL37JGFW.cjs.map → chunk-V3CHHWOE.cjs.map} +1 -1
  70. package/dist/chunk-WSQKHXW7.cjs +5726 -0
  71. package/dist/chunk-WSQKHXW7.cjs.map +1 -0
  72. package/dist/{chunk-AA4ZEVCC.cjs → chunk-X4N4WUKB.cjs} +336 -608
  73. package/dist/chunk-X4N4WUKB.cjs.map +1 -0
  74. package/dist/commands/index.cjs +74 -1
  75. package/dist/commands/index.cjs.map +1 -1
  76. package/dist/commands/index.d.cts +38 -353
  77. package/dist/commands/index.d.ts +38 -353
  78. package/dist/commands/index.js +73 -0
  79. package/dist/commands/index.js.map +1 -1
  80. package/dist/comment-store-BEMgVlAA.d.cts +135 -0
  81. package/dist/comment-store-BEMgVlAA.d.ts +135 -0
  82. package/dist/compare/index.cjs +15 -0
  83. package/dist/compare/index.cjs.map +1 -0
  84. package/dist/compare/index.d.cts +58 -0
  85. package/dist/compare/index.d.ts +58 -0
  86. package/dist/compare/index.js +15 -0
  87. package/dist/compare/index.js.map +1 -0
  88. package/dist/controller-BIJiNoVs.d.cts +1842 -0
  89. package/dist/controller-BIJiNoVs.d.ts +1842 -0
  90. package/dist/documents-CHT1U5Vs.d.ts +86 -0
  91. package/dist/documents-Gp7wb0Me.d.cts +86 -0
  92. package/dist/extensions/index.cjs +100 -5
  93. package/dist/extensions/index.cjs.map +1 -1
  94. package/dist/extensions/index.d.cts +16 -4
  95. package/dist/extensions/index.d.ts +16 -4
  96. package/dist/extensions/index.js +100 -5
  97. package/dist/generation/index.cjs +2317 -0
  98. package/dist/generation/index.cjs.map +1 -0
  99. package/dist/generation/index.d.cts +1015 -0
  100. package/dist/generation/index.d.ts +1015 -0
  101. package/dist/generation/index.js +2317 -0
  102. package/dist/generation/index.js.map +1 -0
  103. package/dist/grammar/index.cjs +48 -3
  104. package/dist/grammar/index.cjs.map +1 -1
  105. package/dist/grammar/index.d.cts +322 -100
  106. package/dist/grammar/index.d.ts +322 -100
  107. package/dist/grammar/index.js +54 -9
  108. package/dist/image-editor/index.cjs +4392 -481
  109. package/dist/image-editor/index.cjs.map +1 -1
  110. package/dist/image-editor/index.d.cts +1249 -97
  111. package/dist/image-editor/index.d.ts +1249 -97
  112. package/dist/image-editor/index.js +4281 -370
  113. package/dist/image-editor/index.js.map +1 -1
  114. package/dist/index-7a6bHTLy.d.ts +1754 -0
  115. package/dist/index-Csd5qI8u.d.cts +1754 -0
  116. package/dist/index.cjs +1682 -99
  117. package/dist/index.cjs.map +1 -1
  118. package/dist/index.d.cts +118 -21
  119. package/dist/index.d.ts +118 -21
  120. package/dist/index.js +1630 -47
  121. package/dist/index.js.map +1 -1
  122. package/dist/mention-text-CDnCFd3c.d.cts +53 -0
  123. package/dist/mention-text-CDnCFd3c.d.ts +53 -0
  124. package/dist/migrate-iTlm3a8h.d.cts +112 -0
  125. package/dist/migrate-iTlm3a8h.d.ts +112 -0
  126. package/dist/model-C1zApQM8.d.cts +219 -0
  127. package/dist/model-C1zApQM8.d.ts +219 -0
  128. package/dist/persistence/index.cjs +676 -0
  129. package/dist/persistence/index.cjs.map +1 -0
  130. package/dist/persistence/index.d.cts +230 -0
  131. package/dist/persistence/index.d.ts +230 -0
  132. package/dist/persistence/index.js +676 -0
  133. package/dist/persistence/index.js.map +1 -0
  134. package/dist/{registry-D5-R90p0.d.cts → registry-BDe-vlv3.d.ts} +2 -40
  135. package/dist/{registry-GJI4gToU.d.ts → registry-CEgwXED1.d.cts} +12 -1
  136. package/dist/{registry-GJI4gToU.d.cts → registry-CEgwXED1.d.ts} +12 -1
  137. package/dist/registry-D32LpIc4.d.cts +354 -0
  138. package/dist/{registry-D5-R90p0.d.ts → registry-DnzpgY27.d.cts} +2 -40
  139. package/dist/registry-evKTP4ls.d.ts +354 -0
  140. package/dist/schema-DYDGdYL9.d.cts +41 -0
  141. package/dist/schema-DYDGdYL9.d.ts +41 -0
  142. package/dist/serializers/index.cjs +12 -3
  143. package/dist/serializers/index.cjs.map +1 -1
  144. package/dist/serializers/index.d.cts +40 -12
  145. package/dist/serializers/index.d.ts +40 -12
  146. package/dist/serializers/index.js +12 -3
  147. package/dist/sourcemap-C8AH3MBQ.d.cts +91 -0
  148. package/dist/sourcemap-DA1Evhrv.d.ts +91 -0
  149. package/dist/spreadsheet/index.cjs +161 -3
  150. package/dist/spreadsheet/index.cjs.map +1 -1
  151. package/dist/spreadsheet/index.d.cts +611 -20
  152. package/dist/spreadsheet/index.d.ts +611 -20
  153. package/dist/spreadsheet/index.js +168 -10
  154. package/dist/storage-BpFfspHm.d.cts +120 -0
  155. package/dist/storage-BpFfspHm.d.ts +120 -0
  156. package/dist/tokens/index.cjs +115 -0
  157. package/dist/tokens/index.cjs.map +1 -0
  158. package/dist/tokens/index.d.cts +212 -0
  159. package/dist/tokens/index.d.ts +212 -0
  160. package/dist/tokens/index.js +115 -0
  161. package/dist/tokens/index.js.map +1 -0
  162. package/dist/{tokens-CyLZmIei.d.cts → tokens-D7yXvUU6.d.cts} +15 -2
  163. package/dist/{tokens-CyLZmIei.d.ts → tokens-D7yXvUU6.d.ts} +15 -2
  164. package/dist/{track-changes-BSF58RK0.d.cts → track-changes-D_VAPELi.d.cts} +110 -144
  165. package/dist/{track-changes-BSF58RK0.d.ts → track-changes-Db_fGEvx.d.ts} +110 -144
  166. package/package.json +21 -1
  167. package/dist/chunk-4SUNJZWB.cjs.map +0 -1
  168. package/dist/chunk-AA4ZEVCC.cjs.map +0 -1
  169. package/dist/chunk-AXV2ZW34.cjs +0 -118
  170. package/dist/chunk-AXV2ZW34.cjs.map +0 -1
  171. package/dist/chunk-B2ZRLN4S.js +0 -118
  172. package/dist/chunk-B2ZRLN4S.js.map +0 -1
  173. package/dist/chunk-GVSMRPHB.cjs.map +0 -1
  174. package/dist/chunk-K3R5NG2D.js +0 -3100
  175. package/dist/chunk-K3R5NG2D.js.map +0 -1
  176. package/dist/chunk-MJZBNWG7.js.map +0 -1
  177. package/dist/chunk-OYXSKQFR.js.map +0 -1
  178. package/dist/chunk-QBJ22YZR.js.map +0 -1
  179. package/dist/chunk-QGHGJ56O.js.map +0 -1
  180. package/dist/chunk-RIKBJLS2.cjs.map +0 -1
  181. package/dist/chunk-XDMXXC4Z.cjs +0 -3100
  182. package/dist/chunk-XDMXXC4Z.cjs.map +0 -1
  183. package/dist/controller-pePCJLT1.d.cts +0 -959
  184. package/dist/controller-pePCJLT1.d.ts +0 -959
  185. package/dist/index-CCr_z9kF.d.cts +0 -899
  186. package/dist/index-Ck6ajwXh.d.ts +0 -899
  187. /package/dist/{chunk-JJDLPEWL.js.map → chunk-QGYDWPRM.js.map} +0 -0
@@ -1,4 +1,30 @@
1
1
  // src/spreadsheet/formula/lexer.ts
2
+ var MAX_QUOTED_SHEET_LENGTH = 2 * 31 + 2;
3
+ var isWordStart = (char) => /[A-Za-z_$\\]/.test(char) || char > "\x7F" && /\p{L}/u.test(char);
4
+ var isWordChar = (char) => /[A-Za-z0-9_$.\\?]/.test(char) || char > "\x7F" && /[\p{L}\p{N}]/u.test(char);
5
+ var CELL_WORD = /^\$?[A-Za-z]+\$?[0-9]+$/;
6
+ function readQuotedSheet(input, start) {
7
+ let name = "";
8
+ let j = start + 1;
9
+ while (j < input.length && j - start <= MAX_QUOTED_SHEET_LENGTH) {
10
+ if (input[j] === "'") {
11
+ if (input[j + 1] === "'") {
12
+ name += "'";
13
+ j += 2;
14
+ continue;
15
+ }
16
+ if (input[j + 1] !== "!" || input[j + 2] === "=" || name === "") return null;
17
+ const colon = name.indexOf(":");
18
+ if (colon > 0 && colon < name.length - 1) {
19
+ return { sheet: name.slice(0, colon), sheetEnd: name.slice(colon + 1), next: j + 2 };
20
+ }
21
+ return { sheet: name, next: j + 2 };
22
+ }
23
+ name += input[j];
24
+ j++;
25
+ }
26
+ return null;
27
+ }
2
28
  function tokenizeFormula(formula) {
3
29
  const tokens = [];
4
30
  let input = formula.trim();
@@ -29,6 +55,13 @@ function tokenizeFormula(formula) {
29
55
  });
30
56
  continue;
31
57
  }
58
+ if (char === "'") {
59
+ const prefix = readQuotedSheet(input, i);
60
+ if (prefix) {
61
+ i = readQualifiedReference(input, prefix.next, i, prefix.sheet, prefix.sheetEnd, offset, tokens);
62
+ continue;
63
+ }
64
+ }
32
65
  if (char === '"' || char === "'") {
33
66
  const quote2 = char;
34
67
  const start = i;
@@ -48,7 +81,8 @@ function tokenizeFormula(formula) {
48
81
  tokens.push({
49
82
  type: "STRING",
50
83
  value: str,
51
- position: start + offset
84
+ position: start + offset,
85
+ end: i + offset
52
86
  });
53
87
  continue;
54
88
  }
@@ -75,7 +109,8 @@ function tokenizeFormula(formula) {
75
109
  tokens.push({
76
110
  type: "PROPERTY_REF",
77
111
  value: prop.trim(),
78
- position: start + offset
112
+ position: start + offset,
113
+ end: i + offset
79
114
  });
80
115
  continue;
81
116
  }
@@ -109,65 +144,96 @@ function tokenizeFormula(formula) {
109
144
  i++;
110
145
  continue;
111
146
  }
112
- if (/[a-zA-Z_$]/.test(char)) {
147
+ if (isWordStart(char)) {
113
148
  const start = i;
114
- while (i < input.length && /[a-zA-Z0-9_$.]/.test(input[i])) {
149
+ while (i < input.length && isWordChar(input[i])) {
115
150
  i++;
116
151
  }
117
152
  const rawWord = input.slice(start, i);
153
+ if (input[i] === "!" && input[i + 1] !== "=") {
154
+ i = readQualifiedReference(input, i + 1, start, rawWord, void 0, offset, tokens);
155
+ continue;
156
+ }
118
157
  if (i < input.length && input[i] === ":") {
119
158
  const colonPos = i;
120
159
  i++;
121
160
  const secondStart = i;
122
- while (i < input.length && /[a-zA-Z0-9_$.]/.test(input[i])) {
161
+ while (i < input.length && isWordChar(input[i])) {
123
162
  i++;
124
163
  }
125
164
  const secondWord = input.slice(secondStart, i);
165
+ if (secondWord.length > 0 && input[i] === "!" && input[i + 1] !== "=") {
166
+ i = readQualifiedReference(input, i + 1, start, rawWord, secondWord, offset, tokens);
167
+ continue;
168
+ }
126
169
  if (secondWord.length > 0) {
127
170
  tokens.push({
128
171
  type: "RANGE",
129
172
  value: `${rawWord}:${secondWord}`.toUpperCase(),
130
- position: start + offset
173
+ position: start + offset,
174
+ end: i + offset
131
175
  });
132
176
  continue;
133
177
  } else {
134
178
  i = colonPos;
135
179
  }
136
180
  }
137
- const upper = rawWord.toUpperCase();
138
- if (upper === "TRUE" || upper === "FALSE") {
139
- tokens.push({
140
- type: "BOOLEAN",
141
- value: upper,
142
- position: start + offset
143
- });
144
- continue;
145
- }
146
- if (/^\$?[A-Za-z]+\$?[0-9]+$/.test(rawWord)) {
147
- if (input[i] === "#") {
148
- i++;
149
- tokens.push({ type: "SPILL_REF", value: upper.replace(/\$/g, ""), position: start + offset });
150
- continue;
151
- }
152
- tokens.push({
153
- type: "CELL_REF",
154
- value: upper.replace(/\$/g, ""),
155
- position: start + offset
156
- });
157
- continue;
158
- }
159
- tokens.push({
160
- type: "IDENTIFIER",
161
- value: upper,
162
- position: start + offset
163
- });
181
+ pushWord(input, rawWord, start, i, offset, tokens);
182
+ if (tokens[tokens.length - 1].type === "SPILL_REF") i++;
164
183
  continue;
165
184
  }
166
185
  throw new Error(`Unexpected character '${char}' at position ${i + offset}`);
167
186
  }
168
- tokens.push({ type: "EOF", value: "", position: input.length + offset });
187
+ tokens.push({ type: "EOF", value: "", position: input.length + offset, end: input.length + offset });
188
+ for (const token of tokens) {
189
+ if (token.end === void 0) token.end = token.position + Math.max(token.value.length, 1);
190
+ }
169
191
  return tokens;
170
192
  }
193
+ function pushWord(input, rawWord, start, end, offset, tokens, qualified) {
194
+ const upper = rawWord.toUpperCase();
195
+ const position = qualified?.position ?? start + offset;
196
+ const extra = qualified ? { sheet: qualified.sheet, ...qualified.sheetEnd ? { sheetEnd: qualified.sheetEnd } : {}, refStart: qualified.refStart } : {};
197
+ if (!qualified && (upper === "TRUE" || upper === "FALSE")) {
198
+ tokens.push({ type: "BOOLEAN", value: upper, position, end: end + offset });
199
+ return;
200
+ }
201
+ if (CELL_WORD.test(rawWord)) {
202
+ if (input[end] === "#") {
203
+ tokens.push({ type: "SPILL_REF", value: upper.replace(/\$/g, ""), position, end: end + 1 + offset, ...extra });
204
+ return;
205
+ }
206
+ tokens.push({ type: "CELL_REF", value: upper.replace(/\$/g, ""), position, end: end + offset, ...extra });
207
+ return;
208
+ }
209
+ tokens.push({ type: "IDENTIFIER", value: upper, position, end: end + offset, ...extra });
210
+ }
211
+ function readQualifiedReference(input, from, prefixStart, sheet, sheetEnd, offset, tokens) {
212
+ let i = from;
213
+ while (i < input.length && isWordChar(input[i])) i++;
214
+ const word = input.slice(from, i);
215
+ if (!word) throw new Error(`Expected a reference after '!' at position ${from + offset}`);
216
+ const qualified = { sheet, sheetEnd, refStart: from + offset, position: prefixStart + offset };
217
+ if (input[i] === ":") {
218
+ let j = i + 1;
219
+ while (j < input.length && isWordChar(input[j])) j++;
220
+ const second = input.slice(i + 1, j);
221
+ if (second) {
222
+ tokens.push({
223
+ type: "RANGE",
224
+ value: `${word}:${second}`.toUpperCase(),
225
+ position: qualified.position,
226
+ end: j + offset,
227
+ sheet,
228
+ ...sheetEnd ? { sheetEnd } : {},
229
+ refStart: qualified.refStart
230
+ });
231
+ return j;
232
+ }
233
+ }
234
+ pushWord(input, word, from, i, offset, tokens, qualified);
235
+ return tokens[tokens.length - 1].type === "SPILL_REF" ? i + 1 : i;
236
+ }
171
237
 
172
238
  // src/spreadsheet/formula/parser.ts
173
239
  var WHOLE_COLUMN_END_ROW = 1048576;
@@ -205,6 +271,7 @@ function parseRange(rangeStr) {
205
271
  endRow: endRef.row
206
272
  };
207
273
  }
274
+ var qualifier = (token) => token.sheet === void 0 ? {} : token.sheetEnd === void 0 ? { sheet: token.sheet } : { sheet: token.sheet, sheetEnd: token.sheetEnd };
208
275
  var FormulaParser = class {
209
276
  tokens = [];
210
277
  current = 0;
@@ -275,7 +342,22 @@ var FormulaParser = class {
275
342
  const argument = this.unary();
276
343
  return { type: "UnaryOp", op, argument };
277
344
  }
278
- return this.primary();
345
+ let expr = this.primary();
346
+ while (this.check("LPAREN") && (expr.type === "FunctionCall" || expr.type === "Invoke" || this.previous().type === "RPAREN")) {
347
+ this.advance();
348
+ expr = { type: "Invoke", callee: expr, args: this.argumentList() };
349
+ }
350
+ return expr;
351
+ }
352
+ /** Arguments after `(` up to and including `)`. Empty positions are `Missing`. */
353
+ argumentList() {
354
+ const args = [];
355
+ if (this.match("RPAREN")) return args;
356
+ do {
357
+ args.push(this.check("COMMA") || this.check("RPAREN") ? { type: "Missing" } : this.expression());
358
+ } while (this.match("COMMA"));
359
+ this.consume("RPAREN", `Expected ')' after function arguments`);
360
+ return args;
279
361
  }
280
362
  primary() {
281
363
  const token = this.peek();
@@ -297,22 +379,27 @@ var FormulaParser = class {
297
379
  }
298
380
  if (token.type === "CELL_REF") {
299
381
  this.advance();
382
+ if (token.sheet === void 0 && this.check("LPAREN")) {
383
+ this.advance();
384
+ return { type: "FunctionCall", name: token.value, args: this.argumentList() };
385
+ }
300
386
  const parsed = parseCellReference(token.value);
301
387
  return {
302
388
  type: "CellRef",
303
389
  ref: token.value,
304
390
  col: parsed.col,
305
- row: parsed.row
391
+ row: parsed.row,
392
+ ...qualifier(token)
306
393
  };
307
394
  }
308
395
  if (token.type === "RANGE") {
309
396
  this.advance();
310
- return parseRange(token.value);
397
+ return { ...parseRange(token.value), ...qualifier(token) };
311
398
  }
312
399
  if (token.type === "SPILL_REF") {
313
400
  this.advance();
314
401
  const parsed = parseCellReference(token.value);
315
- return { type: "SpillRef", ref: token.value, col: parsed.col, row: parsed.row };
402
+ return { type: "SpillRef", ref: token.value, col: parsed.col, row: parsed.row, ...qualifier(token) };
316
403
  }
317
404
  if (token.type === "PROPERTY_REF") {
318
405
  this.advance();
@@ -325,21 +412,20 @@ var FormulaParser = class {
325
412
  const name = token.value;
326
413
  this.advance();
327
414
  if (this.check("LPAREN")) {
328
- this.consume("LPAREN", `Expected '(' after function ${name}`);
329
- const args = [];
330
- if (!this.check("RPAREN")) {
331
- do {
332
- args.push(this.expression());
333
- } while (this.match("COMMA"));
415
+ if (token.sheet !== void 0) {
416
+ throw new Error(`Unexpected '(' after ${token.sheet}!${name} at position ${this.peek().position}`);
334
417
  }
335
- this.consume("RPAREN", `Expected ')' after function arguments`);
418
+ this.consume("LPAREN", `Expected '(' after function ${name}`);
336
419
  return {
337
420
  type: "FunctionCall",
338
421
  name,
339
- args
422
+ args: this.argumentList()
340
423
  };
341
424
  }
342
- return { type: "Name", name };
425
+ if (token.sheetEnd !== void 0) {
426
+ throw new Error(`A 3-D reference can't name ${name} at position ${token.position}`);
427
+ }
428
+ return token.sheet !== void 0 ? { type: "Name", name, sheet: token.sheet } : { type: "Name", name };
343
429
  }
344
430
  if (this.match("LPAREN")) {
345
431
  const expr = this.expression();
@@ -392,6 +478,35 @@ function parseFormula(formula) {
392
478
  return parser.parse();
393
479
  }
394
480
 
481
+ // src/spreadsheet/formula/lambda.ts
482
+ var FormulaLambda = class {
483
+ constructor(params, body, env, sheet, native, label = "LAMBDA") {
484
+ this.params = params;
485
+ this.body = body;
486
+ this.env = env;
487
+ this.sheet = sheet;
488
+ this.native = native;
489
+ this.label = label;
490
+ }
491
+ params;
492
+ body;
493
+ env;
494
+ sheet;
495
+ native;
496
+ label;
497
+ toString() {
498
+ return `[${this.label}]`;
499
+ }
500
+ };
501
+ var isFormulaLambda = (value) => value instanceof FormulaLambda;
502
+ var MAX_LAMBDA_DEPTH = 1e3;
503
+ function lookupEnvironment(env, name) {
504
+ for (let e = env; e; e = e.parent) {
505
+ if (e.vars.has(name)) return e;
506
+ }
507
+ return null;
508
+ }
509
+
395
510
  // src/spreadsheet/formula/values.ts
396
511
  var FORMULA_ERRORS = /* @__PURE__ */ new Set([
397
512
  "#DIV/0!",
@@ -982,209 +1097,1343 @@ function parseCriteria(criterion) {
982
1097
  };
983
1098
  }
984
1099
 
985
- // src/spreadsheet/formula/format.ts
986
- function formatCellDisplay(cell) {
987
- if (!cell) return "";
988
- if (cell.error) return cell.error;
989
- const { value, numberFormat } = cell;
990
- if (numberFormat && value !== "" && value !== null && value !== void 0 && typeof value !== "boolean") {
991
- if (/^\[h\]/i.test(numberFormat) && typeof value === "number") return formatElapsedHours(value);
992
- if (numberFormat === "# ?/?" && typeof value === "number") return formatFraction(value);
993
- const formatted = formatValueWithCode(value, numberFormat);
994
- if (formatted !== null) return formatted;
995
- }
996
- return toText(value);
997
- }
998
- function formatFraction(value) {
999
- const sign = value < 0 ? "-" : "";
1000
- const abs = Math.abs(value);
1001
- let whole = Math.floor(abs);
1002
- const frac = abs - whole;
1003
- let best = { num: 0, den: 1, err: frac };
1004
- for (let den = 2; den <= 9; den++) {
1005
- const num = Math.round(frac * den);
1006
- const err = Math.abs(frac - num / den);
1007
- if (err < best.err - 1e-12) best = { num, den, err };
1008
- }
1009
- if (best.num === best.den) {
1010
- whole += 1;
1011
- best = { num: 0, den: 1, err: 0 };
1012
- }
1013
- if (best.num === 0) return `${sign}${whole}`;
1014
- return `${sign}${whole === 0 ? "" : `${whole} `}${best.num}/${best.den}`;
1015
- }
1016
- function formatElapsedHours(days) {
1017
- const total = Math.round(Math.abs(days) * 86400);
1018
- const p = (n) => String(n).padStart(2, "0");
1019
- const text = `${Math.floor(total / 3600)}:${p(Math.floor(total / 60) % 60)}:${p(total % 60)}`;
1020
- return days < 0 ? `-${text}` : text;
1021
- }
1022
- function formatValueWithCode(value, format) {
1023
- if (isDateFormat(format)) {
1024
- const date = toDate(value);
1025
- return date ? formatDate(date, format) : null;
1026
- }
1027
- const num = typeof value === "boolean" ? null : numericValue(value);
1028
- return num === null ? toText(value) : formatNumber(num, format);
1029
- }
1030
- function tokenize(format) {
1031
- const out = [];
1032
- const push = (literal, text) => {
1033
- const last = out[out.length - 1];
1034
- if (last && last.literal === literal) last.text += text;
1035
- else out.push({ literal, text });
1036
- };
1037
- for (let i = 0; i < format.length; i++) {
1038
- const char = format[i];
1039
- if (char === '"') {
1040
- const end = format.indexOf('"', i + 1);
1041
- const stop = end === -1 ? format.length : end;
1042
- push(true, format.slice(i + 1, stop));
1043
- i = stop;
1044
- } else if (char === "\\" && i + 1 < format.length) {
1045
- push(true, format[++i]);
1100
+ // src/spreadsheet/numfmt/decimal.ts
1101
+ var MAX_DECIMALS = 400;
1102
+ function significantDigits(a, n) {
1103
+ const s = a.toExponential(Math.max(0, Math.min(100, n - 1)));
1104
+ const e = s.indexOf("e");
1105
+ const digits = [];
1106
+ for (let i = 0; i < e; i++) {
1107
+ const c = s.charCodeAt(i);
1108
+ if (c >= 48 && c <= 57) digits.push(c - 48);
1109
+ }
1110
+ return { digits, exp: Number(s.slice(e + 1)) };
1111
+ }
1112
+ function exactDigits(a) {
1113
+ const view = new DataView(new ArrayBuffer(8));
1114
+ view.setFloat64(0, a);
1115
+ const hi = view.getUint32(0);
1116
+ const lo = view.getUint32(4);
1117
+ const biased = hi >>> 20 & 2047;
1118
+ let mantissa = BigInt(hi & 1048575) << 32n | BigInt(lo);
1119
+ let exp2;
1120
+ if (biased === 0) {
1121
+ exp2 = -1074;
1122
+ } else {
1123
+ mantissa |= 1n << 52n;
1124
+ exp2 = biased - 1075;
1125
+ }
1126
+ const digits = exp2 >= 0 ? (mantissa << BigInt(exp2)).toString() : (mantissa * 5n ** BigInt(-exp2)).toString();
1127
+ return digits.replace(/0+$/, "");
1128
+ }
1129
+ function isExactTie(a) {
1130
+ const isTie = (ds) => ds.length >= 16 && ds[15] === "5" && /^0*$/.test(ds.slice(16));
1131
+ return isTie(significantDigits(a, 30).digits.join("")) && isTie(exactDigits(a).padEnd(16, "0"));
1132
+ }
1133
+ function decimalDigits(a) {
1134
+ if (!Number.isFinite(a) || a <= 0) return { digits: new Array(15).fill(0), exp: 0 };
1135
+ const { digits: d16, exp } = significantDigits(a, 16);
1136
+ const digits = d16.slice(0, 15);
1137
+ while (digits.length < 15) digits.push(0);
1138
+ const sixteenth = d16[15] ?? 0;
1139
+ if (sixteenth <= 4) return { digits, exp };
1140
+ if (sixteenth === 5) {
1141
+ if (isExactTie(a)) return { digits, exp };
1142
+ const r = significantDigits(a, 15);
1143
+ while (r.digits.length < 15) r.digits.push(0);
1144
+ return r;
1145
+ }
1146
+ let e = exp;
1147
+ let carry = true;
1148
+ for (let i = 14; i >= 0; i--) {
1149
+ if (digits[i] === 9) {
1150
+ digits[i] = 0;
1046
1151
  } else {
1047
- push(false, char);
1152
+ digits[i] += 1;
1153
+ carry = false;
1154
+ break;
1155
+ }
1156
+ }
1157
+ if (carry) {
1158
+ digits.fill(0);
1159
+ digits[0] = 1;
1160
+ e += 1;
1161
+ }
1162
+ return { digits, exp: e };
1163
+ }
1164
+ function fixedFromDigits(digits, exp, decimals) {
1165
+ const decimalsCalc = Math.min(decimals, MAX_DECIMALS);
1166
+ const keep = exp + decimalsCalc + 1;
1167
+ let m;
1168
+ if (keep <= 0) {
1169
+ m = keep === 0 && (digits[0] ?? 0) >= 5 ? [1] : [];
1170
+ } else if (keep >= digits.length) {
1171
+ m = digits.slice();
1172
+ while (m.length < keep) m.push(0);
1173
+ } else {
1174
+ m = digits.slice(0, keep);
1175
+ if ((digits[keep] ?? 0) >= 5) {
1176
+ let carry = true;
1177
+ for (let i = m.length - 1; i >= 0; i--) {
1178
+ if (m[i] === 9) {
1179
+ m[i] = 0;
1180
+ } else {
1181
+ m[i] += 1;
1182
+ carry = false;
1183
+ break;
1184
+ }
1185
+ }
1186
+ if (carry) m.unshift(1);
1187
+ }
1188
+ }
1189
+ let lead = 0;
1190
+ while (lead < m.length && m[lead] === 0) lead++;
1191
+ m = m.slice(lead);
1192
+ const toS = (ds) => ds.join("");
1193
+ let int;
1194
+ let frac;
1195
+ if (m.length <= decimalsCalc) {
1196
+ int = "";
1197
+ frac = "0".repeat(decimalsCalc - m.length) + toS(m);
1198
+ } else {
1199
+ const split = m.length - decimalsCalc;
1200
+ int = toS(m.slice(0, split));
1201
+ frac = toS(m.slice(split));
1202
+ }
1203
+ if (decimals > decimalsCalc) frac += "0".repeat(decimals - decimalsCalc);
1204
+ return [int, frac];
1205
+ }
1206
+ function fixed(a, decimals) {
1207
+ if (!Number.isFinite(a) || a <= 0) return ["", "0".repeat(Math.min(decimals, MAX_DECIMALS * 4))];
1208
+ const { digits, exp } = decimalDigits(a);
1209
+ return fixedFromDigits(digits, exp, decimals);
1210
+ }
1211
+ var charCount = (s) => Array.from(s).length;
1212
+ function formatGeneralFit(n, maxChars) {
1213
+ if (!Number.isFinite(n)) return null;
1214
+ if (n === 0) return maxChars >= 1 ? "0" : null;
1215
+ const sign = n < 0 ? "-" : "";
1216
+ const { digits, exp: x } = decimalDigits(Math.abs(n));
1217
+ const intLen = sign.length + Math.max(x, 0) + 1;
1218
+ if (x >= -4 && intLen <= maxChars) {
1219
+ const avail = maxChars - intLen;
1220
+ let decimals = avail >= 2 ? avail - 1 : 0;
1221
+ for (; ; ) {
1222
+ const [i, f0] = fixedFromDigits(digits, x, decimals);
1223
+ const f = f0.replace(/0+$/, "");
1224
+ if (i === "" && f === "") break;
1225
+ let s = sign + (i === "" ? "0" : i);
1226
+ if (f !== "") s += `.${f}`;
1227
+ if (charCount(s) <= maxChars) return s;
1228
+ if (decimals === 0) break;
1229
+ decimals -= 1;
1230
+ }
1231
+ }
1232
+ for (let md = Math.min(maxChars, 14); md >= 0; md--) {
1233
+ let e = x;
1234
+ let [mi, mf] = fixedFromDigits(digits, 0, md);
1235
+ if (mi.length > 1) {
1236
+ e += 1;
1237
+ [mi, mf] = fixedFromDigits(digits, -1, md);
1238
+ }
1239
+ mf = mf.replace(/0+$/, "");
1240
+ let s = sign + (mi === "" ? "0" : mi);
1241
+ if (mf !== "") s += `.${mf}`;
1242
+ s += `E${e < 0 ? "-" : "+"}${String(Math.abs(e)).padStart(2, "0")}`;
1243
+ if (charCount(s) <= maxChars) return s;
1244
+ }
1245
+ return null;
1246
+ }
1247
+
1248
+ // src/spreadsheet/numfmt/builtin.ts
1249
+ var BUILTINS = [
1250
+ [0, "General"],
1251
+ [1, "0"],
1252
+ [2, "0.00"],
1253
+ [3, "#,##0"],
1254
+ [4, "#,##0.00"],
1255
+ [5, '"$"#,##0_);\\("$"#,##0\\)'],
1256
+ [6, '"$"#,##0_);[Red]\\("$"#,##0\\)'],
1257
+ [7, '"$"#,##0.00_);\\("$"#,##0.00\\)'],
1258
+ [8, '"$"#,##0.00_);[Red]\\("$"#,##0.00\\)'],
1259
+ [9, "0%"],
1260
+ [10, "0.00%"],
1261
+ [11, "0.00E+00"],
1262
+ [12, "# ?/?"],
1263
+ [13, "# ??/??"],
1264
+ [14, "m/d/yyyy"],
1265
+ [15, "d-mmm-yy"],
1266
+ [16, "d-mmm"],
1267
+ [17, "mmm-yy"],
1268
+ [18, "h:mm AM/PM"],
1269
+ [19, "h:mm:ss AM/PM"],
1270
+ [20, "h:mm"],
1271
+ [21, "h:mm:ss"],
1272
+ [22, "m/d/yyyy h:mm"],
1273
+ [37, "#,##0 ;(#,##0)"],
1274
+ [38, "#,##0 ;[Red](#,##0)"],
1275
+ [39, "#,##0.00;(#,##0.00)"],
1276
+ [40, "#,##0.00;[Red](#,##0.00)"],
1277
+ [41, '_(* #,##0_);_(* \\(#,##0\\);_(* "-"_);_(@_)'],
1278
+ [42, '_("$"* #,##0_);_("$"* \\(#,##0\\);_("$"* "-"_);_(@_)'],
1279
+ [43, '_(* #,##0.00_);_(* \\(#,##0.00\\);_(* "-"??_);_(@_)'],
1280
+ [44, '_("$"* #,##0.00_);_("$"* \\(#,##0.00\\);_("$"* "-"??_);_(@_)'],
1281
+ [45, "mm:ss"],
1282
+ [46, "[h]:mm:ss"],
1283
+ [47, "mmss.0"],
1284
+ [48, "##0.0E+0"],
1285
+ [49, "@"]
1286
+ ];
1287
+ function builtinNumberFormat(id) {
1288
+ return BUILTINS.find(([i]) => i === id)?.[1];
1289
+ }
1290
+ function builtinNumberFormatId(code) {
1291
+ const exact = BUILTINS.find(([, c]) => c === code);
1292
+ if (exact) return exact[0];
1293
+ const norm = (s) => s.replace(/\\/g, "").toLowerCase();
1294
+ const n = norm(code);
1295
+ return BUILTINS.find(([, c]) => norm(c) === n)?.[0];
1296
+ }
1297
+
1298
+ // src/spreadsheet/numfmt/parse.ts
1299
+ var MAX_SECTIONS = 4;
1300
+ var isCalendarPart = (d) => d.k === "year" || d.k === "month" || d.k === "day";
1301
+ function literalText(tok) {
1302
+ switch (tok.t) {
1303
+ case "lit":
1304
+ case "denom":
1305
+ return tok.s;
1306
+ case "digit":
1307
+ return tok.c;
1308
+ case "point":
1309
+ return ".";
1310
+ case "comma":
1311
+ return ",";
1312
+ case "percent":
1313
+ return "%";
1314
+ case "exp":
1315
+ return `${tok.upper ? "E" : "e"}${tok.plus ? "+" : "-"}`;
1316
+ case "slash":
1317
+ return "/";
1318
+ default:
1319
+ return "";
1320
+ }
1321
+ }
1322
+ function testCond(c, v) {
1323
+ switch (c.op) {
1324
+ case "=":
1325
+ return v === c.value;
1326
+ case "<>":
1327
+ return v !== c.value;
1328
+ case "<":
1329
+ return v < c.value;
1330
+ case "<=":
1331
+ return v <= c.value;
1332
+ case ">":
1333
+ return v > c.value;
1334
+ case ">=":
1335
+ return v >= c.value;
1336
+ }
1337
+ }
1338
+ var isDigitTok = (t, c) => t?.t === "digit" && (c === void 0 || t.c === c);
1339
+ function splitSections(code) {
1340
+ const cs = Array.from(code);
1341
+ const lastClose = cs.lastIndexOf("]");
1342
+ const out = [""];
1343
+ let quoted = false;
1344
+ let escape = false;
1345
+ let i = 0;
1346
+ while (i < cs.length) {
1347
+ const c = cs[i];
1348
+ i += 1;
1349
+ const cur = out.length - 1;
1350
+ if (escape) {
1351
+ out[cur] += c;
1352
+ escape = false;
1353
+ continue;
1354
+ }
1355
+ if (c === '"') {
1356
+ quoted = !quoted;
1357
+ out[cur] += c;
1358
+ } else if ((c === "\\" || c === "_" || c === "*") && !quoted) {
1359
+ escape = true;
1360
+ out[cur] += c;
1361
+ } else if (c === "[" && !quoted && lastClose >= i) {
1362
+ let s = c;
1363
+ while (i < cs.length) {
1364
+ const d = cs[i];
1365
+ i += 1;
1366
+ s += d;
1367
+ if (d === "]") break;
1368
+ }
1369
+ out[cur] += s;
1370
+ } else if (c === ";" && !quoted) {
1371
+ if (out.length >= MAX_SECTIONS) break;
1372
+ out.push("");
1373
+ } else {
1374
+ out[cur] += c;
1048
1375
  }
1049
1376
  }
1050
1377
  return out;
1051
1378
  }
1052
- var codeText = (format) => tokenize(format).filter((t) => !t.literal).map((t) => t.text).join("");
1053
- function isDateFormat(format) {
1054
- const code = codeText(format);
1055
- if (/[0#?]/.test(code)) return false;
1056
- return /[ydhs]|m/i.test(code);
1057
- }
1058
- function formatNumber(num, format) {
1059
- const sections = splitSections(format);
1060
- let section = sections[0];
1061
- let value = num;
1062
- let sign = num < 0 ? "-" : "";
1063
- if (num < 0 && sections.length > 1) {
1064
- section = sections[1];
1065
- value = Math.abs(num);
1066
- sign = "";
1067
- } else if (num === 0 && sections.length > 2) {
1068
- section = sections[2];
1069
- }
1070
- let prefix = "";
1071
- let suffix = "";
1072
- let pattern = "";
1073
- for (const token of tokenize(section)) {
1074
- if (token.literal) {
1075
- if (pattern) suffix += token.text;
1076
- else prefix += token.text;
1379
+ var lowerAscii = (c) => c >= "A" && c <= "Z" ? c.toLowerCase() : c;
1380
+ function startsWithCi(cs, i, pat) {
1381
+ for (let k = 0; k < pat.length; k++) {
1382
+ const c = cs[i + k];
1383
+ if (c === void 0 || lowerAscii(c) !== pat[k]) return false;
1384
+ }
1385
+ return true;
1386
+ }
1387
+ function pushLit(toks, s) {
1388
+ if (s === "") return;
1389
+ const last = toks[toks.length - 1];
1390
+ if (last?.t === "lit") last.s += s;
1391
+ else toks.push({ t: "lit", s });
1392
+ }
1393
+ var CURRENCY_CHARS = /* @__PURE__ */ new Set(["$", "\u20AC", "\xA3", "\xA5", "\u20B9", "\u20A9", "\u20BD", "\xA2", "\u20AA", "\u20AB", "\u0E3F"]);
1394
+ var isCurrencyChar = (c) => CURRENCY_CHARS.has(c);
1395
+ var NAMED_COLORS = /* @__PURE__ */ new Set(["black", "blue", "cyan", "green", "magenta", "red", "white", "yellow"]);
1396
+ function parseColor(lc) {
1397
+ if (NAMED_COLORS.has(lc)) return lc;
1398
+ if (!lc.startsWith("color")) return void 0;
1399
+ const rest = lc.slice(5).trim();
1400
+ if (!/^\+?\d{1,3}$/.test(rest)) return void 0;
1401
+ const n = Number(rest);
1402
+ return n >= 1 && n <= 56 ? `color${n}` : void 0;
1403
+ }
1404
+ function parseCond(s) {
1405
+ const t = s.trim();
1406
+ let op;
1407
+ let rest;
1408
+ if (t.startsWith("<=")) [op, rest] = ["<=", t.slice(2)];
1409
+ else if (t.startsWith(">=")) [op, rest] = [">=", t.slice(2)];
1410
+ else if (t.startsWith("<>")) [op, rest] = ["<>", t.slice(2)];
1411
+ else if (t.startsWith("<")) [op, rest] = ["<", t.slice(1)];
1412
+ else if (t.startsWith(">")) [op, rest] = [">", t.slice(1)];
1413
+ else if (t.startsWith("=")) [op, rest] = ["=", t.slice(1)];
1414
+ else return void 0;
1415
+ const r = rest.trim();
1416
+ if (!/^[+-]?(\d+\.?\d*|\.\d+)(e[+-]?\d+)?$/i.test(r)) return void 0;
1417
+ const value = Number(r);
1418
+ return Number.isFinite(value) ? { op, value } : void 0;
1419
+ }
1420
+ function bracket(raw, content) {
1421
+ const lc = content.trim().toLowerCase();
1422
+ const color = parseColor(lc);
1423
+ if (color) {
1424
+ raw.color = color;
1425
+ return;
1426
+ }
1427
+ if (lc.startsWith("<") || lc.startsWith(">") || lc.startsWith("=")) {
1428
+ const c = parseCond(lc);
1429
+ if (c) raw.cond = c;
1430
+ return;
1431
+ }
1432
+ if (content.startsWith("$")) {
1433
+ const sym = content.slice(1).split("-")[0];
1434
+ if (sym !== "") {
1435
+ raw.currency = true;
1436
+ pushLit(raw.toks, sym);
1437
+ }
1438
+ return;
1439
+ }
1440
+ const first = lc[0];
1441
+ if ((first === "h" || first === "m" || first === "s") && Array.from(lc).every((c) => c === first)) {
1442
+ const n = Math.min(Array.from(lc).length, 255);
1443
+ raw.toks.push({ t: "date", d: { k: first === "h" ? "eh" : first === "m" ? "em" : "es", n } });
1444
+ }
1445
+ }
1446
+ function tokenize(src) {
1447
+ const cs = Array.from(src);
1448
+ const raw = { toks: [], currency: false };
1449
+ const toks = raw.toks;
1450
+ const lastClose = cs.lastIndexOf("]");
1451
+ let i = 0;
1452
+ while (i < cs.length) {
1453
+ const c = cs[i];
1454
+ if (c === '"') {
1455
+ let s = "";
1456
+ i += 1;
1457
+ while (i < cs.length) {
1458
+ const d = cs[i];
1459
+ i += 1;
1460
+ if (d === '"') break;
1461
+ s += d;
1462
+ }
1463
+ if (Array.from(s).some(isCurrencyChar)) raw.currency = true;
1464
+ pushLit(toks, s);
1465
+ continue;
1466
+ }
1467
+ if (c === "\\") {
1468
+ const d = cs[i + 1];
1469
+ if (d !== void 0) {
1470
+ if (isCurrencyChar(d)) raw.currency = true;
1471
+ pushLit(toks, d);
1472
+ }
1473
+ i += 2;
1474
+ continue;
1475
+ }
1476
+ if (c === "_") {
1477
+ if (cs[i + 1] !== void 0) pushLit(toks, " ");
1478
+ i += 2;
1479
+ continue;
1480
+ }
1481
+ if (c === "*") {
1482
+ const d = cs[i + 1];
1483
+ if (d !== void 0) toks.push({ t: "fill", c: d });
1484
+ i += 2;
1077
1485
  continue;
1078
1486
  }
1079
- for (let i = 0; i < token.text.length; i++) {
1080
- const char = token.text[i];
1081
- const isExponent = /e/i.test(char) && /[+-]/.test(token.text[i + 1] ?? "");
1082
- const isExponentSign = /[+-]/.test(char) && /e$/i.test(pattern);
1083
- if (/[0#?.,]/.test(char) || pattern && (isExponent || isExponentSign)) {
1084
- pattern += char;
1085
- } else if (pattern) {
1086
- suffix += char;
1487
+ if (c === "[") {
1488
+ const close = lastClose > i ? cs.indexOf("]", i + 1) : -1;
1489
+ if (close !== -1) {
1490
+ bracket(raw, cs.slice(i + 1, close).join(""));
1491
+ i = close + 1;
1087
1492
  } else {
1088
- prefix += char;
1089
- }
1090
- }
1091
- }
1092
- const percents = (prefix + suffix).split("%").length - 1;
1093
- value = Math.abs(value) * Math.pow(100, percents);
1094
- if (!pattern) return `${sign}${prefix}${suffix}`;
1095
- const expMatch = /^([^Ee]*)[Ee]([+-])(0+)$/.exec(pattern);
1096
- if (expMatch) {
1097
- const [, mantissa, expSign, expDigits] = expMatch;
1098
- const decimals = (mantissa.split(".")[1] ?? "").length;
1099
- const [m, e] = value.toExponential(decimals).split("e");
1100
- const exponent = Number(e);
1101
- const expText = String(Math.abs(exponent)).padStart(expDigits.length, "0");
1102
- const shownSign = exponent < 0 ? "-" : expSign === "+" ? "+" : "";
1103
- return `${sign}${prefix}${m}E${shownSign}${expText}${suffix}`;
1104
- }
1105
- const [intPattern, decPattern = ""] = pattern.split(".");
1106
- const maxDecimals = (decPattern.match(/[0#?]/g) ?? []).length;
1107
- const minDecimals = (decPattern.match(/0/g) ?? []).length;
1108
- const minInt = (intPattern.match(/0/g) ?? []).length;
1109
- const grouping = intPattern.includes(",");
1110
- const rounded = roundHalfAwayFromZero(value, maxDecimals);
1111
- let [intPart, decPart = ""] = rounded.toFixed(maxDecimals).split(".");
1112
- while (decPart.length > minDecimals && decPart.endsWith("0")) decPart = decPart.slice(0, -1);
1113
- if (intPart === "0" && minInt === 0) intPart = "";
1114
- intPart = intPart.padStart(minInt, "0");
1115
- if (grouping) intPart = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
1116
- const showSign = sign && /[1-9]/.test(intPart + decPart) ? sign : "";
1117
- return `${showSign}${prefix}${intPart}${decPart ? `.${decPart}` : ""}${suffix}`;
1118
- }
1119
- function splitSections(format) {
1120
- const sections = [""];
1121
- let quoted = false;
1122
- for (let i = 0; i < format.length; i++) {
1123
- const char = format[i];
1124
- if (char === '"') quoted = !quoted;
1125
- if (char === "\\" && i + 1 < format.length) {
1126
- sections[sections.length - 1] += char + format[++i];
1493
+ pushLit(toks, "[");
1494
+ i += 1;
1495
+ }
1496
+ continue;
1497
+ }
1498
+ if (c === "0" || c === "#" || c === "?") toks.push({ t: "digit", c });
1499
+ else if (c === ".") toks.push({ t: "point" });
1500
+ else if (c === ",") toks.push({ t: "comma" });
1501
+ else if (c === "%") toks.push({ t: "percent" });
1502
+ else if (c === "@") toks.push({ t: "at" });
1503
+ else if (c === "/") {
1504
+ toks.push({ t: "slash" });
1505
+ i += 1;
1506
+ if (cs[i] !== void 0 && cs[i] >= "1" && cs[i] <= "9") {
1507
+ let s = "";
1508
+ while (i < cs.length && cs[i] >= "0" && cs[i] <= "9") {
1509
+ s += cs[i];
1510
+ i += 1;
1511
+ }
1512
+ toks.push({ t: "denom", s });
1513
+ }
1514
+ continue;
1515
+ } else if ((c === "e" || c === "E") && (cs[i + 1] === "+" || cs[i + 1] === "-")) {
1516
+ toks.push({ t: "exp", upper: c === "E", plus: cs[i + 1] === "+" });
1517
+ i += 2;
1518
+ continue;
1519
+ } else if ((c === "G" || c === "g") && startsWithCi(cs, i, "general")) {
1520
+ toks.push({ t: "general" });
1521
+ i += 7;
1522
+ continue;
1523
+ } else if ((c === "A" || c === "a") && startsWithCi(cs, i, "am/pm")) {
1524
+ toks.push({ t: "date", d: { k: "ampm" } });
1525
+ i += 5;
1127
1526
  continue;
1527
+ } else if ((c === "A" || c === "a") && startsWithCi(cs, i, "a/p")) {
1528
+ toks.push({ t: "date", d: { k: "ap", a: c, p: cs[i + 2] ?? "P" } });
1529
+ i += 3;
1530
+ continue;
1531
+ } else if ("yYmMdDhHsSeE".includes(c)) {
1532
+ const lc = lowerAscii(c);
1533
+ let n = 0;
1534
+ while (cs[i + n] !== void 0 && lowerAscii(cs[i + n]) === lc) n += 1;
1535
+ const n8 = Math.min(n, 255);
1536
+ const d = lc === "y" ? { k: "year", n: n8 } : lc === "e" ? { k: "year", n: 4 } : lc === "m" ? { k: "month", n: n8 } : lc === "d" ? { k: "day", n: n8 } : lc === "h" ? { k: "hour", n: n8 } : { k: "second", n: n8 };
1537
+ toks.push({ t: "date", d });
1538
+ i += Math.max(n, 1);
1539
+ continue;
1540
+ } else {
1541
+ if (isCurrencyChar(c)) raw.currency = true;
1542
+ pushLit(toks, c);
1128
1543
  }
1129
- if (char === ";" && !quoted) sections.push("");
1130
- else sections[sections.length - 1] += char;
1544
+ i += 1;
1131
1545
  }
1132
- return sections;
1546
+ return raw;
1133
1547
  }
1548
+ var datePartOf = (t) => t?.t === "date" ? t.d : void 0;
1549
+ function analyze(raw) {
1550
+ const toks = raw.toks;
1551
+ const isDate = toks.some((t) => t.t === "date");
1552
+ const hasGeneral = toks.some((t) => t.t === "general");
1553
+ const sec = {
1554
+ toks: [],
1555
+ color: raw.color,
1556
+ cond: raw.cond,
1557
+ kind: "number",
1558
+ thousands: false,
1559
+ scale: 0,
1560
+ percent: 0,
1561
+ hasAt: toks.some((t) => t.t === "at"),
1562
+ hasDigits: false,
1563
+ exp: false,
1564
+ fraction: false,
1565
+ currency: raw.currency,
1566
+ fill: toks.some((t) => t.t === "fill")
1567
+ };
1568
+ if (isDate) {
1569
+ sec.kind = "date";
1570
+ const prevPart = [];
1571
+ let last;
1572
+ for (let i2 = 0; i2 < toks.length; i2++) {
1573
+ prevPart.push(last);
1574
+ const d = datePartOf(toks[i2]);
1575
+ if (d) last = d;
1576
+ }
1577
+ const nextPart = new Array(toks.length);
1578
+ let next;
1579
+ for (let i2 = toks.length - 1; i2 >= 0; i2--) {
1580
+ nextPart[i2] = next;
1581
+ const d = datePartOf(toks[i2]);
1582
+ if (d) next = d;
1583
+ }
1584
+ for (let i2 = 0; i2 < toks.length; i2++) {
1585
+ const t = toks[i2];
1586
+ if (t.t !== "date" || t.d.k !== "month" || t.d.n > 2) continue;
1587
+ const prev = prevPart[i2];
1588
+ const nxt = nextPart[i2];
1589
+ const afterH = prev?.k === "hour" || prev?.k === "eh";
1590
+ const beforeS = nxt?.k === "second" || nxt?.k === "es";
1591
+ if (afterH || beforeS) toks[i2] = { t: "date", d: { k: "minute", n: t.d.n } };
1592
+ }
1593
+ const out2 = [];
1594
+ let i = 0;
1595
+ while (i < toks.length) {
1596
+ const t = toks[i];
1597
+ if (t.t === "point" && isDigitTok(toks[i + 1], "0")) {
1598
+ let n = 0;
1599
+ while (isDigitTok(toks[i + 1 + n], "0")) n += 1;
1600
+ out2.push({ t: "date", d: { k: "subsec", n: Math.min(n, 255) } });
1601
+ i += 1 + n;
1602
+ continue;
1603
+ }
1604
+ if (t.t === "date" || t.t === "fill") out2.push(t);
1605
+ else pushLit(out2, literalText(t));
1606
+ i += 1;
1607
+ }
1608
+ sec.toks = out2;
1609
+ return sec;
1610
+ }
1611
+ if (hasGeneral) {
1612
+ sec.kind = "general";
1613
+ const out2 = [];
1614
+ for (const t of toks) {
1615
+ if (t.t === "general" || t.t === "fill") out2.push(t);
1616
+ else pushLit(out2, literalText(t));
1617
+ }
1618
+ sec.toks = out2;
1619
+ return sec;
1620
+ }
1621
+ sec.hasDigits = toks.some((t) => t.t === "digit");
1622
+ if (sec.hasAt && !sec.hasDigits) {
1623
+ sec.kind = "text";
1624
+ const out2 = [];
1625
+ for (const t of toks) {
1626
+ if (t.t === "at" || t.t === "fill") out2.push(t);
1627
+ else pushLit(out2, literalText(t));
1628
+ }
1629
+ sec.toks = out2;
1630
+ return sec;
1631
+ }
1632
+ let seenPoint = false;
1633
+ let seenExp = false;
1634
+ let seenDigit = false;
1635
+ let fractionSlash = false;
1636
+ const digitAfterCommas = new Array(toks.length).fill(false);
1637
+ const firstNonLitAfter = new Array(toks.length);
1638
+ let after = false;
1639
+ let nonLit;
1640
+ for (let j = toks.length - 1; j >= 0; j--) {
1641
+ digitAfterCommas[j] = after;
1642
+ firstNonLitAfter[j] = nonLit;
1643
+ const t = toks[j];
1644
+ if (t.t !== "comma") after = t.t === "digit";
1645
+ if (t.t !== "lit") nonLit = t;
1646
+ }
1647
+ let lastExp = -1;
1648
+ for (let j = toks.length - 1; j >= 0; j--) {
1649
+ if (toks[j].t === "exp") {
1650
+ lastExp = j;
1651
+ break;
1652
+ }
1653
+ }
1654
+ const firstDigit = toks.findIndex((t) => t.t === "digit");
1655
+ const scientific = firstDigit !== -1 && toks.slice(firstDigit).some((t) => t.t === "exp");
1656
+ for (let i = 0; i < toks.length; i++) {
1657
+ const t = toks[i];
1658
+ let replace;
1659
+ switch (t.t) {
1660
+ case "digit":
1661
+ seenDigit = true;
1662
+ break;
1663
+ case "point":
1664
+ if (seenPoint || seenExp) replace = { t: "lit", s: "." };
1665
+ else seenPoint = true;
1666
+ break;
1667
+ case "percent":
1668
+ sec.percent += 1;
1669
+ break;
1670
+ case "exp":
1671
+ if (seenExp || !seenDigit) replace = { t: "lit", s: literalText(t) };
1672
+ else {
1673
+ seenExp = true;
1674
+ sec.exp = true;
1675
+ }
1676
+ break;
1677
+ case "comma": {
1678
+ const nextDigit = digitAfterCommas[i];
1679
+ if (seenDigit && nextDigit && !seenPoint && !seenExp) {
1680
+ sec.thousands = true;
1681
+ replace = { t: "lit", s: "" };
1682
+ } else if (seenDigit && scientific) {
1683
+ replace = { t: "lit", s: "" };
1684
+ } else if (seenDigit && !nextDigit) {
1685
+ sec.scale += 1;
1686
+ replace = { t: "lit", s: "" };
1687
+ } else {
1688
+ replace = { t: "lit", s: "," };
1689
+ }
1690
+ break;
1691
+ }
1692
+ case "slash": {
1693
+ const nextOk = toks[i + 1]?.t === "denom" || firstNonLitAfter[i]?.t === "digit";
1694
+ const hasExp = seenExp || lastExp > i;
1695
+ if (!fractionSlash && seenDigit && nextOk && !seenPoint && !hasExp) {
1696
+ fractionSlash = true;
1697
+ sec.fraction = true;
1698
+ } else {
1699
+ replace = { t: "lit", s: "/" };
1700
+ }
1701
+ break;
1702
+ }
1703
+ case "denom":
1704
+ if (!(toks[i - 1]?.t === "slash" && sec.fraction)) replace = { t: "lit", s: t.s };
1705
+ break;
1706
+ default:
1707
+ break;
1708
+ }
1709
+ if (replace) toks[i] = replace;
1710
+ }
1711
+ if (sec.fraction) {
1712
+ for (let i = 0; i < toks.length; i++) if (toks[i].t === "point") toks[i] = { t: "lit", s: "." };
1713
+ }
1714
+ const out = [];
1715
+ for (const t of toks) {
1716
+ if (t.t === "lit") pushLit(out, t.s);
1717
+ else out.push(t);
1718
+ }
1719
+ sec.toks = out;
1720
+ return sec;
1721
+ }
1722
+ function parseSections(code) {
1723
+ return splitSections(code).map((s) => analyze(tokenize(s)));
1724
+ }
1725
+
1726
+ // src/spreadsheet/numfmt/render.ts
1727
+ var MAX_DATE_SERIAL = 2958465;
1134
1728
  var MONTHS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
1135
- var DAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
1136
- var pad3 = (n) => String(n).padStart(2, "0");
1137
- function formatDate(date, format) {
1138
- const parts = [];
1139
- for (const token of tokenize(format)) {
1140
- if (token.literal) {
1141
- parts.push(token);
1729
+ var WEEKDAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
1730
+ var UNIX_1899_12_302 = -25569;
1731
+ function civilFromDays(z0) {
1732
+ const z = z0 + 719468;
1733
+ const era = Math.floor(z / 146097);
1734
+ const doe = z - era * 146097;
1735
+ const yoe = Math.floor((doe - Math.floor(doe / 1460) + Math.floor(doe / 36524) - Math.floor(doe / 146096)) / 365);
1736
+ const doy = doe - (365 * yoe + Math.floor(yoe / 4) - Math.floor(yoe / 100));
1737
+ const mp = Math.floor((5 * doy + 2) / 153);
1738
+ const d = doy - Math.floor((153 * mp + 2) / 5) + 1;
1739
+ const m = mp < 10 ? mp + 3 : mp - 9;
1740
+ return { y: yoe + era * 400 + (m <= 2 ? 1 : 0), m, d };
1741
+ }
1742
+ function dateFromSerial(day) {
1743
+ const weekday = ((day + 6) % 7 + 7) % 7;
1744
+ if (day === 60) return { year: 1900, month: 2, day: 29, weekday };
1745
+ if (day === 0) return { year: 1900, month: 1, day: 0, weekday };
1746
+ const real = day < 60 ? day + 1 : day;
1747
+ const { y, m, d } = civilFromDays(real + UNIX_1899_12_302);
1748
+ return { year: y, month: m, day: d, weekday };
1749
+ }
1750
+ var Out = class {
1751
+ text = "";
1752
+ fill;
1753
+ /** True when every digit shown is zero (a number section that rounded to zero). */
1754
+ zero = false;
1755
+ push(s) {
1756
+ this.text += s;
1757
+ }
1758
+ setFill(c) {
1759
+ if (!this.fill) this.fill = { char: c, index: this.text.length };
1760
+ }
1761
+ prepend(s) {
1762
+ this.text = s + this.text;
1763
+ if (this.fill) this.fill.index += s.length;
1764
+ }
1765
+ };
1766
+ function renderText(sec, text) {
1767
+ const out = new Out();
1768
+ for (const t of sec.toks) {
1769
+ if (t.t === "at") out.push(text);
1770
+ else if (t.t === "fill") out.setFill(t.c);
1771
+ else out.push(literalText(t));
1772
+ }
1773
+ return out;
1774
+ }
1775
+ function renderGeneral(sec, v, width) {
1776
+ const out = new Out();
1777
+ for (const t of sec.toks) {
1778
+ if (t.t === "general") out.push(formatGeneralFit(v, width) ?? "#".repeat(Math.min(Math.max(width, 1), 11)));
1779
+ else if (t.t === "fill") out.setFill(t.c);
1780
+ else out.push(literalText(t));
1781
+ }
1782
+ return out;
1783
+ }
1784
+ var padNum = (n, w) => w >= 2 ? String(n).padStart(Math.min(w, 20), "0") : String(n);
1785
+ function renderDate(sec, v) {
1786
+ if (!Number.isFinite(v) || v < 0 || v >= MAX_DATE_SERIAL + 1) return null;
1787
+ let subDigits = 0;
1788
+ for (const t of sec.toks) if (t.t === "date" && t.d.k === "subsec") subDigits = Math.max(subDigits, t.d.n);
1789
+ subDigits = Math.min(subDigits, 3);
1790
+ const unit = 10 ** (3 - subDigits);
1791
+ let day = Math.floor(v);
1792
+ let ms = Math.round((v - Math.floor(v)) * 864e5 / unit) * unit;
1793
+ if (ms >= 864e5) {
1794
+ ms -= 864e5;
1795
+ day += 1;
1796
+ }
1797
+ const totalMs = day * 864e5 + ms;
1798
+ const needsDate = sec.toks.some((t) => t.t === "date" && isCalendarPart(t.d));
1799
+ if (needsDate && day > MAX_DATE_SERIAL) return null;
1800
+ const dt = needsDate ? dateFromSerial(day) : dateFromSerial(0);
1801
+ const ampm = sec.toks.some((t) => t.t === "date" && (t.d.k === "ampm" || t.d.k === "ap"));
1802
+ const hour = Math.floor(ms / 36e5);
1803
+ const minute = Math.floor(ms / 6e4) % 60;
1804
+ const second = Math.floor(ms / 1e3) % 60;
1805
+ const milli = ms % 1e3;
1806
+ const monthName = MONTHS[dt.month - 1] ?? "";
1807
+ const dayName = WEEKDAYS[dt.weekday] ?? "";
1808
+ const out = new Out();
1809
+ for (const t of sec.toks) {
1810
+ if (t.t === "fill") {
1811
+ out.setFill(t.c);
1812
+ continue;
1813
+ }
1814
+ if (t.t !== "date") {
1815
+ out.push(literalText(t));
1142
1816
  continue;
1143
1817
  }
1144
- const re = /AM\/PM|am\/pm|A\/P|a\/p|y+|m+|d+|h+|s+|[^ymdhsaA]+|./gi;
1145
- for (const match of token.text.match(re) ?? []) {
1146
- const isCode = /^(AM\/PM|A\/P|y+|m+|d+|h+|s+)$/i.test(match);
1147
- parts.push({ literal: !isCode, text: match });
1148
- }
1149
- }
1150
- const codes = parts.map((p, i) => ({ ...p, i })).filter((p) => !p.literal);
1151
- const twelveHour = codes.some((p) => /^(AM\/PM|A\/P)$/i.test(p.text));
1152
- return parts.map((part, index) => {
1153
- if (part.literal) return part.text;
1154
- const code = part.text;
1155
- const lower = code.toLowerCase();
1156
- if (/^(am\/pm|a\/p)$/.test(lower)) {
1157
- const pm = date.getHours() >= 12;
1158
- const full = pm ? "PM" : "AM";
1159
- const text = lower === "a/p" ? full[0] : full;
1160
- return code[0] === code[0].toLowerCase() ? text.toLowerCase() : text;
1161
- }
1162
- if (lower.startsWith("y")) {
1163
- return lower.length <= 2 ? pad3(date.getFullYear() % 100) : String(date.getFullYear());
1164
- }
1165
- if (lower.startsWith("d")) {
1166
- if (lower.length >= 4) return DAYS[date.getDay()];
1167
- if (lower.length === 3) return DAYS[date.getDay()].slice(0, 3);
1168
- return lower.length === 2 ? pad3(date.getDate()) : String(date.getDate());
1169
- }
1170
- if (lower.startsWith("h")) {
1171
- let hours = date.getHours();
1172
- if (twelveHour) hours = hours % 12 || 12;
1173
- return lower.length >= 2 ? pad3(hours) : String(hours);
1174
- }
1175
- if (lower.startsWith("s")) {
1176
- return lower.length >= 2 ? pad3(date.getSeconds()) : String(date.getSeconds());
1177
- }
1178
- const position = codes.findIndex((c) => c.i === index);
1179
- const prev = codes[position - 1]?.text.toLowerCase() ?? "";
1180
- const next = codes[position + 1]?.text.toLowerCase() ?? "";
1181
- if (lower.length <= 2 && (prev.startsWith("h") || next.startsWith("s"))) {
1182
- return lower.length === 2 ? pad3(date.getMinutes()) : String(date.getMinutes());
1183
- }
1184
- if (lower.length >= 4) return MONTHS[date.getMonth()];
1185
- if (lower.length === 3) return MONTHS[date.getMonth()].slice(0, 3);
1186
- return lower.length === 2 ? pad3(date.getMonth() + 1) : String(date.getMonth() + 1);
1187
- }).join("");
1818
+ const d = t.d;
1819
+ let s;
1820
+ switch (d.k) {
1821
+ case "year":
1822
+ s = d.n <= 2 ? String((dt.year % 100 + 100) % 100).padStart(2, "0") : String(dt.year).padStart(4, "0");
1823
+ break;
1824
+ case "month":
1825
+ s = d.n <= 2 ? padNum(dt.month, d.n) : d.n === 3 ? monthName.slice(0, 3) : d.n === 5 ? monthName.slice(0, 1) : monthName;
1826
+ break;
1827
+ case "day":
1828
+ s = d.n <= 2 ? padNum(dt.day, d.n) : d.n === 3 ? dayName.slice(0, 3) : dayName;
1829
+ break;
1830
+ case "hour":
1831
+ s = padNum(ampm ? hour % 12 || 12 : hour, d.n);
1832
+ break;
1833
+ case "minute":
1834
+ s = padNum(minute, d.n);
1835
+ break;
1836
+ case "second":
1837
+ s = padNum(second, d.n);
1838
+ break;
1839
+ case "subsec": {
1840
+ s = `.${String(milli).padStart(3, "0")}`.slice(0, 1 + Math.min(d.n, 3));
1841
+ if (d.n > 3) s += "0".repeat(d.n - 3);
1842
+ break;
1843
+ }
1844
+ case "eh":
1845
+ s = padNum(Math.floor(totalMs / 36e5), d.n);
1846
+ break;
1847
+ case "em":
1848
+ s = padNum(Math.floor(totalMs / 6e4), d.n);
1849
+ break;
1850
+ case "es":
1851
+ s = padNum(Math.floor(totalMs / 1e3), d.n);
1852
+ break;
1853
+ case "ampm":
1854
+ s = hour >= 12 ? "PM" : "AM";
1855
+ break;
1856
+ case "ap":
1857
+ s = hour >= 12 ? d.p : d.a;
1858
+ break;
1859
+ }
1860
+ out.push(s);
1861
+ }
1862
+ return out;
1863
+ }
1864
+ var digitChar = (toks, i) => {
1865
+ const t = toks[i];
1866
+ return t?.t === "digit" ? t.c : "0";
1867
+ };
1868
+ function fillInt(toks, idxs, digits, thousands, forceZero, pieces) {
1869
+ let rem = digits.length;
1870
+ let pos = 0;
1871
+ const count = idxs.length;
1872
+ for (let k = 0; k < count; k++) {
1873
+ const ti = idxs[count - 1 - k];
1874
+ const leftmost = k + 1 === count;
1875
+ const ph = digitChar(toks, ti);
1876
+ const rev = [];
1877
+ const pushDigit = (c) => {
1878
+ if (thousands && pos > 0 && pos % 3 === 0) rev.push(",");
1879
+ rev.push(c);
1880
+ pos += 1;
1881
+ };
1882
+ const take = leftmost ? rem : Math.min(rem, 1);
1883
+ if (take === 0) {
1884
+ if (ph === "0") pushDigit("0");
1885
+ else if (forceZero && k === 0) pushDigit("0");
1886
+ else if (ph === "?") {
1887
+ if (thousands && pos > 0 && pos % 3 === 0) rev.push(" ");
1888
+ rev.push(" ");
1889
+ pos += 1;
1890
+ }
1891
+ } else {
1892
+ for (let n = 0; n < take; n++) {
1893
+ rem -= 1;
1894
+ pushDigit(digits[rem] ?? "0");
1895
+ }
1896
+ }
1897
+ pieces[ti] = rev.reverse().join("");
1898
+ }
1899
+ }
1900
+ function withCommas(digits, thousands) {
1901
+ if (!thousands) return digits;
1902
+ const n = digits.length;
1903
+ let s = "";
1904
+ for (let i = 0; i < n; i++) {
1905
+ if (i > 0 && (n - i) % 3 === 0) s += ",";
1906
+ s += digits[i];
1907
+ }
1908
+ return s;
1909
+ }
1910
+ function fillFrac(toks, idxs, frac, pieces) {
1911
+ let lastSig = -1;
1912
+ for (let i = frac.length - 1; i >= 0; i--) {
1913
+ if (frac[i] !== "0") {
1914
+ lastSig = i;
1915
+ break;
1916
+ }
1917
+ }
1918
+ idxs.forEach((ti, i) => {
1919
+ const ph = digitChar(toks, ti);
1920
+ pieces[ti] = i <= lastSig ? frac[i] ?? "0" : ph === "0" ? "0" : ph === "?" ? " " : "";
1921
+ });
1922
+ }
1923
+ function digitIndices(toks, start, end) {
1924
+ const out = [];
1925
+ for (let i = start; i < end; i++) if (toks[i]?.t === "digit") out.push(i);
1926
+ return out;
1927
+ }
1928
+ function renderNumber(sec, v) {
1929
+ let x = Math.abs(v);
1930
+ if (sec.percent > 0 && !sec.exp) x *= 100 ** Math.min(sec.percent, 20);
1931
+ if (sec.scale > 0) x /= 1e3 ** Math.min(sec.scale, 110);
1932
+ if (!Number.isFinite(x)) x = Number.MAX_VALUE;
1933
+ const toks = sec.toks;
1934
+ const n = toks.length;
1935
+ const pieces = new Array(n).fill("");
1936
+ const before = new Array(n + 1).fill("");
1937
+ const lits = new Array(n);
1938
+ let zero = false;
1939
+ if (sec.fraction) {
1940
+ renderFraction(sec, x, pieces, lits);
1941
+ } else {
1942
+ const pointIdx = toks.findIndex((t) => t.t === "point");
1943
+ const expIdx = toks.findIndex((t) => t.t === "exp");
1944
+ const point = pointIdx === -1 ? void 0 : pointIdx;
1945
+ const exp = expIdx === -1 ? void 0 : expIdx;
1946
+ const intEnd = point ?? exp ?? n;
1947
+ const fracEnd = exp ?? n;
1948
+ const intIdx = digitIndices(toks, 0, intEnd);
1949
+ const fracIdx = point === void 0 ? [] : digitIndices(toks, point + 1, fracEnd);
1950
+ let intS;
1951
+ let fracS;
1952
+ if (exp !== void 0) {
1953
+ const expDigitIdx = digitIndices(toks, exp + 1, n);
1954
+ const step = Math.max(intIdx.length, 1);
1955
+ let e;
1956
+ if (x === 0) {
1957
+ e = 0;
1958
+ intS = "0".repeat(intIdx.length);
1959
+ fracS = "0".repeat(fracIdx.length);
1960
+ } else {
1961
+ const { digits, exp: xe } = decimalDigits(x);
1962
+ e = xe - (xe % step + step) % step;
1963
+ [intS, fracS] = fixedFromDigits(digits, xe - e, fracIdx.length);
1964
+ if (intS.length > step) {
1965
+ e += step;
1966
+ [intS, fracS] = fixedFromDigits(digits, xe - e, fracIdx.length);
1967
+ }
1968
+ }
1969
+ const et = toks[exp];
1970
+ const upper = et.t === "exp" ? et.upper : true;
1971
+ const plus = et.t === "exp" ? et.plus : true;
1972
+ if (intS === "" && /^0*$/.test(fracS)) e = 0;
1973
+ const head = upper ? "E" : "e";
1974
+ const sign = e < 0 ? "-" : plus ? "+" : "";
1975
+ const expDigits = String(Math.abs(e));
1976
+ if (expDigitIdx.length > 0) {
1977
+ pieces[exp] = head;
1978
+ before[expDigitIdx[0]] = sign;
1979
+ fillInt(toks, expDigitIdx, expDigits, false, false, pieces);
1980
+ } else {
1981
+ pieces[exp] = `${head}${sign}${expDigits}`;
1982
+ }
1983
+ } else {
1984
+ [intS, fracS] = fixed(x, fracIdx.length);
1985
+ }
1986
+ zero = /^0*$/.test(intS + fracS);
1987
+ fillInt(toks, intIdx, intS, sec.thousands, false, pieces);
1988
+ const anchor = point ?? exp;
1989
+ if (intIdx.length === 0 && intS !== "" && anchor !== void 0) before[anchor] = withCommas(intS, sec.thousands);
1990
+ fillFrac(toks, fracIdx, fracS, pieces);
1991
+ if (point !== void 0) pieces[point] = ".";
1992
+ }
1993
+ const out = new Out();
1994
+ for (let i = 0; i < n; i++) {
1995
+ out.push(before[i]);
1996
+ const t = toks[i];
1997
+ if (t.t === "lit") out.push(lits[i] ?? t.s);
1998
+ else if (t.t === "percent") out.push("%");
1999
+ else if (t.t === "fill") out.setFill(t.c);
2000
+ else if (t.t === "at" || t.t === "general" || t.t === "date" || t.t === "comma") continue;
2001
+ else out.push(pieces[i]);
2002
+ }
2003
+ out.zero = zero;
2004
+ return out;
2005
+ }
2006
+ function approximate(x, maxDen) {
2007
+ if (Number.isNaN(x) || x <= 0 || !Number.isFinite(x)) return [0, 1];
2008
+ if (x >= 1e15) return [Math.round(x), 1];
2009
+ let [p0, q0, p1, q1] = [0, 1, 1, 0];
2010
+ let r = x;
2011
+ for (let iter = 0; iter < 64; iter++) {
2012
+ const aF = Math.floor(r);
2013
+ if (aF > 1e15) break;
2014
+ const q2 = aF * q1 + q0;
2015
+ if (q2 > maxDen) break;
2016
+ const p2 = aF * p1 + p0;
2017
+ [p0, q0, p1, q1] = [p1, q1, p2, q2];
2018
+ const frac = r - aF;
2019
+ if (frac < 1e-12) break;
2020
+ r = 1 / frac;
2021
+ }
2022
+ if (q1 === 0) return [Math.round(x), 1];
2023
+ const k = Math.floor((maxDen - q0) / q1);
2024
+ const pb = p0 + k * p1;
2025
+ const qb = q0 + k * q1;
2026
+ const errA = Math.abs(p1 / q1 - x);
2027
+ const errB = qb > 0 ? Math.abs(pb / qb - x) : Infinity;
2028
+ return errB < errA ? [pb, qb] : [p1, q1];
2029
+ }
2030
+ function shownWidth(t) {
2031
+ if (t.t === "lit" || t.t === "denom") return Array.from(t.s).length;
2032
+ return t.t === "digit" || t.t === "slash" ? 1 : 0;
2033
+ }
2034
+ function blankRange(toks, start, end, spaces, pieces, lits) {
2035
+ for (let i = start; i < end && i < toks.length; i++) {
2036
+ const t = toks[i];
2037
+ const s = spaces ? " ".repeat(shownWidth(t)) : "";
2038
+ if (t.t === "lit") lits[i] = s;
2039
+ else if (t.t === "digit" || t.t === "slash" || t.t === "denom") pieces[i] = s;
2040
+ }
2041
+ }
2042
+ function renderFraction(sec, x, pieces, lits) {
2043
+ const toks = sec.toks;
2044
+ const isDigit = (i) => toks[i]?.t === "digit";
2045
+ const slash = toks.findIndex((t) => t.t === "slash");
2046
+ if (slash === -1) return;
2047
+ let numLast = -1;
2048
+ for (let i = slash - 1; i >= 0; i--) {
2049
+ if (isDigit(i)) {
2050
+ numLast = i;
2051
+ break;
2052
+ }
2053
+ }
2054
+ if (numLast === -1) return;
2055
+ let numStart = numLast;
2056
+ while (numStart > 0 && isDigit(numStart - 1)) numStart -= 1;
2057
+ const wholeIdx = digitIndices(toks, 0, numStart);
2058
+ const numIdx = digitIndices(toks, numStart, numLast + 1);
2059
+ const sepStart = wholeIdx.length > 0 ? wholeIdx[wholeIdx.length - 1] + 1 : numStart;
2060
+ let fixedDen;
2061
+ let denStart;
2062
+ const afterSlash = toks[slash + 1];
2063
+ if (afterSlash?.t === "denom") {
2064
+ const parsed = Number(afterSlash.s);
2065
+ fixedDen = Math.min(Math.max(Number.isFinite(parsed) ? parsed : 1e9, 1), 1e9);
2066
+ denStart = slash + 1;
2067
+ } else {
2068
+ denStart = slash + 1;
2069
+ while (toks[denStart]?.t === "lit") denStart += 1;
2070
+ }
2071
+ let denEnd = denStart;
2072
+ if (fixedDen !== void 0) denEnd += 1;
2073
+ else while (isDigit(denEnd)) denEnd += 1;
2074
+ const denIdx = digitIndices(toks, denStart, denEnd);
2075
+ const hasQ = (idx) => idx.some((i) => isDigitTok2(toks[i], "?"));
2076
+ const hasWhole = wholeIdx.length > 0;
2077
+ let whole = hasWhole ? Math.floor(x) : 0;
2078
+ const f = hasWhole ? x - Math.floor(x) : x;
2079
+ let num;
2080
+ let den;
2081
+ if (fixedDen !== void 0) {
2082
+ num = Math.round(f * fixedDen);
2083
+ den = fixedDen;
2084
+ } else {
2085
+ const digits = Math.min(Math.max(denIdx.length, 1), 7);
2086
+ [num, den] = approximate(f, 10 ** digits - 1);
2087
+ }
2088
+ if (hasWhole && num >= den && den > 0) {
2089
+ whole += Math.floor(num / den);
2090
+ num %= den;
2091
+ }
2092
+ const wholeS = whole >= 1 ? fixed(whole, 0)[0] : "";
2093
+ const numZeroPh = numIdx.some((i) => isDigitTok2(toks[i], "0"));
2094
+ if (hasWhole && num === 0 && !numZeroPh) {
2095
+ fillInt(toks, wholeIdx, wholeS, sec.thousands, true, pieces);
2096
+ const spaces = hasQ(wholeIdx) || hasQ(numIdx) || hasQ(denIdx);
2097
+ blankRange(toks, sepStart, denEnd, spaces, pieces, lits);
2098
+ return;
2099
+ }
2100
+ if (hasWhole) {
2101
+ const unitsHash = isDigitTok2(toks[wholeIdx[wholeIdx.length - 1]], "#");
2102
+ const force = wholeS === "" && num === 0 && !unitsHash;
2103
+ fillInt(toks, wholeIdx, wholeS, sec.thousands, force, pieces);
2104
+ const wholeShown = wholeIdx.some((i) => /[0-9]/.test(pieces[i] ?? ""));
2105
+ if (!wholeShown) {
2106
+ const spaces = hasQ(wholeIdx) || hasQ(numIdx);
2107
+ blankRange(toks, sepStart, numStart, spaces, pieces, lits);
2108
+ }
2109
+ }
2110
+ fillInt(toks, numIdx, fixed(num, 0)[0] || "0", false, true, pieces);
2111
+ pieces[slash] = "/";
2112
+ const denS = fixed(den, 0)[0] || "0";
2113
+ if (fixedDen !== void 0) {
2114
+ pieces[denStart] = denS;
2115
+ } else {
2116
+ const zeros = denIdx.filter((i) => isDigitTok2(toks[i], "0")).length;
2117
+ const padded = "0".repeat(Math.max(zeros - denS.length, 0)) + denS;
2118
+ const count = denIdx.length;
2119
+ denIdx.forEach((ti, k) => {
2120
+ let s = k + 1 === count ? padded.slice(k) : padded[k] ?? "";
2121
+ if (s === "") s = isDigitTok2(toks[ti], "?") ? " " : isDigitTok2(toks[ti], "0") ? "0" : "";
2122
+ pieces[ti] = s;
2123
+ });
2124
+ }
2125
+ }
2126
+ function isDigitTok2(t, c) {
2127
+ return t?.t === "digit" && t.c === c;
2128
+ }
2129
+
2130
+ // src/spreadsheet/numfmt/index.ts
2131
+ var GENERAL_WIDTH = 11;
2132
+ var OVERFLOW = "########";
2133
+ var MAX_NUMBER_FORMAT_CODE_CHARS = 255;
2134
+ function tooLong(code) {
2135
+ return code.length > MAX_NUMBER_FORMAT_CODE_CHARS && Array.from(code).length > MAX_NUMBER_FORMAT_CODE_CHARS;
2136
+ }
2137
+ var SPECIAL_CODES = ["00000", "00000-0000", "[<=9999999]###-####;(###) ###-####", "000-00-0000"];
2138
+ var NumberFormat = class _NumberFormat {
2139
+ code;
2140
+ general;
2141
+ sections;
2142
+ /** Sections that apply to numbers (a trailing text section is excluded). */
2143
+ numSections;
2144
+ /** Index of the section that applies to text, if any. */
2145
+ textSection;
2146
+ constructor(code, uncapped) {
2147
+ this.code = code;
2148
+ const trimmed = code.trim();
2149
+ if (trimmed === "" || trimmed.toLowerCase() === "general" || trimmed === "checkbox" || !uncapped && tooLong(code)) {
2150
+ this.general = true;
2151
+ this.sections = [];
2152
+ this.numSections = 0;
2153
+ this.textSection = void 0;
2154
+ return;
2155
+ }
2156
+ this.general = false;
2157
+ this.sections = parseSections(code);
2158
+ const len = this.sections.length;
2159
+ const lastIsText = this.sections[len - 1]?.kind === "text";
2160
+ this.textSection = len >= 4 ? 3 : lastIsText ? len - 1 : void 0;
2161
+ this.numSections = len >= 4 ? 3 : lastIsText ? len - 1 : len;
2162
+ }
2163
+ /**
2164
+ * Parses a format code. Never throws: unknown or malformed parts degrade to
2165
+ * literal text, and a code longer than 255 characters degrades to General.
2166
+ */
2167
+ static parse(code) {
2168
+ return new _NumberFormat(code, false);
2169
+ }
2170
+ /** `parse` without the 255-character cap (for tests that reach renderer edge cases). */
2171
+ static parseUncapped(code) {
2172
+ return new _NumberFormat(code, true);
2173
+ }
2174
+ /** Like `parse`, but remembers recently parsed codes (bounded), so formatting many values parses a code once. */
2175
+ static parseCached(code) {
2176
+ if (code.length > CACHE_MAX_CODE_LENGTH) return _NumberFormat.parse(code);
2177
+ let fmt = parseCache.get(code);
2178
+ if (!fmt) {
2179
+ fmt = _NumberFormat.parse(code);
2180
+ if (parseCache.size >= CACHE_CAPACITY) parseCache.clear();
2181
+ parseCache.set(code, fmt);
2182
+ }
2183
+ return fmt;
2184
+ }
2185
+ static general() {
2186
+ return _NumberFormat.parse("General");
2187
+ }
2188
+ isGeneral() {
2189
+ return this.general;
2190
+ }
2191
+ numberSections() {
2192
+ return this.sections.slice(0, this.numSections);
2193
+ }
2194
+ /** True when the code contains date or time tokens. */
2195
+ isDate() {
2196
+ return this.numberSections().some((s) => s.kind === "date");
2197
+ }
2198
+ isPercent() {
2199
+ return this.numberSections().some((s) => s.kind === "number" && s.percent > 0);
2200
+ }
2201
+ /** True for a pure text format such as `@`. */
2202
+ isText() {
2203
+ return this.numSections === 0 && this.textSection !== void 0;
2204
+ }
2205
+ /** Best guess at the format category. */
2206
+ kind() {
2207
+ if (this.general) return "general";
2208
+ if (this.isText()) return "text";
2209
+ if (SPECIAL_CODES.includes(this.code)) return "special";
2210
+ const s = this.sections[0];
2211
+ if (!s) return "custom";
2212
+ if (s.kind === "general") return "general";
2213
+ if (s.kind === "text") return "text";
2214
+ if (s.kind === "date") {
2215
+ const calendar = s.toks.some((t) => t.t === "date" && (t.d.k === "year" || t.d.k === "month" || t.d.k === "day"));
2216
+ return calendar ? "date" : "time";
2217
+ }
2218
+ if (s.fraction) return "fraction";
2219
+ if (s.exp) return "scientific";
2220
+ if (s.percent > 0) return "percentage";
2221
+ if (s.fill && s.hasDigits) return "accounting";
2222
+ if (s.currency && s.hasDigits) return "currency";
2223
+ if (s.hasDigits) return "number";
2224
+ return "custom";
2225
+ }
2226
+ /**
2227
+ * Picks the number section for `v`, and whether the minus sign is dropped
2228
+ * (a negative section shows the absolute value). Null means General.
2229
+ */
2230
+ select(v) {
2231
+ const secs = this.numberSections();
2232
+ const k = secs.length;
2233
+ const s0 = secs[0];
2234
+ if (!s0) return null;
2235
+ const c0 = s0.cond;
2236
+ const c1 = secs[1]?.cond;
2237
+ if (c0 || c1) {
2238
+ let idx;
2239
+ if (c0 && testCond(c0, v)) idx = 0;
2240
+ else if (c1 && testCond(c1, v)) idx = 1;
2241
+ else if (c0 && c1) idx = 2;
2242
+ else if (!c0) idx = 0;
2243
+ else idx = 1;
2244
+ const sec = secs[idx];
2245
+ return sec ? { sec, dropSign: k >= 2 } : null;
2246
+ }
2247
+ if (v < 0 && k >= 2) return { sec: secs[1], dropSign: true };
2248
+ if (v === 0 && k >= 3) return { sec: secs[2], dropSign: false };
2249
+ return { sec: s0, dropSign: false };
2250
+ }
2251
+ /**
2252
+ * Formats a number. `overflow` is true when the value can't be shown (a
2253
+ * date out of range, painted as `########`).
2254
+ */
2255
+ formatNumber(n) {
2256
+ const general = () => ({
2257
+ text: formatGeneralFit(n, GENERAL_WIDTH) ?? OVERFLOW,
2258
+ numeric: true,
2259
+ overflow: false
2260
+ });
2261
+ if (!Number.isFinite(n)) return { text: "#NUM!", numeric: false, overflow: true };
2262
+ if (this.general) return general();
2263
+ const picked = this.select(n);
2264
+ if (!picked) return general();
2265
+ const { sec, dropSign } = picked;
2266
+ const minus = n < 0 && !dropSign;
2267
+ let out;
2268
+ switch (sec.kind) {
2269
+ case "text":
2270
+ out = renderText(sec, formatGeneralFit(n, GENERAL_WIDTH) ?? "");
2271
+ break;
2272
+ case "general":
2273
+ out = renderGeneral(sec, Math.abs(n), minus ? GENERAL_WIDTH - 1 : GENERAL_WIDTH);
2274
+ break;
2275
+ case "date":
2276
+ out = minus ? null : renderDate(sec, Math.abs(n));
2277
+ break;
2278
+ default:
2279
+ out = renderNumber(sec, Math.abs(n));
2280
+ }
2281
+ if (!out) return withColor({ text: OVERFLOW, numeric: true, overflow: true }, sec.color);
2282
+ if (minus && sec.kind !== "text" && !out.zero) out.prepend("-");
2283
+ return withColor({ text: out.text, numeric: true, overflow: false, ...out.fill ? { fill: out.fill } : {} }, sec.color);
2284
+ }
2285
+ /** Formats text through the text section (`@`), or returns it unchanged. */
2286
+ formatText(text) {
2287
+ const sec = this.textSection === void 0 ? void 0 : this.sections[this.textSection];
2288
+ if (!sec) return { text, numeric: false };
2289
+ const out = renderText(sec, text);
2290
+ return withColor({ text: out.text, numeric: false, ...out.fill ? { fill: out.fill } : {} }, sec.color);
2291
+ }
2292
+ /**
2293
+ * Formats a cell value: numbers through the number sections, ISO date text
2294
+ * (`2026-03-04`, `2026-03-04 18:00:00`) and `Date`s as serial numbers, other
2295
+ * text through the text section; booleans show TRUE/FALSE and error values
2296
+ * themselves.
2297
+ */
2298
+ format(value) {
2299
+ if (value === "" || value === null || value === void 0) return { text: "", numeric: false };
2300
+ if (typeof value === "boolean") return { text: value ? "TRUE" : "FALSE", numeric: false };
2301
+ if (typeof value === "number") {
2302
+ const { overflow: _overflow, ...rest } = this.formatNumber(value);
2303
+ return rest;
2304
+ }
2305
+ if (typeof value === "string" && FORMULA_ERRORS.has(value)) return { text: value, numeric: false };
2306
+ const serial = dateValueToSerial(value);
2307
+ if (serial !== null) {
2308
+ const { overflow: _overflow, ...rest } = this.formatNumber(serial);
2309
+ return rest;
2310
+ }
2311
+ return this.formatText(String(value));
2312
+ }
2313
+ };
2314
+ function withColor(value, color) {
2315
+ if (color) value.color = color;
2316
+ return value;
2317
+ }
2318
+ var CACHE_CAPACITY = 64;
2319
+ var CACHE_MAX_CODE_LENGTH = 256;
2320
+ var parseCache = /* @__PURE__ */ new Map();
2321
+ function clearNumberFormatCache() {
2322
+ parseCache.clear();
2323
+ }
2324
+ var ISO_DATE_TIME = /^(\d{4})-(\d{1,2})-(\d{1,2})(?:[ T](\d{1,2}):(\d{2})(?::(\d{2}(?:\.\d+)?))?)?$/;
2325
+ function dateValueToSerial(value) {
2326
+ if (value instanceof Date) {
2327
+ if (isNaN(value.getTime())) return null;
2328
+ const serial2 = serialFromYmd(value.getFullYear(), value.getMonth() + 1, value.getDate());
2329
+ return serial2 === null ? null : serial2 + timeFraction(value.getHours(), value.getMinutes(), value.getSeconds() + value.getMilliseconds() / 1e3);
2330
+ }
2331
+ if (typeof value !== "string" || value.length < 8 || value.length > 30) return null;
2332
+ const m = ISO_DATE_TIME.exec(value.trim());
2333
+ if (!m) return null;
2334
+ const [y, mo, d] = [Number(m[1]), Number(m[2]), Number(m[3])];
2335
+ if (mo < 1 || mo > 12 || d < 1 || d > daysInMonth(y, mo)) return null;
2336
+ const serial = serialFromYmd(y, mo, d);
2337
+ if (serial === null) return null;
2338
+ if (m[4] === void 0) return serial;
2339
+ const [hh, mm, ss] = [Number(m[4]), Number(m[5]), Number(m[6] ?? 0)];
2340
+ if (hh > 23 || mm > 59 || ss >= 60) return null;
2341
+ return serial + timeFraction(hh, mm, ss);
2342
+ }
2343
+ function formatNumberFormatValue(value, code) {
2344
+ return NumberFormat.parseCached(code).format(value);
2345
+ }
2346
+ function formatTextFunction(value, code) {
2347
+ if (typeof value === "string" && FORMULA_ERRORS.has(value)) return { error: value };
2348
+ if (code === "") return { text: "" };
2349
+ if (tooLong(code)) return { error: "#VALUE!" };
2350
+ const fmt = NumberFormat.parseCached(code);
2351
+ let n;
2352
+ if (value === "" || value === null || value === void 0) n = 0;
2353
+ else if (typeof value === "number") n = value;
2354
+ else if (typeof value === "boolean") return { text: fmt.formatText(value ? "TRUE" : "FALSE").text };
2355
+ else {
2356
+ const serial = dateValueToSerial(value);
2357
+ const parsed = serial ?? (typeof value === "string" ? parseNumberText(value) : null);
2358
+ if (parsed === null) return { text: fmt.formatText(String(value)).text };
2359
+ n = parsed;
2360
+ }
2361
+ if (!Number.isFinite(n)) return { error: "#NUM!" };
2362
+ const f = fmt.formatNumber(n);
2363
+ return f.overflow ? { error: "#VALUE!" } : { text: f.text };
2364
+ }
2365
+ var INDEXED_COLORS = [
2366
+ "#000000",
2367
+ "#FFFFFF",
2368
+ "#FF0000",
2369
+ "#00FF00",
2370
+ "#0000FF",
2371
+ "#FFFF00",
2372
+ "#FF00FF",
2373
+ "#00FFFF",
2374
+ "#800000",
2375
+ "#008000",
2376
+ "#000080",
2377
+ "#808000",
2378
+ "#800080",
2379
+ "#008080",
2380
+ "#C0C0C0",
2381
+ "#808080",
2382
+ "#9999FF",
2383
+ "#993366",
2384
+ "#FFFFCC",
2385
+ "#CCFFFF",
2386
+ "#660066",
2387
+ "#FF8080",
2388
+ "#0066CC",
2389
+ "#CCCCFF",
2390
+ "#000080",
2391
+ "#FF00FF",
2392
+ "#FFFF00",
2393
+ "#00FFFF",
2394
+ "#800080",
2395
+ "#800000",
2396
+ "#008080",
2397
+ "#0000FF",
2398
+ "#00CCFF",
2399
+ "#CCFFFF",
2400
+ "#CCFFCC",
2401
+ "#FFFF99",
2402
+ "#99CCFF",
2403
+ "#FF99CC",
2404
+ "#CC99FF",
2405
+ "#FFCC99",
2406
+ "#3366FF",
2407
+ "#33CCCC",
2408
+ "#99CC00",
2409
+ "#FFCC00",
2410
+ "#FF9900",
2411
+ "#FF6600",
2412
+ "#666699",
2413
+ "#969696",
2414
+ "#003366",
2415
+ "#339966",
2416
+ "#003300",
2417
+ "#333300",
2418
+ "#993300",
2419
+ "#993366",
2420
+ "#333399",
2421
+ "#333333"
2422
+ ];
2423
+ var NAMED_CSS = {
2424
+ black: "#000000",
2425
+ blue: "#0000FF",
2426
+ cyan: "#00FFFF",
2427
+ green: "#00FF00",
2428
+ magenta: "#FF00FF",
2429
+ red: "#FF0000",
2430
+ white: "#FFFFFF",
2431
+ yellow: "#FFFF00"
2432
+ };
2433
+ function numberFormatColorToCss(color) {
2434
+ const named = NAMED_CSS[color];
2435
+ if (named) return named;
2436
+ return INDEXED_COLORS[Number(color.slice(5)) - 1] ?? "#000000";
1188
2437
  }
1189
2438
 
1190
2439
  // src/spreadsheet/formula/functions/helpers.ts
@@ -2197,15 +3446,19 @@ var TEXT_FUNCTIONS = [
2197
3446
  {
2198
3447
  name: "TEXT",
2199
3448
  category: "text",
2200
- description: 'Formats a number or date as text using a format code such as "0.00", "#,##0", "0%" or "yyyy-mm-dd".',
3449
+ description: 'Formats a number or date as text using an Excel format code such as "0.00", "#,##0", "0%", "# ?/?" or "yyyy-mm-dd".',
2201
3450
  args: [arg("value", "The number or date to format."), arg("format_text", "The format code.", "text")],
2202
3451
  evaluate: (ctx) => {
2203
3452
  const value = ctx.scalar(0);
2204
3453
  if (isFormulaError(value)) return value;
2205
3454
  const format = ctx.text(1);
2206
3455
  if (failed(format)) return format;
2207
- const result = formatValueWithCode(value, format);
2208
- return result ?? ctx.argError(0, "#VALUE!", `isn't a date, so it can't use the format "${format}".`);
3456
+ const result = formatTextFunction(value, format);
3457
+ if ("text" in result) return result.text;
3458
+ if (result.error === "#VALUE!") {
3459
+ return Array.from(format).length > 255 ? ctx.argError(1, "#VALUE!", "is longer than 255 characters.") : ctx.argError(0, "#VALUE!", `is outside the date range, so it can't use the format "${format}".`);
3460
+ }
3461
+ return result.error;
2209
3462
  }
2210
3463
  }
2211
3464
  ];
@@ -2270,15 +3523,15 @@ function tableLookup(ctx, vertical) {
2270
3523
  if (failed(table)) return table;
2271
3524
  const index = ctx.integer(2);
2272
3525
  if (failed(index)) return index;
2273
- const approximate = ctx.boolean(3, true);
2274
- if (failed(approximate)) return approximate;
3526
+ const approximate2 = ctx.boolean(3, true);
3527
+ if (failed(approximate2)) return approximate2;
2275
3528
  const span = vertical ? table.cols : table.rows;
2276
3529
  if (index < 1) return ctx.argError(2, "#VALUE!", "must be 1 or more.");
2277
3530
  if (index > span) {
2278
3531
  return ctx.argError(2, "#REF!", `is larger than the number of ${vertical ? "columns" : "rows"} in the table (${span}).`);
2279
3532
  }
2280
3533
  const keys = vertical ? sliceRange(table, 0, table.rows, 0, 1) : sliceRange(table, 0, 1, 0, table.cols);
2281
- const position = lookupIndex(keys, target, approximate ? "sorted-asc" : "wildcard");
3534
+ const position = lookupIndex(keys, target, approximate2 ? "sorted-asc" : "wildcard");
2282
3535
  if (position === -1) return notFound(ctx, target);
2283
3536
  return vertical ? rangeGet(table, position, index - 1) : rangeGet(table, index - 1, position);
2284
3537
  }
@@ -2984,6 +4237,88 @@ var ELEMENTWISE_BUILTINS = {
2984
4237
  IFNA: [0]
2985
4238
  };
2986
4239
 
4240
+ // src/spreadsheet/formula/functions/lambda.ts
4241
+ var special = (name) => (ctx) => ctx.error("#ERROR!", `${name} must be evaluated by the formula evaluator.`);
4242
+ var LAMBDA_ARG = arg("lambda", "A LAMBDA function, e.g. LAMBDA(x, x*2), or a function name such as SUM.");
4243
+ var LAMBDA_FUNCTIONS = [
4244
+ {
4245
+ name: "LAMBDA",
4246
+ category: "logical",
4247
+ description: "Creates a reusable function: LAMBDA(x, y, x+y). Call it directly, LAMBDA(x, x+1)(2), store it in LET or a defined name, or pass it to MAP, REDUCE, SCAN, BYROW, BYCOL or MAKEARRAY.",
4248
+ args: [
4249
+ repeating(
4250
+ "parameter_or_calculation",
4251
+ "Parameter names, then the calculation that uses them (always the last argument)."
4252
+ )
4253
+ ],
4254
+ evaluate: special("LAMBDA")
4255
+ },
4256
+ {
4257
+ name: "ISOMITTED",
4258
+ category: "logical",
4259
+ description: "Checks whether a LAMBDA parameter was left out of the call, e.g. LAMBDA(x, y, IF(ISOMITTED(y), x, x+y))(1,).",
4260
+ args: [arg("argument", "A LAMBDA parameter name.")],
4261
+ evaluate: special("ISOMITTED")
4262
+ },
4263
+ {
4264
+ name: "MAP",
4265
+ category: "logical",
4266
+ description: "Applies a LAMBDA to each value of one or more arrays: MAP(A1:A3, LAMBDA(v, v*2)).",
4267
+ args: [
4268
+ arg("array1", "An array to map."),
4269
+ repeating("array_or_lambda", "More arrays, then the LAMBDA taking one parameter per array.")
4270
+ ],
4271
+ evaluate: special("MAP")
4272
+ },
4273
+ {
4274
+ name: "REDUCE",
4275
+ category: "logical",
4276
+ description: "Reduces an array to one value by applying LAMBDA(accumulator, value) to each value, row by row.",
4277
+ args: [
4278
+ arg("initial_value", "The starting accumulator (may be left empty)."),
4279
+ arg("array", "The array to reduce."),
4280
+ LAMBDA_ARG
4281
+ ],
4282
+ evaluate: special("REDUCE")
4283
+ },
4284
+ {
4285
+ name: "SCAN",
4286
+ category: "logical",
4287
+ description: "Like REDUCE, but returns every intermediate accumulator value (running totals).",
4288
+ args: [
4289
+ arg("initial_value", "The starting accumulator (may be left empty)."),
4290
+ arg("array", "The array to scan."),
4291
+ LAMBDA_ARG
4292
+ ],
4293
+ evaluate: special("SCAN")
4294
+ },
4295
+ {
4296
+ name: "BYROW",
4297
+ category: "logical",
4298
+ description: "Applies a LAMBDA to each row of an array and returns one value per row: BYROW(A1:C3, LAMBDA(r, SUM(r))).",
4299
+ args: [arg("array", "The array whose rows are passed to the LAMBDA."), LAMBDA_ARG],
4300
+ evaluate: special("BYROW")
4301
+ },
4302
+ {
4303
+ name: "BYCOL",
4304
+ category: "logical",
4305
+ description: "Applies a LAMBDA to each column of an array and returns one value per column.",
4306
+ args: [arg("array", "The array whose columns are passed to the LAMBDA."), LAMBDA_ARG],
4307
+ evaluate: special("BYCOL")
4308
+ },
4309
+ {
4310
+ name: "MAKEARRAY",
4311
+ category: "logical",
4312
+ description: "Builds an array of the given size from LAMBDA(row, column): MAKEARRAY(3, 3, LAMBDA(r, c, r*c)).",
4313
+ args: [
4314
+ arg("rows", "Number of rows.", "number"),
4315
+ arg("columns", "Number of columns.", "number"),
4316
+ LAMBDA_ARG
4317
+ ],
4318
+ evaluate: special("MAKEARRAY")
4319
+ }
4320
+ ];
4321
+
2987
4322
  // src/spreadsheet/formula/functions/index.ts
2988
4323
  var BUILTIN_FORMULA_FUNCTIONS = [
2989
4324
  ...MATH_FUNCTIONS,
@@ -2994,7 +4329,8 @@ var BUILTIN_FORMULA_FUNCTIONS = [
2994
4329
  ...LOOKUP_FUNCTIONS,
2995
4330
  ...DATE_FUNCTIONS,
2996
4331
  ...INFORMATION_FUNCTIONS,
2997
- ...ARRAY_FUNCTIONS
4332
+ ...ARRAY_FUNCTIONS,
4333
+ ...LAMBDA_FUNCTIONS
2998
4334
  ].map((def) => {
2999
4335
  const elementwise = ELEMENTWISE_BUILTINS[def.name];
3000
4336
  return elementwise && def.elementwise === void 0 ? { ...def, elementwise } : def;
@@ -3044,17 +4380,17 @@ function layoutOf(def) {
3044
4380
  return { fixed: def.args.slice(0, groupStart), group: def.args.slice(groupStart), groupStart };
3045
4381
  }
3046
4382
  function getFormulaArgumentSpecIndex(def, argIndex) {
3047
- const { fixed, group, groupStart } = layoutOf(def);
3048
- if (argIndex < fixed.length) return argIndex;
4383
+ const { fixed: fixed2, group, groupStart } = layoutOf(def);
4384
+ if (argIndex < fixed2.length) return argIndex;
3049
4385
  if (group.length === 0) return -1;
3050
4386
  return groupStart + (argIndex - groupStart) % group.length;
3051
4387
  }
3052
4388
  function checkFormulaArgumentCount(def, count) {
3053
- const { fixed, group } = layoutOf(def);
3054
- const requiredFixed = fixed.filter((arg2) => !arg2.optional).length;
4389
+ const { fixed: fixed2, group } = layoutOf(def);
4390
+ const requiredFixed = fixed2.filter((arg2) => !arg2.optional).length;
3055
4391
  const groupRequired = group.length > 0 && !group[0].optional;
3056
4392
  const min2 = requiredFixed + (groupRequired ? group.length : 0);
3057
- const max2 = group.length > 0 ? Infinity : fixed.length;
4393
+ const max2 = group.length > 0 ? Infinity : fixed2.length;
3058
4394
  const given = `${count} ${count === 1 ? "was" : "were"} given`;
3059
4395
  const plural = (n) => `${n} argument${n === 1 ? "" : "s"}`;
3060
4396
  if (count < min2) {
@@ -3065,8 +4401,8 @@ function checkFormulaArgumentCount(def, count) {
3065
4401
  const expected = min2 === max2 ? plural(max2) : `at most ${plural(max2)}`;
3066
4402
  return max2 === 0 ? `${def.name} takes no arguments, but ${given}.` : `${def.name} takes ${expected}, but ${given}.`;
3067
4403
  }
3068
- if (group.length > 1 && count > fixed.length) {
3069
- const extra = (count - fixed.length) % group.length;
4404
+ if (group.length > 1 && count > fixed2.length) {
4405
+ const extra = (count - fixed2.length) % group.length;
3070
4406
  if (extra !== 0 && !(def.allowTrailingArgument && extra === 1)) {
3071
4407
  const names = group.map((arg2) => arg2.name).join(", ");
3072
4408
  return `${def.name} expects its repeating arguments (${names}) in complete groups.`;
@@ -3077,7 +4413,7 @@ function checkFormulaArgumentCount(def, count) {
3077
4413
  function getFormulaSignatureParts(nameOrDef, activeArgIndex) {
3078
4414
  const def = typeof nameOrDef === "string" ? getFormulaFunction(nameOrDef) : nameOrDef;
3079
4415
  if (!def) return [];
3080
- const { fixed, group, groupStart } = layoutOf(def);
4416
+ const { fixed: fixed2, group, groupStart } = layoutOf(def);
3081
4417
  let activeSpec = -1;
3082
4418
  let activeRepeat = 0;
3083
4419
  if (activeArgIndex !== void 0 && activeArgIndex !== null && activeArgIndex >= 0) {
@@ -3091,7 +4427,7 @@ function getFormulaSignatureParts(nameOrDef, activeArgIndex) {
3091
4427
  if (parts.length > 1) parts.push({ text: ", ", specIndex: null, active: false });
3092
4428
  parts.push({ text, specIndex, active: specIndex === activeSpec && repeat === activeRepeat });
3093
4429
  };
3094
- fixed.forEach((arg2, i) => pushArg(arg2.optional ? `[${arg2.name}]` : arg2.name, i));
4430
+ fixed2.forEach((arg2, i) => pushArg(arg2.optional ? `[${arg2.name}]` : arg2.name, i));
3095
4431
  if (group.length > 0) {
3096
4432
  const groupOptional = !!group[0].optional;
3097
4433
  group.forEach((arg2, i) => {
@@ -3151,11 +4487,30 @@ function buildArray(rows, cols, fn) {
3151
4487
  }
3152
4488
  var isMultiArray = (value) => Array.isArray(value) && value.length > 1;
3153
4489
  var firstValue = (value) => Array.isArray(value) ? value.length ? value[0] : "" : value;
4490
+ var sheetLabel = (node) => node.sheet === void 0 ? "" : `${node.sheetEnd === void 0 ? node.sheet : `${node.sheet}:${node.sheetEnd}`}!`;
4491
+ var SPECIAL_FORMS = /* @__PURE__ */ new Set(["LET", "LAMBDA", "ISOMITTED", "MAP", "REDUCE", "SCAN", "BYROW", "BYCOL", "MAKEARRAY"]);
4492
+ function elementValue(value) {
4493
+ if (isFormulaLambda(value)) return "#CALC!";
4494
+ if (Array.isArray(value)) return value.length === 1 ? elementValue(value[0]) : "#CALC!";
4495
+ return value;
4496
+ }
4497
+ function asRange(value) {
4498
+ if (Array.isArray(value)) return toFormulaRange(value);
4499
+ return { rows: 1, cols: 1, values: [value] };
4500
+ }
4501
+ var LITERAL_BLANK = { type: "Literal", value: "" };
3154
4502
  var FormulaEvaluator = class {
3155
4503
  context;
3156
4504
  diagnostics = [];
3157
- // LET variables, innermost scope last.
3158
- scopes = [];
4505
+ // LET and LAMBDA variables (innermost first, chained to outer scopes).
4506
+ env = null;
4507
+ // Sheet that unqualified references refer to while evaluating a
4508
+ // sheet-scoped defined name (undefined: the formula's own sheet).
4509
+ defaultSheet = void 0;
4510
+ lambdaDepth = 0;
4511
+ // Defined names being evaluated, to stop names that refer to themselves.
4512
+ nameStack = [];
4513
+ parsedNames = /* @__PURE__ */ new Map();
3159
4514
  constructor(context = {}) {
3160
4515
  this.context = context;
3161
4516
  }
@@ -3165,7 +4520,14 @@ var FormulaEvaluator = class {
3165
4520
  */
3166
4521
  evaluate(astOrFormula) {
3167
4522
  const ast = typeof astOrFormula === "string" ? parseFormula(astOrFormula) : astOrFormula;
3168
- return this.evaluateNode(ast);
4523
+ const value = this.evaluateNode(ast);
4524
+ if (isFormulaLambda(value)) {
4525
+ return this.fail(
4526
+ "#CALC!",
4527
+ "The formula returns a LAMBDA function without calling it. Add its arguments in parentheses, e.g. LAMBDA(x, x+1)(2)."
4528
+ );
4529
+ }
4530
+ return value;
3169
4531
  }
3170
4532
  /**
3171
4533
  * Evaluates a formula and explains errors. Never throws: malformed formulas
@@ -3175,9 +4537,15 @@ var FormulaEvaluator = class {
3175
4537
  */
3176
4538
  evaluateWithDiagnostics(astOrFormula) {
3177
4539
  const saved = this.diagnostics;
3178
- const savedScopes = this.scopes;
4540
+ const savedEnv = this.env;
4541
+ const savedSheet = this.defaultSheet;
4542
+ const savedDepth = this.lambdaDepth;
4543
+ const savedNames = this.nameStack;
3179
4544
  this.diagnostics = [];
3180
- this.scopes = [];
4545
+ this.env = null;
4546
+ this.defaultSheet = void 0;
4547
+ this.lambdaDepth = 0;
4548
+ this.nameStack = [];
3181
4549
  try {
3182
4550
  let value = this.evaluate(astOrFormula);
3183
4551
  let array;
@@ -3198,10 +4566,16 @@ var FormulaEvaluator = class {
3198
4566
  }
3199
4567
  return array ? { value, array } : { value };
3200
4568
  } catch (err) {
4569
+ if (err instanceof RangeError) {
4570
+ return { value: "#NUM!", error: "#NUM!", message: "The calculation is nested too deeply (recursion limit)." };
4571
+ }
3201
4572
  return { value: "#ERROR!", error: "#ERROR!", message: err?.message || "Formula error" };
3202
4573
  } finally {
3203
4574
  this.diagnostics = saved;
3204
- this.scopes = savedScopes;
4575
+ this.env = savedEnv;
4576
+ this.defaultSheet = savedSheet;
4577
+ this.lambdaDepth = savedDepth;
4578
+ this.nameStack = savedNames;
3205
4579
  }
3206
4580
  }
3207
4581
  /** Records why an error happened and returns the error code. */
@@ -3219,11 +4593,16 @@ var FormulaEvaluator = class {
3219
4593
  if (!this.context.resolveCell) {
3220
4594
  throw new Error(`Cannot resolve cell reference ${node.ref}: no cell resolver provided`);
3221
4595
  }
3222
- const value = this.context.resolveCell(node.col, node.row);
4596
+ if (node.sheetEnd !== void 0) return this.evaluate3d(node);
4597
+ const value = this.context.resolveCell(node.col, node.row, node.sheet ?? this.defaultSheet);
3223
4598
  if (isFormulaError(value)) {
4599
+ const label = `${sheetLabel(node)}${node.ref}`;
4600
+ if (value === "#REF!" && node.sheet !== void 0) {
4601
+ return this.fail("#REF!", `${label} refers to a sheet that doesn't exist.`);
4602
+ }
3224
4603
  this.fail(
3225
4604
  value,
3226
- value === "#CYCLE!" ? `Cell ${node.ref} is part of a circular reference.` : `Cell ${node.ref} contains an error (${value}).`
4605
+ value === "#CYCLE!" ? `Cell ${label} is part of a circular reference.` : `Cell ${label} contains an error (${value}).`
3227
4606
  );
3228
4607
  }
3229
4608
  return value;
@@ -3232,11 +4611,15 @@ var FormulaEvaluator = class {
3232
4611
  return this.evaluateRange(node);
3233
4612
  case "SpillRef":
3234
4613
  return this.evaluateSpillRef(node);
3235
- case "Name": {
3236
- for (let i = this.scopes.length - 1; i >= 0; i--) {
3237
- if (this.scopes[i].has(node.name)) return this.scopes[i].get(node.name);
3238
- }
3239
- return this.fail("#NAME?", "This formula contains an unknown name.");
4614
+ case "Name":
4615
+ return this.evaluateName(node);
4616
+ case "Missing":
4617
+ return "";
4618
+ case "Invoke": {
4619
+ const callee = this.evaluateNode(node.callee);
4620
+ if (isFormulaLambda(callee)) return this.invokeLambda(callee, node.args);
4621
+ if (isFormulaError(callee)) return callee;
4622
+ return this.fail("#VALUE!", "Only a LAMBDA function can be called with arguments in parentheses.");
3240
4623
  }
3241
4624
  case "PropertyRef": {
3242
4625
  if (!this.context.resolveProperty) {
@@ -3258,6 +4641,9 @@ var FormulaEvaluator = class {
3258
4641
  if (isFormulaError(left)) return left;
3259
4642
  const right = this.evaluateNode(node.right);
3260
4643
  if (isFormulaError(right)) return right;
4644
+ if (isFormulaLambda(left) || isFormulaLambda(right)) {
4645
+ return this.fail("#VALUE!", `The ${node.op} operator can't use a LAMBDA function; call it with arguments first.`);
4646
+ }
3261
4647
  if (isMultiArray(left) || isMultiArray(right)) {
3262
4648
  const a = shapeOf(left);
3263
4649
  const b = shapeOf(right);
@@ -3285,6 +4671,7 @@ var FormulaEvaluator = class {
3285
4671
  }
3286
4672
  }
3287
4673
  unary(op, raw) {
4674
+ if (isFormulaLambda(raw)) return this.fail("#VALUE!", `The ${op} operator can't use a LAMBDA function.`);
3288
4675
  const val = toNumber(raw);
3289
4676
  if (isFormulaError(val)) {
3290
4677
  return isFormulaError(raw) ? raw : this.fail("#VALUE!", `The ${op} operator can't use text (${quote(raw)}).`);
@@ -3399,7 +4786,10 @@ var FormulaEvaluator = class {
3399
4786
  if (!this.context.resolveSpill) {
3400
4787
  throw new Error(`Cannot resolve spill range ${node.ref}#: no spill resolver provided`);
3401
4788
  }
3402
- const spill = this.context.resolveSpill(node.col, node.row);
4789
+ if (node.sheetEnd !== void 0) {
4790
+ return this.fail("#REF!", `A 3-D reference can't use the spill operator (${sheetLabel(node)}${node.ref}#).`);
4791
+ }
4792
+ const spill = this.context.resolveSpill(node.col, node.row, node.sheet ?? this.defaultSheet);
3403
4793
  if (typeof spill === "string" || !spill) {
3404
4794
  const code = isFormulaError(spill) ? spill : "#REF!";
3405
4795
  return this.fail(
@@ -3416,8 +4806,14 @@ var FormulaEvaluator = class {
3416
4806
  if (!this.context.resolveRange) {
3417
4807
  throw new Error(`Cannot resolve range ${node.raw}: no range resolver provided`);
3418
4808
  }
3419
- const values = this.context.resolveRange(node.startCol, node.startRow, node.endCol, node.endRow);
3420
- if (!Array.isArray(values)) return values;
4809
+ if (node.sheetEnd !== void 0) return this.evaluate3d(node);
4810
+ const values = this.context.resolveRange(node.startCol, node.startRow, node.endCol, node.endRow, node.sheet ?? this.defaultSheet);
4811
+ if (!Array.isArray(values)) {
4812
+ if (values === "#REF!" && node.sheet !== void 0) {
4813
+ return this.fail("#REF!", `${sheetLabel(node)}${node.raw} refers to a sheet that doesn't exist.`);
4814
+ }
4815
+ return values;
4816
+ }
3421
4817
  const cols = Math.abs(colNameToIndex(node.endCol) - colNameToIndex(node.startCol)) + 1;
3422
4818
  if (values.length % cols === 0) withRangeShape(values, values.length / cols, cols);
3423
4819
  const error = values.find(isFormulaError);
@@ -3425,11 +4821,28 @@ var FormulaEvaluator = class {
3425
4821
  return values;
3426
4822
  }
3427
4823
  callFunction(node) {
4824
+ const variables = lookupEnvironment(this.env, node.name);
4825
+ if (variables) {
4826
+ const bound = variables.vars.get(node.name);
4827
+ if (isFormulaLambda(bound)) return this.invokeLambda(bound, node.args);
4828
+ }
3428
4829
  const def = getFormulaFunction(node.name);
3429
- if (!def) return this.fail("#NAME?", `Unknown function "${node.name.toUpperCase()}".`);
4830
+ if (!def) {
4831
+ const named = this.lookupDefinedName(node.name);
4832
+ if (named) {
4833
+ const value = this.evaluateDefinedName(named, node.name);
4834
+ if (isFormulaLambda(value)) return this.invokeLambda(value, node.args);
4835
+ if (isFormulaError(value)) return value;
4836
+ return this.fail("#VALUE!", `The name ${node.name} doesn't refer to a LAMBDA function, so it can't be called.`);
4837
+ }
4838
+ if (variables) {
4839
+ return this.fail("#VALUE!", `The variable ${node.name} doesn't hold a LAMBDA function, so it can't be called.`);
4840
+ }
4841
+ return this.fail("#NAME?", `Unknown function "${node.name.toUpperCase()}".`);
4842
+ }
3430
4843
  const countError = checkFormulaArgumentCount(def, node.args.length);
3431
4844
  if (countError) return this.fail("#ERROR!", countError);
3432
- if (def.name === "LET") return this.evaluateLet(node);
4845
+ if (SPECIAL_FORMS.has(def.name)) return this.evaluateSpecialForm(def.name, node);
3433
4846
  const args = this.lazyArguments(node.args);
3434
4847
  if (def.elementwise) {
3435
4848
  const lifted = this.liftOverArrays(def, args);
@@ -3482,25 +4895,319 @@ var FormulaEvaluator = class {
3482
4895
  return firstValue(this.finishCall(def, def.evaluate(this.createContext(def, elementArgs))));
3483
4896
  });
3484
4897
  }
4898
+ // --- Names, LET and LAMBDA ---
4899
+ /** A bare name: a variable, a defined name, or #NAME?. */
4900
+ evaluateName(node) {
4901
+ if (node.sheet === void 0) {
4902
+ const variables = lookupEnvironment(this.env, node.name);
4903
+ if (variables) return variables.vars.get(node.name);
4904
+ }
4905
+ const named = this.lookupDefinedName(node.name, node.sheet);
4906
+ if (named) return this.evaluateDefinedName(named, `${sheetLabel(node)}${node.name}`);
4907
+ if (node.sheet !== void 0) {
4908
+ return this.fail("#NAME?", `There is no name ${node.name} on sheet ${node.sheet}.`);
4909
+ }
4910
+ return this.fail("#NAME?", `This formula contains an unknown name.`);
4911
+ }
4912
+ lookupDefinedName(name, sheet) {
4913
+ return this.context.resolveName?.(name, sheet ?? this.defaultSheet) ?? null;
4914
+ }
4915
+ /**
4916
+ * Evaluates a defined name's formula, outside any LET/LAMBDA variables, with
4917
+ * unqualified references pointing at the name's sheet (for sheet-scoped
4918
+ * names).
4919
+ */
4920
+ evaluateDefinedName(def, label) {
4921
+ const key = (def.key ?? label).toUpperCase();
4922
+ if (this.nameStack.includes(key)) {
4923
+ return this.fail("#CYCLE!", `The name ${label} refers to itself.`);
4924
+ }
4925
+ let ast;
4926
+ if (typeof def.formula === "string") {
4927
+ const text = def.formula.startsWith("=") ? def.formula : `=${def.formula}`;
4928
+ let cached = this.parsedNames.get(text);
4929
+ if (!cached) {
4930
+ try {
4931
+ cached = parseFormula(text);
4932
+ } catch {
4933
+ return this.fail("#NAME?", `The name ${label} has a formula that can't be read.`);
4934
+ }
4935
+ if (this.parsedNames.size > 500) this.parsedNames.clear();
4936
+ this.parsedNames.set(text, cached);
4937
+ }
4938
+ ast = cached;
4939
+ } else {
4940
+ ast = def.formula;
4941
+ }
4942
+ const savedEnv = this.env;
4943
+ const savedSheet = this.defaultSheet;
4944
+ this.nameStack.push(key);
4945
+ this.env = null;
4946
+ if (def.sheet !== void 0) this.defaultSheet = def.sheet;
4947
+ try {
4948
+ return this.evaluateNode(ast);
4949
+ } finally {
4950
+ this.env = savedEnv;
4951
+ this.defaultSheet = savedSheet;
4952
+ this.nameStack.pop();
4953
+ }
4954
+ }
4955
+ /** A 3-D reference (`Sheet1:Sheet3!A1`, `Sheet1:Sheet3!A1:B2`): every sheet's values, in tab order. */
4956
+ evaluate3d(node) {
4957
+ const label = node.type === "CellRef" ? `${sheetLabel(node)}${node.ref}` : `${sheetLabel(node)}${node.raw}`;
4958
+ const sheets = this.context.resolveSheetSpan?.(node.sheet, node.sheetEnd);
4959
+ if (!Array.isArray(sheets)) {
4960
+ return this.fail("#REF!", `${label} refers to a sheet that doesn't exist.`);
4961
+ }
4962
+ const values = [];
4963
+ for (const sheet of sheets) {
4964
+ const part = node.type === "CellRef" ? [this.context.resolveCell(node.col, node.row, sheet)] : this.context.resolveRange(node.startCol, node.startRow, node.endCol, node.endRow, sheet);
4965
+ if (!Array.isArray(part)) return this.fail("#REF!", `${label} refers to a sheet that doesn't exist.`);
4966
+ for (const value of part) values.push(value ?? "");
4967
+ }
4968
+ const error = values.find(isFormulaError);
4969
+ if (error) this.fail(error, `The 3-D reference ${label} contains an error (${error}).`);
4970
+ return withRangeShape(values, values.length, 1);
4971
+ }
4972
+ /** Calls a lambda with argument expressions, evaluated in the caller's scope. */
4973
+ invokeLambda(lambda, argNodes) {
4974
+ const params = lambda.params;
4975
+ const positionsOk = argNodes.length === params.length || argNodes.length === 0 && params.length === 1;
4976
+ if (!positionsOk) {
4977
+ return this.fail(
4978
+ "#VALUE!",
4979
+ `${lambda.label} expects ${params.length} argument${params.length === 1 ? "" : "s"}, but ${argNodes.length} ${argNodes.length === 1 ? "was" : "were"} given.`
4980
+ );
4981
+ }
4982
+ const values = [];
4983
+ const omitted = /* @__PURE__ */ new Set();
4984
+ for (let i = 0; i < params.length; i++) {
4985
+ const node = argNodes[i];
4986
+ if (!node || node.type === "Missing") {
4987
+ omitted.add(i);
4988
+ values.push("");
4989
+ } else {
4990
+ values.push(this.evaluateNode(node));
4991
+ }
4992
+ }
4993
+ return this.invokeWithValues(lambda, values, omitted);
4994
+ }
4995
+ /** Calls a lambda with argument values (`omitted`: positions left out, for ISOMITTED). */
4996
+ invokeWithValues(lambda, values, omitted) {
4997
+ if (this.lambdaDepth >= MAX_LAMBDA_DEPTH) {
4998
+ return this.fail("#NUM!", `LAMBDA calls are nested more than ${MAX_LAMBDA_DEPTH} deep (runaway recursion?).`);
4999
+ }
5000
+ if (lambda.native) return lambda.native(values);
5001
+ const vars = /* @__PURE__ */ new Map();
5002
+ let omittedNames;
5003
+ lambda.params.forEach((param, i) => {
5004
+ vars.set(param, values[i] ?? "");
5005
+ if (omitted?.has(i)) (omittedNames ??= /* @__PURE__ */ new Set()).add(param);
5006
+ });
5007
+ const savedEnv = this.env;
5008
+ const savedSheet = this.defaultSheet;
5009
+ this.env = { vars, omitted: omittedNames, parent: lambda.env };
5010
+ this.defaultSheet = lambda.sheet;
5011
+ this.lambdaDepth++;
5012
+ try {
5013
+ return this.evaluateNode(lambda.body);
5014
+ } finally {
5015
+ this.lambdaDepth--;
5016
+ this.env = savedEnv;
5017
+ this.defaultSheet = savedSheet;
5018
+ }
5019
+ }
5020
+ evaluateSpecialForm(name, node) {
5021
+ switch (name) {
5022
+ case "LET":
5023
+ return this.evaluateLet(node);
5024
+ case "LAMBDA":
5025
+ return this.evaluateLambda(node);
5026
+ case "ISOMITTED": {
5027
+ const arg2 = node.args[0];
5028
+ if (arg2?.type !== "Name" || arg2.sheet !== void 0) return false;
5029
+ const variables = lookupEnvironment(this.env, arg2.name);
5030
+ return !!variables?.omitted?.has(arg2.name);
5031
+ }
5032
+ case "MAP":
5033
+ return this.evaluateMap(node);
5034
+ case "REDUCE":
5035
+ case "SCAN":
5036
+ return this.evaluateReduce(node, name === "SCAN");
5037
+ case "BYROW":
5038
+ case "BYCOL":
5039
+ return this.evaluateByLine(node, name === "BYROW");
5040
+ default:
5041
+ return this.evaluateMakeArray(node);
5042
+ }
5043
+ }
3485
5044
  /** LET(name1, value1, [name2, value2, ...], calculation). */
3486
5045
  evaluateLet(node) {
3487
5046
  if (node.args.length % 2 === 0) {
3488
5047
  return this.fail("#ERROR!", "LET needs name/value pairs followed by a calculation.");
3489
5048
  }
3490
- const scope = /* @__PURE__ */ new Map();
3491
- this.scopes.push(scope);
5049
+ const vars = /* @__PURE__ */ new Map();
5050
+ const savedEnv = this.env;
5051
+ this.env = { vars, parent: savedEnv };
3492
5052
  try {
3493
5053
  for (let i = 0; i < node.args.length - 1; i += 2) {
3494
5054
  const nameNode = node.args[i];
3495
- if (nameNode.type !== "Name") {
5055
+ if (nameNode.type !== "Name" || nameNode.sheet !== void 0) {
3496
5056
  return this.fail("#ERROR!", `Argument ${i + 1} of LET must be a name (names can't look like cell references).`);
3497
5057
  }
3498
- scope.set(nameNode.name, this.evaluateNode(node.args[i + 1]));
5058
+ vars.set(nameNode.name, this.evaluateNode(node.args[i + 1]));
3499
5059
  }
3500
5060
  return this.evaluateNode(node.args[node.args.length - 1]);
3501
5061
  } finally {
3502
- this.scopes.pop();
5062
+ this.env = savedEnv;
5063
+ }
5064
+ }
5065
+ /** LAMBDA([parameter1, ...], calculation): a function value closing over the current variables. */
5066
+ evaluateLambda(node) {
5067
+ const params = [];
5068
+ for (let i = 0; i < node.args.length - 1; i++) {
5069
+ const param = node.args[i];
5070
+ if (param.type !== "Name" || param.sheet !== void 0) {
5071
+ return this.fail("#VALUE!", `Parameter ${i + 1} of LAMBDA must be a name (names can't look like cell references).`);
5072
+ }
5073
+ if (params.includes(param.name)) {
5074
+ return this.fail("#VALUE!", `LAMBDA parameter ${param.name} is listed twice.`);
5075
+ }
5076
+ params.push(param.name);
5077
+ }
5078
+ const body = node.args[node.args.length - 1];
5079
+ if (body.type === "Missing") return this.fail("#VALUE!", "LAMBDA needs a calculation as its last argument.");
5080
+ return new FormulaLambda(params, body, this.env, this.defaultSheet);
5081
+ }
5082
+ /**
5083
+ * The function argument of a LAMBDA helper: a lambda, or a built-in
5084
+ * function given by name (`BYROW(A1:C3, SUM)`) unless a variable or defined
5085
+ * name has that name. Returns an error string when it is neither.
5086
+ */
5087
+ lambdaArgument(fn, node, index) {
5088
+ if (!node) return this.fail("#VALUE!", `${fn} needs a LAMBDA function.`);
5089
+ if (node.type === "Name" && node.sheet === void 0 && !lookupEnvironment(this.env, node.name) && !this.lookupDefinedName(node.name)) {
5090
+ const def = getFormulaFunction(node.name);
5091
+ if (def && !SPECIAL_FORMS.has(def.name)) {
5092
+ return new FormulaLambda([], null, null, void 0, (values) => this.callBuiltinWithValues(def, values), def.name);
5093
+ }
5094
+ }
5095
+ const value = this.evaluateNode(node);
5096
+ if (isFormulaLambda(value)) return value;
5097
+ if (isFormulaError(value)) return value;
5098
+ return this.fail("#VALUE!", `Argument ${index + 1} of ${fn} must be a LAMBDA function.`);
5099
+ }
5100
+ /** Calls a built-in function with already evaluated arguments. */
5101
+ callBuiltinWithValues(def, values) {
5102
+ const countError = checkFormulaArgumentCount(def, values.length);
5103
+ if (countError) return this.fail("#VALUE!", countError);
5104
+ const args = values.map((value, index) => ({ index, node: LITERAL_BLANK, value: () => value }));
5105
+ if (def.elementwise) {
5106
+ const lifted = this.liftOverArrays(def, args);
5107
+ if (lifted !== void 0) return lifted;
5108
+ }
5109
+ return this.finishCall(def, def.evaluate(this.createContext(def, args)));
5110
+ }
5111
+ /** Checks a lambda takes `count` parameters (native functions take any count). */
5112
+ checkArity(fn, lambda, count) {
5113
+ if (lambda.native || lambda.params.length === count) return null;
5114
+ return this.fail(
5115
+ "#VALUE!",
5116
+ `The LAMBDA passed to ${fn} must take ${count} parameter${count === 1 ? "" : "s"}, but it takes ${lambda.params.length}.`
5117
+ );
5118
+ }
5119
+ /** MAP(array1, [array2, ...], lambda): calls the lambda with each element. */
5120
+ evaluateMap(node) {
5121
+ const lambda = this.lambdaArgument("MAP", node.args[node.args.length - 1], node.args.length - 1);
5122
+ if (typeof lambda === "string") return lambda;
5123
+ const arrays = [];
5124
+ for (let i = 0; i < node.args.length - 1; i++) {
5125
+ const value = this.evaluateNode(node.args[i]);
5126
+ if (isFormulaError(value)) return value;
5127
+ arrays.push(value);
5128
+ }
5129
+ const arity = this.checkArity("MAP", lambda, arrays.length);
5130
+ if (arity) return arity;
5131
+ let rows = 1;
5132
+ let cols = 1;
5133
+ for (const array of arrays) {
5134
+ const shape = shapeOf(array);
5135
+ rows = Math.max(rows, shape.rows);
5136
+ cols = Math.max(cols, shape.cols);
5137
+ }
5138
+ if (rows * cols > MAX_FORMULA_ARRAY_SIZE) return this.fail("#NUM!", "The array result is too large.");
5139
+ return buildArray(
5140
+ rows,
5141
+ cols,
5142
+ (r, c) => elementValue(this.invokeWithValues(lambda, arrays.map((array) => broadcastGet(array, r, c))))
5143
+ );
5144
+ }
5145
+ /**
5146
+ * REDUCE([initial_value], array, lambda) / SCAN(...): folds the array row
5147
+ * by row with lambda(accumulator, value). SCAN returns every intermediate
5148
+ * value, in the array's shape.
5149
+ */
5150
+ evaluateReduce(node, scan) {
5151
+ const fn = scan ? "SCAN" : "REDUCE";
5152
+ let acc = this.evaluateNode(node.args[0]);
5153
+ const arrayValue = this.evaluateNode(node.args[1]);
5154
+ if (isFormulaError(arrayValue)) return arrayValue;
5155
+ const lambda = this.lambdaArgument(fn, node.args[2], 2);
5156
+ if (typeof lambda === "string") return lambda;
5157
+ const arity = this.checkArity(fn, lambda, 2);
5158
+ if (arity) return arity;
5159
+ const range = asRange(arrayValue);
5160
+ const results = scan ? new Array(range.rows * range.cols) : [];
5161
+ for (let r = 0; r < range.rows; r++) {
5162
+ for (let c = 0; c < range.cols; c++) {
5163
+ acc = this.invokeWithValues(lambda, [acc, rangeGet(range, r, c)]);
5164
+ if (scan) results[c * range.rows + r] = elementValue(acc);
5165
+ }
3503
5166
  }
5167
+ if (scan) return withRangeShape(results, range.rows, range.cols);
5168
+ return acc;
5169
+ }
5170
+ /** BYROW(array, lambda) / BYCOL(array, lambda): one result per row (a column) or per column (a row). */
5171
+ evaluateByLine(node, byRow) {
5172
+ const fn = byRow ? "BYROW" : "BYCOL";
5173
+ const arrayValue = this.evaluateNode(node.args[0]);
5174
+ if (isFormulaError(arrayValue)) return arrayValue;
5175
+ const lambda = this.lambdaArgument(fn, node.args[1], 1);
5176
+ if (typeof lambda === "string") return lambda;
5177
+ const arity = this.checkArity(fn, lambda, 1);
5178
+ if (arity) return arity;
5179
+ const range = asRange(arrayValue);
5180
+ const count = byRow ? range.rows : range.cols;
5181
+ const out = [];
5182
+ for (let i = 0; i < count; i++) {
5183
+ const line = [];
5184
+ const length = byRow ? range.cols : range.rows;
5185
+ for (let j = 0; j < length; j++) line.push(byRow ? rangeGet(range, i, j) : rangeGet(range, j, i));
5186
+ const slice = byRow ? withRangeShape(line, 1, length) : withRangeShape(line, length, 1);
5187
+ out.push(elementValue(this.invokeWithValues(lambda, [slice])));
5188
+ }
5189
+ return byRow ? withRangeShape(out, count, 1) : withRangeShape(out, 1, count);
5190
+ }
5191
+ /** MAKEARRAY(rows, columns, lambda): lambda(row, column) for every 1-based position. */
5192
+ evaluateMakeArray(node) {
5193
+ const size = (index) => {
5194
+ const value = firstValue(this.evaluateNode(node.args[index]));
5195
+ if (isFormulaError(value)) return value;
5196
+ const num = toNumber(value);
5197
+ if (isFormulaError(num)) return this.fail("#VALUE!", `Argument ${index + 1} of MAKEARRAY must be a number.`);
5198
+ return Math.trunc(num);
5199
+ };
5200
+ const rows = size(0);
5201
+ if (typeof rows === "string") return rows;
5202
+ const cols = size(1);
5203
+ if (typeof cols === "string") return cols;
5204
+ if (rows < 1 || cols < 1) return this.fail("#VALUE!", "MAKEARRAY needs at least one row and one column.");
5205
+ if (rows * cols > MAX_FORMULA_ARRAY_SIZE) return this.fail("#NUM!", "The array result is too large.");
5206
+ const lambda = this.lambdaArgument("MAKEARRAY", node.args[2], 2);
5207
+ if (typeof lambda === "string") return lambda;
5208
+ const arity = this.checkArity("MAKEARRAY", lambda, 2);
5209
+ if (arity) return arity;
5210
+ return buildArray(rows, cols, (r, c) => elementValue(this.invokeWithValues(lambda, [r + 1, c + 1])));
3504
5211
  }
3505
5212
  createContext(def, args) {
3506
5213
  const has = (index) => index >= 0 && index < args.length;
@@ -3590,11 +5297,17 @@ export {
3590
5297
  parseRange,
3591
5298
  FormulaParser,
3592
5299
  parseFormula,
5300
+ FormulaLambda,
5301
+ isFormulaLambda,
5302
+ MAX_LAMBDA_DEPTH,
3593
5303
  serialFromYmd,
3594
5304
  parseCellInput,
3595
5305
  parseNumberText,
3596
5306
  FORMULA_ERRORS,
3597
5307
  isFormulaError,
5308
+ isBlank,
5309
+ toBoolean,
5310
+ toText,
3598
5311
  numberToText,
3599
5312
  numericValue,
3600
5313
  compareNumbers,
@@ -3603,10 +5316,19 @@ export {
3603
5316
  colNameToIndex,
3604
5317
  indexToColName,
3605
5318
  parseIsoDate,
5319
+ serialToDate,
3606
5320
  wildcardToRegExp,
3607
5321
  parseCriteria,
3608
- formatCellDisplay,
3609
- formatValueWithCode,
5322
+ formatGeneralFit,
5323
+ builtinNumberFormat,
5324
+ builtinNumberFormatId,
5325
+ MAX_NUMBER_FORMAT_CODE_CHARS,
5326
+ NumberFormat,
5327
+ clearNumberFormatCache,
5328
+ dateValueToSerial,
5329
+ formatNumberFormatValue,
5330
+ formatTextFunction,
5331
+ numberFormatColorToCss,
3610
5332
  registerFormulaFunction,
3611
5333
  unregisterFormulaFunction,
3612
5334
  getFormulaFunction,
@@ -3620,4 +5342,4 @@ export {
3620
5342
  MAX_FORMULA_ARRAY_SIZE,
3621
5343
  FormulaEvaluator
3622
5344
  };
3623
- //# sourceMappingURL=chunk-QBJ22YZR.js.map
5345
+ //# sourceMappingURL=chunk-FH6F67DN.js.map