@cj-tech-master/excelts 9.2.1 → 9.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (383) hide show
  1. package/README.md +25 -2
  2. package/README_zh.md +29 -6
  3. package/dist/browser/index.browser.d.ts +1 -1
  4. package/dist/browser/index.browser.js +4 -0
  5. package/dist/browser/index.d.ts +1 -1
  6. package/dist/browser/index.js +4 -0
  7. package/dist/browser/modules/excel/cell.d.ts +17 -3
  8. package/dist/browser/modules/excel/cell.js +170 -22
  9. package/dist/browser/modules/excel/defined-names.d.ts +96 -1
  10. package/dist/browser/modules/excel/defined-names.js +411 -21
  11. package/dist/browser/modules/excel/image.d.ts +11 -0
  12. package/dist/browser/modules/excel/image.js +24 -1
  13. package/dist/browser/modules/excel/stream/workbook-reader.browser.d.ts +9 -3
  14. package/dist/browser/modules/excel/stream/workbook-reader.browser.js +14 -0
  15. package/dist/browser/modules/excel/stream/workbook-reader.d.ts +2 -1
  16. package/dist/browser/modules/excel/stream/workbook-writer.browser.d.ts +39 -5
  17. package/dist/browser/modules/excel/stream/workbook-writer.browser.js +48 -1
  18. package/dist/browser/modules/excel/stream/workbook-writer.d.ts +3 -2
  19. package/dist/browser/modules/excel/stream/worksheet-reader.js +17 -1
  20. package/dist/browser/modules/excel/stream/worksheet-writer.d.ts +39 -6
  21. package/dist/browser/modules/excel/stream/worksheet-writer.js +45 -5
  22. package/dist/browser/modules/excel/table.js +15 -2
  23. package/dist/browser/modules/excel/types.d.ts +133 -2
  24. package/dist/browser/modules/excel/utils/col-cache.d.ts +1 -0
  25. package/dist/browser/modules/excel/utils/col-cache.js +15 -0
  26. package/dist/browser/modules/excel/utils/drawing-utils.d.ts +3 -3
  27. package/dist/browser/modules/excel/utils/drawing-utils.js +4 -0
  28. package/dist/browser/modules/excel/utils/external-link-formula.d.ts +76 -0
  29. package/dist/browser/modules/excel/utils/external-link-formula.js +208 -0
  30. package/dist/browser/modules/excel/utils/iterate-stream.d.ts +9 -3
  31. package/dist/browser/modules/excel/utils/iterate-stream.js +3 -1
  32. package/dist/browser/modules/excel/utils/ooxml-paths.d.ts +19 -0
  33. package/dist/browser/modules/excel/utils/ooxml-paths.js +37 -2
  34. package/dist/browser/modules/excel/utils/shared-strings.d.ts +8 -3
  35. package/dist/browser/modules/excel/utils/shared-strings.js +21 -2
  36. package/dist/browser/modules/excel/utils/workbook-protection.d.ts +30 -0
  37. package/dist/browser/modules/excel/utils/workbook-protection.js +30 -0
  38. package/dist/browser/modules/excel/workbook.browser.d.ts +257 -6
  39. package/dist/browser/modules/excel/workbook.browser.js +318 -34
  40. package/dist/browser/modules/excel/workbook.d.ts +1 -1
  41. package/dist/browser/modules/excel/worksheet.d.ts +3 -1
  42. package/dist/browser/modules/excel/worksheet.js +21 -2
  43. package/dist/browser/modules/excel/xlsx/rel-type.d.ts +15 -0
  44. package/dist/browser/modules/excel/xlsx/rel-type.js +16 -1
  45. package/dist/browser/modules/excel/xlsx/xform/book/defined-name-xform.d.ts +6 -5
  46. package/dist/browser/modules/excel/xlsx/xform/book/defined-name-xform.js +21 -86
  47. package/dist/browser/modules/excel/xlsx/xform/book/external-link-xform.d.ts +84 -0
  48. package/dist/browser/modules/excel/xlsx/xform/book/external-link-xform.js +330 -0
  49. package/dist/browser/modules/excel/xlsx/xform/book/external-reference-xform.d.ts +17 -0
  50. package/dist/browser/modules/excel/xlsx/xform/book/external-reference-xform.js +24 -0
  51. package/dist/browser/modules/excel/xlsx/xform/book/workbook-calc-properties-xform.d.ts +3 -0
  52. package/dist/browser/modules/excel/xlsx/xform/book/workbook-calc-properties-xform.js +11 -2
  53. package/dist/browser/modules/excel/xlsx/xform/book/workbook-protection-xform.d.ts +20 -0
  54. package/dist/browser/modules/excel/xlsx/xform/book/workbook-protection-xform.js +66 -0
  55. package/dist/browser/modules/excel/xlsx/xform/book/workbook-xform.js +38 -5
  56. package/dist/browser/modules/excel/xlsx/xform/core/content-types-xform.js +19 -1
  57. package/dist/browser/modules/excel/xlsx/xform/core/metadata-xform.d.ts +56 -0
  58. package/dist/browser/modules/excel/xlsx/xform/core/metadata-xform.js +158 -0
  59. package/dist/browser/modules/excel/xlsx/xform/drawing/absolute-anchor-xform.d.ts +26 -0
  60. package/dist/browser/modules/excel/xlsx/xform/drawing/absolute-anchor-xform.js +105 -0
  61. package/dist/browser/modules/excel/xlsx/xform/drawing/base-cell-anchor-xform.js +3 -0
  62. package/dist/browser/modules/excel/xlsx/xform/drawing/drawing-xform.js +10 -2
  63. package/dist/browser/modules/excel/xlsx/xform/sheet/cell-xform.d.ts +1 -1
  64. package/dist/browser/modules/excel/xlsx/xform/sheet/cell-xform.js +166 -8
  65. package/dist/browser/modules/excel/xlsx/xform/sheet/data-validations-xform.js +1 -1
  66. package/dist/browser/modules/excel/xlsx/xform/sheet/ignored-errors-xform.d.ts +21 -0
  67. package/dist/browser/modules/excel/xlsx/xform/sheet/ignored-errors-xform.js +80 -0
  68. package/dist/browser/modules/excel/xlsx/xform/sheet/worksheet-xform.js +9 -4
  69. package/dist/browser/modules/excel/xlsx/xform/style/border-xform.js +4 -1
  70. package/dist/browser/modules/excel/xlsx/xlsx.browser.d.ts +172 -13
  71. package/dist/browser/modules/excel/xlsx/xlsx.browser.js +410 -20
  72. package/dist/browser/modules/excel/xlsx/xlsx.d.ts +7 -4
  73. package/dist/browser/modules/excel/xlsx/xlsx.js +4 -5
  74. package/dist/browser/modules/formula/compile/address-utils.d.ts +62 -0
  75. package/dist/browser/modules/formula/compile/address-utils.js +83 -0
  76. package/dist/browser/modules/formula/compile/binder.d.ts +42 -0
  77. package/dist/browser/modules/formula/compile/binder.js +487 -0
  78. package/dist/browser/modules/formula/compile/bound-ast.d.ts +230 -0
  79. package/dist/browser/modules/formula/compile/bound-ast.js +80 -0
  80. package/dist/browser/modules/formula/compile/compiled-formula.d.ts +137 -0
  81. package/dist/browser/modules/formula/compile/compiled-formula.js +383 -0
  82. package/dist/browser/modules/formula/compile/dependency-analysis.d.ts +93 -0
  83. package/dist/browser/modules/formula/compile/dependency-analysis.js +432 -0
  84. package/dist/browser/modules/formula/compile/structured-ref-utils.d.ts +93 -0
  85. package/dist/browser/modules/formula/compile/structured-ref-utils.js +136 -0
  86. package/dist/browser/modules/formula/default-syntax-probe.d.ts +79 -0
  87. package/dist/browser/modules/formula/default-syntax-probe.js +83 -0
  88. package/dist/browser/modules/formula/functions/_date-context.d.ts +4 -0
  89. package/dist/browser/modules/formula/functions/_date-context.js +29 -0
  90. package/dist/browser/modules/formula/functions/_shared.d.ts +121 -0
  91. package/dist/browser/modules/formula/functions/_shared.js +381 -0
  92. package/dist/browser/modules/formula/functions/conditional.d.ts +27 -0
  93. package/dist/browser/modules/formula/functions/conditional.js +343 -0
  94. package/dist/browser/modules/formula/functions/database.d.ts +37 -0
  95. package/dist/browser/modules/formula/functions/database.js +274 -0
  96. package/dist/browser/modules/formula/functions/date.d.ts +61 -0
  97. package/dist/browser/modules/formula/functions/date.js +855 -0
  98. package/dist/browser/modules/formula/functions/dynamic-array.d.ts +23 -0
  99. package/dist/browser/modules/formula/functions/dynamic-array.js +860 -0
  100. package/dist/browser/modules/formula/functions/engineering.d.ts +57 -0
  101. package/dist/browser/modules/formula/functions/engineering.js +1128 -0
  102. package/dist/browser/modules/formula/functions/financial.d.ts +202 -0
  103. package/dist/browser/modules/formula/functions/financial.js +2296 -0
  104. package/dist/browser/modules/formula/functions/lookup.d.ts +18 -0
  105. package/dist/browser/modules/formula/functions/lookup.js +886 -0
  106. package/dist/browser/modules/formula/functions/math.d.ts +114 -0
  107. package/dist/browser/modules/formula/functions/math.js +1406 -0
  108. package/dist/browser/modules/formula/functions/statistical.d.ts +193 -0
  109. package/dist/browser/modules/formula/functions/statistical.js +3390 -0
  110. package/dist/browser/modules/formula/functions/text.d.ts +86 -0
  111. package/dist/browser/modules/formula/functions/text.js +1845 -0
  112. package/dist/browser/modules/formula/host-registry.d.ts +53 -0
  113. package/dist/browser/modules/formula/host-registry.js +69 -0
  114. package/dist/browser/modules/formula/index.d.ts +39 -0
  115. package/dist/browser/modules/formula/index.js +49 -0
  116. package/dist/browser/modules/formula/install.d.ts +62 -0
  117. package/dist/browser/modules/formula/install.js +88 -0
  118. package/dist/browser/modules/formula/integration/apply-writeback-plan.d.ts +26 -0
  119. package/dist/browser/modules/formula/integration/apply-writeback-plan.js +210 -0
  120. package/dist/browser/modules/formula/integration/calculate-formulas-impl.d.ts +30 -0
  121. package/dist/browser/modules/formula/integration/calculate-formulas-impl.js +616 -0
  122. package/dist/browser/modules/formula/integration/calculate-formulas.d.ts +67 -0
  123. package/dist/browser/modules/formula/integration/calculate-formulas.js +68 -0
  124. package/dist/browser/modules/formula/integration/formula-instance.d.ts +64 -0
  125. package/dist/browser/modules/formula/integration/formula-instance.js +79 -0
  126. package/dist/browser/modules/formula/integration/workbook-adapter.d.ts +26 -0
  127. package/dist/browser/modules/formula/integration/workbook-adapter.js +324 -0
  128. package/dist/browser/modules/formula/integration/workbook-snapshot.d.ts +267 -0
  129. package/dist/browser/modules/formula/integration/workbook-snapshot.js +77 -0
  130. package/dist/browser/modules/formula/materialize/build-writeback-plan.d.ts +34 -0
  131. package/dist/browser/modules/formula/materialize/build-writeback-plan.js +473 -0
  132. package/dist/browser/modules/formula/materialize/spill-engine.d.ts +9 -0
  133. package/dist/browser/modules/formula/materialize/spill-engine.js +38 -0
  134. package/dist/browser/modules/formula/materialize/types.d.ts +179 -0
  135. package/dist/browser/modules/formula/materialize/types.js +29 -0
  136. package/dist/browser/modules/formula/materialize/writeback-plan.d.ts +167 -0
  137. package/dist/browser/modules/formula/materialize/writeback-plan.js +27 -0
  138. package/dist/browser/modules/formula/runtime/evaluator.d.ts +151 -0
  139. package/dist/browser/modules/formula/runtime/evaluator.js +2291 -0
  140. package/dist/browser/modules/formula/runtime/function-registry.d.ts +47 -0
  141. package/dist/browser/modules/formula/runtime/function-registry.js +840 -0
  142. package/dist/browser/modules/formula/runtime/values.d.ts +211 -0
  143. package/dist/browser/modules/formula/runtime/values.js +385 -0
  144. package/dist/browser/modules/formula/syntax/ast.d.ts +129 -0
  145. package/dist/browser/modules/formula/syntax/ast.js +28 -0
  146. package/dist/browser/modules/formula/syntax/parser.d.ts +18 -0
  147. package/dist/browser/modules/formula/syntax/parser.js +439 -0
  148. package/dist/browser/modules/formula/syntax/token-types.d.ts +153 -0
  149. package/dist/browser/modules/formula/syntax/token-types.js +59 -0
  150. package/dist/browser/modules/formula/syntax/tokenizer.d.ts +10 -0
  151. package/dist/browser/modules/formula/syntax/tokenizer.js +1074 -0
  152. package/dist/browser/modules/pdf/excel-bridge.js +9 -0
  153. package/dist/cjs/index.js +4 -0
  154. package/dist/cjs/modules/excel/cell.js +170 -22
  155. package/dist/cjs/modules/excel/defined-names.js +411 -21
  156. package/dist/cjs/modules/excel/image.js +24 -1
  157. package/dist/cjs/modules/excel/stream/workbook-reader.browser.js +14 -0
  158. package/dist/cjs/modules/excel/stream/workbook-writer.browser.js +48 -1
  159. package/dist/cjs/modules/excel/stream/worksheet-reader.js +17 -1
  160. package/dist/cjs/modules/excel/stream/worksheet-writer.js +45 -5
  161. package/dist/cjs/modules/excel/table.js +15 -2
  162. package/dist/cjs/modules/excel/utils/col-cache.js +15 -0
  163. package/dist/cjs/modules/excel/utils/drawing-utils.js +4 -0
  164. package/dist/cjs/modules/excel/utils/external-link-formula.js +212 -0
  165. package/dist/cjs/modules/excel/utils/iterate-stream.js +3 -1
  166. package/dist/cjs/modules/excel/utils/ooxml-paths.js +42 -2
  167. package/dist/cjs/modules/excel/utils/shared-strings.js +21 -2
  168. package/dist/cjs/modules/excel/utils/workbook-protection.js +33 -0
  169. package/dist/cjs/modules/excel/workbook.browser.js +318 -34
  170. package/dist/cjs/modules/excel/worksheet.js +20 -1
  171. package/dist/cjs/modules/excel/xlsx/rel-type.js +16 -1
  172. package/dist/cjs/modules/excel/xlsx/xform/book/defined-name-xform.js +21 -86
  173. package/dist/cjs/modules/excel/xlsx/xform/book/external-link-xform.js +333 -0
  174. package/dist/cjs/modules/excel/xlsx/xform/book/external-reference-xform.js +27 -0
  175. package/dist/cjs/modules/excel/xlsx/xform/book/workbook-calc-properties-xform.js +11 -2
  176. package/dist/cjs/modules/excel/xlsx/xform/book/workbook-protection-xform.js +69 -0
  177. package/dist/cjs/modules/excel/xlsx/xform/book/workbook-xform.js +38 -5
  178. package/dist/cjs/modules/excel/xlsx/xform/core/content-types-xform.js +18 -0
  179. package/dist/cjs/modules/excel/xlsx/xform/core/metadata-xform.js +161 -0
  180. package/dist/cjs/modules/excel/xlsx/xform/drawing/absolute-anchor-xform.js +108 -0
  181. package/dist/cjs/modules/excel/xlsx/xform/drawing/base-cell-anchor-xform.js +3 -0
  182. package/dist/cjs/modules/excel/xlsx/xform/drawing/drawing-xform.js +10 -2
  183. package/dist/cjs/modules/excel/xlsx/xform/sheet/cell-xform.js +166 -8
  184. package/dist/cjs/modules/excel/xlsx/xform/sheet/data-validations-xform.js +1 -1
  185. package/dist/cjs/modules/excel/xlsx/xform/sheet/ignored-errors-xform.js +83 -0
  186. package/dist/cjs/modules/excel/xlsx/xform/sheet/worksheet-xform.js +9 -4
  187. package/dist/cjs/modules/excel/xlsx/xform/style/border-xform.js +4 -1
  188. package/dist/cjs/modules/excel/xlsx/xlsx.browser.js +408 -18
  189. package/dist/cjs/modules/excel/xlsx/xlsx.js +4 -5
  190. package/dist/cjs/modules/formula/compile/address-utils.js +89 -0
  191. package/dist/cjs/modules/formula/compile/binder.js +489 -0
  192. package/dist/cjs/modules/formula/compile/bound-ast.js +68 -0
  193. package/dist/cjs/modules/formula/compile/compiled-formula.js +387 -0
  194. package/dist/cjs/modules/formula/compile/dependency-analysis.js +437 -0
  195. package/dist/cjs/modules/formula/compile/structured-ref-utils.js +141 -0
  196. package/dist/cjs/modules/formula/default-syntax-probe.js +87 -0
  197. package/dist/cjs/modules/formula/functions/_date-context.js +33 -0
  198. package/dist/cjs/modules/formula/functions/_shared.js +396 -0
  199. package/dist/cjs/modules/formula/functions/conditional.js +354 -0
  200. package/dist/cjs/modules/formula/functions/database.js +288 -0
  201. package/dist/cjs/modules/formula/functions/date.js +883 -0
  202. package/dist/cjs/modules/formula/functions/dynamic-array.js +881 -0
  203. package/dist/cjs/modules/formula/functions/engineering.js +1183 -0
  204. package/dist/cjs/modules/formula/functions/financial.js +2348 -0
  205. package/dist/cjs/modules/formula/functions/lookup.js +902 -0
  206. package/dist/cjs/modules/formula/functions/math.js +1487 -0
  207. package/dist/cjs/modules/formula/functions/statistical.js +3488 -0
  208. package/dist/cjs/modules/formula/functions/text.js +1889 -0
  209. package/dist/cjs/modules/formula/host-registry.js +75 -0
  210. package/dist/cjs/modules/formula/index.js +58 -0
  211. package/dist/cjs/modules/formula/install.js +93 -0
  212. package/dist/cjs/modules/formula/integration/apply-writeback-plan.js +213 -0
  213. package/dist/cjs/modules/formula/integration/calculate-formulas-impl.js +619 -0
  214. package/dist/cjs/modules/formula/integration/calculate-formulas.js +71 -0
  215. package/dist/cjs/modules/formula/integration/formula-instance.js +82 -0
  216. package/dist/cjs/modules/formula/integration/workbook-adapter.js +327 -0
  217. package/dist/cjs/modules/formula/integration/workbook-snapshot.js +84 -0
  218. package/dist/cjs/modules/formula/materialize/build-writeback-plan.js +475 -0
  219. package/dist/cjs/modules/formula/materialize/spill-engine.js +42 -0
  220. package/dist/cjs/modules/formula/materialize/types.js +32 -0
  221. package/dist/cjs/modules/formula/materialize/writeback-plan.js +28 -0
  222. package/dist/cjs/modules/formula/runtime/evaluator.js +2298 -0
  223. package/dist/cjs/modules/formula/runtime/function-registry.js +846 -0
  224. package/dist/cjs/modules/formula/runtime/values.js +385 -0
  225. package/dist/cjs/modules/formula/syntax/ast.js +8 -0
  226. package/dist/cjs/modules/formula/syntax/parser.js +440 -0
  227. package/dist/cjs/modules/formula/syntax/token-types.js +32 -0
  228. package/dist/cjs/modules/formula/syntax/tokenizer.js +1076 -0
  229. package/dist/cjs/modules/pdf/excel-bridge.js +9 -0
  230. package/dist/esm/index.browser.js +4 -0
  231. package/dist/esm/index.js +4 -0
  232. package/dist/esm/modules/excel/cell.js +170 -22
  233. package/dist/esm/modules/excel/defined-names.js +411 -21
  234. package/dist/esm/modules/excel/image.js +24 -1
  235. package/dist/esm/modules/excel/stream/workbook-reader.browser.js +14 -0
  236. package/dist/esm/modules/excel/stream/workbook-writer.browser.js +48 -1
  237. package/dist/esm/modules/excel/stream/worksheet-reader.js +17 -1
  238. package/dist/esm/modules/excel/stream/worksheet-writer.js +45 -5
  239. package/dist/esm/modules/excel/table.js +15 -2
  240. package/dist/esm/modules/excel/utils/col-cache.js +15 -0
  241. package/dist/esm/modules/excel/utils/drawing-utils.js +4 -0
  242. package/dist/esm/modules/excel/utils/external-link-formula.js +208 -0
  243. package/dist/esm/modules/excel/utils/iterate-stream.js +3 -1
  244. package/dist/esm/modules/excel/utils/ooxml-paths.js +37 -2
  245. package/dist/esm/modules/excel/utils/shared-strings.js +21 -2
  246. package/dist/esm/modules/excel/utils/workbook-protection.js +30 -0
  247. package/dist/esm/modules/excel/workbook.browser.js +318 -34
  248. package/dist/esm/modules/excel/worksheet.js +21 -2
  249. package/dist/esm/modules/excel/xlsx/rel-type.js +16 -1
  250. package/dist/esm/modules/excel/xlsx/xform/book/defined-name-xform.js +21 -86
  251. package/dist/esm/modules/excel/xlsx/xform/book/external-link-xform.js +330 -0
  252. package/dist/esm/modules/excel/xlsx/xform/book/external-reference-xform.js +24 -0
  253. package/dist/esm/modules/excel/xlsx/xform/book/workbook-calc-properties-xform.js +11 -2
  254. package/dist/esm/modules/excel/xlsx/xform/book/workbook-protection-xform.js +66 -0
  255. package/dist/esm/modules/excel/xlsx/xform/book/workbook-xform.js +38 -5
  256. package/dist/esm/modules/excel/xlsx/xform/core/content-types-xform.js +19 -1
  257. package/dist/esm/modules/excel/xlsx/xform/core/metadata-xform.js +158 -0
  258. package/dist/esm/modules/excel/xlsx/xform/drawing/absolute-anchor-xform.js +105 -0
  259. package/dist/esm/modules/excel/xlsx/xform/drawing/base-cell-anchor-xform.js +3 -0
  260. package/dist/esm/modules/excel/xlsx/xform/drawing/drawing-xform.js +10 -2
  261. package/dist/esm/modules/excel/xlsx/xform/sheet/cell-xform.js +166 -8
  262. package/dist/esm/modules/excel/xlsx/xform/sheet/data-validations-xform.js +1 -1
  263. package/dist/esm/modules/excel/xlsx/xform/sheet/ignored-errors-xform.js +80 -0
  264. package/dist/esm/modules/excel/xlsx/xform/sheet/worksheet-xform.js +9 -4
  265. package/dist/esm/modules/excel/xlsx/xform/style/border-xform.js +4 -1
  266. package/dist/esm/modules/excel/xlsx/xlsx.browser.js +410 -20
  267. package/dist/esm/modules/excel/xlsx/xlsx.js +4 -5
  268. package/dist/esm/modules/formula/compile/address-utils.js +83 -0
  269. package/dist/esm/modules/formula/compile/binder.js +487 -0
  270. package/dist/esm/modules/formula/compile/bound-ast.js +80 -0
  271. package/dist/esm/modules/formula/compile/compiled-formula.js +383 -0
  272. package/dist/esm/modules/formula/compile/dependency-analysis.js +432 -0
  273. package/dist/esm/modules/formula/compile/structured-ref-utils.js +136 -0
  274. package/dist/esm/modules/formula/default-syntax-probe.js +83 -0
  275. package/dist/esm/modules/formula/functions/_date-context.js +29 -0
  276. package/dist/esm/modules/formula/functions/_shared.js +381 -0
  277. package/dist/esm/modules/formula/functions/conditional.js +343 -0
  278. package/dist/esm/modules/formula/functions/database.js +274 -0
  279. package/dist/esm/modules/formula/functions/date.js +855 -0
  280. package/dist/esm/modules/formula/functions/dynamic-array.js +860 -0
  281. package/dist/esm/modules/formula/functions/engineering.js +1128 -0
  282. package/dist/esm/modules/formula/functions/financial.js +2296 -0
  283. package/dist/esm/modules/formula/functions/lookup.js +886 -0
  284. package/dist/esm/modules/formula/functions/math.js +1406 -0
  285. package/dist/esm/modules/formula/functions/statistical.js +3390 -0
  286. package/dist/esm/modules/formula/functions/text.js +1845 -0
  287. package/dist/esm/modules/formula/host-registry.js +69 -0
  288. package/dist/esm/modules/formula/index.js +49 -0
  289. package/dist/esm/modules/formula/install.js +88 -0
  290. package/dist/esm/modules/formula/integration/apply-writeback-plan.js +210 -0
  291. package/dist/esm/modules/formula/integration/calculate-formulas-impl.js +616 -0
  292. package/dist/esm/modules/formula/integration/calculate-formulas.js +68 -0
  293. package/dist/esm/modules/formula/integration/formula-instance.js +79 -0
  294. package/dist/esm/modules/formula/integration/workbook-adapter.js +324 -0
  295. package/dist/esm/modules/formula/integration/workbook-snapshot.js +77 -0
  296. package/dist/esm/modules/formula/materialize/build-writeback-plan.js +473 -0
  297. package/dist/esm/modules/formula/materialize/spill-engine.js +38 -0
  298. package/dist/esm/modules/formula/materialize/types.js +29 -0
  299. package/dist/esm/modules/formula/materialize/writeback-plan.js +27 -0
  300. package/dist/esm/modules/formula/runtime/evaluator.js +2291 -0
  301. package/dist/esm/modules/formula/runtime/function-registry.js +840 -0
  302. package/dist/esm/modules/formula/runtime/values.js +385 -0
  303. package/dist/esm/modules/formula/syntax/ast.js +28 -0
  304. package/dist/esm/modules/formula/syntax/parser.js +439 -0
  305. package/dist/esm/modules/formula/syntax/token-types.js +59 -0
  306. package/dist/esm/modules/formula/syntax/tokenizer.js +1074 -0
  307. package/dist/esm/modules/pdf/excel-bridge.js +9 -0
  308. package/dist/iife/excelts.iife.js +2302 -373
  309. package/dist/iife/excelts.iife.js.map +1 -1
  310. package/dist/iife/excelts.iife.min.js +34 -34
  311. package/dist/types/index.browser.d.ts +1 -1
  312. package/dist/types/index.d.ts +1 -1
  313. package/dist/types/modules/excel/cell.d.ts +17 -3
  314. package/dist/types/modules/excel/defined-names.d.ts +96 -1
  315. package/dist/types/modules/excel/image.d.ts +11 -0
  316. package/dist/types/modules/excel/stream/workbook-reader.browser.d.ts +9 -3
  317. package/dist/types/modules/excel/stream/workbook-reader.d.ts +2 -1
  318. package/dist/types/modules/excel/stream/workbook-writer.browser.d.ts +39 -5
  319. package/dist/types/modules/excel/stream/workbook-writer.d.ts +3 -2
  320. package/dist/types/modules/excel/stream/worksheet-writer.d.ts +39 -6
  321. package/dist/types/modules/excel/types.d.ts +133 -2
  322. package/dist/types/modules/excel/utils/col-cache.d.ts +1 -0
  323. package/dist/types/modules/excel/utils/drawing-utils.d.ts +3 -3
  324. package/dist/types/modules/excel/utils/external-link-formula.d.ts +76 -0
  325. package/dist/types/modules/excel/utils/iterate-stream.d.ts +9 -3
  326. package/dist/types/modules/excel/utils/ooxml-paths.d.ts +19 -0
  327. package/dist/types/modules/excel/utils/shared-strings.d.ts +8 -3
  328. package/dist/types/modules/excel/utils/workbook-protection.d.ts +30 -0
  329. package/dist/types/modules/excel/workbook.browser.d.ts +257 -6
  330. package/dist/types/modules/excel/workbook.d.ts +1 -1
  331. package/dist/types/modules/excel/worksheet.d.ts +3 -1
  332. package/dist/types/modules/excel/xlsx/rel-type.d.ts +15 -0
  333. package/dist/types/modules/excel/xlsx/xform/book/defined-name-xform.d.ts +6 -5
  334. package/dist/types/modules/excel/xlsx/xform/book/external-link-xform.d.ts +84 -0
  335. package/dist/types/modules/excel/xlsx/xform/book/external-reference-xform.d.ts +17 -0
  336. package/dist/types/modules/excel/xlsx/xform/book/workbook-calc-properties-xform.d.ts +3 -0
  337. package/dist/types/modules/excel/xlsx/xform/book/workbook-protection-xform.d.ts +20 -0
  338. package/dist/types/modules/excel/xlsx/xform/core/metadata-xform.d.ts +56 -0
  339. package/dist/types/modules/excel/xlsx/xform/drawing/absolute-anchor-xform.d.ts +26 -0
  340. package/dist/types/modules/excel/xlsx/xform/sheet/cell-xform.d.ts +1 -1
  341. package/dist/types/modules/excel/xlsx/xform/sheet/ignored-errors-xform.d.ts +21 -0
  342. package/dist/types/modules/excel/xlsx/xlsx.browser.d.ts +172 -13
  343. package/dist/types/modules/excel/xlsx/xlsx.d.ts +7 -4
  344. package/dist/types/modules/formula/compile/address-utils.d.ts +62 -0
  345. package/dist/types/modules/formula/compile/binder.d.ts +42 -0
  346. package/dist/types/modules/formula/compile/bound-ast.d.ts +230 -0
  347. package/dist/types/modules/formula/compile/compiled-formula.d.ts +137 -0
  348. package/dist/types/modules/formula/compile/dependency-analysis.d.ts +93 -0
  349. package/dist/types/modules/formula/compile/structured-ref-utils.d.ts +93 -0
  350. package/dist/types/modules/formula/default-syntax-probe.d.ts +79 -0
  351. package/dist/types/modules/formula/functions/_date-context.d.ts +4 -0
  352. package/dist/types/modules/formula/functions/_shared.d.ts +121 -0
  353. package/dist/types/modules/formula/functions/conditional.d.ts +27 -0
  354. package/dist/types/modules/formula/functions/database.d.ts +37 -0
  355. package/dist/types/modules/formula/functions/date.d.ts +61 -0
  356. package/dist/types/modules/formula/functions/dynamic-array.d.ts +23 -0
  357. package/dist/types/modules/formula/functions/engineering.d.ts +57 -0
  358. package/dist/types/modules/formula/functions/financial.d.ts +202 -0
  359. package/dist/types/modules/formula/functions/lookup.d.ts +18 -0
  360. package/dist/types/modules/formula/functions/math.d.ts +114 -0
  361. package/dist/types/modules/formula/functions/statistical.d.ts +193 -0
  362. package/dist/types/modules/formula/functions/text.d.ts +86 -0
  363. package/dist/types/modules/formula/host-registry.d.ts +53 -0
  364. package/dist/types/modules/formula/index.d.ts +39 -0
  365. package/dist/types/modules/formula/install.d.ts +62 -0
  366. package/dist/types/modules/formula/integration/apply-writeback-plan.d.ts +26 -0
  367. package/dist/types/modules/formula/integration/calculate-formulas-impl.d.ts +30 -0
  368. package/dist/types/modules/formula/integration/calculate-formulas.d.ts +67 -0
  369. package/dist/types/modules/formula/integration/formula-instance.d.ts +64 -0
  370. package/dist/types/modules/formula/integration/workbook-adapter.d.ts +26 -0
  371. package/dist/types/modules/formula/integration/workbook-snapshot.d.ts +267 -0
  372. package/dist/types/modules/formula/materialize/build-writeback-plan.d.ts +34 -0
  373. package/dist/types/modules/formula/materialize/spill-engine.d.ts +9 -0
  374. package/dist/types/modules/formula/materialize/types.d.ts +179 -0
  375. package/dist/types/modules/formula/materialize/writeback-plan.d.ts +167 -0
  376. package/dist/types/modules/formula/runtime/evaluator.d.ts +151 -0
  377. package/dist/types/modules/formula/runtime/function-registry.d.ts +47 -0
  378. package/dist/types/modules/formula/runtime/values.d.ts +211 -0
  379. package/dist/types/modules/formula/syntax/ast.d.ts +129 -0
  380. package/dist/types/modules/formula/syntax/parser.d.ts +18 -0
  381. package/dist/types/modules/formula/syntax/token-types.d.ts +153 -0
  382. package/dist/types/modules/formula/syntax/tokenizer.d.ts +10 -0
  383. package/package.json +28 -28
