@cj-tech-master/excelts 9.2.1 → 9.3.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 (383) hide show
  1. package/README.md +25 -2
  2. package/README_zh.md +29 -6
  3. package/dist/browser/index.browser.d.ts +1 -1
  4. package/dist/browser/index.browser.js +4 -0
  5. package/dist/browser/index.d.ts +1 -1
  6. package/dist/browser/index.js +4 -0
  7. package/dist/browser/modules/excel/cell.d.ts +17 -3
  8. package/dist/browser/modules/excel/cell.js +170 -22
  9. package/dist/browser/modules/excel/defined-names.d.ts +96 -1
  10. package/dist/browser/modules/excel/defined-names.js +411 -21
  11. package/dist/browser/modules/excel/image.d.ts +11 -0
  12. package/dist/browser/modules/excel/image.js +24 -1
  13. package/dist/browser/modules/excel/stream/workbook-reader.browser.d.ts +9 -3
  14. package/dist/browser/modules/excel/stream/workbook-reader.browser.js +14 -0
  15. package/dist/browser/modules/excel/stream/workbook-reader.d.ts +2 -1
  16. package/dist/browser/modules/excel/stream/workbook-writer.browser.d.ts +39 -5
  17. package/dist/browser/modules/excel/stream/workbook-writer.browser.js +48 -1
  18. package/dist/browser/modules/excel/stream/workbook-writer.d.ts +3 -2
  19. package/dist/browser/modules/excel/stream/worksheet-reader.js +17 -1
  20. package/dist/browser/modules/excel/stream/worksheet-writer.d.ts +39 -6
  21. package/dist/browser/modules/excel/stream/worksheet-writer.js +45 -5
  22. package/dist/browser/modules/excel/table.js +15 -2
  23. package/dist/browser/modules/excel/types.d.ts +133 -2
  24. package/dist/browser/modules/excel/utils/col-cache.d.ts +1 -0
  25. package/dist/browser/modules/excel/utils/col-cache.js +15 -0
  26. package/dist/browser/modules/excel/utils/drawing-utils.d.ts +3 -3
  27. package/dist/browser/modules/excel/utils/drawing-utils.js +4 -0
  28. package/dist/browser/modules/excel/utils/external-link-formula.d.ts +76 -0
  29. package/dist/browser/modules/excel/utils/external-link-formula.js +208 -0
  30. package/dist/browser/modules/excel/utils/iterate-stream.d.ts +9 -3
  31. package/dist/browser/modules/excel/utils/iterate-stream.js +3 -1
  32. package/dist/browser/modules/excel/utils/ooxml-paths.d.ts +19 -0
  33. package/dist/browser/modules/excel/utils/ooxml-paths.js +37 -2
  34. package/dist/browser/modules/excel/utils/shared-strings.d.ts +8 -3
  35. package/dist/browser/modules/excel/utils/shared-strings.js +21 -2
  36. package/dist/browser/modules/excel/utils/workbook-protection.d.ts +30 -0
  37. package/dist/browser/modules/excel/utils/workbook-protection.js +30 -0
  38. package/dist/browser/modules/excel/workbook.browser.d.ts +257 -6
  39. package/dist/browser/modules/excel/workbook.browser.js +318 -34
  40. package/dist/browser/modules/excel/workbook.d.ts +1 -1
  41. package/dist/browser/modules/excel/worksheet.d.ts +3 -1
  42. package/dist/browser/modules/excel/worksheet.js +21 -2
  43. package/dist/browser/modules/excel/xlsx/rel-type.d.ts +15 -0
  44. package/dist/browser/modules/excel/xlsx/rel-type.js +16 -1
  45. package/dist/browser/modules/excel/xlsx/xform/book/defined-name-xform.d.ts +6 -5
  46. package/dist/browser/modules/excel/xlsx/xform/book/defined-name-xform.js +21 -86
  47. package/dist/browser/modules/excel/xlsx/xform/book/external-link-xform.d.ts +84 -0
  48. package/dist/browser/modules/excel/xlsx/xform/book/external-link-xform.js +330 -0
  49. package/dist/browser/modules/excel/xlsx/xform/book/external-reference-xform.d.ts +17 -0
  50. package/dist/browser/modules/excel/xlsx/xform/book/external-reference-xform.js +24 -0
  51. package/dist/browser/modules/excel/xlsx/xform/book/workbook-calc-properties-xform.d.ts +3 -0
  52. package/dist/browser/modules/excel/xlsx/xform/book/workbook-calc-properties-xform.js +11 -2
  53. package/dist/browser/modules/excel/xlsx/xform/book/workbook-protection-xform.d.ts +20 -0
  54. package/dist/browser/modules/excel/xlsx/xform/book/workbook-protection-xform.js +66 -0
  55. package/dist/browser/modules/excel/xlsx/xform/book/workbook-xform.js +38 -5
  56. package/dist/browser/modules/excel/xlsx/xform/core/content-types-xform.js +19 -1
  57. package/dist/browser/modules/excel/xlsx/xform/core/metadata-xform.d.ts +56 -0
  58. package/dist/browser/modules/excel/xlsx/xform/core/metadata-xform.js +158 -0
  59. package/dist/browser/modules/excel/xlsx/xform/drawing/absolute-anchor-xform.d.ts +26 -0
  60. package/dist/browser/modules/excel/xlsx/xform/drawing/absolute-anchor-xform.js +105 -0
  61. package/dist/browser/modules/excel/xlsx/xform/drawing/base-cell-anchor-xform.js +3 -0
  62. package/dist/browser/modules/excel/xlsx/xform/drawing/drawing-xform.js +10 -2
  63. package/dist/browser/modules/excel/xlsx/xform/sheet/cell-xform.d.ts +1 -1
  64. package/dist/browser/modules/excel/xlsx/xform/sheet/cell-xform.js +166 -8
  65. package/dist/browser/modules/excel/xlsx/xform/sheet/data-validations-xform.js +1 -1
  66. package/dist/browser/modules/excel/xlsx/xform/sheet/ignored-errors-xform.d.ts +21 -0
  67. package/dist/browser/modules/excel/xlsx/xform/sheet/ignored-errors-xform.js +80 -0
  68. package/dist/browser/modules/excel/xlsx/xform/sheet/worksheet-xform.js +9 -4
  69. package/dist/browser/modules/excel/xlsx/xform/style/border-xform.js +4 -1
  70. package/dist/browser/modules/excel/xlsx/xlsx.browser.d.ts +172 -13
  71. package/dist/browser/modules/excel/xlsx/xlsx.browser.js +410 -20
  72. package/dist/browser/modules/excel/xlsx/xlsx.d.ts +7 -4
  73. package/dist/browser/modules/excel/xlsx/xlsx.js +4 -5
  74. package/dist/browser/modules/formula/compile/address-utils.d.ts +62 -0
  75. package/dist/browser/modules/formula/compile/address-utils.js +83 -0
  76. package/dist/browser/modules/formula/compile/binder.d.ts +42 -0
  77. package/dist/browser/modules/formula/compile/binder.js +487 -0
  78. package/dist/browser/modules/formula/compile/bound-ast.d.ts +230 -0
  79. package/dist/browser/modules/formula/compile/bound-ast.js +80 -0
  80. package/dist/browser/modules/formula/compile/compiled-formula.d.ts +137 -0
  81. package/dist/browser/modules/formula/compile/compiled-formula.js +383 -0
  82. package/dist/browser/modules/formula/compile/dependency-analysis.d.ts +93 -0
  83. package/dist/browser/modules/formula/compile/dependency-analysis.js +432 -0
  84. package/dist/browser/modules/formula/compile/structured-ref-utils.d.ts +93 -0
  85. package/dist/browser/modules/formula/compile/structured-ref-utils.js +136 -0
  86. package/dist/browser/modules/formula/default-syntax-probe.d.ts +79 -0
  87. package/dist/browser/modules/formula/default-syntax-probe.js +83 -0
  88. package/dist/browser/modules/formula/functions/_date-context.d.ts +4 -0
  89. package/dist/browser/modules/formula/functions/_date-context.js +29 -0
  90. package/dist/browser/modules/formula/functions/_shared.d.ts +121 -0
  91. package/dist/browser/modules/formula/functions/_shared.js +381 -0
  92. package/dist/browser/modules/formula/functions/conditional.d.ts +27 -0
  93. package/dist/browser/modules/formula/functions/conditional.js +343 -0
  94. package/dist/browser/modules/formula/functions/database.d.ts +37 -0
  95. package/dist/browser/modules/formula/functions/database.js +274 -0
  96. package/dist/browser/modules/formula/functions/date.d.ts +61 -0
  97. package/dist/browser/modules/formula/functions/date.js +855 -0
  98. package/dist/browser/modules/formula/functions/dynamic-array.d.ts +23 -0
  99. package/dist/browser/modules/formula/functions/dynamic-array.js +860 -0
  100. package/dist/browser/modules/formula/functions/engineering.d.ts +57 -0
  101. package/dist/browser/modules/formula/functions/engineering.js +1128 -0
  102. package/dist/browser/modules/formula/functions/financial.d.ts +202 -0
  103. package/dist/browser/modules/formula/functions/financial.js +2296 -0
  104. package/dist/browser/modules/formula/functions/lookup.d.ts +18 -0
  105. package/dist/browser/modules/formula/functions/lookup.js +886 -0
  106. package/dist/browser/modules/formula/functions/math.d.ts +114 -0
  107. package/dist/browser/modules/formula/functions/math.js +1406 -0
  108. package/dist/browser/modules/formula/functions/statistical.d.ts +193 -0
  109. package/dist/browser/modules/formula/functions/statistical.js +3390 -0
  110. package/dist/browser/modules/formula/functions/text.d.ts +86 -0
  111. package/dist/browser/modules/formula/functions/text.js +1845 -0
  112. package/dist/browser/modules/formula/host-registry.d.ts +53 -0
  113. package/dist/browser/modules/formula/host-registry.js +69 -0
  114. package/dist/browser/modules/formula/index.d.ts +39 -0
  115. package/dist/browser/modules/formula/index.js +49 -0
  116. package/dist/browser/modules/formula/install.d.ts +62 -0
  117. package/dist/browser/modules/formula/install.js +88 -0
  118. package/dist/browser/modules/formula/integration/apply-writeback-plan.d.ts +26 -0
  119. package/dist/browser/modules/formula/integration/apply-writeback-plan.js +210 -0
  120. package/dist/browser/modules/formula/integration/calculate-formulas-impl.d.ts +30 -0
  121. package/dist/browser/modules/formula/integration/calculate-formulas-impl.js +616 -0
  122. package/dist/browser/modules/formula/integration/calculate-formulas.d.ts +67 -0
  123. package/dist/browser/modules/formula/integration/calculate-formulas.js +68 -0
  124. package/dist/browser/modules/formula/integration/formula-instance.d.ts +64 -0
  125. package/dist/browser/modules/formula/integration/formula-instance.js +79 -0
  126. package/dist/browser/modules/formula/integration/workbook-adapter.d.ts +26 -0
  127. package/dist/browser/modules/formula/integration/workbook-adapter.js +324 -0
  128. package/dist/browser/modules/formula/integration/workbook-snapshot.d.ts +267 -0
  129. package/dist/browser/modules/formula/integration/workbook-snapshot.js +77 -0
  130. package/dist/browser/modules/formula/materialize/build-writeback-plan.d.ts +34 -0
  131. package/dist/browser/modules/formula/materialize/build-writeback-plan.js +473 -0
  132. package/dist/browser/modules/formula/materialize/spill-engine.d.ts +9 -0
  133. package/dist/browser/modules/formula/materialize/spill-engine.js +38 -0
  134. package/dist/browser/modules/formula/materialize/types.d.ts +179 -0
  135. package/dist/browser/modules/formula/materialize/types.js +29 -0
  136. package/dist/browser/modules/formula/materialize/writeback-plan.d.ts +167 -0
  137. package/dist/browser/modules/formula/materialize/writeback-plan.js +27 -0
  138. package/dist/browser/modules/formula/runtime/evaluator.d.ts +151 -0
  139. package/dist/browser/modules/formula/runtime/evaluator.js +2291 -0
  140. package/dist/browser/modules/formula/runtime/function-registry.d.ts +47 -0
  141. package/dist/browser/modules/formula/runtime/function-registry.js +840 -0
  142. package/dist/browser/modules/formula/runtime/values.d.ts +211 -0
  143. package/dist/browser/modules/formula/runtime/values.js +385 -0
  144. package/dist/browser/modules/formula/syntax/ast.d.ts +129 -0
  145. package/dist/browser/modules/formula/syntax/ast.js +28 -0
  146. package/dist/browser/modules/formula/syntax/parser.d.ts +18 -0
  147. package/dist/browser/modules/formula/syntax/parser.js +439 -0
  148. package/dist/browser/modules/formula/syntax/token-types.d.ts +153 -0
  149. package/dist/browser/modules/formula/syntax/token-types.js +59 -0
  150. package/dist/browser/modules/formula/syntax/tokenizer.d.ts +10 -0
  151. package/dist/browser/modules/formula/syntax/tokenizer.js +1074 -0
  152. package/dist/browser/modules/pdf/excel-bridge.js +9 -0
  153. package/dist/cjs/index.js +4 -0
  154. package/dist/cjs/modules/excel/cell.js +170 -22
  155. package/dist/cjs/modules/excel/defined-names.js +411 -21
  156. package/dist/cjs/modules/excel/image.js +24 -1
  157. package/dist/cjs/modules/excel/stream/workbook-reader.browser.js +14 -0
  158. package/dist/cjs/modules/excel/stream/workbook-writer.browser.js +48 -1
  159. package/dist/cjs/modules/excel/stream/worksheet-reader.js +17 -1
  160. package/dist/cjs/modules/excel/stream/worksheet-writer.js +45 -5
  161. package/dist/cjs/modules/excel/table.js +15 -2
  162. package/dist/cjs/modules/excel/utils/col-cache.js +15 -0
  163. package/dist/cjs/modules/excel/utils/drawing-utils.js +4 -0
  164. package/dist/cjs/modules/excel/utils/external-link-formula.js +212 -0
  165. package/dist/cjs/modules/excel/utils/iterate-stream.js +3 -1
  166. package/dist/cjs/modules/excel/utils/ooxml-paths.js +42 -2
  167. package/dist/cjs/modules/excel/utils/shared-strings.js +21 -2
  168. package/dist/cjs/modules/excel/utils/workbook-protection.js +33 -0
  169. package/dist/cjs/modules/excel/workbook.browser.js +318 -34
  170. package/dist/cjs/modules/excel/worksheet.js +20 -1
  171. package/dist/cjs/modules/excel/xlsx/rel-type.js +16 -1
  172. package/dist/cjs/modules/excel/xlsx/xform/book/defined-name-xform.js +21 -86
  173. package/dist/cjs/modules/excel/xlsx/xform/book/external-link-xform.js +333 -0
  174. package/dist/cjs/modules/excel/xlsx/xform/book/external-reference-xform.js +27 -0
  175. package/dist/cjs/modules/excel/xlsx/xform/book/workbook-calc-properties-xform.js +11 -2
  176. package/dist/cjs/modules/excel/xlsx/xform/book/workbook-protection-xform.js +69 -0
  177. package/dist/cjs/modules/excel/xlsx/xform/book/workbook-xform.js +38 -5
  178. package/dist/cjs/modules/excel/xlsx/xform/core/content-types-xform.js +18 -0
  179. package/dist/cjs/modules/excel/xlsx/xform/core/metadata-xform.js +161 -0
  180. package/dist/cjs/modules/excel/xlsx/xform/drawing/absolute-anchor-xform.js +108 -0
  181. package/dist/cjs/modules/excel/xlsx/xform/drawing/base-cell-anchor-xform.js +3 -0
  182. package/dist/cjs/modules/excel/xlsx/xform/drawing/drawing-xform.js +10 -2
  183. package/dist/cjs/modules/excel/xlsx/xform/sheet/cell-xform.js +166 -8
  184. package/dist/cjs/modules/excel/xlsx/xform/sheet/data-validations-xform.js +1 -1
  185. package/dist/cjs/modules/excel/xlsx/xform/sheet/ignored-errors-xform.js +83 -0
  186. package/dist/cjs/modules/excel/xlsx/xform/sheet/worksheet-xform.js +9 -4
  187. package/dist/cjs/modules/excel/xlsx/xform/style/border-xform.js +4 -1
  188. package/dist/cjs/modules/excel/xlsx/xlsx.browser.js +408 -18
  189. package/dist/cjs/modules/excel/xlsx/xlsx.js +4 -5
  190. package/dist/cjs/modules/formula/compile/address-utils.js +89 -0
  191. package/dist/cjs/modules/formula/compile/binder.js +489 -0
  192. package/dist/cjs/modules/formula/compile/bound-ast.js +68 -0
  193. package/dist/cjs/modules/formula/compile/compiled-formula.js +387 -0
  194. package/dist/cjs/modules/formula/compile/dependency-analysis.js +437 -0
  195. package/dist/cjs/modules/formula/compile/structured-ref-utils.js +141 -0
  196. package/dist/cjs/modules/formula/default-syntax-probe.js +87 -0
  197. package/dist/cjs/modules/formula/functions/_date-context.js +33 -0
  198. package/dist/cjs/modules/formula/functions/_shared.js +396 -0
  199. package/dist/cjs/modules/formula/functions/conditional.js +354 -0
  200. package/dist/cjs/modules/formula/functions/database.js +288 -0
  201. package/dist/cjs/modules/formula/functions/date.js +883 -0
  202. package/dist/cjs/modules/formula/functions/dynamic-array.js +881 -0
  203. package/dist/cjs/modules/formula/functions/engineering.js +1183 -0
  204. package/dist/cjs/modules/formula/functions/financial.js +2348 -0
  205. package/dist/cjs/modules/formula/functions/lookup.js +902 -0
  206. package/dist/cjs/modules/formula/functions/math.js +1487 -0
  207. package/dist/cjs/modules/formula/functions/statistical.js +3488 -0
  208. package/dist/cjs/modules/formula/functions/text.js +1889 -0
  209. package/dist/cjs/modules/formula/host-registry.js +75 -0
  210. package/dist/cjs/modules/formula/index.js +58 -0
  211. package/dist/cjs/modules/formula/install.js +93 -0
  212. package/dist/cjs/modules/formula/integration/apply-writeback-plan.js +213 -0
  213. package/dist/cjs/modules/formula/integration/calculate-formulas-impl.js +619 -0
  214. package/dist/cjs/modules/formula/integration/calculate-formulas.js +71 -0
  215. package/dist/cjs/modules/formula/integration/formula-instance.js +82 -0
  216. package/dist/cjs/modules/formula/integration/workbook-adapter.js +327 -0
  217. package/dist/cjs/modules/formula/integration/workbook-snapshot.js +84 -0
  218. package/dist/cjs/modules/formula/materialize/build-writeback-plan.js +475 -0
  219. package/dist/cjs/modules/formula/materialize/spill-engine.js +42 -0
  220. package/dist/cjs/modules/formula/materialize/types.js +32 -0
  221. package/dist/cjs/modules/formula/materialize/writeback-plan.js +28 -0
  222. package/dist/cjs/modules/formula/runtime/evaluator.js +2298 -0
  223. package/dist/cjs/modules/formula/runtime/function-registry.js +846 -0
  224. package/dist/cjs/modules/formula/runtime/values.js +385 -0
  225. package/dist/cjs/modules/formula/syntax/ast.js +8 -0
  226. package/dist/cjs/modules/formula/syntax/parser.js +440 -0
  227. package/dist/cjs/modules/formula/syntax/token-types.js +32 -0
  228. package/dist/cjs/modules/formula/syntax/tokenizer.js +1076 -0
  229. package/dist/cjs/modules/pdf/excel-bridge.js +9 -0
  230. package/dist/esm/index.browser.js +4 -0
  231. package/dist/esm/index.js +4 -0
  232. package/dist/esm/modules/excel/cell.js +170 -22
  233. package/dist/esm/modules/excel/defined-names.js +411 -21
  234. package/dist/esm/modules/excel/image.js +24 -1
  235. package/dist/esm/modules/excel/stream/workbook-reader.browser.js +14 -0
  236. package/dist/esm/modules/excel/stream/workbook-writer.browser.js +48 -1
  237. package/dist/esm/modules/excel/stream/worksheet-reader.js +17 -1
  238. package/dist/esm/modules/excel/stream/worksheet-writer.js +45 -5
  239. package/dist/esm/modules/excel/table.js +15 -2
  240. package/dist/esm/modules/excel/utils/col-cache.js +15 -0
  241. package/dist/esm/modules/excel/utils/drawing-utils.js +4 -0
  242. package/dist/esm/modules/excel/utils/external-link-formula.js +208 -0
  243. package/dist/esm/modules/excel/utils/iterate-stream.js +3 -1
  244. package/dist/esm/modules/excel/utils/ooxml-paths.js +37 -2
  245. package/dist/esm/modules/excel/utils/shared-strings.js +21 -2
  246. package/dist/esm/modules/excel/utils/workbook-protection.js +30 -0
  247. package/dist/esm/modules/excel/workbook.browser.js +318 -34
  248. package/dist/esm/modules/excel/worksheet.js +21 -2
  249. package/dist/esm/modules/excel/xlsx/rel-type.js +16 -1
  250. package/dist/esm/modules/excel/xlsx/xform/book/defined-name-xform.js +21 -86
  251. package/dist/esm/modules/excel/xlsx/xform/book/external-link-xform.js +330 -0
  252. package/dist/esm/modules/excel/xlsx/xform/book/external-reference-xform.js +24 -0
  253. package/dist/esm/modules/excel/xlsx/xform/book/workbook-calc-properties-xform.js +11 -2
  254. package/dist/esm/modules/excel/xlsx/xform/book/workbook-protection-xform.js +66 -0
  255. package/dist/esm/modules/excel/xlsx/xform/book/workbook-xform.js +38 -5
  256. package/dist/esm/modules/excel/xlsx/xform/core/content-types-xform.js +19 -1
  257. package/dist/esm/modules/excel/xlsx/xform/core/metadata-xform.js +158 -0
  258. package/dist/esm/modules/excel/xlsx/xform/drawing/absolute-anchor-xform.js +105 -0
  259. package/dist/esm/modules/excel/xlsx/xform/drawing/base-cell-anchor-xform.js +3 -0
  260. package/dist/esm/modules/excel/xlsx/xform/drawing/drawing-xform.js +10 -2
  261. package/dist/esm/modules/excel/xlsx/xform/sheet/cell-xform.js +166 -8
  262. package/dist/esm/modules/excel/xlsx/xform/sheet/data-validations-xform.js +1 -1
  263. package/dist/esm/modules/excel/xlsx/xform/sheet/ignored-errors-xform.js +80 -0
  264. package/dist/esm/modules/excel/xlsx/xform/sheet/worksheet-xform.js +9 -4
  265. package/dist/esm/modules/excel/xlsx/xform/style/border-xform.js +4 -1
  266. package/dist/esm/modules/excel/xlsx/xlsx.browser.js +410 -20
  267. package/dist/esm/modules/excel/xlsx/xlsx.js +4 -5
  268. package/dist/esm/modules/formula/compile/address-utils.js +83 -0
  269. package/dist/esm/modules/formula/compile/binder.js +487 -0
  270. package/dist/esm/modules/formula/compile/bound-ast.js +80 -0
  271. package/dist/esm/modules/formula/compile/compiled-formula.js +383 -0
  272. package/dist/esm/modules/formula/compile/dependency-analysis.js +432 -0
  273. package/dist/esm/modules/formula/compile/structured-ref-utils.js +136 -0
  274. package/dist/esm/modules/formula/default-syntax-probe.js +83 -0
  275. package/dist/esm/modules/formula/functions/_date-context.js +29 -0
  276. package/dist/esm/modules/formula/functions/_shared.js +381 -0
  277. package/dist/esm/modules/formula/functions/conditional.js +343 -0
  278. package/dist/esm/modules/formula/functions/database.js +274 -0
  279. package/dist/esm/modules/formula/functions/date.js +855 -0
  280. package/dist/esm/modules/formula/functions/dynamic-array.js +860 -0
  281. package/dist/esm/modules/formula/functions/engineering.js +1128 -0
  282. package/dist/esm/modules/formula/functions/financial.js +2296 -0
  283. package/dist/esm/modules/formula/functions/lookup.js +886 -0
  284. package/dist/esm/modules/formula/functions/math.js +1406 -0
  285. package/dist/esm/modules/formula/functions/statistical.js +3390 -0
  286. package/dist/esm/modules/formula/functions/text.js +1845 -0
  287. package/dist/esm/modules/formula/host-registry.js +69 -0
  288. package/dist/esm/modules/formula/index.js +49 -0
  289. package/dist/esm/modules/formula/install.js +88 -0
  290. package/dist/esm/modules/formula/integration/apply-writeback-plan.js +210 -0
  291. package/dist/esm/modules/formula/integration/calculate-formulas-impl.js +616 -0
  292. package/dist/esm/modules/formula/integration/calculate-formulas.js +68 -0
  293. package/dist/esm/modules/formula/integration/formula-instance.js +79 -0
  294. package/dist/esm/modules/formula/integration/workbook-adapter.js +324 -0
  295. package/dist/esm/modules/formula/integration/workbook-snapshot.js +77 -0
  296. package/dist/esm/modules/formula/materialize/build-writeback-plan.js +473 -0
  297. package/dist/esm/modules/formula/materialize/spill-engine.js +38 -0
  298. package/dist/esm/modules/formula/materialize/types.js +29 -0
  299. package/dist/esm/modules/formula/materialize/writeback-plan.js +27 -0
  300. package/dist/esm/modules/formula/runtime/evaluator.js +2291 -0
  301. package/dist/esm/modules/formula/runtime/function-registry.js +840 -0
  302. package/dist/esm/modules/formula/runtime/values.js +385 -0
  303. package/dist/esm/modules/formula/syntax/ast.js +28 -0
  304. package/dist/esm/modules/formula/syntax/parser.js +439 -0
  305. package/dist/esm/modules/formula/syntax/token-types.js +59 -0
  306. package/dist/esm/modules/formula/syntax/tokenizer.js +1074 -0
  307. package/dist/esm/modules/pdf/excel-bridge.js +9 -0
  308. package/dist/iife/excelts.iife.js +2302 -373
  309. package/dist/iife/excelts.iife.js.map +1 -1
  310. package/dist/iife/excelts.iife.min.js +34 -34
  311. package/dist/types/index.browser.d.ts +1 -1
  312. package/dist/types/index.d.ts +1 -1
  313. package/dist/types/modules/excel/cell.d.ts +17 -3
  314. package/dist/types/modules/excel/defined-names.d.ts +96 -1
  315. package/dist/types/modules/excel/image.d.ts +11 -0
  316. package/dist/types/modules/excel/stream/workbook-reader.browser.d.ts +9 -3
  317. package/dist/types/modules/excel/stream/workbook-reader.d.ts +2 -1
  318. package/dist/types/modules/excel/stream/workbook-writer.browser.d.ts +39 -5
  319. package/dist/types/modules/excel/stream/workbook-writer.d.ts +3 -2
  320. package/dist/types/modules/excel/stream/worksheet-writer.d.ts +39 -6
  321. package/dist/types/modules/excel/types.d.ts +133 -2
  322. package/dist/types/modules/excel/utils/col-cache.d.ts +1 -0
  323. package/dist/types/modules/excel/utils/drawing-utils.d.ts +3 -3
  324. package/dist/types/modules/excel/utils/external-link-formula.d.ts +76 -0
  325. package/dist/types/modules/excel/utils/iterate-stream.d.ts +9 -3
  326. package/dist/types/modules/excel/utils/ooxml-paths.d.ts +19 -0
  327. package/dist/types/modules/excel/utils/shared-strings.d.ts +8 -3
  328. package/dist/types/modules/excel/utils/workbook-protection.d.ts +30 -0
  329. package/dist/types/modules/excel/workbook.browser.d.ts +257 -6
  330. package/dist/types/modules/excel/workbook.d.ts +1 -1
  331. package/dist/types/modules/excel/worksheet.d.ts +3 -1
  332. package/dist/types/modules/excel/xlsx/rel-type.d.ts +15 -0
  333. package/dist/types/modules/excel/xlsx/xform/book/defined-name-xform.d.ts +6 -5
  334. package/dist/types/modules/excel/xlsx/xform/book/external-link-xform.d.ts +84 -0
  335. package/dist/types/modules/excel/xlsx/xform/book/external-reference-xform.d.ts +17 -0
  336. package/dist/types/modules/excel/xlsx/xform/book/workbook-calc-properties-xform.d.ts +3 -0
  337. package/dist/types/modules/excel/xlsx/xform/book/workbook-protection-xform.d.ts +20 -0
  338. package/dist/types/modules/excel/xlsx/xform/core/metadata-xform.d.ts +56 -0
  339. package/dist/types/modules/excel/xlsx/xform/drawing/absolute-anchor-xform.d.ts +26 -0
  340. package/dist/types/modules/excel/xlsx/xform/sheet/cell-xform.d.ts +1 -1
  341. package/dist/types/modules/excel/xlsx/xform/sheet/ignored-errors-xform.d.ts +21 -0
  342. package/dist/types/modules/excel/xlsx/xlsx.browser.d.ts +172 -13
  343. package/dist/types/modules/excel/xlsx/xlsx.d.ts +7 -4
  344. package/dist/types/modules/formula/compile/address-utils.d.ts +62 -0
  345. package/dist/types/modules/formula/compile/binder.d.ts +42 -0
  346. package/dist/types/modules/formula/compile/bound-ast.d.ts +230 -0
  347. package/dist/types/modules/formula/compile/compiled-formula.d.ts +137 -0
  348. package/dist/types/modules/formula/compile/dependency-analysis.d.ts +93 -0
  349. package/dist/types/modules/formula/compile/structured-ref-utils.d.ts +93 -0
  350. package/dist/types/modules/formula/default-syntax-probe.d.ts +79 -0
  351. package/dist/types/modules/formula/functions/_date-context.d.ts +4 -0
  352. package/dist/types/modules/formula/functions/_shared.d.ts +121 -0
  353. package/dist/types/modules/formula/functions/conditional.d.ts +27 -0
  354. package/dist/types/modules/formula/functions/database.d.ts +37 -0
  355. package/dist/types/modules/formula/functions/date.d.ts +61 -0
  356. package/dist/types/modules/formula/functions/dynamic-array.d.ts +23 -0
  357. package/dist/types/modules/formula/functions/engineering.d.ts +57 -0
  358. package/dist/types/modules/formula/functions/financial.d.ts +202 -0
  359. package/dist/types/modules/formula/functions/lookup.d.ts +18 -0
  360. package/dist/types/modules/formula/functions/math.d.ts +114 -0
  361. package/dist/types/modules/formula/functions/statistical.d.ts +193 -0
  362. package/dist/types/modules/formula/functions/text.d.ts +86 -0
  363. package/dist/types/modules/formula/host-registry.d.ts +53 -0
  364. package/dist/types/modules/formula/index.d.ts +39 -0
  365. package/dist/types/modules/formula/install.d.ts +62 -0
  366. package/dist/types/modules/formula/integration/apply-writeback-plan.d.ts +26 -0
  367. package/dist/types/modules/formula/integration/calculate-formulas-impl.d.ts +30 -0
  368. package/dist/types/modules/formula/integration/calculate-formulas.d.ts +67 -0
  369. package/dist/types/modules/formula/integration/formula-instance.d.ts +64 -0
  370. package/dist/types/modules/formula/integration/workbook-adapter.d.ts +26 -0
  371. package/dist/types/modules/formula/integration/workbook-snapshot.d.ts +267 -0
  372. package/dist/types/modules/formula/materialize/build-writeback-plan.d.ts +34 -0
  373. package/dist/types/modules/formula/materialize/spill-engine.d.ts +9 -0
  374. package/dist/types/modules/formula/materialize/types.d.ts +179 -0
  375. package/dist/types/modules/formula/materialize/writeback-plan.d.ts +167 -0
  376. package/dist/types/modules/formula/runtime/evaluator.d.ts +151 -0
  377. package/dist/types/modules/formula/runtime/function-registry.d.ts +47 -0
  378. package/dist/types/modules/formula/runtime/values.d.ts +211 -0
  379. package/dist/types/modules/formula/syntax/ast.d.ts +129 -0
  380. package/dist/types/modules/formula/syntax/parser.d.ts +18 -0
  381. package/dist/types/modules/formula/syntax/token-types.d.ts +153 -0
  382. package/dist/types/modules/formula/syntax/tokenizer.d.ts +10 -0
  383. package/package.json +28 -28
