@pixerate/editor 0.23.0 → 0.24.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 (155) hide show
  1. package/README.md +96 -1
  2. package/THIRD_PARTY_NOTICES.md +51 -0
  3. package/dist/agent/index.cjs +9 -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 +3 -2
  8. package/dist/agent/index.js.map +1 -1
  9. package/dist/canvas/index.cjs +1210 -17
  10. package/dist/canvas/index.cjs.map +1 -1
  11. package/dist/canvas/index.d.cts +318 -1
  12. package/dist/canvas/index.d.ts +318 -1
  13. package/dist/canvas/index.js +1199 -6
  14. package/dist/canvas/index.js.map +1 -1
  15. package/dist/{chunk-QBJ22YZR.js → chunk-3CN2WD3X.js} +1354 -206
  16. package/dist/chunk-3CN2WD3X.js.map +1 -0
  17. package/dist/{chunk-JJDLPEWL.js → chunk-3LV6RRPH.js} +2 -2
  18. package/dist/{chunk-MJZBNWG7.js → chunk-5S3Z4PPF.js} +78 -557
  19. package/dist/chunk-5S3Z4PPF.js.map +1 -0
  20. package/dist/chunk-664LQYFJ.js +115 -0
  21. package/dist/chunk-664LQYFJ.js.map +1 -0
  22. package/dist/{chunk-AA4ZEVCC.cjs → chunk-D27YFC46.cjs} +101 -580
  23. package/dist/chunk-D27YFC46.cjs.map +1 -0
  24. package/dist/chunk-D6FH2YAX.cjs +131 -0
  25. package/dist/chunk-D6FH2YAX.cjs.map +1 -0
  26. package/dist/{chunk-4SUNJZWB.cjs → chunk-FD4LPFAE.cjs} +48 -200
  27. package/dist/chunk-FD4LPFAE.cjs.map +1 -0
  28. package/dist/chunk-FODNLX5N.js +749 -0
  29. package/dist/chunk-FODNLX5N.js.map +1 -0
  30. package/dist/{chunk-OYXSKQFR.js → chunk-FPENXAEJ.js} +35 -187
  31. package/dist/chunk-FPENXAEJ.js.map +1 -0
  32. package/dist/chunk-HLIMTC6N.js +1 -0
  33. package/dist/chunk-HLIMTC6N.js.map +1 -0
  34. package/dist/{chunk-QGHGJ56O.js → chunk-ITBSKI26.js} +1483 -19
  35. package/dist/chunk-ITBSKI26.js.map +1 -0
  36. package/dist/{chunk-XDMXXC4Z.cjs → chunk-MA2F5KHF.cjs} +350 -227
  37. package/dist/chunk-MA2F5KHF.cjs.map +1 -0
  38. package/dist/{chunk-RIKBJLS2.cjs → chunk-MCSJFWAO.cjs} +1356 -208
  39. package/dist/chunk-MCSJFWAO.cjs.map +1 -0
  40. package/dist/chunk-MKA3AHLV.cjs +256 -0
  41. package/dist/chunk-MKA3AHLV.cjs.map +1 -0
  42. package/dist/chunk-OLZJXL4R.cjs +749 -0
  43. package/dist/chunk-OLZJXL4R.cjs.map +1 -0
  44. package/dist/chunk-OXSV64JX.js +689 -0
  45. package/dist/chunk-OXSV64JX.js.map +1 -0
  46. package/dist/chunk-P3RQ5UJE.cjs +689 -0
  47. package/dist/chunk-P3RQ5UJE.cjs.map +1 -0
  48. package/dist/chunk-P6ODPZ4W.js +256 -0
  49. package/dist/chunk-P6ODPZ4W.js.map +1 -0
  50. package/dist/chunk-PGNZKFQS.js +685 -0
  51. package/dist/chunk-PGNZKFQS.js.map +1 -0
  52. package/dist/chunk-PL4JB3BF.cjs +1 -0
  53. package/dist/chunk-PL4JB3BF.cjs.map +1 -0
  54. package/dist/{chunk-GVSMRPHB.cjs → chunk-SBCZ6RN5.cjs} +1568 -104
  55. package/dist/chunk-SBCZ6RN5.cjs.map +1 -0
  56. package/dist/{chunk-CL37JGFW.cjs → chunk-SML64VJG.cjs} +4 -4
  57. package/dist/{chunk-CL37JGFW.cjs.map → chunk-SML64VJG.cjs.map} +1 -1
  58. package/dist/chunk-TNNJXF46.cjs +115 -0
  59. package/dist/chunk-TNNJXF46.cjs.map +1 -0
  60. package/dist/chunk-U5HZSFCS.js +131 -0
  61. package/dist/chunk-U5HZSFCS.js.map +1 -0
  62. package/dist/{chunk-K3R5NG2D.js → chunk-U5KWZU4X.js} +251 -128
  63. package/dist/chunk-U5KWZU4X.js.map +1 -0
  64. package/dist/chunk-XNORY7M6.cjs +685 -0
  65. package/dist/chunk-XNORY7M6.cjs.map +1 -0
  66. package/dist/commands/index.d.cts +5 -353
  67. package/dist/commands/index.d.ts +5 -353
  68. package/dist/comment-store-BEMgVlAA.d.cts +135 -0
  69. package/dist/comment-store-BEMgVlAA.d.ts +135 -0
  70. package/dist/compare/index.cjs +15 -0
  71. package/dist/compare/index.cjs.map +1 -0
  72. package/dist/compare/index.d.cts +58 -0
  73. package/dist/compare/index.d.ts +58 -0
  74. package/dist/compare/index.js +15 -0
  75. package/dist/compare/index.js.map +1 -0
  76. package/dist/{controller-pePCJLT1.d.cts → controller-BhLLAoi8.d.cts} +45 -4
  77. package/dist/{controller-pePCJLT1.d.ts → controller-BhLLAoi8.d.ts} +45 -4
  78. package/dist/documents-BQjzJNe2.d.ts +76 -0
  79. package/dist/documents-KRTPhvst.d.cts +76 -0
  80. package/dist/extensions/index.cjs +53 -5
  81. package/dist/extensions/index.cjs.map +1 -1
  82. package/dist/extensions/index.d.cts +7 -3
  83. package/dist/extensions/index.d.ts +7 -3
  84. package/dist/extensions/index.js +52 -4
  85. package/dist/generation/index.cjs +1373 -0
  86. package/dist/generation/index.cjs.map +1 -0
  87. package/dist/generation/index.d.cts +554 -0
  88. package/dist/generation/index.d.ts +554 -0
  89. package/dist/generation/index.js +1373 -0
  90. package/dist/generation/index.js.map +1 -0
  91. package/dist/grammar/index.cjs +48 -3
  92. package/dist/grammar/index.cjs.map +1 -1
  93. package/dist/grammar/index.d.cts +322 -100
  94. package/dist/grammar/index.d.ts +322 -100
  95. package/dist/grammar/index.js +54 -9
  96. package/dist/image-editor/index.cjs +1831 -30
  97. package/dist/image-editor/index.cjs.map +1 -1
  98. package/dist/image-editor/index.d.cts +590 -3
  99. package/dist/image-editor/index.d.ts +590 -3
  100. package/dist/image-editor/index.js +1807 -6
  101. package/dist/image-editor/index.js.map +1 -1
  102. package/dist/{index-CCr_z9kF.d.cts → index-BhJBSm2d.d.cts} +370 -4
  103. package/dist/{index-Ck6ajwXh.d.ts → index-fJrlkJCd.d.ts} +370 -4
  104. package/dist/index.cjs +162 -25
  105. package/dist/index.cjs.map +1 -1
  106. package/dist/index.d.cts +14 -8
  107. package/dist/index.d.ts +14 -8
  108. package/dist/index.js +174 -37
  109. package/dist/index.js.map +1 -1
  110. package/dist/migrate-iTlm3a8h.d.cts +112 -0
  111. package/dist/migrate-iTlm3a8h.d.ts +112 -0
  112. package/dist/persistence/index.cjs +676 -0
  113. package/dist/persistence/index.cjs.map +1 -0
  114. package/dist/persistence/index.d.cts +347 -0
  115. package/dist/persistence/index.d.ts +347 -0
  116. package/dist/persistence/index.js +676 -0
  117. package/dist/persistence/index.js.map +1 -0
  118. package/dist/{registry-D5-R90p0.d.cts → registry-BDe-vlv3.d.ts} +2 -40
  119. package/dist/registry-D32LpIc4.d.cts +354 -0
  120. package/dist/{registry-D5-R90p0.d.ts → registry-DnzpgY27.d.cts} +2 -40
  121. package/dist/registry-evKTP4ls.d.ts +354 -0
  122. package/dist/schema-DYDGdYL9.d.cts +41 -0
  123. package/dist/schema-DYDGdYL9.d.ts +41 -0
  124. package/dist/serializers/index.cjs +4 -3
  125. package/dist/serializers/index.cjs.map +1 -1
  126. package/dist/serializers/index.d.cts +2 -1
  127. package/dist/serializers/index.d.ts +2 -1
  128. package/dist/serializers/index.js +3 -2
  129. package/dist/sourcemap-C8AH3MBQ.d.cts +91 -0
  130. package/dist/sourcemap-DA1Evhrv.d.ts +91 -0
  131. package/dist/spreadsheet/index.cjs +37 -3
  132. package/dist/spreadsheet/index.cjs.map +1 -1
  133. package/dist/spreadsheet/index.d.cts +188 -17
  134. package/dist/spreadsheet/index.d.ts +188 -17
  135. package/dist/spreadsheet/index.js +43 -9
  136. package/dist/{tokens-CyLZmIei.d.cts → tokens-D7yXvUU6.d.cts} +15 -2
  137. package/dist/{tokens-CyLZmIei.d.ts → tokens-D7yXvUU6.d.ts} +15 -2
  138. package/dist/{track-changes-BSF58RK0.d.ts → track-changes-BK5sVf4_.d.cts} +18 -136
  139. package/dist/{track-changes-BSF58RK0.d.cts → track-changes-CDjCrwKe.d.ts} +18 -136
  140. package/package.json +16 -1
  141. package/dist/chunk-4SUNJZWB.cjs.map +0 -1
  142. package/dist/chunk-AA4ZEVCC.cjs.map +0 -1
  143. package/dist/chunk-AXV2ZW34.cjs +0 -118
  144. package/dist/chunk-AXV2ZW34.cjs.map +0 -1
  145. package/dist/chunk-B2ZRLN4S.js +0 -118
  146. package/dist/chunk-B2ZRLN4S.js.map +0 -1
  147. package/dist/chunk-GVSMRPHB.cjs.map +0 -1
  148. package/dist/chunk-K3R5NG2D.js.map +0 -1
  149. package/dist/chunk-MJZBNWG7.js.map +0 -1
  150. package/dist/chunk-OYXSKQFR.js.map +0 -1
  151. package/dist/chunk-QBJ22YZR.js.map +0 -1
  152. package/dist/chunk-QGHGJ56O.js.map +0 -1
  153. package/dist/chunk-RIKBJLS2.cjs.map +0 -1
  154. package/dist/chunk-XDMXXC4Z.cjs.map +0 -1
  155. /package/dist/{chunk-JJDLPEWL.js.map → chunk-3LV6RRPH.js.map} +0 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/home/runner/work/editor/editor/packages/core/dist/chunk-MCSJFWAO.cjs","../src/spreadsheet/formula/lexer.ts","../src/spreadsheet/formula/parser.ts","../src/spreadsheet/formula/values.ts","../src/spreadsheet/formula/parse-input.ts","../src/spreadsheet/formula/criteria.ts","../src/spreadsheet/numfmt/decimal.ts","../src/spreadsheet/numfmt/builtin.ts","../src/spreadsheet/numfmt/parse.ts","../src/spreadsheet/numfmt/render.ts","../src/spreadsheet/numfmt/index.ts","../src/spreadsheet/formula/functions/helpers.ts","../src/spreadsheet/formula/functions/math.ts","../src/spreadsheet/formula/functions/statistical.ts","../src/spreadsheet/formula/functions/conditional.ts","../src/spreadsheet/formula/functions/logical.ts","../src/spreadsheet/formula/functions/text.ts","../src/spreadsheet/formula/functions/lookup.ts","../src/spreadsheet/formula/functions/date.ts","../src/spreadsheet/formula/functions/information.ts","../src/spreadsheet/formula/functions/array.ts","../src/spreadsheet/formula/functions/index.ts","../src/spreadsheet/formula/registry.ts","../src/spreadsheet/formula/evaluator.ts"],"names":["quote","y","out","approximate","min","max","l"],"mappings":"AAAA;ACsBO,SAAS,eAAA,CAAgB,OAAA,EAAiC;AAC/D,EAAA,MAAM,OAAA,EAAyB,CAAC,CAAA;AAEhC,EAAA,IAAI,MAAA,EAAQ,OAAA,CAAQ,IAAA,CAAK,CAAA;AACzB,EAAA,IAAI,OAAA,EAAS,CAAA;AACb,EAAA,GAAA,CAAI,KAAA,CAAM,UAAA,CAAW,GAAG,CAAA,EAAG;AACzB,IAAA,MAAA,EAAQ,KAAA,CAAM,KAAA,CAAM,CAAC,CAAA;AACrB,IAAA,OAAA,EAAS,CAAA;AAAA,EACX;AAEA,EAAA,IAAI,EAAA,EAAI,CAAA;AACR,EAAA,MAAA,CAAO,EAAA,EAAI,KAAA,CAAM,MAAA,EAAQ;AACvB,IAAA,MAAM,KAAA,EAAO,KAAA,CAAM,CAAC,CAAA;AAGpB,IAAA,GAAA,CAAI,IAAA,CAAK,IAAA,CAAK,IAAI,CAAA,EAAG;AACnB,MAAA,CAAA,EAAA;AACA,MAAA,QAAA;AAAA,IACF;AAGA,IAAA,GAAA,CAAI,IAAA,CAAK,IAAA,CAAK,IAAI,EAAA,GAAM,KAAA,IAAS,IAAA,GAAO,EAAA,EAAI,EAAA,EAAI,KAAA,CAAM,OAAA,GAAU,IAAA,CAAK,IAAA,CAAK,KAAA,CAAM,EAAA,EAAI,CAAC,CAAC,CAAA,EAAI;AACxF,MAAA,MAAM,MAAA,EAAQ,CAAA;AACd,MAAA,IAAI,OAAA,EAAS,KAAA,IAAS,GAAA;AACtB,MAAA,CAAA,EAAA;AACA,MAAA,MAAA,CAAO,EAAA,EAAI,KAAA,CAAM,OAAA,GAAA,CAAW,IAAA,CAAK,IAAA,CAAK,KAAA,CAAM,CAAC,CAAC,EAAA,GAAM,KAAA,CAAM,CAAC,EAAA,IAAM,IAAA,GAAO,CAAC,MAAA,CAAA,EAAU;AACjF,QAAA,GAAA,CAAI,KAAA,CAAM,CAAC,EAAA,IAAM,GAAA,EAAK,OAAA,EAAS,IAAA;AAC/B,QAAA,CAAA,EAAA;AAAA,MACF;AACA,MAAA,MAAA,CAAO,IAAA,CAAK;AAAA,QACV,IAAA,EAAM,QAAA;AAAA,QACN,KAAA,EAAO,KAAA,CAAM,KAAA,CAAM,KAAA,EAAO,CAAC,CAAA;AAAA,QAC3B,QAAA,EAAU,MAAA,EAAQ;AAAA,MACpB,CAAC,CAAA;AACD,MAAA,QAAA;AAAA,IACF;AAGA,IAAA,GAAA,CAAI,KAAA,IAAS,IAAA,GAAO,KAAA,IAAS,GAAA,EAAK;AAChC,MAAA,MAAMA,OAAAA,EAAQ,IAAA;AACd,MAAA,MAAM,MAAA,EAAQ,CAAA;AACd,MAAA,CAAA,EAAA;AACA,MAAA,IAAI,IAAA,EAAM,EAAA;AACV,MAAA,MAAA,CAAO,EAAA,EAAI,KAAA,CAAM,OAAA,GAAU,KAAA,CAAM,CAAC,EAAA,IAAMA,MAAAA,EAAO;AAC7C,QAAA,GAAA,CAAI,KAAA,CAAM,CAAC,EAAA,IAAM,KAAA,GAAQ,EAAA,EAAI,EAAA,EAAI,KAAA,CAAM,MAAA,EAAQ;AAC7C,UAAA,CAAA,EAAA;AAAA,QACF;AACA,QAAA,IAAA,GAAO,KAAA,CAAM,CAAC,CAAA;AACd,QAAA,CAAA,EAAA;AAAA,MACF;AACA,MAAA,GAAA,CAAI,EAAA,GAAK,KAAA,CAAM,MAAA,EAAQ;AACrB,QAAA,MAAM,IAAI,KAAA,CAAM,CAAA,yCAAA,EAA4C,MAAA,EAAQ,MAAM,CAAA,CAAA;AAC5E,MAAA;AACA,MAAA;AACY,MAAA;AACJ,QAAA;AACC,QAAA;AACW,QAAA;AACnB,MAAA;AACD,MAAA;AACF,IAAA;AAGkB,IAAA;AACF,MAAA;AACF,MAAA;AAC0D,QAAA;AACtE,MAAA;AACoE,MAAA;AACtD,MAAA;AACd,MAAA;AACF,IAAA;AAGkB,IAAA;AACF,MAAA;AACd,MAAA;AACW,MAAA;AACkC,MAAA;AAC5B,QAAA;AACf,QAAA;AACF,MAAA;AAC0C,MAAA;AACxC,QAAA;AACF,MAAA;AACY,MAAA;AACJ,QAAA;AACW,QAAA;AACC,QAAA;AACnB,MAAA;AACD,MAAA;AACF,IAAA;AAGkB,IAAA;AACgD,MAAA;AAChE,MAAA;AACA,MAAA;AACF,IAAA;AACkB,IAAA;AACgD,MAAA;AAChE,MAAA;AACA,MAAA;AACF,IAAA;AACkB,IAAA;AAC+C,MAAA;AAC/D,MAAA;AACA,MAAA;AACF,IAAA;AAGqC,IAAA;AACY,IAAA;AACnC,MAAA;AACJ,QAAA;AAC4B,QAAA;AACpB,QAAA;AACf,MAAA;AACI,MAAA;AACL,MAAA;AACF,IAAA;AAGuE,IAAA;AACF,MAAA;AACnE,MAAA;AACA,MAAA;AACF,IAAA;AAG6B,IAAA;AACb,MAAA;AAC8C,MAAA;AAC1D,QAAA;AACF,MAAA;AACoC,MAAA;AAGM,MAAA;AACvB,QAAA;AACjB,QAAA;AACoB,QAAA;AACwC,QAAA;AAC1D,UAAA;AACF,QAAA;AAC6C,QAAA;AAClB,QAAA;AACb,UAAA;AACJ,YAAA;AACwC,YAAA;AAC5B,YAAA;AACnB,UAAA;AACD,UAAA;AACK,QAAA;AAED,UAAA;AACN,QAAA;AACF,MAAA;AAGkC,MAAA;AACS,MAAA;AAC7B,QAAA;AACJ,UAAA;AACC,UAAA;AACW,UAAA;AACnB,QAAA;AACD,QAAA;AACF,MAAA;AAG6C,MAAA;AAErB,QAAA;AACpB,UAAA;AACkE,UAAA;AAClE,UAAA;AACF,QAAA;AACY,QAAA;AACJ,UAAA;AACwB,UAAA;AACZ,UAAA;AACnB,QAAA;AACD,QAAA;AACF,MAAA;AAGY,MAAA;AACJ,QAAA;AACC,QAAA;AACW,QAAA;AACnB,MAAA;AACD,MAAA;AACF,IAAA;AAE0E,IAAA;AAC5E,EAAA;AAEuE,EAAA;AAChE,EAAA;AACT;ADpDmF;AACA;AExK/C;AAyE0C;AAChC,EAAA;AAChC,EAAA;AACsC,IAAA;AAClD,EAAA;AACO,EAAA;AACqB,IAAA;AACA,IAAA;AAC5B,EAAA;AACF;AAEwD;AACI,EAAA;AACzB,EAAA;AAGyB,EAAA;AACjD,IAAA;AACC,MAAA;AACN,MAAA;AAC4B,MAAA;AAClB,MAAA;AACc,MAAA;AAChB,MAAA;AACV,IAAA;AACF,EAAA;AAEyC,EAAA;AACJ,EAAA;AAC9B,EAAA;AACC,IAAA;AACN,IAAA;AACmB,IAAA;AACA,IAAA;AACJ,IAAA;AACA,IAAA;AACjB,EAAA;AACF;AAE2B;AACS,iBAAA;AAChB,kBAAA;AAEkB,EAAA;AACpB,IAAA;AAChB,EAAA;AAEwB,EAAA;AACU,IAAA;AACM,MAAA;AACtC,IAAA;AAC6B,IAAA;AACG,IAAA;AAC2C,MAAA;AAC3E,IAAA;AACO,IAAA;AACT,EAAA;AAE8B,EAAA;AACL,IAAA;AACzB,EAAA;AAE8B,EAAA;AACE,IAAA;AAI5B,IAAA;AAC2B,MAAA;AACM,MAAA;AACgB,MAAA;AACnD,IAAA;AAEO,IAAA;AACT,EAAA;AAEiC,EAAA;AACN,IAAA;AAEO,IAAA;AACH,MAAA;AACC,MAAA;AACqB,MAAA;AACnD,IAAA;AAEO,IAAA;AACT,EAAA;AAE4B,EAAA;AACK,IAAA;AAEM,IAAA;AACR,MAAA;AACO,MAAA;AACe,MAAA;AACnD,IAAA;AAEO,IAAA;AACT,EAAA;AAEkC,EAAA;AACD,IAAA;AAEM,IAAA;AACR,MAAA;AACO,MAAA;AACe,MAAA;AACnD,IAAA;AAEO,IAAA;AACT,EAAA;AAEkC,EAAA;AACV,IAAA;AAEU,IAAA;AACH,MAAA;AACF,MAAA;AACwB,MAAA;AACnD,IAAA;AAEO,IAAA;AACT,EAAA;AAEyB,EAAA;AACgB,IAAA;AACV,MAAA;AACC,MAAA;AACW,MAAA;AACzC,IAAA;AACoB,IAAA;AACtB,EAAA;AAE2B,EAAA;AACD,IAAA;AAEK,IAAA;AACd,MAAA;AAC4C,MAAA;AAC3D,IAAA;AAE6B,IAAA;AACd,MAAA;AACgC,MAAA;AAC/C,IAAA;AAE8B,IAAA;AACf,MAAA;AAC2C,MAAA;AAC1D,IAAA;AAE4B,IAAA;AACb,MAAA;AACgC,MAAA;AAC/C,IAAA;AAE+B,IAAA;AAChB,MAAA;AACgC,MAAA;AACtC,MAAA;AACC,QAAA;AACK,QAAA;AACC,QAAA;AACA,QAAA;AACd,MAAA;AACF,IAAA;AAE4B,IAAA;AACb,MAAA;AACgB,MAAA;AAC/B,IAAA;AAEgC,IAAA;AACjB,MAAA;AACgC,MAAA;AAC6B,MAAA;AAC5E,IAAA;AAEmC,IAAA;AACpB,MAAA;AACN,MAAA;AACC,QAAA;AACU,QAAA;AAClB,MAAA;AACF,IAAA;AAGiC,IAAA;AACZ,MAAA;AACN,MAAA;AAEa,MAAA;AACoC,QAAA;AACnC,QAAA;AACE,QAAA;AACtB,UAAA;AAC0B,YAAA;AACF,UAAA;AAC7B,QAAA;AAC8D,QAAA;AACvD,QAAA;AACC,UAAA;AACN,UAAA;AACA,UAAA;AACF,QAAA;AACF,MAAA;AAG4B,MAAA;AAC9B,IAAA;AAE0B,IAAA;AACK,MAAA;AACyB,MAAA;AAC/C,MAAA;AACT,IAAA;AAEyE,IAAA;AAC3E,EAAA;AAE2C,EAAA;AACf,IAAA;AACF,MAAA;AACP,QAAA;AACN,QAAA;AACT,MAAA;AACF,IAAA;AACO,IAAA;AACT,EAAA;AAEiD,EAAA;AACgB,IAAA;AAChD,MAAA;AACN,MAAA;AACT,IAAA;AACO,IAAA;AACT,EAAA;AAEqC,EAAA;AACR,IAAA;AACC,IAAA;AAC9B,EAAA;AAEgC,EAAA;AACJ,IAAA;AACL,IAAA;AACvB,EAAA;AAE2B,EAAA;AACG,IAAA;AAC9B,EAAA;AAE6B,EAAA;AACI,IAAA;AACjC,EAAA;AAEiC,EAAA;AACI,IAAA;AACrC,EAAA;AAE6D,EAAA;AACjB,IAAA;AACnB,IAAA;AACzB,EAAA;AACF;AAEuD;AACf,EAAA;AACC,EAAA;AACnB,EAAA;AACtB;AF+CmF;AACA;AGjY7C;AACpC,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACD;AAc+D;AACF,EAAA;AAC9D;AAEgG;AAOtC;AACtB,EAAA;AACP,EAAA;AACwB,EAAA;AACb,EAAA;AACG,EAAA;AAC3C;AAG4D;AACxB,EAAA;AACP,EAAA;AACY,EAAA;AACS,EAAA;AACD,EAAA;AAClB,EAAA;AACC,EAAA;AACvB,EAAA;AACT;AAOiB;AAS+B;AACH,EAAA;AACvB,EAAA;AAC8D,EAAA;AAChD,EAAA;AACF,EAAA;AACmB,EAAA;AACb,EAAA;AACxC;AAGwC;AACT,EAAA;AACmB,EAAA;AACtB,EAAA;AAC5B;AAGiD;AACwB,EAAA;AACnD,EAAA;AACuD,EAAA;AAC7E;AAM4D;AACa,EAAA;AACJ,EAAA;AACxC,EAAA;AACA,EAAA;AACD,EAAA;AACU,EAAA;AACtC;AAGwD;AACQ,EAAA;AAChC,EAAA;AAChC;AAGiF;AAGtC;AACd,EAAA;AACW,EAAA;AACC,EAAA;AAC4B,EAAA;AAC5D,EAAA;AACT;AAM6D;AACvC,EAAA;AAG6D,EAAA;AACH,EAAA;AAC3D,EAAA;AACA,EAAA;AACa,EAAA;AAClC;AAG0D;AAChC,EAAA;AACA,EAAA;AACQ,EAAA;AAClC;AAUqE;AACvC,EAAA;AACC,EAAA;AACG,EAAA;AACF,IAAA;AAEC,IAAA;AACM,IAAA;AACI,IAAA;AACnC,IAAA;AACwD,IAAA;AACD,IAAA;AACd,IAAA;AACzB,IAAA;AACO,IAAA;AAC7B,EAAA;AACgC,EAAA;AACT,IAAA;AACC,IAAA;AACE,IAAA;AACqB,IAAA;AACW,IAAA;AACjC,IAAA;AACzB,EAAA;AACkE,EAAA;AACZ,IAAA;AAC1C,IAAA;AACwB,MAAA;AACqD,MAAA;AACvF,IAAA;AACF,EAAA;AACoC,EAAA;AACkC,EAAA;AACnB,EAAA;AACwC,EAAA;AAC7F;AAMyE;AAC5C,EAAA;AACC,EAAA;AACsC,EAAA;AACR,EAAA;AAC1B,EAAA;AAClC;AAG+E;AACzC,EAAA;AAC4B,EAAA;AACf,EAAA;AACnD;AAGiF;AAE7B;AACtC,EAAA;AACkB,EAAA;AACS,EAAA;AACY,IAAA;AACnD,EAAA;AACe,EAAA;AACjB;AAEsD;AAClC,EAAA;AACP,EAAA;AACK,EAAA;AACU,IAAA;AACe,IAAA;AACN,IAAA;AACnC,EAAA;AACO,EAAA;AACT;AAeuE;AAGmB;AAClD,EAAA;AAC/B,EAAA;AACT;AAG4D;AACtB,EAAA;AACuC,EAAA;AAC7B,EAAA;AAChD;AAG+C;AAStC;AACa,EAAA;AACiC,EAAA;AAC2B,IAAA;AAChF,EAAA;AAC6C,EAAA;AAC/C;AAQkC;AACiC;AAGf;AACgB,EAAA;AACV,EAAA;AACW,EAAA;AAC3C,EAAA;AACA,EAAA;AACZ,EAAA;AACH,EAAA;AAC2C,IAAA;AAC4B,IAAA;AAE5B,IAAA;AACpD,EAAA;AACgC,EAAA;AACe,EAAA;AAErB,EAAA;AACM,IAAA;AACe,IAAA;AAC/C,EAAA;AACO,EAAA;AACT;AAEiB;AAOyC;AACuB,EAAA;AACzC,EAAA;AACrB,EAAA;AACiC,EAAA;AAC8B,EAAA;AACL,EAAA;AAC7E;AAGoF;AAEjC;AAEkB,EAAA;AAC1B,EAAA;AAC3C;AAGiD;AAC9B,EAAA;AACE,IAAA;AACH,IAAA;AACD,IAAA;AACC,IAAA;AACE,IAAA;AACA,IAAA;AAClB,EAAA;AAC6D,EAAA;AAC/D;AAE0D;AACnB,EAAA;AACwC,EAAA;AAClE,EAAA;AACU,IAAA;AACH,IAAA;AACD,IAAA;AACC,IAAA;AACE,IAAA;AACA,IAAA;AACpB,EAAA;AACF;AAIuD;AAIP;AAIzC;AAEc,EAAA;AAEnB;AHkPiF;AACA;AIhlBxD;AACzB,EAAA;AAAW,EAAA;AAAY,EAAA;AAAS,EAAA;AAAS,EAAA;AAAO,EAAA;AAChD,EAAA;AAAQ,EAAA;AAAU,EAAA;AAAa,EAAA;AAAW,EAAA;AAAY,EAAA;AACxD;AAGgF;AAC5C,EAAA;AACiB,EAAA;AAC7B,EAAA;AACwD,EAAA;AACR,EAAA;AAC1C,EAAA;AAC9B;AAEgG;AAE/B;AACf,EAAA;AACN,EAAA;AACF,EAAA;AAC1C;AAOsC;AAEd;AACA;AAO+D;AACzB,EAAA;AACX,EAAA;AAC9B,EAAA;AACI,EAAA;AACa,EAAA;AACH,EAAA;AAEgD,EAAA;AACpD,EAAA;AAC8B,EAAA;AAC7D;AAGmG;AAG1C;AAqBM;AACT,EAAA;AACoB,EAAA;AACnD,EAAA;AACO,EAAA;AACgC,EAAA;AACE,EAAA;AACM,EAAA;AACtC,EAAA;AACjB,EAAA;AACkB,IAAA;AACK,MAAA;AAEd,MAAA;AACiD,QAAA;AACnE,MAAA;AACK,IAAA;AACsE,MAAA;AACpE,MAAA;AAC6B,QAAA;AAC5B,QAAA;AACmD,QAAA;AACtB,QAAA;AACqC,QAAA;AAC1E,MAAA;AACF,IAAA;AACF,EAAA;AACoC,EAAA;AACtC;AAM6D;AACnB,EAAA;AACnB,EAAA;AAC0C,EAAA;AAChB,EAAA;AACF,EAAA;AACJ,EAAA;AAC3C;AAEoD;AAE+B;AAC7D,EAAA;AACa,EAAA;AACA,EAAA;AAC1B,EAAA;AACT;AAEmE;AAGI;AAC1B,EAAA;AACW,EAAA;AACvB,EAAA;AACM,EAAA;AACL,EAAA;AAC8C,EAAA;AAChF;AAEiD;AAC1B,EAAA;AAEsC,EAAA;AACT,IAAA;AACJ,IAAA;AACL,IAAA;AACzC,EAAA;AAC0B,EAAA;AACR,EAAA;AACc,EAAA;AACiD,EAAA;AAC3D,EAAA;AACxB;AAE4D;AAGb;AACrC,EAAA;AACE,EAAA;AACK,EAAA;AACD,EAAA;AACS,EAAA;AACf,IAAA;AACmB,IAAA;AACG,EAAA;AACH,IAAA;AAC3B,EAAA;AACuB,EAAA;AACV,IAAA;AACc,IAAA;AACM,IAAA;AACvB,MAAA;AACO,MAAA;AACf,IAAA;AACF,EAAA;AACqB,EAAA;AACT,IAAA;AACiB,IAAA;AAC7B,EAAA;AACqB,EAAA;AACQ,EAAA;AACzB,EAAA;AACQ,EAAA;AAEe,IAAA;AACkB,IAAA;AACK,IAAA;AACpB,IAAA;AACN,IAAA;AACgD,IAAA;AACb,IAAA;AACpD,EAAA;AACI,IAAA;AACX,EAAA;AAC2E,EAAA;AAGZ,EAAA;AAC1C,EAAA;AACW,EAAA;AACF,EAAA;AACZ,EAAA;AACJ,EAAA;AACoB,EAAA;AAI5B,EAAA;AAM2D,EAAA;AACnE;AAGsD;AAGL;AACpB,EAAA;AACF,EAAA;AACK,EAAA;AACO,EAAA;AACL,EAAA;AACe,EAAA;AACjB,EAAA;AACS,EAAA;AACI,EAAA;AACC,EAAA;AAC2B,EAAA;AACrD,EAAA;AACA,EAAA;AACpB;AAEiD;AACvB,EAAA;AACC,EAAA;AAC+C,EAAA;AACnC,EAAA;AACvC;AAGgG;AACvD,EAAA;AAC5B,EAAA;AACc,EAAA;AACsD,EAAA;AACjD,IAAA;AACiB,MAAA;AACtC,MAAA;AACL,MAAA;AACF,IAAA;AACF,EAAA;AAC4B,EAAA;AACkB,EAAA;AACc,EAAA;AACgB,EAAA;AACxD,EAAA;AACE,EAAA;AACA,EAAA;AAC6B,EAAA;AACO,EAAA;AACzC,EAAA;AACI,IAAA;AACD,IAAA;AACL,IAAA;AACQ,EAAA;AACd,IAAA;AACT,EAAA;AAC6E,EAAA;AAC/E;AAEqG;AAEvB;AAEC,EAAA;AAC9B,EAAA;AACrB,EAAA;AAC5B;AAE8E;AACpC,EAAA;AACqC,EAAA;AACa,EAAA;AAC7D,EAAA;AACL,EAAA;AACqB,IAAA;AAEvB,IAAA;AACuB,MAAA;AAC8BC,MAAAA;AACzE,IAAA;AACA,IAAA;AAC2C,MAAA;AAC8B,MAAA;AACzE,IAAA;AAEmD,IAAA;AACoB,IAAA;AACzE,EAAA;AACwB,EAAA;AACkB,IAAA;AACV,IAAA;AACA,IAAA;AAE2C,MAAA;AAEI,MAAA;AAC7E,IAAA;AACkB,IAAA;AACY,IAAA;AAC+C,IAAA;AAC/C,IAAA;AACX,IAAA;AAC0D,IAAA;AAC/E,EAAA;AAEoD,EAAA;AAC5B,EAAA;AACD,IAAA;AAC+B,IAAA;AACmB,IAAA;AACvB,IAAA;AACuB,IAAA;AACzE,EAAA;AACO,EAAA;AACT;AAEkD;AACxB,EAAA;AACd,EAAA;AAKY,IAAA;AAEgC,IAAA;AACtD,EAAA;AACwB,EAAA;AAC8C,EAAA;AAEJ,EAAA;AAC/B,IAAA;AACU,IAAA;AAEX,IAAA;AACyC,MAAA;AACzE,IAAA;AACF,EAAA;AACO,EAAA;AACT;AJ+fmF;AACA;AK/3BzB;AAC3C,EAAA;AAC4B,EAAA;AACjB,IAAA;AACsB,IAAA;AACP,MAAA;AACZ,IAAA;AACb,MAAA;AACa,IAAA;AACb,MAAA;AACL,IAAA;AACsB,MAAA;AAC7B,IAAA;AACF,EAAA;AACoC,EAAA;AACtC;AAEiF;AAEI;AAaT;AACvC,EAAA;AACS,IAAA;AAC5C,EAAA;AACoC,EAAA;AACN,IAAA;AAC9B,EAAA;AACuD,EAAA;AAE1B,EAAA;AACyB,EAAA;AAC/B,EAAA;AACA,EAAA;AAEH,EAAA;AAC6B,IAAA;AACE,IAAA;AAEpC,IAAA;AACf,EAAA;AAE4C,EAAA;AACgC,EAAA;AACJ,EAAA;AAEe,EAAA;AACvC,EAAA;AACyB,EAAA;AAE1C,EAAA;AACzB,IAAA;AACsB,IAAA;AACkD,MAAA;AACzC,IAAA;AACW,MAAA;AACV,IAAA;AAC4C,MAAA;AAC5C,IAAA;AAEsC,MAAA;AACnE,IAAA;AACiC,MAAA;AACqC,MAAA;AAC7E,IAAA;AAC0E,IAAA;AAC5E,EAAA;AAGyD,EAAA;AAE/B,EAAA;AACsD,IAAA;AAChF,EAAA;AAE4B,EAAA;AAER,IAAA;AACc,MAAA;AACiB,MAAA;AACjD,IAAA;AACF,EAAA;AAEkC,EAAA;AAChB,EAAA;AAC8D,IAAA;AACrE,MAAA;AACT,IAAA;AAC4B,IAAA;AACkB,IAAA;AAChD,EAAA;AACF;ALs2BmF;AACA;AM58BvD;AAMwD;AACvB,EAAA;AACpC,EAAA;AACG,EAAA;AACE,EAAA;AACF,IAAA;AACkB,IAAA;AAC5C,EAAA;AAC6C,EAAA;AAC/C;AAGwC;AACM,EAAA;AACxB,EAAA;AACO,EAAA;AACA,EAAA;AACE,EAAA;AAC2B,EAAA;AACpD,EAAA;AACc,EAAA;AACT,IAAA;AACF,EAAA;AACa,IAAA;AACF,IAAA;AAClB,EAAA;AAC+E,EAAA;AAChD,EAAA;AACjC;AAGwC;AACkD,EAAA;AAET,EAAA;AACjF;AAU4E;AACM,EAAA;AAC5B,EAAA;AACtB,EAAA;AACU,EAAA;AACX,EAAA;AACY,EAAA;AACpB,EAAA;AACqB,IAAA;AAEP,IAAA;AACW,IAAA;AACrC,IAAA;AACT,EAAA;AACQ,EAAA;AACI,EAAA;AACkB,EAAA;AACP,IAAA;AACP,MAAA;AACP,IAAA;AACQ,MAAA;AACL,MAAA;AACR,MAAA;AACF,IAAA;AACF,EAAA;AACW,EAAA;AACI,IAAA;AACD,IAAA;AACP,IAAA;AACP,EAAA;AACwB,EAAA;AAC1B;AAOmG;AAC7C,EAAA;AAClB,EAAA;AAC9B,EAAA;AACW,EAAA;AACoC,IAAA;AACjB,EAAA;AACf,IAAA;AACe,IAAA;AAC3B,EAAA;AACmB,IAAA;AACM,IAAA;AAChB,MAAA;AAC4B,MAAA;AACtB,QAAA;AACP,UAAA;AACF,QAAA;AACG,UAAA;AACA,UAAA;AACR,UAAA;AACF,QAAA;AACF,MAAA;AACsB,MAAA;AACxB,IAAA;AACF,EAAA;AACW,EAAA;AAC8B,EAAA;AACzB,EAAA;AACwB,EAAA;AACpC,EAAA;AACA,EAAA;AAC0B,EAAA;AACtB,IAAA;AAC4C,IAAA;AAC7C,EAAA;AACoB,IAAA;AACE,IAAA;AACF,IAAA;AAC3B,EAAA;AACuE,EAAA;AACtD,EAAA;AACnB;AAGqE;AACU,EAAA;AACtC,EAAA;AACK,EAAA;AAC9C;AAE+C;AAO8B;AAC3C,EAAA;AACU,EAAA;AACf,EAAA;AACyB,EAAA;AACN,EAAA;AAEX,EAAA;AACR,IAAA;AACe,IAAA;AAC/B,IAAA;AAC4C,MAAA;AACrB,MAAA;AACJ,MAAA;AACO,MAAA;AACT,MAAA;AACa,MAAA;AACjB,MAAA;AACR,MAAA;AACd,IAAA;AACF,EAAA;AACqD,EAAA;AAC3C,IAAA;AACoC,IAAA;AACzB,IAAA;AACZ,MAAA;AACoC,MAAA;AAC3C,IAAA;AACyB,IAAA;AACS,IAAA;AACR,IAAA;AACuC,IAAA;AAC5B,IAAA;AACvC,EAAA;AACO,EAAA;AACT;AN06BmF;AACA;AOxmCxB;AAC5C,EAAA;AACN,EAAA;AACG,EAAA;AACC,EAAA;AACG,EAAA;AACiB,EAAA;AACK,EAAA;AACC,EAAA;AACK,EAAA;AAClC,EAAA;AACI,EAAA;AACG,EAAA;AACH,EAAA;AACE,EAAA;AACC,EAAA;AACA,EAAA;AACH,EAAA;AACC,EAAA;AACI,EAAA;AACG,EAAA;AACT,EAAA;AACG,EAAA;AACM,EAAA;AACC,EAAA;AACK,EAAA;AACA,EAAA;AACK,EAAA;AACmB,EAAA;AACS,EAAA;AACD,EAAA;AACS,EAAA;AACvD,EAAA;AACI,EAAA;AACH,EAAA;AACE,EAAA;AACP,EAAA;AACV;AAMoE;AACvB,EAAA;AAC7C;AAGwE;AACrB,EAAA;AACxB,EAAA;AACoC,EAAA;AAC1C,EAAA;AAC+B,EAAA;AACpD;APmmCmF;AACA;AQ7oC9D;AAQgF;AAsBvD;AAC7B,EAAA;AACR,IAAA;AACA,IAAA;AACQ,MAAA;AACR,IAAA;AACQ,MAAA;AACR,IAAA;AACI,MAAA;AACJ,IAAA;AACI,MAAA;AACJ,IAAA;AACI,MAAA;AACJ,IAAA;AACmD,MAAA;AACnD,IAAA;AACI,MAAA;AACT,IAAA;AACS,MAAA;AACX,EAAA;AACF;AASsD;AACtC,EAAA;AACP,IAAA;AACY,MAAA;AACZ,IAAA;AACY,MAAA;AACZ,IAAA;AACU,MAAA;AACV,IAAA;AACW,MAAA;AACX,IAAA;AACU,MAAA;AACV,IAAA;AACW,MAAA;AAClB,EAAA;AACF;AAqBgH;AAQ1D;AAC1B,EAAA;AACU,EAAA;AACX,EAAA;AACZ,EAAA;AACA,EAAA;AACL,EAAA;AACc,EAAA;AACN,IAAA;AACT,IAAA;AACoB,IAAA;AACb,IAAA;AACE,MAAA;AACH,MAAA;AACT,MAAA;AACF,IAAA;AACe,IAAA;AACH,MAAA;AACE,MAAA;AACgD,IAAA;AACnD,MAAA;AACG,MAAA;AACqC,IAAA;AAEzC,MAAA;AACc,MAAA;AACN,QAAA;AACT,QAAA;AACA,QAAA;AACU,QAAA;AACjB,MAAA;AACY,MAAA;AACmB,IAAA;AACC,MAAA;AACrB,MAAA;AACN,IAAA;AACO,MAAA;AACd,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAE4E;AAEP;AAC9B,EAAA;AACjB,IAAA;AACsC,IAAA;AAC1D,EAAA;AACO,EAAA;AACT;AAE+C;AAC/B,EAAA;AACmB,EAAA;AACA,EAAA;AACH,EAAA;AAChC;AAEoD;AACM;AAEM;AAED;AAC5B,EAAA;AACG,EAAA;AACN,EAAA;AACS,EAAA;AAClB,EAAA;AACoB,EAAA;AAC3C;AAEgD;AAC7B,EAAA;AACb,EAAA;AACA,EAAA;AACkD,EAAA;AACK,EAAA;AACA,EAAA;AACF,EAAA;AACA,EAAA;AACA,EAAA;AAC7C,EAAA;AACQ,EAAA;AACyC,EAAA;AACvC,EAAA;AAC0B,EAAA;AAClD;AAUkD;AACV,EAAA;AACX,EAAA;AAChB,EAAA;AACG,IAAA;AACZ,IAAA;AACF,EAAA;AACoE,EAAA;AAC5C,IAAA;AACJ,IAAA;AAClB,IAAA;AACF,EAAA;AAC6B,EAAA;AACc,IAAA;AACzB,IAAA;AACC,MAAA;AACM,MAAA;AACvB,IAAA;AACA,IAAA;AACF,EAAA;AACkB,EAAA;AAC6D,EAAA;AAChC,IAAA;AAC6B,IAAA;AAC5E,EAAA;AAEF;AAEoC;AACT,EAAA;AACoB,EAAA;AAC5B,EAAA;AAEmB,EAAA;AAC5B,EAAA;AACc,EAAA;AACN,IAAA;AACC,IAAA;AACL,MAAA;AACH,MAAA;AACiB,MAAA;AACN,QAAA;AACT,QAAA;AACU,QAAA;AACV,QAAA;AACP,MAAA;AACuD,MAAA;AACxC,MAAA;AACf,MAAA;AACF,IAAA;AACgB,IAAA;AACI,MAAA;AACG,MAAA;AACmB,QAAA;AACvB,QAAA;AACjB,MAAA;AACK,MAAA;AACL,MAAA;AACF,IAAA;AACe,IAAA;AACiC,MAAA;AACzC,MAAA;AACL,MAAA;AACF,IAAA;AACe,IAAA;AACK,MAAA;AACgC,MAAA;AAC7C,MAAA;AACL,MAAA;AACF,IAAA;AACe,IAAA;AAC0C,MAAA;AACrC,MAAA;AAC4B,QAAA;AAChC,QAAA;AACP,MAAA;AACY,QAAA;AACZ,QAAA;AACP,MAAA;AACA,MAAA;AACF,IAAA;AACoE,IAAA;AACxB,IAAA;AACA,IAAA;AACE,IAAA;AACL,IAAA;AACrB,IAAA;AACM,MAAA;AACnB,MAAA;AACoD,MAAA;AAC/C,QAAA;AAC8C,QAAA;AAC3C,UAAA;AACJ,UAAA;AACP,QAAA;AAC2B,QAAA;AAC7B,MAAA;AACA,MAAA;AACyE,IAAA;AACR,MAAA;AAC5D,MAAA;AACL,MAAA;AACqE,IAAA;AAC3C,MAAA;AACrB,MAAA;AACL,MAAA;AACmE,IAAA;AAC1B,MAAA;AACpC,MAAA;AACL,MAAA;AACiE,IAAA;AACC,MAAA;AAC7D,MAAA;AACL,MAAA;AACqC,IAAA;AACd,MAAA;AACf,MAAA;AAC6D,MAAA;AAC3C,MAAA;AAKA,MAAA;AAQA,MAAA;AACR,MAAA;AAClB,MAAA;AACK,IAAA;AACiC,MAAA;AACvB,MAAA;AACjB,IAAA;AACK,IAAA;AACP,EAAA;AACO,EAAA;AACT;AAE0F;AAEtD;AACjB,EAAA;AAC6B,EAAA;AACO,EAAA;AAChC,EAAA;AACZ,IAAA;AACI,IAAA;AACD,IAAA;AACJ,IAAA;AACK,IAAA;AACJ,IAAA;AACE,IAAA;AAC2B,IAAA;AACzB,IAAA;AACN,IAAA;AACK,IAAA;AACI,IAAA;AACuB,IAAA;AACvC,EAAA;AACY,EAAA;AACC,IAAA;AAGoC,IAAA;AAC3C,IAAA;AACkC,IAAA;AAClB,MAAA;AACU,MAAA;AACd,MAAA;AAChB,IAAA;AACmE,IAAA;AAC/D,IAAA;AACuC,IAAA;AAC3B,MAAA;AACc,MAAA;AACd,MAAA;AAChB,IAAA;AACsC,IAAA;AACpB,MAAA;AACsC,MAAA;AAC/B,MAAA;AACD,MAAA;AAC2B,MAAA;AACC,MAAA;AACyB,MAAA;AAC7E,IAAA;AACoB,IAAA;AACZ,IAAA;AACgB,IAAA;AACN,MAAA;AACqC,MAAA;AAC3C,QAAA;AACsC,QAAA;AACiB,QAAA;AACtD,QAAA;AACT,QAAA;AACF,MAAA;AACgD,MAAA;AAChB,MAAA;AAC3B,MAAA;AACP,IAAA;AACWC,IAAAA;AACJ,IAAA;AACT,EAAA;AACgB,EAAA;AACH,IAAA;AACS,IAAA;AACE,IAAA;AAC+B,MAAA;AACnB,MAAA;AAClC,IAAA;AACWA,IAAAA;AACJ,IAAA;AACT,EAAA;AACgD,EAAA;AACf,EAAA;AACpB,IAAA;AACS,IAAA;AACE,IAAA;AAC0B,MAAA;AACd,MAAA;AAClC,IAAA;AACWA,IAAAA;AACJ,IAAA;AACT,EAAA;AAEgB,EAAA;AACF,EAAA;AACE,EAAA;AACI,EAAA;AAIiD,EAAA;AACC,EAAA;AAC1D,EAAA;AACR,EAAA;AACuC,EAAA;AACnB,IAAA;AACA,IAAA;AACN,IAAA;AACqB,IAAA;AACT,IAAA;AAC9B,EAAA;AACc,EAAA;AAC6B,EAAA;AAChB,IAAA;AACb,MAAA;AACV,MAAA;AACF,IAAA;AACF,EAAA;AACwD,EAAA;AACqB,EAAA;AACvC,EAAA;AACpB,IAAA;AACZ,IAAA;AACS,IAAA;AACN,MAAA;AACS,QAAA;AACZ,QAAA;AACG,MAAA;AACoD,QAAA;AACtC,QAAA;AACjB,QAAA;AACG,MAAA;AACY,QAAA;AACf,QAAA;AACG,MAAA;AACgE,QAAA;AAC9D,QAAA;AACO,UAAA;AACA,UAAA;AACZ,QAAA;AACA,QAAA;AACY,MAAA;AACwB,QAAA;AACkB,QAAA;AACpC,UAAA;AACY,UAAA;AACM,QAAA;AAGN,UAAA;AACM,QAAA;AACrB,UAAA;AACe,UAAA;AACvB,QAAA;AACwB,UAAA;AAC/B,QAAA;AACA,QAAA;AACF,MAAA;AACc,MAAA;AAE4D,QAAA;AAEpC,QAAA;AACgC,QAAA;AAClD,UAAA;AACD,UAAA;AACV,QAAA;AACwB,UAAA;AAC/B,QAAA;AACA,QAAA;AACF,MAAA;AACK,MAAA;AACsE,QAAA;AACzE,QAAA;AACF,MAAA;AACE,QAAA;AACJ,IAAA;AACuB,IAAA;AACzB,EAAA;AACkB,EAAA;AAE6D,IAAA;AAC/E,EAAA;AAEoB,EAAA;AACE,EAAA;AACe,IAAA;AACpB,IAAA;AACjB,EAAA;AACW,EAAA;AACJ,EAAA;AACT;AAGuD;AACK,EAAA;AAC5D;ARiiCmF;AACA;ASnkDpD;AAEiD;AACI;AAC5D;AAGgD;AACvD,EAAA;AACkB,EAAA;AACX,EAAA;AAC2D,EAAA;AACR,EAAA;AAChC,EAAA;AACQ,EAAA;AACf,EAAA;AACmB,EAAA;AACvD;AAO2G;AACjE,EAAA;AACwB,EAAA;AACF,EAAA;AAC5B,EAAA;AACsB,EAAA;AACZ,EAAA;AAC9C;AAGiB;AACR,kBAAA;AACP,EAAA;AAAA;AAEO,kBAAA;AACe,EAAA;AACP,IAAA;AACf,EAAA;AACyB,EAAA;AACwC,IAAA;AACjE,EAAA;AACyB,EAAA;AACF,IAAA;AACe,IAAA;AACtC,EAAA;AACF;AAG4D;AACtC,EAAA;AACM,EAAA;AACO,IAAA;AACS,IAAA;AACZ,IAAA;AAC9B,EAAA;AACO,EAAA;AACT;AAG2E;AACrD,EAAA;AACM,EAAA;AACsD,IAAA;AACtC,IAAA;AACZ,IAAA;AAC9B,EAAA;AACO,EAAA;AACT;AAEsG;AAGtC;AACO,EAAA;AACrD,EAAA;AACgE,EAAA;AAC/C,EAAA;AACT,EAAA;AACF,EAAA;AAC2C,EAAA;AAC3C,EAAA;AACd,IAAA;AACC,IAAA;AACT,EAAA;AACmC,EAAA;AACyC,EAAA;AAC7B,EAAA;AACc,EAAA;AACgB,EAAA;AACvC,EAAA;AACG,EAAA;AACF,EAAA;AACpB,EAAA;AACuB,EAAA;AACF,EAAA;AACpB,EAAA;AACM,EAAA;AACJ,IAAA;AACH,MAAA;AACf,MAAA;AACF,IAAA;AACoB,IAAA;AACK,MAAA;AACvB,MAAA;AACF,IAAA;AACY,IAAA;AACR,IAAA;AACS,IAAA;AACN,MAAA;AACqE,QAAA;AACxE,QAAA;AACG,MAAA;AACwE,QAAA;AAC3E,QAAA;AACG,MAAA;AACoE,QAAA;AACvE,QAAA;AACG,MAAA;AAC0C,QAAA;AAC7C,QAAA;AACG,MAAA;AACmB,QAAA;AACtB,QAAA;AACG,MAAA;AACmB,QAAA;AACtB,QAAA;AACa,MAAA;AACyD,QAAA;AAClC,QAAA;AACpC,QAAA;AACF,MAAA;AACK,MAAA;AAC4C,QAAA;AAC/C,QAAA;AACG,MAAA;AACyC,QAAA;AAC5C,QAAA;AACG,MAAA;AACuC,QAAA;AAC1C,QAAA;AACG,MAAA;AACqB,QAAA;AACxB,QAAA;AACG,MAAA;AACsB,QAAA;AACzB,QAAA;AACJ,IAAA;AACU,IAAA;AACZ,EAAA;AACO,EAAA;AACT;AAEsD;AACpC,EAAA;AACgB,EAAA;AAClC;AAQ8H;AAC3G,EAAA;AACP,EAAA;AACS,EAAA;AACa,EAAA;AACD,IAAA;AACF,IAAA;AACE,IAAA;AACN,IAAA;AACU,IAAA;AACwB,MAAA;AAC7C,MAAA;AACH,MAAA;AACT,IAAA;AAC6C,IAAA;AAC7B,IAAA;AACe,MAAA;AACe,MAAA;AACvB,MAAA;AACoC,QAAA;AAC3C,QAAA;AACL,QAAA;AACT,MAAA;AACK,IAAA;AAC0B,MAAA;AACtB,QAAA;AACqB,QAAA;AAC9B,MAAA;AACF,IAAA;AACkC,IAAA;AACpC,EAAA;AACF;AAEgE;AACvC,EAAA;AACN,EAAA;AACT,EAAA;AACoB,EAAA;AACW,IAAA;AACxB,IAAA;AACf,EAAA;AACO,EAAA;AACT;AAGqF;AACrE,EAAA;AAC6B,EAAA;AACpB,IAAA;AACT,MAAA;AACV,MAAA;AACF,IAAA;AACF,EAAA;AACwB,EAAA;AACO,IAAA;AACkD,IAAA;AAChF,EAAA;AACH;AAEyE;AAChD,EAAA;AACiD,EAAA;AACjE,EAAA;AACT;AAG2D;AACvC,EAAA;AAEmD,EAAA;AACd,EAAA;AACnB,EAAA;AACnB,EAAA;AACF,EAAA;AAG8B,EAAA;AACI,EAAA;AACE,EAAA;AACxC,EAAA;AACO,EAAA;AACmB,IAAA;AAC9B,EAAA;AACiD,IAAA;AACJ,IAAA;AACN,IAAA;AACJ,IAAA;AACT,IAAA;AACR,IAAA;AACoB,IAAA;AACqC,IAAA;AAC5E,IAAA;AACA,IAAA;AACmB,IAAA;AAC4B,MAAA;AACX,MAAA;AAClC,MAAA;AACS,MAAA;AAEP,QAAA;AAC2B,QAAA;AACE,QAAA;AAC5B,MAAA;AACsC,QAAA;AACV,QAAA;AAC6B,QAAA;AACtC,QAAA;AACjB,UAAA;AACyD,UAAA;AAChE,QAAA;AACF,MAAA;AACmB,MAAA;AACuB,MAAA;AACF,MAAA;AACG,MAAA;AAChB,MAAA;AACa,MAAA;AACJ,MAAA;AAGR,MAAA;AACZ,QAAA;AACW,QAAA;AACiC,QAAA;AACrD,MAAA;AACmC,QAAA;AAC1C,MAAA;AACK,IAAA;AACkC,MAAA;AACzC,IAAA;AAC+B,IAAA;AACyB,IAAA;AAChC,IAAA;AACyD,IAAA;AAC5C,IAAA;AACI,IAAA;AAC3C,EAAA;AACoB,EAAA;AACQ,EAAA;AACR,IAAA;AACF,IAAA;AAC0B,IAAA;AACF,IAAA;AACA,IAAA;AACgC,IAAA;AACjD,IAAA;AACzB,EAAA;AACW,EAAA;AACJ,EAAA;AACT;AAGyE;AACL,EAAA;AAC3B,EAAA;AACL,EAAA;AAC1B,EAAA;AAC8B,EAAA;AACb,IAAA;AACR,IAAA;AACM,IAAA;AACJ,IAAA;AACI,IAAA;AACa,IAAA;AACjB,IAAA;AACC,IAAA;AACV,IAAA;AACV,EAAA;AACsC,EAAA;AAEC,EAAA;AACnB,EAAA;AACA,EAAA;AACa,EAAA;AACa,EAAA;AACP,EAAA;AACzC;AAGoC;AAC2B,EAAA;AACb,EAAA;AAClD;AAGuI;AAChF,EAAA;AACnC,IAAA;AAC+B,IAAA;AAClB,IAAA;AAC+C,IAAA;AAC9E,EAAA;AACF;AAa0G;AACvF,EAAA;AAC6B,EAAA;AACK,EAAA;AACjC,EAAA;AACJ,EAAA;AACuB,EAAA;AACnB,IAAA;AACJ,MAAA;AACV,MAAA;AACF,IAAA;AACF,EAAA;AACoB,EAAA;AACL,EAAA;AAC2C,EAAA;AACX,EAAA;AACQ,EAAA;AACoB,EAAA;AACvE,EAAA;AACA,EAAA;AAC6B,EAAA;AACF,EAAA;AACK,IAAA;AACoD,IAAA;AACnE,IAAA;AACd,EAAA;AACc,IAAA;AAC6B,IAAA;AAClD,EAAA;AACa,EAAA;AACyB,EAAA;AACC,EAAA;AACW,EAAA;AACsB,EAAA;AACrC,EAAA;AACI,EAAA;AACE,EAAA;AACrC,EAAA;AACA,EAAA;AACwB,EAAA;AACG,IAAA;AACvB,IAAA;AACD,EAAA;AACgD,IAAA;AACT,IAAA;AAC9C,EAAA;AACuC,EAAA;AACR,IAAA;AACtB,IAAA;AACT,EAAA;AACiD,EAAA;AACY,EAAA;AACpB,EAAA;AACoB,IAAA;AACC,IAAA;AACL,IAAA;AACvD,IAAA;AACF,EAAA;AACc,EAAA;AAEyD,IAAA;AACxB,IAAA;AACe,IAAA;AACS,IAAA;AACpD,IAAA;AAC6B,MAAA;AACa,MAAA;AAC3D,IAAA;AACF,EAAA;AACkE,EAAA;AAClD,EAAA;AACiB,EAAA;AACL,EAAA;AACP,IAAA;AACd,EAAA;AAGwD,IAAA;AACC,IAAA;AACzC,IAAA;AACK,IAAA;AACkC,MAAA;AACe,MAAA;AAC5D,MAAA;AACd,IAAA;AACH,EAAA;AACF;AAE4D;AACvB,EAAA;AACrC;ATwgDmF;AACA;AUx8D7D;AAEL;AAO2B;AAiCJ;AAEyC,EAAA;AACjF;AAEoF;AAG1D;AACf,EAAA;AACQ,EAAA;AACA,EAAA;AAAA;AAEA,EAAA;AAAA;AAEA,EAAA;AAEoC,EAAA;AACvC,IAAA;AACc,IAAA;AAE+C,IAAA;AACxD,MAAA;AACE,MAAA;AACE,MAAA;AACA,MAAA;AACnB,MAAA;AACF,IAAA;AACe,IAAA;AACmB,IAAA;AACR,IAAA;AAC0B,IAAA;AACK,IAAA;AACA,IAAA;AAC3D,EAAA;AAAA;AAAA;AAAA;AAAA;AAMyC,EAAA;AACJ,IAAA;AACrC,EAAA;AAAA;AAGiD,EAAA;AACb,IAAA;AACpC,EAAA;AAAA;AAG+C,EAAA;AAC0B,IAAA;AAC1C,IAAA;AACnB,IAAA;AACqB,MAAA;AAC2B,MAAA;AAChC,MAAA;AAC1B,IAAA;AACO,IAAA;AACT,EAAA;AAE+B,EAAA;AACM,IAAA;AACrC,EAAA;AAEqB,EAAA;AACP,IAAA;AACd,EAAA;AAEoC,EAAA;AACY,IAAA;AAChD,EAAA;AAAA;AAGkB,EAAA;AAC0C,IAAA;AAC5D,EAAA;AAEqB,EAAA;AAC0D,IAAA;AAC/E,EAAA;AAAA;AAGkB,EAAA;AACsC,IAAA;AACxD,EAAA;AAAA;AAGyB,EAAA;AACE,IAAA;AACC,IAAA;AACoB,IAAA;AACrB,IAAA;AACV,IAAA;AACkB,IAAA;AACH,IAAA;AACP,IAAA;AACwD,MAAA;AAClD,MAAA;AAC7B,IAAA;AACuB,IAAA;AACL,IAAA;AACQ,IAAA;AACQ,IAAA;AACI,IAAA;AACd,IAAA;AACjB,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAAA;AAMsE,EAAA;AACnC,IAAA;AAClB,IAAA;AACE,IAAA;AACD,IAAA;AACF,IAAA;AACM,IAAA;AACN,IAAA;AACR,MAAA;AAC6B,MAAA;AACK,MAAA;AACb,MAAA;AACL,MAAA;AACT,MAAA;AAES,MAAA;AACqB,MAAA;AAC3C,IAAA;AAC2D,IAAA;AACG,IAAA;AAC5B,IAAA;AACpC,EAAA;AAAA;AAAA;AAAA;AAAA;AAMsE,EAAA;AACC,IAAA;AACvB,MAAA;AACnC,MAAA;AACC,MAAA;AACZ,IAAA;AAC2E,IAAA;AAC1C,IAAA;AACL,IAAA;AACA,IAAA;AACF,IAAA;AACF,IAAA;AACpB,IAAA;AACc,IAAA;AACX,MAAA;AAC2D,QAAA;AAC9D,QAAA;AACG,MAAA;AAC+D,QAAA;AAClE,QAAA;AACG,MAAA;AAC6C,QAAA;AAChD,QAAA;AACF,MAAA;AACqC,QAAA;AACvC,IAAA;AAC8E,IAAA;AAEhB,IAAA;AACa,IAAA;AAC7E,EAAA;AAAA;AAG+C,EAAA;AAC+B,IAAA;AACpC,IAAA;AACR,IAAA;AAC8C,IAAA;AAChF,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQ6C,EAAA;AACmC,IAAA;AACL,IAAA;AAC1C,IAAA;AACmC,MAAA;AACzD,MAAA;AACT,IAAA;AAC2E,IAAA;AACrC,IAAA;AACjB,IAAA;AAC8C,MAAA;AAC1D,MAAA;AACT,IAAA;AACoC,IAAA;AACtC,EAAA;AACF;AAEsG;AAC3E,EAAA;AAClB,EAAA;AACT;AAKuB;AAEO;AACmB;AAGF;AAC5B,EAAA;AACnB;AAIsB;AAM2C;AACpC,EAAA;AACU,IAAA;AAC2C,IAAA;AACN,IAAA;AAC1E,EAAA;AAC+E,EAAA;AACtC,EAAA;AAC1B,EAAA;AAC6C,EAAA;AACK,EAAA;AAC5B,EAAA;AACT,EAAA;AACG,EAAA;AACoC,EAAA;AACxB,EAAA;AACJ,EAAA;AACzC;AAK4F;AACxC,EAAA;AACpD;AAUuG;AACzB,EAAA;AACzC,EAAA;AACU,EAAA;AACJ,EAAA;AACrC,EAAA;AAC2D,EAAA;AACvB,EAAA;AACmC,EAAA;AACtE,EAAA;AACmC,IAAA;AACyC,IAAA;AACR,IAAA;AACnE,IAAA;AACN,EAAA;AACiD,EAAA;AACrB,EAAA;AAC8B,EAAA;AAC5D;AAGuB;AACrB,EAAA;AAAW,EAAA;AAAW,EAAA;AAAW,EAAA;AAAW,EAAA;AAAW,EAAA;AAAW,EAAA;AAAW,EAAA;AAC7E,EAAA;AAAW,EAAA;AAAW,EAAA;AAAW,EAAA;AAAW,EAAA;AAAW,EAAA;AAAW,EAAA;AAAW,EAAA;AAC7E,EAAA;AAAW,EAAA;AAAW,EAAA;AAAW,EAAA;AAAW,EAAA;AAAW,EAAA;AAAW,EAAA;AAAW,EAAA;AAC7E,EAAA;AAAW,EAAA;AAAW,EAAA;AAAW,EAAA;AAAW,EAAA;AAAW,EAAA;AAAW,EAAA;AAAW,EAAA;AAC7E,EAAA;AAAW,EAAA;AAAW,EAAA;AAAW,EAAA;AAAW,EAAA;AAAW,EAAA;AAAW,EAAA;AAAW,EAAA;AAC7E,EAAA;AAAW,EAAA;AAAW,EAAA;AAAW,EAAA;AAAW,EAAA;AAAW,EAAA;AAAW,EAAA;AAAW,EAAA;AAC7E,EAAA;AAAW,EAAA;AAAW,EAAA;AAAW,EAAA;AAAW,EAAA;AAAW,EAAA;AAAW,EAAA;AAAW,EAAA;AAC/E;AAE0C;AACjC,EAAA;AACD,EAAA;AACA,EAAA;AACC,EAAA;AACE,EAAA;AACJ,EAAA;AACE,EAAA;AACC,EAAA;AACV;AAGyE;AAC1C,EAAA;AACX,EAAA;AACmC,EAAA;AACvD;AV45DmF;AACA;AWrwEnC;AAMS;AAMF;AAWP;AACnB,EAAA;AAC7B;AASmB;AACS,EAAA;AACA,EAAA;AACH,EAAA;AACqC,EAAA;AACqB,IAAA;AACjF,EAAA;AACO,EAAA;AACT;AX2uEmF;AACA;AYjyExB;AACjB;AAGqC;AAC/B,EAAA;AAEkB,EAAA;AACK,EAAA;AACd,EAAA;AACzD;AAE6C;AAC3C,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACyD,IAAA;AACnD,IAAA;AACQ,MAAA;AACA,MAAA;AACb,MAAA;AACqB,MAAA;AAC1B,MAAA;AACT,IAAA;AACF,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AAC8D,IAAA;AACxD,IAAA;AACQ,MAAA;AACA,MAAA;AACK,MAAA;AAChB,MAAA;AACqB,MAAA;AACV,MAAA;AAC3B,IAAA;AACF,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACY,IAAA;AACN,IAAA;AACO,MAAA;AACA,MAAA;AACQ,MAAA;AACH,MAAA;AACyB,MAAA;AACxD,IAAA;AACF,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACY,IAAA;AACN,IAAA;AACO,MAAA;AACA,MAAA;AACM,MAAA;AACH,MAAA;AACO,MAAA;AACpC,IAAA;AACF,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACY,IAAA;AACN,IAAA;AACO,MAAA;AACA,MAAA;AACM,MAAA;AACH,MAAA;AACS,MAAA;AACtC,IAAA;AACF,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACA,IAAA;AAC+B,IAAA;AAC9C,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACA,IAAA;AAC6B,IAAA;AAC5C,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACA,IAAA;AACwD,IAAA;AACvE,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACA,IAAA;AAEX,IAAA;AAAU,MAAA;AAC4D,MAAA;AACtE,IAAA;AACJ,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AAC8D,IAAA;AACxD,IAAA;AACQ,MAAA;AACA,MAAA;AACI,MAAA;AACA,MAAA;AAC+B,MAAA;AACtB,MAAA;AACuB,MAAA;AAC/D,IAAA;AACF,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AAC2D,IAAA;AACrD,IAAA;AACO,MAAA;AACA,MAAA;AACI,MAAA;AACA,MAAA;AACyC,MAAA;AACV,MAAA;AAC7D,IAAA;AACF,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AAC2B,IAAA;AACsC,IAAA;AAChF,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AAC2B,IAAA;AACsC,IAAA;AAChF,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACN,IAAA;AACc,IAAA;AACvB,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACN,IAAA;AACG,IAAA;AACkB,IAAA;AAC9B,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACiE,IAAA;AACpE,IAAA;AACS,IAAA;AACU,MAAA;AACA,MAAA;AACH,MAAA;AACA,MAAA;AACG,MAAA;AACF,MAAA;AACc,MAAA;AACa,MAAA;AACtD,IAAA;AACF,EAAA;AACF;AAQmB;AACU,EAAA;AACM,EAAA;AACR,EAAA;AACD,EAAA;AACyB,IAAA;AACjD,EAAA;AACoC,EAAA;AACD,IAAA;AACnC,EAAA;AACyE,EAAA;AAC3E;AZwxEmF;AACA;Aap+Eb;AAGoC;AAC3E,EAAA;AACJ,EAAA;AACH,EAAA;AACA,EAAA;AACG,EAAA;AACa,EAAA;AAC2C,IAAA;AACjF,EAAA;AACkD,EAAA;AACpD;AAEgE;AACrC,EAAA;AACA,EAAA;AACmC,EAAA;AACjD,EAAA;AACqB,EAAA;AACnB,EAAA;AACC,EAAA;AACqC,EAAA;AACnB,EAAA;AAClC;AAEoD;AAClD,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACA,IAAA;AACM,IAAA;AACY,MAAA;AACU,MAAA;AACrB,MAAA;AACN,MAAA;AACA,MAAA;AACc,MAAA;AACD,QAAA;AACY,QAAA;AACxB,UAAA;AACT,UAAA;AACF,QAAA;AACF,MAAA;AACiC,MAAA;AACnC,IAAA;AACF,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACgD,IAAA;AAC1C,IAAA;AACL,MAAA;AACyB,MAAA;AACS,QAAA;AAC9C,MAAA;AACO,MAAA;AACT,IAAA;AACF,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACgD,IAAA;AACQ,IAAA;AACvE,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACsC,IAAA;AAChC,IAAA;AACQ,MAAA;AACC,MAAA;AACU,MAAA;AACtC,IAAA;AACF,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACA,IAAA;AACM,IAAA;AACQ,MAAA;AACA,MAAA;AACK,MAAA;AAEX,MAAA;AACkC,MAAA;AAC9C,MAAA;AACT,IAAA;AACF,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACA,IAAA;AACM,IAAA;AACQ,MAAA;AACA,MAAA;AACK,MAAA;AACX,MAAA;AACkC,MAAA;AAC9C,MAAA;AACT,IAAA;AACF,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACA,IAAA;AACM,IAAA;AACQ,MAAA;AACA,MAAA;AACwB,MAAA;AACJ,MAAA;AACL,MAAA;AACmC,MAAA;AAC7E,IAAA;AACF,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACA,IAAA;AACM,IAAA;AACQ,MAAA;AACA,MAAA;AACc,MAAA;AACb,MAAA;AACV,MAAA;AACQ,MAAA;AACiB,QAAA;AAClB,QAAA;AACE,QAAA;AACd,UAAA;AACK,UAAA;AACd,QAAA;AACF,MAAA;AAEmB,MAAA;AACO,QAAA;AACoB,UAAA;AAC5C,QAAA;AACF,MAAA;AAC4E,MAAA;AAC9E,IAAA;AACF,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACA,IAAA;AACM,IAAA;AACK,MAAA;AACwB,MAAA;AAChD,IAAA;AACF,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACA,IAAA;AACM,IAAA;AACK,MAAA;AACa,MAAA;AACrC,IAAA;AACF,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AAC0D,IAAA;AACH,IAAA;AACtE,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AAC0D,IAAA;AACf,IAAA;AAC1D,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACP,IAAA;AACyC,MAAA;AACa,MAAA;AACxC,MAAA;AACpB,IAAA;AACmB,IAAA;AACO,MAAA;AACA,MAAA;AACK,MAAA;AACJ,MAAA;AACI,MAAA;AACH,MAAA;AACqC,MAAA;AACpD,MAAA;AACe,MAAA;AACqB,QAAA;AAC/C,MAAA;AACO,MAAA;AACT,IAAA;AACF,EAAA;AACF;Abg+EmF;AACA;AchrFjF;AAYoB;AAC4C,EAAA;AACP,EAAA;AAC9B,IAAA;AACC,IAAA;AACqD,IAAA;AACN,MAAA;AACzE,IAAA;AACkC,IAAA;AACI,IAAA;AACC,IAAA;AACD,IAAA;AACgB,MAAA;AACtD,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAGsG;AAC5E,EAAA;AACY,EAAA;AACE,IAAA;AACJ,MAAA;AACK,MAAA;AACD,MAAA;AACJ,MAAA;AACC,MAAA;AACjC,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAIiC;AACnB,EAAA;AACqB,EAAA;AAC1B,EAAA;AACT;AAGsC;AAE+C;AACW;AAGjD;AACL,EAAA;AACd,IAAA;AACC,IAAA;AACa,IAAA;AACb,IAAA;AACZ,IAAA;AACG,IAAA;AACW,MAAA;AACC,MAAA;AACjB,MAAA;AACX,IAAA;AAC8C,IAAA;AACE,IAAA;AAClD,EAAA;AACF;AAGgD;AACN,EAAA;AACZ,IAAA;AACC,IAAA;AACa,IAAA;AACf,IAAA;AACuB,IAAA;AACA,IAAA;AAClD,EAAA;AACF;AAEqB;AACS,EAAA;AAC0B,EAAA;AACxD;AAEoD;AAClD,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACP,IAAA;AACsC,MAAA;AACM,MAAA;AAC0B,MAAA;AAC5E,IAAA;AAC6B,IAAA;AAC/B,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACyD,IAAA;AACxC,IAAA;AAChC,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACuD,IAAA;AACjD,IAAA;AACO,MAAA;AACC,MAAA;AACa,MAAA;AACY,MAAA;AACpD,IAAA;AACF,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACP,IAAA;AACa,IAAA;AACO,MAAA;AACC,MAAA;AACa,MAAA;AACY,MAAA;AACpD,IAAA;AACF,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACP,IAAA;AACsC,MAAA;AACM,MAAA;AAC2B,MAAA;AAC7E,IAAA;AACiC,IAAA;AACnC,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACiE,IAAA;AAC5C,IAAA;AACpC,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AAC6D,IAAA;AAC5C,IAAA;AAChC,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AAC6D,IAAA;AAC5C,IAAA;AAChC,EAAA;AACF;AdypFmF;AACA;Aej0FX;AAC3C,EAAA;AACe,EAAA;AACf,IAAA;AACC,IAAA;AACE,MAAA;AACS,QAAA;AACd,QAAA;AAC4B,QAAA;AACU,QAAA;AACpD,QAAA;AAC0B,UAAA;AACyB,UAAA;AACP,UAAA;AACjD,QAAA;AACF,MAAA;AACA,MAAA;AACF,IAAA;AAC0B,IAAA;AACD,IAAA;AACT,IAAA;AAClB,EAAA;AACgE,EAAA;AACzD,EAAA;AACT;AAEsC;AAEU;AAC9C,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACP,IAAA;AACmD,MAAA;AACM,MAAA;AAClC,MAAA;AAC7B,IAAA;AACmB,IAAA;AACc,MAAA;AACD,MAAA;AACG,MAAA;AACE,MAAA;AACrC,IAAA;AACF,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACqD,IAAA;AAC/C,IAAA;AACQ,MAAA;AACqB,MAAA;AAChD,IAAA;AACF,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACkD,IAAA;AAC5C,IAAA;AACQ,MAAA;AACgB,MAAA;AAC3C,IAAA;AACF,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACP,IAAA;AACuD,MAAA;AAChC,MAAA;AAC7B,IAAA;AACmB,IAAA;AACgC,MAAA;AAChB,QAAA;AACD,QAAA;AACO,QAAA;AACvC,MAAA;AACmE,MAAA;AACrE,IAAA;AACF,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACP,IAAA;AACqC,MAAA;AACmB,MAAA;AACW,MAAA;AACzE,IAAA;AACuB,IAAA;AACJ,IAAA;AACc,MAAA;AACQ,MAAA;AACC,MAAA;AAChC,MAAA;AACgC,MAAA;AACR,QAAA;AACQ,QAAA;AACiC,QAAA;AACzE,MAAA;AAC2C,MAAA;AACkC,MAAA;AAC/E,IAAA;AACF,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACE,IAAA;AACI,IAAA;AACe,MAAA;AACqB,MAAA;AACvD,IAAA;AACF,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACE,IAAA;AACI,IAAA;AACe,MAAA;AACoB,MAAA;AACtD,IAAA;AACF,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACE,IAAA;AACI,IAAA;AACe,MAAA;AACuC,MAAA;AACzE,IAAA;AACF,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AAC4C,IAAA;AACtC,IAAA;AACU,MAAA;AACK,MAAA;AAClC,IAAA;AACF,EAAA;AACF;Afi0FmF;AACA;AgBr9FhC;AAGuC;AACjE,EAAA;AACE,EAAA;AACqB,EAAA;AACpB,EAAA;AACoD,EAAA;AACzD,EAAA;AACvB;AAG8D;AACpC,EAAA;AACG,EAAA;AACf,EAAA;AACe,EAAA;AACW,IAAA;AAC5B,IAAA;AACV,EAAA;AACwC,EAAA;AACG,EAAA;AACqB,EAAA;AACa,EAAA;AAC7C,IAAA;AACL,IAAA;AAC3B,EAAA;AAC8B,EAAA;AAC4C,IAAA;AACtC,IAAA;AACpC,EAAA;AACO,EAAA;AACT;AAG0F;AAC/D,EAAA;AACE,EAAA;AACA,EAAA;AACE,EAAA;AACC,EAAA;AACJ,EAAA;AACoB,EAAA;AACV,IAAA;AACpC,EAAA;AACI,EAAA;AACiB,EAAA;AAEwC,IAAA;AACtB,IAAA;AACa,IAAA;AACR,IAAA;AACrC,EAAA;AACqC,IAAA;AAC5C,EAAA;AAC4E,EAAA;AAC7D,EAAA;AACjB;AAE6C;AAC3C,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACsD,IAAA;AAChD,IAAA;AACY,MAAA;AACU,MAAA;AACrB,MAAA;AACc,MAAA;AAClC,IAAA;AACF,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACP,IAAA;AACkD,MAAA;AACK,MAAA;AACA,MAAA;AAC7D,IAAA;AACmB,IAAA;AACW,MAAA;AACE,MAAA;AACG,MAAA;AACD,MAAA;AACF,MAAA;AACS,MAAA;AACrB,MAAA;AAIV,MAAA;AACV,IAAA;AACF,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACF,IAAA;AACQ,IAAA;AACM,MAAA;AACuB,MAAA;AAChD,IAAA;AACF,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACF,IAAA;AACQ,IAAA;AACM,MAAA;AACuB,MAAA;AAChD,IAAA;AACF,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACF,IAAA;AACQ,IAAA;AACM,MAAA;AACE,MAAA;AAC8C,MAAA;AACzE,IAAA;AACF,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACF,IAAA;AACQ,IAAA;AACM,MAAA;AAEuC,MAAA;AAChE,IAAA;AACF,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACF,IAAA;AACQ,IAAA;AACM,MAAA;AACW,MAAA;AACpC,IAAA;AACF,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACsB,IAAA;AAChB,IAAA;AACmB,MAAA;AACsC,MAAA;AAC5E,IAAA;AACF,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACsB,IAAA;AAChB,IAAA;AACmB,MAAA;AACE,MAAA;AACuB,MAAA;AAC/D,IAAA;AACF,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACP,IAAA;AACJ,MAAA;AACyE,MAAA;AACd,MAAA;AAC7D,IAAA;AACmB,IAAA;AACgB,MAAA;AACK,MAAA;AACX,MAAA;AACD,MAAA;AAC2C,MAAA;AACX,MAAA;AAC5D,IAAA;AACF,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACP,IAAA;AACwC,MAAA;AACI,MAAA;AACuB,MAAA;AACzE,IAAA;AACsC,IAAA;AACxC,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACP,IAAA;AACwC,MAAA;AACI,MAAA;AACuB,MAAA;AACzE,IAAA;AACqC,IAAA;AACvC,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACP,IAAA;AACJ,MAAA;AAC8C,MAAA;AACC,MAAA;AACyB,MAAA;AAC1E,IAAA;AACmB,IAAA;AACM,MAAA;AACE,MAAA;AACC,MAAA;AACE,MAAA;AACF,MAAA;AACE,MAAA;AACD,MAAA;AAC6B,MAAA;AAC1B,MAAA;AACD,MAAA;AAC2C,MAAA;AAC5D,MAAA;AACuB,MAAA;AACM,QAAA;AACd,QAAA;AAC3B,MAAA;AACyE,MAAA;AAC3E,IAAA;AACF,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACP,IAAA;AACyC,MAAA;AAC+B,MAAA;AAChB,MAAA;AACb,MAAA;AACjD,IAAA;AACmB,IAAA;AACM,MAAA;AACE,MAAA;AACE,MAAA;AACD,MAAA;AAC2C,MAAA;AAC1C,MAAA;AACD,MAAA;AAC2C,MAAA;AAC3C,MAAA;AACE,MAAA;AAC4C,MAAA;AAC1E,IAAA;AACF,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACkE,IAAA;AAC5D,IAAA;AACgB,MAAA;AACK,MAAA;AACO,MAAA;AACf,QAAA;AAC9B,MAAA;AACoC,MAAA;AACtC,IAAA;AACF,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACgD,IAAA;AAC1C,IAAA;AACG,MAAA;AACE,MAAA;AACF,MAAA;AACE,MAAA;AACT,MAAA;AACf,IAAA;AACF,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACF,IAAA;AACQ,IAAA;AACS,MAAA;AACQ,MAAA;AACI,MAAA;AACX,MAAA;AAEiD,MAAA;AACN,MAAA;AAC/D,MAAA;AACT,IAAA;AACF,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AAC4D,IAAA;AACtD,IAAA;AACS,MAAA;AACQ,MAAA;AACT,MAAA;AACE,MAAA;AACoB,MAAA;AACX,MAAA;AACJ,MAAA;AAEC,QAAA;AAEjC,MAAA;AACc,MAAA;AAChB,IAAA;AACF,EAAA;AACF;AhBw8FmF;AACA;AiB/vGzB;AAUyD;AAC7E,EAAA;AACA,EAAA;AACT,EAAA;AAEkB,EAAA;AACwB,IAAA;AAC5C,IAAA;AACiB,MAAA;AACO,MAAA;AACgC,MAAA;AACpE,QAAA;AACT,MAAA;AACF,IAAA;AACO,IAAA;AACT,EAAA;AAEqD,EAAA;AACvC,IAAA;AAC4B,IAAA;AACA,MAAA;AACmC,MAAA;AAClC,MAAA;AACkB,MAAA;AACpD,MAAA;AACP,IAAA;AACO,IAAA;AACT,EAAA;AAEW,EAAA;AACP,EAAA;AACmB,EAAA;AACiB,IAAA;AACmC,IAAA;AAClC,IAAA;AACjB,IAAA;AACsC,IAAA;AAEK,IAAA;AACxC,IAAA;AAChB,MAAA;AACK,MAAA;AACd,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAG6G;AAC9E,EAAA;AACH,EAAA;AACgB,EAAA;AAC4B,IAAA;AACtE,EAAA;AACqC,EAAA;AACvC;AAGiD;AAGyB;AAC7C,EAAA;AACQ,EAAA;AACV,EAAA;AACC,EAAA;AACC,EAAA;AACD,EAAA;AACa,EAAA;AACPC,EAAAA;AAEW,EAAA;AAC0B,EAAA;AACnD,EAAA;AAC2D,IAAA;AAC7E,EAAA;AAE4E,EAAA;AACJ,EAAA;AACxB,EAAA;AACiC,EAAA;AACnF;AAE+C;AAC7C,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACP,IAAA;AACwD,MAAA;AACV,MAAA;AACyB,MAAA;AAClD,MAAA;AAC3B,IAAA;AACwC,IAAA;AAC1C,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACP,IAAA;AACqD,MAAA;AACP,MAAA;AACmB,MAAA;AAC5C,MAAA;AAC3B,IAAA;AACyC,IAAA;AAC3C,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACP,IAAA;AACoC,MAAA;AACwB,MAAA;AACzC,MAAA;AACzB,IAAA;AACmB,IAAA;AACU,MAAA;AACQ,MAAA;AACT,MAAA;AACD,MAAA;AACG,MAAA;AACH,MAAA;AACmD,MAAA;AACtB,MAAA;AACM,MAAA;AAC9D,IAAA;AACF,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACP,IAAA;AAC2C,MAAA;AACmB,MAAA;AACS,MAAA;AAC7E,IAAA;AACmB,IAAA;AACQ,MAAA;AACC,MAAA;AACH,MAAA;AACC,MAAA;AAC2B,MAAA;AAC3B,MAAA;AAE+B,MAAA;AAC/C,QAAA;AACA,QAAA;AACR,MAAA;AACiC,MAAA;AACyC,QAAA;AAC1E,MAAA;AACiC,MAAA;AACyC,QAAA;AAC1E,MAAA;AACyC,MAAA;AACwB,MAAA;AACA,MAAA;AAC1B,MAAA;AACzC,IAAA;AACF,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACP,IAAA;AACoC,MAAA;AACwB,MAAA;AAC5C,MAAA;AACoD,MAAA;AACjD,MAAA;AACqD,MAAA;AAC9E,IAAA;AACmB,IAAA;AACU,MAAA;AACQ,MAAA;AACT,MAAA;AACD,MAAA;AACC,MAAA;AACC,MAAA;AACO,MAAA;AACJ,MAAA;AACK,MAAA;AACJ,MAAA;AAE6B,MAAA;AACjC,MAAA;AACiD,MAAA;AACxC,QAAA;AACpC,MAAA;AACkG,MAAA;AACtE,MAAA;AACyC,MAAA;AACO,MAAA;AAEN,MAAA;AACM,MAAA;AAC9D,MAAA;AAC0D,QAAA;AACxE,MAAA;AACsE,MAAA;AACxE,IAAA;AACF,EAAA;AACF;AjBwuGmF;AACA;AkB38Gb;AACW;AAG5B;AACoB,EAAA;AACU,EAAA;AACjC,EAAA;AACzC,EAAA;AACT;AAE8F;AACrE,EAAA;AACe,EAAA;AACxC;AAE2E;AAE9B;AAC3C,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACN,IAAA;AACG,IAAA;AAAA;AAE8B,IAAA;AAC1C,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACN,IAAA;AACG,IAAA;AACkC,IAAA;AAC9C,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACP,IAAA;AACwD,MAAA;AAClB,MAAA;AACE,MAAA;AAC9C,IAAA;AACmB,IAAA;AACO,MAAA;AACC,MAAA;AACE,MAAA;AACD,MAAA;AACD,MAAA;AACD,MAAA;AACqC,MAAA;AACpC,MAAA;AACe,MAAA;AACxB,MAAA;AAC4C,MAAA;AAChC,QAAA;AAC5B,MAAA;AACyB,MAAA;AAC3B,IAAA;AACF,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACF,IAAA;AAC4C,IAAA;AACzD,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACF,IAAA;AAC6C,IAAA;AAC1D,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACF,IAAA;AACwC,IAAA;AACrD,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACwB,IAAA;AAClB,IAAA;AACM,MAAA;AACE,MAAA;AACI,MAAA;AACJ,MAAA;AACD,MAAA;AACK,MAAA;AACU,MAAA;AACM,MAAA;AACO,MAAA;AACtD,IAAA;AACF,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACgD,IAAA;AAC1C,IAAA;AACK,MAAA;AACE,MAAA;AACA,MAAA;AACE,MAAA;AACG,MAAA;AAC/B,IAAA;AACF,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACoB,IAAA;AACd,IAAA;AACO,MAAA;AACE,MAAA;AACE,MAAA;AACD,MAAA;AACkB,MAAA;AAC/C,IAAA;AACF,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACgE,IAAA;AAC1D,IAAA;AACO,MAAA;AACE,MAAA;AACE,MAAA;AACD,MAAA;AAC2C,MAAA;AACxE,IAAA;AACF,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AAC0D,IAAA;AACpD,IAAA;AACO,MAAA;AACE,MAAA;AACJ,MAAA;AACE,MAAA;AACS,MAAA;AACjB,MAAA;AACS,QAAA;AAC+B,QAAA;AAClB,UAAA;AACd,UAAA;AACK,UAAA;AAC8C,UAAA;AACvC,UAAA;AAClC,QAAA;AACF,MAAA;AACgD,MAAA;AACQ,MAAA;AAC5C,MAAA;AACsB,MAAA;AACD,MAAA;AAC4C,QAAA;AAClB,QAAA;AAC3D,MAAA;AACc,MAAA;AAChB,IAAA;AACF,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACP,IAAA;AACJ,MAAA;AACyC,MAAA;AAC7B,MAAA;AACd,IAAA;AACmB,IAAA;AACO,MAAA;AACE,MAAA;AACJ,MAAA;AACE,MAAA;AACD,MAAA;AACE,MAAA;AACkC,MAAA;AAEgB,MAAA;AACtC,MAAA;AACF,MAAA;AAC5B,QAAA;AAC0B,UAAA;AAC1B,QAAA;AACI,UAAA;AACJ,QAAA;AAC0B,UAAA;AAC1B,QAAA;AACa,UAAA;AACP,QAAA;AAC6B,UAAA;AACR,UAAA;AAChC,QAAA;AACW,QAAA;AACmD,UAAA;AAC9B,UAAA;AAChC,QAAA;AACA,QAAA;AACkC,UAAA;AACpC,MAAA;AACF,IAAA;AACF,EAAA;AACF;AlBu8GmF;AACA;AmBpqH3B;AAGgD;AAEpD;AAClD,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACD,IAAA;AACW,IAAA;AACzB,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACD,IAAA;AAC2C,IAAA;AACzD,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACD,IAAA;AACF,IAAA;AACmE,MAAA;AAC7E,IAAA;AACF,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACD,IAAA;AACkB,IAAA;AAChC,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACD,IAAA;AAC+B,IAAA;AAC7C,EAAA;AACF;AnBmqHmF;AACA;AoB9rH3D;AAG6D;AACtD,EAAA;AACwB,EAAA;AACnB,EAAA;AACS,EAAA;AAC7C;AAGsD;AAChC,EAAA;AACW,EAAA;AACU,IAAA;AACzC,EAAA;AAC4C,EAAA;AAC9C;AAEyD;AACnC,EAAA;AAC+C,EAAA;AAC5D,EAAA;AACT;AAEyD;AACnC,EAAA;AAC+C,EAAA;AAC5D,EAAA;AACT;AAK6C;AAI4B;AAC/B,EAAA;AACpB,EAAA;AACsC,EAAA;AACZ,EAAA;AAChD;AAGuE;AAC7C,EAAA;AACA,EAAA;AAC0C,EAAA;AACL,EAAA;AAC/D;AAEmF;AAC/C,EAAA;AACR,EAAA;AACkD,EAAA;AAC9E;AAGuD;AACnB,EAAA;AACN,EAAA;AACc,EAAA;AAC5C;AAE8C;AAC5C,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACP,IAAA;AACiD,MAAA;AACtC,MAAA;AACuD,MAAA;AACxE,IAAA;AACmB,IAAA;AACY,MAAA;AACH,MAAA;AACK,MAAA;AACH,MAAA;AAExB,MAAA;AAC2D,MAAA;AACK,MAAA;AAC/D,MAAA;AAC+B,QAAA;AACpC,MAAA;AAEkC,MAAA;AACX,MAAA;AACgB,MAAA;AACK,QAAA;AACZ,QAAA;AAC2B,UAAA;AACzD,QAAA;AAC4B,QAAA;AAC9B,MAAA;AACuB,MAAA;AACkC,QAAA;AACzD,MAAA;AAC6D,MAAA;AAC/D,IAAA;AACF,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACP,IAAA;AAC+C,MAAA;AAC5B,MAAA;AACqD,MAAA;AACzD,MAAA;AACrB,IAAA;AACmB,IAAA;AACY,MAAA;AACH,MAAA;AACI,MAAA;AACJ,MAAA;AACI,MAAA;AACJ,MAAA;AACQ,MAAA;AACR,MAAA;AACe,MAAA;AACT,MAAA;AACqC,QAAA;AACrE,MAAA;AAEoC,MAAA;AAGA,MAAA;AACtC,IAAA;AACF,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACP,IAAA;AAC+C,MAAA;AACyB,MAAA;AACA,MAAA;AACC,MAAA;AACH,MAAA;AAC5E,IAAA;AACuB,IAAA;AACJ,IAAA;AACY,MAAA;AACH,MAAA;AAC6B,MAAA;AAC3B,MAAA;AACiB,MAAA;AACjB,QAAA;AACH,QAAA;AACnB,QAAA;AACoD,QAAA;AACK,QAAA;AACxD,QAAA;AAC+B,UAAA;AACpC,QAAA;AAC0C,QAAA;AACN,UAAA;AACpC,QAAA;AACQ,QAAA;AAC0B,QAAA;AACR,QAAA;AACY,QAAA;AACxC,MAAA;AAC2B,MAAA;AAES,MAAA;AAER,QAAA;AAC6C,UAAA;AAC1C,UAAA;AAC3B,QAAA;AACsB,QAAA;AAEE,MAAA;AAC0C,MAAA;AACxE,IAAA;AACF,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACP,IAAA;AACuC,MAAA;AACiC,MAAA;AACnD,MAAA;AAC3B,IAAA;AACmB,IAAA;AACY,MAAA;AACH,MAAA;AACQ,MAAA;AACR,MAAA;AACc,MAAA;AACR,MAAA;AAIzB,MAAA;AACa,QAAA;AACY,UAAA;AAC2C,UAAA;AACtE,QAAA;AACH,MAAA;AAC6D,MAAA;AACrB,MAAA;AAClB,QAAA;AACM,QAAA;AACX,QAAA;AACsB,QAAA;AACzC,MAAA;AAEqC,MAAA;AAEc,MAAA;AACW,MAAA;AAChE,IAAA;AACF,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACP,IAAA;AACuC,MAAA;AACuB,MAAA;AACP,MAAA;AACJ,MAAA;AACzD,IAAA;AACmB,IAAA;AACS,MAAA;AACD,MAAA;AACI,MAAA;AACJ,MAAA;AACI,MAAA;AACH,MAAA;AACE,MAAA;AACH,MAAA;AAC4B,MAAA;AACQ,MAAA;AACtC,MAAA;AACQ,MAAA;AACwC,QAAA;AACvE,MAAA;AACkE,MAAA;AACpE,IAAA;AACF,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACmD,IAAA;AAC7C,IAAA;AACY,MAAA;AACH,MAAA;AAC8B,MAAA;AACpC,MAAA;AAEiB,MAAA;AACgC,QAAA;AACrE,MAAA;AACiD,MAAA;AACnD,IAAA;AACF,EAAA;AACA,EAAA;AACQ,IAAA;AACI,IAAA;AACG,IAAA;AACP,IAAA;AACwD,MAAA;AACM,MAAA;AACN,MAAA;AACD,MAAA;AACkB,MAAA;AAC/E,IAAA;AACU,IAAA;AACS,IAAA;AACY,MAAA;AACJ,MAAA;AACI,MAAA;AACJ,MAAA;AACE,MAAA;AACHC,MAAAA;AACG,MAAA;AACHC,MAAAA;AACY,MAAA;AACR,MAAA;AACyB,MAAA;AACQ,MAAA;AACZ,MAAA;AACkB,MAAA;AACrC,QAAA;AAC9B,MAAA;AACsED,MAAAA;AAC/C,MAAA;AAC8B,MAAA;AACa,MAAA;AACpE,IAAA;AACF,EAAA;AACA,EAAA;AAAA;AAAA;AAGQ,IAAA;AACI,IAAA;AAER,IAAA;AACI,IAAA;AACiE,MAAA;AACrB,MAAA;AAClD,IAAA;AACuB,IAAA;AACiB,IAAA;AAC1C,EAAA;AACF;AAOwF;AACjF,EAAA;AACE,EAAA;AACE,EAAA;AACE,EAAA;AACN,EAAA;AACA,EAAA;AACE,EAAA;AACD,EAAA;AACA,EAAA;AACG,EAAA;AACF,EAAA;AACF,EAAA;AACE,EAAA;AACA,EAAA;AACD,EAAA;AACE,EAAA;AACF,EAAA;AACC,EAAA;AACF,EAAA;AACC,EAAA;AACE,EAAA;AACI,EAAA;AACH,EAAA;AACH,EAAA;AACC,EAAA;AACA,EAAA;AACD,EAAA;AACA,EAAA;AACC,EAAA;AACF,EAAA;AACI,EAAA;AACF,EAAA;AACE,EAAA;AACH,EAAA;AACD,EAAA;AACI,EAAA;AACC,EAAA;AACF,EAAA;AACC,EAAA;AACH,EAAA;AACA,EAAA;AACK,EAAA;AACH,EAAA;AACV;ApBwpHmF;AACA;AqBhhIJ;AAC1E,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACU;AAEoC,EAAA;AAC+B,EAAA;AACjF;ArBihIkF;AACA;AsB/7HvB;AACvC;AAEW;AACV,EAAA;AACH,EAAA;AAC4B,EAAA;AACY,IAAA;AACzD,EAAA;AACF;AAMqF;AACpE,EAAA;AACiC,EAAA;AACT,EAAA;AAC+B,IAAA;AACtE,EAAA;AAC0C,EAAA;AAC5C;AAGiE;AAChD,EAAA;AAC0B,EAAA;AAC3C;AAEwF;AACvE,EAAA;AACuB,EAAA;AACxC;AAGsG;AACrF,EAAA;AAEqB,EAAA;AAEtC;AAS+B;AACM,EAAA;AACc,EAAA;AACQ,EAAA;AACoB,EAAA;AACtC,EAAA;AACgC,EAAA;AACzE;AAUkE;AACJ,EAAA;AACqB,EAAA;AACF,EAAA;AACjF;AAMsG;AACnD,EAAA;AACb,EAAA;AACL,EAAA;AACsB,EAAA;AACvD;AAMwG;AACjE,EAAA;AACsB,EAAA;AACP,EAAA;AACQ,EAAA;AACZ,EAAA;AACM,EAAA;AACU,EAAA;AAE/C,EAAA;AACqD,IAAA;AAClB,IAAA;AACpD,EAAA;AACiB,EAAA;AACoD,IAAA;AACS,IAAA;AAC9E,EAAA;AAC8C,EAAA;AACC,IAAA;AACmB,IAAA;AACV,MAAA;AACQ,MAAA;AAC9D,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAkB0B;AACoD,EAAA;AAC1D,EAAA;AAC+B,EAAA;AAEhC,EAAA;AACE,EAAA;AACiE,EAAA;AACtB,IAAA;AACV,IAAA;AAC0B,MAAA;AAC5E,IAAA;AACF,EAAA;AAE+F,EAAA;AAC9B,EAAA;AACgB,IAAA;AACF,IAAA;AAC/E,EAAA;AAEuE,EAAA;AAEjD,EAAA;AACa,IAAA;AACP,IAAA;AACsB,MAAA;AACE,MAAA;AACjD,IAAA;AACyB,IAAA;AACuB,MAAA;AACC,MAAA;AACjD,IAAA;AAC4D,IAAA;AAC/D,EAAA;AAEwD,EAAA;AACjD,EAAA;AACT;AAG2F;AAG/E,EAAA;AACZ;AtB83HmF;AACA;AuB9kIxB;AAC9C,EAAA;AACA,EAAA;AACF,EAAA;AACC,EAAA;AACF,EAAA;AACC,EAAA;AACE,EAAA;AACA,EAAA;AACA,EAAA;AACD,EAAA;AACZ;AAG2D;AACJ,EAAA;AACvD;AAE0F;AAGpD;AAG2B;AACV,EAAA;AACnB,EAAA;AACU,EAAA;AAC9C;AAMqE;AACjC,EAAA;AACA,EAAA;AACD,EAAA;AACA,EAAA;AACc,EAAA;AACT,EAAA;AACxC;AAG8F;AACxE,EAAA;AACW,EAAA;AACmB,IAAA;AAClD,EAAA;AACqC,EAAA;AACvC;AAGgG;AAEC;AAEnE;AACpB,EAAA;AACoC,kBAAA;AAAA;AAED,kBAAA;AAEE,EAAA;AAC5B,IAAA;AACjB,EAAA;AAAA;AAAA;AAAA;AAAA;AAMqD,EAAA;AACyB,IAAA;AAChD,IAAA;AAC9B,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQwF,EAAA;AACnE,IAAA;AACM,IAAA;AACL,IAAA;AACL,IAAA;AACX,IAAA;AACoC,MAAA;AAClC,MAAA;AACsB,MAAA;AAC0B,QAAA;AAC1B,QAAA;AAC1B,MAAA;AAC2B,MAAA;AACZ,QAAA;AACkD,QAAA;AACvB,QAAA;AACtC,UAAA;AACO,UAAA;AACkD,UAAA;AAC3D,QAAA;AAC0B,QAAA;AACnB,QAAA;AACT,MAAA;AAC0C,MAAA;AACzB,IAAA;AACqD,MAAA;AACtE,IAAA;AACmB,MAAA;AACL,MAAA;AAChB,IAAA;AACF,EAAA;AAAA;AAG8D,EAAA;AACrB,IAAA;AAChC,IAAA;AACT,EAAA;AAEyC,EAAA;AACpB,IAAA;AACZ,MAAA;AACmD,QAAA;AACE,QAAA;AAC5C,QAAA;AAEE,MAAA;AACiB,QAAA;AAC4B,UAAA;AAC3D,QAAA;AACyD,QAAA;AAC9B,QAAA;AACpB,UAAA;AACH,YAAA;AAEoB,YAAA;AAEtB,UAAA;AACF,QAAA;AACO,QAAA;AACT,MAAA;AAEK,MAAA;AAC2B,QAAA;AAE3B,MAAA;AAC8B,QAAA;AAEtB,MAAA;AACuC,QAAA;AACsB,UAAA;AACxE,QAAA;AACmE,QAAA;AACrE,MAAA;AAEoB,MAAA;AACiB,QAAA;AACwB,UAAA;AAC3D,QAAA;AACiD,QAAA;AACnD,MAAA;AAEgB,MAAA;AACqD,QAAA;AACxB,QAAA;AACpB,QAAA;AACa,UAAA;AACuC,UAAA;AAC3E,QAAA;AAC0C,QAAA;AAC5C,MAAA;AAEiB,MAAA;AACyB,QAAA;AACP,QAAA;AACS,QAAA;AACR,QAAA;AACa,QAAA;AAEvB,UAAA;AACC,UAAA;AACa,UAAA;AACA,UAAA;AACgC,UAAA;AAC5B,UAAA;AACL,YAAA;AACHE,YAAAA;AACK,YAAA;AACJ,YAAA;AACE,YAAA;AAClC,UAAA;AACH,QAAA;AACyB,QAAA;AACK,QAAA;AACJ,QAAA;AACI,QAAA;AACE,QAAA;AAClC,MAAA;AAEK,MAAA;AAC0B,QAAA;AAE/B,MAAA;AAC2D,QAAA;AAC7D,IAAA;AACF,EAAA;AAEyC,EAAA;AACf,IAAA;AACC,IAAA;AAC0C,MAAA;AACnE,IAAA;AACyB,IAAA;AACF,IAAA;AAC4B,IAAA;AACrD,EAAA;AAAA;AAGuD,EAAA;AACT,IAAA;AACZ,MAAA;AACM,QAAA;AACE,QAAA;AACmC,QAAA;AACzE,MAAA;AACuB,MAAA;AACqC,MAAA;AACpC,MAAA;AACoC,MAAA;AAClD,MAAA;AACA,MAAA;AACN,MAAA;AACQ,MAAA;AACL,QAAA;AACU,UAAA;AACb,UAAA;AACG,QAAA;AACU,UAAA;AACb,UAAA;AACG,QAAA;AACU,UAAA;AACb,UAAA;AACG,QAAA;AACoE,UAAA;AAC1D,UAAA;AACb,UAAA;AACF,QAAA;AACwB,UAAA;AAC1B,MAAA;AAC6D,MAAA;AAC/D,IAAA;AAEY,IAAA;AACL,MAAA;AAC+B,QAAA;AAC/B,MAAA;AACmC,QAAA;AACnC,MAAA;AACmC,QAAA;AACnC,MAAA;AACiC,QAAA;AACjC,MAAA;AACkC,QAAA;AAClC,MAAA;AACiC,QAAA;AACjC,MAAA;AACkC,QAAA;AACvC,MAAA;AACsD,QAAA;AACxD,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAAA;AAMiH,EAAA;AACpF,IAAA;AACmB,MAAA;AACsB,MAAA;AACpE,IAAA;AAC6B,IAAA;AACwB,MAAA;AACrD,IAAA;AAC0B,IAAA;AACqB,IAAA;AACd,IAAA;AACN,IAAA;AACoC,IAAA;AACrD,IAAA;AAC4D,IAAA;AACE,IAAA;AACP,MAAA;AACjE,IAAA;AAC4E,IAAA;AAC9E,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOqD,EAAA;AACX,IAAA;AACN,IAAA;AACY,IAAA;AACH,IAAA;AACJ,IAAA;AACH,MAAA;AACqB,MAAA;AACiB,MAAA;AAClD,MAAA;AACC,QAAA;AAC8B,QAAA;AACxB,MAAA;AACU,QAAA;AACc,QAAA;AACrD,MAAA;AAC2E,MAAA;AAC7E,IAAA;AACc,IAAA;AAChB,EAAA;AAEkD,EAAA;AAChB,IAAA;AACwB,MAAA;AACxD,IAAA;AAC0D,IAAA;AACjB,IAAA;AACO,MAAA;AAClC,MAAA;AACV,QAAA;AAEsC,QAAA;AAExC,MAAA;AACF,IAAA;AAC0E,IAAA;AAClC,IAAA;AAC2D,IAAA;AAC5F,IAAA;AACT,EAAA;AAE8C,EAAA;AACZ,IAAA;AACkB,MAAA;AAClD,IAAA;AAC4E,IAAA;AACzC,IAAA;AAEqC,IAAA;AACK,IAAA;AACrC,IAAA;AACyD,IAAA;AAC1F,IAAA;AACT,EAAA;AAEkD,EAAA;AACR,IAAA;AAC4B,IAAA;AAEF,IAAA;AACZ,IAAA;AAEF,IAAA;AAEX,IAAA;AACpB,IAAA;AACyB,MAAA;AACX,MAAA;AACnC,IAAA;AACuE,IAAA;AACzE,EAAA;AAEqE,EAAA;AACP,IAAA;AACS,MAAA;AACrE,IAAA;AACsD,IAAA;AACxD,EAAA;AAE2D,EAAA;AACtB,IAAA;AACA,IAAA;AACjC,MAAA;AACA,MAAA;AACa,MAAA;AACoD,QAAA;AACzC,QAAA;AACxB,MAAA;AACA,IAAA;AACJ,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQqF,EAAA;AACI,IAAA;AACX,IAAA;AACG,IAAA;AAEpE,IAAA;AACA,IAAA;AACW,IAAA;AACI,MAAA;AACO,MAAA;AACC,MAAA;AACA,MAAA;AAClC,IAAA;AACoE,IAAA;AAC5B,IAAA;AACiB,MAAA;AACtB,QAAA;AACa,QAAA;AACgB,QAAA;AAC7D,MAAA;AAC2E,MAAA;AAC7E,IAAA;AACH,EAAA;AAAA;AAGiD,EAAA;AACf,IAAA;AACF,MAAA;AAC9B,IAAA;AACmC,IAAA;AACb,IAAA;AAClB,IAAA;AACgD,MAAA;AACpB,QAAA;AACE,QAAA;AACiB,UAAA;AAC/C,QAAA;AAC4D,QAAA;AAC9D,MAAA;AACwD,MAAA;AACxD,IAAA;AACgB,MAAA;AAClB,IAAA;AACF,EAAA;AAEuG,EAAA;AAE3C,IAAA;AACW,IAAA;AAE9B,IAAA;AAEL,IAAA;AACX,MAAA;AACS,MAAA;AACoB,MAAA;AACsB,MAAA;AAC1E,IAAA;AAEsE,IAAA;AAClB,MAAA;AAC5B,MAAA;AACQ,MAAA;AACR,MAAA;AACoD,MAAA;AAC5E,IAAA;AAE0D,IAAA;AAClC,MAAA;AACiC,MAAA;AAC7B,QAAA;AACF,QAAA;AAEiB,UAAA;AAChC,QAAA;AACO,UAAA;AACd,QAAA;AACF,MAAA;AACO,MAAA;AACT,IAAA;AAEO,IAAA;AACK,MAAA;AACV,MAAA;AAC0E,MAAA;AAC1E,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AAC8B,MAAA;AACM,QAAA;AACyB,QAAA;AAC7D,MAAA;AAC2B,MAAA;AACyB,QAAA;AAC5B,QAAA;AACiB,QAAA;AACzC,MAAA;AAC8B,MAAA;AACsB,QAAA;AAC5B,QAAA;AACQ,QAAA;AACN,QAAA;AACiC,QAAA;AAC3D,MAAA;AACiB,MAAA;AACO,QAAA;AACQ,QAAA;AACoB,QAAA;AACS,QAAA;AAC7D,MAAA;AACkB,MAAA;AACK,QAAA;AACwB,QAAA;AACf,QAAA;AACsB,QAAA;AACtD,MAAA;AACA,MAAA;AACuB,MAAA;AAGG,QAAA;AACiB,QAAA;AACN,UAAA;AACe,UAAA;AACe,UAAA;AAChC,UAAA;AACjC,QAAA;AACO,QAAA;AACT,MAAA;AACF,IAAA;AACF,EAAA;AACF;AvBghImF;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/home/runner/work/editor/editor/packages/core/dist/chunk-MCSJFWAO.cjs","sourcesContent":[null,"export type FormulaTokenType =\n | 'NUMBER'\n | 'STRING'\n | 'BOOLEAN'\n | 'ERROR'\n | 'CELL_REF'\n | 'RANGE'\n | 'SPILL_REF'\n | 'PROPERTY_REF'\n | 'IDENTIFIER'\n | 'OPERATOR'\n | 'LPAREN'\n | 'RPAREN'\n | 'COMMA'\n | 'EOF';\n\nexport interface FormulaToken {\n type: FormulaTokenType;\n value: string;\n position: number;\n}\n\nexport function tokenizeFormula(formula: string): FormulaToken[] {\n const tokens: FormulaToken[] = [];\n // Strip leading '=' if present\n let input = formula.trim();\n let offset = 0;\n if (input.startsWith('=')) {\n input = input.slice(1);\n offset = 1;\n }\n\n let i = 0;\n while (i < input.length) {\n const char = input[i];\n\n // Skip whitespace\n if (/\\s/.test(char)) {\n i++;\n continue;\n }\n\n // Number literal (int or float)\n if (/\\d/.test(char) || (char === '.' && i + 1 < input.length && /\\d/.test(input[i + 1]))) {\n const start = i;\n let hasDot = char === '.';\n i++;\n while (i < input.length && (/\\d/.test(input[i]) || (input[i] === '.' && !hasDot))) {\n if (input[i] === '.') hasDot = true;\n i++;\n }\n tokens.push({\n type: 'NUMBER',\n value: input.slice(start, i),\n position: start + offset\n });\n continue;\n }\n\n // String literal (\"...\" or '...')\n if (char === '\"' || char === \"'\") {\n const quote = char;\n const start = i;\n i++;\n let str = '';\n while (i < input.length && input[i] !== quote) {\n if (input[i] === '\\\\' && i + 1 < input.length) {\n i++;\n }\n str += input[i];\n i++;\n }\n if (i >= input.length) {\n throw new Error(`Unterminated string starting at position ${start + offset}`);\n }\n i++; // skip closing quote\n tokens.push({\n type: 'STRING',\n value: str,\n position: start + offset\n });\n continue;\n }\n\n // Error literal, e.g. #REF! left behind by deleting a referenced row\n if (char === '#') {\n const match = /^#(?:REF!|DIV\\/0!|VALUE!|NAME\\?|N\\/A|NUM!|CYCLE!|ERROR!|SPILL!|CALC!)/.exec(input.slice(i));\n if (!match) {\n throw new Error(`Unexpected character '#' at position ${i + offset}`);\n }\n tokens.push({ type: 'ERROR', value: match[0], position: i + offset });\n i += match[0].length;\n continue;\n }\n\n // Property reference: [property_name] e.g. [status], [title], [effortHours]\n if (char === '[') {\n const start = i;\n i++;\n let prop = '';\n while (i < input.length && input[i] !== ']') {\n prop += input[i];\n i++;\n }\n if (i < input.length && input[i] === ']') {\n i++;\n }\n tokens.push({\n type: 'PROPERTY_REF',\n value: prop.trim(),\n position: start + offset\n });\n continue;\n }\n\n // Single character delimiters\n if (char === '(') {\n tokens.push({ type: 'LPAREN', value: '(', position: i + offset });\n i++;\n continue;\n }\n if (char === ')') {\n tokens.push({ type: 'RPAREN', value: ')', position: i + offset });\n i++;\n continue;\n }\n if (char === ',') {\n tokens.push({ type: 'COMMA', value: ',', position: i + offset });\n i++;\n continue;\n }\n\n // Multi-character comparison operators: <=, >=, <>, !=\n const twoChars = input.slice(i, i + 2);\n if (['<=', '>=', '<>', '!='].includes(twoChars)) {\n tokens.push({\n type: 'OPERATOR',\n value: twoChars === '!=' ? '<>' : twoChars,\n position: i + offset\n });\n i += 2;\n continue;\n }\n\n // Single character operators: +, -, *, /, ^, &, =, <, >, and @ (implicit intersection)\n if (['+', '-', '*', '/', '^', '&', '=', '<', '>', '@'].includes(char)) {\n tokens.push({ type: 'OPERATOR', value: char, position: i + offset });\n i++;\n continue;\n }\n\n // Identifiers, Cell references, and Ranges (e.g. SUM, A1, B2:B10, A:A)\n if (/[a-zA-Z_$]/.test(char)) {\n const start = i;\n while (i < input.length && /[a-zA-Z0-9_$.]/.test(input[i])) {\n i++;\n }\n const rawWord = input.slice(start, i);\n\n // Check if followed by colon indicating a range e.g. A1:B10 or A:A\n if (i < input.length && input[i] === ':') {\n const colonPos = i;\n i++; // skip colon\n const secondStart = i;\n while (i < input.length && /[a-zA-Z0-9_$.]/.test(input[i])) {\n i++;\n }\n const secondWord = input.slice(secondStart, i);\n if (secondWord.length > 0) {\n tokens.push({\n type: 'RANGE',\n value: `${rawWord}:${secondWord}`.toUpperCase(),\n position: start + offset\n });\n continue;\n } else {\n // Revert back\n i = colonPos;\n }\n }\n\n // Check for boolean literal\n const upper = rawWord.toUpperCase();\n if (upper === 'TRUE' || upper === 'FALSE') {\n tokens.push({\n type: 'BOOLEAN',\n value: upper,\n position: start + offset\n });\n continue;\n }\n\n // Check for Cell Reference (e.g. A1, $B$12, AA100)\n if (/^\\$?[A-Za-z]+\\$?[0-9]+$/.test(rawWord)) {\n // A1# is the whole spill range of the dynamic array anchored at A1.\n if (input[i] === '#') {\n i++;\n tokens.push({ type: 'SPILL_REF', value: upper.replace(/\\$/g, ''), position: start + offset });\n continue;\n }\n tokens.push({\n type: 'CELL_REF',\n value: upper.replace(/\\$/g, ''),\n position: start + offset\n });\n continue;\n }\n\n // Function or generic identifier\n tokens.push({\n type: 'IDENTIFIER',\n value: upper,\n position: start + offset\n });\n continue;\n }\n\n throw new Error(`Unexpected character '${char}' at position ${i + offset}`);\n }\n\n tokens.push({ type: 'EOF', value: '', position: input.length + offset });\n return tokens;\n}\n","import { FormulaToken, tokenizeFormula, FormulaTokenType } from './lexer';\n\n/** Row number used for the open end of whole-column ranges such as `A:A`. */\nexport const WHOLE_COLUMN_END_ROW = 1048576;\n\nexport type ASTNode =\n | LiteralNode\n | CellRefNode\n | RangeNode\n | SpillRefNode\n | NameNode\n | PropertyRefNode\n | BinaryOpNode\n | UnaryOpNode\n | FunctionCallNode;\n\nexport interface LiteralNode {\n type: 'Literal';\n value: number | string | boolean;\n}\n\nexport interface CellRefNode {\n type: 'CellRef';\n ref: string;\n col: string;\n row: number;\n}\n\nexport interface RangeNode {\n type: 'Range';\n raw: string;\n startCol: string;\n startRow: number;\n endCol: string;\n endRow: number;\n}\n\n/** `A1#`: the whole spill range of the dynamic array anchored at A1. */\nexport interface SpillRefNode {\n type: 'SpillRef';\n /** Anchor cell, e.g. `A1`. */\n ref: string;\n col: string;\n row: number;\n}\n\n/** A bare name: a LET variable, or `#NAME?` when nothing defines it. */\nexport interface NameNode {\n type: 'Name';\n name: string;\n}\n\nexport interface PropertyRefNode {\n type: 'PropertyRef';\n property: string;\n}\n\nexport interface BinaryOpNode {\n type: 'BinaryOp';\n op: string;\n left: ASTNode;\n right: ASTNode;\n}\n\nexport interface UnaryOpNode {\n type: 'UnaryOp';\n op: string;\n argument: ASTNode;\n}\n\nexport interface FunctionCallNode {\n type: 'FunctionCall';\n name: string;\n args: ASTNode[];\n}\n\nexport function parseCellReference(ref: string): { col: string; row: number } {\n const match = ref.match(/^([A-Za-z]+)(\\d+)$/);\n if (!match) {\n throw new Error(`Invalid cell reference: ${ref}`);\n }\n return {\n col: match[1].toUpperCase(),\n row: parseInt(match[2], 10)\n };\n}\n\nexport function parseRange(rangeStr: string): RangeNode {\n const [start, end] = rangeStr.replace(/\\$/g, '').split(':');\n const raw = rangeStr.toUpperCase();\n\n // Whole-column range, e.g. A:A or B:D\n if (/^[A-Za-z]+$/.test(start) && /^[A-Za-z]+$/.test(end)) {\n return {\n type: 'Range',\n raw,\n startCol: start.toUpperCase(),\n startRow: 1,\n endCol: end.toUpperCase(),\n endRow: WHOLE_COLUMN_END_ROW\n };\n }\n\n const startRef = parseCellReference(start);\n const endRef = parseCellReference(end);\n return {\n type: 'Range',\n raw,\n startCol: startRef.col,\n startRow: startRef.row,\n endCol: endRef.col,\n endRow: endRef.row\n };\n}\n\nexport class FormulaParser {\n private tokens: FormulaToken[] = [];\n private current = 0;\n\n constructor(tokens: FormulaToken[]) {\n this.tokens = tokens;\n }\n\n public parse(): ASTNode {\n if (this.peek().type === 'EOF') {\n return { type: 'Literal', value: '' };\n }\n const expr = this.expression();\n if (this.peek().type !== 'EOF') {\n throw new Error(`Unexpected token at position ${this.peek().position}: ${this.peek().value}`);\n }\n return expr;\n }\n\n private expression(): ASTNode {\n return this.comparison();\n }\n\n private comparison(): ASTNode {\n let expr = this.concatenation();\n\n while (\n this.matchOperator('=', '<>', '<', '>', '<=', '>=')\n ) {\n const op = this.previous().value;\n const right = this.concatenation();\n expr = { type: 'BinaryOp', op, left: expr, right };\n }\n\n return expr;\n }\n\n private concatenation(): ASTNode {\n let expr = this.addition();\n\n while (this.matchOperator('&')) {\n const op = this.previous().value;\n const right = this.addition();\n expr = { type: 'BinaryOp', op, left: expr, right };\n }\n\n return expr;\n }\n\n private addition(): ASTNode {\n let expr = this.multiplication();\n\n while (this.matchOperator('+', '-')) {\n const op = this.previous().value;\n const right = this.multiplication();\n expr = { type: 'BinaryOp', op, left: expr, right };\n }\n\n return expr;\n }\n\n private multiplication(): ASTNode {\n let expr = this.exponentiation();\n\n while (this.matchOperator('*', '/')) {\n const op = this.previous().value;\n const right = this.exponentiation();\n expr = { type: 'BinaryOp', op, left: expr, right };\n }\n\n return expr;\n }\n\n private exponentiation(): ASTNode {\n let expr = this.unary();\n\n while (this.matchOperator('^')) {\n const op = this.previous().value;\n const right = this.unary();\n expr = { type: 'BinaryOp', op, left: expr, right };\n }\n\n return expr;\n }\n\n private unary(): ASTNode {\n if (this.matchOperator('-', '+', '@')) {\n const op = this.previous().value;\n const argument = this.unary();\n return { type: 'UnaryOp', op, argument };\n }\n return this.primary();\n }\n\n private primary(): ASTNode {\n const token = this.peek();\n\n if (token.type === 'NUMBER') {\n this.advance();\n return { type: 'Literal', value: parseFloat(token.value) };\n }\n\n if (token.type === 'STRING') {\n this.advance();\n return { type: 'Literal', value: token.value };\n }\n\n if (token.type === 'BOOLEAN') {\n this.advance();\n return { type: 'Literal', value: token.value === 'TRUE' };\n }\n\n if (token.type === 'ERROR') {\n this.advance();\n return { type: 'Literal', value: token.value };\n }\n\n if (token.type === 'CELL_REF') {\n this.advance();\n const parsed = parseCellReference(token.value);\n return {\n type: 'CellRef',\n ref: token.value,\n col: parsed.col,\n row: parsed.row\n };\n }\n\n if (token.type === 'RANGE') {\n this.advance();\n return parseRange(token.value);\n }\n\n if (token.type === 'SPILL_REF') {\n this.advance();\n const parsed = parseCellReference(token.value);\n return { type: 'SpillRef', ref: token.value, col: parsed.col, row: parsed.row };\n }\n\n if (token.type === 'PROPERTY_REF') {\n this.advance();\n return {\n type: 'PropertyRef',\n property: token.value\n };\n }\n\n // Function call e.g. SUM(...)\n if (token.type === 'IDENTIFIER') {\n const name = token.value;\n this.advance();\n\n if (this.check('LPAREN')) {\n this.consume('LPAREN', `Expected '(' after function ${name}`);\n const args: ASTNode[] = [];\n if (!this.check('RPAREN')) {\n do {\n args.push(this.expression());\n } while (this.match('COMMA'));\n }\n this.consume('RPAREN', `Expected ')' after function arguments`);\n return {\n type: 'FunctionCall',\n name,\n args\n };\n }\n\n // A bare name: a LET variable, otherwise #NAME? (as in spreadsheet apps).\n return { type: 'Name', name };\n }\n\n if (this.match('LPAREN')) {\n const expr = this.expression();\n this.consume('RPAREN', `Expected ')' after expression`);\n return expr;\n }\n\n throw new Error(`Unexpected token at position ${token.position}: ${token.value || token.type}`);\n }\n\n private match(...types: string[]): boolean {\n for (const type of types) {\n if (this.check(type)) {\n this.advance();\n return true;\n }\n }\n return false;\n }\n\n private matchOperator(...ops: string[]): boolean {\n if (this.check('OPERATOR') && ops.includes(this.peek().value)) {\n this.advance();\n return true;\n }\n return false;\n }\n\n private check(type: string): boolean {\n if (this.isAtEnd()) return false;\n return this.peek().type === type;\n }\n\n private advance(): FormulaToken {\n if (!this.isAtEnd()) this.current++;\n return this.previous();\n }\n\n private isAtEnd(): boolean {\n return this.peek().type === 'EOF';\n }\n\n private peek(): FormulaToken {\n return this.tokens[this.current];\n }\n\n private previous(): FormulaToken {\n return this.tokens[this.current - 1];\n }\n\n private consume(type: string, message: string): FormulaToken {\n if (this.check(type)) return this.advance();\n throw new Error(message);\n }\n}\n\nexport function parseFormula(formula: string): ASTNode {\n const tokens = tokenizeFormula(formula);\n const parser = new FormulaParser(tokens);\n return parser.parse();\n}\n","// Portions ported from gridcraft/crates/core/src/value.rs (MIT OR Apache-2.0,\n// © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n/**\n * Value helpers shared by the formula evaluator and the built-in functions:\n * error values, spreadsheet coercion rules, comparisons, ranges and dates.\n */\nimport { parseNumberText } from './parse-input';\n\n/** Spreadsheet error values. They propagate through operators and functions. */\nexport const FORMULA_ERRORS = new Set([\n '#DIV/0!',\n '#VALUE!',\n '#REF!',\n '#NAME?',\n '#N/A',\n '#NUM!',\n '#CYCLE!',\n '#ERROR!',\n '#SPILL!',\n '#CALC!'\n]);\n\nexport type FormulaErrorCode =\n | '#DIV/0!'\n | '#VALUE!'\n | '#REF!'\n | '#NAME?'\n | '#N/A'\n | '#NUM!'\n | '#CYCLE!'\n | '#ERROR!'\n | '#SPILL!'\n | '#CALC!';\n\nexport function isFormulaError(value: unknown): value is string {\n return typeof value === 'string' && FORMULA_ERRORS.has(value);\n}\n\nexport const isBlank = (value: unknown): boolean => value === '' || value === null || value === undefined;\n\n/**\n * Coerces to a number like arithmetic does: blanks are 0, booleans 1/0,\n * numeric text parses (`\"1,000\"`, `\"50%\"`, `\"$3\"`, `\"(5)\"`, dates and times\n * as serial numbers), other text is #VALUE!.\n */\nexport function toNumber(value: unknown): number | string {\n if (isFormulaError(value)) return value;\n if (isBlank(value)) return 0;\n if (typeof value === 'boolean') return value ? 1 : 0;\n if (typeof value === 'number') return value;\n return parseNumberText(String(value)) ?? '#VALUE!';\n}\n\n/** Coerces to a boolean: numbers are non-zero, \"TRUE\"/\"FALSE\" text is parsed, other text is #VALUE!. */\nexport function toBoolean(value: unknown): boolean | string {\n if (isFormulaError(value)) return value;\n if (isBlank(value)) return false;\n if (typeof value === 'boolean') return value;\n if (typeof value === 'number') return value !== 0;\n const upper = String(value).trim().toUpperCase();\n if (upper === 'TRUE') return true;\n if (upper === 'FALSE') return false;\n return '#VALUE!';\n}\n\n/** Coerces to text like `&` does: numbers use up to 15 significant digits (`0.1+0.2` is \"0.3\"). */\nexport const toText = (value: unknown): string =>\n isBlank(value)\n ? ''\n : typeof value === 'boolean'\n ? (value ? 'TRUE' : 'FALSE')\n : typeof value === 'number'\n ? numberToText(value)\n : String(value);\n\n/**\n * Number to text as Excel converts it in `&` and General display: up to 15\n * significant digits, scientific for very large or very small magnitudes.\n */\nexport function numberToText(n: number): string {\n if (n === 0 || !Number.isFinite(n)) return '0';\n const a = Math.abs(n);\n if (((a < 1e-9 || a >= 1e15) && Math.round(a) !== a) || a >= 1e15) return sciText(n, 15);\n let mag = Math.floor(Math.log10(a));\n if (a >= Math.pow(10, mag + 1)) mag++; // log10 can land just below an exact power of ten\n const decimals = Math.min(Math.max(14 - mag, 0), 30);\n return trimDecimal(n.toFixed(decimals));\n}\n\n/** Removes trailing zeros (and a trailing point) from a decimal string. */\nfunction trimDecimal(s: string): string {\n if (!s.includes('.')) return s;\n const t = s.replace(/0+$/, '').replace(/\\.$/, '');\n return t === '-0' ? '0' : t;\n}\n\n/** Scientific text like `1.23456789012346E+15` with up to `sig` significant digits. */\nfunction sciText(n: number, sig: number): string {\n const [mantissa, exp] = n.toExponential(Math.max(sig - 1, 0)).split('e');\n const e = Number(exp);\n return `${trimDecimal(mantissa)}E${e < 0 ? '-' : '+'}${String(Math.abs(e)).padStart(2, '0')}`;\n}\n\n/**\n * Number held by a value, or null. Typed cell values are stored as text, so\n * numeric text (\"120\") counts as a number here; booleans and blanks do not.\n */\nexport function numericValue(value: unknown): number | null {\n if (typeof value === 'number') return Number.isFinite(value) ? value : null;\n if (typeof value !== 'string' || FORMULA_ERRORS.has(value)) return null;\n const trimmed = value.trim();\n if (trimmed === '') return null;\n const num = Number(trimmed);\n return Number.isFinite(num) ? num : null;\n}\n\n/** Normalizes a cell value for lookups and criteria: numeric text becomes a number. */\nexport function normalizeValue(value: unknown): unknown {\n const num = typeof value === 'string' ? numericValue(value) : null;\n return num === null ? value : num;\n}\n\n/** `ERROR.TYPE`-style order of error values, used when two errors are compared. */\nconst ERROR_ORDER = ['#NULL!', '#DIV/0!', '#VALUE!', '#REF!', '#NAME?', '#NUM!', '#N/A', '#SPILL!', '#CALC!', '#CYCLE!', '#ERROR!'];\n\n/** Type rank for comparison and sorting: numbers < text < booleans < errors; blanks rank last. */\nfunction valueRank(value: unknown): number {\n if (isBlank(value)) return 4;\n if (typeof value === 'number') return 0;\n if (typeof value === 'boolean') return 2;\n if (typeof value === 'string' && FORMULA_ERRORS.has(value)) return 3;\n return 1;\n}\n\n/**\n * Compares numbers the way Excel does: both are rounded to 15 significant\n * digits first, so binary noise such as `0.1+0.2` versus `0.3` counts as equal.\n */\nexport function compareNumbers(a: number, b: number): number {\n if (a === b) return 0;\n // Numbers further apart than the 15th digit can't round together, and\n // rounding keeps their order: skip the rounding on the sort hot path.\n if (Math.abs(a - b) > Math.max(Math.abs(a), Math.abs(b)) * 1e-13) return a < b ? -1 : 1;\n const round15 = (x: number) => (!Number.isFinite(x) || x === 0 ? x : Number(x.toExponential(14)));\n const x = round15(a);\n const y = round15(b);\n return x < y ? -1 : x > y ? 1 : 0;\n}\n\n/** Case-insensitive text order. */\nexport function compareText(a: string, b: string): number {\n const x = a.toLowerCase();\n const y = b.toLowerCase();\n return x < y ? -1 : x > y ? 1 : 0;\n}\n\n/**\n * Excel's comparison for the `=`, `<>`, `<`… operators: numbers < text <\n * booleans < errors, numbers compared at 15 significant digits, text\n * case-insensitively. A blank acts as 0, \"\" or FALSE depending on the other\n * side. Dates are ISO text: two ISO dates compare chronologically\n * (\"2026-1-5\" is before \"2026-01-10\"), and an ISO date against a number\n * compares as its serial number.\n */\nexport function compareValues(left: unknown, right: unknown): number {\n const lRank = valueRank(left);\n const rRank = valueRank(right);\n if (lRank === 4 || rRank === 4) {\n if (lRank === rRank) return 0;\n // A blank against a number, text or boolean takes that type's empty value.\n const blankFirst = lRank === 4;\n const other = blankFirst ? right : left;\n const otherRank = blankFirst ? rRank : lRank;\n let cmp: number;\n if (otherRank === 0) cmp = compareNumbers(0, other as number);\n else if (otherRank === 1) cmp = String(other) === '' ? 0 : -1;\n else if (otherRank === 2) cmp = other ? -1 : 0;\n else return lRank - rRank;\n return blankFirst ? cmp : -cmp;\n }\n if (lRank === 1 && rRank === 1) {\n const a = String(left);\n const b = String(right);\n const ta = isoDateTime(a);\n const tb = ta === null ? null : isoDateTime(b);\n if (ta !== null && tb !== null) return Math.sign(ta - tb);\n return compareText(a, b);\n }\n if ((lRank === 0 && rRank === 1) || (lRank === 1 && rRank === 0)) {\n const date = parseIsoDate(lRank === 1 ? left : right);\n if (date) {\n const serial = dateToSerial(date);\n return lRank === 0 ? compareNumbers(left as number, serial) : compareNumbers(serial, right as number);\n }\n }\n if (lRank !== rRank) return lRank - rRank;\n if (lRank === 0) return compareNumbers(left as number, right as number);\n if (lRank === 2) return Number(left) - Number(right);\n return Math.sign(ERROR_ORDER.indexOf(left as string) - ERROR_ORDER.indexOf(right as string));\n}\n\n/**\n * Sort order (ascending): numbers, text, booleans, errors, then blanks last.\n * Errors are equal to each other, so sorting keeps their order.\n */\nexport function sortCompareValues(left: unknown, right: unknown): number {\n const lBlank = isBlank(left);\n const rBlank = isBlank(right);\n if (lBlank || rBlank) return lBlank === rBlank ? 0 : lBlank ? 1 : -1;\n if (isFormulaError(left) && isFormulaError(right)) return 0;\n return compareValues(left, right);\n}\n\n/** Rounds half away from zero, compensating for binary floating point (ROUND(1.005, 2) = 1.01). */\nexport function roundHalfAwayFromZero(value: number, decimals: number): number {\n const factor = Math.pow(10, decimals);\n const scaled = Number((Math.abs(value) * factor).toPrecision(15));\n return (Math.sign(value) * Math.round(scaled)) / factor;\n}\n\n/** Strips binary floating point noise (0.1 * 3 = 0.30000000000000004 -> 0.3). */\nexport const cleanFloat = (value: number): number => Number(value.toPrecision(15));\n\nexport function colNameToIndex(col: string): number {\n let index = 0;\n const upper = col.toUpperCase();\n for (let i = 0; i < upper.length; i++) {\n index = index * 26 + (upper.charCodeAt(i) - 65 + 1);\n }\n return index - 1;\n}\n\nexport function indexToColName(index: number): string {\n let num = index + 1;\n let name = '';\n while (num > 0) {\n const mod = (num - 1) % 26;\n name = String.fromCharCode(65 + mod) + name;\n num = Math.floor((num - mod) / 26);\n }\n return name;\n}\n\n// --- Ranges ---\n\n/**\n * A rectangular block of values. `values` is column-major (the order in which\n * `EvaluationContext.resolveRange` returns cells): `values[col * rows + row]`.\n */\nexport interface FormulaRange {\n rows: number;\n cols: number;\n values: any[];\n}\n\n/** Shapes of the arrays produced by range references and range-returning functions. */\nconst rangeShapes = new WeakMap<any[], { rows: number; cols: number }>();\n\n/** Records the shape of a column-major array so functions can treat it as a 2D range. */\nexport function withRangeShape<T extends any[]>(values: T, rows: number, cols: number): T {\n rangeShapes.set(values, { rows, cols });\n return values;\n}\n\n/** Views an array as a range. Arrays without a known shape are a single column. */\nexport function toFormulaRange(values: any[]): FormulaRange {\n const shape = rangeShapes.get(values);\n if (shape && shape.rows * shape.cols === values.length) return { ...shape, values };\n return { rows: values.length, cols: 1, values };\n}\n\nexport const rangeGet = (range: FormulaRange, row: number, col: number): any =>\n row < range.rows && col < range.cols ? range.values[col * range.rows + row] : '';\n\n/** Builds a column-major array (with its shape recorded) from a sub-block of a range. */\nexport function sliceRange(\n range: FormulaRange,\n rowStart: number,\n rowCount: number,\n colStart: number,\n colCount: number,\n): any[] {\n const out: any[] = [];\n for (let c = colStart; c < colStart + colCount; c++) {\n for (let r = rowStart; r < rowStart + rowCount; r++) out.push(rangeGet(range, r, c));\n }\n return withRangeShape(out, rowCount, colCount);\n}\n\n// --- Dates ---\n//\n// Dates are ISO text (\"2026-01-31\", or \"2026-01-31 13:45:00\" with a time), as\n// returned by TODAY(). Numbers are read as spreadsheet serial dates (days since\n// 1899-12-30), so values pasted from Excel or Numbers still work.\n\nconst MS_PER_DAY = 24 * 60 * 60 * 1000;\nconst pad = (n: number, width = 2) => String(n).padStart(width, '0');\n\n/** Parses a date value (ISO text, serial number or Date) into a local Date, or null. */\nexport function toDate(value: unknown): Date | null {\n if (value instanceof Date) return isNaN(value.getTime()) ? null : value;\n if (typeof value === 'number') return serialToDate(value);\n if (typeof value !== 'string' || FORMULA_ERRORS.has(value)) return null;\n const text = value.trim();\n if (text === '') return null;\n const iso = /^(\\d{4})-(\\d{1,2})-(\\d{1,2})(?:[ T](\\d{1,2}):(\\d{2})(?::(\\d{2}))?)?$/.exec(text);\n if (iso) {\n const [, y, m, d, hh = '0', mm = '0', ss = '0'] = iso;\n const date = new Date(Number(y), Number(m) - 1, Number(d), Number(hh), Number(mm), Number(ss));\n // Reject impossible dates such as 2026-02-30 instead of rolling them over.\n return date.getMonth() === Number(m) - 1 ? date : null;\n }\n const serial = numericValue(text);\n if (serial !== null) return serialToDate(serial);\n // Other textual dates (\"Jan 5, 2026\", \"1/5/2026\") use the platform parser.\n if (/[a-z/]/i.test(text)) {\n const parsed = Date.parse(text);\n return isNaN(parsed) ? null : new Date(parsed);\n }\n return null;\n}\n\nconst ISO_DATE = /^(\\d{4})-(\\d{1,2})-(\\d{1,2})(?:[ T](\\d{1,2}):(\\d{2})(?::(\\d{2}))?)?$/;\n\n/**\n * Parses ISO date text (\"2026-01-31\" or \"2026-01-31 13:45[:00]\") into a\n * local Date, or null. Only ISO text counts: date arithmetic and date\n * comparisons never reinterpret other text or numbers as dates.\n */\nexport function parseIsoDate(value: unknown): Date | null {\n if (typeof value !== 'string' || value.length < 8 || value.length > 19) return null;\n const iso = ISO_DATE.exec(value.trim());\n if (!iso) return null;\n const [, y, m, d, hh = '0', mm = '0', ss = '0'] = iso;\n const date = new Date(Number(y), Number(m) - 1, Number(d), Number(hh), Number(mm), Number(ss));\n return date.getMonth() === Number(m) - 1 && date.getDate() === Number(d) ? date : null;\n}\n\n/** Whether ISO date text includes a time of day. */\nexport const isoDateHasTime = (value: string): boolean => /[ T]\\d/.test(value.trim());\n\nfunction isoDateTime(value: string): number | null {\n // Cheap pre-check: ISO dates start with four digits and a dash.\n if (value.length < 8 || value.charCodeAt(4) !== 45 /* - */) return null;\n return parseIsoDate(value)?.getTime() ?? null;\n}\n\n/** Spreadsheet serial number (days since 1899-12-30, fraction = time of day). */\nexport function dateToSerial(date: Date): number {\n const utc = Date.UTC(\n date.getFullYear(),\n date.getMonth(),\n date.getDate(),\n date.getHours(),\n date.getMinutes(),\n date.getSeconds(),\n );\n return cleanFloat((utc - Date.UTC(1899, 11, 30)) / MS_PER_DAY);\n}\n\nexport function serialToDate(serial: number): Date | null {\n if (!Number.isFinite(serial)) return null;\n const utc = new Date(Date.UTC(1899, 11, 30) + Math.round(serial * MS_PER_DAY));\n return new Date(\n utc.getUTCFullYear(),\n utc.getUTCMonth(),\n utc.getUTCDate(),\n utc.getUTCHours(),\n utc.getUTCMinutes(),\n utc.getUTCSeconds(),\n );\n}\n\n/** Formats a local date as \"YYYY-MM-DD\" (the format TODAY() returns). */\nexport const formatIsoDate = (date: Date): string =>\n `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;\n\n/** Formats a local date and time as \"YYYY-MM-DD HH:MM:SS\" (the format NOW() returns). */\nexport const formatIsoDateTime = (date: Date): string =>\n `${formatIsoDate(date)} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;\n\n/** Whole days from `start` to `end`, ignoring time of day and DST shifts. */\nexport const daysBetween = (start: Date, end: Date): number =>\n Math.round(\n (Date.UTC(end.getFullYear(), end.getMonth(), end.getDate()) -\n Date.UTC(start.getFullYear(), start.getMonth(), start.getDate())) /\n MS_PER_DAY,\n );\n","// Portions ported from gridcraft/crates/core/src/parse.rs and gridcraft/crates/core/src/date.rs\n// (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n/**\n * Parsing what people type into a cell (and numeric text in formulas).\n *\n * Recognises numbers with thousands separators, percentages, currency,\n * scientific notation, fractions (`1 1/2`), dates (`3/4/2026`, `2026-03-04`,\n * `4-Mar-2026`, `Mar 4, 2026`), times (`10:30`, `10:30:15 PM`) and\n * date-times. Locale: en-US (month/day/year).\n *\n * Representation: our engine keeps dates as ISO text (`2026-03-04`, or\n * `2026-03-04 18:00:00` with a time), so a typed date becomes ISO text, not a\n * serial number. A time typed on its own (`12:00`) has no date to anchor ISO\n * text to, so it becomes a fraction of a day (0.5), as in Excel.\n */\nimport type { CellFormat } from '../types';\nimport { FORMULA_ERRORS } from './values';\n\n/** Kind of automatic number format chosen for typed input. */\nexport type CellInputFormatKind = 'number' | 'currency' | 'percentage' | 'date' | 'time';\n\nexport interface ParsedCellInput {\n /** The typed value: number, boolean, ISO date text, error code or text. `''` for empty input. */\n value: string | number | boolean;\n type: 'empty' | 'text' | 'number' | 'boolean' | 'error' | 'date';\n /**\n * Number format code Excel would apply automatically (e.g. `\"$\"#,##0.00`,\n * `0%`, `#,##0`, `m/d/yyyy`, `h:mm`), or undefined for General.\n */\n numberFormat?: string;\n /** What kind of format that is, if any. */\n formatKind?: CellInputFormatKind;\n /** `formatKind` mapped onto `CellFormat` (times have no `CellFormat` and map to undefined). */\n cellFormat?: CellFormat;\n}\n\n// --- Date helpers (from date.rs) ---\n\nexport const MONTH_NAMES = [\n 'January', 'February', 'March', 'April', 'May', 'June',\n 'July', 'August', 'September', 'October', 'November', 'December',\n];\n\n/** Days since 1970-01-01 for a civil date (Howard Hinnant's algorithm). */\nexport function daysFromCivil(year: number, month: number, day: number): number {\n const y = month <= 2 ? year - 1 : year;\n const era = Math.floor((y >= 0 ? y : y - 399) / 400);\n const yoe = y - era * 400;\n const doy = Math.floor((153 * (month > 2 ? month - 3 : month + 9) + 2) / 5) + day - 1;\n const doe = yoe * 365 + Math.floor(yoe / 4) - Math.floor(yoe / 100) + doy;\n return era * 146097 + doe - 719468;\n}\n\nexport const isLeapYear = (y: number): boolean => (y % 4 === 0 && y % 100 !== 0) || y % 400 === 0;\n\nexport function daysInMonth(year: number, month: number): number {\n if (month === 2) return isLeapYear(year) ? 29 : 28;\n if ([4, 6, 9, 11].includes(month)) return 30;\n return month >= 1 && month <= 12 ? 31 : 0;\n}\n\n/**\n * Days in a month of Excel's 1900 date system: February 1900 has 29 days\n * (serial 60 is the fictitious 1900-02-29, kept for Lotus 1-2-3 compatibility).\n */\nexport const daysInMonth1900 = (year: number, month: number): number =>\n year === 1900 && month === 2 ? 29 : daysInMonth(year, month);\n\nconst UNIX_1899_12_30 = -25569;\nconst MAX_SERIAL_1900 = 2_958_465; // 9999-12-31\n\n/**\n * Excel 1900-system serial for a date (1 = 1900-01-01, 60 = the fictitious\n * 1900-02-29). Month and day overflow roll over like `DATE()`; years 0–1899\n * are added to 1900. Null before the epoch or after 9999-12-31.\n */\nexport function serialFromYmd(year: number, month: number, day: number): number | null {\n if (![year, month, day].every(Number.isSafeInteger)) return null;\n let y = year >= 0 && year < 1900 ? year + 1900 : year;\n const m0 = month - 1;\n y += Math.floor(m0 / 12);\n const m = (((m0 % 12) + 12) % 12) + 1;\n if (y < 1900 || y > 9999) return null;\n // Up to February 1900 the serial is one lower than the day count from 1899-12-30.\n const first = daysFromCivil(y, m, 1) - UNIX_1899_12_30 - (y === 1900 && m <= 2 ? 1 : 0);\n const serial = first + day - 1;\n return serial >= 0 && serial <= MAX_SERIAL_1900 ? serial : null;\n}\n\n/** Fraction of a day for a time. */\nexport const timeFraction = (h: number, m: number, s: number): number => (h * 3600 + m * 60 + s) / 86400;\n\n/** The year used for dates typed without one. */\nconst currentYear = (): number => new Date().getFullYear();\n\n// --- Parsing (from parse.rs) ---\n\ninterface DateParts {\n year: number;\n month: number;\n day: number;\n /** Fraction of a day, when a time was typed. */\n time?: number;\n}\n\ntype Numeric =\n | { kind: 'number'; value: number; format?: string }\n | { kind: 'date'; date: DateParts; format: string }\n | { kind: 'time'; value: number; format: string };\n\n/**\n * Interprets typed cell input (not formulas). Text that isn't a number,\n * date, time, boolean or error stays text; a leading apostrophe forces text.\n */\nexport function parseCellInput(input: string): ParsedCellInput {\n if (input === '') return { value: '', type: 'empty' };\n if (input.startsWith(\"'\")) return { value: input.slice(1), type: 'text' };\n const t = input.trim();\n const upper = t.toUpperCase();\n if (upper === 'TRUE') return { value: true, type: 'boolean' };\n if (upper === 'FALSE') return { value: false, type: 'boolean' };\n if (FORMULA_ERRORS.has(upper)) return { value: upper, type: 'error' };\n const numeric = parseNumeric(t);\n if (numeric) {\n if (numeric.kind === 'date') {\n const iso = isoText(numeric.date);\n // ISO text can't hold the fictitious 1900-02-29: it stays text.\n if (iso !== null) {\n return { value: iso, type: 'date', numberFormat: numeric.format, formatKind: 'date', cellFormat: 'date' };\n }\n } else {\n const formatKind = numeric.kind === 'time' ? 'time' : formatKindOf(numeric.format);\n return {\n value: normalizeZero(numeric.value),\n type: 'number',\n ...(numeric.format ? { numberFormat: numeric.format } : {}),\n ...(formatKind ? { formatKind } : {}),\n ...(formatKind && formatKind !== 'time' ? { cellFormat: formatKind } : {}),\n };\n }\n }\n return { value: input, type: 'text' };\n}\n\n/**\n * Numeric text as arithmetic coerces it (\"1,000\", \"50%\", \"$3\", \"1/2/2020\",\n * \"10:00\"); dates become 1900-system serial numbers. Null when it isn't numeric.\n */\nexport function parseNumberText(text: string): number | null {\n const numeric = parseNumeric(text.trim());\n if (!numeric) return null;\n if (numeric.kind !== 'date') return normalizeZero(numeric.value);\n const { year, month, day, time = 0 } = numeric.date;\n const serial = serialFromYmd(year, month, day);\n return serial === null ? null : serial + time;\n}\n\nconst normalizeZero = (n: number) => (n === 0 ? 0 : n);\n\nfunction formatKindOf(format: string | undefined): CellInputFormatKind | undefined {\n if (!format) return undefined;\n if (format.includes('$')) return 'currency';\n if (format.includes('%')) return 'percentage';\n return 'number';\n}\n\nconst pad = (n: number, width = 2) => String(n).padStart(width, '0');\n\n/** ISO text for parsed date parts, or null for the fictitious 1900-02-29. */\nfunction isoText({ year, month, day, time }: DateParts): string | null {\n if (day > daysInMonth(year, month)) return null;\n const date = `${pad(year, 4)}-${pad(month)}-${pad(day)}`;\n if (time === undefined) return date;\n let seconds = Math.round(time * 86400);\n if (seconds >= 86400) seconds = 86399; // never roll a typed time into the next day\n return `${date} ${pad(Math.floor(seconds / 3600))}:${pad(Math.floor(seconds / 60) % 60)}:${pad(seconds % 60)}`;\n}\n\nfunction parseNumeric(t: string): Numeric | null {\n if (t === '') return null;\n // Parenthesised negatives: (100) = -100.\n if (t.startsWith('(') && t.endsWith(')') && t.length >= 2) {\n const inner = parseNumeric(t.slice(1, -1).trim());\n if (!inner || inner.kind === 'date') return null;\n return { ...inner, value: -inner.value };\n }\n const plain = parsePlain(t);\n if (plain) return plain;\n const fraction = parseFraction(t);\n if (fraction !== null) return { kind: 'number', value: fraction, format: '# ?/?' };\n return parseDateTime(t);\n}\n\nconst startsWithDigitOrDot = (s: string) => /^[0-9.]/.test(s);\n\n/** Signs, currency, thousands separators, percent, exponent. */\nfunction parsePlain(t: string): Numeric | null {\n let s = t;\n let neg = false;\n let currency = false;\n let percent = false;\n if (s.startsWith('-')) {\n neg = true;\n s = s.slice(1).trimStart();\n } else if (s.startsWith('+')) {\n s = s.slice(1).trimStart();\n }\n if (s.startsWith('$')) {\n currency = true;\n s = s.slice(1).trimStart();\n if (!neg && s.startsWith('-')) {\n neg = true;\n s = s.slice(1);\n }\n }\n if (s.endsWith('%')) {\n percent = true;\n s = s.slice(0, -1).trimEnd();\n }\n if (s === '') return null;\n const hasSep = s.includes(',');\n let digits: string;\n if (hasSep) {\n // Thousands separators must be in groups of three before the decimal point.\n const dot = s.indexOf('.');\n const int = dot === -1 ? s : s.slice(0, dot);\n const frac = dot === -1 ? null : s.slice(dot + 1);\n const groups = int.split(',');\n const first = groups[0];\n if (first === '' || first.length > 3 || groups.slice(1).some((g) => g.length !== 3)) return null;\n digits = groups.join('') + (frac !== null ? `.${frac}` : '');\n } else {\n digits = s;\n }\n if (!/^[0-9.eE+-]+$/.test(digits) || !startsWithDigitOrDot(digits)) return null;\n // Rust's f64 parser accepts exactly decimal literals; JS `Number` is looser\n // (\"1.\" and \"1e\" differ), so check the shape too.\n if (!/^(\\d+\\.?\\d*|\\.\\d+)([eE][+-]?\\d+)?$/.test(digits)) return null;\n let n = Number(digits);\n if (!Number.isFinite(n)) return null;\n const sci = /[eE]/.test(digits);\n if (percent) n /= 100;\n if (neg) n = -n;\n const hasDot = digits.includes('.');\n const format = currency\n ? hasDot ? '\"$\"#,##0.00' : '\"$\"#,##0'\n : percent\n ? hasDot ? '0.00%' : '0%'\n : sci\n ? '0.00E+00'\n : hasSep\n ? hasDot ? '#,##0.00' : '#,##0'\n : undefined;\n return { kind: 'number', value: n, ...(format ? { format } : {}) };\n}\n\nconst parseDecimal = (s: string): number | null =>\n /^[+-]?(\\d+\\.?\\d*|\\.\\d+)([eE][+-]?\\d+)?$/.test(s) ? Number(s) : null;\n\n/** `1 1/2`, `0 3/4` (a bare `1/2` is a date). */\nfunction parseFraction(t: string): number | null {\n const space = t.indexOf(' ');\n if (space === -1) return null;\n const whole = t.slice(0, space);\n const frac = t.slice(space + 1).trim();\n const neg = whole.startsWith('-');\n const w = parseDecimal(whole.replace(/^-+/, ''));\n const slash = frac.indexOf('/');\n if (w === null || slash === -1) return null;\n const a = parseDecimal(frac.slice(0, slash));\n const b = parseDecimal(frac.slice(slash + 1));\n if (a === null || b === null || b === 0 || !Number.isFinite(w)) return null;\n const v = w + a / b;\n return neg ? -v : v;\n}\n\nfunction monthFromName(s: string): number | null {\n const l = s.toLowerCase();\n if (l.length < 3) return null;\n const index = MONTH_NAMES.findIndex((m) => m.toLowerCase().startsWith(l));\n return index === -1 ? null : index + 1;\n}\n\n/** `h:mm`, `h:mm:ss`, `h:mm:ss.000`, `h AM`, with optional AM/PM. */\nfunction parseTime(input: string): { fraction: number; seconds: boolean; ampm: boolean } | null {\n const lower = input.trim().toLowerCase();\n let body = lower;\n let pm: boolean | null = null;\n for (const [suffix, isPm] of [['am', false], ['a', false], ['pm', true], ['p', true]] as const) {\n if (lower.endsWith(suffix)) {\n body = lower.slice(0, -suffix.length).trim();\n pm = isPm;\n break;\n }\n }\n const parts = body.split(':');\n if (parts.length === 1 && pm === null) return null;\n if (parts.length > 3 || parts.some((p) => p === '')) return null;\n const num = (p: string | undefined) => (p === undefined ? 0 : parseDecimal(p));\n let h = num(parts[0]);\n const m = num(parts[1]);\n const s = num(parts[2]);\n if (h === null || m === null || s === null) return null;\n if (m >= 60 || s >= 60 || h < 0 || m < 0 || s < 0) return null;\n if (pm !== null) {\n if (h > 12) return null;\n if (h === 12) h = 0;\n if (pm) h += 12;\n } else if (h >= 10000) {\n return null;\n }\n return { fraction: timeFraction(h, m, s), seconds: parts.length === 3, ampm: pm !== null };\n}\n\nconst expandYear = (text: string, y: number) => (text.length <= 2 ? (y < 30 ? 2000 + y : 1900 + y) : y);\n\nfunction validYmd(year: number, month: number, day: number): DateParts | null {\n // Dates before 1900 stay text (`DATE()` would add 1900 to such a year).\n if (month < 1 || month > 12 || day < 1 || year < 1900 || year > 9999) return null;\n if (day > daysInMonth1900(year, month)) return null;\n return { year, month, day };\n}\n\nfunction parseDate(input: string): { date: DateParts; format: string } | null {\n const t = input.trim().replace(/,+$/, '');\n const num = (s: string) => (/^[+-]?\\d+$/.test(s.trim()) ? Number(s.trim()) : null);\n const withFormat = (date: DateParts | null, format: string) => (date ? { date, format } : null);\n const parts = t.split(/[/.-]/);\n if (parts.length === 3) {\n const [a, b, c] = parts.map((p) => p.trim());\n // ISO yyyy-mm-dd or yyyy/mm/dd\n if (a.length === 4) {\n const [y, m, d] = [num(a), num(b), num(c)];\n if (y !== null && m !== null && d !== null) return withFormat(validYmd(y, m, d), 'yyyy-mm-dd');\n }\n {\n const [m, d, y] = [num(a), num(b), num(c)];\n if (m !== null && d !== null && y !== null) return withFormat(validYmd(expandYear(c, y), m, d), 'm/d/yyyy');\n }\n // 4-Mar-2026\n const [d, m, y] = [num(a), monthFromName(b), num(c)];\n if (d !== null && m !== null && y !== null) return withFormat(validYmd(expandYear(c, y), m, d), 'd-mmm-yy');\n }\n if (parts.length === 2) {\n const [a, b] = parts.map((p) => p.trim());\n const [x, y] = [num(a), num(b)];\n if (x !== null && y !== null) {\n // m/d in the current year\n if (x >= 1 && x <= 12 && y >= 1 && y <= 31) return withFormat(validYmd(currentYear(), x, y), 'd-mmm');\n // m/yyyy\n if (x >= 1 && x <= 12 && b.length === 4) return withFormat(validYmd(y, x, 1), 'mmm-yy');\n }\n const dayA = num(a);\n const monthB = monthFromName(b);\n if (dayA !== null && monthB !== null) return withFormat(validYmd(currentYear(), monthB, dayA), 'd-mmm');\n const monthA = monthFromName(a);\n const yearB = num(b);\n if (monthA !== null && yearB !== null) return withFormat(validYmd(expandYear(b, yearB), monthA, 1), 'mmm-yy');\n }\n // \"Mar 4, 2026\" / \"March 4 2026\" / \"4 March 2026\"\n const words = t.split(/[ ,]/).filter((w) => w !== '');\n if (words.length === 3) {\n const [w0, w1, w2] = words;\n let [m, d, y] = [monthFromName(w0), num(w1), num(w2)];\n if (m !== null && d !== null && y !== null) return withFormat(validYmd(expandYear(w2, y), m, d), 'd-mmm-yy');\n [d, m, y] = [num(w0), monthFromName(w1), num(w2)];\n if (d !== null && m !== null && y !== null) return withFormat(validYmd(expandYear(w2, y), m, d), 'd-mmm-yy');\n }\n return null;\n}\n\nfunction parseDateTime(t: string): Numeric | null {\n const time = parseTime(t);\n if (time) {\n const format =\n time.fraction >= 1\n ? '[h]:mm:ss' // 24 hours or more: elapsed hours\n : time.seconds\n ? time.ampm ? 'h:mm:ss AM/PM' : 'h:mm:ss'\n : time.ampm ? 'h:mm AM/PM' : 'h:mm';\n return { kind: 'time', value: time.fraction, format };\n }\n const date = parseDate(t);\n if (date) return { kind: 'date', date: date.date, format: date.format };\n // date + time: split at a space before a time-looking tail.\n for (let i = t.indexOf(' '); i !== -1; i = t.indexOf(' ', i + 1)) {\n const d = parseDate(t.slice(0, i));\n const tm = d ? parseTime(t.slice(i + 1)) : null;\n // A date-time's time of day must stay within the day.\n if (d && tm && tm.fraction < 1) {\n return { kind: 'date', date: { ...d.date, time: tm.fraction }, format: 'm/d/yyyy h:mm' };\n }\n }\n return null;\n}\n","import { FORMULA_ERRORS, compareValues, isBlank, isFormulaError, numericValue, parseIsoDate } from './values';\nimport { parseCellInput } from './parse-input';\n\n/** Tests one value against a criterion. */\nexport type FormulaCriteriaPredicate = (value: unknown) => boolean;\n\n/**\n * Builds a regular expression from a spreadsheet wildcard pattern: `*` is any\n * run of characters, `?` any single character, and `~` escapes the next\n * character (`~*` is a literal asterisk). Matching is case-insensitive.\n */\nexport function wildcardToRegExp(pattern: string): RegExp {\n let source = '';\n for (let i = 0; i < pattern.length; i++) {\n const char = pattern[i];\n if (char === '~' && i + 1 < pattern.length) {\n source += escapeRegExp(pattern[++i]);\n } else if (char === '*') {\n source += '[\\\\s\\\\S]*';\n } else if (char === '?') {\n source += '[\\\\s\\\\S]';\n } else {\n source += escapeRegExp(char);\n }\n }\n return new RegExp(`^${source}$`, 'i');\n}\n\nconst escapeRegExp = (text: string) => text.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n\nexport const hasWildcards = (pattern: string): boolean => /(^|[^~])[*?]/.test(pattern);\n\n/**\n * Parses a criterion as used by SUMIF, COUNTIFS etc.:\n * - a number or boolean matches equal values (`5` matches 5 and the text \"5\");\n * - text may start with `=`, `<>`, `<`, `<=`, `>` or `>=`. Numeric operands\n * (typed-input rules: `\">1,000\"`, `\"<50%\"`, `\">$5\"`) compare numbers; date\n * operands (`\">=3/1/2026\"`, `\"<2026-03-01\"`) compare ISO dates (and serial\n * numbers) chronologically; text operands compare text case-insensitively;\n * - `=`/`<>` text operands and plain text support `*` and `?` wildcards;\n * - `\"\"` and `\"=\"` match blank cells, `\"<>\"` matches non-blank cells.\n * Error values in the data never match.\n */\nexport function parseCriteria(criterion: unknown): FormulaCriteriaPredicate {\n if (typeof criterion === 'number') {\n return (value) => numericValue(value) === criterion;\n }\n if (typeof criterion === 'boolean') {\n return (value) => value === criterion;\n }\n if (isBlank(criterion)) return (value) => isBlank(value);\n\n const text = String(criterion);\n const match = /^(<>|<=|>=|=|<|>)?([\\s\\S]*)$/.exec(text)!;\n const op = match[1] ?? '=';\n const operand = match[2];\n\n if (operand === '') {\n if (op === '=') return (value) => isBlank(value);\n if (op === '<>') return (value) => !isBlank(value);\n // \"<\" or \">\" with nothing to compare: nothing matches.\n return () => false;\n }\n\n const parsed = parseCellInput(operand.trim());\n const operandNumber = numericValue(operand) ?? (parsed.type === 'number' ? (parsed.value as number) : null);\n const operandDate = parsed.type === 'date' ? (parsed.value as string) : null;\n // Dates are ISO text; a number in the data compares as a serial date.\n const isDateValue = (value: unknown) => typeof value === 'number' || parseIsoDate(value) !== null;\n const operandUpper = operand.trim().toUpperCase();\n const operandBoolean = operandUpper === 'TRUE' ? true : operandUpper === 'FALSE' ? false : null;\n\n if (op === '=' || op === '<>') {\n let equals: FormulaCriteriaPredicate;\n if (operandDate !== null) {\n equals = (value) => isDateValue(value) && compareValues(value, operandDate) === 0;\n } else if (operandNumber !== null) {\n equals = (value) => numericValue(value) === operandNumber;\n } else if (operandBoolean !== null) {\n equals = (value) => value === operandBoolean || (typeof value === 'string' && value.trim().toUpperCase() === operandUpper);\n } else if (isFormulaError(operand)) {\n // Counting errors (\"#N/A\") is the one case where error values match.\n return op === '=' ? (value) => value === operand : (value) => value !== operand && !isBlank(value);\n } else {\n const regex = wildcardToRegExp(operand);\n equals = (value) => typeof value === 'string' && !isFormulaError(value) && regex.test(value);\n }\n return op === '=' ? (value) => !isFormulaError(value) && equals(value) : (value) => !isFormulaError(value) && !equals(value);\n }\n\n const test = (cmp: number) =>\n op === '<' ? cmp < 0 : op === '<=' ? cmp <= 0 : op === '>' ? cmp > 0 : cmp >= 0;\n\n if (operandDate !== null) {\n return (value) => isDateValue(value) && test(compareValues(value, operandDate));\n }\n\n if (operandNumber !== null) {\n // Numeric comparisons only consider numbers (and numeric text).\n return (value) => {\n const num = numericValue(value);\n return num !== null && test(num - operandNumber);\n };\n }\n\n const lower = operand.toLowerCase();\n return (value) => {\n if (typeof value !== 'string' || value === '' || FORMULA_ERRORS.has(value) || numericValue(value) !== null) {\n return false;\n }\n const v = value.toLowerCase();\n return test(v < lower ? -1 : v > lower ? 1 : 0);\n };\n}\n","// Portions ported from gridcraft/crates/numfmt/src/decimal.rs (MIT OR Apache-2.0,\n// © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n/**\n * Decimal-string rounding and the General format.\n *\n * Excel works with 15 significant digits, so numbers are first written with\n * 15 significant digits and then rounded half away from zero on that decimal\n * string. That makes 2.675 with `0.00` show 2.68 and 1.005 show 1.01, as in\n * Excel, even though neither is exact in binary.\n */\n\n/** Most decimals ever computed; placeholders beyond this are padded with zeros. */\nexport const MAX_DECIMALS = 400;\n\n/**\n * `n` significant digits of a positive finite `a` (correctly rounded) and the\n * power of ten of the first digit.\n */\nfunction significantDigits(a: number, n: number): { digits: number[]; exp: number } {\n const s = a.toExponential(Math.max(0, Math.min(100, n - 1)));\n const e = s.indexOf('e');\n const digits: number[] = [];\n for (let i = 0; i < e; i++) {\n const c = s.charCodeAt(i);\n if (c >= 48 && c <= 57) digits.push(c - 48);\n }\n return { digits, exp: Number(s.slice(e + 1)) };\n}\n\n/** The exact decimal expansion of a positive finite double, as significant digits (no trailing zeros). */\nfunction exactDigits(a: number): string {\n const view = new DataView(new ArrayBuffer(8));\n view.setFloat64(0, a);\n const hi = view.getUint32(0);\n const lo = view.getUint32(4);\n const biased = (hi >>> 20) & 0x7ff;\n let mantissa = (BigInt(hi & 0xfffff) << 32n) | BigInt(lo);\n let exp2: number;\n if (biased === 0) {\n exp2 = -1074;\n } else {\n mantissa |= 1n << 52n;\n exp2 = biased - 1075;\n }\n const digits = exp2 >= 0 ? (mantissa << BigInt(exp2)).toString() : (mantissa * 5n ** BigInt(-exp2)).toString();\n return digits.replace(/0+$/, '');\n}\n\n/** True when `a`'s exact decimal expansion has a 5 as its 16th significant digit and only zeros after. */\nfunction isExactTie(a: number): boolean {\n const isTie = (ds: string) => ds.length >= 16 && ds[15] === '5' && /^0*$/.test(ds.slice(16));\n // A cheap screen first (30 digits), then the exact expansion.\n return isTie(significantDigits(a, 30).digits.join('')) && isTie(exactDigits(a).padEnd(16, '0'));\n}\n\n/**\n * The 15 significant digits of a positive finite number and the power of ten\n * of the first digit: `a ≈ d0.d1d2… × 10^exp`.\n *\n * The value is rounded to the nearest 15-digit decimal. An exact tie at the\n * 16th digit goes toward zero, as Excel does: `1234567890123455` shows\n * `1234567890123450`, not `…460`.\n */\nexport function decimalDigits(a: number): { digits: number[]; exp: number } {\n if (!Number.isFinite(a) || a <= 0) return { digits: new Array(15).fill(0), exp: 0 };\n const { digits: d16, exp } = significantDigits(a, 16);\n const digits = d16.slice(0, 15);\n while (digits.length < 15) digits.push(0);\n const sixteenth = d16[15] ?? 0;\n if (sixteenth <= 4) return { digits, exp };\n if (sixteenth === 5) {\n if (isExactTie(a)) return { digits, exp };\n // Not a tie: round the exact value (the 16-digit rounding may be a double rounding).\n const r = significantDigits(a, 15);\n while (r.digits.length < 15) r.digits.push(0);\n return r;\n }\n let e = exp;\n let carry = true;\n for (let i = 14; i >= 0; i--) {\n if (digits[i] === 9) {\n digits[i] = 0;\n } else {\n digits[i] += 1;\n carry = false;\n break;\n }\n }\n if (carry) {\n digits.fill(0);\n digits[0] = 1;\n e += 1;\n }\n return { digits, exp: e };\n}\n\n/**\n * Rounds the number `digits × 10^exp` (see `decimalDigits`) to `decimals`\n * places, half away from zero. Returns the integer digits (`''` when the\n * integer part is zero) and exactly `decimals` fraction digits.\n */\nexport function fixedFromDigits(digits: number[], exp: number, decimals: number): [string, string] {\n const decimalsCalc = Math.min(decimals, MAX_DECIMALS);\n const keep = exp + decimalsCalc + 1;\n let m: number[];\n if (keep <= 0) {\n m = keep === 0 && (digits[0] ?? 0) >= 5 ? [1] : [];\n } else if (keep >= digits.length) {\n m = digits.slice();\n while (m.length < keep) m.push(0);\n } else {\n m = digits.slice(0, keep);\n if ((digits[keep] ?? 0) >= 5) {\n let carry = true;\n for (let i = m.length - 1; i >= 0; i--) {\n if (m[i] === 9) {\n m[i] = 0;\n } else {\n m[i] += 1;\n carry = false;\n break;\n }\n }\n if (carry) m.unshift(1);\n }\n }\n let lead = 0;\n while (lead < m.length && m[lead] === 0) lead++;\n m = m.slice(lead);\n const toS = (ds: number[]) => ds.join('');\n let int: string;\n let frac: string;\n if (m.length <= decimalsCalc) {\n int = '';\n frac = '0'.repeat(decimalsCalc - m.length) + toS(m);\n } else {\n const split = m.length - decimalsCalc;\n int = toS(m.slice(0, split));\n frac = toS(m.slice(split));\n }\n if (decimals > decimalsCalc) frac += '0'.repeat(decimals - decimalsCalc);\n return [int, frac];\n}\n\n/** Rounds a non-negative number to `decimals` places (see `fixedFromDigits`). */\nexport function fixed(a: number, decimals: number): [string, string] {\n if (!Number.isFinite(a) || a <= 0) return ['', '0'.repeat(Math.min(decimals, MAX_DECIMALS * 4))];\n const { digits, exp } = decimalDigits(a);\n return fixedFromDigits(digits, exp, decimals);\n}\n\nconst charCount = (s: string) => Array.from(s).length;\n\n/**\n * The General format squeezed into `maxChars` characters, as Excel shows it\n * in a narrow column: fewer decimals, then scientific notation; `null` when\n * even that does not fit (the caller paints `####`).\n */\nexport function formatGeneralFit(n: number, maxChars: number): string | null {\n if (!Number.isFinite(n)) return null;\n if (n === 0) return maxChars >= 1 ? '0' : null;\n const sign = n < 0 ? '-' : '';\n const { digits, exp: x } = decimalDigits(Math.abs(n));\n const intLen = sign.length + Math.max(x, 0) + 1;\n // Fixed notation for magnitudes from 1E-4 up to what fits.\n if (x >= -4 && intLen <= maxChars) {\n const avail = maxChars - intLen;\n let decimals = avail >= 2 ? avail - 1 : 0;\n for (;;) {\n const [i, f0] = fixedFromDigits(digits, x, decimals);\n const f = f0.replace(/0+$/, '');\n if (i === '' && f === '') break; // rounded away to zero: use scientific\n let s = sign + (i === '' ? '0' : i);\n if (f !== '') s += `.${f}`;\n if (charCount(s) <= maxChars) return s;\n if (decimals === 0) break;\n decimals -= 1;\n }\n }\n for (let md = Math.min(maxChars, 14); md >= 0; md--) {\n let e = x;\n let [mi, mf] = fixedFromDigits(digits, 0, md);\n if (mi.length > 1) {\n e += 1;\n [mi, mf] = fixedFromDigits(digits, -1, md);\n }\n mf = mf.replace(/0+$/, '');\n let s = sign + (mi === '' ? '0' : mi);\n if (mf !== '') s += `.${mf}`;\n s += `E${e < 0 ? '-' : '+'}${String(Math.abs(e)).padStart(2, '0')}`;\n if (charCount(s) <= maxChars) return s;\n }\n return null;\n}\n","// Portions ported from gridcraft/crates/numfmt/src/builtin.rs (MIT OR Apache-2.0,\n// © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n/** Built-in number formats (ECMA-376 §18.8.30, en-US). */\n\nconst BUILTINS: ReadonlyArray<readonly [number, string]> = [\n [0, 'General'],\n [1, '0'],\n [2, '0.00'],\n [3, '#,##0'],\n [4, '#,##0.00'],\n [5, '\"$\"#,##0_);\\\\(\"$\"#,##0\\\\)'],\n [6, '\"$\"#,##0_);[Red]\\\\(\"$\"#,##0\\\\)'],\n [7, '\"$\"#,##0.00_);\\\\(\"$\"#,##0.00\\\\)'],\n [8, '\"$\"#,##0.00_);[Red]\\\\(\"$\"#,##0.00\\\\)'],\n [9, '0%'],\n [10, '0.00%'],\n [11, '0.00E+00'],\n [12, '# ?/?'],\n [13, '# ??/??'],\n [14, 'm/d/yyyy'],\n [15, 'd-mmm-yy'],\n [16, 'd-mmm'],\n [17, 'mmm-yy'],\n [18, 'h:mm AM/PM'],\n [19, 'h:mm:ss AM/PM'],\n [20, 'h:mm'],\n [21, 'h:mm:ss'],\n [22, 'm/d/yyyy h:mm'],\n [37, '#,##0 ;(#,##0)'],\n [38, '#,##0 ;[Red](#,##0)'],\n [39, '#,##0.00;(#,##0.00)'],\n [40, '#,##0.00;[Red](#,##0.00)'],\n [41, '_(* #,##0_);_(* \\\\(#,##0\\\\);_(* \"-\"_);_(@_)'],\n [42, '_(\"$\"* #,##0_);_(\"$\"* \\\\(#,##0\\\\);_(\"$\"* \"-\"_);_(@_)'],\n [43, '_(* #,##0.00_);_(* \\\\(#,##0.00\\\\);_(* \"-\"??_);_(@_)'],\n [44, '_(\"$\"* #,##0.00_);_(\"$\"* \\\\(#,##0.00\\\\);_(\"$\"* \"-\"??_);_(@_)'],\n [45, 'mm:ss'],\n [46, '[h]:mm:ss'],\n [47, 'mmss.0'],\n [48, '##0.0E+0'],\n [49, '@'],\n];\n\n/**\n * Built-in format code for an id (0–49, en-US), e.g. 14 → `m/d/yyyy`.\n * Locale-specific ids (23–36) and unknown ids return undefined.\n */\nexport function builtinNumberFormat(id: number): string | undefined {\n return BUILTINS.find(([i]) => i === id)?.[1];\n}\n\n/** Id of a built-in format code. Matching ignores case and backslash escapes. */\nexport function builtinNumberFormatId(code: string): number | undefined {\n const exact = BUILTINS.find(([, c]) => c === code);\n if (exact) return exact[0];\n const norm = (s: string) => s.replace(/\\\\/g, '').toLowerCase();\n const n = norm(code);\n return BUILTINS.find(([, c]) => norm(c) === n)?.[0];\n}\n","// Portions ported from gridcraft/crates/numfmt/src/parse.rs (MIT OR Apache-2.0,\n// © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n/** Number format code parsing: sections, bracket tags and tokens. */\n\n/** Colour from a `[Red]` or `[ColorN]` section tag (`color1`…`color56` = palette index). */\nexport type NumberFormatColor =\n | 'black'\n | 'blue'\n | 'cyan'\n | 'green'\n | 'magenta'\n | 'red'\n | 'white'\n | 'yellow'\n | `color${number}`;\n\n/** At most this many sections are honoured (`pos;neg;zero;text`); extra ones are ignored. */\nconst MAX_SECTIONS = 4;\n\nexport type DatePart =\n | { k: 'year' | 'month' | 'minute' | 'day' | 'hour' | 'second' | 'subsec' | 'eh' | 'em' | 'es'; n: number }\n | { k: 'ampm' }\n /** `A/P` with the letters as typed. */\n | { k: 'ap'; a: string; p: string };\n\nexport const isCalendarPart = (d: DatePart): boolean => d.k === 'year' || d.k === 'month' || d.k === 'day';\n\nexport type Tok =\n | { t: 'lit'; s: string }\n /** `0`, `#` or `?`. */\n | { t: 'digit'; c: string }\n /** The decimal point (only the first one in a number section). */\n | { t: 'point' }\n /** Unresolved comma (resolved during analysis into thousands/scale markers or a literal). */\n | { t: 'comma' }\n | { t: 'percent' }\n | { t: 'exp'; upper: boolean; plus: boolean }\n /** The fraction bar (only after analysis decides the section is a fraction). */\n | { t: 'slash' }\n /** A fixed fraction denominator such as the `8` in `?/8`. */\n | { t: 'denom'; s: string }\n | { t: 'fill'; c: string }\n | { t: 'at' }\n | { t: 'general' }\n | { t: 'date'; d: DatePart };\n\n/** The text a token shows when it has no special meaning in its section. */\nexport function literalText(tok: Tok): string {\n switch (tok.t) {\n case 'lit':\n case 'denom':\n return tok.s;\n case 'digit':\n return tok.c;\n case 'point':\n return '.';\n case 'comma':\n return ',';\n case 'percent':\n return '%';\n case 'exp':\n return `${tok.upper ? 'E' : 'e'}${tok.plus ? '+' : '-'}`;\n case 'slash':\n return '/';\n default:\n return '';\n }\n}\n\nexport type CondOp = '=' | '<>' | '<' | '<=' | '>' | '>=';\n\nexport interface Cond {\n op: CondOp;\n value: number;\n}\n\nexport function testCond(c: Cond, v: number): boolean {\n switch (c.op) {\n case '=':\n return v === c.value;\n case '<>':\n return v !== c.value;\n case '<':\n return v < c.value;\n case '<=':\n return v <= c.value;\n case '>':\n return v > c.value;\n case '>=':\n return v >= c.value;\n }\n}\n\nexport type SecKind = 'number' | 'date' | 'text' | 'general';\n\nexport interface Section {\n toks: Tok[];\n color?: NumberFormatColor;\n cond?: Cond;\n kind: SecKind;\n thousands: boolean;\n /** Number of scaling commas (each divides by 1000). */\n scale: number;\n percent: number;\n hasAt: boolean;\n hasDigits: boolean;\n exp: boolean;\n fraction: boolean;\n currency: boolean;\n fill: boolean;\n}\n\nconst isDigitTok = (t: Tok | undefined, c?: string): boolean => t?.t === 'digit' && (c === undefined || t.c === c);\n\n/**\n * Splits a code into sections at unquoted, unescaped, unbracketed semicolons.\n * A `[` opens a bracket only when a `]` follows it somewhere; the bracket then\n * runs to the next `]`. An unclosed `[` is a literal, so a later `;` still\n * separates sections. Linear in the code length.\n */\nexport function splitSections(code: string): string[] {\n const cs = Array.from(code);\n const lastClose = cs.lastIndexOf(']');\n const out: string[] = [''];\n let quoted = false;\n let escape = false;\n let i = 0;\n while (i < cs.length) {\n const c = cs[i];\n i += 1;\n const cur = out.length - 1;\n if (escape) {\n out[cur] += c;\n escape = false;\n continue;\n }\n if (c === '\"') {\n quoted = !quoted;\n out[cur] += c;\n } else if ((c === '\\\\' || c === '_' || c === '*') && !quoted) {\n escape = true;\n out[cur] += c;\n } else if (c === '[' && !quoted && lastClose >= i) {\n // `i` is already past the `[`; the closing bracket is at or after it.\n let s = c;\n while (i < cs.length) {\n const d = cs[i];\n i += 1;\n s += d;\n if (d === ']') break;\n }\n out[cur] += s;\n } else if (c === ';' && !quoted) {\n if (out.length >= MAX_SECTIONS) break;\n out.push('');\n } else {\n out[cur] += c;\n }\n }\n return out;\n}\n\nconst lowerAscii = (c: string) => (c >= 'A' && c <= 'Z' ? c.toLowerCase() : c);\n\nfunction startsWithCi(cs: string[], i: number, pat: string): boolean {\n for (let k = 0; k < pat.length; k++) {\n const c = cs[i + k];\n if (c === undefined || lowerAscii(c) !== pat[k]) return false;\n }\n return true;\n}\n\nfunction pushLit(toks: Tok[], s: string): void {\n if (s === '') return;\n const last = toks[toks.length - 1];\n if (last?.t === 'lit') last.s += s;\n else toks.push({ t: 'lit', s });\n}\n\nconst CURRENCY_CHARS = new Set(['$', '€', '£', '¥', '₹', '₩', '₽', '¢', '₪', '₫', '฿']);\nconst isCurrencyChar = (c: string) => CURRENCY_CHARS.has(c);\n\nconst NAMED_COLORS = new Set(['black', 'blue', 'cyan', 'green', 'magenta', 'red', 'white', 'yellow']);\n\nfunction parseColor(lc: string): NumberFormatColor | undefined {\n if (NAMED_COLORS.has(lc)) return lc as NumberFormatColor;\n if (!lc.startsWith('color')) return undefined;\n const rest = lc.slice(5).trim();\n if (!/^\\+?\\d{1,3}$/.test(rest)) return undefined;\n const n = Number(rest);\n return n >= 1 && n <= 56 ? `color${n}` : undefined;\n}\n\nfunction parseCond(s: string): Cond | undefined {\n const t = s.trim();\n let op: CondOp;\n let rest: string;\n if (t.startsWith('<=')) [op, rest] = ['<=', t.slice(2)];\n else if (t.startsWith('>=')) [op, rest] = ['>=', t.slice(2)];\n else if (t.startsWith('<>')) [op, rest] = ['<>', t.slice(2)];\n else if (t.startsWith('<')) [op, rest] = ['<', t.slice(1)];\n else if (t.startsWith('>')) [op, rest] = ['>', t.slice(1)];\n else if (t.startsWith('=')) [op, rest] = ['=', t.slice(1)];\n else return undefined;\n const r = rest.trim();\n if (!/^[+-]?(\\d+\\.?\\d*|\\.\\d+)(e[+-]?\\d+)?$/i.test(r)) return undefined;\n const value = Number(r);\n return Number.isFinite(value) ? { op, value } : undefined;\n}\n\ninterface Raw {\n toks: Tok[];\n color?: NumberFormatColor;\n cond?: Cond;\n currency: boolean;\n}\n\n/** Handles the content of a `[...]` tag. */\nfunction bracket(raw: Raw, content: string): void {\n const lc = content.trim().toLowerCase();\n const color = parseColor(lc);\n if (color) {\n raw.color = color;\n return;\n }\n if (lc.startsWith('<') || lc.startsWith('>') || lc.startsWith('=')) {\n const c = parseCond(lc);\n if (c) raw.cond = c;\n return;\n }\n if (content.startsWith('$')) {\n const sym = content.slice(1).split('-')[0];\n if (sym !== '') {\n raw.currency = true;\n pushLit(raw.toks, sym);\n }\n return;\n }\n const first = lc[0];\n if ((first === 'h' || first === 'm' || first === 's') && Array.from(lc).every((c) => c === first)) {\n const n = Math.min(Array.from(lc).length, 255);\n raw.toks.push({ t: 'date', d: { k: first === 'h' ? 'eh' : first === 'm' ? 'em' : 'es', n } });\n }\n // Anything else ([DBNum1], [natural], locale tags…) is ignored.\n}\n\nfunction tokenize(src: string): Raw {\n const cs = Array.from(src);\n const raw: Raw = { toks: [], currency: false };\n const toks = raw.toks;\n // With no `]` after a `[`, that `[` is a literal; checking this once keeps the scan linear.\n const lastClose = cs.lastIndexOf(']');\n let i = 0;\n while (i < cs.length) {\n const c = cs[i];\n if (c === '\"') {\n let s = '';\n i += 1;\n while (i < cs.length) {\n const d = cs[i];\n i += 1;\n if (d === '\"') break;\n s += d;\n }\n if (Array.from(s).some(isCurrencyChar)) raw.currency = true;\n pushLit(toks, s);\n continue;\n }\n if (c === '\\\\') {\n const d = cs[i + 1];\n if (d !== undefined) {\n if (isCurrencyChar(d)) raw.currency = true;\n pushLit(toks, d);\n }\n i += 2;\n continue;\n }\n if (c === '_') {\n if (cs[i + 1] !== undefined) pushLit(toks, ' ');\n i += 2;\n continue;\n }\n if (c === '*') {\n const d = cs[i + 1];\n if (d !== undefined) toks.push({ t: 'fill', c: d });\n i += 2;\n continue;\n }\n if (c === '[') {\n const close = lastClose > i ? cs.indexOf(']', i + 1) : -1;\n if (close !== -1) {\n bracket(raw, cs.slice(i + 1, close).join(''));\n i = close + 1;\n } else {\n pushLit(toks, '[');\n i += 1;\n }\n continue;\n }\n if (c === '0' || c === '#' || c === '?') toks.push({ t: 'digit', c });\n else if (c === '.') toks.push({ t: 'point' });\n else if (c === ',') toks.push({ t: 'comma' });\n else if (c === '%') toks.push({ t: 'percent' });\n else if (c === '@') toks.push({ t: 'at' });\n else if (c === '/') {\n toks.push({ t: 'slash' });\n i += 1;\n if (cs[i] !== undefined && cs[i] >= '1' && cs[i] <= '9') {\n let s = '';\n while (i < cs.length && cs[i] >= '0' && cs[i] <= '9') {\n s += cs[i];\n i += 1;\n }\n toks.push({ t: 'denom', s });\n }\n continue;\n } else if ((c === 'e' || c === 'E') && (cs[i + 1] === '+' || cs[i + 1] === '-')) {\n toks.push({ t: 'exp', upper: c === 'E', plus: cs[i + 1] === '+' });\n i += 2;\n continue;\n } else if ((c === 'G' || c === 'g') && startsWithCi(cs, i, 'general')) {\n toks.push({ t: 'general' });\n i += 7;\n continue;\n } else if ((c === 'A' || c === 'a') && startsWithCi(cs, i, 'am/pm')) {\n toks.push({ t: 'date', d: { k: 'ampm' } });\n i += 5;\n continue;\n } else if ((c === 'A' || c === 'a') && startsWithCi(cs, i, 'a/p')) {\n toks.push({ t: 'date', d: { k: 'ap', a: c, p: cs[i + 2] ?? 'P' } });\n i += 3;\n continue;\n } else if ('yYmMdDhHsSeE'.includes(c)) {\n const lc = lowerAscii(c);\n let n = 0;\n while (cs[i + n] !== undefined && lowerAscii(cs[i + n]) === lc) n += 1;\n const n8 = Math.min(n, 255);\n const d: DatePart =\n lc === 'y'\n ? { k: 'year', n: n8 }\n : lc === 'e'\n ? { k: 'year', n: 4 }\n : lc === 'm'\n ? { k: 'month', n: n8 }\n : lc === 'd'\n ? { k: 'day', n: n8 }\n : lc === 'h'\n ? { k: 'hour', n: n8 }\n : { k: 'second', n: n8 };\n toks.push({ t: 'date', d });\n i += Math.max(n, 1);\n continue;\n } else {\n if (isCurrencyChar(c)) raw.currency = true;\n pushLit(toks, c);\n }\n i += 1;\n }\n return raw;\n}\n\nconst datePartOf = (t: Tok | undefined): DatePart | undefined => (t?.t === 'date' ? t.d : undefined);\n\nfunction analyze(raw: Raw): Section {\n const toks = raw.toks;\n const isDate = toks.some((t) => t.t === 'date');\n const hasGeneral = toks.some((t) => t.t === 'general');\n const sec: Section = {\n toks: [],\n color: raw.color,\n cond: raw.cond,\n kind: 'number',\n thousands: false,\n scale: 0,\n percent: 0,\n hasAt: toks.some((t) => t.t === 'at'),\n hasDigits: false,\n exp: false,\n fraction: false,\n currency: raw.currency,\n fill: toks.some((t) => t.t === 'fill'),\n };\n if (isDate) {\n sec.kind = 'date';\n // m/mm after hours or before seconds means minutes. Scanning for the\n // nearest date part either side is linear overall (prev/next are tracked).\n const prevPart: Array<DatePart | undefined> = [];\n let last: DatePart | undefined;\n for (let i = 0; i < toks.length; i++) {\n prevPart.push(last);\n const d = datePartOf(toks[i]);\n if (d) last = d;\n }\n const nextPart: Array<DatePart | undefined> = new Array(toks.length);\n let next: DatePart | undefined;\n for (let i = toks.length - 1; i >= 0; i--) {\n nextPart[i] = next;\n const d = datePartOf(toks[i]);\n if (d) next = d;\n }\n for (let i = 0; i < toks.length; i++) {\n const t = toks[i];\n if (t.t !== 'date' || t.d.k !== 'month' || t.d.n > 2) continue;\n const prev = prevPart[i];\n const nxt = nextPart[i];\n const afterH = prev?.k === 'hour' || prev?.k === 'eh';\n const beforeS = nxt?.k === 'second' || nxt?.k === 'es';\n if (afterH || beforeS) toks[i] = { t: 'date', d: { k: 'minute', n: t.d.n } };\n }\n const out: Tok[] = [];\n let i = 0;\n while (i < toks.length) {\n const t = toks[i];\n if (t.t === 'point' && isDigitTok(toks[i + 1], '0')) {\n let n = 0;\n while (isDigitTok(toks[i + 1 + n], '0')) n += 1;\n out.push({ t: 'date', d: { k: 'subsec', n: Math.min(n, 255) } });\n i += 1 + n;\n continue;\n }\n if (t.t === 'date' || t.t === 'fill') out.push(t);\n else pushLit(out, literalText(t));\n i += 1;\n }\n sec.toks = out;\n return sec;\n }\n if (hasGeneral) {\n sec.kind = 'general';\n const out: Tok[] = [];\n for (const t of toks) {\n if (t.t === 'general' || t.t === 'fill') out.push(t);\n else pushLit(out, literalText(t));\n }\n sec.toks = out;\n return sec;\n }\n sec.hasDigits = toks.some((t) => t.t === 'digit');\n if (sec.hasAt && !sec.hasDigits) {\n sec.kind = 'text';\n const out: Tok[] = [];\n for (const t of toks) {\n if (t.t === 'at' || t.t === 'fill') out.push(t);\n else pushLit(out, literalText(t));\n }\n sec.toks = out;\n return sec;\n }\n // Number section.\n let seenPoint = false;\n let seenExp = false;\n let seenDigit = false;\n let fractionSlash = false;\n // Precomputed so each comma and slash is O(1): whether the first non-comma\n // token after a position is a digit, the first non-literal token after a\n // position, and where the last exponent marker is.\n const digitAfterCommas: boolean[] = new Array(toks.length).fill(false);\n const firstNonLitAfter: Array<Tok | undefined> = new Array(toks.length);\n let after = false;\n let nonLit: Tok | undefined;\n for (let j = toks.length - 1; j >= 0; j--) {\n digitAfterCommas[j] = after;\n firstNonLitAfter[j] = nonLit;\n const t = toks[j];\n if (t.t !== 'comma') after = t.t === 'digit';\n if (t.t !== 'lit') nonLit = t;\n }\n let lastExp = -1;\n for (let j = toks.length - 1; j >= 0; j--) {\n if (toks[j].t === 'exp') {\n lastExp = j;\n break;\n }\n }\n const firstDigit = toks.findIndex((t) => t.t === 'digit');\n const scientific = firstDigit !== -1 && toks.slice(firstDigit).some((t) => t.t === 'exp');\n for (let i = 0; i < toks.length; i++) {\n const t = toks[i];\n let replace: Tok | undefined;\n switch (t.t) {\n case 'digit':\n seenDigit = true;\n break;\n case 'point':\n if (seenPoint || seenExp) replace = { t: 'lit', s: '.' };\n else seenPoint = true;\n break;\n case 'percent':\n sec.percent += 1;\n break;\n case 'exp':\n if (seenExp || !seenDigit) replace = { t: 'lit', s: literalText(t) };\n else {\n seenExp = true;\n sec.exp = true;\n }\n break;\n case 'comma': {\n const nextDigit = digitAfterCommas[i];\n if (seenDigit && nextDigit && !seenPoint && !seenExp) {\n sec.thousands = true;\n replace = { t: 'lit', s: '' };\n } else if (seenDigit && scientific) {\n // Observed: a scientific section never scales; any other comma after\n // the first digit shows nothing (`#,e+#`, `#e+#,`, `#e+000,00`).\n replace = { t: 'lit', s: '' };\n } else if (seenDigit && !nextDigit) {\n sec.scale += 1;\n replace = { t: 'lit', s: '' };\n } else {\n replace = { t: 'lit', s: ',' };\n }\n break;\n }\n case 'slash': {\n // Literal text may sit between the bar and the denominator (`#/ #`, `# ?=/=?`).\n const nextOk = toks[i + 1]?.t === 'denom' || firstNonLitAfter[i]?.t === 'digit';\n // An exponent kept before this bar or any marker still ahead of it.\n const hasExp = seenExp || lastExp > i;\n if (!fractionSlash && seenDigit && nextOk && !seenPoint && !hasExp) {\n fractionSlash = true;\n sec.fraction = true;\n } else {\n replace = { t: 'lit', s: '/' };\n }\n break;\n }\n case 'denom':\n if (!(toks[i - 1]?.t === 'slash' && sec.fraction)) replace = { t: 'lit', s: t.s };\n break;\n default:\n break;\n }\n if (replace) toks[i] = replace;\n }\n if (sec.fraction) {\n // A decimal point in a fraction section is literal.\n for (let i = 0; i < toks.length; i++) if (toks[i].t === 'point') toks[i] = { t: 'lit', s: '.' };\n }\n // Merge literals (removed commas left empty ones).\n const out: Tok[] = [];\n for (const t of toks) {\n if (t.t === 'lit') pushLit(out, t.s);\n else out.push(t);\n }\n sec.toks = out;\n return sec;\n}\n\n/** Parses a whole format code into its sections. */\nexport function parseSections(code: string): Section[] {\n return splitSections(code).map((s) => analyze(tokenize(s)));\n}\n","// Portions ported from gridcraft/crates/numfmt/src/render.rs and gridcraft/crates/core/src/date.rs\n// (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n/** Rendering a value with a parsed section. */\nimport { decimalDigits, fixed, fixedFromDigits, formatGeneralFit } from './decimal';\nimport { isCalendarPart, literalText, type Section, type Tok } from './parse';\n\n/** Last serial that is a valid date in the 1900 date system: 9999-12-31. */\nexport const MAX_DATE_SERIAL = 2_958_465;\n\nconst MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];\nconst WEEKDAYS = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];\nconst UNIX_1899_12_30 = -25569;\n\n/** Civil date for days since 1970-01-01 (Howard Hinnant's algorithm). */\nfunction civilFromDays(z0: number): { y: number; m: number; d: number } {\n const z = z0 + 719468;\n const era = Math.floor(z / 146097);\n const doe = z - era * 146097;\n const yoe = Math.floor((doe - Math.floor(doe / 1460) + Math.floor(doe / 36524) - Math.floor(doe / 146096)) / 365);\n const doy = doe - (365 * yoe + Math.floor(yoe / 4) - Math.floor(yoe / 100));\n const mp = Math.floor((5 * doy + 2) / 153);\n const d = doy - Math.floor((153 * mp + 2) / 5) + 1;\n const m = mp < 10 ? mp + 3 : mp - 9;\n return { y: yoe + era * 400 + (m <= 2 ? 1 : 0), m, d };\n}\n\n/**\n * Year, month, day and weekday (0 = Sunday) of a whole 1900-system serial.\n * Serial 0 is the fictitious 1900-01-00 and serial 60 the fictitious\n * 1900-02-29; weekdays continue that calendar (serial 1 is a Sunday).\n */\nexport function dateFromSerial(day: number): { year: number; month: number; day: number; weekday: number } {\n const weekday = (((day + 6) % 7) + 7) % 7;\n if (day === 60) return { year: 1900, month: 2, day: 29, weekday };\n if (day === 0) return { year: 1900, month: 1, day: 0, weekday };\n const real = day < 60 ? day + 1 : day;\n const { y, m, d } = civilFromDays(real + UNIX_1899_12_30);\n return { year: y, month: m, day: d, weekday };\n}\n\n/** Text being built plus the first repeat-fill position. */\nexport class Out {\n text = '';\n fill: { char: string; index: number } | undefined;\n /** True when every digit shown is zero (a number section that rounded to zero). */\n zero = false;\n push(s: string): void {\n this.text += s;\n }\n setFill(c: string): void {\n if (!this.fill) this.fill = { char: c, index: this.text.length };\n }\n prepend(s: string): void {\n this.text = s + this.text;\n if (this.fill) this.fill.index += s.length;\n }\n}\n\n/** Renders a text value through a text section (`@` is replaced by the text). */\nexport function renderText(sec: Section, text: string): Out {\n const out = new Out();\n for (const t of sec.toks) {\n if (t.t === 'at') out.push(text);\n else if (t.t === 'fill') out.setFill(t.c);\n else out.push(literalText(t));\n }\n return out;\n}\n\n/** Renders a number through a General section (literals around the General keyword). */\nexport function renderGeneral(sec: Section, v: number, width: number): Out {\n const out = new Out();\n for (const t of sec.toks) {\n if (t.t === 'general') out.push(formatGeneralFit(v, width) ?? '#'.repeat(Math.min(Math.max(width, 1), 11)));\n else if (t.t === 'fill') out.setFill(t.c);\n else out.push(literalText(t));\n }\n return out;\n}\n\nconst padNum = (n: number, w: number) => (w >= 2 ? String(n).padStart(Math.min(w, 20), '0') : String(n));\n\n/** Renders a date/time. `null` when the serial is outside the supported range. */\nexport function renderDate(sec: Section, v: number): Out | null {\n if (!Number.isFinite(v) || v < 0 || v >= MAX_DATE_SERIAL + 1) return null;\n let subDigits = 0;\n for (const t of sec.toks) if (t.t === 'date' && t.d.k === 'subsec') subDigits = Math.max(subDigits, t.d.n);\n subDigits = Math.min(subDigits, 3);\n const unit = 10 ** (3 - subDigits);\n let day = Math.floor(v);\n let ms = Math.round(((v - Math.floor(v)) * 86_400_000) / unit) * unit;\n if (ms >= 86_400_000) {\n ms -= 86_400_000;\n day += 1;\n }\n const totalMs = day * 86_400_000 + ms;\n const needsDate = sec.toks.some((t) => t.t === 'date' && isCalendarPart(t.d));\n if (needsDate && day > MAX_DATE_SERIAL) return null;\n const dt = needsDate ? dateFromSerial(day) : dateFromSerial(0);\n const ampm = sec.toks.some((t) => t.t === 'date' && (t.d.k === 'ampm' || t.d.k === 'ap'));\n const hour = Math.floor(ms / 3_600_000);\n const minute = Math.floor(ms / 60_000) % 60;\n const second = Math.floor(ms / 1000) % 60;\n const milli = ms % 1000;\n const monthName = MONTHS[dt.month - 1] ?? '';\n const dayName = WEEKDAYS[dt.weekday] ?? '';\n const out = new Out();\n for (const t of sec.toks) {\n if (t.t === 'fill') {\n out.setFill(t.c);\n continue;\n }\n if (t.t !== 'date') {\n out.push(literalText(t));\n continue;\n }\n const d = t.d;\n let s: string;\n switch (d.k) {\n case 'year':\n s = d.n <= 2 ? String(((dt.year % 100) + 100) % 100).padStart(2, '0') : String(dt.year).padStart(4, '0');\n break;\n case 'month':\n s = d.n <= 2 ? padNum(dt.month, d.n) : d.n === 3 ? monthName.slice(0, 3) : d.n === 5 ? monthName.slice(0, 1) : monthName;\n break;\n case 'day':\n s = d.n <= 2 ? padNum(dt.day, d.n) : d.n === 3 ? dayName.slice(0, 3) : dayName;\n break;\n case 'hour':\n s = padNum(ampm ? hour % 12 || 12 : hour, d.n);\n break;\n case 'minute':\n s = padNum(minute, d.n);\n break;\n case 'second':\n s = padNum(second, d.n);\n break;\n case 'subsec': {\n s = `.${String(milli).padStart(3, '0')}`.slice(0, 1 + Math.min(d.n, 3));\n if (d.n > 3) s += '0'.repeat(d.n - 3);\n break;\n }\n case 'eh':\n s = padNum(Math.floor(totalMs / 3_600_000), d.n);\n break;\n case 'em':\n s = padNum(Math.floor(totalMs / 60_000), d.n);\n break;\n case 'es':\n s = padNum(Math.floor(totalMs / 1000), d.n);\n break;\n case 'ampm':\n s = hour >= 12 ? 'PM' : 'AM';\n break;\n case 'ap':\n s = hour >= 12 ? d.p : d.a;\n break;\n }\n out.push(s);\n }\n return out;\n}\n\nconst digitChar = (toks: Tok[], i: number): string => {\n const t = toks[i];\n return t?.t === 'digit' ? t.c : '0';\n};\n\n/**\n * Assigns integer digits right-to-left to the placeholders at `idxs`; the\n * leftmost placeholder takes all remaining digits. `forceZero` shows `0` in\n * the units placeholder when there are no digits. A blank `?` pads with a\n * space, and so does the thousands separator it would carry.\n */\nfunction fillInt(toks: Tok[], idxs: number[], digits: string, thousands: boolean, forceZero: boolean, pieces: string[]): void {\n let rem = digits.length;\n let pos = 0;\n const count = idxs.length;\n for (let k = 0; k < count; k++) {\n const ti = idxs[count - 1 - k];\n const leftmost = k + 1 === count;\n const ph = digitChar(toks, ti);\n const rev: string[] = [];\n const pushDigit = (c: string) => {\n if (thousands && pos > 0 && pos % 3 === 0) rev.push(',');\n rev.push(c);\n pos += 1;\n };\n const take = leftmost ? rem : Math.min(rem, 1);\n if (take === 0) {\n if (ph === '0') pushDigit('0');\n else if (forceZero && k === 0) pushDigit('0');\n else if (ph === '?') {\n if (thousands && pos > 0 && pos % 3 === 0) rev.push(' ');\n rev.push(' ');\n pos += 1;\n }\n } else {\n for (let n = 0; n < take; n++) {\n rem -= 1;\n pushDigit(digits[rem] ?? '0');\n }\n }\n pieces[ti] = rev.reverse().join('');\n }\n}\n\nfunction withCommas(digits: string, thousands: boolean): string {\n if (!thousands) return digits;\n const n = digits.length;\n let s = '';\n for (let i = 0; i < n; i++) {\n if (i > 0 && (n - i) % 3 === 0) s += ',';\n s += digits[i];\n }\n return s;\n}\n\n/** Fraction-digit placeholders: trailing zeros show as `0`, nothing (`#`) or a space (`?`). */\nfunction fillFrac(toks: Tok[], idxs: number[], frac: string, pieces: string[]): void {\n let lastSig = -1;\n for (let i = frac.length - 1; i >= 0; i--) {\n if (frac[i] !== '0') {\n lastSig = i;\n break;\n }\n }\n idxs.forEach((ti, i) => {\n const ph = digitChar(toks, ti);\n pieces[ti] = i <= lastSig ? (frac[i] ?? '0') : ph === '0' ? '0' : ph === '?' ? ' ' : '';\n });\n}\n\nfunction digitIndices(toks: Tok[], start: number, end: number): number[] {\n const out: number[] = [];\n for (let i = start; i < end; i++) if (toks[i]?.t === 'digit') out.push(i);\n return out;\n}\n\n/** Renders a non-negative number through a number section. */\nexport function renderNumber(sec: Section, v: number): Out {\n let x = Math.abs(v);\n // Observed: `%` in a scientific section is only a literal (`#%e+#` shows 123456.789 as `1%e+5`).\n if (sec.percent > 0 && !sec.exp) x *= 100 ** Math.min(sec.percent, 20);\n if (sec.scale > 0) x /= 1000 ** Math.min(sec.scale, 110);\n if (!Number.isFinite(x)) x = Number.MAX_VALUE;\n const toks = sec.toks;\n const n = toks.length;\n // Pieces replace the token's own output; `before` is inserted ahead of a\n // token; `lits` replaces a literal's text (fractions blank hidden parts).\n const pieces: string[] = new Array(n).fill('');\n const before: string[] = new Array(n + 1).fill('');\n const lits: Array<string | undefined> = new Array(n);\n let zero = false;\n if (sec.fraction) {\n renderFraction(sec, x, pieces, lits);\n } else {\n const pointIdx = toks.findIndex((t) => t.t === 'point');\n const expIdx = toks.findIndex((t) => t.t === 'exp');\n const point = pointIdx === -1 ? undefined : pointIdx;\n const exp = expIdx === -1 ? undefined : expIdx;\n const intEnd = point ?? exp ?? n;\n const fracEnd = exp ?? n;\n const intIdx = digitIndices(toks, 0, intEnd);\n const fracIdx = point === undefined ? [] : digitIndices(toks, point + 1, fracEnd);\n let intS: string;\n let fracS: string;\n if (exp !== undefined) {\n const expDigitIdx = digitIndices(toks, exp + 1, n);\n const step = Math.max(intIdx.length, 1);\n let e: number;\n if (x === 0) {\n // Observed: zero shows `0` in every integer placeholder, `#` and `?` too.\n e = 0;\n intS = '0'.repeat(intIdx.length);\n fracS = '0'.repeat(fracIdx.length);\n } else {\n const { digits, exp: xe } = decimalDigits(x);\n e = xe - (((xe % step) + step) % step);\n [intS, fracS] = fixedFromDigits(digits, xe - e, fracIdx.length);\n if (intS.length > step) {\n e += step;\n [intS, fracS] = fixedFromDigits(digits, xe - e, fracIdx.length);\n }\n }\n const et = toks[exp];\n const upper = et.t === 'exp' ? et.upper : true;\n const plus = et.t === 'exp' ? et.plus : true;\n if (intS === '' && /^0*$/.test(fracS)) e = 0;\n const head = upper ? 'E' : 'e';\n const sign = e < 0 ? '-' : plus ? '+' : '';\n const expDigits = String(Math.abs(e));\n // The sign sits right before the exponent digits, after any literal that\n // follows the `E`; `?` pads the digits with spaces.\n if (expDigitIdx.length > 0) {\n pieces[exp] = head;\n before[expDigitIdx[0]] = sign;\n fillInt(toks, expDigitIdx, expDigits, false, false, pieces);\n } else {\n pieces[exp] = `${head}${sign}${expDigits}`;\n }\n } else {\n [intS, fracS] = fixed(x, fracIdx.length);\n }\n zero = /^0*$/.test(intS + fracS);\n fillInt(toks, intIdx, intS, sec.thousands, false, pieces);\n const anchor = point ?? exp;\n if (intIdx.length === 0 && intS !== '' && anchor !== undefined) before[anchor] = withCommas(intS, sec.thousands);\n fillFrac(toks, fracIdx, fracS, pieces);\n if (point !== undefined) pieces[point] = '.';\n }\n const out = new Out();\n for (let i = 0; i < n; i++) {\n out.push(before[i]);\n const t = toks[i];\n if (t.t === 'lit') out.push(lits[i] ?? t.s);\n else if (t.t === 'percent') out.push('%');\n else if (t.t === 'fill') out.setFill(t.c);\n else if (t.t === 'at' || t.t === 'general' || t.t === 'date' || t.t === 'comma') continue;\n else out.push(pieces[i]);\n }\n out.zero = zero;\n return out;\n}\n\n/** Best rational approximation of `x` with denominator at most `maxDen` (continued fractions). */\nexport function approximate(x: number, maxDen: number): [number, number] {\n if (Number.isNaN(x) || x <= 0 || !Number.isFinite(x)) return [0, 1];\n if (x >= 1e15) return [Math.round(x), 1];\n let [p0, q0, p1, q1] = [0, 1, 1, 0];\n let r = x;\n for (let iter = 0; iter < 64; iter++) {\n const aF = Math.floor(r);\n if (aF > 1e15) break;\n const q2 = aF * q1 + q0;\n if (q2 > maxDen) break;\n const p2 = aF * p1 + p0;\n [p0, q0, p1, q1] = [p1, q1, p2, q2];\n const frac = r - aF;\n if (frac < 1e-12) break;\n r = 1 / frac;\n }\n if (q1 === 0) return [Math.round(x), 1];\n // Semiconvergent bound.\n const k = Math.floor((maxDen - q0) / q1);\n const pb = p0 + k * p1;\n const qb = q0 + k * q1;\n const errA = Math.abs(p1 / q1 - x);\n const errB = qb > 0 ? Math.abs(pb / qb - x) : Infinity;\n return errB < errA ? [pb, qb] : [p1, q1];\n}\n\n/** Display width of a token in a fraction section, for blanking a hidden part with spaces. */\nfunction shownWidth(t: Tok): number {\n if (t.t === 'lit' || t.t === 'denom') return Array.from(t.s).length;\n return t.t === 'digit' || t.t === 'slash' ? 1 : 0;\n}\n\n/** Replaces the literals, placeholders and bar in `[start, end)` with spaces (`spaces`) or nothing. */\nfunction blankRange(toks: Tok[], start: number, end: number, spaces: boolean, pieces: string[], lits: Array<string | undefined>): void {\n for (let i = start; i < end && i < toks.length; i++) {\n const t = toks[i];\n const s = spaces ? ' '.repeat(shownWidth(t)) : '';\n if (t.t === 'lit') lits[i] = s;\n else if (t.t === 'digit' || t.t === 'slash' || t.t === 'denom') pieces[i] = s;\n }\n}\n\n/**\n * A fraction section is `[whole] [sep] num [lit] / [lit] den`: the numerator\n * is the run of placeholders ending at the last one before the bar, the whole\n * part is every placeholder before it, `sep` the literals between them.\n *\n * Observed Excel behaviour: when the whole part shows no digit its separator\n * disappears (or becomes spaces when the whole part or the numerator has a\n * `?`); when the fraction is zero and the numerator has no `0` placeholder,\n * the separator, numerator, bar and denominator disappear (or become spaces\n * when any part has a `?`) and the whole part shows at least `0`.\n */\nfunction renderFraction(sec: Section, x: number, pieces: string[], lits: Array<string | undefined>): void {\n const toks = sec.toks;\n const isDigit = (i: number) => toks[i]?.t === 'digit';\n const slash = toks.findIndex((t) => t.t === 'slash');\n if (slash === -1) return;\n let numLast = -1;\n for (let i = slash - 1; i >= 0; i--) {\n if (isDigit(i)) {\n numLast = i;\n break;\n }\n }\n if (numLast === -1) return;\n let numStart = numLast;\n while (numStart > 0 && isDigit(numStart - 1)) numStart -= 1;\n const wholeIdx = digitIndices(toks, 0, numStart);\n const numIdx = digitIndices(toks, numStart, numLast + 1);\n const sepStart = wholeIdx.length > 0 ? wholeIdx[wholeIdx.length - 1] + 1 : numStart;\n let fixedDen: number | undefined;\n let denStart: number;\n const afterSlash = toks[slash + 1];\n if (afterSlash?.t === 'denom') {\n const parsed = Number(afterSlash.s);\n fixedDen = Math.min(Math.max(Number.isFinite(parsed) ? parsed : 1e9, 1), 1_000_000_000);\n denStart = slash + 1;\n } else {\n denStart = slash + 1;\n while (toks[denStart]?.t === 'lit') denStart += 1;\n }\n let denEnd = denStart;\n if (fixedDen !== undefined) denEnd += 1;\n else while (isDigit(denEnd)) denEnd += 1;\n const denIdx = digitIndices(toks, denStart, denEnd);\n const hasQ = (idx: number[]) => idx.some((i) => isDigitTok(toks[i], '?'));\n const hasWhole = wholeIdx.length > 0;\n let whole = hasWhole ? Math.floor(x) : 0;\n const f = hasWhole ? x - Math.floor(x) : x;\n let num: number;\n let den: number;\n if (fixedDen !== undefined) {\n num = Math.round(f * fixedDen);\n den = fixedDen;\n } else {\n const digits = Math.min(Math.max(denIdx.length, 1), 7);\n [num, den] = approximate(f, 10 ** digits - 1);\n }\n if (hasWhole && num >= den && den > 0) {\n whole += Math.floor(num / den);\n num %= den;\n }\n const wholeS = whole >= 1 ? fixed(whole, 0)[0] : '';\n const numZeroPh = numIdx.some((i) => isDigitTok(toks[i], '0'));\n if (hasWhole && num === 0 && !numZeroPh) {\n fillInt(toks, wholeIdx, wholeS, sec.thousands, true, pieces);\n const spaces = hasQ(wholeIdx) || hasQ(numIdx) || hasQ(denIdx);\n blankRange(toks, sepStart, denEnd, spaces, pieces, lits);\n return;\n }\n if (hasWhole) {\n // A zero value shows `0` in a `?` or `0` units place, nothing in a `#` one.\n const unitsHash = isDigitTok(toks[wholeIdx[wholeIdx.length - 1]], '#');\n const force = wholeS === '' && num === 0 && !unitsHash;\n fillInt(toks, wholeIdx, wholeS, sec.thousands, force, pieces);\n const wholeShown = wholeIdx.some((i) => /[0-9]/.test(pieces[i] ?? ''));\n if (!wholeShown) {\n const spaces = hasQ(wholeIdx) || hasQ(numIdx);\n blankRange(toks, sepStart, numStart, spaces, pieces, lits);\n }\n }\n fillInt(toks, numIdx, fixed(num, 0)[0] || '0', false, true, pieces);\n pieces[slash] = '/';\n const denS = fixed(den, 0)[0] || '0';\n if (fixedDen !== undefined) {\n pieces[denStart] = denS;\n } else {\n // Denominator digits are zero padded to the `0` placeholders, then left\n // aligned; `?` pads with spaces after them.\n const zeros = denIdx.filter((i) => isDigitTok(toks[i], '0')).length;\n const padded = '0'.repeat(Math.max(zeros - denS.length, 0)) + denS;\n const count = denIdx.length;\n denIdx.forEach((ti, k) => {\n let s = k + 1 === count ? padded.slice(k) : (padded[k] ?? '');\n if (s === '') s = isDigitTok(toks[ti], '?') ? ' ' : isDigitTok(toks[ti], '0') ? '0' : '';\n pieces[ti] = s;\n });\n }\n}\n\nfunction isDigitTok(t: Tok | undefined, c: string): boolean {\n return t?.t === 'digit' && t.c === c;\n}\n","// Portions ported from gridcraft/crates/numfmt/src/lib.rs (MIT OR Apache-2.0,\n// © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n/**\n * Excel number formats: format codes (`#,##0.00;[Red](#,##0.00)`, dates,\n * fractions, scientific…), the General format and the `TEXT()` worksheet\n * function. en-US only, 1900 date system.\n *\n * Our engine keeps dates as ISO text (`2026-03-04`, `2026-03-04 18:00:00`) and\n * times as day fractions; ISO text is converted to a 1900-system serial number\n * before formatting.\n */\nimport { parseNumberText, serialFromYmd, daysInMonth, timeFraction } from '../formula/parse-input';\nimport { FORMULA_ERRORS } from '../formula/values';\nimport { formatGeneralFit } from './decimal';\nimport { parseSections, testCond, type NumberFormatColor, type Section } from './parse';\nimport { Out, renderDate, renderGeneral, renderNumber, renderText } from './render';\n\nexport { formatGeneralFit } from './decimal';\nexport { builtinNumberFormat, builtinNumberFormatId } from './builtin';\nexport type { NumberFormatColor } from './parse';\n\n/** Width (in characters) of General output, as in a default-width cell. */\nconst GENERAL_WIDTH = 11;\n/** What Excel paints when a date is out of range. */\nconst OVERFLOW = '########';\n\n/**\n * Longest number format code, in characters, that is parsed (Excel's own\n * limit). A longer code displays its value as General and makes `TEXT`\n * return `#VALUE!`. The cap also bounds work that scales with the code.\n */\nexport const MAX_NUMBER_FORMAT_CODE_CHARS = 255;\n\n/** Best guess at the ribbon's number-format category. */\nexport type NumberFormatKind =\n | 'general'\n | 'number'\n | 'currency'\n | 'accounting'\n | 'date'\n | 'time'\n | 'percentage'\n | 'fraction'\n | 'scientific'\n | 'text'\n /** Zip codes, phone numbers, social security numbers. */\n | 'special'\n | 'custom';\n\n/** A value formatted with a number format, ready to paint. */\nexport interface FormattedNumberValue {\n text: string;\n /** Colour from a `[Red]` / `[ColorN]` tag of the section used. */\n color?: NumberFormatColor;\n /**\n * Repeat-fill character from `*x` and its position (UTF-16 index) in\n * `text`; a renderer may repeat it to fill the cell width.\n */\n fill?: { char: string; index: number };\n /** True for numbers (right-aligned by default); text, booleans and errors are false. */\n numeric: boolean;\n}\n\n/** True when `code` is longer than `MAX_NUMBER_FORMAT_CODE_CHARS` characters. */\nfunction tooLong(code: string): boolean {\n // A code of more than 255 UTF-16 units can still be short in characters; count only when needed.\n return code.length > MAX_NUMBER_FORMAT_CODE_CHARS && Array.from(code).length > MAX_NUMBER_FORMAT_CODE_CHARS;\n}\n\nconst SPECIAL_CODES = ['00000', '00000-0000', '[<=9999999]###-####;(###) ###-####', '000-00-0000'];\n\n/** A parsed number format code. Parsing never fails: malformed parts become literal text. */\nexport class NumberFormat {\n readonly code: string;\n private readonly general: boolean;\n private readonly sections: Section[];\n /** Sections that apply to numbers (a trailing text section is excluded). */\n private readonly numSections: number;\n /** Index of the section that applies to text, if any. */\n private readonly textSection: number | undefined;\n\n private constructor(code: string, uncapped: boolean) {\n this.code = code;\n const trimmed = code.trim();\n // `checkbox` marks a checkbox cell: any value displays as General.\n if (trimmed === '' || trimmed.toLowerCase() === 'general' || trimmed === 'checkbox' || (!uncapped && tooLong(code))) {\n this.general = true;\n this.sections = [];\n this.numSections = 0;\n this.textSection = undefined;\n return;\n }\n this.general = false;\n this.sections = parseSections(code);\n const len = this.sections.length;\n const lastIsText = this.sections[len - 1]?.kind === 'text';\n this.textSection = len >= 4 ? 3 : lastIsText ? len - 1 : undefined;\n this.numSections = len >= 4 ? 3 : lastIsText ? len - 1 : len;\n }\n\n /**\n * Parses a format code. Never throws: unknown or malformed parts degrade to\n * literal text, and a code longer than 255 characters degrades to General.\n */\n static parse(code: string): NumberFormat {\n return new NumberFormat(code, false);\n }\n\n /** `parse` without the 255-character cap (for tests that reach renderer edge cases). */\n static parseUncapped(code: string): NumberFormat {\n return new NumberFormat(code, true);\n }\n\n /** Like `parse`, but remembers recently parsed codes (bounded), so formatting many values parses a code once. */\n static parseCached(code: string): NumberFormat {\n if (code.length > CACHE_MAX_CODE_LENGTH) return NumberFormat.parse(code);\n let fmt = parseCache.get(code);\n if (!fmt) {\n fmt = NumberFormat.parse(code);\n if (parseCache.size >= CACHE_CAPACITY) parseCache.clear();\n parseCache.set(code, fmt);\n }\n return fmt;\n }\n\n static general(): NumberFormat {\n return NumberFormat.parse('General');\n }\n\n isGeneral(): boolean {\n return this.general;\n }\n\n private numberSections(): Section[] {\n return this.sections.slice(0, this.numSections);\n }\n\n /** True when the code contains date or time tokens. */\n isDate(): boolean {\n return this.numberSections().some((s) => s.kind === 'date');\n }\n\n isPercent(): boolean {\n return this.numberSections().some((s) => s.kind === 'number' && s.percent > 0);\n }\n\n /** True for a pure text format such as `@`. */\n isText(): boolean {\n return this.numSections === 0 && this.textSection !== undefined;\n }\n\n /** Best guess at the format category. */\n kind(): NumberFormatKind {\n if (this.general) return 'general';\n if (this.isText()) return 'text';\n if (SPECIAL_CODES.includes(this.code)) return 'special';\n const s = this.sections[0];\n if (!s) return 'custom';\n if (s.kind === 'general') return 'general';\n if (s.kind === 'text') return 'text';\n if (s.kind === 'date') {\n const calendar = s.toks.some((t) => t.t === 'date' && (t.d.k === 'year' || t.d.k === 'month' || t.d.k === 'day'));\n return calendar ? 'date' : 'time';\n }\n if (s.fraction) return 'fraction';\n if (s.exp) return 'scientific';\n if (s.percent > 0) return 'percentage';\n if (s.fill && s.hasDigits) return 'accounting';\n if (s.currency && s.hasDigits) return 'currency';\n if (s.hasDigits) return 'number';\n return 'custom';\n }\n\n /**\n * Picks the number section for `v`, and whether the minus sign is dropped\n * (a negative section shows the absolute value). Null means General.\n */\n private select(v: number): { sec: Section; dropSign: boolean } | null {\n const secs = this.numberSections();\n const k = secs.length;\n const s0 = secs[0];\n if (!s0) return null;\n const c0 = s0.cond;\n const c1 = secs[1]?.cond;\n if (c0 || c1) {\n let idx: number;\n if (c0 && testCond(c0, v)) idx = 0;\n else if (c1 && testCond(c1, v)) idx = 1;\n else if (c0 && c1) idx = 2;\n else if (!c0) idx = 0;\n else idx = 1;\n // Excel never adds a minus sign in a multi-section conditional format.\n const sec = secs[idx];\n return sec ? { sec, dropSign: k >= 2 } : null;\n }\n if (v < 0 && k >= 2) return { sec: secs[1], dropSign: true };\n if (v === 0 && k >= 3) return { sec: secs[2], dropSign: false };\n return { sec: s0, dropSign: false };\n }\n\n /**\n * Formats a number. `overflow` is true when the value can't be shown (a\n * date out of range, painted as `########`).\n */\n formatNumber(n: number): FormattedNumberValue & { overflow: boolean } {\n const general = (): FormattedNumberValue & { overflow: boolean } => ({\n text: formatGeneralFit(n, GENERAL_WIDTH) ?? OVERFLOW,\n numeric: true,\n overflow: false,\n });\n if (!Number.isFinite(n)) return { text: '#NUM!', numeric: false, overflow: true };\n if (this.general) return general();\n const picked = this.select(n);\n if (!picked) return general();\n const { sec, dropSign } = picked;\n const minus = n < 0 && !dropSign;\n let out: Out | null;\n switch (sec.kind) {\n case 'text':\n out = renderText(sec, formatGeneralFit(n, GENERAL_WIDTH) ?? '');\n break;\n case 'general':\n out = renderGeneral(sec, Math.abs(n), minus ? GENERAL_WIDTH - 1 : GENERAL_WIDTH);\n break;\n case 'date':\n out = minus ? null : renderDate(sec, Math.abs(n));\n break;\n default:\n out = renderNumber(sec, Math.abs(n));\n }\n if (!out) return withColor({ text: OVERFLOW, numeric: true, overflow: true }, sec.color);\n // Excel drops the minus sign when the section shows a negative number as all zeros.\n if (minus && sec.kind !== 'text' && !out.zero) out.prepend('-');\n return withColor({ text: out.text, numeric: true, overflow: false, ...(out.fill ? { fill: out.fill } : {}) }, sec.color);\n }\n\n /** Formats text through the text section (`@`), or returns it unchanged. */\n formatText(text: string): FormattedNumberValue {\n const sec = this.textSection === undefined ? undefined : this.sections[this.textSection];\n if (!sec) return { text, numeric: false };\n const out = renderText(sec, text);\n return withColor({ text: out.text, numeric: false, ...(out.fill ? { fill: out.fill } : {}) }, sec.color);\n }\n\n /**\n * Formats a cell value: numbers through the number sections, ISO date text\n * (`2026-03-04`, `2026-03-04 18:00:00`) and `Date`s as serial numbers, other\n * text through the text section; booleans show TRUE/FALSE and error values\n * themselves.\n */\n format(value: unknown): FormattedNumberValue {\n if (value === '' || value === null || value === undefined) return { text: '', numeric: false };\n if (typeof value === 'boolean') return { text: value ? 'TRUE' : 'FALSE', numeric: false };\n if (typeof value === 'number') {\n const { overflow: _overflow, ...rest } = this.formatNumber(value);\n return rest;\n }\n if (typeof value === 'string' && FORMULA_ERRORS.has(value)) return { text: value, numeric: false };\n const serial = dateValueToSerial(value);\n if (serial !== null) {\n const { overflow: _overflow, ...rest } = this.formatNumber(serial);\n return rest;\n }\n return this.formatText(String(value));\n }\n}\n\nfunction withColor<T extends FormattedNumberValue>(value: T, color: NumberFormatColor | undefined): T {\n if (color) value.color = color;\n return value;\n}\n\n// --- Parse cache ---\n\n/** Most codes kept. A full cache is emptied, so memory stays bounded. */\nconst CACHE_CAPACITY = 64;\n/** Longer codes are parsed every time and never stored. */\nconst CACHE_MAX_CODE_LENGTH = 256;\nconst parseCache = new Map<string, NumberFormat>();\n\n/** Empties the parse cache used by `NumberFormat.parseCached` (mainly for tests). */\nexport function clearNumberFormatCache(): void {\n parseCache.clear();\n}\n\n// --- Dates ---\n\nconst ISO_DATE_TIME = /^(\\d{4})-(\\d{1,2})-(\\d{1,2})(?:[ T](\\d{1,2}):(\\d{2})(?::(\\d{2}(?:\\.\\d+)?))?)?$/;\n\n/**\n * 1900-system serial number of a date value: ISO date text (with an optional\n * time) or a `Date` (its local date and time). Null for anything else.\n */\nexport function dateValueToSerial(value: unknown): number | null {\n if (value instanceof Date) {\n if (isNaN(value.getTime())) return null;\n const serial = serialFromYmd(value.getFullYear(), value.getMonth() + 1, value.getDate());\n return serial === null ? null : serial + timeFraction(value.getHours(), value.getMinutes(), value.getSeconds() + value.getMilliseconds() / 1000);\n }\n if (typeof value !== 'string' || value.length < 8 || value.length > 30) return null;\n const m = ISO_DATE_TIME.exec(value.trim());\n if (!m) return null;\n const [y, mo, d] = [Number(m[1]), Number(m[2]), Number(m[3])];\n if (mo < 1 || mo > 12 || d < 1 || d > daysInMonth(y, mo)) return null;\n const serial = serialFromYmd(y, mo, d);\n if (serial === null) return null;\n if (m[4] === undefined) return serial;\n const [hh, mm, ss] = [Number(m[4]), Number(m[5]), Number(m[6] ?? 0)];\n if (hh > 23 || mm > 59 || ss >= 60) return null;\n return serial + timeFraction(hh, mm, ss);\n}\n\n// --- Public helpers ---\n\n/** Formats a cell value with a format code (see `NumberFormat.format`). */\nexport function formatNumberFormatValue(value: unknown, code: string): FormattedNumberValue {\n return NumberFormat.parseCached(code).format(value);\n}\n\n/**\n * The `TEXT()` worksheet function: formats a value with a format code.\n * Blanks count as 0; numeric and date text (`\"5\"`, `\"3/4/2026\"`,\n * `\"2026-03-04\"`) is converted to a number first; other text and booleans\n * (as TRUE/FALSE) go through the text section, or are returned as is.\n * Errors: the value's own error, `#VALUE!` for a code over 255 characters or\n * a date out of range, `#NUM!` for a non-finite number.\n */\nexport function formatTextFunction(value: unknown, code: string): { text: string } | { error: string } {\n if (typeof value === 'string' && FORMULA_ERRORS.has(value)) return { error: value };\n if (code === '') return { text: '' };\n if (tooLong(code)) return { error: '#VALUE!' };\n const fmt = NumberFormat.parseCached(code);\n let n: number;\n if (value === '' || value === null || value === undefined) n = 0;\n else if (typeof value === 'number') n = value;\n else if (typeof value === 'boolean') return { text: fmt.formatText(value ? 'TRUE' : 'FALSE').text };\n else {\n const serial = dateValueToSerial(value);\n const parsed = serial ?? (typeof value === 'string' ? parseNumberText(value) : null);\n if (parsed === null) return { text: fmt.formatText(String(value)).text };\n n = parsed;\n }\n if (!Number.isFinite(n)) return { error: '#NUM!' };\n const f = fmt.formatNumber(n);\n return f.overflow ? { error: '#VALUE!' } : { text: f.text };\n}\n\n/** Excel's default 56-colour palette, for `[Color1]`…`[Color56]`. */\nconst INDEXED_COLORS = [\n '#000000', '#FFFFFF', '#FF0000', '#00FF00', '#0000FF', '#FFFF00', '#FF00FF', '#00FFFF',\n '#800000', '#008000', '#000080', '#808000', '#800080', '#008080', '#C0C0C0', '#808080',\n '#9999FF', '#993366', '#FFFFCC', '#CCFFFF', '#660066', '#FF8080', '#0066CC', '#CCCCFF',\n '#000080', '#FF00FF', '#FFFF00', '#00FFFF', '#800080', '#800000', '#008080', '#0000FF',\n '#00CCFF', '#CCFFFF', '#CCFFCC', '#FFFF99', '#99CCFF', '#FF99CC', '#CC99FF', '#FFCC99',\n '#3366FF', '#33CCCC', '#99CC00', '#FFCC00', '#FF9900', '#FF6600', '#666699', '#969696',\n '#003366', '#339966', '#003300', '#333300', '#993300', '#993366', '#333399', '#333333',\n];\n\nconst NAMED_CSS: Record<string, string> = {\n black: '#000000',\n blue: '#0000FF',\n cyan: '#00FFFF',\n green: '#00FF00',\n magenta: '#FF00FF',\n red: '#FF0000',\n white: '#FFFFFF',\n yellow: '#FFFF00',\n};\n\n/** CSS colour for a format colour (`red` → `#FF0000`, `color10` → `#008000`). */\nexport function numberFormatColorToCss(color: NumberFormatColor): string {\n const named = NAMED_CSS[color];\n if (named) return named;\n return INDEXED_COLORS[Number(color.slice(5)) - 1] ?? '#000000';\n}\n","import type {\n FormulaArgumentSpec,\n FormulaArgumentType,\n FormulaFunctionContext,\n FormulaFunctionDefinition,\n} from '../registry';\nimport { isFormulaError } from '../values';\n\n/** Shorthand for an argument spec. */\nexport const arg = (\n name: string,\n description: string,\n type: FormulaArgumentType = 'any',\n extra: Partial<FormulaArgumentSpec> = {},\n): FormulaArgumentSpec => ({ name, description, type, ...extra });\n\nexport const optional = (\n name: string,\n description: string,\n type: FormulaArgumentType = 'any',\n): FormulaArgumentSpec => arg(name, description, type, { optional: true });\n\nexport const repeating = (\n name: string,\n description: string,\n type: FormulaArgumentType = 'any',\n): FormulaArgumentSpec => arg(name, description, type, { repeating: true });\n\nexport type FunctionTable = FormulaFunctionDefinition[];\n\n/**\n * True for error strings. Narrows `number | string` (etc.) results of the\n * context helpers; plain `string` results (ctx.text) are not narrowed, since\n * text and error codes are both strings.\n */\nexport function failed(value: string): boolean;\nexport function failed<T>(value: T): value is Extract<T, string>;\nexport function failed(value: unknown): boolean {\n return isFormulaError(value);\n}\n\n/**\n * Applies `fn` to a numeric argument (`number`), propagating errors and\n * turning non-finite results into #NUM!.\n */\nexport function unaryMath(\n ctx: FormulaFunctionContext,\n fn: (value: number) => number | string,\n): number | string {\n const value = ctx.number(0);\n if (failed(value)) return value;\n const result = fn(value);\n if (typeof result === 'number' && !Number.isFinite(result)) {\n return ctx.error('#NUM!', `${ctx.name} produced a number that is out of range.`);\n }\n return result;\n}\n","import { cleanFloat, roundHalfAwayFromZero } from '../values';\nimport { arg, failed, optional, repeating, unaryMath, type FunctionTable } from './helpers';\n\nconst NUMBER = arg('number', 'The number to use.', 'number');\nconst NUM_DIGITS = optional('num_digits', 'Decimal places to round to; negative values round to the left of the decimal point (default 0).', 'number');\n\n/** Rounds `value` to `digits` places with `mode` (away from or toward zero). */\nfunction roundWith(value: number, digits: number, mode: 'up' | 'down'): number {\n const factor = Math.pow(10, Math.trunc(digits));\n // toPrecision strips float noise so ROUNDUP(0.1*3, 1) stays 0.3.\n const scaled = Number((Math.abs(value) * factor).toPrecision(15));\n const rounded = mode === 'up' ? Math.ceil(scaled) : Math.floor(scaled);\n return cleanFloat((Math.sign(value) * rounded) / factor);\n}\n\nexport const MATH_FUNCTIONS: FunctionTable = [\n {\n name: 'SUM',\n category: 'math',\n description: 'Adds numbers and the numbers in ranges. Text and blank cells are ignored.',\n args: [repeating('value', 'A number, cell or range to add.', 'number')],\n evaluate: (ctx) => {\n const nums = ctx.numbers();\n if (failed(nums)) return nums;\n let total = 0;\n for (const num of nums) total += num;\n return total;\n },\n },\n {\n name: 'PRODUCT',\n category: 'math',\n description: 'Multiplies numbers and the numbers in ranges. Text and blank cells are ignored.',\n args: [repeating('value', 'A number, cell or range to multiply.', 'number')],\n evaluate: (ctx) => {\n const nums = ctx.numbers();\n if (failed(nums)) return nums;\n if (nums.length === 0) return 0;\n let product = 1;\n for (const num of nums) product *= num;\n return cleanFloat(product);\n },\n },\n {\n name: 'ROUND',\n category: 'math',\n description: 'Rounds a number to a number of decimal places, halves away from zero.',\n args: [NUMBER, NUM_DIGITS],\n evaluate: (ctx) => {\n const num = ctx.number(0);\n if (failed(num)) return num;\n const decimals = ctx.number(1, 0);\n if (failed(decimals)) return decimals;\n return roundHalfAwayFromZero(num, Math.trunc(decimals));\n },\n },\n {\n name: 'ROUNDUP',\n category: 'math',\n description: 'Rounds a number away from zero to a number of decimal places.',\n args: [NUMBER, NUM_DIGITS],\n evaluate: (ctx) => {\n const num = ctx.number(0);\n if (failed(num)) return num;\n const digits = ctx.number(1, 0);\n if (failed(digits)) return digits;\n return roundWith(num, digits, 'up');\n },\n },\n {\n name: 'ROUNDDOWN',\n category: 'math',\n description: 'Rounds a number toward zero to a number of decimal places.',\n args: [NUMBER, NUM_DIGITS],\n evaluate: (ctx) => {\n const num = ctx.number(0);\n if (failed(num)) return num;\n const digits = ctx.number(1, 0);\n if (failed(digits)) return digits;\n return roundWith(num, digits, 'down');\n },\n },\n {\n name: 'INT',\n category: 'math',\n description: 'Rounds a number down to the nearest integer (INT(-1.5) is -2).',\n args: [NUMBER],\n evaluate: (ctx) => unaryMath(ctx, Math.floor),\n },\n {\n name: 'ABS',\n category: 'math',\n description: 'Returns the absolute value of a number.',\n args: [NUMBER],\n evaluate: (ctx) => unaryMath(ctx, Math.abs),\n },\n {\n name: 'SIGN',\n category: 'math',\n description: 'Returns 1 for positive numbers, -1 for negative numbers and 0 for zero.',\n args: [NUMBER],\n evaluate: (ctx) => unaryMath(ctx, (n) => (n > 0 ? 1 : n < 0 ? -1 : 0)),\n },\n {\n name: 'SQRT',\n category: 'math',\n description: 'Returns the square root of a number.',\n args: [NUMBER],\n evaluate: (ctx) =>\n unaryMath(ctx, (n) =>\n n < 0 ? ctx.argError(0, '#NUM!', \"can't be negative.\") : Math.sqrt(n),\n ),\n },\n {\n name: 'POWER',\n category: 'math',\n description: 'Raises a number to a power.',\n args: [arg('number', 'The base.', 'number'), arg('power', 'The exponent.', 'number')],\n evaluate: (ctx) => {\n const base = ctx.number(0);\n if (failed(base)) return base;\n const exponent = ctx.number(1);\n if (failed(exponent)) return exponent;\n if (base === 0 && exponent < 0) return ctx.error('#DIV/0!', 'POWER of zero to a negative exponent divides by zero.');\n const result = Math.pow(base, exponent);\n return Number.isFinite(result) ? result : ctx.error('#NUM!', 'POWER produced a number that is out of range.');\n },\n },\n {\n name: 'MOD',\n category: 'math',\n description: 'Returns the remainder after division. The result has the sign of the divisor.',\n args: [arg('number', 'The number to divide.', 'number'), arg('divisor', 'The number to divide by.', 'number')],\n evaluate: (ctx) => {\n const num = ctx.number(0);\n if (failed(num)) return num;\n const divisor = ctx.number(1);\n if (failed(divisor)) return divisor;\n if (divisor === 0) return ctx.argError(1, '#DIV/0!', \"can't be zero.\");\n return cleanFloat(num - divisor * Math.floor(num / divisor));\n },\n },\n {\n name: 'CEILING',\n category: 'math',\n description: 'Rounds a number up to the nearest multiple of significance.',\n args: [NUMBER, optional('significance', 'The multiple to round to (default 1).', 'number')],\n evaluate: (ctx) => roundToMultiple(ctx.name, ctx.number(0), ctx.number(1, 1), Math.ceil, ctx),\n },\n {\n name: 'FLOOR',\n category: 'math',\n description: 'Rounds a number down to the nearest multiple of significance.',\n args: [NUMBER, optional('significance', 'The multiple to round to (default 1).', 'number')],\n evaluate: (ctx) => roundToMultiple(ctx.name, ctx.number(0), ctx.number(1, 1), Math.floor, ctx),\n },\n {\n name: 'PI',\n category: 'math',\n description: 'Returns the number π (3.14159…).',\n args: [],\n evaluate: () => Math.PI,\n },\n {\n name: 'RAND',\n category: 'math',\n description: 'Returns a random number between 0 (inclusive) and 1 (exclusive). Changes on every recalculation.',\n args: [],\n volatile: true,\n evaluate: () => Math.random(),\n },\n {\n name: 'RANDBETWEEN',\n category: 'math',\n description: 'Returns a random integer between bottom and top, inclusive. Changes on every recalculation.',\n args: [arg('bottom', 'The smallest integer to return.', 'number'), arg('top', 'The largest integer to return.', 'number')],\n volatile: true,\n evaluate: (ctx) => {\n const bottom = ctx.number(0);\n if (failed(bottom)) return bottom;\n const top = ctx.number(1);\n if (failed(top)) return top;\n const lo = Math.ceil(bottom);\n const hi = Math.floor(top);\n if (lo > hi) return ctx.error('#NUM!', 'RANDBETWEEN needs bottom to be less than or equal to top.');\n return lo + Math.floor(Math.random() * (hi - lo + 1));\n },\n },\n];\n\nfunction roundToMultiple(\n name: string,\n number: number | string,\n significance: number | string,\n round: (n: number) => number,\n ctx: { error: (code: '#NUM!' | '#DIV/0!', message?: string) => string },\n): number | string {\n if (failed(number)) return number;\n if (failed(significance)) return significance;\n if (number === 0) return 0;\n if (significance === 0) {\n return name === 'FLOOR' ? ctx.error('#DIV/0!', 'FLOOR needs a significance other than zero.') : 0;\n }\n if (number > 0 && significance < 0) {\n return ctx.error('#NUM!', `${name} can't round a positive number to a negative significance.`);\n }\n return cleanFloat(round(cleanFloat(number / significance)) * significance);\n}\n","import { cleanFloat, isBlank, isFormulaError } from '../values';\nimport type { FormulaFunctionContext } from '../registry';\nimport { arg, failed, optional, repeating, type FunctionTable } from './helpers';\n\nconst VALUES = repeating('value', 'A number, cell or range.', 'number');\n\n/** Numbers of argument 0 plus a positive integer k from argument 1, for LARGE/SMALL. */\nfunction kth(ctx: FormulaFunctionContext, pick: (sorted: number[], k: number) => number): number | string {\n const nums = ctx.numbers(0, 1);\n if (failed(nums)) return nums;\n const k = ctx.number(1);\n if (failed(k)) return k;\n const index = Math.ceil(k);\n if (index < 1 || index > nums.length) {\n return ctx.argError(1, '#NUM!', `must be between 1 and the number of values (${nums.length}).`);\n }\n return pick([...nums].sort((a, b) => a - b), index);\n}\n\nfunction variance(ctx: FormulaFunctionContext): number | string {\n const nums = ctx.numbers();\n if (failed(nums)) return nums;\n if (nums.length < 2) return ctx.error('#DIV/0!', `${ctx.name} needs at least two numbers.`);\n let mean = 0;\n for (const num of nums) mean += num;\n mean /= nums.length;\n let squares = 0;\n for (const num of nums) squares += (num - mean) ** 2;\n return squares / (nums.length - 1);\n}\n\nexport const STATISTICAL_FUNCTIONS: FunctionTable = [\n {\n name: 'AVERAGE',\n category: 'statistical',\n description: 'Returns the arithmetic mean of numbers. Blank cells and text are ignored.',\n args: [VALUES],\n evaluate: (ctx) => {\n const items = ctx.flatValues();\n const error = items.find(isFormulaError);\n if (error) return error;\n let total = 0;\n let count = 0;\n for (const item of items) {\n const num = Number(item);\n if (!isNaN(num) && !isBlank(item)) {\n total += num;\n count++;\n }\n }\n return count === 0 ? 0 : total / count;\n },\n },\n {\n name: 'COUNT',\n category: 'statistical',\n description: 'Counts the values that are numbers.',\n args: [repeating('value', 'A value, cell or range to count.')],\n evaluate: (ctx) => {\n let count = 0;\n for (const item of ctx.flatValues()) {\n if (!isNaN(Number(item)) && !isBlank(item)) count++;\n }\n return count;\n },\n },\n {\n name: 'COUNTA',\n category: 'statistical',\n description: 'Counts the values that are not blank.',\n args: [repeating('value', 'A value, cell or range to count.')],\n evaluate: (ctx) => ctx.flatValues().filter((item) => !isBlank(item)).length,\n },\n {\n name: 'COUNTBLANK',\n category: 'statistical',\n description: 'Counts the blank cells in a range.',\n args: [arg('range', 'The range to check.', 'range')],\n evaluate: (ctx) => {\n const range = ctx.range(0);\n if (failed(range)) return range;\n return range.values.filter(isBlank).length;\n },\n },\n {\n name: 'MIN',\n category: 'statistical',\n description: 'Returns the smallest number. Text and blank cells are ignored.',\n args: [VALUES],\n evaluate: (ctx) => {\n const nums = ctx.numbers();\n if (failed(nums)) return nums;\n if (nums.length === 0) return 0;\n // Loop instead of Math.min(...nums), which overflows the stack on large ranges.\n let result = nums[0];\n for (const num of nums) result = Math.min(result, num);\n return result;\n },\n },\n {\n name: 'MAX',\n category: 'statistical',\n description: 'Returns the largest number. Text and blank cells are ignored.',\n args: [VALUES],\n evaluate: (ctx) => {\n const nums = ctx.numbers();\n if (failed(nums)) return nums;\n if (nums.length === 0) return 0;\n let result = nums[0];\n for (const num of nums) result = Math.max(result, num);\n return result;\n },\n },\n {\n name: 'MEDIAN',\n category: 'statistical',\n description: 'Returns the middle number of a set of numbers.',\n args: [VALUES],\n evaluate: (ctx) => {\n const nums = ctx.numbers();\n if (failed(nums)) return nums;\n if (nums.length === 0) return ctx.error('#NUM!', 'MEDIAN needs at least one number.');\n const sorted = [...nums].sort((a, b) => a - b);\n const mid = Math.floor(sorted.length / 2);\n return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;\n },\n },\n {\n name: 'MODE',\n category: 'statistical',\n description: 'Returns the most frequently occurring number. Ties go to the number that appears first.',\n args: [VALUES],\n evaluate: (ctx) => {\n const nums = ctx.numbers();\n if (failed(nums)) return nums;\n const counts = new Map<number, number>();\n let best: number | null = null;\n let bestCount = 1;\n for (const num of nums) {\n const count = (counts.get(num) ?? 0) + 1;\n counts.set(num, count);\n if (count > bestCount) {\n best = num;\n bestCount = count;\n }\n }\n // A later number that ties the best count must not replace an earlier one.\n if (best !== null) {\n for (const num of nums) {\n if (counts.get(num) === bestCount) return num;\n }\n }\n return ctx.error('#N/A', 'MODE found no number that appears more than once.');\n },\n },\n {\n name: 'STDEV',\n category: 'statistical',\n description: 'Estimates the standard deviation of a sample.',\n args: [VALUES],\n evaluate: (ctx) => {\n const v = variance(ctx);\n return failed(v) ? v : cleanFloat(Math.sqrt(v));\n },\n },\n {\n name: 'VAR',\n category: 'statistical',\n description: 'Estimates the variance of a sample.',\n args: [VALUES],\n evaluate: (ctx) => {\n const v = variance(ctx);\n return failed(v) ? v : cleanFloat(v);\n },\n },\n {\n name: 'LARGE',\n category: 'statistical',\n description: 'Returns the k-th largest number (LARGE(range, 1) is the maximum).',\n args: [arg('array', 'The numbers to look through.', 'range'), arg('k', 'Position from the largest (1 = largest).', 'number')],\n evaluate: (ctx) => kth(ctx, (sorted, k) => sorted[sorted.length - k]),\n },\n {\n name: 'SMALL',\n category: 'statistical',\n description: 'Returns the k-th smallest number (SMALL(range, 1) is the minimum).',\n args: [arg('array', 'The numbers to look through.', 'range'), arg('k', 'Position from the smallest (1 = smallest).', 'number')],\n evaluate: (ctx) => kth(ctx, (sorted, k) => sorted[k - 1]),\n },\n {\n name: 'RANK',\n category: 'statistical',\n description: 'Returns the rank of a number in a list. Equal numbers share a rank.',\n args: [\n arg('number', 'The number to rank.', 'number'),\n arg('ref', 'The list of numbers to rank against.', 'range'),\n optional('order', '0 or omitted ranks the largest as 1; any other value ranks the smallest as 1.', 'number'),\n ],\n evaluate: (ctx) => {\n const num = ctx.number(0);\n if (failed(num)) return num;\n const nums = ctx.numbers(1, 2);\n if (failed(nums)) return nums;\n const order = ctx.number(2, 0);\n if (failed(order)) return order;\n if (!nums.includes(num)) return ctx.argError(0, '#N/A', `(${num}) must be one of the numbers in argument 2.`);\n let rank = 1;\n for (const other of nums) {\n if (order === 0 ? other > num : other < num) rank++;\n }\n return rank;\n },\n },\n];\n","import type { FormulaFunctionContext } from '../registry';\nimport { parseCriteria } from '../criteria';\nimport { isFormulaError, numericValue, rangeGet, type FormulaRange } from '../values';\nimport { arg, failed, optional, repeating, type FunctionTable } from './helpers';\n\nconst CRITERIA_DESCRIPTION =\n 'The condition, e.g. 5, \">5\", \"<>done\", \"a*\" (wildcards * and ?), or \"\" for blank cells.';\n\n/**\n * Evaluates (criteria_range, criteria) argument pairs starting at `firstPair`\n * against cells of `base`'s shape. Returns one flag per cell (column-major),\n * or an error string.\n */\nfunction matchMask(\n ctx: FormulaFunctionContext,\n base: FormulaRange,\n baseIndex: number,\n firstPair: number,\n): boolean[] | string {\n const mask = new Array<boolean>(base.rows * base.cols).fill(true);\n for (let i = firstPair; i + 1 < ctx.args.length; i += 2) {\n const range = ctx.range(i);\n if (failed(range)) return range;\n if (i !== baseIndex && (range.rows !== base.rows || range.cols !== base.cols)) {\n return ctx.argError(i, '#VALUE!', `must be the same size as argument ${baseIndex + 1} (${base.rows}×${base.cols}).`);\n }\n const criterion = ctx.scalar(i + 1);\n if (isFormulaError(criterion)) return criterion;\n const matches = parseCriteria(criterion);\n for (let k = 0; k < mask.length; k++) {\n if (mask[k] && !matches(range.values[k])) mask[k] = false;\n }\n }\n return mask;\n}\n\n/** Numbers of `target` (resized to `base`'s shape) at matching positions. */\nfunction matchedNumbers(target: FormulaRange, base: FormulaRange, mask: boolean[]): number[] | string {\n const nums: number[] = [];\n for (let c = 0; c < base.cols; c++) {\n for (let r = 0; r < base.rows; r++) {\n if (!mask[c * base.rows + r]) continue;\n const value = rangeGet(target, r, c);\n if (isFormulaError(value)) return value;\n const num = numericValue(value);\n if (num !== null) nums.push(num);\n }\n }\n return nums;\n}\n\ntype Aggregate = (nums: number[], ctx: FormulaFunctionContext) => number | string;\n\nconst sum: Aggregate = (nums) => {\n let total = 0;\n for (const num of nums) total += num;\n return total;\n};\nconst average: Aggregate = (nums, ctx) =>\n nums.length === 0\n ? ctx.error('#DIV/0!', `${ctx.name} found no matching numbers to average.`)\n : (sum(nums, ctx) as number) / nums.length;\nconst max: Aggregate = (nums) => nums.reduce((a, b) => Math.max(a, b), nums.length ? -Infinity : 0);\nconst min: Aggregate = (nums) => nums.reduce((a, b) => Math.min(a, b), nums.length ? Infinity : 0);\n\n/** SUMIF / AVERAGEIF: (range, criteria, [target_range]). */\nfunction singleCriterion(aggregate: Aggregate) {\n return (ctx: FormulaFunctionContext) => {\n const base = ctx.range(0);\n if (failed(base)) return base;\n const mask = matchMask(ctx, base, 0, 0);\n if (failed(mask)) return mask;\n let target = base;\n if (ctx.has(2)) {\n const range = ctx.range(2);\n if (failed(range)) return range;\n target = range;\n }\n const nums = matchedNumbers(target, base, mask);\n return failed(nums) ? nums : aggregate(nums, ctx);\n };\n}\n\n/** SUMIFS / AVERAGEIFS / MAXIFS / MINIFS: (target_range, criteria_range1, criteria1, ...). */\nfunction multipleCriteria(aggregate: Aggregate) {\n return (ctx: FormulaFunctionContext) => {\n const target = ctx.range(0);\n if (failed(target)) return target;\n const mask = matchMask(ctx, target, 0, 1);\n if (failed(mask)) return mask;\n const nums = matchedNumbers(target, target, mask);\n return failed(nums) ? nums : aggregate(nums, ctx);\n };\n}\n\nconst criteriaPair = [\n repeating('criteria_range', 'A range to test. Must be the same size as the other ranges.', 'range'),\n repeating('criteria', CRITERIA_DESCRIPTION, 'criteria'),\n];\n\nexport const CONDITIONAL_FUNCTIONS: FunctionTable = [\n {\n name: 'SUMIF',\n category: 'math',\n description: 'Adds the cells that meet a condition.',\n args: [\n arg('range', 'The range to test.', 'range'),\n arg('criteria', CRITERIA_DESCRIPTION, 'criteria'),\n optional('sum_range', 'The cells to add (default: range itself).', 'range'),\n ],\n evaluate: singleCriterion(sum),\n },\n {\n name: 'SUMIFS',\n category: 'math',\n description: 'Adds the cells that meet every condition.',\n args: [arg('sum_range', 'The cells to add.', 'range'), ...criteriaPair],\n evaluate: multipleCriteria(sum),\n },\n {\n name: 'COUNTIF',\n category: 'statistical',\n description: 'Counts the cells that meet a condition.',\n args: [arg('range', 'The range to test.', 'range'), arg('criteria', CRITERIA_DESCRIPTION, 'criteria')],\n evaluate: (ctx) => {\n const base = ctx.range(0);\n if (failed(base)) return base;\n const mask = matchMask(ctx, base, 0, 0);\n return failed(mask) ? mask : mask.filter(Boolean).length;\n },\n },\n {\n name: 'COUNTIFS',\n category: 'statistical',\n description: 'Counts the cells that meet every condition.',\n args: criteriaPair,\n evaluate: (ctx) => {\n const base = ctx.range(0);\n if (failed(base)) return base;\n const mask = matchMask(ctx, base, 0, 0);\n return failed(mask) ? mask : mask.filter(Boolean).length;\n },\n },\n {\n name: 'AVERAGEIF',\n category: 'statistical',\n description: 'Averages the cells that meet a condition.',\n args: [\n arg('range', 'The range to test.', 'range'),\n arg('criteria', CRITERIA_DESCRIPTION, 'criteria'),\n optional('average_range', 'The cells to average (default: range itself).', 'range'),\n ],\n evaluate: singleCriterion(average),\n },\n {\n name: 'AVERAGEIFS',\n category: 'statistical',\n description: 'Averages the cells that meet every condition.',\n args: [arg('average_range', 'The cells to average.', 'range'), ...criteriaPair],\n evaluate: multipleCriteria(average),\n },\n {\n name: 'MAXIFS',\n category: 'statistical',\n description: 'Returns the largest number among the cells that meet every condition (0 if none match).',\n args: [arg('max_range', 'The cells to take the maximum of.', 'range'), ...criteriaPair],\n evaluate: multipleCriteria(max),\n },\n {\n name: 'MINIFS',\n category: 'statistical',\n description: 'Returns the smallest number among the cells that meet every condition (0 if none match).',\n args: [arg('min_range', 'The cells to take the minimum of.', 'range'), ...criteriaPair],\n evaluate: multipleCriteria(min),\n },\n];\n","import type { FormulaFunctionContext } from '../registry';\nimport { compareValues, isBlank, isFormulaError, normalizeValue, numericValue, toBoolean } from '../values';\nimport { arg, failed, optional, repeating, type FunctionTable } from './helpers';\n\n/**\n * Logical values among the arguments, for AND/OR/XOR. In ranges, blanks and\n * non-logical text are skipped; a direct text argument is #VALUE!.\n */\nfunction logicalValues(ctx: FormulaFunctionContext): boolean[] | string {\n const values: boolean[] = [];\n for (let i = 0; i < ctx.args.length; i++) {\n const value = ctx.value(i);\n if (Array.isArray(value)) {\n for (const item of value) {\n if (isFormulaError(item)) return item;\n if (isBlank(item)) continue;\n if (typeof item === 'boolean') values.push(item);\n else if (typeof item === 'number') values.push(item !== 0);\n else {\n const num = numericValue(item);\n const bool = num !== null ? num !== 0 : toBoolean(item);\n if (typeof bool === 'boolean') values.push(bool);\n }\n }\n continue;\n }\n const bool = ctx.boolean(i);\n if (failed(bool)) return bool;\n values.push(bool);\n }\n if (values.length === 0) return ctx.error('#VALUE!', `${ctx.name} found no logical values to test.`);\n return values;\n}\n\nconst LOGICALS = repeating('logical', 'A condition, logical value or range of logical values.', 'logical');\n\nexport const LOGICAL_FUNCTIONS: FunctionTable = [\n {\n name: 'IF',\n category: 'logical',\n description: 'Returns one value if a condition is true and another if it is false.',\n args: [\n arg('logical_test', 'The condition to test.', 'logical'),\n arg('value_if_true', 'The result when the condition is TRUE.'),\n optional('value_if_false', 'The result when the condition is FALSE (default FALSE).'),\n ],\n evaluate: (ctx) => {\n const condition = ctx.boolean(0);\n if (failed(condition)) return condition;\n if (condition) return ctx.value(1);\n return ctx.has(2) ? ctx.value(2) : false;\n },\n },\n {\n name: 'IFERROR',\n category: 'logical',\n description: 'Returns a value, or an alternative if the value is an error.',\n args: [arg('value', 'The value to check.'), arg('value_if_error', 'The result when value is an error.')],\n evaluate: (ctx) => {\n const value = ctx.value(0);\n return isFormulaError(value) ? ctx.value(1) : value;\n },\n },\n {\n name: 'IFNA',\n category: 'logical',\n description: 'Returns a value, or an alternative if the value is #N/A.',\n args: [arg('value', 'The value to check.'), arg('value_if_na', 'The result when value is #N/A.')],\n evaluate: (ctx) => {\n const value = ctx.value(0);\n return value === '#N/A' ? ctx.value(1) : value;\n },\n },\n {\n name: 'IFS',\n category: 'logical',\n description: 'Returns the value for the first condition that is TRUE.',\n args: [\n repeating('logical_test', 'A condition to test.', 'logical'),\n repeating('value_if_true', 'The result when this condition is the first TRUE one.'),\n ],\n evaluate: (ctx) => {\n for (let i = 0; i + 1 < ctx.args.length; i += 2) {\n const condition = ctx.boolean(i);\n if (failed(condition)) return condition;\n if (condition) return ctx.value(i + 1);\n }\n return ctx.error('#N/A', 'None of the conditions in IFS were TRUE.');\n },\n },\n {\n name: 'SWITCH',\n category: 'logical',\n description: 'Compares an expression with a list of values and returns the result for the first match. An extra last argument is the default.',\n args: [\n arg('expression', 'The value to compare.'),\n repeating('value', 'A value to compare the expression with.'),\n repeating('result', 'The result when the expression equals this value.'),\n ],\n allowTrailingArgument: true,\n evaluate: (ctx) => {\n const expression = ctx.scalar(0);\n if (isFormulaError(expression)) return expression;\n const target = normalizeValue(expression);\n let i = 1;\n for (; i + 1 < ctx.args.length; i += 2) {\n const candidate = ctx.scalar(i);\n if (isFormulaError(candidate)) return candidate;\n if (compareValues(target, normalizeValue(candidate)) === 0) return ctx.value(i + 1);\n }\n if (i < ctx.args.length) return ctx.value(i);\n return ctx.error('#N/A', 'SWITCH found no matching value and has no default.');\n },\n },\n {\n name: 'AND',\n category: 'logical',\n description: 'Returns TRUE if every argument is TRUE.',\n args: [LOGICALS],\n evaluate: (ctx) => {\n const values = logicalValues(ctx);\n return failed(values) ? values : values.every(Boolean);\n },\n },\n {\n name: 'OR',\n category: 'logical',\n description: 'Returns TRUE if any argument is TRUE.',\n args: [LOGICALS],\n evaluate: (ctx) => {\n const values = logicalValues(ctx);\n return failed(values) ? values : values.some(Boolean);\n },\n },\n {\n name: 'XOR',\n category: 'logical',\n description: 'Returns TRUE if an odd number of arguments are TRUE.',\n args: [LOGICALS],\n evaluate: (ctx) => {\n const values = logicalValues(ctx);\n return failed(values) ? values : values.filter(Boolean).length % 2 === 1;\n },\n },\n {\n name: 'NOT',\n category: 'logical',\n description: 'Reverses a logical value.',\n args: [arg('logical', 'The value to reverse.', 'logical')],\n evaluate: (ctx) => {\n const value = ctx.boolean(0);\n return failed(value) ? value : !value;\n },\n },\n];\n","import type { FormulaFunctionContext } from '../registry';\nimport { wildcardToRegExp } from '../criteria';\nimport { formatTextFunction } from '../../numfmt';\nimport { dateToSerial, isBlank, isFormulaError, toDate, toText } from '../values';\nimport { parseNumberText } from '../parse-input';\nimport { arg, failed, optional, repeating, type FunctionTable } from './helpers';\n\nconst TEXT = arg('text', 'The text to use.', 'text');\n\n/** Text argument plus a non-negative character count (default `fallback`). */\nfunction textAndCount(ctx: FormulaFunctionContext, countIndex: number, fallback?: number) {\n const text = ctx.text(0);\n if (failed(text)) return text;\n const count = ctx.integer(countIndex, fallback);\n if (failed(count)) return count;\n if (count < 0) return ctx.argError(countIndex, '#VALUE!', \"can't be negative.\");\n return { text, count };\n}\n\n/** Converts text such as \"1,234.5\", \"$12\", \"50%\" or an ISO date to a number, or null. */\nexport function parseNumericText(text: string): number | null {\n let trimmed = text.trim();\n if (trimmed === '') return null;\n let scale = 1;\n if (trimmed.endsWith('%')) {\n trimmed = trimmed.slice(0, -1).trim();\n scale = 0.01;\n }\n const negative = /^\\(.*\\)$/.test(trimmed);\n if (negative) trimmed = trimmed.slice(1, -1);\n const cleaned = trimmed.replace(/^([+-]?)[$€£¥]/, '$1').replace(/,(?=\\d{3}(\\D|$))/g, '');\n if (cleaned !== '' && /^[+-]?(\\d+\\.?\\d*|\\.\\d+)(e[+-]?\\d+)?$/i.test(cleaned)) {\n const num = Number(cleaned) * scale;\n return negative ? -num : num;\n }\n if (scale === 1 && !negative) {\n const date = /^\\d{4}-\\d{1,2}-\\d{1,2}/.test(trimmed) ? toDate(trimmed) : null;\n if (date) return dateToSerial(date);\n }\n return null;\n}\n\n/** FIND / SEARCH: 1-based position of `find_text` in `within_text`. */\nfunction findText(ctx: FormulaFunctionContext, caseInsensitive: boolean): number | string {\n const needle = ctx.text(0);\n if (failed(needle)) return needle;\n const haystack = ctx.text(1);\n if (failed(haystack)) return haystack;\n const start = ctx.integer(2, 1);\n if (failed(start)) return start;\n if (start < 1 || start > haystack.length + 1) {\n return ctx.argError(2, '#VALUE!', `must be between 1 and the length of the text (${haystack.length}).`);\n }\n let index: number;\n if (caseInsensitive) {\n // SEARCH supports wildcards: find the first start position where the pattern matches a prefix.\n const pattern = wildcardToRegExp(needle).source.slice(1, -1);\n const regex = new RegExp(pattern, 'i');\n const match = regex.exec(haystack.slice(start - 1));\n index = match ? match.index + start - 1 : -1;\n } else {\n index = haystack.indexOf(needle, start - 1);\n }\n if (index === -1) return ctx.error('#VALUE!', `${ctx.name} couldn't find \"${needle}\" in \"${haystack}\".`);\n return index + 1;\n}\n\nexport const TEXT_FUNCTIONS: FunctionTable = [\n {\n name: 'CONCAT',\n category: 'text',\n description: 'Joins text, numbers and the values of ranges into one piece of text.',\n args: [repeating('text', 'A value, cell or range to join.', 'text')],\n evaluate: (ctx) => {\n const items = ctx.flatValues();\n const error = items.find(isFormulaError);\n if (error) return error;\n return items.map(toText).join('');\n },\n },\n {\n name: 'TEXTJOIN',\n category: 'text',\n description: 'Joins values with a delimiter, optionally skipping empty values.',\n args: [\n arg('delimiter', 'Text to put between values.', 'text'),\n arg('ignore_empty', 'TRUE to skip empty values.', 'logical'),\n repeating('text', 'A value, cell or range to join.', 'text'),\n ],\n evaluate: (ctx) => {\n const delimiter = ctx.text(0);\n if (failed(delimiter)) return delimiter;\n const ignoreEmpty = ctx.boolean(1);\n if (failed(ignoreEmpty)) return ignoreEmpty;\n const items = ctx.flatValues(2);\n const error = items.find(isFormulaError);\n if (error) return error;\n return items\n .map(toText)\n .filter((text) => !ignoreEmpty || text !== '')\n .join(delimiter);\n },\n },\n {\n name: 'UPPER',\n category: 'text',\n description: 'Converts text to upper case.',\n args: [TEXT],\n evaluate: (ctx) => {\n const text = ctx.text(0);\n return failed(text) ? text : text.toUpperCase();\n },\n },\n {\n name: 'LOWER',\n category: 'text',\n description: 'Converts text to lower case.',\n args: [TEXT],\n evaluate: (ctx) => {\n const text = ctx.text(0);\n return failed(text) ? text : text.toLowerCase();\n },\n },\n {\n name: 'PROPER',\n category: 'text',\n description: 'Capitalizes the first letter of each word and lower-cases the rest.',\n args: [TEXT],\n evaluate: (ctx) => {\n const text = ctx.text(0);\n if (failed(text)) return text;\n return text.toLowerCase().replace(/(^|[^\\p{L}])(\\p{L})/gu, (_, before, letter) => before + letter.toUpperCase());\n },\n },\n {\n name: 'TRIM',\n category: 'text',\n description: 'Removes leading and trailing spaces and collapses runs of inner spaces to one, like Excel.',\n args: [TEXT],\n evaluate: (ctx) => {\n const text = ctx.text(0);\n // Inner runs of spaces collapse (Excel); tabs and line breaks inside are kept.\n return failed(text) ? text : text.trim().replace(/ {2,}/g, ' ');\n },\n },\n {\n name: 'LEN',\n category: 'text',\n description: 'Returns the number of characters in text.',\n args: [TEXT],\n evaluate: (ctx) => {\n const text = ctx.text(0);\n return failed(text) ? text : text.length;\n },\n },\n {\n name: 'LEFT',\n category: 'text',\n description: 'Returns the first characters of text.',\n args: [TEXT, optional('num_chars', 'How many characters to return (default 1).', 'number')],\n evaluate: (ctx) => {\n const parts = textAndCount(ctx, 1, 1);\n return typeof parts === 'string' ? parts : parts.text.slice(0, parts.count);\n },\n },\n {\n name: 'RIGHT',\n category: 'text',\n description: 'Returns the last characters of text.',\n args: [TEXT, optional('num_chars', 'How many characters to return (default 1).', 'number')],\n evaluate: (ctx) => {\n const parts = textAndCount(ctx, 1, 1);\n if (typeof parts === 'string') return parts;\n return parts.count === 0 ? '' : parts.text.slice(-parts.count);\n },\n },\n {\n name: 'MID',\n category: 'text',\n description: 'Returns characters from the middle of text, starting at a position.',\n args: [\n TEXT,\n arg('start_num', 'Position of the first character (1 = first).', 'number'),\n arg('num_chars', 'How many characters to return.', 'number'),\n ],\n evaluate: (ctx) => {\n const parts = textAndCount(ctx, 2);\n if (typeof parts === 'string') return parts;\n const start = ctx.integer(1);\n if (failed(start)) return start;\n if (start < 1) return ctx.argError(1, '#VALUE!', 'must be 1 or more.');\n return parts.text.slice(start - 1, start - 1 + parts.count);\n },\n },\n {\n name: 'FIND',\n category: 'text',\n description: 'Returns the position of text within other text (case-sensitive, no wildcards).',\n args: [\n arg('find_text', 'The text to find.', 'text'),\n arg('within_text', 'The text to search.', 'text'),\n optional('start_num', 'Position to start searching from (default 1).', 'number'),\n ],\n evaluate: (ctx) => findText(ctx, false),\n },\n {\n name: 'SEARCH',\n category: 'text',\n description: 'Returns the position of text within other text (case-insensitive, supports * and ? wildcards).',\n args: [\n arg('find_text', 'The text to find.', 'text'),\n arg('within_text', 'The text to search.', 'text'),\n optional('start_num', 'Position to start searching from (default 1).', 'number'),\n ],\n evaluate: (ctx) => findText(ctx, true),\n },\n {\n name: 'SUBSTITUTE',\n category: 'text',\n description: 'Replaces occurrences of old text with new text (all of them, or only the given instance).',\n args: [\n TEXT,\n arg('old_text', 'The text to replace.', 'text'),\n arg('new_text', 'The replacement text.', 'text'),\n optional('instance_num', 'Which occurrence to replace (default: all).', 'number'),\n ],\n evaluate: (ctx) => {\n const text = ctx.text(0);\n if (failed(text)) return text;\n const oldText = ctx.text(1);\n if (failed(oldText)) return oldText;\n const newText = ctx.text(2);\n if (failed(newText)) return newText;\n if (oldText === '') return text;\n if (!ctx.has(3)) return text.split(oldText).join(newText);\n const instance = ctx.integer(3);\n if (failed(instance)) return instance;\n if (instance < 1) return ctx.argError(3, '#VALUE!', 'must be 1 or more.');\n let index = -1;\n for (let n = 0; n < instance; n++) {\n index = text.indexOf(oldText, index + 1);\n if (index === -1) return text;\n }\n return text.slice(0, index) + newText + text.slice(index + oldText.length);\n },\n },\n {\n name: 'REPLACE',\n category: 'text',\n description: 'Replaces part of text, by position, with new text.',\n args: [\n arg('old_text', 'The text to change.', 'text'),\n arg('start_num', 'Position of the first character to replace (1 = first).', 'number'),\n arg('num_chars', 'How many characters to replace.', 'number'),\n arg('new_text', 'The replacement text.', 'text'),\n ],\n evaluate: (ctx) => {\n const text = ctx.text(0);\n if (failed(text)) return text;\n const start = ctx.integer(1);\n if (failed(start)) return start;\n if (start < 1) return ctx.argError(1, '#VALUE!', 'must be 1 or more.');\n const count = ctx.integer(2);\n if (failed(count)) return count;\n if (count < 0) return ctx.argError(2, '#VALUE!', \"can't be negative.\");\n const newText = ctx.text(3);\n if (failed(newText)) return newText;\n return text.slice(0, start - 1) + newText + text.slice(start - 1 + count);\n },\n },\n {\n name: 'REPT',\n category: 'text',\n description: 'Repeats text a number of times.',\n args: [TEXT, arg('number_times', 'How many times to repeat the text.', 'number')],\n evaluate: (ctx) => {\n const parts = textAndCount(ctx, 1);\n if (typeof parts === 'string') return parts;\n if (parts.text.length * parts.count > 32767) {\n return ctx.error('#VALUE!', 'REPT would produce text longer than 32,767 characters.');\n }\n return parts.text.repeat(parts.count);\n },\n },\n {\n name: 'EXACT',\n category: 'text',\n description: 'Returns TRUE if two pieces of text are identical, including case.',\n args: [arg('text1', 'The first text.', 'text'), arg('text2', 'The second text.', 'text')],\n evaluate: (ctx) => {\n const a = ctx.text(0);\n if (failed(a)) return a;\n const b = ctx.text(1);\n if (failed(b)) return b;\n return a === b;\n },\n },\n {\n name: 'VALUE',\n category: 'text',\n description: 'Converts text that looks like a number (e.g. \"1,234.5\", \"50%\", \"$12\") or an ISO date to a number.',\n args: [TEXT],\n evaluate: (ctx) => {\n const value = ctx.scalar(0);\n if (isFormulaError(value)) return value;\n if (typeof value === 'number') return value;\n if (isBlank(value)) return 0;\n // Typed-input rules (\"3/4/2026\", \"10:30 PM\", \"1 1/2\") after the lenient ones (\"€12\").\n const num = parseNumericText(toText(value)) ?? parseNumberText(toText(value));\n if (num === null) return ctx.argError(0, '#VALUE!', `(\"${toText(value)}\") can't be converted to a number.`);\n return num;\n },\n },\n {\n name: 'TEXT',\n category: 'text',\n description: 'Formats a number or date as text using an Excel format code such as \"0.00\", \"#,##0\", \"0%\", \"# ?/?\" or \"yyyy-mm-dd\".',\n args: [arg('value', 'The number or date to format.'), arg('format_text', 'The format code.', 'text')],\n evaluate: (ctx) => {\n const value = ctx.scalar(0);\n if (isFormulaError(value)) return value;\n const format = ctx.text(1);\n if (failed(format)) return format;\n const result = formatTextFunction(value, format);\n if ('text' in result) return result.text;\n if (result.error === '#VALUE!') {\n return Array.from(format).length > 255\n ? ctx.argError(1, '#VALUE!', 'is longer than 255 characters.')\n : ctx.argError(0, '#VALUE!', `is outside the date range, so it can't use the format \"${format}\".`);\n }\n return result.error;\n },\n },\n];\n","import type { FormulaFunctionContext } from '../registry';\nimport { hasWildcards, wildcardToRegExp } from '../criteria';\nimport {\n compareValues,\n isBlank,\n isFormulaError,\n normalizeValue,\n rangeGet,\n sliceRange,\n type FormulaRange,\n} from '../values';\nimport { arg, failed, optional, type FunctionTable } from './helpers';\n\n/**\n * Lookup functions return single values. A function that would return a whole\n * row or column (INDEX with row 0, XLOOKUP with a 2D return array) returns an\n * array: other functions can aggregate it (`SUM(INDEX(A1:C3, 0, 2))`), and a\n * cell showing it spills it into the cells below/right (dynamic arrays).\n */\n\ntype MatchMode = 'exact' | 'wildcard' | 'next-smaller' | 'next-larger' | 'sorted-asc' | 'sorted-desc';\n\nconst sameType = (a: unknown, b: unknown) =>\n (typeof a === 'number') === (typeof b === 'number') && (typeof a === 'boolean') === (typeof b === 'boolean');\n\n/**\n * Finds `target` in `values`; returns its index or -1.\n * - `exact`: equal values (case-insensitive text, numeric text = number);\n * - `wildcard`: like exact, but text targets may use `*`, `?` and `~`;\n * - `next-smaller` / `next-larger`: exact match, else the closest smaller / larger value;\n * - `sorted-asc` / `sorted-desc`: approximate match on sorted data, as MATCH 1 / -1 and\n * VLOOKUP TRUE: the last value <= target (ascending) or >= target (descending).\n */\nexport function lookupIndex(values: readonly unknown[], target: unknown, mode: MatchMode, reverse = false): number {\n const wanted = normalizeValue(target);\n const order = values.map((_, i) => i);\n if (reverse) order.reverse();\n\n if (mode === 'exact' || mode === 'wildcard') {\n const regex = mode === 'wildcard' && typeof wanted === 'string' && hasWildcards(wanted) ? wildcardToRegExp(wanted) : null;\n for (const i of order) {\n const value = normalizeValue(values[i]);\n if (isFormulaError(value) || isBlank(value)) continue;\n if (regex ? typeof value === 'string' && regex.test(value) : sameType(value, wanted) && compareValues(value, wanted) === 0) {\n return i;\n }\n }\n return -1;\n }\n\n if (mode === 'sorted-asc' || mode === 'sorted-desc') {\n let found = -1;\n for (let i = 0; i < values.length; i++) {\n const value = normalizeValue(values[i]);\n if (isFormulaError(value) || isBlank(value) || !sameType(value, wanted)) continue;\n const cmp = compareValues(value, wanted);\n if (mode === 'sorted-asc' ? cmp <= 0 : cmp >= 0) found = i;\n else break;\n }\n return found;\n }\n\n let best = -1;\n let bestValue: unknown;\n for (const i of order) {\n const value = normalizeValue(values[i]);\n if (isFormulaError(value) || isBlank(value) || !sameType(value, wanted)) continue;\n const cmp = compareValues(value, wanted);\n if (cmp === 0) return i;\n const candidate = mode === 'next-smaller' ? cmp < 0 : cmp > 0;\n const closer =\n best === -1 || (mode === 'next-smaller' ? compareValues(value, bestValue) > 0 : compareValues(value, bestValue) < 0);\n if (candidate && closer) {\n best = i;\n bestValue = value;\n }\n }\n return best;\n}\n\n/** Values of a one-row or one-column range, or an error if the range is 2D. */\nfunction vector(ctx: FormulaFunctionContext, index: number): { range: FormulaRange; values: any[] } | string {\n const range = ctx.range(index);\n if (failed(range)) return range;\n if (range.rows !== 1 && range.cols !== 1) {\n return ctx.argError(index, '#N/A', 'must be a single row or column.');\n }\n return { range, values: range.values };\n}\n\nconst notFound = (ctx: FormulaFunctionContext, value: unknown) =>\n ctx.error('#N/A', `${ctx.name} couldn't find ${typeof value === 'string' ? `\"${value}\"` : String(value)}.`);\n\n/** VLOOKUP / HLOOKUP share everything but orientation. */\nfunction tableLookup(ctx: FormulaFunctionContext, vertical: boolean): any {\n const target = ctx.scalar(0);\n if (isFormulaError(target)) return target;\n const table = ctx.range(1);\n if (failed(table)) return table;\n const index = ctx.integer(2);\n if (failed(index)) return index;\n const approximate = ctx.boolean(3, true);\n if (failed(approximate)) return approximate;\n\n const span = vertical ? table.cols : table.rows;\n if (index < 1) return ctx.argError(2, '#VALUE!', 'must be 1 or more.');\n if (index > span) {\n return ctx.argError(2, '#REF!', `is larger than the number of ${vertical ? 'columns' : 'rows'} in the table (${span}).`);\n }\n\n const keys = vertical ? sliceRange(table, 0, table.rows, 0, 1) : sliceRange(table, 0, 1, 0, table.cols);\n const position = lookupIndex(keys, target, approximate ? 'sorted-asc' : 'wildcard');\n if (position === -1) return notFound(ctx, target);\n return vertical ? rangeGet(table, position, index - 1) : rangeGet(table, index - 1, position);\n}\n\nexport const LOOKUP_FUNCTIONS: FunctionTable = [\n {\n name: 'VLOOKUP',\n category: 'lookup',\n description: 'Looks for a value in the first column of a table and returns the value in the same row of another column.',\n args: [\n arg('lookup_value', 'The value to find in the first column.'),\n arg('table_array', 'The table to search.', 'range'),\n arg('col_index_num', 'Which column to return (1 = first column).', 'number'),\n optional('range_lookup', 'FALSE for an exact match; TRUE or omitted for the closest match in a sorted first column.', 'logical'),\n ],\n evaluate: (ctx) => tableLookup(ctx, true),\n },\n {\n name: 'HLOOKUP',\n category: 'lookup',\n description: 'Looks for a value in the first row of a table and returns the value in the same column of another row.',\n args: [\n arg('lookup_value', 'The value to find in the first row.'),\n arg('table_array', 'The table to search.', 'range'),\n arg('row_index_num', 'Which row to return (1 = first row).', 'number'),\n optional('range_lookup', 'FALSE for an exact match; TRUE or omitted for the closest match in a sorted first row.', 'logical'),\n ],\n evaluate: (ctx) => tableLookup(ctx, false),\n },\n {\n name: 'MATCH',\n category: 'lookup',\n description: 'Returns the position of a value in a row or column.',\n args: [\n arg('lookup_value', 'The value to find.'),\n arg('lookup_array', 'A single row or column to search.', 'range'),\n optional('match_type', '0 = exact; 1 (default) = largest value <= lookup_value in ascending data; -1 = smallest value >= lookup_value in descending data.', 'number'),\n ],\n evaluate: (ctx) => {\n const target = ctx.scalar(0);\n if (isFormulaError(target)) return target;\n const list = vector(ctx, 1);\n if (failed(list)) return list;\n const type = ctx.number(2, 1);\n if (failed(type)) return type;\n const mode: MatchMode = type === 0 ? 'wildcard' : type > 0 ? 'sorted-asc' : 'sorted-desc';\n const position = lookupIndex(list.values, target, mode);\n return position === -1 ? notFound(ctx, target) : position + 1;\n },\n },\n {\n name: 'INDEX',\n category: 'lookup',\n description: 'Returns the value at a row and column of a range. Row or column 0 returns the whole column or row.',\n args: [\n arg('array', 'The range to read from.', 'range'),\n arg('row_num', 'The row (1 = first row; 0 = every row).', 'number'),\n optional('column_num', 'The column (1 = first column; 0 = every column).', 'number'),\n ],\n evaluate: (ctx) => {\n const range = ctx.range(0);\n if (failed(range)) return range;\n let row = ctx.integer(1);\n if (failed(row)) return row;\n let col = ctx.integer(2, ctx.has(2) ? undefined : 1);\n if (failed(col)) return col;\n // INDEX(single_row, n) picks the n-th column.\n if (!ctx.has(2) && range.rows === 1 && range.cols > 1) {\n col = row;\n row = 1;\n }\n if (row < 0 || row > range.rows) {\n return ctx.argError(1, '#REF!', `is outside the range (1 to ${range.rows}).`);\n }\n if (col < 0 || col > range.cols) {\n return ctx.argError(2, '#REF!', `is outside the range (1 to ${range.cols}).`);\n }\n if (row === 0 && col === 0) return range.values;\n if (row === 0) return sliceRange(range, 0, range.rows, col - 1, 1);\n if (col === 0) return sliceRange(range, row - 1, 1, 0, range.cols);\n return rangeGet(range, row - 1, col - 1);\n },\n },\n {\n name: 'XLOOKUP',\n category: 'lookup',\n description: 'Finds a value in one range and returns the value at the same position in another range.',\n args: [\n arg('lookup_value', 'The value to find.'),\n arg('lookup_array', 'A single row or column to search.', 'range'),\n arg('return_array', 'The range to return a value from; same height (or width) as lookup_array.', 'range'),\n optional('if_not_found', 'Returned when nothing matches (default #N/A).'),\n optional('match_mode', '0 (default) = exact; -1 = exact or next smaller; 1 = exact or next larger; 2 = wildcard match.', 'number'),\n optional('search_mode', '1 (default) = first to last; -1 = last to first.', 'number'),\n ],\n evaluate: (ctx) => {\n const target = ctx.scalar(0);\n if (isFormulaError(target)) return target;\n const list = vector(ctx, 1);\n if (failed(list)) return list;\n const result = ctx.range(2);\n if (failed(result)) return result;\n const matchMode = ctx.integer(4, 0);\n if (failed(matchMode)) return matchMode;\n const searchMode = ctx.integer(5, 1);\n if (failed(searchMode)) return searchMode;\n\n const vertical = list.range.cols === 1 && list.range.rows > 1;\n const length = list.values.length;\n if ((vertical ? result.rows : result.cols) !== length && !(length === 1 && (result.rows === 1 || result.cols === 1))) {\n return ctx.argError(2, '#VALUE!', 'must be the same height (or width) as argument 2.');\n }\n const modes: Record<number, MatchMode> = { 0: 'exact', [-1]: 'next-smaller', 1: 'next-larger', 2: 'wildcard' };\n const mode = modes[matchMode];\n if (!mode) return ctx.argError(4, '#VALUE!', 'must be 0, -1, 1 or 2.');\n if (![1, -1, 2, -2].includes(searchMode)) return ctx.argError(5, '#VALUE!', 'must be 1, -1, 2 or -2.');\n\n const position = lookupIndex(list.values, target, mode, searchMode < 0);\n if (position === -1) return ctx.has(3) ? ctx.value(3) : notFound(ctx, target);\n if (vertical) {\n return result.cols === 1 ? rangeGet(result, position, 0) : sliceRange(result, position, 1, 0, result.cols);\n }\n return result.rows === 1 ? rangeGet(result, 0, position) : sliceRange(result, 0, result.rows, position, 1);\n },\n },\n];\n","import type { FormulaFunctionContext } from '../registry';\nimport { daysBetween, formatIsoDate, formatIsoDateTime, isBlank, isFormulaError, toDate } from '../values';\nimport { arg, failed, optional, type FunctionTable } from './helpers';\n\n/**\n * Dates are ISO text (\"2026-01-31\"), as TODAY() has always returned, so they\n * display readably and sort correctly as text. Date arguments also accept\n * spreadsheet serial numbers. Date functions return ISO text; part functions\n * (YEAR, WEEKDAY, ...) and differences (DAYS, DATEDIF) return numbers.\n */\n\nconst START_DATE = arg('start_date', 'The date to start from.', 'date');\nconst DATE = arg('date', 'A date (e.g. \"2026-01-31\" or a cell containing one).', 'date');\n\n/** Adds months, clamping the day to the end of the target month (Jan 31 + 1 month = Feb 28). */\nfunction addMonths(date: Date, months: number): Date {\n const target = new Date(date.getFullYear(), date.getMonth() + months, 1);\n const lastDay = new Date(target.getFullYear(), target.getMonth() + 1, 0).getDate();\n target.setDate(Math.min(date.getDate(), lastDay));\n return target;\n}\n\nfunction datePart(ctx: FormulaFunctionContext, part: (date: Date) => number): number | string {\n const date = ctx.date(0);\n return failed(date) ? date : part(date);\n}\n\nconst isWorkday = (date: Date) => date.getDay() !== 0 && date.getDay() !== 6;\n\nexport const DATE_FUNCTIONS: FunctionTable = [\n {\n name: 'TODAY',\n category: 'date',\n description: 'Returns today\\'s date (YYYY-MM-DD). Changes on every recalculation.',\n args: [],\n volatile: true,\n // Local calendar date (toISOString would give the UTC date).\n evaluate: () => formatIsoDate(new Date()),\n },\n {\n name: 'NOW',\n category: 'date',\n description: 'Returns the current date and time (YYYY-MM-DD HH:MM:SS). Changes on every recalculation.',\n args: [],\n volatile: true,\n evaluate: () => formatIsoDateTime(new Date()),\n },\n {\n name: 'DATE',\n category: 'date',\n description: 'Builds a date from a year, month and day. Months and days outside their range roll over.',\n args: [\n arg('year', 'The year (0-1899 are added to 1900).', 'number'),\n arg('month', 'The month (1-12).', 'number'),\n arg('day', 'The day of the month.', 'number'),\n ],\n evaluate: (ctx) => {\n let year = ctx.integer(0);\n if (failed(year)) return year;\n const month = ctx.integer(1);\n if (failed(month)) return month;\n const day = ctx.integer(2);\n if (failed(day)) return day;\n if (year < 0 || year > 9999) return ctx.argError(0, '#NUM!', 'must be between 0 and 9999.');\n if (year < 1900) year += 1900;\n const date = new Date(year, month - 1, 1);\n date.setDate(day);\n if (date.getFullYear() < 1900 || date.getFullYear() > 9999) {\n return ctx.error('#NUM!', 'DATE produced a date outside the years 1900-9999.');\n }\n return formatIsoDate(date);\n },\n },\n {\n name: 'YEAR',\n category: 'date',\n description: 'Returns the year of a date.',\n args: [DATE],\n evaluate: (ctx) => datePart(ctx, (d) => d.getFullYear()),\n },\n {\n name: 'MONTH',\n category: 'date',\n description: 'Returns the month of a date (1-12).',\n args: [DATE],\n evaluate: (ctx) => datePart(ctx, (d) => d.getMonth() + 1),\n },\n {\n name: 'DAY',\n category: 'date',\n description: 'Returns the day of the month of a date (1-31).',\n args: [DATE],\n evaluate: (ctx) => datePart(ctx, (d) => d.getDate()),\n },\n {\n name: 'WEEKDAY',\n category: 'date',\n description: 'Returns the day of the week of a date.',\n args: [DATE, optional('return_type', '1 (default) = Sunday 1 to Saturday 7; 2 = Monday 1 to Sunday 7; 3 = Monday 0 to Sunday 6.', 'number')],\n evaluate: (ctx) => {\n const date = ctx.date(0);\n if (failed(date)) return date;\n const type = ctx.integer(1, 1);\n if (failed(type)) return type;\n const day = date.getDay(); // 0 = Sunday\n if (type === 1) return day + 1;\n if (type === 2) return day === 0 ? 7 : day;\n if (type === 3) return day === 0 ? 6 : day - 1;\n return ctx.argError(1, '#NUM!', 'must be 1, 2 or 3.');\n },\n },\n {\n name: 'DAYS',\n category: 'date',\n description: 'Returns the number of days between two dates.',\n args: [arg('end_date', 'The later date.', 'date'), START_DATE],\n evaluate: (ctx) => {\n const end = ctx.date(0);\n if (failed(end)) return end;\n const start = ctx.date(1);\n if (failed(start)) return start;\n return daysBetween(start, end);\n },\n },\n {\n name: 'EDATE',\n category: 'date',\n description: 'Returns the date a number of months before or after a date.',\n args: [START_DATE, arg('months', 'Months to add (negative for earlier dates).', 'number')],\n evaluate: (ctx) => {\n const start = ctx.date(0);\n if (failed(start)) return start;\n const months = ctx.integer(1);\n if (failed(months)) return months;\n return formatIsoDate(addMonths(start, months));\n },\n },\n {\n name: 'EOMONTH',\n category: 'date',\n description: 'Returns the last day of the month a number of months before or after a date.',\n args: [START_DATE, arg('months', 'Months to add (0 = same month).', 'number')],\n evaluate: (ctx) => {\n const start = ctx.date(0);\n if (failed(start)) return start;\n const months = ctx.integer(1);\n if (failed(months)) return months;\n return formatIsoDate(new Date(start.getFullYear(), start.getMonth() + months + 1, 0));\n },\n },\n {\n name: 'NETWORKDAYS',\n category: 'date',\n description: 'Counts the working days (Monday to Friday) between two dates, inclusive, minus holidays.',\n args: [START_DATE, arg('end_date', 'The last date.', 'date'), optional('holidays', 'Dates to exclude.', 'range')],\n evaluate: (ctx) => {\n const start = ctx.date(0);\n if (failed(start)) return start;\n const end = ctx.date(1);\n if (failed(end)) return end;\n const holidays = new Set<string>();\n if (ctx.has(2)) {\n const raw = ctx.value(2);\n for (const value of Array.isArray(raw) ? raw : [raw]) {\n if (isFormulaError(value)) return value;\n if (isBlank(value)) continue;\n const date = toDate(value);\n if (!date) return ctx.argError(2, '#VALUE!', 'must contain only dates.');\n holidays.add(formatIsoDate(date));\n }\n }\n const sign = daysBetween(start, end) < 0 ? -1 : 1;\n const [from, to] = sign < 0 ? [end, start] : [start, end];\n let count = 0;\n const total = daysBetween(from, to);\n for (let i = 0; i <= total; i++) {\n const day = new Date(from.getFullYear(), from.getMonth(), from.getDate() + i);\n if (isWorkday(day) && !holidays.has(formatIsoDate(day))) count++;\n }\n return sign * count;\n },\n },\n {\n name: 'DATEDIF',\n category: 'date',\n description: 'Returns the difference between two dates in years, months or days.',\n args: [\n START_DATE,\n arg('end_date', 'The later date.', 'date'),\n arg('unit', '\"Y\" years, \"M\" months, \"D\" days, \"MD\" days ignoring months and years, \"YM\" months ignoring years, \"YD\" days ignoring years.', 'text'),\n ],\n evaluate: (ctx) => {\n const start = ctx.date(0);\n if (failed(start)) return start;\n const end = ctx.date(1);\n if (failed(end)) return end;\n const unit = ctx.text(2);\n if (failed(unit)) return unit;\n if (daysBetween(start, end) < 0) return ctx.error('#NUM!', 'DATEDIF needs the start date to be on or before the end date.');\n\n let months = (end.getFullYear() - start.getFullYear()) * 12 + end.getMonth() - start.getMonth();\n if (end.getDate() < start.getDate()) months--;\n switch (unit.trim().toUpperCase()) {\n case 'Y':\n return Math.floor(months / 12);\n case 'M':\n return months;\n case 'D':\n return daysBetween(start, end);\n case 'YM':\n return months % 12;\n case 'MD': {\n const anchor = addMonths(start, months);\n return daysBetween(anchor, end);\n }\n case 'YD': {\n const anchor = addMonths(start, Math.floor(months / 12) * 12);\n return daysBetween(anchor, end);\n }\n default:\n return ctx.argError(2, '#NUM!', 'must be \"Y\", \"M\", \"D\", \"MD\", \"YM\" or \"YD\".');\n }\n },\n },\n];\n","import type { FormulaFunctionContext } from '../registry';\nimport { isBlank, isFormulaError, numericValue } from '../values';\nimport { arg, type FunctionTable } from './helpers';\n\nconst VALUE = arg('value', 'The value or cell to check.');\n\n/** IS* functions look at a single value without propagating errors. */\nconst check = (test: (value: unknown) => boolean) => (ctx: FormulaFunctionContext) => test(ctx.scalar(0));\n\nexport const INFORMATION_FUNCTIONS: FunctionTable = [\n {\n name: 'ISBLANK',\n category: 'information',\n description: 'Returns TRUE if the value is empty.',\n args: [VALUE],\n evaluate: check(isBlank),\n },\n {\n name: 'ISNUMBER',\n category: 'information',\n description: 'Returns TRUE if the value is a number (typed cell values that look like numbers count).',\n args: [VALUE],\n evaluate: check((value) => numericValue(value) !== null),\n },\n {\n name: 'ISTEXT',\n category: 'information',\n description: 'Returns TRUE if the value is text that is not a number.',\n args: [VALUE],\n evaluate: check(\n (value) => typeof value === 'string' && !isBlank(value) && !isFormulaError(value) && numericValue(value) === null,\n ),\n },\n {\n name: 'ISERROR',\n category: 'information',\n description: 'Returns TRUE if the value is any error.',\n args: [VALUE],\n evaluate: check(isFormulaError),\n },\n {\n name: 'ISNA',\n category: 'information',\n description: 'Returns TRUE if the value is the #N/A error.',\n args: [VALUE],\n evaluate: check((value) => value === '#N/A'),\n },\n];\n","import type { FormulaFunctionContext } from '../registry';\nimport {\n compareValues,\n isBlank,\n isFormulaError,\n normalizeValue,\n rangeGet,\n toBoolean,\n toFormulaRange,\n withRangeShape,\n type FormulaRange,\n} from '../values';\nimport { arg, failed, optional, repeating, type FunctionTable } from './helpers';\n\n/**\n * Dynamic-array functions. They return shaped, column-major arrays (see\n * `withRangeShape`); a cell holding one spills the array into the cells\n * below and to the right. Empty results are #CALC!, as in Excel.\n */\n\n/** Largest array SEQUENCE and RANDARRAY may build. */\nconst MAX_ARRAY_CELLS = 1_000_000;\n\n/** An argument as a range; a single value is a 1x1 range. */\nfunction rangeArg(ctx: FormulaFunctionContext, index: number): FormulaRange | string {\n const value = ctx.value(index);\n if (Array.isArray(value)) return toFormulaRange(value);\n if (isFormulaError(value)) return value;\n return { rows: 1, cols: 1, values: [value] };\n}\n\n/** Builds a shaped column-major array from row-major rows. */\nfunction fromRows(rows: any[][], cols: number): any[] {\n const out: any[] = [];\n for (let c = 0; c < cols; c++) {\n for (const row of rows) out.push(row[c]);\n }\n return withRangeShape(out, rows.length, cols);\n}\n\nconst rowOf = (range: FormulaRange, r: number): any[] => {\n const row: any[] = [];\n for (let c = 0; c < range.cols; c++) row.push(rangeGet(range, r, c));\n return row;\n};\n\nconst colOf = (range: FormulaRange, c: number): any[] => {\n const col: any[] = [];\n for (let r = 0; r < range.rows; r++) col.push(rangeGet(range, r, c));\n return col;\n};\n\n/** The rows (or, `byCol`, the columns) of a range as lists. */\nconst linesOf = (range: FormulaRange, byCol: boolean): any[][] =>\n byCol\n ? Array.from({ length: range.cols }, (_, c) => colOf(range, c))\n : Array.from({ length: range.rows }, (_, r) => rowOf(range, r));\n\n/** Rebuilds a range from lines (rows, or columns when `byCol`). */\nfunction fromLines(lines: any[][], byCol: boolean, width: number): any[] {\n if (!byCol) return fromRows(lines, width);\n const out: any[] = [];\n for (const line of lines) for (const v of line) out.push(v);\n return withRangeShape(out, width, lines.length);\n}\n\n/** Sort comparison for SORT/SORTBY: numeric text sorts as numbers, blanks last in either order. */\nfunction compareForSort(a: unknown, b: unknown, order: number): number {\n const blankA = isBlank(a);\n const blankB = isBlank(b);\n if (blankA || blankB) return blankA === blankB ? 0 : blankA ? 1 : -1;\n return compareValues(normalizeValue(a), normalizeValue(b)) * order;\n}\n\nconst sortOrder = (ctx: FormulaFunctionContext, index: number): number | string => {\n const order = ctx.integer(index, 1);\n if (failed(order)) return order;\n return order === 1 || order === -1 ? order : ctx.argError(index, '#VALUE!', 'must be 1 (ascending) or -1 (descending).');\n};\n\n/** A value used as a FILTER condition. */\nfunction includeFlag(value: unknown): boolean | string {\n if (isFormulaError(value)) return value;\n const flag = toBoolean(value);\n return isFormulaError(flag) ? '#VALUE!' : flag;\n}\n\nexport const ARRAY_FUNCTIONS: FunctionTable = [\n {\n name: 'FILTER',\n category: 'lookup',\n description: 'Returns the rows (or columns) of an array whose include value is TRUE. The result spills.',\n args: [\n arg('array', 'The range or array to filter.', 'range'),\n arg('include', 'TRUE/FALSE for each row (a column as tall as array) or each column (a row as wide as array).'),\n optional('if_empty', 'Returned when nothing matches (default #CALC!).'),\n ],\n evaluate: (ctx) => {\n const range = rangeArg(ctx, 0);\n if (failed(range)) return range;\n const include = rangeArg(ctx, 1);\n if (failed(include)) return include;\n\n let byCol: boolean;\n if (include.cols === 1 && include.rows === range.rows) byCol = false;\n else if (include.rows === 1 && include.cols === range.cols) byCol = true;\n else {\n return ctx.argError(1, '#VALUE!', 'must be a single column as tall as argument 1 (or a single row as wide).');\n }\n\n const lines = linesOf(range, byCol);\n const kept: any[][] = [];\n for (let i = 0; i < lines.length; i++) {\n const flag = includeFlag(include.values[i]);\n if (typeof flag === 'string') {\n return flag === '#VALUE!' ? ctx.argError(1, '#VALUE!', `has a value that isn't TRUE or FALSE (${include.values[i]}).`) : flag;\n }\n if (flag) kept.push(lines[i]);\n }\n if (kept.length === 0) {\n return ctx.has(2) ? ctx.value(2) : ctx.error('#CALC!', 'FILTER found no matching values.');\n }\n return fromLines(kept, byCol, byCol ? range.rows : range.cols);\n },\n },\n {\n name: 'SORT',\n category: 'lookup',\n description: 'Sorts the rows (or columns) of an array by one of its columns (or rows). The result spills.',\n args: [\n arg('array', 'The range or array to sort.', 'range'),\n optional('sort_index', 'The column (or row, with by_col) to sort by; 1 = first (default).', 'number'),\n optional('sort_order', '1 = ascending (default), -1 = descending.', 'number'),\n optional('by_col', 'TRUE sorts columns by a row; FALSE (default) sorts rows by a column.', 'logical'),\n ],\n evaluate: (ctx) => {\n const range = rangeArg(ctx, 0);\n if (failed(range)) return range;\n const index = ctx.integer(1, 1);\n if (failed(index)) return index;\n const order = sortOrder(ctx, 2);\n if (failed(order)) return order;\n const byCol = ctx.boolean(3, false);\n if (failed(byCol)) return byCol;\n const width = byCol ? range.rows : range.cols;\n if (index < 1 || index > width) {\n return ctx.argError(1, '#VALUE!', `must be between 1 and ${width}.`);\n }\n const lines = linesOf(range, byCol)\n .map((line, position) => ({ line, position }))\n .sort((a, b) => compareForSort(a.line[index - 1], b.line[index - 1], order) || a.position - b.position)\n .map((entry) => entry.line);\n return fromLines(lines, byCol, width);\n },\n },\n {\n name: 'SORTBY',\n category: 'lookup',\n description: 'Sorts an array by the values in other ranges (one value per row or column). The result spills.',\n args: [\n arg('array', 'The range or array to sort.', 'range'),\n arg('by_array', 'A column as tall as array (or a row as wide) to sort by.', 'range'),\n optional('sort_order', '1 = ascending (default), -1 = descending.', 'number'),\n arg('by_array', 'Another range to sort by, for ties.', 'range', { repeating: true, optional: true }),\n arg('sort_order', 'Its order: 1 = ascending (default), -1 = descending.', 'number', { repeating: true, optional: true }),\n ],\n allowTrailingArgument: true,\n evaluate: (ctx) => {\n const range = rangeArg(ctx, 0);\n if (failed(range)) return range;\n const keys: Array<{ values: any[]; order: number }> = [];\n let byCol: boolean | null = null;\n for (let i = 1; i < ctx.args.length; i += 2) {\n const by = rangeArg(ctx, i);\n if (failed(by)) return by;\n let keyByCol: boolean;\n if (by.cols === 1 && by.rows === range.rows) keyByCol = false;\n else if (by.rows === 1 && by.cols === range.cols) keyByCol = true;\n else {\n return ctx.argError(i, '#VALUE!', 'must be a single column as tall as argument 1 (or a single row as wide).');\n }\n if (byCol !== null && byCol !== keyByCol) {\n return ctx.argError(i, '#VALUE!', 'must have the same orientation as the other sort ranges.');\n }\n byCol = keyByCol;\n const order = sortOrder(ctx, i + 1);\n if (failed(order)) return order;\n keys.push({ values: by.values, order });\n }\n const sortByCol = byCol ?? false;\n const lines = linesOf(range, sortByCol)\n .map((line, position) => ({ line, position }))\n .sort((a, b) => {\n for (const key of keys) {\n const result = compareForSort(key.values[a.position], key.values[b.position], key.order);\n if (result !== 0) return result;\n }\n return a.position - b.position;\n })\n .map((entry) => entry.line);\n return fromLines(lines, sortByCol, sortByCol ? range.rows : range.cols);\n },\n },\n {\n name: 'UNIQUE',\n category: 'lookup',\n description: 'Returns the distinct rows (or columns) of an array, in order of first appearance. The result spills.',\n args: [\n arg('array', 'The range or array.', 'range'),\n optional('by_col', 'TRUE compares columns; FALSE (default) compares rows.', 'logical'),\n optional('exactly_once', 'TRUE returns only the rows that appear exactly once (default FALSE).', 'logical'),\n ],\n evaluate: (ctx) => {\n const range = rangeArg(ctx, 0);\n if (failed(range)) return range;\n const byCol = ctx.boolean(1, false);\n if (failed(byCol)) return byCol;\n const exactlyOnce = ctx.boolean(2, false);\n if (failed(exactlyOnce)) return exactlyOnce;\n\n // Text compares case-insensitively and numeric text equals its number.\n const keyOf = (line: any[]) =>\n JSON.stringify(\n line.map((v) => {\n const n = normalizeValue(v);\n return typeof n === 'string' ? `s:${n.toLowerCase()}` : isBlank(n) ? 'blank' : `${typeof n}:${String(n)}`;\n }),\n );\n const counts = new Map<string, { line: any[]; count: number }>();\n for (const line of linesOf(range, byCol)) {\n const key = keyOf(line);\n const entry = counts.get(key);\n if (entry) entry.count++;\n else counts.set(key, { line, count: 1 });\n }\n const lines = Array.from(counts.values())\n .filter((entry) => !exactlyOnce || entry.count === 1)\n .map((entry) => entry.line);\n if (lines.length === 0) return ctx.error('#CALC!', 'UNIQUE found no values that appear exactly once.');\n return fromLines(lines, byCol, byCol ? range.rows : range.cols);\n },\n },\n {\n name: 'SEQUENCE',\n category: 'math',\n description: 'Returns a grid of sequential numbers, filled row by row. The result spills.',\n args: [\n arg('rows', 'The number of rows.', 'number'),\n optional('columns', 'The number of columns (default 1).', 'number'),\n optional('start', 'The first number (default 1).', 'number'),\n optional('step', 'The increment (default 1).', 'number'),\n ],\n evaluate: (ctx) => {\n const rows = ctx.integer(0);\n if (failed(rows)) return rows;\n const cols = ctx.integer(1, 1);\n if (failed(cols)) return cols;\n const start = ctx.number(2, 1);\n if (failed(start)) return start;\n const step = ctx.number(3, 1);\n if (failed(step)) return step;\n if (rows < 1 || cols < 1) return ctx.error('#CALC!', 'SEQUENCE needs at least one row and one column.');\n if (rows * cols > MAX_ARRAY_CELLS) return ctx.error('#NUM!', `SEQUENCE can return at most ${MAX_ARRAY_CELLS} values.`);\n const out: number[] = [];\n for (let c = 0; c < cols; c++) {\n for (let r = 0; r < rows; r++) out.push(start + step * (r * cols + c));\n }\n return rows * cols === 1 ? out[0] : withRangeShape(out, rows, cols);\n },\n },\n {\n name: 'TRANSPOSE',\n category: 'lookup',\n description: 'Swaps the rows and columns of an array. The result spills.',\n args: [arg('array', 'The range or array to transpose.', 'range')],\n evaluate: (ctx) => {\n const range = rangeArg(ctx, 0);\n if (failed(range)) return range;\n if (range.rows * range.cols === 1) return range.values[0];\n const out: any[] = [];\n // Column c of the result is row c of the source.\n for (let r = 0; r < range.rows; r++) {\n for (let c = 0; c < range.cols; c++) out.push(rangeGet(range, r, c));\n }\n return withRangeShape(out, range.cols, range.rows);\n },\n },\n {\n name: 'RANDARRAY',\n category: 'math',\n description: 'Returns a grid of random numbers. Changes on every recalculation. The result spills.',\n args: [\n optional('rows', 'The number of rows (default 1).', 'number'),\n optional('columns', 'The number of columns (default 1).', 'number'),\n optional('min', 'The smallest number (default 0).', 'number'),\n optional('max', 'The largest number (default 1).', 'number'),\n optional('integer', 'TRUE returns whole numbers; FALSE (default) decimals.', 'logical'),\n ],\n volatile: true,\n evaluate: (ctx) => {\n const rows = ctx.integer(0, 1);\n if (failed(rows)) return rows;\n const cols = ctx.integer(1, 1);\n if (failed(cols)) return cols;\n const min = ctx.number(2, 0);\n if (failed(min)) return min;\n const max = ctx.number(3, 1);\n if (failed(max)) return max;\n const integer = ctx.boolean(4, false);\n if (failed(integer)) return integer;\n if (rows < 1 || cols < 1) return ctx.error('#CALC!', 'RANDARRAY needs at least one row and one column.');\n if (rows * cols > MAX_ARRAY_CELLS) return ctx.error('#NUM!', `RANDARRAY can return at most ${MAX_ARRAY_CELLS} values.`);\n if (min > max) return ctx.argError(2, '#VALUE!', \"can't be larger than max.\");\n if (integer && (!Number.isInteger(min) || !Number.isInteger(max))) {\n return ctx.error('#VALUE!', 'RANDARRAY needs whole min and max values when integer is TRUE.');\n }\n const next = () => (integer ? min + Math.floor(Math.random() * (max - min + 1)) : min + Math.random() * (max - min));\n const out: number[] = [];\n for (let i = 0; i < rows * cols; i++) out.push(next());\n return rows * cols === 1 ? out[0] : withRangeShape(out, rows, cols);\n },\n },\n {\n // Evaluated by the evaluator itself (it defines names); listed here for\n // autocomplete, signatures and argument-count checks.\n name: 'LET',\n category: 'logical',\n description:\n 'Names the results of calculations so a formula can reuse them: LET(x, A1*2, x+x). The last argument is the calculation that uses the names.',\n args: [\n repeating('name', 'A name to define (letters, not a cell reference).'),\n repeating('name_value', 'The value of the name.'),\n ],\n allowTrailingArgument: true,\n evaluate: (ctx) => ctx.error('#ERROR!', 'LET must be evaluated by the formula evaluator.'),\n },\n];\n\n/**\n * Built-in scalar functions applied element by element to arrays (see\n * `FormulaFunctionDefinition.elementwise`). IF, IFERROR and IFNA lift on\n * their first argument only.\n */\nexport const ELEMENTWISE_BUILTINS: Readonly<Record<string, true | readonly number[]>> = {\n ABS: true,\n ROUND: true,\n ROUNDUP: true,\n ROUNDDOWN: true,\n INT: true,\n MOD: true,\n POWER: true,\n SQRT: true,\n SIGN: true,\n CEILING: true,\n FLOOR: true,\n LEN: true,\n UPPER: true,\n LOWER: true,\n TRIM: true,\n PROPER: true,\n LEFT: true,\n RIGHT: true,\n MID: true,\n FIND: true,\n SEARCH: true,\n SUBSTITUTE: true,\n REPLACE: true,\n REPT: true,\n EXACT: true,\n VALUE: true,\n TEXT: true,\n YEAR: true,\n MONTH: true,\n DAY: true,\n WEEKDAY: true,\n EDATE: true,\n EOMONTH: true,\n DATE: true,\n NOT: true,\n ISBLANK: true,\n ISNUMBER: true,\n ISTEXT: true,\n ISERROR: true,\n ISNA: true,\n IF: [0],\n IFERROR: [0],\n IFNA: [0],\n};\n","import type { FormulaFunctionDefinition } from '../registry';\nimport { MATH_FUNCTIONS } from './math';\nimport { STATISTICAL_FUNCTIONS } from './statistical';\nimport { CONDITIONAL_FUNCTIONS } from './conditional';\nimport { LOGICAL_FUNCTIONS } from './logical';\nimport { TEXT_FUNCTIONS } from './text';\nimport { LOOKUP_FUNCTIONS } from './lookup';\nimport { DATE_FUNCTIONS } from './date';\nimport { INFORMATION_FUNCTIONS } from './information';\nimport { ARRAY_FUNCTIONS, ELEMENTWISE_BUILTINS } from './array';\n\n/** Every built-in formula function. The registry is seeded from this list. */\nexport const BUILTIN_FORMULA_FUNCTIONS: readonly FormulaFunctionDefinition[] = [\n ...MATH_FUNCTIONS,\n ...STATISTICAL_FUNCTIONS,\n ...CONDITIONAL_FUNCTIONS,\n ...LOGICAL_FUNCTIONS,\n ...TEXT_FUNCTIONS,\n ...LOOKUP_FUNCTIONS,\n ...DATE_FUNCTIONS,\n ...INFORMATION_FUNCTIONS,\n ...ARRAY_FUNCTIONS,\n].map((def) => {\n // Scalar built-ins that apply element by element to arrays.\n const elementwise = ELEMENTWISE_BUILTINS[def.name];\n return elementwise && def.elementwise === undefined ? { ...def, elementwise } : def;\n});\n","import type { ASTNode } from './parser';\nimport type { FormulaErrorCode, FormulaRange } from './values';\nimport { BUILTIN_FORMULA_FUNCTIONS } from './functions';\n\nexport type FormulaFunctionCategory =\n | 'math'\n | 'statistical'\n | 'logical'\n | 'text'\n | 'lookup'\n | 'date'\n | 'information';\n\n/** What an argument expects. Used for documentation, hints and messages. */\nexport type FormulaArgumentType =\n | 'any'\n | 'number'\n | 'text'\n | 'logical'\n | 'date'\n | 'range'\n | 'criteria';\n\nexport interface FormulaArgumentSpec {\n /** Argument name shown in signatures, e.g. `sum_range`. */\n name: string;\n description: string;\n type?: FormulaArgumentType;\n /** The argument may be left out. */\n optional?: boolean;\n /**\n * The argument repeats. Consecutive repeating arguments form a group that\n * repeats together (`criteria_range, criteria` in SUMIFS). The group must be\n * the last arguments, and is required once unless its first spec is optional.\n */\n repeating?: boolean;\n}\n\n/** One argument of a call, evaluated lazily so functions like IF can short-circuit. */\nexport interface FormulaArgument {\n /** 0-based position in the call. */\n readonly index: number;\n readonly node: ASTNode;\n /** Evaluates the argument (once; later calls return the cached value). Ranges evaluate to arrays. */\n value(): any;\n}\n\n/** What a function implementation receives. Coercion helpers report argument-indexed diagnostics. */\nexport interface FormulaFunctionContext {\n /** Upper-case function name. */\n readonly name: string;\n readonly args: readonly FormulaArgument[];\n /** Returns `code` and records `message` as the reason (shown to the user if the cell ends up with that error). */\n error(code: FormulaErrorCode, message?: string): string;\n /** Like `error`, phrased as `Argument N of NAME <reason>`. */\n argError(index: number, code: FormulaErrorCode, reason: string): string;\n /** Whether argument `index` was supplied. */\n has(index: number): boolean;\n /** Raw (lazily evaluated) value of an argument; ranges are arrays. */\n value(index: number): any;\n /** Single value of an argument: a one-cell range is unwrapped, larger ranges are #VALUE!. */\n scalar(index: number): any;\n /** Argument as a number (or an error string). Text that isn't numeric is #VALUE!. */\n number(index: number, fallback?: number): number | string;\n /** Argument as an integer (truncated toward zero). */\n integer(index: number, fallback?: number): number | string;\n /** Argument as text (or an error string). */\n text(index: number, fallback?: string): string;\n /** Argument as a boolean (or an error string). */\n boolean(index: number, fallback?: boolean): boolean | string;\n /** Argument as a date (or an error string). */\n date(index: number): Date | string;\n /** Argument as a 2D range (or an error string if it is not a range). */\n range(index: number): FormulaRange | string;\n /** Every value of the arguments `from` (default 0) up to `to` (exclusive, default all), ranges flattened. */\n flatValues(from?: number, to?: number): any[];\n /**\n * Numbers among the arguments `from`..`to` (exclusive), as SUM reads them:\n * text, booleans and blanks are skipped; the first error is returned instead.\n */\n numbers(from?: number, to?: number): number[] | string;\n}\n\nexport interface FormulaFunctionDefinition {\n /** Function name; matched case-insensitively. */\n name: string;\n category: FormulaFunctionCategory;\n description: string;\n args: FormulaArgumentSpec[];\n /** Result changes on every recalculation (RAND, NOW, TODAY). */\n volatile?: boolean;\n /**\n * A repeating group may end with one extra argument (SWITCH's `default`).\n * Otherwise a group must be complete.\n */\n allowTrailingArgument?: boolean;\n /**\n * A scalar function that is applied element by element when given arrays\n * (dynamic arrays): `=LEN(A1:A3)` returns three lengths. `true` lifts when\n * any argument is a multi-value array; a list of argument indices lifts\n * only when one of those is (IF lifts on its condition). Arguments typed\n * `range` are always passed whole.\n */\n elementwise?: boolean | readonly number[];\n evaluate(ctx: FormulaFunctionContext): any;\n}\n\n// --- Registry ---\n\nconst registry = new Map<string, FormulaFunctionDefinition>();\nlet builtinsLoaded = false;\n\nfunction ensureBuiltins(): void {\n if (builtinsLoaded) return;\n builtinsLoaded = true;\n for (const def of BUILTIN_FORMULA_FUNCTIONS) {\n if (!registry.has(def.name)) registry.set(def.name, def);\n }\n}\n\n/**\n * Adds (or replaces) a formula function. Replacing a built-in is allowed, so\n * apps can customise behaviour; the change is global.\n */\nexport function registerFormulaFunction(definition: FormulaFunctionDefinition): void {\n ensureBuiltins();\n const name = definition.name.trim().toUpperCase();\n if (!/^[A-Z_][A-Z0-9_.]*$/.test(name)) {\n throw new Error(`Invalid formula function name \"${definition.name}\"`);\n }\n registry.set(name, { ...definition, name });\n}\n\n/** Removes a formula function. Returns false if it was not registered. */\nexport function unregisterFormulaFunction(name: string): boolean {\n ensureBuiltins();\n return registry.delete(name.toUpperCase());\n}\n\nexport function getFormulaFunction(name: string): FormulaFunctionDefinition | undefined {\n ensureBuiltins();\n return registry.get(name.toUpperCase());\n}\n\n/** All registered functions sorted by name, optionally limited to one category. */\nexport function listFormulaFunctions(category?: FormulaFunctionCategory): FormulaFunctionDefinition[] {\n ensureBuiltins();\n return Array.from(registry.values())\n .filter((def) => !category || def.category === category)\n .sort((a, b) => a.name.localeCompare(b.name));\n}\n\n/**\n * Functions matching `query`, for autocomplete: names starting with the query\n * first, then names containing it. An empty query lists every function.\n */\nexport function searchFormulaFunctions(\n query: string,\n options: { limit?: number; category?: FormulaFunctionCategory } = {},\n): FormulaFunctionDefinition[] {\n const q = query.trim().toUpperCase();\n const all = listFormulaFunctions(options.category);\n const prefix = all.filter((def) => def.name.startsWith(q));\n const contains = q ? all.filter((def) => !def.name.startsWith(q) && def.name.includes(q)) : [];\n const results = [...prefix, ...contains];\n return options.limit !== undefined ? results.slice(0, options.limit) : results;\n}\n\n// --- Argument layout and signatures ---\n\ninterface ArgumentLayout {\n fixed: FormulaArgumentSpec[];\n group: FormulaArgumentSpec[];\n groupStart: number;\n}\n\nfunction layoutOf(def: FormulaFunctionDefinition): ArgumentLayout {\n const groupStart = def.args.findIndex((arg) => arg.repeating);\n if (groupStart === -1) return { fixed: def.args, group: [], groupStart: def.args.length };\n return { fixed: def.args.slice(0, groupStart), group: def.args.slice(groupStart), groupStart };\n}\n\n/**\n * Index into `def.args` of the spec describing the call argument at\n * `argIndex`, or -1 if the function takes no argument there.\n */\nexport function getFormulaArgumentSpecIndex(def: FormulaFunctionDefinition, argIndex: number): number {\n const { fixed, group, groupStart } = layoutOf(def);\n if (argIndex < fixed.length) return argIndex;\n if (group.length === 0) return -1;\n return groupStart + ((argIndex - groupStart) % group.length);\n}\n\n/**\n * Checks the number of arguments of a call. Returns a message such as\n * `SUMIF needs at least 2 arguments, but 1 was given.`, or null when valid.\n */\nexport function checkFormulaArgumentCount(def: FormulaFunctionDefinition, count: number): string | null {\n const { fixed, group } = layoutOf(def);\n const requiredFixed = fixed.filter((arg) => !arg.optional).length;\n const groupRequired = group.length > 0 && !group[0].optional;\n const min = requiredFixed + (groupRequired ? group.length : 0);\n const max = group.length > 0 ? Infinity : fixed.length;\n const given = `${count} ${count === 1 ? 'was' : 'were'} given`;\n const plural = (n: number) => `${n} argument${n === 1 ? '' : 's'}`;\n\n if (count < min) {\n const expected = min === max ? plural(min) : `at least ${plural(min)}`;\n return `${def.name} needs ${expected}, but ${given}.`;\n }\n if (count > max) {\n const expected = min === max ? plural(max) : `at most ${plural(max)}`;\n return max === 0 ? `${def.name} takes no arguments, but ${given}.` : `${def.name} takes ${expected}, but ${given}.`;\n }\n if (group.length > 1 && count > fixed.length) {\n const extra = (count - fixed.length) % group.length;\n if (extra !== 0 && !(def.allowTrailingArgument && extra === 1)) {\n const names = group.map((arg) => arg.name).join(', ');\n return `${def.name} expects its repeating arguments (${names}) in complete groups.`;\n }\n }\n return null;\n}\n\n/** A piece of a rendered signature; `specIndex` is set on argument names. */\nexport interface FormulaSignaturePart {\n text: string;\n specIndex: number | null;\n active: boolean;\n}\n\n/**\n * Renders a signature as parts, e.g. `SUMIFS(` `sum_range` `, ` ... `)`, with\n * the argument at call position `activeArgIndex` (if any) marked active. A\n * repeating group is shown twice (`value1, [value2, ...]`); arguments past\n * the first repetition highlight the second one.\n */\nexport function getFormulaSignatureParts(\n nameOrDef: string | FormulaFunctionDefinition,\n activeArgIndex?: number | null,\n): FormulaSignaturePart[] {\n const def = typeof nameOrDef === 'string' ? getFormulaFunction(nameOrDef) : nameOrDef;\n if (!def) return [];\n const { fixed, group, groupStart } = layoutOf(def);\n\n let activeSpec = -1;\n let activeRepeat = 0;\n if (activeArgIndex !== undefined && activeArgIndex !== null && activeArgIndex >= 0) {\n activeSpec = getFormulaArgumentSpecIndex(def, activeArgIndex);\n if (activeSpec >= groupStart && group.length > 0) {\n activeRepeat = Math.floor((activeArgIndex - groupStart) / group.length) > 0 ? 1 : 0;\n }\n }\n\n const parts: FormulaSignaturePart[] = [{ text: `${def.name}(`, specIndex: null, active: false }];\n const pushArg = (text: string, specIndex: number, repeat = 0) => {\n if (parts.length > 1) parts.push({ text: ', ', specIndex: null, active: false });\n parts.push({ text, specIndex, active: specIndex === activeSpec && repeat === activeRepeat });\n };\n\n fixed.forEach((arg, i) => pushArg(arg.optional ? `[${arg.name}]` : arg.name, i));\n\n if (group.length > 0) {\n const groupOptional = !!group[0].optional;\n group.forEach((arg, i) => {\n const open = groupOptional && i === 0 ? '[' : '';\n pushArg(`${open}${arg.name}1`, groupStart + i, 0);\n });\n group.forEach((arg, i) => {\n const open = !groupOptional && i === 0 ? '[' : '';\n pushArg(`${open}${arg.name}2`, groupStart + i, 1);\n });\n parts.push({ text: ', ...]', specIndex: null, active: false });\n }\n\n parts.push({ text: ')', specIndex: null, active: false });\n return parts;\n}\n\n/** Signature string such as `SUMIF(range, criteria, [sum_range])`. */\nexport function getFormulaSignature(nameOrDef: string | FormulaFunctionDefinition): string {\n return getFormulaSignatureParts(nameOrDef)\n .map((part) => part.text)\n .join('');\n}\n","import { ASTNode, parseFormula, type FunctionCallNode, type RangeNode, type SpillRefNode } from './parser';\nimport {\n checkFormulaArgumentCount,\n getFormulaArgumentSpecIndex,\n getFormulaFunction,\n type FormulaArgument,\n type FormulaFunctionContext,\n type FormulaFunctionDefinition,\n} from './registry';\nimport {\n colNameToIndex,\n isBlank,\n isFormulaError,\n numericValue,\n toBoolean,\n toDate,\n toFormulaRange,\n toNumber,\n toText,\n compareValues,\n withRangeShape,\n parseIsoDate,\n isoDateHasTime,\n dateToSerial,\n serialToDate,\n formatIsoDate,\n formatIsoDateTime,\n cleanFloat,\n type FormulaErrorCode,\n type FormulaRange,\n} from './values';\n\nexport { FORMULA_ERRORS, isFormulaError, colNameToIndex, indexToColName } from './values';\nexport type { FormulaErrorCode, FormulaRange } from './values';\n\nexport interface EvaluationContext {\n resolveCell?: (col: string, row: number) => any;\n /**\n * Values of a rectangular range, column by column (all rows of the first\n * column, then the next column). Lookup functions rely on this order.\n */\n resolveRange?: (startCol: string, startRow: number, endCol: string, endRow: number) => any[];\n resolveProperty?: (property: string) => any;\n /**\n * The spilled array anchored at a cell, for `A1#` references: its shape and\n * values (column-major, like `resolveRange`), or an error code (e.g.\n * `#REF!` when the cell holds no array).\n */\n resolveSpill?: (col: string, row: number) => FormulaRange | string;\n /**\n * The cell holding the formula being evaluated, used by the implicit\n * intersection operator (`=@A:A` reads the cell in the formula's row).\n */\n getFormulaCell?: () => { col: string; row: number } | null;\n}\n\n/** A human-readable reason for an error value. */\nexport interface FormulaDiagnostic {\n code: FormulaErrorCode;\n message: string;\n}\n\nexport interface FormulaEvaluationResult {\n /** The computed value; an error code such as `#VALUE!` when the formula fails. */\n value: any;\n /** The error code when `value` is an error. */\n error?: FormulaErrorCode;\n /** Why the formula failed, e.g. `Argument 2 of SUMIF must be a range.` */\n message?: string;\n /**\n * Set when the formula returned an array of more than one value (a\n * dynamic array). `value` is then its top-left value; a spreadsheet spills\n * the whole array into the cells below and to the right.\n */\n array?: FormulaRange;\n}\n\nconst DEFAULT_MESSAGES: Record<FormulaErrorCode, string> = {\n '#DIV/0!': 'The formula divides by zero.',\n '#VALUE!': 'A value in the formula has the wrong type.',\n '#REF!': 'The formula refers to a cell or range that was deleted.',\n '#NAME?': 'The formula contains an unknown name.',\n '#N/A': 'A value is not available.',\n '#NUM!': 'The formula produced an invalid number.',\n '#CYCLE!': 'This formula is part of a circular reference.',\n '#ERROR!': 'The formula could not be evaluated.',\n '#SPILL!': \"The formula's array result can't spill into the cells it needs.\",\n '#CALC!': 'The formula produced an empty array.',\n};\n\n/** Generic explanation of an error code, used when no specific reason was recorded. */\nexport function describeFormulaError(code: string): string {\n return DEFAULT_MESSAGES[code as FormulaErrorCode] ?? 'The formula could not be evaluated.';\n}\n\nconst quote = (value: unknown) => (typeof value === 'string' ? `\"${value}\"` : String(value));\n\n/** Largest array a formula may build (SEQUENCE, RANDARRAY, broadcasting). */\nexport const MAX_FORMULA_ARRAY_SIZE = 1_000_000;\n\n/** The (rows, cols) shape of a value: 1x1 for scalars. */\nfunction shapeOf(value: unknown): { rows: number; cols: number } {\n if (!Array.isArray(value)) return { rows: 1, cols: 1 };\n const range = toFormulaRange(value);\n return { rows: range.rows, cols: range.cols };\n}\n\n/**\n * Element (row, col) of a value broadcast to a larger shape: scalars and\n * single rows/columns repeat; positions past a smaller array are #N/A.\n */\nfunction broadcastGet(value: unknown, row: number, col: number): any {\n if (!Array.isArray(value)) return value;\n const range = toFormulaRange(value);\n const r = range.rows === 1 ? 0 : row;\n const c = range.cols === 1 ? 0 : col;\n if (r >= range.rows || c >= range.cols) return '#N/A';\n return range.values[c * range.rows + r];\n}\n\n/** Builds a shaped, column-major array by calling `fn` for every (row, col). */\nfunction buildArray(rows: number, cols: number, fn: (row: number, col: number) => any): any[] {\n const out: any[] = [];\n for (let c = 0; c < cols; c++) {\n for (let r = 0; r < rows; r++) out.push(fn(r, c));\n }\n return withRangeShape(out, rows, cols);\n}\n\n/** A multi-value array (one-value arrays behave like scalars). */\nconst isMultiArray = (value: unknown): value is any[] => Array.isArray(value) && value.length > 1;\n\nconst firstValue = (value: any): any => (Array.isArray(value) ? (value.length ? value[0] : '') : value);\n\nexport class FormulaEvaluator {\n private context: EvaluationContext;\n private diagnostics: FormulaDiagnostic[] = [];\n // LET variables, innermost scope last.\n private scopes: Array<Map<string, any>> = [];\n\n constructor(context: EvaluationContext = {}) {\n this.context = context;\n }\n\n /**\n * Evaluates a formula. Errors in the formula are returned as error values\n * (`#VALUE!` etc.); malformed formulas and missing resolvers throw.\n */\n public evaluate(astOrFormula: ASTNode | string): any {\n const ast = typeof astOrFormula === 'string' ? parseFormula(astOrFormula) : astOrFormula;\n return this.evaluateNode(ast);\n }\n\n /**\n * Evaluates a formula and explains errors. Never throws: malformed formulas\n * return `#ERROR!` with the parser's message. A result with several values\n * (a range or a dynamic array) is returned in `array`, with its top-left\n * value as `value`; a one-value array is just that value.\n */\n public evaluateWithDiagnostics(astOrFormula: ASTNode | string): FormulaEvaluationResult {\n const saved = this.diagnostics;\n const savedScopes = this.scopes;\n this.diagnostics = [];\n this.scopes = [];\n try {\n let value = this.evaluate(astOrFormula);\n let array: FormulaRange | undefined;\n if (Array.isArray(value)) {\n if (value.length > 1) array = toFormulaRange(value);\n value = firstValue(value);\n }\n if (isFormulaError(value)) {\n const code = value as FormulaErrorCode;\n const diagnostic = this.diagnostics.find((d) => d.code === code);\n const result: FormulaEvaluationResult = {\n value,\n error: code,\n message: diagnostic?.message ?? describeFormulaError(code),\n };\n if (array) result.array = array;\n return result;\n }\n return array ? { value, array } : { value };\n } catch (err: any) {\n return { value: '#ERROR!', error: '#ERROR!', message: err?.message || 'Formula error' };\n } finally {\n this.diagnostics = saved;\n this.scopes = savedScopes;\n }\n }\n\n /** Records why an error happened and returns the error code. */\n private fail(code: FormulaErrorCode, message: string): string {\n this.diagnostics.push({ code, message });\n return code;\n }\n\n private evaluateNode(node: ASTNode): any {\n switch (node.type) {\n case 'Literal':\n if (node.value === '#REF!') return this.fail('#REF!', 'This formula refers to a cell or range that was deleted.');\n if (node.value === '#NAME?') return this.fail('#NAME?', 'This formula contains an unknown name.');\n return node.value;\n\n case 'CellRef': {\n if (!this.context.resolveCell) {\n throw new Error(`Cannot resolve cell reference ${node.ref}: no cell resolver provided`);\n }\n const value = this.context.resolveCell(node.col, node.row);\n if (isFormulaError(value)) {\n this.fail(\n value as FormulaErrorCode,\n value === '#CYCLE!'\n ? `Cell ${node.ref} is part of a circular reference.`\n : `Cell ${node.ref} contains an error (${value}).`,\n );\n }\n return value;\n }\n\n case 'Range':\n return this.evaluateRange(node);\n\n case 'SpillRef':\n return this.evaluateSpillRef(node);\n\n case 'Name': {\n for (let i = this.scopes.length - 1; i >= 0; i--) {\n if (this.scopes[i].has(node.name)) return this.scopes[i].get(node.name);\n }\n return this.fail('#NAME?', 'This formula contains an unknown name.');\n }\n\n case 'PropertyRef': {\n if (!this.context.resolveProperty) {\n throw new Error(`Cannot resolve property [${node.property}]: no property resolver provided`);\n }\n return this.context.resolveProperty(node.property);\n }\n\n case 'UnaryOp': {\n if (node.op === '@') return this.implicitIntersection(node.argument);\n const raw = this.evaluateNode(node.argument);\n if (isMultiArray(raw)) {\n const { rows, cols } = shapeOf(raw);\n return buildArray(rows, cols, (r, c) => this.unary(node.op, broadcastGet(raw, r, c)));\n }\n return this.unary(node.op, firstValue(raw));\n }\n\n case 'BinaryOp': {\n const left = this.evaluateNode(node.left);\n if (isFormulaError(left)) return left;\n const right = this.evaluateNode(node.right);\n if (isFormulaError(right)) return right;\n if (isMultiArray(left) || isMultiArray(right)) {\n // Array arithmetic: operate element by element (=A1:A3*2).\n const a = shapeOf(left);\n const b = shapeOf(right);\n const rows = Math.max(a.rows, b.rows);\n const cols = Math.max(a.cols, b.cols);\n if (rows * cols > MAX_FORMULA_ARRAY_SIZE) return this.fail('#NUM!', 'The array result is too large.');\n return buildArray(rows, cols, (r, c) => {\n const l = broadcastGet(left, r, c);\n if (isFormulaError(l)) return l;\n const rv = broadcastGet(right, r, c);\n if (isFormulaError(rv)) return rv;\n return this.binary(node.op, l, rv);\n });\n }\n const l = firstValue(left);\n if (isFormulaError(l)) return l;\n const r = firstValue(right);\n if (isFormulaError(r)) return r;\n return this.binary(node.op, l, r);\n }\n\n case 'FunctionCall':\n return this.callFunction(node);\n\n default:\n throw new Error(`Unknown AST node: ${(node as any).type}`);\n }\n }\n\n private unary(op: string, raw: any): any {\n const val = toNumber(raw);\n if (isFormulaError(val)) {\n return isFormulaError(raw) ? raw : this.fail('#VALUE!', `The ${op} operator can't use text (${quote(raw)}).`);\n }\n if (op === '-') return -(val as number);\n if (op === '+') return val;\n throw new Error(`Unsupported unary operator: ${op}`);\n }\n\n /** A binary operator on two single values. */\n private binary(op: string, left: any, right: any): any {\n if (['+', '-', '*', '/', '^'].includes(op)) {\n if (op === '+' || op === '-') {\n const leftDate = parseIsoDate(left);\n const rightDate = parseIsoDate(right);\n if (leftDate || rightDate) return this.dateArithmetic(op, left, right, leftDate, rightDate);\n }\n const a = toNumber(left);\n if (isFormulaError(a)) return this.fail('#VALUE!', `The ${op} operator can't use text (${quote(left)}).`);\n const b = toNumber(right);\n if (isFormulaError(b)) return this.fail('#VALUE!', `The ${op} operator can't use text (${quote(right)}).`);\n const x = a as number;\n const y = b as number;\n let result: number;\n switch (op) {\n case '+':\n result = x + y;\n break;\n case '-':\n result = x - y;\n break;\n case '*':\n result = x * y;\n break;\n case '/':\n if (y === 0) return this.fail('#DIV/0!', 'The formula divides by zero.');\n result = x / y;\n break;\n default:\n result = Math.pow(x, y);\n }\n return Number.isFinite(result) ? result : this.fail('#NUM!', 'The result is too large or not a number.');\n }\n\n switch (op) {\n case '&':\n return toText(left) + toText(right);\n case '=':\n return compareValues(left, right) === 0;\n case '<>':\n return compareValues(left, right) !== 0;\n case '<':\n return compareValues(left, right) < 0;\n case '<=':\n return compareValues(left, right) <= 0;\n case '>':\n return compareValues(left, right) > 0;\n case '>=':\n return compareValues(left, right) >= 0;\n default:\n throw new Error(`Unsupported binary operator: ${op}`);\n }\n }\n\n /**\n * Dates are ISO text: date +/- days is a date, date - date is a number of\n * days (fractional when times are involved).\n */\n private dateArithmetic(op: '+' | '-', left: any, right: any, leftDate: Date | null, rightDate: Date | null): any {\n if (leftDate && rightDate) {\n if (op === '+') return this.fail('#VALUE!', \"Two dates can't be added; subtract them to count the days between.\");\n return cleanFloat(dateToSerial(leftDate) - dateToSerial(rightDate));\n }\n if (!leftDate && op === '-') {\n return this.fail('#VALUE!', `A date (${quote(right)}) can't be subtracted from a number.`);\n }\n const date = (leftDate ?? rightDate)!;\n const dateText = String(leftDate ? left : right);\n const other = leftDate ? right : left;\n const days = toNumber(other);\n if (isFormulaError(days)) return this.fail('#VALUE!', `The ${op} operator can't use text (${quote(other)}).`);\n const n = days as number;\n const result = serialToDate(dateToSerial(date) + (op === '+' ? n : -n));\n if (!result || result.getFullYear() < 1 || result.getFullYear() > 9999) {\n return this.fail('#NUM!', 'The resulting date is out of range.');\n }\n return isoDateHasTime(dateText) || !Number.isInteger(n) ? formatIsoDateTime(result) : formatIsoDate(result);\n }\n\n /**\n * `@` (implicit intersection): a range reference reads the cell in the\n * formula's row (one column) or column (one row); arrays give their\n * top-left value.\n */\n private implicitIntersection(argument: ASTNode): any {\n const value = this.evaluateNode(argument);\n if (!Array.isArray(value)) return value;\n if (value.length <= 1) return firstValue(value);\n const cell = this.context.getFormulaCell?.();\n if (argument.type === 'Range' && cell) {\n const range = toFormulaRange(value);\n const top = Math.min(argument.startRow, argument.endRow);\n const left = Math.min(colNameToIndex(argument.startCol), colNameToIndex(argument.endCol));\n if (range.cols === 1) {\n const r = cell.row - top;\n if (r >= 0 && r < range.rows) return range.values[r];\n } else if (range.rows === 1) {\n const c = colNameToIndex(cell.col) - left;\n if (c >= 0 && c < range.cols) return range.values[c];\n }\n return this.fail('#VALUE!', `The @ operator found no cell of ${argument.raw} in the formula's row or column.`);\n }\n return value[0];\n }\n\n private evaluateSpillRef(node: SpillRefNode): any {\n if (!this.context.resolveSpill) {\n throw new Error(`Cannot resolve spill range ${node.ref}#: no spill resolver provided`);\n }\n const spill = this.context.resolveSpill(node.col, node.row);\n if (typeof spill === 'string' || !spill) {\n const code = (isFormulaError(spill) ? spill : '#REF!') as FormulaErrorCode;\n return this.fail(\n code,\n code === '#REF!'\n ? `${node.ref}# refers to ${node.ref}, which doesn't hold a spilled array.`\n : `The spill range ${node.ref}# isn't available (${code}).`,\n );\n }\n const values = withRangeShape(spill.values.slice(), spill.rows, spill.cols);\n const error = values.find(isFormulaError);\n if (error) this.fail(error as FormulaErrorCode, `The spill range ${node.ref}# contains an error (${error}).`);\n return values;\n }\n\n private evaluateRange(node: RangeNode): any[] {\n if (!this.context.resolveRange) {\n throw new Error(`Cannot resolve range ${node.raw}: no range resolver provided`);\n }\n const values = this.context.resolveRange(node.startCol, node.startRow, node.endCol, node.endRow);\n if (!Array.isArray(values)) return values;\n // Record the 2D shape so lookups can address rows and columns.\n const cols = Math.abs(colNameToIndex(node.endCol) - colNameToIndex(node.startCol)) + 1;\n if (values.length % cols === 0) withRangeShape(values, values.length / cols, cols);\n const error = values.find(isFormulaError);\n if (error) this.fail(error as FormulaErrorCode, `The range ${node.raw} contains an error (${error}).`);\n return values;\n }\n\n private callFunction(node: FunctionCallNode): any {\n const def = getFormulaFunction(node.name);\n if (!def) return this.fail('#NAME?', `Unknown function \"${node.name.toUpperCase()}\".`);\n\n const countError = checkFormulaArgumentCount(def, node.args.length);\n if (countError) return this.fail('#ERROR!', countError);\n\n if (def.name === 'LET') return this.evaluateLet(node);\n\n const args = this.lazyArguments(node.args);\n if (def.elementwise) {\n const lifted = this.liftOverArrays(def, args);\n if (lifted !== undefined) return lifted;\n }\n return this.finishCall(def, def.evaluate(this.createContext(def, args)));\n }\n\n private finishCall(def: FormulaFunctionDefinition, result: any): any {\n if (typeof result === 'number' && !Number.isFinite(result)) {\n return this.fail('#NUM!', `${def.name} produced an invalid number.`);\n }\n return result === undefined || result === null ? '' : result;\n }\n\n private lazyArguments(nodes: ASTNode[]): FormulaArgument[] {\n const cache = new Map<number, any>();\n return nodes.map((node, index) => ({\n index,\n node,\n value: () => {\n if (!cache.has(index)) cache.set(index, this.evaluateNode(node));\n return cache.get(index);\n },\n }));\n }\n\n /**\n * Applies a scalar function element by element when it receives arrays\n * (`=LEN(A1:A3)`, `=IF(A1:A3>1, \"y\", \"n\")`). Arguments typed `range` are\n * passed whole. Returns undefined when no trigger argument is a\n * multi-value array.\n */\n private liftOverArrays(def: FormulaFunctionDefinition, args: FormulaArgument[]): any {\n const liftable = (index: number) => def.args[getFormulaArgumentSpecIndex(def, index)]?.type !== 'range';\n const triggers = def.elementwise === true ? args.map((a) => a.index) : (def.elementwise as readonly number[]);\n if (!triggers.some((i) => i < args.length && liftable(i) && isMultiArray(args[i].value()))) return undefined;\n\n let rows = 1;\n let cols = 1;\n for (const a of args) {\n if (!liftable(a.index)) continue;\n const shape = shapeOf(a.value());\n rows = Math.max(rows, shape.rows);\n cols = Math.max(cols, shape.cols);\n }\n if (rows * cols > MAX_FORMULA_ARRAY_SIZE) return this.fail('#NUM!', 'The array result is too large.');\n return buildArray(rows, cols, (r, c) => {\n const elementArgs: FormulaArgument[] = args.map((a) => {\n if (!liftable(a.index)) return a;\n const element = broadcastGet(a.value(), r, c);\n return { index: a.index, node: a.node, value: () => element };\n });\n return firstValue(this.finishCall(def, def.evaluate(this.createContext(def, elementArgs))));\n });\n }\n\n /** LET(name1, value1, [name2, value2, ...], calculation). */\n private evaluateLet(node: FunctionCallNode): any {\n if (node.args.length % 2 === 0) {\n return this.fail('#ERROR!', 'LET needs name/value pairs followed by a calculation.');\n }\n const scope = new Map<string, any>();\n this.scopes.push(scope);\n try {\n for (let i = 0; i < node.args.length - 1; i += 2) {\n const nameNode = node.args[i];\n if (nameNode.type !== 'Name') {\n return this.fail('#ERROR!', `Argument ${i + 1} of LET must be a name (names can't look like cell references).`);\n }\n scope.set(nameNode.name, this.evaluateNode(node.args[i + 1]));\n }\n return this.evaluateNode(node.args[node.args.length - 1]);\n } finally {\n this.scopes.pop();\n }\n }\n\n private createContext(def: FormulaFunctionDefinition, args: FormulaArgument[]): FormulaFunctionContext {\n\n const has = (index: number) => index >= 0 && index < args.length;\n const value = (index: number) => (has(index) ? args[index].value() : '');\n const argError = (index: number, code: FormulaErrorCode, reason: string) =>\n this.fail(code, `Argument ${index + 1} of ${def.name} ${reason}`);\n\n const scalar = (index: number) => {\n const v = value(index);\n if (!Array.isArray(v)) return v;\n if (v.length <= 1) return v.length === 1 ? v[0] : '';\n return argError(index, '#VALUE!', 'must be a single value, not a range.');\n };\n\n const number = (index: number, fallback?: number): number | string => {\n if (!has(index) && fallback !== undefined) return fallback;\n const v = scalar(index);\n if (isFormulaError(v)) return v;\n const num = toNumber(v);\n return isFormulaError(num) ? argError(index, '#VALUE!', `can't be text (${quote(v)}).`) : num;\n };\n\n const flatValues = (from = 0, to = args.length): any[] => {\n const items: any[] = [];\n for (let i = from; i < Math.min(to, args.length); i++) {\n const v = args[i].value();\n if (Array.isArray(v)) {\n // No spread: push(...v) overflows the stack for large ranges.\n for (const item of v) items.push(item);\n } else {\n items.push(v);\n }\n }\n return items;\n };\n\n return {\n name: def.name,\n args,\n error: (code, message) => this.fail(code, message ?? describeFormulaError(code)),\n argError,\n has,\n value,\n scalar,\n number,\n integer: (index, fallback) => {\n const num = number(index, fallback);\n return isFormulaError(num) ? num : Math.trunc(num as number);\n },\n text: (index, fallback) => {\n if (!has(index) && fallback !== undefined) return fallback;\n const v = scalar(index);\n return isFormulaError(v) ? v : toText(v);\n },\n boolean: (index, fallback) => {\n if (!has(index) && fallback !== undefined) return fallback;\n const v = scalar(index);\n if (isFormulaError(v)) return v;\n const bool = toBoolean(v);\n return isFormulaError(bool) ? argError(index, '#VALUE!', `must be TRUE or FALSE, not text (${quote(v)}).`) : bool;\n },\n date: (index) => {\n const v = scalar(index);\n if (isFormulaError(v)) return v;\n if (isBlank(v)) return argError(index, '#VALUE!', 'is empty, but a date is needed.');\n return toDate(v) ?? argError(index, '#VALUE!', `(${quote(v)}) isn't a valid date.`);\n },\n range: (index) => {\n const v = value(index);\n if (Array.isArray(v)) return toFormulaRange(v);\n if (isFormulaError(v)) return v;\n return argError(index, '#VALUE!', 'must be a range.');\n },\n flatValues,\n numbers: (from, to) => {\n // Numbers among the arguments (text and blanks are skipped, as in SUM);\n // the first error found is returned instead.\n const nums: number[] = [];\n for (const item of flatValues(from, to)) {\n if (isFormulaError(item)) return item;\n if (isBlank(item) || typeof item === 'boolean') continue;\n const num = typeof item === 'number' ? item : numericValue(item);\n if (num !== null) nums.push(num);\n }\n return nums;\n },\n };\n }\n}\n"]}