@@ -0,0 +1,2296 @@
1
+ /**
2
+ * Financial Functions — Native RuntimeValue implementation.
3
+ */
4
+ import { excelToDate } from "../../../utils/utils.base.js";
5
+ import { RVKind, ERRORS, isError, isArray, toNumberRV, toBooleanRV, rvNumber, rvBoolean } from "../runtime/values.js";
6
+ import { isDate1904 } from "./_date-context.js";
7
+ import { flattenNumbers, firstError } from "./_shared.js";
8
+ /**
9
+ * Convert an Excel serial to a UTC `Date`, honouring the active date1904 mode.
10
+ *
11
+ * All date math in this module must read fields with the `getUTC*` family
12
+ * because `excelToDate()` returns a `Date` whose UTC timeline corresponds
13
+ * to the Excel serial. Using local-time accessors would make bond maths
14
+ * drift by a day whenever the host lives west of UTC.
15
+ */
16
+ function toDate(serial) {
17
+ return excelToDate(serial, isDate1904());
18
+ }
19
+ // ============================================================================
20
+ // Financial Functions
21
+ // ============================================================================
22
+ export const fnPMT = args => {
23
+ const rate = toNumberRV(args[0]);
24
+ if (isError(rate)) {
25
+ return rate;
26
+ }
27
+ const nper = toNumberRV(args[1]);
28
+ if (isError(nper)) {
29
+ return nper;
30
+ }
31
+ const pv = toNumberRV(args[2]);
32
+ if (isError(pv)) {
33
+ return pv;
34
+ }
35
+ const fv = args.length > 3 ? toNumberRV(args[3]) : rvNumber(0);
36
+ if (isError(fv)) {
37
+ return fv;
38
+ }
39
+ const type = args.length > 4 ? toNumberRV(args[4]) : rvNumber(0);
40
+ if (isError(type)) {
41
+ return type;
42
+ }
43
+ // Excel collapses any truthy `type` to 1 and any falsy to 0. Passing a
44
+ // non-binary number silently changed the PMT result; normalise up front
45
+ // so callers can't smuggle intermediate values in.
46
+ const typeBit = type.value ? 1 : 0;
47
+ // The simplified closed-form PMT divides by `nper` (rate=0 branch) or by
48
+ // `(1+rate)^nper - 1` (rate!=0 branch). Both collapse at nper=0, so we
49
+ // return #DIV/0! before attempting the math. The explicit guard matches
50
+ // Excel's behaviour on PMT(0, 0, …) and PMT(r, 0, …).
51
+ // Any `nper = 0` collapses the annuity equation (there are no periods
52
+ // over which to amortise). Regardless of `rate`, this is #DIV/0! —
53
+ // previously only the `rate = 0 && nper = 0` branch was guarded, so
54
+ // `PMT(0.05, 0, 1000)` produced NaN and silently serialised as null.
55
+ if (nper.value === 0) {
56
+ return ERRORS.DIV0;
57
+ }
58
+ if (rate.value === 0) {
59
+ return rvNumber(-(pv.value + fv.value) / nper.value);
60
+ }
61
+ const pvif = Math.pow(1 + rate.value, nper.value);
62
+ return rvNumber(-(rate.value * (pv.value * pvif + fv.value)) / (pvif - 1) / (1 + rate.value * typeBit));
63
+ };
64
+ export const fnFV = args => {
65
+ const rate = toNumberRV(args[0]);
66
+ if (isError(rate)) {
67
+ return rate;
68
+ }
69
+ const nper = toNumberRV(args[1]);
70
+ if (isError(nper)) {
71
+ return nper;
72
+ }
73
+ const pmt = toNumberRV(args[2]);
74
+ if (isError(pmt)) {
75
+ return pmt;
76
+ }
77
+ const pv = args.length > 3 ? toNumberRV(args[3]) : rvNumber(0);
78
+ if (isError(pv)) {
79
+ return pv;
80
+ }
81
+ const type = args.length > 4 ? toNumberRV(args[4]) : rvNumber(0);
82
+ if (isError(type)) {
83
+ return type;
84
+ }
85
+ const typeBit = type.value ? 1 : 0;
86
+ // At rate=0 the formula reduces to `-(pv + pmt*nper)`; when both rate
87
+ // and nper are zero the underlying annuity equation is mathematically
88
+ // undefined (no periods over which to accrue), so Excel returns
89
+ // #DIV/0! — guard explicitly to keep the behaviour obvious.
90
+ if (nper.value === 0 && rate.value === 0) {
91
+ return ERRORS.DIV0;
92
+ }
93
+ if (rate.value === 0) {
94
+ return rvNumber(-(pv.value + pmt.value * nper.value));
95
+ }
96
+ const pvif = Math.pow(1 + rate.value, nper.value);
97
+ return rvNumber(-(pv.value * pvif + pmt.value * (1 + rate.value * typeBit) * ((pvif - 1) / rate.value)));
98
+ };
99
+ export const fnPV = args => {
100
+ const rate = toNumberRV(args[0]);
101
+ if (isError(rate)) {
102
+ return rate;
103
+ }
104
+ const nper = toNumberRV(args[1]);
105
+ if (isError(nper)) {
106
+ return nper;
107
+ }
108
+ const pmt = toNumberRV(args[2]);
109
+ if (isError(pmt)) {
110
+ return pmt;
111
+ }
112
+ const fv = args.length > 3 ? toNumberRV(args[3]) : rvNumber(0);
113
+ if (isError(fv)) {
114
+ return fv;
115
+ }
116
+ const type = args.length > 4 ? toNumberRV(args[4]) : rvNumber(0);
117
+ if (isError(type)) {
118
+ return type;
119
+ }
120
+ const typeBit = type.value ? 1 : 0;
121
+ // Same boundary as FV/PMT: at rate=0 and nper=0 the annuity is
122
+ // mathematically undefined, so surface #DIV/0! instead of the
123
+ // coincidentally-finite `-fv` that the formula would otherwise yield.
124
+ if (nper.value === 0 && rate.value === 0) {
125
+ return ERRORS.DIV0;
126
+ }
127
+ if (rate.value === 0) {
128
+ return rvNumber(-pmt.value * nper.value - fv.value);
129
+ }
130
+ const pvif = Math.pow(1 + rate.value, nper.value);
131
+ return rvNumber(-(fv.value + pmt.value * (1 + rate.value * typeBit) * ((pvif - 1) / rate.value)) / pvif);
132
+ };
133
+ export const fnNPV = args => {
134
+ const rate = toNumberRV(args[0]);
135
+ if (isError(rate)) {
136
+ return rate;
137
+ }
138
+ // NPV divides each cash flow by `(1 + rate)^i`. When `1 + rate == 0`
139
+ // (i.e. rate == -1) the discount factor is zero for every period and
140
+ // the series is undefined — Excel returns #DIV/0!. Guard explicitly so
141
+ // we don't fall through to Infinity/NaN in the pow-and-divide below.
142
+ if (1 + rate.value === 0) {
143
+ return ERRORS.DIV0;
144
+ }
145
+ const values = [];
146
+ for (let i = 1; i < args.length; i++) {
147
+ const a = args[i];
148
+ if (isArray(a)) {
149
+ for (const row of a.rows) {
150
+ for (const cell of row) {
151
+ if (cell.kind === RVKind.Number) {
152
+ values.push(cell.value);
153
+ }
154
+ }
155
+ }
156
+ }
157
+ else {
158
+ const n = toNumberRV(a);
159
+ if (isError(n)) {
160
+ return n;
161
+ }
162
+ values.push(n.value);
163
+ }
164
+ }
165
+ // Excel requires at least one cash flow; `NPV(rate)` with nothing to
166
+ // discount is a #VALUE! in the desktop app. (The previous code happily
167
+ // returned 0, which could mask buggy callers.)
168
+ if (values.length === 0) {
169
+ return ERRORS.VALUE;
170
+ }
171
+ let npv = 0;
172
+ for (let i = 0; i < values.length; i++) {
173
+ npv += values[i] / Math.pow(1 + rate.value, i + 1);
174
+ }
175
+ return isFinite(npv) ? rvNumber(npv) : ERRORS.NUM;
176
+ };
177
+ export const fnIRR = args => {
178
+ if (!isArray(args[0])) {
179
+ return ERRORS.VALUE;
180
+ }
181
+ const values = [];
182
+ for (const row of args[0].rows) {
183
+ for (const cell of row) {
184
+ if (cell.kind === RVKind.Number) {
185
+ values.push(cell.value);
186
+ }
187
+ }
188
+ }
189
+ if (values.length < 2) {
190
+ return ERRORS.NUM;
191
+ }
192
+ // Excel requires at least one sign change in the cash-flow series — an
193
+ // IRR cannot exist for an all-positive or all-negative stream. Without
194
+ // this guard, Newton would drift toward an asymptote near g = -1 and
195
+ // might return a spurious "converged" value.
196
+ let hasPos = false;
197
+ let hasNeg = false;
198
+ for (const v of values) {
199
+ if (v > 0) {
200
+ hasPos = true;
201
+ }
202
+ if (v < 0) {
203
+ hasNeg = true;
204
+ }
205
+ }
206
+ if (!hasPos || !hasNeg) {
207
+ return ERRORS.NUM;
208
+ }
209
+ const guessRV = args.length > 1 ? toNumberRV(args[1]) : rvNumber(0.1);
210
+ if (isError(guessRV)) {
211
+ return guessRV;
212
+ }
213
+ const npvAt = (g) => {
214
+ let s = 0;
215
+ for (let i = 0; i < values.length; i++) {
216
+ s += values[i] / Math.pow(1 + g, i);
217
+ }
218
+ return s;
219
+ };
220
+ // Bracket a root by scanning a reasonable rate range once; then Newton
221
+ // refines within the bracket. This is more robust than pure Newton from
222
+ // a single starting point, which for multi-sign flows can wander to a
223
+ // different root or diverge.
224
+ let g = guessRV.value;
225
+ for (let iter = 0; iter < 100; iter++) {
226
+ let npv = 0;
227
+ let dnpv = 0;
228
+ for (let i = 0; i < values.length; i++) {
229
+ const p = Math.pow(1 + g, i);
230
+ if (!isFinite(p) || p === 0) {
231
+ break;
232
+ }
233
+ npv += values[i] / p;
234
+ dnpv -= (i * values[i]) / (p * (1 + g));
235
+ }
236
+ if (!isFinite(npv) || !isFinite(dnpv) || Math.abs(dnpv) < 1e-15) {
237
+ break;
238
+ }
239
+ const newGuess = g - npv / dnpv;
240
+ if (!isFinite(newGuess) || newGuess <= -1) {
241
+ break;
242
+ }
243
+ if (Math.abs(newGuess - g) < 1e-10) {
244
+ return rvNumber(newGuess);
245
+ }
246
+ g = newGuess;
247
+ }
248
+ // Newton failed to converge: fall back to a bracketing search over a
249
+ // wide range to at least find *some* root.
250
+ const sampleRates = [-0.99, -0.5, -0.1, 0, 0.1, 0.25, 0.5, 1, 2, 5, 10];
251
+ let prevG = sampleRates[0];
252
+ let prevV = npvAt(prevG);
253
+ for (let i = 1; i < sampleRates.length; i++) {
254
+ const currG = sampleRates[i];
255
+ const currV = npvAt(currG);
256
+ if (isFinite(prevV) && isFinite(currV) && prevV * currV < 0) {
257
+ // Bisection within [prevG, currG].
258
+ let lo = prevG;
259
+ let hi = currG;
260
+ let loV = prevV;
261
+ for (let b = 0; b < 100; b++) {
262
+ const mid = (lo + hi) / 2;
263
+ const midV = npvAt(mid);
264
+ if (!isFinite(midV)) {
265
+ break;
266
+ }
267
+ if (Math.abs(midV) < 1e-10 || hi - lo < 1e-12) {
268
+ return rvNumber(mid);
269
+ }
270
+ if (loV * midV < 0) {
271
+ hi = mid;
272
+ }
273
+ else {
274
+ lo = mid;
275
+ loV = midV;
276
+ }
277
+ }
278
+ return rvNumber((lo + hi) / 2);
279
+ }
280
+ prevG = currG;
281
+ prevV = currV;
282
+ }
283
+ return ERRORS.NUM;
284
+ };
285
+ export const fnNPER = args => {
286
+ const rate = toNumberRV(args[0]);
287
+ if (isError(rate)) {
288
+ return rate;
289
+ }
290
+ const pmt = toNumberRV(args[1]);
291
+ if (isError(pmt)) {
292
+ return pmt;
293
+ }
294
+ const pv = toNumberRV(args[2]);
295
+ if (isError(pv)) {
296
+ return pv;
297
+ }
298
+ const fv = args.length > 3 ? toNumberRV(args[3]) : rvNumber(0);
299
+ if (isError(fv)) {
300
+ return fv;
301
+ }
302
+ const type = args.length > 4 ? toNumberRV(args[4]) : rvNumber(0);
303
+ if (isError(type)) {
304
+ return type;
305
+ }
306
+ // When rate=0 NPER reduces to `-(pv + fv) / pmt`; a zero payment on a
307
+ // zero-rate loan has no solution (no periods can make the balance
308
+ // amortise), so surface #DIV/0! rather than NaN.
309
+ if (rate.value === 0 && pmt.value === 0) {
310
+ return ERRORS.DIV0;
311
+ }
312
+ if (rate.value === 0) {
313
+ return rvNumber(-(pv.value + fv.value) / pmt.value);
314
+ }
315
+ const num = pmt.value * (1 + rate.value * type.value) - fv.value * rate.value;
316
+ const den = pv.value * rate.value + pmt.value * (1 + rate.value * type.value);
317
+ if (num / den <= 0) {
318
+ return ERRORS.NUM;
319
+ }
320
+ const result = Math.log(num / den) / Math.log(1 + rate.value);
321
+ // A negative NPER is mathematically valid (it represents "how many
322
+ // periods ago did this cashflow happen") but has no financial meaning
323
+ // in Excel — the documented convention is that NPER models a forward-
324
+ // looking loan/annuity. Reject negatives rather than returning them as
325
+ // silently-wrong period counts. (R8 quirk from worker audit)
326
+ if (!Number.isFinite(result) || result < 0) {
327
+ return ERRORS.NUM;
328
+ }
329
+ return rvNumber(result);
330
+ };
331
+ export const fnRATE = args => {
332
+ const nper = toNumberRV(args[0]);
333
+ if (isError(nper)) {
334
+ return nper;
335
+ }
336
+ const pmt = toNumberRV(args[1]);
337
+ if (isError(pmt)) {
338
+ return pmt;
339
+ }
340
+ const pv = toNumberRV(args[2]);
341
+ if (isError(pv)) {
342
+ return pv;
343
+ }
344
+ const fv = args.length > 3 ? toNumberRV(args[3]) : rvNumber(0);
345
+ if (isError(fv)) {
346
+ return fv;
347
+ }
348
+ const type = args.length > 4 ? toNumberRV(args[4]) : rvNumber(0);
349
+ if (isError(type)) {
350
+ return type;
351
+ }
352
+ const guess = args.length > 5 ? toNumberRV(args[5]) : rvNumber(0.1);
353
+ if (isError(guess)) {
354
+ return guess;
355
+ }
356
+ let g = guess.value;
357
+ const nperV = nper.value;
358
+ const pmtV = pmt.value;
359
+ const pvV = pv.value;
360
+ const fvV = fv.value;
361
+ const typeV = type.value;
362
+ if (nperV <= 0) {
363
+ return ERRORS.NUM;
364
+ }
365
+ // Newton-Raphson. f(g) = PV·(1+g)^n + PMT·(1+g·type)·((1+g)^n − 1)/g + FV.
366
+ //
367
+ // The analytic derivative of the PMT term contains a 1/g² factor and a
368
+ // `(1+g)^n − 1` cancellation, both of which lose precision as g → 0. To
369
+ // handle that regime robustly we switch to a centred finite difference
370
+ // when |g| is small; the finite-difference derivative is exactly what the
371
+ // Newton iteration needs and stays well-conditioned down to g = 0 (we
372
+ // pick the step size ~sqrt(eps) to balance truncation vs rounding error).
373
+ const EPS = 1e-7;
374
+ const f = (x) => {
375
+ if (Math.abs(x) < 1e-12) {
376
+ // g = 0 limit: f(0) = PV + PMT·n + FV (the (1+g·type) factor → 1 and
377
+ // the ((1+g)^n − 1)/g factor → n).
378
+ return pvV + pmtV * nperV + fvV;
379
+ }
380
+ const pvifLocal = Math.pow(1 + x, nperV);
381
+ return pvV * pvifLocal + pmtV * (1 + x * typeV) * ((pvifLocal - 1) / x) + fvV;
382
+ };
383
+ for (let iter = 0; iter < 100; iter++) {
384
+ if (g <= -1) {
385
+ g = -0.99;
386
+ }
387
+ const fg = f(g);
388
+ let df;
389
+ if (Math.abs(g) < EPS) {
390
+ const h = 1e-5;
391
+ df = (f(g + h) - f(g - h)) / (2 * h);
392
+ }
393
+ else {
394
+ const pvif = Math.pow(1 + g, nperV);
395
+ const fvifa = (pvif - 1) / g;
396
+ df =
397
+ nperV * pvV * Math.pow(1 + g, nperV - 1) +
398
+ (pmtV * (1 + g * typeV) * (nperV * Math.pow(1 + g, nperV - 1) * g - pvif + 1)) / (g * g) +
399
+ (typeV ? pmtV * fvifa : 0);
400
+ }
401
+ if (!isFinite(df) || Math.abs(df) < 1e-15) {
402
+ break;
403
+ }
404
+ const newGuess = g - fg / df;
405
+ if (!isFinite(newGuess)) {
406
+ break;
407
+ }
408
+ if (Math.abs(newGuess - g) < 1e-10) {
409
+ return rvNumber(newGuess);
410
+ }
411
+ g = newGuess;
412
+ }
413
+ // Did not converge after 100 iterations
414
+ return ERRORS.NUM;
415
+ };
416
+ export const fnSLN = args => {
417
+ const cost = toNumberRV(args[0]);
418
+ if (isError(cost)) {
419
+ return cost;
420
+ }
421
+ const salvage = toNumberRV(args[1]);
422
+ if (isError(salvage)) {
423
+ return salvage;
424
+ }
425
+ const life = toNumberRV(args[2]);
426
+ if (isError(life)) {
427
+ return life;
428
+ }
429
+ if (life.value === 0) {
430
+ return ERRORS.DIV0;
431
+ }
432
+ return rvNumber((cost.value - salvage.value) / life.value);
433
+ };
434
+ /**
435
+ * SYD — Sum-of-years'-digits depreciation.
436
+ *
437
+ * SYD(cost, salvage, life, per)
438
+ *
439
+ * The depreciation assigned to period `per` by the sum-of-years'-digits
440
+ * method: `(cost - salvage) * (life - per + 1) * 2 / (life * (life + 1))`.
441
+ * Excel rejects `life = 0` and period outside [1, life] with #NUM!.
442
+ */
443
+ export const fnSYD = args => {
444
+ const cost = toNumberRV(args[0]);
445
+ if (isError(cost)) {
446
+ return cost;
447
+ }
448
+ const salvage = toNumberRV(args[1]);
449
+ if (isError(salvage)) {
450
+ return salvage;
451
+ }
452
+ const life = toNumberRV(args[2]);
453
+ if (isError(life)) {
454
+ return life;
455
+ }
456
+ const per = toNumberRV(args[3]);
457
+ if (isError(per)) {
458
+ return per;
459
+ }
460
+ if (life.value <= 0 || per.value < 1 || per.value > life.value) {
461
+ return ERRORS.NUM;
462
+ }
463
+ return rvNumber(((cost.value - salvage.value) * (life.value - per.value + 1) * 2) /
464
+ (life.value * (life.value + 1)));
465
+ };
466
+ /**
467
+ * VDB — Variable Declining Balance depreciation.
468
+ *
469
+ * VDB(cost, salvage, life, start_period, end_period, [factor], [no_switch])
470
+ *
471
+ * Applies declining-balance depreciation (with a default factor of 2
472
+ * for double-declining) between two fractional periods. By default
473
+ * (`no_switch = FALSE`) the method silently switches to straight-line
474
+ * when that yields a larger deduction, matching Excel's documented
475
+ * behaviour. `no_switch = TRUE` forces declining-balance for all
476
+ * periods.
477
+ */
478
+ export const fnVDB = args => {
479
+ const cost = toNumberRV(args[0]);
480
+ if (isError(cost)) {
481
+ return cost;
482
+ }
483
+ const salvage = toNumberRV(args[1]);
484
+ if (isError(salvage)) {
485
+ return salvage;
486
+ }
487
+ const life = toNumberRV(args[2]);
488
+ if (isError(life)) {
489
+ return life;
490
+ }
491
+ const start = toNumberRV(args[3]);
492
+ if (isError(start)) {
493
+ return start;
494
+ }
495
+ const end = toNumberRV(args[4]);
496
+ if (isError(end)) {
497
+ return end;
498
+ }
499
+ const factorRV = args.length > 5 ? toNumberRV(args[5]) : rvNumber(2);
500
+ if (isError(factorRV)) {
501
+ return factorRV;
502
+ }
503
+ const noSwitchRV = args.length > 6 ? toBooleanRV(args[6]) : rvBoolean(false);
504
+ if (isError(noSwitchRV)) {
505
+ return noSwitchRV;
506
+ }
507
+ const c = cost.value;
508
+ const s = salvage.value;
509
+ const l = life.value;
510
+ const sp = start.value;
511
+ const ep = end.value;
512
+ const factor = factorRV.value;
513
+ const noSwitch = noSwitchRV.value;
514
+ if (c < 0 || s < 0 || l <= 0 || factor <= 0 || sp < 0 || ep <= sp || ep > l) {
515
+ return ERRORS.NUM;
516
+ }
517
+ // Walk the integer period boundaries that lie inside [sp, ep] and
518
+ // accumulate the declining-balance depreciation per whole period; clip
519
+ // the fractional period pieces at the two endpoints. Excel's DDB
520
+ // behaviour is already "clamp so book value never dips below salvage",
521
+ // so we replicate that here.
522
+ const periodDepn = (book, periodLen) => {
523
+ const decline = Math.min(book * (factor / l) * periodLen, book - s);
524
+ return decline < 0 ? 0 : decline;
525
+ };
526
+ let book = c;
527
+ let total = 0;
528
+ // Optional switch to straight-line: once DB deduction would be less
529
+ // than the equivalent SL deduction, Excel keeps running SL for the
530
+ // remaining whole periods. Handled by tracking `switched` below.
531
+ let switched = false;
532
+ let p = 0;
533
+ while (p < l) {
534
+ const lo = Math.max(sp, p);
535
+ const hi = Math.min(ep, p + 1);
536
+ if (hi > lo) {
537
+ const frac = hi - lo;
538
+ let dep;
539
+ if (!noSwitch && !switched) {
540
+ const dbDep = book * (factor / l) * frac;
541
+ const slDep = ((book - s) / (l - p)) * frac;
542
+ if (slDep > dbDep) {
543
+ switched = true;
544
+ dep = slDep;
545
+ }
546
+ else {
547
+ dep = periodDepn(book, frac);
548
+ }
549
+ }
550
+ else if (switched) {
551
+ dep = ((book - s) / Math.max(1e-10, l - p)) * frac;
552
+ }
553
+ else {
554
+ dep = periodDepn(book, frac);
555
+ }
556
+ total += dep;
557
+ book -= dep;
558
+ }
559
+ p++;
560
+ if (book <= s || p >= ep) {
561
+ break;
562
+ }
563
+ }
564
+ return rvNumber(total);
565
+ };
566
+ export const fnDB = args => {
567
+ const cost = toNumberRV(args[0]);
568
+ if (isError(cost)) {
569
+ return cost;
570
+ }
571
+ const salvage = toNumberRV(args[1]);
572
+ if (isError(salvage)) {
573
+ return salvage;
574
+ }
575
+ const life = toNumberRV(args[2]);
576
+ if (isError(life)) {
577
+ return life;
578
+ }
579
+ const period = toNumberRV(args[3]);
580
+ if (isError(period)) {
581
+ return period;
582
+ }
583
+ const month = args.length > 4 ? toNumberRV(args[4]) : rvNumber(12);
584
+ if (isError(month)) {
585
+ return month;
586
+ }
587
+ // Excel validates: cost >= 0, salvage >= 0, life > 0, period > 0, 1 <= month <= 12.
588
+ // Also period must be <= life + 1 (the "stub" trailing month period).
589
+ if (cost.value < 0 ||
590
+ salvage.value < 0 ||
591
+ life.value <= 0 ||
592
+ period.value < 1 ||
593
+ month.value < 1 ||
594
+ month.value > 12 ||
595
+ period.value > life.value + 1) {
596
+ return ERRORS.NUM;
597
+ }
598
+ if (cost.value === 0) {
599
+ return rvNumber(0);
600
+ }
601
+ // Depreciation rate rounded to 3 decimal places per Excel's published formula.
602
+ const rate = salvage.value === 0
603
+ ? 1
604
+ : Math.round((1 - Math.pow(salvage.value / cost.value, 1 / life.value)) * 1000) / 1000;
605
+ // The "trailing stub" period is ceil(life) + 1 when the first year is partial
606
+ // (month < 12) and equals ceil(life) when month === 12.
607
+ const stubPeriod = month.value === 12 ? Math.ceil(life.value) : Math.ceil(life.value) + 1;
608
+ let totalDepreciation = 0;
609
+ let depn = 0;
610
+ const periods = Math.min(Math.floor(period.value), stubPeriod);
611
+ for (let p = 1; p <= periods; p++) {
612
+ if (p === 1) {
613
+ depn = (cost.value * rate * month.value) / 12;
614
+ }
615
+ else if (p === stubPeriod) {
616
+ depn = ((cost.value - totalDepreciation) * rate * (12 - month.value)) / 12;
617
+ }
618
+ else {
619
+ depn = (cost.value - totalDepreciation) * rate;
620
+ }
621
+ totalDepreciation += depn;
622
+ }
623
+ return rvNumber(depn);
624
+ };
625
+ export const fnDDB = args => {
626
+ const cost = toNumberRV(args[0]);
627
+ if (isError(cost)) {
628
+ return cost;
629
+ }
630
+ const salvage = toNumberRV(args[1]);
631
+ if (isError(salvage)) {
632
+ return salvage;
633
+ }
634
+ const life = toNumberRV(args[2]);
635
+ if (isError(life)) {
636
+ return life;
637
+ }
638
+ const period = toNumberRV(args[3]);
639
+ if (isError(period)) {
640
+ return period;
641
+ }
642
+ const factor = args.length > 4 ? toNumberRV(args[4]) : rvNumber(2);
643
+ if (isError(factor)) {
644
+ return factor;
645
+ }
646
+ // Excel validates: cost, salvage >= 0; life, period, factor > 0; period <= life.
647
+ if (cost.value < 0 ||
648
+ salvage.value < 0 ||
649
+ life.value <= 0 ||
650
+ period.value < 1 ||
651
+ period.value > life.value ||
652
+ factor.value <= 0) {
653
+ return ERRORS.NUM;
654
+ }
655
+ let bookValue = cost.value;
656
+ let depn = 0;
657
+ const periods = Math.floor(period.value);
658
+ for (let p = 1; p <= periods; p++) {
659
+ depn = Math.min(bookValue * (factor.value / life.value), bookValue - salvage.value);
660
+ if (depn < 0) {
661
+ depn = 0;
662
+ }
663
+ bookValue -= depn;
664
+ }
665
+ return rvNumber(depn);
666
+ };
667
+ /** Internal PMT computation that returns a raw number (for IPMT/PPMT/CUMPRINC/CUMIPMT). */
668
+ function pmtRaw(rate, nper, pv, fv, type) {
669
+ if (rate === 0) {
670
+ return -(pv + fv) / nper;
671
+ }
672
+ const pvif = Math.pow(1 + rate, nper);
673
+ return -(rate * (pv * pvif + fv)) / (pvif - 1) / (1 + rate * type);
674
+ }
675
+ /**
676
+ * Internal IPMT computation that returns a raw number.
677
+ *
678
+ * Excel's IPMT reports the interest portion of a period as a cash flow
679
+ * *out* — i.e. negative when the loan principal is positive (you pay
680
+ * interest). The balance-accumulation loop above produces a positive
681
+ * running balance for a positive `pv`, so the `bal * rate` product
682
+ * would also be positive; we negate at return to match Excel's sign
683
+ * convention. The CUMIPMT / PPMT paths that consume this helper rely
684
+ * on the sign being correct so `IPMT + PPMT ≡ PMT` holds.
685
+ */
686
+ function ipmtRaw(rate, per, nper, pv, fv, type) {
687
+ const pmt = pmtRaw(rate, nper, pv, fv, type);
688
+ if (rate === 0) {
689
+ return 0;
690
+ }
691
+ // Compute FV of original loan at period (per-1)
692
+ let bal = pv;
693
+ for (let i = 1; i < per; i++) {
694
+ bal = bal * (1 + rate) + pmt * (1 + rate * type);
695
+ }
696
+ const ipmt = type === 1 && per === 1 ? 0 : -bal * rate;
697
+ return type === 1 ? ipmt / (1 + rate) : ipmt;
698
+ }
699
+ export const fnIPMT = args => {
700
+ const rate = toNumberRV(args[0]);
701
+ if (isError(rate)) {
702
+ return rate;
703
+ }
704
+ const per = toNumberRV(args[1]);
705
+ if (isError(per)) {
706
+ return per;
707
+ }
708
+ const nper = toNumberRV(args[2]);
709
+ if (isError(nper)) {
710
+ return nper;
711
+ }
712
+ const pv = toNumberRV(args[3]);
713
+ if (isError(pv)) {
714
+ return pv;
715
+ }
716
+ const fv = args.length > 4 ? toNumberRV(args[4]) : rvNumber(0);
717
+ if (isError(fv)) {
718
+ return fv;
719
+ }
720
+ const type = args.length > 5 ? toNumberRV(args[5]) : rvNumber(0);
721
+ if (isError(type)) {
722
+ return type;
723
+ }
724
+ return rvNumber(ipmtRaw(rate.value, per.value, nper.value, pv.value, fv.value, type.value));
725
+ };
726
+ export const fnPPMT = args => {
727
+ const rate = toNumberRV(args[0]);
728
+ if (isError(rate)) {
729
+ return rate;
730
+ }
731
+ const per = toNumberRV(args[1]);
732
+ if (isError(per)) {
733
+ return per;
734
+ }
735
+ const nper = toNumberRV(args[2]);
736
+ if (isError(nper)) {
737
+ return nper;
738
+ }
739
+ const pv = toNumberRV(args[3]);
740
+ if (isError(pv)) {
741
+ return pv;
742
+ }
743
+ const fv = args.length > 4 ? toNumberRV(args[4]) : rvNumber(0);
744
+ if (isError(fv)) {
745
+ return fv;
746
+ }
747
+ const type = args.length > 5 ? toNumberRV(args[5]) : rvNumber(0);
748
+ if (isError(type)) {
749
+ return type;
750
+ }
751
+ const pmtVal = pmtRaw(rate.value, nper.value, pv.value, fv.value, type.value);
752
+ const ipmtVal = ipmtRaw(rate.value, per.value, nper.value, pv.value, fv.value, type.value);
753
+ return rvNumber(pmtVal - ipmtVal);
754
+ };
755
+ /**
756
+ * FVSCHEDULE — future value with a schedule of varying rates.
757
+ *
758
+ * FVSCHEDULE(principal, schedule)
759
+ *
760
+ * Compounds `principal` through each rate in `schedule`:
761
+ * FV = principal · ∏(1 + rᵢ)
762
+ *
763
+ * Excel treats blanks in the schedule as zero (no-op compounding) and
764
+ * propagates any error it encounters. Text values produce #VALUE!.
765
+ */
766
+ export const fnFVSCHEDULE = args => {
767
+ const principal = toNumberRV(args[0]);
768
+ if (isError(principal)) {
769
+ return principal;
770
+ }
771
+ const scheduleArg = args[1];
772
+ let fv = principal.value;
773
+ const visit = (v) => {
774
+ if (v.kind === RVKind.Error) {
775
+ return v;
776
+ }
777
+ if (v.kind === RVKind.Blank) {
778
+ return null;
779
+ } // treat blanks as 0%
780
+ if (v.kind === RVKind.Number) {
781
+ fv *= 1 + v.value;
782
+ return null;
783
+ }
784
+ if (v.kind === RVKind.Boolean) {
785
+ fv *= 1 + (v.value ? 1 : 0);
786
+ return null;
787
+ }
788
+ // Strings / other: Excel surfaces #VALUE!
789
+ return ERRORS.VALUE;
790
+ };
791
+ if (scheduleArg.kind === RVKind.Array) {
792
+ for (const row of scheduleArg.rows) {
793
+ for (const cell of row) {
794
+ const err = visit(cell);
795
+ if (err) {
796
+ return err;
797
+ }
798
+ }
799
+ }
800
+ }
801
+ else {
802
+ const err = visit(scheduleArg);
803
+ if (err) {
804
+ return err;
805
+ }
806
+ }
807
+ return rvNumber(fv);
808
+ };
809
+ /**
810
+ * PDURATION — number of periods required for an investment to reach a
811
+ * specified value.
812
+ *
813
+ * PDURATION(rate, pv, fv) = (log fv − log pv) / log(1 + rate)
814
+ *
815
+ * Excel requires `rate > 0` and `pv, fv > 0`.
816
+ */
817
+ export const fnPDURATION = args => {
818
+ const rate = toNumberRV(args[0]);
819
+ if (isError(rate)) {
820
+ return rate;
821
+ }
822
+ const pv = toNumberRV(args[1]);
823
+ if (isError(pv)) {
824
+ return pv;
825
+ }
826
+ const fv = toNumberRV(args[2]);
827
+ if (isError(fv)) {
828
+ return fv;
829
+ }
830
+ if (rate.value <= 0 || pv.value <= 0 || fv.value <= 0) {
831
+ return ERRORS.NUM;
832
+ }
833
+ return rvNumber((Math.log(fv.value) - Math.log(pv.value)) / Math.log(1 + rate.value));
834
+ };
835
+ /**
836
+ * RRI — equivalent interest rate for the growth of an investment.
837
+ *
838
+ * RRI(nper, pv, fv) = (fv / pv)^(1/nper) − 1
839
+ *
840
+ * Excel requires `nper > 0`, `pv > 0`, `fv >= 0`.
841
+ */
842
+ export const fnRRI = args => {
843
+ const nper = toNumberRV(args[0]);
844
+ if (isError(nper)) {
845
+ return nper;
846
+ }
847
+ const pv = toNumberRV(args[1]);
848
+ if (isError(pv)) {
849
+ return pv;
850
+ }
851
+ const fv = toNumberRV(args[2]);
852
+ if (isError(fv)) {
853
+ return fv;
854
+ }
855
+ if (nper.value <= 0 || pv.value <= 0 || fv.value < 0) {
856
+ return ERRORS.NUM;
857
+ }
858
+ return rvNumber(Math.pow(fv.value / pv.value, 1 / nper.value) - 1);
859
+ };
860
+ export const fnEFFECT = args => {
861
+ const nomRate = toNumberRV(args[0]);
862
+ if (isError(nomRate)) {
863
+ return nomRate;
864
+ }
865
+ const npery = toNumberRV(args[1]);
866
+ if (isError(npery)) {
867
+ return npery;
868
+ }
869
+ if (nomRate.value <= 0 || npery.value < 1) {
870
+ return ERRORS.NUM;
871
+ }
872
+ return rvNumber(Math.pow(1 + nomRate.value / Math.floor(npery.value), Math.floor(npery.value)) - 1);
873
+ };
874
+ export const fnNOMINAL = args => {
875
+ const effRate = toNumberRV(args[0]);
876
+ if (isError(effRate)) {
877
+ return effRate;
878
+ }
879
+ const npery = toNumberRV(args[1]);
880
+ if (isError(npery)) {
881
+ return npery;
882
+ }
883
+ if (effRate.value <= 0 || npery.value < 1) {
884
+ return ERRORS.NUM;
885
+ }
886
+ const np = Math.floor(npery.value);
887
+ return rvNumber(np * (Math.pow(effRate.value + 1, 1 / np) - 1));
888
+ };
889
+ export const fnXNPV = args => {
890
+ const rate = toNumberRV(args[0]);
891
+ if (isError(rate)) {
892
+ return rate;
893
+ }
894
+ if (!isArray(args[1]) || !isArray(args[2])) {
895
+ return ERRORS.VALUE;
896
+ }
897
+ // Excel requires rate > -1. At rate = -1 the discount factor is
898
+ // singular; below -1 `Math.pow(negative, non-integer)` yields NaN
899
+ // for most date offsets.
900
+ if (rate.value <= -1) {
901
+ return ERRORS.NUM;
902
+ }
903
+ const rawValues = flattenNumbers([args[1]]);
904
+ const valuesErr = firstError(rawValues);
905
+ if (valuesErr) {
906
+ return valuesErr;
907
+ }
908
+ const rawDates = flattenNumbers([args[2]]);
909
+ const datesErr = firstError(rawDates);
910
+ if (datesErr) {
911
+ return datesErr;
912
+ }
913
+ const values = rawValues.map(n => n.value);
914
+ const dates = rawDates.map(n => n.value);
915
+ if (values.length === 0 || values.length !== dates.length) {
916
+ return ERRORS.NUM;
917
+ }
918
+ const d0 = dates[0];
919
+ let npv = 0;
920
+ for (let i = 0; i < values.length; i++) {
921
+ npv += values[i] / Math.pow(1 + rate.value, (dates[i] - d0) / 365);
922
+ }
923
+ return isFinite(npv) ? rvNumber(npv) : ERRORS.NUM;
924
+ };
925
+ export const fnXIRR = args => {
926
+ if (!isArray(args[0]) || !isArray(args[1])) {
927
+ return ERRORS.VALUE;
928
+ }
929
+ const rawValues = flattenNumbers([args[0]]);
930
+ const valuesErr = firstError(rawValues);
931
+ if (valuesErr) {
932
+ return valuesErr;
933
+ }
934
+ const rawDates = flattenNumbers([args[1]]);
935
+ const datesErr = firstError(rawDates);
936
+ if (datesErr) {
937
+ return datesErr;
938
+ }
939
+ const values = rawValues.map(n => n.value);
940
+ const dates = rawDates.map(n => n.value);
941
+ if (values.length < 2 || values.length !== dates.length) {
942
+ return ERRORS.NUM;
943
+ }
944
+ // Excel's XIRR requires both at least one positive and at least one
945
+ // negative cash flow, same as IRR. Without this guard Newton would drift
946
+ // toward the singularity at rate = -1 with no valid root.
947
+ let xHasPos = false;
948
+ let xHasNeg = false;
949
+ for (const v of values) {
950
+ if (v > 0) {
951
+ xHasPos = true;
952
+ }
953
+ if (v < 0) {
954
+ xHasNeg = true;
955
+ }
956
+ }
957
+ if (!xHasPos || !xHasNeg) {
958
+ return ERRORS.NUM;
959
+ }
960
+ const guessRV = args.length > 2 ? toNumberRV(args[2]) : rvNumber(0.1);
961
+ if (isError(guessRV)) {
962
+ return guessRV;
963
+ }
964
+ const d0 = dates[0];
965
+ const xnpvAt = (g) => {
966
+ if (g <= -1) {
967
+ return Number.NaN;
968
+ }
969
+ let s = 0;
970
+ for (let i = 0; i < values.length; i++) {
971
+ const t = (dates[i] - d0) / 365;
972
+ const p = Math.pow(1 + g, t);
973
+ if (!isFinite(p)) {
974
+ return Number.NaN;
975
+ }
976
+ s += values[i] / p;
977
+ }
978
+ return s;
979
+ };
980
+ let g = guessRV.value;
981
+ for (let iter = 0; iter < 100; iter++) {
982
+ let npv = 0;
983
+ let dnpv = 0;
984
+ for (let i = 0; i < values.length; i++) {
985
+ const t = (dates[i] - d0) / 365;
986
+ npv += values[i] / Math.pow(1 + g, t);
987
+ dnpv -= (t * values[i]) / Math.pow(1 + g, t + 1);
988
+ }
989
+ if (!isFinite(npv) || !isFinite(dnpv) || Math.abs(dnpv) < 1e-15) {
990
+ break;
991
+ }
992
+ const newG = g - npv / dnpv;
993
+ if (!isFinite(newG) || newG <= -1) {
994
+ break;
995
+ }
996
+ if (Math.abs(newG - g) < 1e-10) {
997
+ return rvNumber(newG);
998
+ }
999
+ g = newG;
1000
+ }
1001
+ // Newton failed — fall back to bisection.
1002
+ const xSampleRates = [-0.99, -0.5, -0.1, 0, 0.1, 0.25, 0.5, 1, 2, 5, 10];
1003
+ let xPrev = xSampleRates[0];
1004
+ let xPrevV = xnpvAt(xPrev);
1005
+ for (let i = 1; i < xSampleRates.length; i++) {
1006
+ const curr = xSampleRates[i];
1007
+ const currV = xnpvAt(curr);
1008
+ if (isFinite(xPrevV) && isFinite(currV) && xPrevV * currV < 0) {
1009
+ let lo = xPrev;
1010
+ let hi = curr;
1011
+ let loV = xPrevV;
1012
+ for (let b = 0; b < 100; b++) {
1013
+ const mid = (lo + hi) / 2;
1014
+ const midV = xnpvAt(mid);
1015
+ if (!isFinite(midV)) {
1016
+ break;
1017
+ }
1018
+ if (Math.abs(midV) < 1e-10 || hi - lo < 1e-12) {
1019
+ return rvNumber(mid);
1020
+ }
1021
+ if (loV * midV < 0) {
1022
+ hi = mid;
1023
+ }
1024
+ else {
1025
+ lo = mid;
1026
+ loV = midV;
1027
+ }
1028
+ }
1029
+ return rvNumber((lo + hi) / 2);
1030
+ }
1031
+ xPrev = curr;
1032
+ xPrevV = currV;
1033
+ }
1034
+ return ERRORS.NUM;
1035
+ };
1036
+ export const fnMIRR = args => {
1037
+ if (!isArray(args[0])) {
1038
+ return ERRORS.VALUE;
1039
+ }
1040
+ const rawValues = flattenNumbers([args[0]]);
1041
+ const valuesErr = firstError(rawValues);
1042
+ if (valuesErr) {
1043
+ return valuesErr;
1044
+ }
1045
+ const values = rawValues.map(n => n.value);
1046
+ const financeRate = toNumberRV(args[1]);
1047
+ if (isError(financeRate)) {
1048
+ return financeRate;
1049
+ }
1050
+ const reinvestRate = toNumberRV(args[2]);
1051
+ if (isError(reinvestRate)) {
1052
+ return reinvestRate;
1053
+ }
1054
+ const n = values.length;
1055
+ if (n < 2) {
1056
+ return ERRORS.NUM;
1057
+ }
1058
+ // Guard the singularities at rate = -1. Without this the pow()s below
1059
+ // become division by zero and produce Infinity/NaN.
1060
+ if (financeRate.value === -1 || reinvestRate.value === -1) {
1061
+ return ERRORS.DIV0;
1062
+ }
1063
+ let npvPos = 0;
1064
+ let npvNeg = 0;
1065
+ for (let i = 0; i < n; i++) {
1066
+ if (values[i] >= 0) {
1067
+ npvPos += values[i] * Math.pow(1 + reinvestRate.value, n - 1 - i);
1068
+ }
1069
+ else {
1070
+ npvNeg += values[i] / Math.pow(1 + financeRate.value, i);
1071
+ }
1072
+ }
1073
+ if (npvNeg === 0) {
1074
+ return ERRORS.DIV0;
1075
+ }
1076
+ return rvNumber(Math.pow(-npvPos / npvNeg, 1 / (n - 1)) - 1);
1077
+ };
1078
+ export const fnISPMT = args => {
1079
+ const rate = toNumberRV(args[0]);
1080
+ if (isError(rate)) {
1081
+ return rate;
1082
+ }
1083
+ const per = toNumberRV(args[1]);
1084
+ if (isError(per)) {
1085
+ return per;
1086
+ }
1087
+ const nper = toNumberRV(args[2]);
1088
+ if (isError(nper)) {
1089
+ return nper;
1090
+ }
1091
+ const pv = toNumberRV(args[3]);
1092
+ if (isError(pv)) {
1093
+ return pv;
1094
+ }
1095
+ // ISPMT's straight-line formula divides by `nper`; zero periods leave
1096
+ // the interest undefined, matching Excel's #DIV/0! on ISPMT(…, 0, …).
1097
+ if (nper.value === 0) {
1098
+ return ERRORS.DIV0;
1099
+ }
1100
+ return rvNumber(pv.value * rate.value * (per.value / nper.value - 1));
1101
+ };
1102
+ export const fnCUMPRINC = args => {
1103
+ const rate = toNumberRV(args[0]);
1104
+ if (isError(rate)) {
1105
+ return rate;
1106
+ }
1107
+ const nper = toNumberRV(args[1]);
1108
+ if (isError(nper)) {
1109
+ return nper;
1110
+ }
1111
+ const pv = toNumberRV(args[2]);
1112
+ if (isError(pv)) {
1113
+ return pv;
1114
+ }
1115
+ const startPeriod = toNumberRV(args[3]);
1116
+ if (isError(startPeriod)) {
1117
+ return startPeriod;
1118
+ }
1119
+ const endPeriod = toNumberRV(args[4]);
1120
+ if (isError(endPeriod)) {
1121
+ return endPeriod;
1122
+ }
1123
+ const type = toNumberRV(args[5]);
1124
+ if (isError(type)) {
1125
+ return type;
1126
+ }
1127
+ if (rate.value <= 0 || nper.value <= 0 || pv.value <= 0) {
1128
+ return ERRORS.NUM;
1129
+ }
1130
+ // Excel requires 1 ≤ start ≤ end ≤ nper and type ∈ {0, 1}.
1131
+ const s = Math.floor(startPeriod.value);
1132
+ const e = Math.floor(endPeriod.value);
1133
+ const n = Math.floor(nper.value);
1134
+ if (s < 1 || e < s || e > n) {
1135
+ return ERRORS.NUM;
1136
+ }
1137
+ if (type.value !== 0 && type.value !== 1) {
1138
+ return ERRORS.NUM;
1139
+ }
1140
+ let cumPrinc = 0;
1141
+ for (let p = s; p <= e; p++) {
1142
+ const pmtVal = pmtRaw(rate.value, nper.value, pv.value, 0, type.value);
1143
+ const ipmtVal = ipmtRaw(rate.value, p, nper.value, pv.value, 0, type.value);
1144
+ cumPrinc += pmtVal - ipmtVal;
1145
+ }
1146
+ return rvNumber(cumPrinc);
1147
+ };
1148
+ export const fnCUMIPMT = args => {
1149
+ const rate = toNumberRV(args[0]);
1150
+ if (isError(rate)) {
1151
+ return rate;
1152
+ }
1153
+ const nper = toNumberRV(args[1]);
1154
+ if (isError(nper)) {
1155
+ return nper;
1156
+ }
1157
+ const pv = toNumberRV(args[2]);
1158
+ if (isError(pv)) {
1159
+ return pv;
1160
+ }
1161
+ const startPeriod = toNumberRV(args[3]);
1162
+ if (isError(startPeriod)) {
1163
+ return startPeriod;
1164
+ }
1165
+ const endPeriod = toNumberRV(args[4]);
1166
+ if (isError(endPeriod)) {
1167
+ return endPeriod;
1168
+ }
1169
+ const type = toNumberRV(args[5]);
1170
+ if (isError(type)) {
1171
+ return type;
1172
+ }
1173
+ if (rate.value <= 0 || nper.value <= 0 || pv.value <= 0) {
1174
+ return ERRORS.NUM;
1175
+ }
1176
+ const sI = Math.floor(startPeriod.value);
1177
+ const eI = Math.floor(endPeriod.value);
1178
+ const nI = Math.floor(nper.value);
1179
+ if (sI < 1 || eI < sI || eI > nI) {
1180
+ return ERRORS.NUM;
1181
+ }
1182
+ if (type.value !== 0 && type.value !== 1) {
1183
+ return ERRORS.NUM;
1184
+ }
1185
+ let cumIpmt = 0;
1186
+ for (let p = sI; p <= eI; p++) {
1187
+ cumIpmt += ipmtRaw(rate.value, p, nper.value, pv.value, 0, type.value);
1188
+ }
1189
+ return rvNumber(cumIpmt);
1190
+ };
1191
+ export const fnDOLLARDE = args => {
1192
+ const fractionalDollar = toNumberRV(args[0]);
1193
+ if (isError(fractionalDollar)) {
1194
+ return fractionalDollar;
1195
+ }
1196
+ const fraction = toNumberRV(args[1]);
1197
+ if (isError(fraction)) {
1198
+ return fraction;
1199
+ }
1200
+ if (fraction.value < 1) {
1201
+ return ERRORS.NUM;
1202
+ }
1203
+ const f = Math.floor(fraction.value);
1204
+ const intPart = Math.trunc(fractionalDollar.value);
1205
+ const fracPart = Math.abs(fractionalDollar.value) - Math.abs(intPart);
1206
+ // The fractional portion of a "fractional dollar" encodes a numerator
1207
+ // with as many digits as the denominator. For fraction = 16, `0.02`
1208
+ // means 2/16 (= 0.125), not 0.02/16 (= 0.00125). Multiplying by
1209
+ // `10^ceil(log10(f))` promotes `0.02` → 2 so the subsequent divide by
1210
+ // `f` recovers the correct rational value. The old code divided
1211
+ // *before* the scale-up, which silently shrank the result by the
1212
+ // power of ten.
1213
+ const scale = f === 1 ? 1 : Math.pow(10, Math.ceil(Math.log10(f)));
1214
+ const numerator = fracPart * scale;
1215
+ const sign = fractionalDollar.value < 0 ? -1 : 1;
1216
+ return rvNumber(sign * (Math.abs(intPart) + numerator / f));
1217
+ };
1218
+ export const fnDOLLARFR = args => {
1219
+ const decimalDollar = toNumberRV(args[0]);
1220
+ if (isError(decimalDollar)) {
1221
+ return decimalDollar;
1222
+ }
1223
+ const fraction = toNumberRV(args[1]);
1224
+ if (isError(fraction)) {
1225
+ return fraction;
1226
+ }
1227
+ if (fraction.value < 1) {
1228
+ return ERRORS.NUM;
1229
+ }
1230
+ const f = Math.floor(fraction.value);
1231
+ const intPart = Math.trunc(decimalDollar.value);
1232
+ const fracPart = Math.abs(decimalDollar.value) - Math.abs(intPart);
1233
+ // DOLLARFR is the inverse of DOLLARDE. The fractional output slot must
1234
+ // encode a numerator in a fixed-width decimal column so that, say,
1235
+ // `2/16 = 0.125` round-trips to `0.02` (two digits after the point
1236
+ // because 16 needs two decimal digits to represent numerators up to
1237
+ // 15). The previous code divided by the scale *after* multiplying,
1238
+ // which moved the numerator into the wrong decimal column and made
1239
+ // DOLLARFR(1.125, 16) return 1.2 instead of 1.02.
1240
+ const scale = f === 1 ? 1 : Math.pow(10, Math.ceil(Math.log10(f)));
1241
+ const numerator = fracPart * f;
1242
+ const sign = decimalDollar.value < 0 ? -1 : 1;
1243
+ return rvNumber(sign * (Math.abs(intPart) + numerator / scale));
1244
+ };
1245
+ /**
1246
+ * Common sanity check for the `basis` argument shared across the
1247
+ * discount/rate family of bond helpers. Excel rejects anything outside
1248
+ * `{0, 1, 2, 3, 4}` with `#NUM!`; previously these functions silently
1249
+ * fell back to basis 0 which produced subtly wrong year-fractions.
1250
+ */
1251
+ function validateBasis(basis) {
1252
+ const b = Math.floor(basis);
1253
+ if (b < 0 || b > 4) {
1254
+ return ERRORS.NUM;
1255
+ }
1256
+ return null;
1257
+ }
1258
+ export const fnDISC = args => {
1259
+ const settlement = toNumberRV(args[0]);
1260
+ if (isError(settlement)) {
1261
+ return settlement;
1262
+ }
1263
+ const maturity = toNumberRV(args[1]);
1264
+ if (isError(maturity)) {
1265
+ return maturity;
1266
+ }
1267
+ const pr = toNumberRV(args[2]);
1268
+ if (isError(pr)) {
1269
+ return pr;
1270
+ }
1271
+ const redemption = toNumberRV(args[3]);
1272
+ if (isError(redemption)) {
1273
+ return redemption;
1274
+ }
1275
+ const basis = args.length > 4 ? toNumberRV(args[4]) : rvNumber(0);
1276
+ if (isError(basis)) {
1277
+ return basis;
1278
+ }
1279
+ const basisErr = validateBasis(basis.value);
1280
+ if (basisErr) {
1281
+ return basisErr;
1282
+ }
1283
+ if (maturity.value <= settlement.value || redemption.value <= 0 || pr.value <= 0) {
1284
+ return ERRORS.NUM;
1285
+ }
1286
+ // Use the same day-count fraction engine the other bond functions rely
1287
+ // on so DISC with basis 0 (30/360) and basis 4 (European 30/360)
1288
+ // actually differ instead of both collapsing to Actual/360.
1289
+ const dcf = dayCountFraction(settlement.value, maturity.value, Math.floor(basis.value));
1290
+ if (dcf <= 0) {
1291
+ return ERRORS.NUM;
1292
+ }
1293
+ return rvNumber((redemption.value - pr.value) / redemption.value / dcf);
1294
+ };
1295
+ export const fnPRICEDISC = args => {
1296
+ const settlement = toNumberRV(args[0]);
1297
+ if (isError(settlement)) {
1298
+ return settlement;
1299
+ }
1300
+ const maturity = toNumberRV(args[1]);
1301
+ if (isError(maturity)) {
1302
+ return maturity;
1303
+ }
1304
+ const disc = toNumberRV(args[2]);
1305
+ if (isError(disc)) {
1306
+ return disc;
1307
+ }
1308
+ const redemption = toNumberRV(args[3]);
1309
+ if (isError(redemption)) {
1310
+ return redemption;
1311
+ }
1312
+ const basis = args.length > 4 ? toNumberRV(args[4]) : rvNumber(0);
1313
+ if (isError(basis)) {
1314
+ return basis;
1315
+ }
1316
+ const basisErr = validateBasis(basis.value);
1317
+ if (basisErr) {
1318
+ return basisErr;
1319
+ }
1320
+ if (maturity.value <= settlement.value || disc.value <= 0 || redemption.value <= 0) {
1321
+ return ERRORS.NUM;
1322
+ }
1323
+ const dcf = dayCountFraction(settlement.value, maturity.value, Math.floor(basis.value));
1324
+ return rvNumber(redemption.value - disc.value * redemption.value * dcf);
1325
+ };
1326
+ export const fnYIELDDISC = args => {
1327
+ const settlement = toNumberRV(args[0]);
1328
+ if (isError(settlement)) {
1329
+ return settlement;
1330
+ }
1331
+ const maturity = toNumberRV(args[1]);
1332
+ if (isError(maturity)) {
1333
+ return maturity;
1334
+ }
1335
+ const pr = toNumberRV(args[2]);
1336
+ if (isError(pr)) {
1337
+ return pr;
1338
+ }
1339
+ const redemption = toNumberRV(args[3]);
1340
+ if (isError(redemption)) {
1341
+ return redemption;
1342
+ }
1343
+ const basis = args.length > 4 ? toNumberRV(args[4]) : rvNumber(0);
1344
+ if (isError(basis)) {
1345
+ return basis;
1346
+ }
1347
+ const basisErr = validateBasis(basis.value);
1348
+ if (basisErr) {
1349
+ return basisErr;
1350
+ }
1351
+ if (maturity.value <= settlement.value || pr.value <= 0 || redemption.value <= 0) {
1352
+ return ERRORS.NUM;
1353
+ }
1354
+ const dcf = dayCountFraction(settlement.value, maturity.value, Math.floor(basis.value));
1355
+ if (dcf <= 0) {
1356
+ return ERRORS.NUM;
1357
+ }
1358
+ return rvNumber((redemption.value - pr.value) / pr.value / dcf);
1359
+ };
1360
+ export const fnRECEIVED = args => {
1361
+ const settlement = toNumberRV(args[0]);
1362
+ if (isError(settlement)) {
1363
+ return settlement;
1364
+ }
1365
+ const maturity = toNumberRV(args[1]);
1366
+ if (isError(maturity)) {
1367
+ return maturity;
1368
+ }
1369
+ const investment = toNumberRV(args[2]);
1370
+ if (isError(investment)) {
1371
+ return investment;
1372
+ }
1373
+ const disc = toNumberRV(args[3]);
1374
+ if (isError(disc)) {
1375
+ return disc;
1376
+ }
1377
+ const basis = args.length > 4 ? toNumberRV(args[4]) : rvNumber(0);
1378
+ if (isError(basis)) {
1379
+ return basis;
1380
+ }
1381
+ const basisErr = validateBasis(basis.value);
1382
+ if (basisErr) {
1383
+ return basisErr;
1384
+ }
1385
+ if (maturity.value <= settlement.value || investment.value <= 0 || disc.value <= 0) {
1386
+ return ERRORS.NUM;
1387
+ }
1388
+ const dcf = dayCountFraction(settlement.value, maturity.value, Math.floor(basis.value));
1389
+ const denom = 1 - disc.value * dcf;
1390
+ if (denom === 0) {
1391
+ return ERRORS.NUM;
1392
+ }
1393
+ return rvNumber(investment.value / denom);
1394
+ };
1395
+ export const fnINTRATE = args => {
1396
+ const settlement = toNumberRV(args[0]);
1397
+ if (isError(settlement)) {
1398
+ return settlement;
1399
+ }
1400
+ const maturity = toNumberRV(args[1]);
1401
+ if (isError(maturity)) {
1402
+ return maturity;
1403
+ }
1404
+ const investment = toNumberRV(args[2]);
1405
+ if (isError(investment)) {
1406
+ return investment;
1407
+ }
1408
+ const redemption = toNumberRV(args[3]);
1409
+ if (isError(redemption)) {
1410
+ return redemption;
1411
+ }
1412
+ const basis = args.length > 4 ? toNumberRV(args[4]) : rvNumber(0);
1413
+ if (isError(basis)) {
1414
+ return basis;
1415
+ }
1416
+ const basisErr = validateBasis(basis.value);
1417
+ if (basisErr) {
1418
+ return basisErr;
1419
+ }
1420
+ if (maturity.value <= settlement.value || investment.value <= 0 || redemption.value <= 0) {
1421
+ return ERRORS.NUM;
1422
+ }
1423
+ const dcf = dayCountFraction(settlement.value, maturity.value, Math.floor(basis.value));
1424
+ if (dcf <= 0) {
1425
+ return ERRORS.NUM;
1426
+ }
1427
+ return rvNumber((redemption.value - investment.value) / investment.value / dcf);
1428
+ };
1429
+ // ============================================================================
1430
+ // Bond Math — day-count conventions and coupon helpers
1431
+ // ============================================================================
1432
+ /**
1433
+ * Day-count fraction between two Excel date serials under a given basis.
1434
+ *
1435
+ * Implements:
1436
+ * basis 0 — US (NASD) 30/360 with end-of-month adjustments.
1437
+ * basis 1 — Actual/Actual (year length from the spanning year).
1438
+ * basis 2 — Actual/360.
1439
+ * basis 3 — Actual/365.
1440
+ * basis 4 — European 30/360.
1441
+ *
1442
+ * Unknown basis values fall back to basis 0 for safety.
1443
+ */
1444
+ function yearBasisDays(basis) {
1445
+ // Approximate denominator used when converting a day-count fraction
1446
+ // back to raw days (for PRICE's A / E accrual ratio). The Actual/
1447
+ // Actual bucket uses 365.25 as an average-year constant; since PRICE
1448
+ // only needs the ratio A/E the small inaccuracy cancels out.
1449
+ switch (basis) {
1450
+ case 0:
1451
+ case 2:
1452
+ case 4:
1453
+ return 360;
1454
+ case 3:
1455
+ return 365;
1456
+ case 1:
1457
+ default:
1458
+ return 365.25;
1459
+ }
1460
+ }
1461
+ function dayCountFraction(startSerial, endSerial, basis) {
1462
+ const startD = toDate(startSerial);
1463
+ const endD = toDate(endSerial);
1464
+ const diffDays = Math.floor(endSerial) - Math.floor(startSerial);
1465
+ switch (basis) {
1466
+ case 1: {
1467
+ // Actual/Actual (ISDA convention). See YEARFRAC in date.ts for the
1468
+ // rationale — the previous simple averaging produced visibly wrong
1469
+ // results like YEARFRAC(2020-01-01, 2021-01-01) ≈ 1.001 instead of 1.
1470
+ const y1 = startD.getUTCFullYear();
1471
+ const y2 = endD.getUTCFullYear();
1472
+ if (y1 === y2) {
1473
+ const yearDays = (Date.UTC(y1 + 1, 0, 1) - Date.UTC(y1, 0, 1)) / 86400000;
1474
+ return diffDays / yearDays;
1475
+ }
1476
+ let leapDays = 0;
1477
+ let nonLeapDays = 0;
1478
+ const sdMs = startD.getTime();
1479
+ const edMs = endD.getTime();
1480
+ for (let y = y1; y <= y2; y++) {
1481
+ const yStart = Math.max(sdMs, Date.UTC(y, 0, 1));
1482
+ const yEnd = Math.min(edMs, Date.UTC(y + 1, 0, 1));
1483
+ if (yEnd <= yStart) {
1484
+ continue;
1485
+ }
1486
+ const d = (yEnd - yStart) / 86400000;
1487
+ const isLeap = (y % 4 === 0 && y % 100 !== 0) || y % 400 === 0;
1488
+ if (isLeap) {
1489
+ leapDays += d;
1490
+ }
1491
+ else {
1492
+ nonLeapDays += d;
1493
+ }
1494
+ }
1495
+ return leapDays / 366 + nonLeapDays / 365;
1496
+ }
1497
+ case 2:
1498
+ return diffDays / 360;
1499
+ case 3:
1500
+ return diffDays / 365;
1501
+ case 4: {
1502
+ const d1 = Math.min(startD.getUTCDate(), 30);
1503
+ const d2 = Math.min(endD.getUTCDate(), 30);
1504
+ const m1 = startD.getUTCMonth() + 1;
1505
+ const m2 = endD.getUTCMonth() + 1;
1506
+ const y1 = startD.getUTCFullYear();
1507
+ const y2 = endD.getUTCFullYear();
1508
+ return ((y2 - y1) * 360 + (m2 - m1) * 30 + (d2 - d1)) / 360;
1509
+ }
1510
+ case 0:
1511
+ default: {
1512
+ let d1 = startD.getUTCDate();
1513
+ const m1 = startD.getUTCMonth() + 1;
1514
+ const y1 = startD.getUTCFullYear();
1515
+ let d2 = endD.getUTCDate();
1516
+ const m2 = endD.getUTCMonth() + 1;
1517
+ const y2 = endD.getUTCFullYear();
1518
+ if (d1 === 31) {
1519
+ d1 = 30;
1520
+ }
1521
+ if (d2 === 31 && d1 >= 30) {
1522
+ d2 = 30;
1523
+ }
1524
+ return ((y2 - y1) * 360 + (m2 - m1) * 30 + (d2 - d1)) / 360;
1525
+ }
1526
+ }
1527
+ }
1528
+ /**
1529
+ * Subtract (or add) `months` from an Excel date serial, clamping to month-end
1530
+ * as needed.
1531
+ *
1532
+ * The round-trip is performed entirely on the UTC timeline: we read UTC
1533
+ * fields from the source date, construct the target date with `Date.UTC`,
1534
+ * and compute the serial as the whole-day difference between the target and
1535
+ * the Excel epoch at UTC midnight (1899-12-30 when date1904 is false,
1536
+ * 1904-01-01 otherwise). Doing this in local time would produce off-by-one
1537
+ * errors in any timezone offset from UTC.
1538
+ */
1539
+ function addMonthsToSerial(serial, months) {
1540
+ const d = toDate(serial);
1541
+ const y = d.getUTCFullYear();
1542
+ const m = d.getUTCMonth();
1543
+ const day = d.getUTCDate();
1544
+ // Determine the calendar day-of-month, clamping to the last day of the
1545
+ // target month so e.g. Jan-31 + 1 month → Feb-28/29 (not Mar-3).
1546
+ const targetYear = y + Math.floor((m + months) / 12);
1547
+ const targetMonth = (((m + months) % 12) + 12) % 12;
1548
+ const lastDay = new Date(Date.UTC(targetYear, targetMonth + 1, 0)).getUTCDate();
1549
+ const targetMs = Date.UTC(targetYear, targetMonth, Math.min(day, lastDay));
1550
+ // Excel serial 0 = 1899-12-30 (1900 epoch) or 1904-01-01 when date1904.
1551
+ const epochMs = isDate1904() ? Date.UTC(1904, 0, 1) : Date.UTC(1899, 11, 30);
1552
+ return Math.round((targetMs - epochMs) / 86400000);
1553
+ }
1554
+ /**
1555
+ * Find the next coupon date on or after settlement, given a maturity date
1556
+ * and coupon frequency (1=annual, 2=semi-annual, 4=quarterly).
1557
+ *
1558
+ * Computed by stepping backward from maturity by (12/frequency) months
1559
+ * until the resulting date is ≤ settlement, then stepping one coupon forward.
1560
+ */
1561
+ function nextCouponAfter(settlement, maturity, frequency) {
1562
+ const stepMonths = Math.round(12 / frequency);
1563
+ let cur = maturity;
1564
+ // Step backward until just before or at settlement.
1565
+ while (cur > settlement) {
1566
+ const prev = addMonthsToSerial(cur, -stepMonths);
1567
+ if (prev <= settlement) {
1568
+ return cur;
1569
+ }
1570
+ cur = prev;
1571
+ }
1572
+ return cur;
1573
+ }
1574
+ /**
1575
+ * Find the previous coupon date (on or before settlement).
1576
+ */
1577
+ function prevCouponOnOrBefore(settlement, maturity, frequency) {
1578
+ const stepMonths = Math.round(12 / frequency);
1579
+ let cur = maturity;
1580
+ while (cur > settlement) {
1581
+ cur = addMonthsToSerial(cur, -stepMonths);
1582
+ }
1583
+ return cur;
1584
+ }
1585
+ /**
1586
+ * Count coupon periods between settlement and maturity (rounded up).
1587
+ */
1588
+ function couponsBetween(settlement, maturity, frequency) {
1589
+ const stepMonths = Math.round(12 / frequency);
1590
+ let count = 0;
1591
+ let cur = maturity;
1592
+ while (cur > settlement) {
1593
+ cur = addMonthsToSerial(cur, -stepMonths);
1594
+ count++;
1595
+ }
1596
+ return count;
1597
+ }
1598
+ /**
1599
+ * Validate the basic bond arguments (frequency and basis). Returns null if
1600
+ * valid, or the appropriate #NUM! error.
1601
+ */
1602
+ function validateBondBasis(frequency, basis) {
1603
+ if (frequency !== 1 && frequency !== 2 && frequency !== 4) {
1604
+ return ERRORS.NUM;
1605
+ }
1606
+ if (basis < 0 || basis > 4) {
1607
+ return ERRORS.NUM;
1608
+ }
1609
+ return null;
1610
+ }
1611
+ /**
1612
+ * PRICE(settlement, maturity, rate, yield, redemption, frequency, [basis])
1613
+ *
1614
+ * Price per $100 face value of a security that pays periodic interest.
1615
+ * Excel formula (standard case, more than one coupon period remaining):
1616
+ *
1617
+ * P = [redemption / (1 + y/f)^(N - 1 + DSC/E)]
1618
+ * + Σ_{k=1..N} [100 * r / f / (1 + y/f)^(k - 1 + DSC/E)]
1619
+ * - 100 * r / f * A/E
1620
+ *
1621
+ * Where:
1622
+ * f = frequency
1623
+ * N = number of coupons from settlement to maturity
1624
+ * DSC = days from settlement to next coupon
1625
+ * E = days in coupon period containing settlement
1626
+ * A = days from beginning of coupon period to settlement
1627
+ */
1628
+ export const fnPRICE = args => {
1629
+ const settlementRV = toNumberRV(args[0]);
1630
+ if (isError(settlementRV)) {
1631
+ return settlementRV;
1632
+ }
1633
+ const maturityRV = toNumberRV(args[1]);
1634
+ if (isError(maturityRV)) {
1635
+ return maturityRV;
1636
+ }
1637
+ const rateRV = toNumberRV(args[2]);
1638
+ if (isError(rateRV)) {
1639
+ return rateRV;
1640
+ }
1641
+ const yieldRV = toNumberRV(args[3]);
1642
+ if (isError(yieldRV)) {
1643
+ return yieldRV;
1644
+ }
1645
+ const redemptionRV = toNumberRV(args[4]);
1646
+ if (isError(redemptionRV)) {
1647
+ return redemptionRV;
1648
+ }
1649
+ const frequencyRV = toNumberRV(args[5]);
1650
+ if (isError(frequencyRV)) {
1651
+ return frequencyRV;
1652
+ }
1653
+ const basisRV = args.length > 6 ? toNumberRV(args[6]) : rvNumber(0);
1654
+ if (isError(basisRV)) {
1655
+ return basisRV;
1656
+ }
1657
+ const settlement = Math.floor(settlementRV.value);
1658
+ const maturity = Math.floor(maturityRV.value);
1659
+ const rate = rateRV.value;
1660
+ const yld = yieldRV.value;
1661
+ const redemption = redemptionRV.value;
1662
+ const frequency = Math.floor(frequencyRV.value);
1663
+ const basis = Math.floor(basisRV.value);
1664
+ if (settlement >= maturity || rate < 0 || yld < 0 || redemption <= 0) {
1665
+ return ERRORS.NUM;
1666
+ }
1667
+ const basisErr = validateBondBasis(frequency, basis);
1668
+ if (basisErr) {
1669
+ return basisErr;
1670
+ }
1671
+ const nextCoupon = nextCouponAfter(settlement, maturity, frequency);
1672
+ const prevCoupon = prevCouponOnOrBefore(settlement, maturity, frequency);
1673
+ // Period length E, accrued A, and days-to-next-coupon DSC must all
1674
+ // honour the selected basis. The previous implementation used raw
1675
+ // `floor(next) - floor(prev)` day counts, which is correct for basis
1676
+ // 1/2/3 (Actual/something) but off by up to ~5 days for basis 0 and 4
1677
+ // (30/360). Using `dayCountFraction` — which already implements every
1678
+ // basis — keeps PRICE consistent with YIELD and the rest of the bond
1679
+ // family.
1680
+ const e = dayCountFraction(prevCoupon, nextCoupon, basis) * yearBasisDays(basis);
1681
+ const a = dayCountFraction(prevCoupon, settlement, basis) * yearBasisDays(basis);
1682
+ const dscDays = e - a;
1683
+ // Fractional position in period (DSC/E).
1684
+ const dscE = e === 0 ? 0 : dscDays / e;
1685
+ const N = couponsBetween(settlement, maturity, frequency);
1686
+ const couponAmt = (100 * rate) / frequency;
1687
+ const discountBase = 1 + yld / frequency;
1688
+ let price = redemption / Math.pow(discountBase, N - 1 + dscE);
1689
+ for (let k = 1; k <= N; k++) {
1690
+ price += couponAmt / Math.pow(discountBase, k - 1 + dscE);
1691
+ }
1692
+ price -= couponAmt * (a / e);
1693
+ return rvNumber(price);
1694
+ };
1695
+ /**
1696
+ * YIELD(settlement, maturity, rate, pr, redemption, frequency, [basis])
1697
+ *
1698
+ * Inverse of PRICE: solve numerically for yield such that PRICE(...y) = pr.
1699
+ * Uses bracketed bisection in [0, 1] (100% yield upper bound covers all
1700
+ * realistic bond scenarios) followed by a light Newton polish.
1701
+ */
1702
+ export const fnYIELD = args => {
1703
+ const settlementRV = toNumberRV(args[0]);
1704
+ if (isError(settlementRV)) {
1705
+ return settlementRV;
1706
+ }
1707
+ const maturityRV = toNumberRV(args[1]);
1708
+ if (isError(maturityRV)) {
1709
+ return maturityRV;
1710
+ }
1711
+ const rateRV = toNumberRV(args[2]);
1712
+ if (isError(rateRV)) {
1713
+ return rateRV;
1714
+ }
1715
+ const prRV = toNumberRV(args[3]);
1716
+ if (isError(prRV)) {
1717
+ return prRV;
1718
+ }
1719
+ const redemptionRV = toNumberRV(args[4]);
1720
+ if (isError(redemptionRV)) {
1721
+ return redemptionRV;
1722
+ }
1723
+ const frequencyRV = toNumberRV(args[5]);
1724
+ if (isError(frequencyRV)) {
1725
+ return frequencyRV;
1726
+ }
1727
+ const basisRV = args.length > 6 ? toNumberRV(args[6]) : rvNumber(0);
1728
+ if (isError(basisRV)) {
1729
+ return basisRV;
1730
+ }
1731
+ if (prRV.value <= 0 || redemptionRV.value <= 0) {
1732
+ return ERRORS.NUM;
1733
+ }
1734
+ const priceAt = (y) => {
1735
+ const result = fnPRICE([
1736
+ settlementRV,
1737
+ maturityRV,
1738
+ rateRV,
1739
+ rvNumber(y),
1740
+ redemptionRV,
1741
+ frequencyRV,
1742
+ basisRV
1743
+ ]);
1744
+ if (result.kind !== RVKind.Number) {
1745
+ return NaN;
1746
+ }
1747
+ return result.value;
1748
+ };
1749
+ // Bisection in [0, 1].
1750
+ let lo = 0;
1751
+ let hi = 1;
1752
+ const fLo = priceAt(lo) - prRV.value;
1753
+ const fHi = priceAt(hi) - prRV.value;
1754
+ if (isNaN(fLo) || isNaN(fHi)) {
1755
+ return ERRORS.NUM;
1756
+ }
1757
+ if (fLo * fHi > 0) {
1758
+ // Try extending upper bound.
1759
+ hi = 10;
1760
+ if ((priceAt(hi) - prRV.value) * fLo > 0) {
1761
+ return ERRORS.NUM;
1762
+ }
1763
+ }
1764
+ for (let i = 0; i < 200; i++) {
1765
+ const mid = (lo + hi) / 2;
1766
+ const f = priceAt(mid) - prRV.value;
1767
+ if (isNaN(f)) {
1768
+ return ERRORS.NUM;
1769
+ }
1770
+ // PRICE decreases as yield increases, so f = priceAtY - pr decreases.
1771
+ if (f > 0) {
1772
+ lo = mid;
1773
+ }
1774
+ else {
1775
+ hi = mid;
1776
+ }
1777
+ if (hi - lo < 1e-12) {
1778
+ break;
1779
+ }
1780
+ }
1781
+ return rvNumber((lo + hi) / 2);
1782
+ };
1783
+ /**
1784
+ * DURATION(settlement, maturity, coupon, yield, frequency, [basis])
1785
+ *
1786
+ * Macaulay duration of a bond: the weighted average time to cash flows,
1787
+ * weighted by present value. Expressed in years.
1788
+ */
1789
+ export const fnDURATION = args => {
1790
+ const settlementRV = toNumberRV(args[0]);
1791
+ if (isError(settlementRV)) {
1792
+ return settlementRV;
1793
+ }
1794
+ const maturityRV = toNumberRV(args[1]);
1795
+ if (isError(maturityRV)) {
1796
+ return maturityRV;
1797
+ }
1798
+ const couponRV = toNumberRV(args[2]);
1799
+ if (isError(couponRV)) {
1800
+ return couponRV;
1801
+ }
1802
+ const yieldRV = toNumberRV(args[3]);
1803
+ if (isError(yieldRV)) {
1804
+ return yieldRV;
1805
+ }
1806
+ const frequencyRV = toNumberRV(args[4]);
1807
+ if (isError(frequencyRV)) {
1808
+ return frequencyRV;
1809
+ }
1810
+ const basisRV = args.length > 5 ? toNumberRV(args[5]) : rvNumber(0);
1811
+ if (isError(basisRV)) {
1812
+ return basisRV;
1813
+ }
1814
+ const settlement = Math.floor(settlementRV.value);
1815
+ const maturity = Math.floor(maturityRV.value);
1816
+ const coupon = couponRV.value;
1817
+ const yld = yieldRV.value;
1818
+ const frequency = Math.floor(frequencyRV.value);
1819
+ const basis = Math.floor(basisRV.value);
1820
+ if (settlement >= maturity || coupon < 0 || yld < 0) {
1821
+ return ERRORS.NUM;
1822
+ }
1823
+ const basisErr = validateBondBasis(frequency, basis);
1824
+ if (basisErr) {
1825
+ return basisErr;
1826
+ }
1827
+ const nextCoupon = nextCouponAfter(settlement, maturity, frequency);
1828
+ const prevCoupon = prevCouponOnOrBefore(settlement, maturity, frequency);
1829
+ const periodDays = Math.floor(nextCoupon) - Math.floor(prevCoupon);
1830
+ const dscDays = Math.floor(nextCoupon) - settlement;
1831
+ const dscE = periodDays === 0 ? 0 : dscDays / periodDays;
1832
+ const N = couponsBetween(settlement, maturity, frequency);
1833
+ const couponPerPeriod = (100 * coupon) / frequency;
1834
+ const discountBase = 1 + yld / frequency;
1835
+ let pv = 0;
1836
+ let weighted = 0;
1837
+ for (let k = 1; k <= N; k++) {
1838
+ const t = (k - 1 + dscE) / frequency; // time in years
1839
+ const cf = k === N ? couponPerPeriod + 100 : couponPerPeriod;
1840
+ const df = Math.pow(discountBase, k - 1 + dscE);
1841
+ pv += cf / df;
1842
+ weighted += (t * cf) / df;
1843
+ }
1844
+ if (pv === 0) {
1845
+ return ERRORS.NUM;
1846
+ }
1847
+ return rvNumber(weighted / pv);
1848
+ };
1849
+ /**
1850
+ * MDURATION — modified duration = DURATION / (1 + yield/frequency).
1851
+ */
1852
+ export const fnMDURATION = args => {
1853
+ const dur = fnDURATION(args);
1854
+ if (dur.kind !== RVKind.Number) {
1855
+ return dur;
1856
+ }
1857
+ const yieldRV = toNumberRV(args[3]);
1858
+ if (isError(yieldRV)) {
1859
+ return yieldRV;
1860
+ }
1861
+ const frequencyRV = toNumberRV(args[4]);
1862
+ if (isError(frequencyRV)) {
1863
+ return frequencyRV;
1864
+ }
1865
+ return rvNumber(dur.value / (1 + yieldRV.value / frequencyRV.value));
1866
+ };
1867
+ /**
1868
+ * ACCRINT(issue, first_interest, settlement, rate, par, frequency, [basis], [calc_method])
1869
+ *
1870
+ * Accrued interest for a security that pays periodic interest.
1871
+ * The simplified implementation (calc_method TRUE, the default) treats
1872
+ * accrued interest from issue to settlement as par * rate * dcf(issue, settlement, basis).
1873
+ */
1874
+ export const fnACCRINT = args => {
1875
+ const issueRV = toNumberRV(args[0]);
1876
+ if (isError(issueRV)) {
1877
+ return issueRV;
1878
+ }
1879
+ // first_interest (args[1]) is unused in the simplified implementation.
1880
+ const settlementRV = toNumberRV(args[2]);
1881
+ if (isError(settlementRV)) {
1882
+ return settlementRV;
1883
+ }
1884
+ const rateRV = toNumberRV(args[3]);
1885
+ if (isError(rateRV)) {
1886
+ return rateRV;
1887
+ }
1888
+ const parRV = toNumberRV(args[4]);
1889
+ if (isError(parRV)) {
1890
+ return parRV;
1891
+ }
1892
+ const frequencyRV = toNumberRV(args[5]);
1893
+ if (isError(frequencyRV)) {
1894
+ return frequencyRV;
1895
+ }
1896
+ const basisRV = args.length > 6 ? toNumberRV(args[6]) : rvNumber(0);
1897
+ if (isError(basisRV)) {
1898
+ return basisRV;
1899
+ }
1900
+ // calc_method (args[7]) — we ignore the distinction between TRUE/FALSE
1901
+ // because the simplified semantics always accrue from issue date.
1902
+ const issue = Math.floor(issueRV.value);
1903
+ const settlement = Math.floor(settlementRV.value);
1904
+ const frequency = Math.floor(frequencyRV.value);
1905
+ const basis = Math.floor(basisRV.value);
1906
+ if (issue >= settlement || rateRV.value <= 0 || parRV.value <= 0) {
1907
+ return ERRORS.NUM;
1908
+ }
1909
+ const basisErr = validateBondBasis(frequency, basis);
1910
+ if (basisErr) {
1911
+ return basisErr;
1912
+ }
1913
+ const dcf = dayCountFraction(issue, settlement, basis);
1914
+ return rvNumber(parRV.value * rateRV.value * dcf);
1915
+ };
1916
+ /**
1917
+ * ACCRINTM(issue, settlement, rate, par, [basis]) — accrued interest
1918
+ * for a security that pays interest at maturity.
1919
+ * result = par × rate × dayCountFraction(issue, settlement, basis)
1920
+ */
1921
+ export const fnACCRINTM = args => {
1922
+ const issueRV = toNumberRV(args[0]);
1923
+ if (isError(issueRV)) {
1924
+ return issueRV;
1925
+ }
1926
+ const settlementRV = toNumberRV(args[1]);
1927
+ if (isError(settlementRV)) {
1928
+ return settlementRV;
1929
+ }
1930
+ const rateRV = toNumberRV(args[2]);
1931
+ if (isError(rateRV)) {
1932
+ return rateRV;
1933
+ }
1934
+ const parRV = toNumberRV(args[3]);
1935
+ if (isError(parRV)) {
1936
+ return parRV;
1937
+ }
1938
+ const basisRV = args.length > 4 ? toNumberRV(args[4]) : rvNumber(0);
1939
+ if (isError(basisRV)) {
1940
+ return basisRV;
1941
+ }
1942
+ const issue = Math.floor(issueRV.value);
1943
+ const settlement = Math.floor(settlementRV.value);
1944
+ const basis = Math.floor(basisRV.value);
1945
+ if (issue >= settlement || rateRV.value <= 0 || parRV.value <= 0) {
1946
+ return ERRORS.NUM;
1947
+ }
1948
+ if (basis < 0 || basis > 4) {
1949
+ return ERRORS.NUM;
1950
+ }
1951
+ const dcf = dayCountFraction(issue, settlement, basis);
1952
+ return rvNumber(parRV.value * rateRV.value * dcf);
1953
+ };
1954
+ /**
1955
+ * TBILLPRICE(settlement, maturity, discount) — price per $100 face value.
1956
+ * price = 100 × (1 - discount × DSM / 360)
1957
+ * where DSM is days from settlement to maturity.
1958
+ */
1959
+ export const fnTBILLPRICE = args => {
1960
+ const settlementRV = toNumberRV(args[0]);
1961
+ if (isError(settlementRV)) {
1962
+ return settlementRV;
1963
+ }
1964
+ const maturityRV = toNumberRV(args[1]);
1965
+ if (isError(maturityRV)) {
1966
+ return maturityRV;
1967
+ }
1968
+ const discountRV = toNumberRV(args[2]);
1969
+ if (isError(discountRV)) {
1970
+ return discountRV;
1971
+ }
1972
+ const settlement = Math.floor(settlementRV.value);
1973
+ const maturity = Math.floor(maturityRV.value);
1974
+ const discount = discountRV.value;
1975
+ if (settlement >= maturity || discount <= 0) {
1976
+ return ERRORS.NUM;
1977
+ }
1978
+ // T-bills have a max maturity of 1 year (~365 days).
1979
+ const dsm = maturity - settlement;
1980
+ if (dsm > 365) {
1981
+ return ERRORS.NUM;
1982
+ }
1983
+ const price = 100 * (1 - (discount * dsm) / 360);
1984
+ if (price <= 0) {
1985
+ return ERRORS.NUM;
1986
+ }
1987
+ return rvNumber(price);
1988
+ };
1989
+ /**
1990
+ * TBILLYIELD(settlement, maturity, pr) — bond-equivalent yield.
1991
+ * yield = (100 - pr) / pr × (360 / DSM)
1992
+ */
1993
+ export const fnTBILLYIELD = args => {
1994
+ const settlementRV = toNumberRV(args[0]);
1995
+ if (isError(settlementRV)) {
1996
+ return settlementRV;
1997
+ }
1998
+ const maturityRV = toNumberRV(args[1]);
1999
+ if (isError(maturityRV)) {
2000
+ return maturityRV;
2001
+ }
2002
+ const prRV = toNumberRV(args[2]);
2003
+ if (isError(prRV)) {
2004
+ return prRV;
2005
+ }
2006
+ const settlement = Math.floor(settlementRV.value);
2007
+ const maturity = Math.floor(maturityRV.value);
2008
+ const pr = prRV.value;
2009
+ if (settlement >= maturity || pr <= 0) {
2010
+ return ERRORS.NUM;
2011
+ }
2012
+ const dsm = maturity - settlement;
2013
+ if (dsm > 365) {
2014
+ return ERRORS.NUM;
2015
+ }
2016
+ return rvNumber(((100 - pr) / pr) * (360 / dsm));
2017
+ };
2018
+ /**
2019
+ * TBILLEQ(settlement, maturity, discount) — bond equivalent yield.
2020
+ * TBILLEQ = (365 × discount) / (360 - discount × DSM)
2021
+ */
2022
+ export const fnTBILLEQ = args => {
2023
+ const settlementRV = toNumberRV(args[0]);
2024
+ if (isError(settlementRV)) {
2025
+ return settlementRV;
2026
+ }
2027
+ const maturityRV = toNumberRV(args[1]);
2028
+ if (isError(maturityRV)) {
2029
+ return maturityRV;
2030
+ }
2031
+ const discountRV = toNumberRV(args[2]);
2032
+ if (isError(discountRV)) {
2033
+ return discountRV;
2034
+ }
2035
+ const settlement = Math.floor(settlementRV.value);
2036
+ const maturity = Math.floor(maturityRV.value);
2037
+ const discount = discountRV.value;
2038
+ if (settlement >= maturity || discount <= 0) {
2039
+ return ERRORS.NUM;
2040
+ }
2041
+ const dsm = maturity - settlement;
2042
+ if (dsm > 365) {
2043
+ return ERRORS.NUM;
2044
+ }
2045
+ const denom = 360 - discount * dsm;
2046
+ if (denom <= 0) {
2047
+ return ERRORS.NUM;
2048
+ }
2049
+ return rvNumber((365 * discount) / denom);
2050
+ };
2051
+ /**
2052
+ * PRICEMAT(settlement, maturity, issue, rate, yld, [basis]) —
2053
+ * price per $100 face value for a security that pays interest at maturity.
2054
+ *
2055
+ * A = DCF(issue, settlement, basis)
2056
+ * DSM = DCF(settlement, maturity, basis)
2057
+ * DIM = DCF(issue, maturity, basis)
2058
+ * price = (100 + DIM × rate × 100) / (1 + DSM × yld) - A × rate × 100
2059
+ */
2060
+ export const fnPRICEMAT = args => {
2061
+ const settlementRV = toNumberRV(args[0]);
2062
+ if (isError(settlementRV)) {
2063
+ return settlementRV;
2064
+ }
2065
+ const maturityRV = toNumberRV(args[1]);
2066
+ if (isError(maturityRV)) {
2067
+ return maturityRV;
2068
+ }
2069
+ const issueRV = toNumberRV(args[2]);
2070
+ if (isError(issueRV)) {
2071
+ return issueRV;
2072
+ }
2073
+ const rateRV = toNumberRV(args[3]);
2074
+ if (isError(rateRV)) {
2075
+ return rateRV;
2076
+ }
2077
+ const yldRV = toNumberRV(args[4]);
2078
+ if (isError(yldRV)) {
2079
+ return yldRV;
2080
+ }
2081
+ const basisRV = args.length > 5 ? toNumberRV(args[5]) : rvNumber(0);
2082
+ if (isError(basisRV)) {
2083
+ return basisRV;
2084
+ }
2085
+ const settlement = Math.floor(settlementRV.value);
2086
+ const maturity = Math.floor(maturityRV.value);
2087
+ const issue = Math.floor(issueRV.value);
2088
+ const rate = rateRV.value;
2089
+ const yld = yldRV.value;
2090
+ const basis = Math.floor(basisRV.value);
2091
+ if (settlement >= maturity || issue >= settlement) {
2092
+ return ERRORS.NUM;
2093
+ }
2094
+ if (rate < 0 || yld < 0) {
2095
+ return ERRORS.NUM;
2096
+ }
2097
+ if (basis < 0 || basis > 4) {
2098
+ return ERRORS.NUM;
2099
+ }
2100
+ const a = dayCountFraction(issue, settlement, basis);
2101
+ const dsm = dayCountFraction(settlement, maturity, basis);
2102
+ const dim = dayCountFraction(issue, maturity, basis);
2103
+ const numerator = 100 + dim * rate * 100;
2104
+ const denominator = 1 + dsm * yld;
2105
+ return rvNumber(numerator / denominator - a * rate * 100);
2106
+ };
2107
+ /**
2108
+ * YIELDMAT(settlement, maturity, issue, rate, pr, [basis]) —
2109
+ * annual yield for a security that pays interest at maturity.
2110
+ *
2111
+ * A = DCF(issue, settlement, basis)
2112
+ * DSM = DCF(settlement, maturity, basis)
2113
+ * DIM = DCF(issue, maturity, basis)
2114
+ * yield = ((1 + DIM × rate) / (pr/100 + A × rate) - 1) / DSM
2115
+ */
2116
+ export const fnYIELDMAT = args => {
2117
+ const settlementRV = toNumberRV(args[0]);
2118
+ if (isError(settlementRV)) {
2119
+ return settlementRV;
2120
+ }
2121
+ const maturityRV = toNumberRV(args[1]);
2122
+ if (isError(maturityRV)) {
2123
+ return maturityRV;
2124
+ }
2125
+ const issueRV = toNumberRV(args[2]);
2126
+ if (isError(issueRV)) {
2127
+ return issueRV;
2128
+ }
2129
+ const rateRV = toNumberRV(args[3]);
2130
+ if (isError(rateRV)) {
2131
+ return rateRV;
2132
+ }
2133
+ const prRV = toNumberRV(args[4]);
2134
+ if (isError(prRV)) {
2135
+ return prRV;
2136
+ }
2137
+ const basisRV = args.length > 5 ? toNumberRV(args[5]) : rvNumber(0);
2138
+ if (isError(basisRV)) {
2139
+ return basisRV;
2140
+ }
2141
+ const settlement = Math.floor(settlementRV.value);
2142
+ const maturity = Math.floor(maturityRV.value);
2143
+ const issue = Math.floor(issueRV.value);
2144
+ const rate = rateRV.value;
2145
+ const pr = prRV.value;
2146
+ const basis = Math.floor(basisRV.value);
2147
+ if (settlement >= maturity || issue >= settlement) {
2148
+ return ERRORS.NUM;
2149
+ }
2150
+ if (rate < 0 || pr <= 0) {
2151
+ return ERRORS.NUM;
2152
+ }
2153
+ if (basis < 0 || basis > 4) {
2154
+ return ERRORS.NUM;
2155
+ }
2156
+ const a = dayCountFraction(issue, settlement, basis);
2157
+ const dsm = dayCountFraction(settlement, maturity, basis);
2158
+ const dim = dayCountFraction(issue, maturity, basis);
2159
+ if (dsm <= 0) {
2160
+ return ERRORS.NUM;
2161
+ }
2162
+ const numer = 1 + dim * rate;
2163
+ const denom = pr / 100 + a * rate;
2164
+ if (denom <= 0) {
2165
+ return ERRORS.NUM;
2166
+ }
2167
+ return rvNumber((numer / denom - 1) / dsm);
2168
+ };
2169
+ // ============================================================================
2170
+ // COUP family — coupon-period queries
2171
+ // ============================================================================
2172
+ /**
2173
+ * Shared validation + parsing for the COUP family. Returns the parsed
2174
+ * settlement/maturity/frequency/basis values, or an error.
2175
+ */
2176
+ function parseCoupArgs(args) {
2177
+ const settlementRV = toNumberRV(args[0]);
2178
+ if (isError(settlementRV)) {
2179
+ return settlementRV;
2180
+ }
2181
+ const maturityRV = toNumberRV(args[1]);
2182
+ if (isError(maturityRV)) {
2183
+ return maturityRV;
2184
+ }
2185
+ const frequencyRV = toNumberRV(args[2]);
2186
+ if (isError(frequencyRV)) {
2187
+ return frequencyRV;
2188
+ }
2189
+ const basisRV = args.length > 3 ? toNumberRV(args[3]) : rvNumber(0);
2190
+ if (isError(basisRV)) {
2191
+ return basisRV;
2192
+ }
2193
+ const settlement = Math.floor(settlementRV.value);
2194
+ const maturity = Math.floor(maturityRV.value);
2195
+ const frequency = Math.floor(frequencyRV.value);
2196
+ const basis = Math.floor(basisRV.value);
2197
+ if (settlement >= maturity) {
2198
+ return ERRORS.NUM;
2199
+ }
2200
+ const err = validateBondBasis(frequency, basis);
2201
+ if (err) {
2202
+ return err;
2203
+ }
2204
+ return { settlement, maturity, frequency, basis };
2205
+ }
2206
+ /**
2207
+ * COUPNCD(settlement, maturity, frequency, [basis]) — next coupon date
2208
+ * after settlement, as an Excel serial.
2209
+ */
2210
+ export const fnCOUPNCD = args => {
2211
+ const p = parseCoupArgs(args);
2212
+ if ("kind" in p) {
2213
+ return p;
2214
+ }
2215
+ return rvNumber(nextCouponAfter(p.settlement, p.maturity, p.frequency));
2216
+ };
2217
+ /**
2218
+ * COUPPCD(settlement, maturity, frequency, [basis]) — previous coupon
2219
+ * date on or before settlement, as an Excel serial.
2220
+ */
2221
+ export const fnCOUPPCD = args => {
2222
+ const p = parseCoupArgs(args);
2223
+ if ("kind" in p) {
2224
+ return p;
2225
+ }
2226
+ return rvNumber(prevCouponOnOrBefore(p.settlement, p.maturity, p.frequency));
2227
+ };
2228
+ /**
2229
+ * COUPNUM(settlement, maturity, frequency, [basis]) — number of
2230
+ * coupons payable between settlement and maturity, rounded up.
2231
+ */
2232
+ export const fnCOUPNUM = args => {
2233
+ const p = parseCoupArgs(args);
2234
+ if ("kind" in p) {
2235
+ return p;
2236
+ }
2237
+ return rvNumber(couponsBetween(p.settlement, p.maturity, p.frequency));
2238
+ };
2239
+ /**
2240
+ * COUPDAYSNC(settlement, maturity, frequency, [basis]) — days from
2241
+ * settlement to the next coupon date.
2242
+ */
2243
+ export const fnCOUPDAYSNC = args => {
2244
+ const p = parseCoupArgs(args);
2245
+ if ("kind" in p) {
2246
+ return p;
2247
+ }
2248
+ const next = nextCouponAfter(p.settlement, p.maturity, p.frequency);
2249
+ if (p.basis === 0 || p.basis === 4) {
2250
+ // 30/360 methods — Excel treats the period day-count via NASD-style
2251
+ // adjustment; dayCountFraction already handles this. Multiply by
2252
+ // 360 (days in 30/360 "year") to get days.
2253
+ return rvNumber(dayCountFraction(p.settlement, next, p.basis) * 360);
2254
+ }
2255
+ // Actual day count: plain serial difference works for basis 1/2/3.
2256
+ return rvNumber(next - p.settlement);
2257
+ };
2258
+ /**
2259
+ * COUPDAYBS(settlement, maturity, frequency, [basis]) — days from the
2260
+ * beginning of the coupon period to settlement.
2261
+ */
2262
+ export const fnCOUPDAYBS = args => {
2263
+ const p = parseCoupArgs(args);
2264
+ if ("kind" in p) {
2265
+ return p;
2266
+ }
2267
+ const prev = prevCouponOnOrBefore(p.settlement, p.maturity, p.frequency);
2268
+ if (p.basis === 0 || p.basis === 4) {
2269
+ return rvNumber(dayCountFraction(prev, p.settlement, p.basis) * 360);
2270
+ }
2271
+ return rvNumber(p.settlement - prev);
2272
+ };
2273
+ /**
2274
+ * COUPDAYS(settlement, maturity, frequency, [basis]) — days in the
2275
+ * coupon period that contains settlement.
2276
+ */
2277
+ export const fnCOUPDAYS = args => {
2278
+ const p = parseCoupArgs(args);
2279
+ if ("kind" in p) {
2280
+ return p;
2281
+ }
2282
+ const prev = prevCouponOnOrBefore(p.settlement, p.maturity, p.frequency);
2283
+ const next = nextCouponAfter(p.settlement, p.maturity, p.frequency);
2284
+ if (p.basis === 1) {
2285
+ // Actual/actual — actual days between the two coupon dates.
2286
+ return rvNumber(next - prev);
2287
+ }
2288
+ if (p.basis === 2 || p.basis === 3) {
2289
+ // Actual/360 or actual/365 — the period length is conventionally the
2290
+ // period basis divided by frequency (e.g. 360/freq, 365/freq).
2291
+ const yearDays = p.basis === 2 ? 360 : 365;
2292
+ return rvNumber(yearDays / p.frequency);
2293
+ }
2294
+ // 30/360 NASD or 30/360 European: 360/freq conventional.
2295
+ return rvNumber(360 / p.frequency);
2296
+ };