@@ -0,0 +1,1845 @@
1
+ /**
2
+ * Text Functions — Native RuntimeValue implementations.
3
+ */
4
+ import { excelToDate } from "../../../utils/utils.base.js";
5
+ import { RVKind, ERRORS, isError, isArray, toNumberRV, toStringRV, toBooleanRV, topLeft, rvNumber, rvString, rvBoolean, rvArray } from "../runtime/values.js";
6
+ import { isDate1904 } from "./_date-context.js";
7
+ import { argToNumber, checkError, excelWildcardToRegex } from "./_shared.js";
8
+ // ============================================================================
9
+ // Local utility
10
+ // ============================================================================
11
+ function escapeRegex(s) {
12
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
13
+ }
14
+ // ============================================================================
15
+ // CONCATENATE / CONCAT
16
+ // ============================================================================
17
+ export const fnCONCATENATE = args => {
18
+ const parts = [];
19
+ for (const a of args) {
20
+ if (isArray(a)) {
21
+ for (const row of a.rows) {
22
+ for (const cell of row) {
23
+ const err = checkError(cell);
24
+ if (err) {
25
+ return err;
26
+ }
27
+ parts.push(toStringRV(cell));
28
+ }
29
+ }
30
+ }
31
+ else {
32
+ const err = checkError(a);
33
+ if (err) {
34
+ return err;
35
+ }
36
+ parts.push(toStringRV(a));
37
+ }
38
+ }
39
+ return rvString(parts.join(""));
40
+ };
41
+ // CONCAT has the same semantics as CONCATENATE
42
+ export const fnCONCAT = fnCONCATENATE;
43
+ // ============================================================================
44
+ // TEXTJOIN
45
+ // ============================================================================
46
+ export const fnTEXTJOIN = args => {
47
+ if (args.length < 3) {
48
+ return ERRORS.VALUE;
49
+ }
50
+ const e0 = checkError(args[0]);
51
+ if (e0) {
52
+ return e0;
53
+ }
54
+ const delimiter = toStringRV(args[0]);
55
+ const ignoreEmptyRV = toBooleanRV(args[1]);
56
+ if (isError(ignoreEmptyRV)) {
57
+ return ignoreEmptyRV;
58
+ }
59
+ const ignoreEmpty = ignoreEmptyRV.value;
60
+ const parts = [];
61
+ for (let i = 2; i < args.length; i++) {
62
+ const a = args[i];
63
+ if (isArray(a)) {
64
+ for (const row of a.rows) {
65
+ for (const cell of row) {
66
+ const err = checkError(cell);
67
+ if (err) {
68
+ return err;
69
+ }
70
+ const s = toStringRV(cell);
71
+ if (ignoreEmpty && s === "") {
72
+ continue;
73
+ }
74
+ parts.push(s);
75
+ }
76
+ }
77
+ }
78
+ else {
79
+ const err = checkError(a);
80
+ if (err) {
81
+ return err;
82
+ }
83
+ const s = toStringRV(a);
84
+ if (ignoreEmpty && s === "") {
85
+ continue;
86
+ }
87
+ parts.push(s);
88
+ }
89
+ }
90
+ return rvString(parts.join(delimiter));
91
+ };
92
+ // ============================================================================
93
+ // LEFT / RIGHT / MID / LEN
94
+ // ============================================================================
95
+ export const fnLEFT = args => {
96
+ const err = checkError(args[0]);
97
+ if (err) {
98
+ return err;
99
+ }
100
+ const text = toStringRV(args[0]);
101
+ let n;
102
+ if (args.length > 1) {
103
+ // Use `argToNumber` so array arguments get implicit-intersection to
104
+ // their top-left cell before numeric coercion — otherwise
105
+ // `LEFT("abc", A1:A2)` would land in `toNumberRV`'s array path and
106
+ // incorrectly surface #VALUE! instead of using A1.
107
+ const nRV = argToNumber(args[1]);
108
+ if (isError(nRV)) {
109
+ return nRV;
110
+ }
111
+ n = nRV.value;
112
+ }
113
+ else {
114
+ n = 1;
115
+ }
116
+ // Excel rejects negative lengths outright. Without this guard,
117
+ // `text.slice(0, -1)` would silently trim the last character.
118
+ if (n < 0) {
119
+ return ERRORS.VALUE;
120
+ }
121
+ return rvString(text.slice(0, Math.trunc(n)));
122
+ };
123
+ export const fnRIGHT = args => {
124
+ const err = checkError(args[0]);
125
+ if (err) {
126
+ return err;
127
+ }
128
+ const text = toStringRV(args[0]);
129
+ let n;
130
+ if (args.length > 1) {
131
+ // Implicit intersection via `argToNumber` — see LEFT for rationale.
132
+ const nRV = argToNumber(args[1]);
133
+ if (isError(nRV)) {
134
+ return nRV;
135
+ }
136
+ n = nRV.value;
137
+ }
138
+ else {
139
+ n = 1;
140
+ }
141
+ if (n < 0) {
142
+ return ERRORS.VALUE;
143
+ }
144
+ const k = Math.trunc(n);
145
+ if (k === 0) {
146
+ return rvString("");
147
+ }
148
+ return rvString(text.slice(-k));
149
+ };
150
+ export const fnMID = args => {
151
+ const err = checkError(args[0]);
152
+ if (err) {
153
+ return err;
154
+ }
155
+ const text = toStringRV(args[0]);
156
+ // Implicit intersection on both numeric arguments so array inputs
157
+ // collapse to their top-left cells before coercion.
158
+ const startNumRV = argToNumber(args[1]);
159
+ if (isError(startNumRV)) {
160
+ return startNumRV;
161
+ }
162
+ const startNum = Math.trunc(startNumRV.value);
163
+ const numCharsRV = argToNumber(args[2]);
164
+ if (isError(numCharsRV)) {
165
+ return numCharsRV;
166
+ }
167
+ const numChars = Math.trunc(numCharsRV.value);
168
+ // MID: start_num must be >= 1, num_chars must be >= 0.
169
+ if (startNum < 1 || numChars < 0) {
170
+ return ERRORS.VALUE;
171
+ }
172
+ return rvString(text.slice(startNum - 1, startNum - 1 + numChars));
173
+ };
174
+ export const fnLEN = args => {
175
+ const err = checkError(args[0]);
176
+ if (err) {
177
+ return err;
178
+ }
179
+ // `toStringRV` doesn't dereference arrays — passing `A1:A2` would hit
180
+ // its `default: ""` branch and silently return 0. Do an implicit
181
+ // intersection via `topLeft` so `LEN(A1:A2)` behaves like Excel's
182
+ // legacy implicit-intersection semantics (pick the first cell).
183
+ return rvNumber(toStringRV(topLeft(args[0])).length);
184
+ };
185
+ // ============================================================================
186
+ // TRIM / LOWER / UPPER / PROPER
187
+ // ============================================================================
188
+ export const fnTRIM = args => {
189
+ const err = checkError(args[0]);
190
+ if (err) {
191
+ return err;
192
+ }
193
+ // Implicit intersection: turn an array argument into its top-left
194
+ // cell before stringifying, to match Excel's legacy behaviour.
195
+ // Excel's TRIM only collapses plain ASCII space (U+0020), NOT tabs,
196
+ // newlines, or non-breaking space (U+00A0).
197
+ return rvString(toStringRV(topLeft(args[0]))
198
+ .replace(/^ +| +$/g, "")
199
+ .replace(/ +/g, " "));
200
+ };
201
+ export const fnLOWER = args => {
202
+ const err = checkError(args[0]);
203
+ if (err) {
204
+ return err;
205
+ }
206
+ return rvString(toStringRV(topLeft(args[0])).toLowerCase());
207
+ };
208
+ export const fnUPPER = args => {
209
+ const err = checkError(args[0]);
210
+ if (err) {
211
+ return err;
212
+ }
213
+ return rvString(toStringRV(topLeft(args[0])).toUpperCase());
214
+ };
215
+ export const fnPROPER = args => {
216
+ const err = checkError(args[0]);
217
+ if (err) {
218
+ return err;
219
+ }
220
+ return rvString(toStringRV(topLeft(args[0])).replace(/\p{L}+/gu, word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()));
221
+ };
222
+ // ============================================================================
223
+ // SUBSTITUTE / REPLACE
224
+ // ============================================================================
225
+ export const fnSUBSTITUTE = args => {
226
+ const err0 = checkError(args[0]);
227
+ if (err0) {
228
+ return err0;
229
+ }
230
+ const err1 = checkError(args[1]);
231
+ if (err1) {
232
+ return err1;
233
+ }
234
+ const err2 = checkError(args[2]);
235
+ if (err2) {
236
+ return err2;
237
+ }
238
+ const text = toStringRV(topLeft(args[0]));
239
+ const oldText = toStringRV(topLeft(args[1]));
240
+ const newText = toStringRV(topLeft(args[2]));
241
+ // An empty old_text is a no-op in Excel. Without this guard we would
242
+ // `"abc".split("").join(newText)` and insert newText between every
243
+ // character, and the regex path would match empty strings infinitely.
244
+ if (oldText === "") {
245
+ return rvString(text);
246
+ }
247
+ if (args.length > 3) {
248
+ const instanceNumRV = toNumberRV(args[3]);
249
+ if (isError(instanceNumRV)) {
250
+ return instanceNumRV;
251
+ }
252
+ // Excel requires a positive integer; zero, negative, or non-numeric
253
+ // values are #VALUE!. Previously we let the replace pass silently
254
+ // no-op (since `count === 0` never matched), masking caller bugs.
255
+ if (!Number.isFinite(instanceNumRV.value) || instanceNumRV.value < 1) {
256
+ return ERRORS.VALUE;
257
+ }
258
+ const instanceNum = Math.trunc(instanceNumRV.value);
259
+ let count = 0;
260
+ return rvString(text.replace(new RegExp(escapeRegex(oldText), "g"), match => {
261
+ count++;
262
+ return count === instanceNum ? newText : match;
263
+ }));
264
+ }
265
+ return rvString(text.split(oldText).join(newText));
266
+ };
267
+ export const fnREPLACE = args => {
268
+ const err = checkError(args[0]);
269
+ if (err) {
270
+ return err;
271
+ }
272
+ const text = toStringRV(args[0]);
273
+ // Implicit intersection on the numeric arguments — see LEFT.
274
+ const startNumRV = argToNumber(args[1]);
275
+ if (isError(startNumRV)) {
276
+ return startNumRV;
277
+ }
278
+ const startNum = Math.trunc(startNumRV.value);
279
+ const numCharsRV = argToNumber(args[2]);
280
+ if (isError(numCharsRV)) {
281
+ return numCharsRV;
282
+ }
283
+ const numChars = Math.trunc(numCharsRV.value);
284
+ // REPLACE: start_num >= 1, num_chars >= 0. Without this check, negative
285
+ // start_num becomes a slice with negative index and silently trims from
286
+ // the right, which does not match Excel's #VALUE! result.
287
+ if (startNum < 1 || numChars < 0) {
288
+ return ERRORS.VALUE;
289
+ }
290
+ const e3 = checkError(args[3]);
291
+ if (e3) {
292
+ return e3;
293
+ }
294
+ const newText = toStringRV(args[3]);
295
+ return rvString(text.slice(0, startNum - 1) + newText + text.slice(startNum - 1 + numChars));
296
+ };
297
+ // ============================================================================
298
+ // FIND / SEARCH
299
+ // ============================================================================
300
+ export const fnFIND = args => {
301
+ const err0 = checkError(args[0]);
302
+ if (err0) {
303
+ return err0;
304
+ }
305
+ const err1 = checkError(args[1]);
306
+ if (err1) {
307
+ return err1;
308
+ }
309
+ const findText = toStringRV(args[0]);
310
+ const withinText = toStringRV(args[1]);
311
+ let startNum;
312
+ if (args.length > 2) {
313
+ // Implicit intersection so an array supplied as start_num collapses
314
+ // to its top-left cell — matches Excel and the other text family.
315
+ const startNumRV = argToNumber(args[2]);
316
+ if (isError(startNumRV)) {
317
+ return startNumRV;
318
+ }
319
+ startNum = Math.trunc(startNumRV.value);
320
+ }
321
+ else {
322
+ startNum = 1;
323
+ }
324
+ // Excel's FIND rejects start_num outside [1, length(withinText)].
325
+ if (startNum < 1 || startNum > withinText.length + 1) {
326
+ return ERRORS.VALUE;
327
+ }
328
+ const idx = withinText.indexOf(findText, startNum - 1);
329
+ return idx === -1 ? ERRORS.VALUE : rvNumber(idx + 1);
330
+ };
331
+ export const fnSEARCH = args => {
332
+ const err0 = checkError(args[0]);
333
+ if (err0) {
334
+ return err0;
335
+ }
336
+ const err1 = checkError(args[1]);
337
+ if (err1) {
338
+ return err1;
339
+ }
340
+ let findText = toStringRV(args[0]);
341
+ const withinText = toStringRV(args[1]);
342
+ let startNum;
343
+ if (args.length > 2) {
344
+ const startNumRV = argToNumber(args[2]);
345
+ if (isError(startNumRV)) {
346
+ return startNumRV;
347
+ }
348
+ startNum = Math.trunc(startNumRV.value);
349
+ }
350
+ else {
351
+ startNum = 1;
352
+ }
353
+ if (startNum < 1 || startNum > withinText.length + 1) {
354
+ return ERRORS.VALUE;
355
+ }
356
+ // Use the shared Excel-wildcard → regex converter so SEARCH, MATCH,
357
+ // XLOOKUP, and SUMIF/COUNTIF agree on escape semantics (`~*`, `~?`, `~~`).
358
+ const pattern = excelWildcardToRegex(findText);
359
+ try {
360
+ const re = new RegExp(pattern, "i");
361
+ const sub = withinText.slice(startNum - 1);
362
+ const match = re.exec(sub);
363
+ return match ? rvNumber(match.index + startNum) : ERRORS.VALUE;
364
+ }
365
+ catch {
366
+ // If regex is invalid, fall back to simple case-insensitive indexOf.
367
+ findText = findText.toLowerCase();
368
+ const idx = withinText.toLowerCase().indexOf(findText, startNum - 1);
369
+ return idx === -1 ? ERRORS.VALUE : rvNumber(idx + 1);
370
+ }
371
+ };
372
+ // ============================================================================
373
+ // REPT
374
+ // ============================================================================
375
+ export const fnREPT = args => {
376
+ const err = checkError(args[0]);
377
+ if (err) {
378
+ return err;
379
+ }
380
+ const text = toStringRV(topLeft(args[0]));
381
+ const timesRV = toNumberRV(args[1]);
382
+ if (isError(timesRV)) {
383
+ return timesRV;
384
+ }
385
+ const times = Math.floor(timesRV.value);
386
+ if (times < 0) {
387
+ return ERRORS.VALUE;
388
+ }
389
+ // Excel caps the result at 32767 characters; we additionally bail out
390
+ // early on huge products so the engine can't be DoS'd into allocating
391
+ // a multi-gigabyte string. (R6-P1-4)
392
+ const total = text.length * times;
393
+ if (total > 32767) {
394
+ return ERRORS.VALUE;
395
+ }
396
+ return rvString(text.repeat(times));
397
+ };
398
+ // ============================================================================
399
+ // TEXT (complex number/date formatting)
400
+ // ============================================================================
401
+ export const fnTEXT = args => {
402
+ const rawVal = topLeft(args[0]);
403
+ if (isError(rawVal)) {
404
+ return rawVal;
405
+ }
406
+ const e1 = checkError(args[1]);
407
+ if (e1) {
408
+ return e1;
409
+ }
410
+ const fmt = toStringRV(args[1]);
411
+ // "@" format = return text as-is
412
+ if (fmt === "@") {
413
+ return rvString(toStringRV(rawVal));
414
+ }
415
+ // Conditional sections: positive;negative;zero;text (up to 4 parts).
416
+ // Text input — when the value is a String/Boolean that doesn't parse
417
+ // as a number — is routed to the 4th section if present. Without this
418
+ // early dispatch, the later `toNumberRV(rawVal)` would #VALUE! and
419
+ // `TEXT("hi", "0;-0;0;@")` could never reach the 4th section.
420
+ const sections = splitFormatSections(fmt);
421
+ const isTextInput = rawVal.kind === RVKind.String;
422
+ if (isTextInput && sections.length >= 4) {
423
+ // The 4th section formats the text value — `@` is a placeholder
424
+ // that re-emits the source string (like Excel's `@` metacharacter).
425
+ return rvString(sections[3].replace(/@/g, toStringRV(rawVal)));
426
+ }
427
+ const valRV = toNumberRV(rawVal);
428
+ if (isError(valRV)) {
429
+ return valRV;
430
+ }
431
+ const val = valRV.value;
432
+ let activeFmt;
433
+ if (sections.length >= 3) {
434
+ activeFmt = val > 0 ? sections[0] : val < 0 ? sections[1] : sections[2];
435
+ }
436
+ else if (sections.length === 2) {
437
+ activeFmt = val >= 0 ? sections[0] : sections[1];
438
+ }
439
+ else {
440
+ activeFmt = sections[0];
441
+ }
442
+ // For negative section, use absolute value (sign is in the format)
443
+ const useVal = sections.length >= 2 && val < 0 ? Math.abs(val) : val;
444
+ return rvString(formatWithCode(useVal, activeFmt, rawVal));
445
+ };
446
+ /**
447
+ * Split format string on `;` separators, respecting quoted strings.
448
+ */
449
+ function splitFormatSections(fmt) {
450
+ const sections = [];
451
+ let current = "";
452
+ let inQuotes = false;
453
+ for (let i = 0; i < fmt.length; i++) {
454
+ if (fmt[i] === '"') {
455
+ inQuotes = !inQuotes;
456
+ current += fmt[i];
457
+ }
458
+ else if (fmt[i] === ";" && !inQuotes) {
459
+ sections.push(current);
460
+ current = "";
461
+ }
462
+ else {
463
+ current += fmt[i];
464
+ }
465
+ }
466
+ sections.push(current);
467
+ return sections;
468
+ }
469
+ /**
470
+ * Format a number using a single format code section.
471
+ */
472
+ function formatWithCode(val, fmt, rawVal) {
473
+ const upper = fmt.toUpperCase();
474
+ // Date/time formats — detect by presence of date/time tokens
475
+ if (upper.includes("YYYY") ||
476
+ upper.includes("YY") ||
477
+ upper.includes("MMMM") ||
478
+ upper.includes("MMM") ||
479
+ upper.includes("DDDD") ||
480
+ upper.includes("DDD") ||
481
+ upper.includes("DD") ||
482
+ /(?:^|[^H])MM(?!M)/.test(upper) ||
483
+ /(?:^|[^M])M(?!M)/.test(upper.replace(/MMMM|MMM/g, "")) ||
484
+ upper.includes("HH") ||
485
+ /(?:^|[^H])H(?!H)/.test(upper) ||
486
+ upper.includes("SS") ||
487
+ upper.includes("AM/PM") ||
488
+ upper.includes("A/P")) {
489
+ return formatDate(val, fmt);
490
+ }
491
+ // Percentage format
492
+ if (fmt.includes("%")) {
493
+ const stripped = fmt.replace(/[^0#.%,]/g, "");
494
+ const dotIdx = stripped.indexOf(".");
495
+ const afterDot = dotIdx >= 0
496
+ ? stripped
497
+ .slice(dotIdx + 1)
498
+ .replace(/%/g, "")
499
+ .replace(/,/g, "")
500
+ : "";
501
+ const decimals = afterDot.length;
502
+ const pctVal = val * 100;
503
+ let result = pctVal.toFixed(decimals);
504
+ if (fmt.includes(",")) {
505
+ const parts = result.split(".");
506
+ parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",");
507
+ result = parts.join(".");
508
+ }
509
+ return result + "%";
510
+ }
511
+ // Scientific notation: 0.00E+00
512
+ if (/[0#]\.?[0#]*E[+-][0#]+/i.test(fmt)) {
513
+ return formatScientific(val, fmt);
514
+ }
515
+ // Fraction format: # ?/? or # ??/??
516
+ if (/[?]+\/[?]+/.test(fmt)) {
517
+ return formatFraction(val, fmt);
518
+ }
519
+ // Number format with 0 and #
520
+ if (fmt.includes("0") || fmt.includes("#")) {
521
+ return formatNumber(val, fmt);
522
+ }
523
+ // Literal-only section (no numeric placeholders). Excel emits the
524
+ // format string verbatim, substituting any `@` with the stringified
525
+ // value. This matters for 3- and 4-section formats like
526
+ // `"pos;neg;zero"` — the selected section has no `#`/`0` but should
527
+ // still appear in the output.
528
+ return fmt.replace(/@/g, String(val));
529
+ }
530
+ /**
531
+ * Format number using #, 0, comma patterns.
532
+ */
533
+ /**
534
+ * Format number using #, 0, comma patterns.
535
+ *
536
+ * The implementation tokenises the pattern left-to-right so that *any*
537
+ * literal character (parentheses, `+`/`-`, currency symbols, letters in
538
+ * quoted `"..."` segments, backslash-escaped characters, leading/trailing
539
+ * punctuation) is preserved in its original position. The previous
540
+ * implementation stripped everything that wasn't `0 # . ,` before analysis
541
+ * and tried to re-inject a handful of literals afterwards, which caused
542
+ * formats like `0;(0)` (→ "(5)" for -5) and `#,##0.00;-#,##0.00` to drop
543
+ * their negative-section literal characters entirely.
544
+ */
545
+ function formatNumber(val, fmt) {
546
+ // Strip `[color]` and `[condition]` tags up front (Excel-style
547
+ // decorations that don't affect the numeric layout in our engine).
548
+ const cleanFmt = fmt.replace(/\[[^\]]*\]/g, "");
549
+ const tokens = [];
550
+ for (let i = 0; i < cleanFmt.length; i++) {
551
+ const ch = cleanFmt[i];
552
+ if (ch === '"') {
553
+ // Quoted literal run: consume until the closing quote.
554
+ let j = i + 1;
555
+ let buf = "";
556
+ while (j < cleanFmt.length && cleanFmt[j] !== '"') {
557
+ buf += cleanFmt[j];
558
+ j++;
559
+ }
560
+ tokens.push({ kind: "literal", text: buf });
561
+ i = j; // skip closing quote
562
+ continue;
563
+ }
564
+ if (ch === "\\" && i + 1 < cleanFmt.length) {
565
+ // Backslash escape: next character is a literal.
566
+ tokens.push({ kind: "literal", text: cleanFmt[i + 1] });
567
+ i++;
568
+ continue;
569
+ }
570
+ if (ch === "0" || ch === "#") {
571
+ tokens.push({ kind: "digit", char: ch });
572
+ continue;
573
+ }
574
+ if (ch === ".") {
575
+ tokens.push({ kind: "dot" });
576
+ continue;
577
+ }
578
+ if (ch === ",") {
579
+ tokens.push({ kind: "comma" });
580
+ continue;
581
+ }
582
+ // Any other character is a literal (space, parentheses, currency
583
+ // symbol, sign, hyphen, etc.).
584
+ tokens.push({ kind: "literal", text: ch });
585
+ }
586
+ // Count integer/fraction digit slots and decide whether to group.
587
+ let sawDot = false;
588
+ const intDigitSlots = [];
589
+ const fracDigitSlots = [];
590
+ let hasGrouping = false;
591
+ for (const t of tokens) {
592
+ if (t.kind === "dot") {
593
+ sawDot = true;
594
+ continue;
595
+ }
596
+ if (t.kind === "digit") {
597
+ if (sawDot) {
598
+ fracDigitSlots.push(t.char);
599
+ }
600
+ else {
601
+ intDigitSlots.push(t.char);
602
+ }
603
+ continue;
604
+ }
605
+ if (t.kind === "comma" && !sawDot) {
606
+ // A comma between digit slots means "thousands grouping".
607
+ hasGrouping = true;
608
+ }
609
+ }
610
+ if (intDigitSlots.length === 0 && fracDigitSlots.length === 0) {
611
+ return fmt; // Nothing to format; return the (raw) pattern.
612
+ }
613
+ // Round to the requested fractional precision.
614
+ const rounded = roundHalfAwayFromZeroFmt(val, fracDigitSlots.length);
615
+ const sign = rounded < 0 ? "-" : "";
616
+ const absStr = Math.abs(rounded).toFixed(fracDigitSlots.length);
617
+ const [intPartRaw, fracPart = ""] = absStr.split(".");
618
+ let intPart = intPartRaw;
619
+ // Pad the integer part to satisfy mandatory `0` slots.
620
+ const mandatoryInt = intDigitSlots.filter(s => s === "0").length;
621
+ if (intPart.length < mandatoryInt) {
622
+ intPart = intPart.padStart(mandatoryInt, "0");
623
+ }
624
+ // Apply thousand grouping if the pattern requested it.
625
+ let groupedInt = intPart;
626
+ if (hasGrouping) {
627
+ groupedInt = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
628
+ }
629
+ // Walk the tokens again and materialise output, interleaving the
630
+ // integer run, decimal point, and fractional run with literals.
631
+ let out = "";
632
+ let intCursor = 0; // index into groupedInt (right-to-left on integer digits)
633
+ // We emit integer digits in source order using a trick: scan tokens
634
+ // left-to-right; the first integer digit slot consumes `groupedInt[0]`
635
+ // if integer has more digits than slots all extras come out before the
636
+ // first digit slot (Excel overflow rule).
637
+ const totalIntSlots = intDigitSlots.length;
638
+ // Determine how much "overflow" we need to prepend. Overflow digits
639
+ // appear right before the first digit token we encounter.
640
+ const overflowDigits = Math.max(0, groupedInt.length - totalIntSlots);
641
+ let fracCursor = 0;
642
+ let emittedOverflow = false;
643
+ let firstDigitTokenIdx = -1;
644
+ for (let k = 0; k < tokens.length; k++) {
645
+ if (tokens[k].kind === "digit") {
646
+ // We only care about the first *integer* digit token — one that
647
+ // comes before the dot. If the pattern has no integer slots, we
648
+ // still need to emit any overflow somewhere; pick the first digit
649
+ // token regardless.
650
+ firstDigitTokenIdx = k;
651
+ break;
652
+ }
653
+ if (tokens[k].kind === "dot") {
654
+ break;
655
+ }
656
+ }
657
+ for (let k = 0; k < tokens.length; k++) {
658
+ const t = tokens[k];
659
+ if (t.kind === "literal") {
660
+ out += t.text;
661
+ continue;
662
+ }
663
+ if (t.kind === "dot") {
664
+ if (fracDigitSlots.length > 0 || fracPart.length > 0) {
665
+ out += ".";
666
+ }
667
+ continue;
668
+ }
669
+ if (t.kind === "comma") {
670
+ // Grouping commas are consumed by the integer-build step above,
671
+ // so trailing commas here are literal (unusual but valid).
672
+ continue;
673
+ }
674
+ // digit token
675
+ if (sawDotAt(tokens, k)) {
676
+ // fractional slot
677
+ if (fracCursor < fracPart.length) {
678
+ out += fracPart[fracCursor];
679
+ }
680
+ else if (t.char === "0") {
681
+ out += "0";
682
+ }
683
+ fracCursor++;
684
+ continue;
685
+ }
686
+ // integer slot
687
+ if (k === firstDigitTokenIdx && !emittedOverflow) {
688
+ if (overflowDigits > 0) {
689
+ out += groupedInt.slice(0, overflowDigits);
690
+ }
691
+ emittedOverflow = true;
692
+ }
693
+ const slotIdx = intCursor;
694
+ // Integer slots map right-to-left onto `groupedInt`'s right-to-left
695
+ // ordering. We'll compute the source character for this slot from
696
+ // the right edge.
697
+ const srcIdx = overflowDigits + slotIdx;
698
+ if (srcIdx < groupedInt.length) {
699
+ out += groupedInt[srcIdx];
700
+ }
701
+ else if (t.char === "0") {
702
+ out += "0";
703
+ }
704
+ intCursor++;
705
+ }
706
+ return sign + out;
707
+ }
708
+ /**
709
+ * Did a dot token appear at any index < k in `tokens`? Used to classify
710
+ * each digit slot as integer vs fractional during the emit pass.
711
+ */
712
+ function sawDotAt(tokens, k) {
713
+ for (let i = 0; i < k; i++) {
714
+ if (tokens[i].kind === "dot") {
715
+ return true;
716
+ }
717
+ }
718
+ return false;
719
+ }
720
+ /**
721
+ * Round `val` to `decimals` fractional digits using Excel's half-away-
722
+ * from-zero convention (the same rule `formatNumber`'s caller relies on
723
+ * to keep `-0.5` displaying as `"-1"` instead of `"0"`).
724
+ */
725
+ function roundHalfAwayFromZeroFmt(val, decimals) {
726
+ if (!isFinite(val) || decimals < 0) {
727
+ return val;
728
+ }
729
+ const factor = Math.pow(10, decimals);
730
+ return ((val < 0 ? -1 : 1) * Math.round(Math.abs(val) * factor)) / factor;
731
+ }
732
+ /**
733
+ * Format a number in scientific notation: 0.00E+00
734
+ */
735
+ function formatScientific(val, fmt) {
736
+ const upper = fmt.toUpperCase();
737
+ const eIdx = upper.indexOf("E");
738
+ const beforeE = fmt.slice(0, eIdx);
739
+ const afterE = fmt.slice(eIdx + 1);
740
+ // Count decimal places in mantissa
741
+ const dotIdx = beforeE.indexOf(".");
742
+ const mantissaDecimals = dotIdx >= 0 ? beforeE.slice(dotIdx + 1).replace(/[^0#]/g, "").length : 0;
743
+ // Count exponent digits
744
+ const expSign = afterE[0] === "+" || afterE[0] === "-" ? afterE[0] : "+";
745
+ const expDigits = afterE.replace(/[^0#]/g, "").length;
746
+ const exp = val === 0 ? 0 : Math.floor(Math.log10(Math.abs(val)));
747
+ const mantissa = val / Math.pow(10, exp);
748
+ const expStr = (exp >= 0 && expSign === "+" ? "+" : exp < 0 ? "-" : "") +
749
+ String(Math.abs(exp)).padStart(expDigits, "0");
750
+ return mantissa.toFixed(mantissaDecimals) + "E" + expStr;
751
+ }
752
+ /**
753
+ * Format a number as a fraction: "# ?/?" or "# ??/??"
754
+ */
755
+ function formatFraction(val, fmt) {
756
+ const whole = Math.floor(Math.abs(val));
757
+ const frac = Math.abs(val) - whole;
758
+ const sign = val < 0 ? "-" : "";
759
+ if (frac === 0) {
760
+ if (fmt.includes("#") || fmt.includes("0")) {
761
+ return sign + whole + " ";
762
+ }
763
+ return sign + whole + " 0/1";
764
+ }
765
+ // Determine max denominator from ? count
766
+ const slashIdx = fmt.indexOf("/");
767
+ const denomPattern = fmt.slice(slashIdx + 1).replace(/[^?0#]/g, "");
768
+ const maxDenom = Math.pow(10, denomPattern.length) - 1;
769
+ // Find best fraction approximation
770
+ let bestNum = 0;
771
+ let bestDen = 1;
772
+ let bestError = Math.abs(frac);
773
+ for (let d = 1; d <= maxDenom; d++) {
774
+ const n = Math.round(frac * d);
775
+ const error = Math.abs(frac - n / d);
776
+ if (error < bestError) {
777
+ bestError = error;
778
+ bestNum = n;
779
+ bestDen = d;
780
+ if (error === 0) {
781
+ break;
782
+ }
783
+ }
784
+ }
785
+ const hasWholePart = fmt.indexOf("?") > 0 && fmt[0] !== "?" && fmt[0] !== "/";
786
+ if (hasWholePart) {
787
+ const numStr = String(bestNum).padStart(denomPattern.length, " ");
788
+ const denStr = String(bestDen).padStart(denomPattern.length, " ");
789
+ return sign + (whole > 0 ? whole + " " : "") + numStr + "/" + denStr;
790
+ }
791
+ return sign + (whole * bestDen + bestNum) + "/" + bestDen;
792
+ }
793
+ /**
794
+ * Day name arrays for date formatting.
795
+ */
796
+ const DAY_NAMES_FULL = [
797
+ "Sunday",
798
+ "Monday",
799
+ "Tuesday",
800
+ "Wednesday",
801
+ "Thursday",
802
+ "Friday",
803
+ "Saturday"
804
+ ];
805
+ const DAY_NAMES_SHORT = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
806
+ const MONTH_NAMES_FULL = [
807
+ "January",
808
+ "February",
809
+ "March",
810
+ "April",
811
+ "May",
812
+ "June",
813
+ "July",
814
+ "August",
815
+ "September",
816
+ "October",
817
+ "November",
818
+ "December"
819
+ ];
820
+ const MONTH_NAMES_SHORT = [
821
+ "Jan",
822
+ "Feb",
823
+ "Mar",
824
+ "Apr",
825
+ "May",
826
+ "Jun",
827
+ "Jul",
828
+ "Aug",
829
+ "Sep",
830
+ "Oct",
831
+ "Nov",
832
+ "Dec"
833
+ ];
834
+ /**
835
+ * Format a numeric Excel serial date with a date/time format code.
836
+ *
837
+ * In the native value system, dates are always numbers (serial values).
838
+ * We always use excelToDate() to convert and then read UTC fields so the
839
+ * output does not drift by a day in timezones west of UTC.
840
+ */
841
+ /**
842
+ * Format a serial-date value using an Excel-style pattern.
843
+ *
844
+ * Unlike a naive multi-pass regex approach, this implementation tokenises
845
+ * the pattern in a single left-to-right sweep and disambiguates the
846
+ * overloaded `m` / `mm` token by looking at its neighbours:
847
+ *
848
+ * - `mm` after `h:` or before `:ss` is rendered as **minutes**
849
+ * - otherwise `mm` is rendered as **month**
850
+ *
851
+ * Without this, a mixed format like `"yyyy-mm-dd hh:mm:ss"` would render
852
+ * the time's minutes as months (both tokens fire the same regex), giving
853
+ * garbage like `"2023-06-15 14:06:45"` for a timestamp at 14:30:45.
854
+ */
855
+ function formatDate(val, fmt) {
856
+ const d = excelToDate(val, isDate1904());
857
+ const year = d.getUTCFullYear();
858
+ const month0 = d.getUTCMonth();
859
+ const dayN = d.getUTCDate();
860
+ const dow = d.getUTCDay();
861
+ const fracDay = val % 1;
862
+ const totalSeconds = Math.round(Math.abs(fracDay) * 86400);
863
+ const hours = Math.floor(totalSeconds / 3600) % 24;
864
+ const minutes = Math.floor(totalSeconds / 60) % 60;
865
+ const seconds = totalSeconds % 60;
866
+ const hasAmPmToken = /AM\/PM/i.test(fmt) || /A\/P/i.test(fmt);
867
+ const h12 = hours % 12 === 0 ? 12 : hours % 12;
868
+ const ampm = hours < 12 ? "AM" : "PM";
869
+ const ap = hours < 12 ? "A" : "P";
870
+ const runs = [];
871
+ let i = 0;
872
+ while (i < fmt.length) {
873
+ const ch = fmt[i];
874
+ const lo = ch.toLowerCase();
875
+ // Handle two-char literals that must not tokenise as letters
876
+ if (fmt.slice(i, i + 5).toUpperCase() === "AM/PM") {
877
+ runs.push({ kind: "literal", text: ampm });
878
+ i += 5;
879
+ continue;
880
+ }
881
+ if (fmt.slice(i, i + 3).toUpperCase() === "A/P") {
882
+ runs.push({ kind: "literal", text: ap });
883
+ i += 3;
884
+ continue;
885
+ }
886
+ if (lo === "y" || lo === "m" || lo === "d" || lo === "h" || lo === "s") {
887
+ let j = i + 1;
888
+ while (j < fmt.length && fmt[j].toLowerCase() === lo) {
889
+ j++;
890
+ }
891
+ runs.push({ kind: "letters", lower: lo, count: j - i });
892
+ i = j;
893
+ continue;
894
+ }
895
+ // Literal: consume until the next token-letter (to keep literal runs
896
+ // contiguous and reduce array churn).
897
+ let j = i + 1;
898
+ while (j < fmt.length) {
899
+ const nx = fmt[j].toLowerCase();
900
+ if (nx === "y" || nx === "m" || nx === "d" || nx === "h" || nx === "s") {
901
+ break;
902
+ }
903
+ if (fmt.slice(j, j + 5).toUpperCase() === "AM/PM") {
904
+ break;
905
+ }
906
+ if (fmt.slice(j, j + 3).toUpperCase() === "A/P") {
907
+ break;
908
+ }
909
+ j++;
910
+ }
911
+ runs.push({ kind: "literal", text: fmt.slice(i, j) });
912
+ i = j;
913
+ }
914
+ // Phase 2: render each run, using neighbour context to decide whether
915
+ // an `m` / `mm` run means minute (when adjacent to an `h` or `s` run)
916
+ // or month (otherwise).
917
+ let out = "";
918
+ for (let r = 0; r < runs.length; r++) {
919
+ const run = runs[r];
920
+ if (run.kind === "literal") {
921
+ out += run.text;
922
+ continue;
923
+ }
924
+ switch (run.lower) {
925
+ case "y":
926
+ out += run.count >= 4 ? String(year).padStart(4, "0") : String(year).slice(-2);
927
+ break;
928
+ case "d":
929
+ if (run.count >= 4) {
930
+ out += DAY_NAMES_FULL[dow];
931
+ }
932
+ else if (run.count === 3) {
933
+ out += DAY_NAMES_SHORT[dow];
934
+ }
935
+ else if (run.count === 2) {
936
+ out += String(dayN).padStart(2, "0");
937
+ }
938
+ else {
939
+ out += String(dayN);
940
+ }
941
+ break;
942
+ case "h":
943
+ out +=
944
+ run.count >= 2
945
+ ? String(hasAmPmToken ? h12 : hours).padStart(2, "0")
946
+ : String(hasAmPmToken ? h12 : hours);
947
+ break;
948
+ case "s":
949
+ out += run.count >= 2 ? String(seconds).padStart(2, "0") : String(seconds);
950
+ break;
951
+ case "m": {
952
+ // Month-names render independent of context.
953
+ if (run.count === 4) {
954
+ out += MONTH_NAMES_FULL[month0];
955
+ break;
956
+ }
957
+ if (run.count === 3) {
958
+ out += MONTH_NAMES_SHORT[month0];
959
+ break;
960
+ }
961
+ // Otherwise `m` / `mm` is month-or-minute. Follow Excel's rule:
962
+ // treat as minute iff an adjacent letter-run is `h` or `s`.
963
+ const isMinute = isAdjacentTimeContext(runs, r);
964
+ const value = isMinute ? minutes : month0 + 1;
965
+ out += run.count >= 2 ? String(value).padStart(2, "0") : String(value);
966
+ break;
967
+ }
968
+ }
969
+ }
970
+ return out;
971
+ }
972
+ /**
973
+ * Determine whether the `m` / `mm` run at `runs[idx]` should render as
974
+ * minutes (true) or months (false). Excel's rule, simplified: minutes
975
+ * when the immediately preceding or following *letter* run is `h` or
976
+ * `s`, ignoring intervening literal separators like `:`.
977
+ */
978
+ function isAdjacentTimeContext(runs, idx) {
979
+ for (let j = idx - 1; j >= 0; j--) {
980
+ const p = runs[j];
981
+ if (p.kind === "letters") {
982
+ if (p.lower === "h" || p.lower === "s") {
983
+ return true;
984
+ }
985
+ break;
986
+ }
987
+ }
988
+ for (let j = idx + 1; j < runs.length; j++) {
989
+ const p = runs[j];
990
+ if (p.kind === "letters") {
991
+ if (p.lower === "h" || p.lower === "s") {
992
+ return true;
993
+ }
994
+ break;
995
+ }
996
+ }
997
+ return false;
998
+ }
999
+ // ============================================================================
1000
+ // VALUE / EXACT
1001
+ // ============================================================================
1002
+ export const fnVALUE = args => {
1003
+ const err = checkError(args[0]);
1004
+ if (err) {
1005
+ return err;
1006
+ }
1007
+ // Delegate to the central numeric-string parser. It rejects empty /
1008
+ // whitespace-only / Infinity / NaN / hex forms the way Excel does, which
1009
+ // a naive `Number(s)` would accept silently.
1010
+ return toNumberRV(topLeft(args[0]));
1011
+ };
1012
+ export const fnEXACT = args => {
1013
+ const err0 = checkError(args[0]);
1014
+ if (err0) {
1015
+ return err0;
1016
+ }
1017
+ const err1 = checkError(args[1]);
1018
+ if (err1) {
1019
+ return err1;
1020
+ }
1021
+ return rvBoolean(toStringRV(args[0]) === toStringRV(args[1]));
1022
+ };
1023
+ // ============================================================================
1024
+ // Additional Text Functions
1025
+ // ============================================================================
1026
+ export const fnCODE = args => {
1027
+ const err = checkError(args[0]);
1028
+ if (err) {
1029
+ return err;
1030
+ }
1031
+ const text = toStringRV(topLeft(args[0]));
1032
+ return text.length > 0 ? rvNumber(text.charCodeAt(0)) : ERRORS.VALUE;
1033
+ };
1034
+ export const fnCHAR = args => {
1035
+ const err = checkError(args[0]);
1036
+ if (err) {
1037
+ return err;
1038
+ }
1039
+ const nRV = toNumberRV(topLeft(args[0]));
1040
+ if (isError(nRV)) {
1041
+ return nRV;
1042
+ }
1043
+ // Excel's CHAR accepts integers in [1, 255] only; outside the ANSI range
1044
+ // it returns #VALUE!. We also truncate fractional inputs toward zero to
1045
+ // match Excel's coercion semantics.
1046
+ const code = Math.trunc(nRV.value);
1047
+ if (code < 1 || code > 255) {
1048
+ return ERRORS.VALUE;
1049
+ }
1050
+ return rvString(String.fromCharCode(code));
1051
+ };
1052
+ export const fnCLEAN = args => {
1053
+ const err = checkError(args[0]);
1054
+ if (err) {
1055
+ return err;
1056
+ }
1057
+ const text = toStringRV(topLeft(args[0]));
1058
+ // Remove non-printable ASCII control characters (0x00-0x1F)
1059
+ let result = "";
1060
+ for (let i = 0; i < text.length; i++) {
1061
+ if (text.charCodeAt(i) >= 32) {
1062
+ result += text[i];
1063
+ }
1064
+ }
1065
+ return rvString(result);
1066
+ };
1067
+ export const fnT = args => {
1068
+ const v = topLeft(args[0]);
1069
+ if (isError(v)) {
1070
+ return v;
1071
+ }
1072
+ return v.kind === RVKind.String ? v : rvString("");
1073
+ };
1074
+ // ============================================================================
1075
+ // More Text Functions
1076
+ // ============================================================================
1077
+ export const fnUNICHAR = args => {
1078
+ const err = checkError(args[0]);
1079
+ if (err) {
1080
+ return err;
1081
+ }
1082
+ const nRV = toNumberRV(args[0]);
1083
+ if (isError(nRV)) {
1084
+ return nRV;
1085
+ }
1086
+ const code = Math.floor(nRV.value);
1087
+ if (code < 1) {
1088
+ return ERRORS.VALUE;
1089
+ }
1090
+ try {
1091
+ return rvString(String.fromCodePoint(code));
1092
+ }
1093
+ catch {
1094
+ return ERRORS.VALUE;
1095
+ }
1096
+ };
1097
+ export const fnUNICODE = args => {
1098
+ const err = checkError(args[0]);
1099
+ if (err) {
1100
+ return err;
1101
+ }
1102
+ const text = toStringRV(args[0]);
1103
+ if (text.length === 0) {
1104
+ return ERRORS.VALUE;
1105
+ }
1106
+ const cp = text.codePointAt(0);
1107
+ return cp !== undefined ? rvNumber(cp) : ERRORS.VALUE;
1108
+ };
1109
+ export const fnBAHTTEXT = args => {
1110
+ const err = checkError(args[0]);
1111
+ if (err) {
1112
+ return err;
1113
+ }
1114
+ return rvString(toStringRV(args[0]));
1115
+ };
1116
+ export const fnDOLLAR = args => {
1117
+ const numRV = toNumberRV(args[0]);
1118
+ if (isError(numRV)) {
1119
+ return numRV;
1120
+ }
1121
+ const num = numRV.value;
1122
+ let decimals;
1123
+ if (args.length > 1) {
1124
+ const decRV = toNumberRV(args[1]);
1125
+ if (isError(decRV)) {
1126
+ return decRV;
1127
+ }
1128
+ decimals = decRV.value;
1129
+ }
1130
+ else {
1131
+ decimals = 2;
1132
+ }
1133
+ const d = Math.floor(decimals);
1134
+ let rounded;
1135
+ if (d < 0) {
1136
+ const factor = Math.pow(10, -d);
1137
+ rounded = Math.round(Math.abs(num) / factor) * factor;
1138
+ }
1139
+ else {
1140
+ rounded = Math.abs(num);
1141
+ }
1142
+ const formatted = rounded.toFixed(Math.max(0, d));
1143
+ const parts = formatted.split(".");
1144
+ parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",");
1145
+ const result = parts.join(".");
1146
+ return rvString(num < 0 ? `($${result})` : `$${result}`);
1147
+ };
1148
+ export const fnFIXED = args => {
1149
+ const numRV = toNumberRV(args[0]);
1150
+ if (isError(numRV)) {
1151
+ return numRV;
1152
+ }
1153
+ const num = numRV.value;
1154
+ let decimals;
1155
+ if (args.length > 1) {
1156
+ const decRV = toNumberRV(args[1]);
1157
+ if (isError(decRV)) {
1158
+ return decRV;
1159
+ }
1160
+ decimals = decRV.value;
1161
+ }
1162
+ else {
1163
+ decimals = 2;
1164
+ }
1165
+ let noCommas;
1166
+ if (args.length > 2) {
1167
+ const ncRV = toBooleanRV(args[2]);
1168
+ if (isError(ncRV)) {
1169
+ return ncRV;
1170
+ }
1171
+ noCommas = ncRV.value;
1172
+ }
1173
+ else {
1174
+ noCommas = false;
1175
+ }
1176
+ const d = Math.floor(decimals);
1177
+ let rounded;
1178
+ if (d < 0) {
1179
+ const factor = Math.pow(10, -d);
1180
+ rounded = Math.round(num / factor) * factor;
1181
+ }
1182
+ else {
1183
+ rounded = num;
1184
+ }
1185
+ let result = rounded.toFixed(Math.max(0, d));
1186
+ if (!noCommas) {
1187
+ const parts = result.split(".");
1188
+ parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",");
1189
+ result = parts.join(".");
1190
+ }
1191
+ return rvString(result);
1192
+ };
1193
+ export const fnASC = args => {
1194
+ const err = checkError(args[0]);
1195
+ if (err) {
1196
+ return err;
1197
+ }
1198
+ const text = toStringRV(args[0]);
1199
+ return rvString(text.replace(/[\uFF01-\uFF5E]/g, ch => String.fromCharCode(ch.charCodeAt(0) - 0xfee0)));
1200
+ };
1201
+ export const fnDBCS = args => {
1202
+ const err = checkError(args[0]);
1203
+ if (err) {
1204
+ return err;
1205
+ }
1206
+ const text = toStringRV(args[0]);
1207
+ return rvString(text.replace(/[!-~]/g, ch => String.fromCharCode(ch.charCodeAt(0) + 0xfee0)));
1208
+ };
1209
+ export const fnJIS = args => fnDBCS(args);
1210
+ export const fnPHONETIC = args => {
1211
+ const err = checkError(args[0]);
1212
+ if (err) {
1213
+ return err;
1214
+ }
1215
+ // Implicit intersection: array → top-left cell, matching the rest of
1216
+ // the text-function family.
1217
+ return rvString(toStringRV(topLeft(args[0])));
1218
+ };
1219
+ export const fnNUMBERVALUE = args => {
1220
+ const e0 = checkError(args[0]);
1221
+ if (e0) {
1222
+ return e0;
1223
+ }
1224
+ let text = toStringRV(args[0]);
1225
+ let decSep = ".";
1226
+ if (args.length > 1) {
1227
+ const e1 = checkError(args[1]);
1228
+ if (e1) {
1229
+ return e1;
1230
+ }
1231
+ decSep = toStringRV(args[1]);
1232
+ }
1233
+ let grpSep = ",";
1234
+ if (args.length > 2) {
1235
+ const e2 = checkError(args[2]);
1236
+ if (e2) {
1237
+ return e2;
1238
+ }
1239
+ grpSep = toStringRV(args[2]);
1240
+ }
1241
+ text = text.split(grpSep).join("");
1242
+ if (decSep !== ".") {
1243
+ text = text.replace(decSep, ".");
1244
+ }
1245
+ // Handle percentage
1246
+ const isPct = text.endsWith("%");
1247
+ if (isPct) {
1248
+ text = text.slice(0, -1);
1249
+ }
1250
+ // `Number("")` is 0, not NaN — reject empty / whitespace-only inputs
1251
+ // so `NUMBERVALUE("")` does not silently produce 0 (R6-P1-6).
1252
+ if (text.trim() === "") {
1253
+ return ERRORS.VALUE;
1254
+ }
1255
+ const n = Number(text);
1256
+ if (isNaN(n)) {
1257
+ return ERRORS.VALUE;
1258
+ }
1259
+ return rvNumber(isPct ? n / 100 : n);
1260
+ };
1261
+ // ============================================================================
1262
+ // Excel 365 Text Functions: TEXTBEFORE, TEXTAFTER, TEXTSPLIT
1263
+ // ============================================================================
1264
+ /**
1265
+ * Parse the common [instance_num, match_mode, match_end, if_not_found]
1266
+ * tail used by TEXTBEFORE / TEXTAFTER. Returns the numeric values (with
1267
+ * defaults filled in) or an error if any argument is malformed.
1268
+ *
1269
+ * - match_mode: 0 = case-sensitive (default), 1 = case-insensitive.
1270
+ * - match_end: 0 = don't treat string edge as delimiter (default),
1271
+ * 1 = treat string edge as a virtual delimiter so that
1272
+ * TEXTAFTER with a missing delimiter returns "".
1273
+ */
1274
+ function parseTextBeforeAfterTail(args) {
1275
+ let inst = 1;
1276
+ if (args.length > 2) {
1277
+ const instRV = toNumberRV(args[2]);
1278
+ if (isError(instRV)) {
1279
+ return instRV;
1280
+ }
1281
+ inst = Math.trunc(instRV.value);
1282
+ }
1283
+ let matchMode = 0;
1284
+ if (args.length > 3) {
1285
+ const mmRV = toNumberRV(args[3]);
1286
+ if (isError(mmRV)) {
1287
+ return mmRV;
1288
+ }
1289
+ const mm = Math.trunc(mmRV.value);
1290
+ if (mm !== 0 && mm !== 1) {
1291
+ return ERRORS.VALUE;
1292
+ }
1293
+ matchMode = mm;
1294
+ }
1295
+ let matchEnd = 0;
1296
+ if (args.length > 4) {
1297
+ const meRV = toNumberRV(args[4]);
1298
+ if (isError(meRV)) {
1299
+ return meRV;
1300
+ }
1301
+ const me = Math.trunc(meRV.value);
1302
+ if (me !== 0 && me !== 1) {
1303
+ return ERRORS.VALUE;
1304
+ }
1305
+ matchEnd = me;
1306
+ }
1307
+ const ifNotFound = args.length > 5 ? args[5] : null;
1308
+ return { inst, matchMode, matchEnd, ifNotFound };
1309
+ }
1310
+ export const fnTEXTBEFORE = args => {
1311
+ const e0 = checkError(args[0]);
1312
+ if (e0) {
1313
+ return e0;
1314
+ }
1315
+ const e1 = checkError(args[1]);
1316
+ if (e1) {
1317
+ return e1;
1318
+ }
1319
+ const text = toStringRV(args[0]);
1320
+ const delimiter = toStringRV(args[1]);
1321
+ const tail = parseTextBeforeAfterTail(args);
1322
+ if ("kind" in tail && tail.kind === RVKind.Error) {
1323
+ return tail;
1324
+ }
1325
+ const { inst, matchMode, matchEnd, ifNotFound } = tail;
1326
+ if (inst === 0) {
1327
+ return ERRORS.VALUE;
1328
+ }
1329
+ if (delimiter === "") {
1330
+ return rvString(inst > 0 ? "" : text);
1331
+ }
1332
+ // For case-insensitive matching we search within the lower-cased
1333
+ // haystack but slice against the original so the returned prefix/
1334
+ // suffix preserves the source text's case.
1335
+ const haystack = matchMode === 1 ? text.toLowerCase() : text;
1336
+ const needle = matchMode === 1 ? delimiter.toLowerCase() : delimiter;
1337
+ const notFound = () => {
1338
+ if (matchEnd === 1 && inst === 1) {
1339
+ // Treat the string end as a virtual delimiter: everything is "before".
1340
+ return rvString(text);
1341
+ }
1342
+ return ifNotFound !== null ? ifNotFound : ERRORS.NA;
1343
+ };
1344
+ if (inst > 0) {
1345
+ let pos = -1;
1346
+ for (let i = 0; i < inst; i++) {
1347
+ pos = haystack.indexOf(needle, pos + 1);
1348
+ if (pos === -1) {
1349
+ return notFound();
1350
+ }
1351
+ }
1352
+ return rvString(text.slice(0, pos));
1353
+ }
1354
+ // Negative: search from end
1355
+ let pos = haystack.length;
1356
+ for (let i = 0; i < -inst; i++) {
1357
+ pos = haystack.lastIndexOf(needle, pos - 1);
1358
+ if (pos === -1) {
1359
+ return notFound();
1360
+ }
1361
+ }
1362
+ return rvString(text.slice(0, pos));
1363
+ };
1364
+ export const fnTEXTAFTER = args => {
1365
+ const e0 = checkError(args[0]);
1366
+ if (e0) {
1367
+ return e0;
1368
+ }
1369
+ const e1 = checkError(args[1]);
1370
+ if (e1) {
1371
+ return e1;
1372
+ }
1373
+ const text = toStringRV(args[0]);
1374
+ const delimiter = toStringRV(args[1]);
1375
+ const tail = parseTextBeforeAfterTail(args);
1376
+ if ("kind" in tail && tail.kind === RVKind.Error) {
1377
+ return tail;
1378
+ }
1379
+ const { inst, matchMode, matchEnd, ifNotFound } = tail;
1380
+ if (inst === 0) {
1381
+ return ERRORS.VALUE;
1382
+ }
1383
+ if (delimiter === "") {
1384
+ return rvString(inst > 0 ? text : "");
1385
+ }
1386
+ const haystack = matchMode === 1 ? text.toLowerCase() : text;
1387
+ const needle = matchMode === 1 ? delimiter.toLowerCase() : delimiter;
1388
+ const notFound = () => {
1389
+ if (matchEnd === 1 && inst === 1) {
1390
+ // String end is a virtual delimiter → everything after it is "".
1391
+ return rvString("");
1392
+ }
1393
+ return ifNotFound !== null ? ifNotFound : ERRORS.NA;
1394
+ };
1395
+ if (inst > 0) {
1396
+ let pos = -1;
1397
+ for (let i = 0; i < inst; i++) {
1398
+ pos = haystack.indexOf(needle, pos + 1);
1399
+ if (pos === -1) {
1400
+ return notFound();
1401
+ }
1402
+ }
1403
+ return rvString(text.slice(pos + delimiter.length));
1404
+ }
1405
+ let pos = haystack.length;
1406
+ for (let i = 0; i < -inst; i++) {
1407
+ pos = haystack.lastIndexOf(needle, pos - 1);
1408
+ if (pos === -1) {
1409
+ return notFound();
1410
+ }
1411
+ }
1412
+ return rvString(text.slice(pos + delimiter.length));
1413
+ };
1414
+ export const fnTEXTSPLIT = args => {
1415
+ const e0 = checkError(args[0]);
1416
+ if (e0) {
1417
+ return e0;
1418
+ }
1419
+ const text = toStringRV(args[0]);
1420
+ let colDelimiter = "";
1421
+ if (args.length > 1) {
1422
+ const e1 = checkError(args[1]);
1423
+ if (e1) {
1424
+ return e1;
1425
+ }
1426
+ colDelimiter = toStringRV(args[1]);
1427
+ }
1428
+ const rowDelimiter = args.length > 2 && args[2].kind !== RVKind.Blank ? toStringRV(args[2]) : "";
1429
+ // `ignore_empty` (4th arg, default FALSE) — when TRUE, suppress empty
1430
+ // fragments produced by consecutive delimiters.
1431
+ let ignoreEmpty = false;
1432
+ if (args.length > 3 && args[3].kind !== RVKind.Blank) {
1433
+ const ieRV = toBooleanRV(args[3]);
1434
+ if (isError(ieRV)) {
1435
+ return ieRV;
1436
+ }
1437
+ ignoreEmpty = ieRV.value;
1438
+ }
1439
+ // `match_mode` (5th arg, default 0 = case-sensitive). When 1 the
1440
+ // delimiter match is case-insensitive; we implement that by lowercasing
1441
+ // both the haystack and the delimiter(s) before splitting, which is
1442
+ // consistent with Excel's specification for TEXTSPLIT.
1443
+ let matchMode = 0;
1444
+ if (args.length > 4 && args[4].kind !== RVKind.Blank) {
1445
+ const mmRV = toNumberRV(args[4]);
1446
+ if (isError(mmRV)) {
1447
+ return mmRV;
1448
+ }
1449
+ matchMode = Math.trunc(mmRV.value);
1450
+ if (matchMode !== 0 && matchMode !== 1) {
1451
+ return ERRORS.VALUE;
1452
+ }
1453
+ }
1454
+ // `pad_with` (6th arg, default #N/A) — value used to fill shorter rows
1455
+ // when the split produces a ragged 2D shape. Explicit error arguments
1456
+ // (e.g. `TEXTSPLIT(…, #VALUE!)`) propagate into the pad cells verbatim,
1457
+ // matching Excel.
1458
+ const pad = args.length > 5 ? topLeft(args[5]) : ERRORS.NA;
1459
+ const splitString = (s, delim) => {
1460
+ if (!delim) {
1461
+ return [s];
1462
+ }
1463
+ if (matchMode === 1) {
1464
+ // Case-insensitive split — find positions by scanning the lowercased
1465
+ // haystack but slice the original so case is preserved in output.
1466
+ const haystack = s.toLowerCase();
1467
+ const needle = delim.toLowerCase();
1468
+ const parts = [];
1469
+ let last = 0;
1470
+ let i = 0;
1471
+ while (i <= haystack.length - needle.length) {
1472
+ if (haystack.slice(i, i + needle.length) === needle) {
1473
+ parts.push(s.slice(last, i));
1474
+ i += needle.length;
1475
+ last = i;
1476
+ }
1477
+ else {
1478
+ i++;
1479
+ }
1480
+ }
1481
+ parts.push(s.slice(last));
1482
+ return parts;
1483
+ }
1484
+ return s.split(delim);
1485
+ };
1486
+ let rows;
1487
+ if (rowDelimiter) {
1488
+ rows = splitString(text, rowDelimiter);
1489
+ }
1490
+ else {
1491
+ rows = [text];
1492
+ }
1493
+ // Split each row into columns, applying ignore_empty per row after the
1494
+ // split. When ignore_empty is TRUE at the row level we also drop rows
1495
+ // that were themselves empty (i.e. empty string from consecutive row
1496
+ // delimiters).
1497
+ const matrix = [];
1498
+ let maxWidth = 0;
1499
+ for (const row of rows) {
1500
+ if (ignoreEmpty && row === "") {
1501
+ continue;
1502
+ }
1503
+ let parts;
1504
+ if (colDelimiter) {
1505
+ parts = splitString(row, colDelimiter);
1506
+ if (ignoreEmpty) {
1507
+ parts = parts.filter(p => p !== "");
1508
+ }
1509
+ }
1510
+ else {
1511
+ parts = [row];
1512
+ }
1513
+ if (parts.length === 0) {
1514
+ // All fragments were empty and ignore_empty discarded them; keep a
1515
+ // pad row so the result is still a well-formed rectangle.
1516
+ parts = [""];
1517
+ }
1518
+ matrix.push(parts.map(p => rvString(p)));
1519
+ if (parts.length > maxWidth) {
1520
+ maxWidth = parts.length;
1521
+ }
1522
+ }
1523
+ if (matrix.length === 0) {
1524
+ // ignore_empty consumed everything → return a single pad cell so the
1525
+ // array is still a valid 1×1 spill (matches Excel).
1526
+ return rvArray([[pad]]);
1527
+ }
1528
+ // Pad ragged rows out to the maximum width with `pad_with`.
1529
+ const result = [];
1530
+ for (const row of matrix) {
1531
+ if (row.length < maxWidth) {
1532
+ const padded = row.slice();
1533
+ while (padded.length < maxWidth) {
1534
+ padded.push(pad);
1535
+ }
1536
+ result.push(padded);
1537
+ }
1538
+ else {
1539
+ result.push(row);
1540
+ }
1541
+ }
1542
+ return rvArray(result);
1543
+ };
1544
+ // ============================================================================
1545
+ // REGEX family (Excel 365, 2024)
1546
+ // ============================================================================
1547
+ /**
1548
+ * Convert an Excel REGEX pattern to a JavaScript RegExp. Excel's regex
1549
+ * dialect is close to PCRE; JavaScript's RegExp is close enough for the
1550
+ * vast majority of practical patterns, but a few constructs (named
1551
+ * captures, look-behind, some Unicode classes) behave slightly
1552
+ * differently. We pass patterns through as-is and let JavaScript's
1553
+ * parser surface #VALUE! on the rare incompatibility.
1554
+ */
1555
+ function compileExcelRegex(pattern, caseSensitive, global) {
1556
+ try {
1557
+ let flags = "u";
1558
+ if (!caseSensitive) {
1559
+ flags += "i";
1560
+ }
1561
+ if (global) {
1562
+ flags += "g";
1563
+ }
1564
+ return new RegExp(pattern, flags);
1565
+ }
1566
+ catch {
1567
+ return null;
1568
+ }
1569
+ }
1570
+ /**
1571
+ * Resolve the optional `case_sensitivity` argument used by every REGEX
1572
+ * function. `0`/FALSE/omitted → case-insensitive (Excel default),
1573
+ * any other value → case-sensitive. Errors propagate.
1574
+ */
1575
+ function resolveCaseSensitivity(arg) {
1576
+ if (arg === undefined) {
1577
+ return { caseSensitive: false };
1578
+ }
1579
+ // Accept boolean or number; anything else coerced via toBooleanRV.
1580
+ const b = toBooleanRV(arg);
1581
+ if (isError(b)) {
1582
+ return b;
1583
+ }
1584
+ return { caseSensitive: b.value };
1585
+ }
1586
+ /**
1587
+ * REGEXTEST(text, pattern, [case_sensitivity]) — returns TRUE iff the
1588
+ * regex matches any substring of `text`.
1589
+ */
1590
+ export const fnREGEXTEST = args => {
1591
+ const textV = toStringRV(topLeft(args[0]));
1592
+ const patternV = toStringRV(topLeft(args[1]));
1593
+ const cs = resolveCaseSensitivity(args[2]);
1594
+ if ("kind" in cs) {
1595
+ return cs; // error
1596
+ }
1597
+ const errCheck = checkError(args[0]) ?? checkError(args[1]);
1598
+ if (errCheck) {
1599
+ return errCheck;
1600
+ }
1601
+ const re = compileExcelRegex(patternV, cs.caseSensitive, false);
1602
+ if (!re) {
1603
+ return ERRORS.VALUE;
1604
+ }
1605
+ return rvBoolean(re.test(textV));
1606
+ };
1607
+ /**
1608
+ * REGEXEXTRACT(text, pattern, [return_mode], [case_sensitivity]) —
1609
+ * return_mode = 0 (default) → first match as a string
1610
+ * return_mode = 1 → all matches as a 1-column array
1611
+ * return_mode = 2 → capture groups of the first match as a 1-row array
1612
+ */
1613
+ export const fnREGEXEXTRACT = args => {
1614
+ const textV = toStringRV(topLeft(args[0]));
1615
+ const patternV = toStringRV(topLeft(args[1]));
1616
+ const errCheck = checkError(args[0]) ?? checkError(args[1]);
1617
+ if (errCheck) {
1618
+ return errCheck;
1619
+ }
1620
+ const modeV = args.length > 2 ? toNumberRV(topLeft(args[2])) : rvNumber(0);
1621
+ if (isError(modeV)) {
1622
+ return modeV;
1623
+ }
1624
+ const mode = Math.trunc(modeV.value);
1625
+ if (mode !== 0 && mode !== 1 && mode !== 2) {
1626
+ return ERRORS.VALUE;
1627
+ }
1628
+ const cs = resolveCaseSensitivity(args[3]);
1629
+ if ("kind" in cs) {
1630
+ return cs;
1631
+ }
1632
+ const needGlobal = mode === 1;
1633
+ const re = compileExcelRegex(patternV, cs.caseSensitive, needGlobal);
1634
+ if (!re) {
1635
+ return ERRORS.VALUE;
1636
+ }
1637
+ if (mode === 0) {
1638
+ const m = re.exec(textV);
1639
+ if (!m) {
1640
+ return ERRORS.NA;
1641
+ }
1642
+ return rvString(m[0]);
1643
+ }
1644
+ if (mode === 1) {
1645
+ const matches = [];
1646
+ let m;
1647
+ // eslint-disable-next-line no-cond-assign
1648
+ while ((m = re.exec(textV)) !== null) {
1649
+ matches.push(m[0]);
1650
+ // Guard against zero-length matches causing an infinite loop.
1651
+ if (m.index === re.lastIndex) {
1652
+ re.lastIndex++;
1653
+ }
1654
+ }
1655
+ if (matches.length === 0) {
1656
+ return ERRORS.NA;
1657
+ }
1658
+ return rvArray(matches.map(s => [rvString(s)]));
1659
+ }
1660
+ // mode === 2 — capture groups of first match as a row array.
1661
+ const m = re.exec(textV);
1662
+ if (!m) {
1663
+ return ERRORS.NA;
1664
+ }
1665
+ // Exclude the full-match element (index 0) — only capture groups.
1666
+ if (m.length <= 1) {
1667
+ // No capture groups defined in the pattern — return the full match.
1668
+ return rvArray([[rvString(m[0])]]);
1669
+ }
1670
+ const row = [];
1671
+ for (let i = 1; i < m.length; i++) {
1672
+ row.push(rvString(m[i] ?? ""));
1673
+ }
1674
+ return rvArray([row]);
1675
+ };
1676
+ /**
1677
+ * REGEXREPLACE(text, pattern, replacement, [occurrence], [case_sensitivity]) —
1678
+ * occurrence = 0 (default) → replace all
1679
+ * occurrence = n (positive) → replace only the n-th match
1680
+ * occurrence = n (negative) → replace only the n-th-last match
1681
+ */
1682
+ export const fnREGEXREPLACE = args => {
1683
+ const textV = toStringRV(topLeft(args[0]));
1684
+ const patternV = toStringRV(topLeft(args[1]));
1685
+ const replacementV = toStringRV(topLeft(args[2]));
1686
+ const errCheck = checkError(args[0]) ?? checkError(args[1]) ?? checkError(args[2]);
1687
+ if (errCheck) {
1688
+ return errCheck;
1689
+ }
1690
+ const occurrenceV = args.length > 3 ? toNumberRV(topLeft(args[3])) : rvNumber(0);
1691
+ if (isError(occurrenceV)) {
1692
+ return occurrenceV;
1693
+ }
1694
+ const occurrence = Math.trunc(occurrenceV.value);
1695
+ const cs = resolveCaseSensitivity(args[4]);
1696
+ if ("kind" in cs) {
1697
+ return cs;
1698
+ }
1699
+ // Always compile with the global flag — we need to enumerate matches
1700
+ // to apply the occurrence filter; `String.replace` without `/g` would
1701
+ // only see the first match and we wouldn't be able to address later
1702
+ // hits for `occurrence > 1`.
1703
+ const re = compileExcelRegex(patternV, cs.caseSensitive, true);
1704
+ if (!re) {
1705
+ return ERRORS.VALUE;
1706
+ }
1707
+ if (occurrence === 0) {
1708
+ // Replace all.
1709
+ return rvString(textV.replace(re, replacementV));
1710
+ }
1711
+ // Collect every match's range so we can address them by index.
1712
+ const ranges = [];
1713
+ let m;
1714
+ // eslint-disable-next-line no-cond-assign
1715
+ while ((m = re.exec(textV)) !== null) {
1716
+ ranges.push({ start: m.index, end: m.index + m[0].length });
1717
+ if (m.index === re.lastIndex) {
1718
+ re.lastIndex++;
1719
+ }
1720
+ }
1721
+ if (ranges.length === 0) {
1722
+ return rvString(textV); // no match → unchanged (Excel behavior)
1723
+ }
1724
+ // Negative index counts from the end; -1 is the last match.
1725
+ const idx = occurrence > 0 ? occurrence - 1 : ranges.length + occurrence;
1726
+ if (idx < 0 || idx >= ranges.length) {
1727
+ // Out-of-range occurrence → unchanged (Excel behavior).
1728
+ return rvString(textV);
1729
+ }
1730
+ const { start, end } = ranges[idx];
1731
+ return rvString(textV.slice(0, start) + replacementV + textV.slice(end));
1732
+ };
1733
+ // ============================================================================
1734
+ // VALUETOTEXT / ARRAYTOTEXT (Excel 365)
1735
+ // ============================================================================
1736
+ /**
1737
+ * Format a single scalar for VALUETOTEXT / ARRAYTOTEXT.
1738
+ *
1739
+ * Format 0 (concise, default):
1740
+ * - Number → plain number string
1741
+ * - String → the string itself (no quotes)
1742
+ * - Boolean → "TRUE" / "FALSE"
1743
+ * - Error → error text (e.g. "#N/A")
1744
+ * - Blank → ""
1745
+ *
1746
+ * Format 1 (strict):
1747
+ * - String → wrapped in double quotes with `""` escapes
1748
+ * - Everything else → same as format 0
1749
+ */
1750
+ function scalarToText(v, strict) {
1751
+ switch (v.kind) {
1752
+ case RVKind.Number:
1753
+ return String(v.value);
1754
+ case RVKind.String:
1755
+ if (strict) {
1756
+ return `"${v.value.replace(/"/g, '""')}"`;
1757
+ }
1758
+ return v.value;
1759
+ case RVKind.Boolean:
1760
+ return v.value ? "TRUE" : "FALSE";
1761
+ case RVKind.Error:
1762
+ return v.code;
1763
+ case RVKind.Blank:
1764
+ return "";
1765
+ }
1766
+ }
1767
+ /**
1768
+ * VALUETOTEXT(value, [format]) — format a scalar or 1×1 array as text.
1769
+ * For multi-cell arrays, this applies implicit intersection at the
1770
+ * evaluator layer — so by the time we see args[0] it is already scalar.
1771
+ */
1772
+ export const fnVALUETOTEXT = args => {
1773
+ const formatV = args.length > 1 ? toNumberRV(topLeft(args[1])) : rvNumber(0);
1774
+ if (isError(formatV)) {
1775
+ return formatV;
1776
+ }
1777
+ const fmt = Math.trunc(formatV.value);
1778
+ if (fmt !== 0 && fmt !== 1) {
1779
+ return ERRORS.VALUE;
1780
+ }
1781
+ const strict = fmt === 1;
1782
+ return rvString(scalarToText(topLeft(args[0]), strict));
1783
+ };
1784
+ /**
1785
+ * ARRAYTOTEXT(array, [format]) — flatten an array to a delimited text
1786
+ * representation.
1787
+ *
1788
+ * Format 0 (concise, default): row-major join with ", ".
1789
+ * Format 1 (strict): wraps output in `{…}`, rows separated by `;`,
1790
+ * cells by `,`; strings inside quoted.
1791
+ */
1792
+ export const fnARRAYTOTEXT = args => {
1793
+ const formatV = args.length > 1 ? toNumberRV(topLeft(args[1])) : rvNumber(0);
1794
+ if (isError(formatV)) {
1795
+ return formatV;
1796
+ }
1797
+ const fmt = Math.trunc(formatV.value);
1798
+ if (fmt !== 0 && fmt !== 1) {
1799
+ return ERRORS.VALUE;
1800
+ }
1801
+ const strict = fmt === 1;
1802
+ const arg = args[0];
1803
+ if (arg.kind !== RVKind.Array) {
1804
+ return rvString(scalarToText(topLeft(arg), strict));
1805
+ }
1806
+ if (!strict) {
1807
+ // Concise: flatten row-major, join with ", ".
1808
+ const parts = [];
1809
+ for (const row of arg.rows) {
1810
+ for (const cell of row) {
1811
+ parts.push(scalarToText(cell, false));
1812
+ }
1813
+ }
1814
+ return rvString(parts.join(", "));
1815
+ }
1816
+ // Strict: `{row1;row2;...}` with rows as `a,b,c` and strings quoted.
1817
+ const rowStrs = [];
1818
+ for (const row of arg.rows) {
1819
+ const cellStrs = [];
1820
+ for (const cell of row) {
1821
+ cellStrs.push(scalarToText(cell, true));
1822
+ }
1823
+ rowStrs.push(cellStrs.join(","));
1824
+ }
1825
+ return rvString(`{${rowStrs.join(";")}}`);
1826
+ };
1827
+ // ============================================================================
1828
+ // ENCODEURL
1829
+ // ============================================================================
1830
+ /**
1831
+ * ENCODEURL(text) — percent-encode a string for URL use.
1832
+ *
1833
+ * Excel follows RFC 3986's "unreserved" character rule: A-Z, a-z, 0-9,
1834
+ * and `- _ . ~` are kept verbatim; everything else is encoded as
1835
+ * `%HH` using the UTF-8 byte sequence. This is exactly what JavaScript's
1836
+ * `encodeURIComponent` does, so we delegate to it.
1837
+ */
1838
+ export const fnENCODEURL = args => {
1839
+ const err = checkError(args[0]);
1840
+ if (err) {
1841
+ return err;
1842
+ }
1843
+ const s = toStringRV(topLeft(args[0]));
1844
+ return rvString(encodeURIComponent(s));
1845
+ };