@veltra/sheet 2.0.0 → 2.1.1

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 (203) hide show
  1. package/dist/index.d.ts +4 -25
  2. package/dist/index.js +3 -22
  3. package/dist/tools/builtin.js +279 -8
  4. package/dist/tools/context.d.ts +54 -11
  5. package/dist/tools/context.js +28 -5
  6. package/dist/tools/download.js +68 -0
  7. package/dist/tools/export.worker.d.ts +16 -0
  8. package/dist/tools/export.worker.js +29 -0
  9. package/dist/tools/registry.d.ts +7 -4
  10. package/dist/tools/registry.js +1 -3
  11. package/dist/types/index.d.ts +1 -0
  12. package/dist/types/sheet.d.ts +6 -5
  13. package/dist/vue/formula-bar.js +393 -0
  14. package/dist/vue/formula-suggest-list.js +66 -0
  15. package/dist/vue/import-file.js +140 -0
  16. package/dist/vue/insert-count-menu-item.js +68 -0
  17. package/dist/vue/insert-image.js +79 -0
  18. package/dist/vue/popup-helpers.js +73 -0
  19. package/dist/vue/popups/border-popup.js +75 -0
  20. package/dist/vue/popups/border-preset-glyph.js +106 -0
  21. package/dist/vue/popups/export-popup.js +51 -0
  22. package/dist/vue/popups/fill-color-popup.js +37 -0
  23. package/dist/vue/popups/find-popup.js +113 -0
  24. package/dist/vue/popups/font-color-popup.js +37 -0
  25. package/dist/vue/popups/font-size-popup.js +63 -0
  26. package/dist/vue/popups/import.worker.d.ts +21 -0
  27. package/dist/vue/popups/import.worker.js +41 -0
  28. package/dist/vue/popups/insert-image-popup.js +38 -0
  29. package/dist/vue/sheet-context-menu.js +200 -0
  30. package/dist/vue/sheet-tabs.js +162 -0
  31. package/dist/vue/sheet-toolbar.js +106 -0
  32. package/dist/vue/sheet.js +144 -138
  33. package/dist/vue/style.d.ts +11 -1
  34. package/dist/vue/style.js +11 -0
  35. package/dist/vue/style2.css +564 -3
  36. package/dist/vue/use-find-replace.js +161 -0
  37. package/dist/vue/use-formula-ref-select.js +37 -0
  38. package/dist/vue/use-formula-suggest.js +126 -0
  39. package/dist/vue/use-sheet-grid.js +180 -0
  40. package/dist/vue/use-sheet-state.js +122 -0
  41. package/dist/vue/use-sheet-tabs-bar.js +90 -0
  42. package/dist/vue/use-tool-groups.js +42 -0
  43. package/dist/vue/use-tool-popup.js +123 -0
  44. package/dist/vue/use-toolbar-scroll.js +72 -0
  45. package/package.json +12 -6
  46. package/src/env.d.ts +14 -0
  47. package/src/index.ts +203 -13
  48. package/src/tools/__test__/builtin.test.ts +208 -66
  49. package/src/tools/__test__/context.test.ts +62 -2
  50. package/src/tools/builtin.ts +350 -6
  51. package/src/tools/context.ts +89 -11
  52. package/src/tools/download.ts +82 -0
  53. package/src/tools/export.worker.ts +61 -0
  54. package/src/tools/registry.ts +16 -1
  55. package/src/types/index.ts +6 -0
  56. package/src/types/sheet.ts +5 -3
  57. package/src/vue/__test__/formula-bar.test.ts +455 -0
  58. package/src/vue/__test__/import-file.test.ts +183 -0
  59. package/src/vue/__test__/insert-count-menu-item.test.ts +123 -0
  60. package/src/vue/__test__/insert-image-popup.test.ts +89 -0
  61. package/src/vue/__test__/insert-image.test.ts +100 -0
  62. package/src/vue/__test__/sheet-component.test.ts +716 -27
  63. package/src/vue/__test__/sheet-context-menu.test.ts +206 -0
  64. package/src/vue/__test__/use-formula-ref-select.test.ts +35 -0
  65. package/src/vue/__test__/use-formula-suggest.test.ts +83 -0
  66. package/src/vue/formula-bar.vue +473 -0
  67. package/src/vue/formula-suggest-list.vue +64 -0
  68. package/src/vue/import-file.ts +173 -0
  69. package/src/vue/insert-count-menu-item.vue +69 -0
  70. package/src/vue/insert-image.ts +90 -0
  71. package/src/vue/parsing.ts +11 -0
  72. package/src/vue/popup-helpers.ts +50 -0
  73. package/src/vue/popups/border-popup.vue +93 -0
  74. package/src/vue/popups/border-preset-glyph.vue +111 -0
  75. package/src/vue/popups/export-popup.vue +52 -0
  76. package/src/vue/popups/fill-color-popup.vue +39 -0
  77. package/src/vue/popups/find-popup.vue +111 -0
  78. package/src/vue/popups/font-color-popup.vue +39 -0
  79. package/src/vue/popups/font-size-popup.vue +52 -0
  80. package/src/vue/popups/import.worker.ts +66 -0
  81. package/src/vue/popups/insert-image-popup.vue +39 -0
  82. package/src/vue/sheet-context-menu.ts +229 -0
  83. package/src/vue/sheet-tabs.vue +162 -0
  84. package/src/vue/sheet-toolbar.vue +91 -0
  85. package/src/vue/sheet.vue +163 -148
  86. package/src/vue/style.scss +646 -3
  87. package/src/vue/style.ts +12 -0
  88. package/src/vue/use-find-replace.ts +200 -0
  89. package/src/vue/use-formula-ref-select.ts +34 -0
  90. package/src/vue/use-formula-suggest.ts +150 -0
  91. package/src/vue/use-sheet-grid.ts +243 -0
  92. package/src/vue/use-sheet-state.ts +161 -0
  93. package/src/vue/use-sheet-tabs-bar.ts +115 -0
  94. package/src/vue/use-tool-groups.ts +61 -0
  95. package/src/vue/use-tool-popup.ts +150 -0
  96. package/src/vue/use-toolbar-scroll.ts +95 -0
  97. package/dist/core/address.d.ts +0 -60
  98. package/dist/core/address.js +0 -142
  99. package/dist/core/address.js.map +0 -1
  100. package/dist/core/cell-store.d.ts +0 -61
  101. package/dist/core/cell-store.js +0 -135
  102. package/dist/core/cell-store.js.map +0 -1
  103. package/dist/core/command/default-registry.d.ts +0 -10
  104. package/dist/core/command/default-registry.js +0 -18
  105. package/dist/core/command/default-registry.js.map +0 -1
  106. package/dist/core/command/history.d.ts +0 -64
  107. package/dist/core/command/history.js +0 -127
  108. package/dist/core/command/history.js.map +0 -1
  109. package/dist/core/command/merge-cells.d.ts +0 -21
  110. package/dist/core/command/merge-cells.js +0 -74
  111. package/dist/core/command/merge-cells.js.map +0 -1
  112. package/dist/core/command/registry.d.ts +0 -17
  113. package/dist/core/command/registry.js +0 -26
  114. package/dist/core/command/registry.js.map +0 -1
  115. package/dist/core/command/set-cell-formula.d.ts +0 -17
  116. package/dist/core/command/set-cell-formula.js +0 -29
  117. package/dist/core/command/set-cell-formula.js.map +0 -1
  118. package/dist/core/command/set-cell-value.d.ts +0 -22
  119. package/dist/core/command/set-cell-value.js +0 -35
  120. package/dist/core/command/set-cell-value.js.map +0 -1
  121. package/dist/core/command/types.d.ts +0 -68
  122. package/dist/core/events.js +0 -29
  123. package/dist/core/events.js.map +0 -1
  124. package/dist/core/formula/ast.d.ts +0 -58
  125. package/dist/core/formula/ast.js +0 -38
  126. package/dist/core/formula/ast.js.map +0 -1
  127. package/dist/core/formula/dependency-graph.d.ts +0 -70
  128. package/dist/core/formula/dependency-graph.js +0 -269
  129. package/dist/core/formula/dependency-graph.js.map +0 -1
  130. package/dist/core/formula/errors.d.ts +0 -25
  131. package/dist/core/formula/errors.js +0 -36
  132. package/dist/core/formula/errors.js.map +0 -1
  133. package/dist/core/formula/evaluator.d.ts +0 -41
  134. package/dist/core/formula/evaluator.js +0 -154
  135. package/dist/core/formula/evaluator.js.map +0 -1
  136. package/dist/core/formula/functions.d.ts +0 -30
  137. package/dist/core/formula/functions.js +0 -209
  138. package/dist/core/formula/functions.js.map +0 -1
  139. package/dist/core/formula/parser.d.ts +0 -7
  140. package/dist/core/formula/parser.js +0 -208
  141. package/dist/core/formula/parser.js.map +0 -1
  142. package/dist/core/formula/tokenizer.d.ts +0 -37
  143. package/dist/core/formula/tokenizer.js +0 -146
  144. package/dist/core/formula/tokenizer.js.map +0 -1
  145. package/dist/core/merge-manager.d.ts +0 -64
  146. package/dist/core/merge-manager.js +0 -111
  147. package/dist/core/merge-manager.js.map +0 -1
  148. package/dist/core/selection.d.ts +0 -31
  149. package/dist/core/selection.js +0 -62
  150. package/dist/core/selection.js.map +0 -1
  151. package/dist/core/sheet.d.ts +0 -105
  152. package/dist/core/sheet.js +0 -202
  153. package/dist/core/sheet.js.map +0 -1
  154. package/dist/core/workbook.d.ts +0 -43
  155. package/dist/core/workbook.js +0 -77
  156. package/dist/core/workbook.js.map +0 -1
  157. package/dist/grid/sheet-grid.d.ts +0 -59
  158. package/dist/grid/sheet-grid.js +0 -230
  159. package/dist/grid/sheet-grid.js.map +0 -1
  160. package/dist/tools/builtin.js.map +0 -1
  161. package/dist/tools/context.js.map +0 -1
  162. package/dist/tools/registry.js.map +0 -1
  163. package/dist/vue/index.d.ts +0 -2
  164. package/dist/vue/sheet.js.map +0 -1
  165. package/dist/vue/sheet.vue.d.ts +0 -24
  166. package/src/core/__test__/address.test.ts +0 -113
  167. package/src/core/__test__/cell-store.test.ts +0 -97
  168. package/src/core/__test__/command.test.ts +0 -377
  169. package/src/core/__test__/formula-adversarial.test.ts +0 -265
  170. package/src/core/__test__/formula-engine.test.ts +0 -470
  171. package/src/core/__test__/formula-functions.test.ts +0 -178
  172. package/src/core/__test__/formula-parser.test.ts +0 -160
  173. package/src/core/__test__/history.test.ts +0 -225
  174. package/src/core/__test__/merge-manager.test.ts +0 -106
  175. package/src/core/__test__/sheet.test.ts +0 -142
  176. package/src/core/address.ts +0 -166
  177. package/src/core/cell-store.ts +0 -173
  178. package/src/core/command/default-registry.ts +0 -15
  179. package/src/core/command/history.ts +0 -167
  180. package/src/core/command/index.ts +0 -7
  181. package/src/core/command/merge-cells.ts +0 -74
  182. package/src/core/command/registry.ts +0 -30
  183. package/src/core/command/set-cell-formula.ts +0 -29
  184. package/src/core/command/set-cell-value.ts +0 -38
  185. package/src/core/command/types.ts +0 -73
  186. package/src/core/events.ts +0 -37
  187. package/src/core/formula/ast.ts +0 -66
  188. package/src/core/formula/dependency-graph.ts +0 -329
  189. package/src/core/formula/errors.ts +0 -41
  190. package/src/core/formula/evaluator.ts +0 -219
  191. package/src/core/formula/functions.ts +0 -266
  192. package/src/core/formula/index.ts +0 -7
  193. package/src/core/formula/parser.ts +0 -198
  194. package/src/core/formula/tokenizer.ts +0 -160
  195. package/src/core/merge-manager.ts +0 -158
  196. package/src/core/selection.ts +0 -66
  197. package/src/core/sheet.ts +0 -257
  198. package/src/core/workbook.ts +0 -99
  199. package/src/grid/__test__/canvas-mock.ts +0 -49
  200. package/src/grid/__test__/setup.ts +0 -3
  201. package/src/grid/__test__/sheet-grid-formula.test.ts +0 -140
  202. package/src/grid/__test__/sheet-grid.test.ts +0 -177
  203. package/src/grid/sheet-grid.ts +0 -283
@@ -1,219 +0,0 @@
1
- import type { CellAddress, CellRange } from '../address'
2
- import type { AstNode, BinaryOperator } from './ast'
3
- import { formulaError, isFormulaError, type FormulaError } from './errors'
4
-
5
- /**
6
- * AST 求值器(纯函数,不持有状态;单元格读取经 FormulaEvalContext 注入)。
7
- *
8
- * 空格(null)参与运算的规则同 Excel:
9
- * - 数字上下文按 0(空单元格 +1 = 1);空字符串字面量参与算术 → #VALUE!
10
- * - 文本上下文按 '';布尔上下文按 FALSE
11
- * - 比较时空格归一为对方类型的零值
12
- * 错误值遇运算即传播(左操作数优先)。
13
- */
14
-
15
- /** 标量值(单元格/字面量可取的全集;null = 空单元格) */
16
- export type ScalarValue = number | string | boolean | null
17
-
18
- /** 求值结果:标量 / 错误 / 区域展开数组(数组仅作为函数参数形态出现) */
19
- export type EvalValue = ScalarValue | FormulaError | (ScalarValue | FormulaError)[]
20
-
21
- /** 求值上下文(由依赖图按公式节点注入) */
22
- export interface FormulaEvalContext {
23
- /** 当前公式所在 sheet(裸引用缺省表) */
24
- readonly currentSheet: string
25
- /** 读取单格(原始存储语义;表不存在 → #REF!) */
26
- readCell(sheet: string, addr: CellAddress): ScalarValue | FormulaError
27
- /** 读取区域(只含稀疏存在的格;表不存在 → #REF!) */
28
- readRange(sheet: string, range: CellRange): (ScalarValue | FormulaError)[] | FormulaError
29
- /** 调用函数(名称未知 → #NAME?;参数个数非法 → #VALUE!) */
30
- callFunction(name: string, nodes: AstNode[], evalNode: (node: AstNode) => EvalValue): EvalValue
31
- }
32
-
33
- const NUMERIC_TEXT_RE = /^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/
34
-
35
- /** 强转数字:null→0,布尔→1/0,数字文本→数字,其余文本→#VALUE!,错误传播 */
36
- export function coerceToNumber(value: EvalValue): number | FormulaError {
37
- if (isFormulaError(value)) return value
38
- if (Array.isArray(value)) return formulaError('#VALUE!')
39
- if (value === null) return 0
40
- switch (typeof value) {
41
- case 'number':
42
- return value
43
- case 'boolean':
44
- return value ? 1 : 0
45
- case 'string': {
46
- const text = value.trim()
47
- if (text === '') return formulaError('#VALUE!')
48
- if (text.toUpperCase() === 'TRUE') return 1
49
- if (text.toUpperCase() === 'FALSE') return 0
50
- if (NUMERIC_TEXT_RE.test(text)) return Number.parseFloat(text)
51
- return formulaError('#VALUE!')
52
- }
53
- }
54
- }
55
-
56
- /** 强转文本:null→'',布尔→TRUE/FALSE,错误传播 */
57
- export function coerceToText(value: EvalValue): string | FormulaError {
58
- if (isFormulaError(value)) return value
59
- if (Array.isArray(value)) return formulaError('#VALUE!')
60
- if (value === null) return ''
61
- switch (typeof value) {
62
- case 'string':
63
- return value
64
- case 'number':
65
- return String(value)
66
- case 'boolean':
67
- return value ? 'TRUE' : 'FALSE'
68
- }
69
- }
70
-
71
- /** 强转布尔:null→FALSE,数字≠0,TRUE/FALSE 文本,其余文本→#VALUE!,错误传播 */
72
- export function coerceToBoolean(value: EvalValue): boolean | FormulaError {
73
- if (isFormulaError(value)) return value
74
- if (Array.isArray(value)) return formulaError('#VALUE!')
75
- if (value === null) return false
76
- switch (typeof value) {
77
- case 'boolean':
78
- return value
79
- case 'number':
80
- return value !== 0
81
- case 'string': {
82
- const text = value.trim().toUpperCase()
83
- if (text === 'TRUE') return true
84
- if (text === 'FALSE') return false
85
- return formulaError('#VALUE!')
86
- }
87
- }
88
- }
89
-
90
- /** 比较:同类型按类型规则(文本大小写不敏感);混合类型 数字 < 文本 < 布尔;null 归一为对方零值 */
91
- export function compareScalars(left: ScalarValue, right: ScalarValue): number {
92
- if (left === null || right === null) {
93
- if (left === null && right === null) return 0
94
- if (left === null) return compareScalars(zeroLike(right), right)
95
- return compareScalars(left, zeroLike(left))
96
- }
97
- const leftType = typeof left
98
- const rightType = typeof right
99
- if (leftType === 'number' && rightType === 'number') {
100
- return left < right ? -1 : left > right ? 1 : 0
101
- }
102
- if (leftType === 'string' && rightType === 'string') {
103
- const a = (left as string).toUpperCase()
104
- const b = (right as string).toUpperCase()
105
- return a < b ? -1 : a > b ? 1 : 0
106
- }
107
- if (leftType === 'boolean' && rightType === 'boolean') {
108
- return left === right ? 0 : left ? 1 : -1
109
- }
110
- return typeRank(leftType) - typeRank(rightType)
111
- }
112
-
113
- function zeroLike(value: ScalarValue): ScalarValue {
114
- if (typeof value === 'number') return 0
115
- if (typeof value === 'string') return ''
116
- return false
117
- }
118
-
119
- function typeRank(type: string): number {
120
- if (type === 'number') return 0
121
- if (type === 'string') return 1
122
- return 2
123
- }
124
-
125
- const COMPARISON_OPS = new Set(['=', '<>', '<', '<=', '>', '>='])
126
-
127
- /** AST 求值 */
128
- export function evaluateAst(node: AstNode, ctx: FormulaEvalContext): EvalValue {
129
- switch (node.kind) {
130
- case 'number':
131
- case 'string':
132
- case 'boolean':
133
- return node.value
134
- case 'name':
135
- return formulaError('#NAME?')
136
- case 'cell':
137
- return ctx.readCell(node.sheet ?? ctx.currentSheet, node.addr)
138
- case 'range':
139
- return ctx.readRange(node.sheet ?? ctx.currentSheet, node.range)
140
- case 'unary': {
141
- const n = coerceToNumber(evaluateAst(node.operand, ctx))
142
- if (isFormulaError(n)) return n
143
- return node.op === '-' ? -n : n
144
- }
145
- case 'percent': {
146
- const n = coerceToNumber(evaluateAst(node.operand, ctx))
147
- if (isFormulaError(n)) return n
148
- return n / 100
149
- }
150
- case 'binary':
151
- return evaluateBinary(node.op, node.left, node.right, ctx)
152
- case 'call':
153
- return ctx.callFunction(node.name, node.args, (arg) => evaluateAst(arg, ctx))
154
- }
155
- }
156
-
157
- function evaluateBinary(
158
- op: BinaryOperator,
159
- leftNode: AstNode,
160
- rightNode: AstNode,
161
- ctx: FormulaEvalContext
162
- ): EvalValue {
163
- const left = evaluateAst(leftNode, ctx)
164
- if (isFormulaError(left)) return left
165
- const right = evaluateAst(rightNode, ctx)
166
- if (isFormulaError(right)) return right
167
-
168
- if (op === '&') {
169
- const l = coerceToText(left)
170
- if (isFormulaError(l)) return l
171
- const r = coerceToText(right)
172
- if (isFormulaError(r)) return r
173
- return l + r
174
- }
175
-
176
- if (COMPARISON_OPS.has(op)) {
177
- if (Array.isArray(left) || Array.isArray(right)) return formulaError('#VALUE!')
178
- const cmp = compareScalars(left, right)
179
- switch (op) {
180
- case '=':
181
- return cmp === 0
182
- case '<>':
183
- return cmp !== 0
184
- case '<':
185
- return cmp < 0
186
- case '<=':
187
- return cmp <= 0
188
- case '>':
189
- return cmp > 0
190
- default:
191
- return cmp >= 0
192
- }
193
- }
194
-
195
- const l = coerceToNumber(left)
196
- if (isFormulaError(l)) return l
197
- const r = coerceToNumber(right)
198
- if (isFormulaError(r)) return r
199
-
200
- switch (op) {
201
- case '+':
202
- return l + r
203
- case '-':
204
- return l - r
205
- case '*':
206
- return l * r
207
- case '/':
208
- if (r === 0) return formulaError('#DIV/0!')
209
- return l / r
210
- case '^': {
211
- if (l === 0 && r < 0) return formulaError('#DIV/0!')
212
- const value = Math.pow(l, r)
213
- return Number.isFinite(value) ? value : formulaError('#VALUE!')
214
- }
215
- // & 与比较运算已在上方提前返回
216
- default:
217
- return formulaError('#ERROR!')
218
- }
219
- }
@@ -1,266 +0,0 @@
1
- import type { AstNode } from './ast'
2
- import { formulaError, isFormulaError, type FormulaError } from './errors'
3
- import {
4
- coerceToBoolean,
5
- coerceToNumber,
6
- coerceToText,
7
- type EvalValue,
8
- type ScalarValue
9
- } from './evaluator'
10
-
11
- /**
12
- * 函数注册表(可扩展)+ 基础函数集。
13
- *
14
- * 参数形态约定:区域引用求值为数组(只含稀疏存在的格),直接参数为标量。
15
- * 聚合函数据此区分 Excel 语义:区域内的文本/布尔被忽略,直接参数则强转(非法 → #VALUE!)。
16
- * lazy 函数自行求值参数(IF 的短路分支,未选分支的副作用/错误不产生)。
17
- */
18
-
19
- export type FormulaFunction =
20
- | { kind?: 'normal'; minArgs?: number; maxArgs?: number; impl: (args: EvalValue[]) => EvalValue }
21
- | {
22
- kind: 'lazy'
23
- minArgs?: number
24
- maxArgs?: number
25
- impl: (nodes: AstNode[], evalNode: (node: AstNode) => EvalValue) => EvalValue
26
- }
27
-
28
- const registry = new Map<string, FormulaFunction>()
29
-
30
- /** 注册函数(名称大小写不敏感;同名覆盖,供扩展/自定义函数) */
31
- export function registerFormulaFunction(name: string, def: FormulaFunction): void {
32
- registry.set(name.toUpperCase(), def)
33
- }
34
-
35
- /** 查询函数(大小写不敏感) */
36
- export function getFormulaFunction(name: string): FormulaFunction | undefined {
37
- return registry.get(name.toUpperCase())
38
- }
39
-
40
- /** 求值器回调:名称解析 + 参数个数校验 + 按 kind 分发 */
41
- export function invokeFormulaFunction(
42
- name: string,
43
- nodes: AstNode[],
44
- evalNode: (node: AstNode) => EvalValue
45
- ): EvalValue {
46
- const def = getFormulaFunction(name)
47
- if (!def) return formulaError('#NAME?')
48
- if (def.minArgs !== undefined && nodes.length < def.minArgs) return formulaError('#VALUE!')
49
- if (def.maxArgs !== undefined && nodes.length > def.maxArgs) return formulaError('#VALUE!')
50
- if (def.kind === 'lazy') return def.impl(nodes, evalNode)
51
- return def.impl(nodes.map(evalNode))
52
- }
53
-
54
- // ─── 内部工具 ────────────────────────────────────────────────
55
-
56
- interface FlatArg {
57
- value: ScalarValue | FormulaError
58
- /** 来自区域展开(区别于直接参数:区域内文本/布尔被聚合函数忽略) */
59
- fromRange: boolean
60
- }
61
-
62
- function flattenArgs(args: EvalValue[]): FlatArg[] {
63
- const out: FlatArg[] = []
64
- for (const arg of args) {
65
- if (Array.isArray(arg)) {
66
- for (const value of arg) out.push({ value, fromRange: true })
67
- } else {
68
- out.push({ value: arg, fromRange: false })
69
- }
70
- }
71
- return out
72
- }
73
-
74
- /** 收集数字:区域内只取数字格;直接参数强转(非法/错误 → 传播) */
75
- function collectNumbers(args: EvalValue[]): number[] | FormulaError {
76
- const numbers: number[] = []
77
- for (const { value, fromRange } of flattenArgs(args)) {
78
- if (isFormulaError(value)) return value
79
- if (fromRange) {
80
- if (typeof value === 'number') numbers.push(value)
81
- continue
82
- }
83
- const n = coerceToNumber(value)
84
- if (isFormulaError(n)) return n
85
- numbers.push(n)
86
- }
87
- return numbers
88
- }
89
-
90
- /** 收集布尔:区域内只取布尔格;直接参数强转(非法文本/错误 → 传播);空值跳过 */
91
- function collectBooleans(args: EvalValue[]): boolean[] | FormulaError {
92
- const booleans: boolean[] = []
93
- for (const { value, fromRange } of flattenArgs(args)) {
94
- if (isFormulaError(value)) return value
95
- if (fromRange) {
96
- if (typeof value === 'boolean') booleans.push(value)
97
- continue
98
- }
99
- if (value === null) continue
100
- const b = coerceToBoolean(value)
101
- if (isFormulaError(b)) return b
102
- booleans.push(b)
103
- }
104
- return booleans
105
- }
106
-
107
- // ─── 基础函数集 ─────────────────────────────────────────────
108
-
109
- registerFormulaFunction('SUM', {
110
- minArgs: 1,
111
- impl(args) {
112
- const numbers = collectNumbers(args)
113
- if (isFormulaError(numbers)) return numbers
114
- let sum = 0
115
- for (const n of numbers) sum += n
116
- return sum
117
- }
118
- })
119
-
120
- registerFormulaFunction('AVERAGE', {
121
- minArgs: 1,
122
- impl(args) {
123
- const numbers = collectNumbers(args)
124
- if (isFormulaError(numbers)) return numbers
125
- if (numbers.length === 0) return formulaError('#DIV/0!')
126
- let sum = 0
127
- for (const n of numbers) sum += n
128
- return sum / numbers.length
129
- }
130
- })
131
-
132
- registerFormulaFunction('MAX', {
133
- minArgs: 1,
134
- impl(args) {
135
- const numbers = collectNumbers(args)
136
- if (isFormulaError(numbers)) return numbers
137
- return numbers.length === 0 ? 0 : Math.max(...numbers)
138
- }
139
- })
140
-
141
- registerFormulaFunction('MIN', {
142
- minArgs: 1,
143
- impl(args) {
144
- const numbers = collectNumbers(args)
145
- if (isFormulaError(numbers)) return numbers
146
- return numbers.length === 0 ? 0 : Math.min(...numbers)
147
- }
148
- })
149
-
150
- registerFormulaFunction('COUNT', {
151
- minArgs: 1,
152
- impl(args) {
153
- let count = 0
154
- for (const { value, fromRange } of flattenArgs(args)) {
155
- if (isFormulaError(value)) return value
156
- if (fromRange) {
157
- if (typeof value === 'number') count++
158
- continue
159
- }
160
- // 直接参数:可强转为数字即计数(含 TRUE/FALSE、数字文本)
161
- if (!isFormulaError(coerceToNumber(value))) count++
162
- }
163
- return count
164
- }
165
- })
166
-
167
- registerFormulaFunction('COUNTA', {
168
- minArgs: 1,
169
- impl(args) {
170
- let count = 0
171
- for (const { value, fromRange } of flattenArgs(args)) {
172
- // 直接错误参数传播;区域内的错误格照常计数
173
- if (!fromRange && isFormulaError(value)) return value
174
- if (value === null) continue
175
- count++
176
- }
177
- return count
178
- }
179
- })
180
-
181
- registerFormulaFunction('IF', {
182
- kind: 'lazy',
183
- minArgs: 2,
184
- maxArgs: 3,
185
- impl(nodes, evalNode) {
186
- const cond = coerceToBoolean(evalNode(nodes[0]!))
187
- if (isFormulaError(cond)) return cond
188
- if (cond) return evalNode(nodes[1]!)
189
- return nodes[2] ? evalNode(nodes[2]) : false
190
- }
191
- })
192
-
193
- registerFormulaFunction('AND', {
194
- minArgs: 1,
195
- impl(args) {
196
- const booleans = collectBooleans(args)
197
- if (isFormulaError(booleans)) return booleans
198
- if (booleans.length === 0) return formulaError('#VALUE!')
199
- return booleans.every(Boolean)
200
- }
201
- })
202
-
203
- registerFormulaFunction('OR', {
204
- minArgs: 1,
205
- impl(args) {
206
- const booleans = collectBooleans(args)
207
- if (isFormulaError(booleans)) return booleans
208
- if (booleans.length === 0) return formulaError('#VALUE!')
209
- return booleans.some(Boolean)
210
- }
211
- })
212
-
213
- registerFormulaFunction('NOT', {
214
- minArgs: 1,
215
- maxArgs: 1,
216
- impl(args) {
217
- const b = coerceToBoolean(args[0]!)
218
- if (isFormulaError(b)) return b
219
- return !b
220
- }
221
- })
222
-
223
- registerFormulaFunction('ROUND', {
224
- minArgs: 2,
225
- maxArgs: 2,
226
- impl(args) {
227
- const n = coerceToNumber(args[0]!)
228
- if (isFormulaError(n)) return n
229
- const d = coerceToNumber(args[1]!)
230
- if (isFormulaError(d)) return d
231
- const factor = 10 ** Math.trunc(d)
232
- // 位数超出 double 精度时四舍五入是恒等/归零
233
- if (!Number.isFinite(factor)) return n
234
- if (factor === 0) return 0
235
- // 远离零方向取整;1e-12 补偿浮点表示误差(如 2.675 存为 2.67499…)
236
- const shifted = n * factor
237
- const rounded = Math.sign(shifted) * Math.round(Math.abs(shifted) + 1e-12)
238
- return rounded / factor
239
- }
240
- })
241
-
242
- registerFormulaFunction('ABS', {
243
- minArgs: 1,
244
- maxArgs: 1,
245
- impl(args) {
246
- const n = coerceToNumber(args[0]!)
247
- if (isFormulaError(n)) return n
248
- return Math.abs(n)
249
- }
250
- })
251
-
252
- registerFormulaFunction('CONCATENATE', {
253
- minArgs: 1,
254
- impl(args) {
255
- let text = ''
256
- for (const arg of args) {
257
- const values = Array.isArray(arg) ? arg : [arg]
258
- for (const value of values) {
259
- const piece = coerceToText(value)
260
- if (isFormulaError(piece)) return piece
261
- text += piece
262
- }
263
- }
264
- return text
265
- }
266
- })
@@ -1,7 +0,0 @@
1
- export * from './errors'
2
- export * from './ast'
3
- export * from './tokenizer'
4
- export * from './parser'
5
- export * from './evaluator'
6
- export * from './functions'
7
- export * from './dependency-graph'
@@ -1,198 +0,0 @@
1
- import { createRange, parseAddress, type CellAddress } from '../address'
2
- import type { AstNode, BinaryOperator } from './ast'
3
- import { FormulaParseError, tokenizeFormula, type FormulaToken } from './tokenizer'
4
-
5
- /**
6
- * Pratt parser:token 序列 → AST。
7
- *
8
- * 优先级(同 Excel,低到高):比较运算 < `&` < `+ -` < `* /` < 一元 `+ -` < `^`(右结合)< `%`。
9
- * 一元运算紧于幂次是 Excel 的特有行为:-2^2 = (-2)^2 = 4。
10
- * 引用消歧:ident 后随 `(` → 函数调用;后随 `!` → 表名前缀;匹配单元格形态 → 引用;
11
- * TRUE/FALSE → 布尔字面量;其余 → 未知名称节点(求值 #NAME?)。
12
- */
13
-
14
- /** 中缀运算符绑定力 [左, 右];右结合运算符右绑定力 = 左 - 1 */
15
- const INFIX_BP: Record<string, readonly [number, number]> = {
16
- '=': [1, 1],
17
- '<>': [1, 1],
18
- '<': [1, 1],
19
- '<=': [1, 1],
20
- '>': [1, 1],
21
- '>=': [1, 1],
22
- '&': [2, 2],
23
- '+': [3, 3],
24
- '-': [3, 3],
25
- '*': [4, 4],
26
- '/': [4, 4],
27
- '^': [6, 5]
28
- }
29
- /** 百分号后缀绑定力(最高) */
30
- const PERCENT_BP = 7
31
- /** 一元 +/- 操作数的绑定力:紧于 ^(6),松于 %(7) */
32
- const UNARY_OPERAND_BP = 6
33
-
34
- /** 单元格引用形态(列限 3 字母;超出按未知名称处理,如 Sheet2) */
35
- const CELL_REF_RE = /^\$?[A-Za-z]{1,3}\$?[1-9]\d*$/
36
-
37
- class Parser {
38
- private pos = 0
39
-
40
- constructor(private readonly tokens: FormulaToken[]) {}
41
-
42
- parse(): AstNode {
43
- const node = this.parseExpression(0)
44
- if (this.pos < this.tokens.length) {
45
- throw new FormulaParseError(`意外的多余输入: ${describeToken(this.tokens[this.pos]!)}`)
46
- }
47
- return node
48
- }
49
-
50
- private peek(): FormulaToken | undefined {
51
- return this.tokens[this.pos]
52
- }
53
-
54
- private next(): FormulaToken | undefined {
55
- return this.tokens[this.pos++]
56
- }
57
-
58
- private peekOp(op: string): boolean {
59
- const token = this.peek()
60
- return token?.type === 'op' && token.op === op
61
- }
62
-
63
- private expectOp(op: string): void {
64
- if (!this.peekOp(op)) {
65
- throw new FormulaParseError(`期望 "${op}",得到 ${describeToken(this.peek())}`)
66
- }
67
- this.pos++
68
- }
69
-
70
- private parseExpression(minBp: number): AstNode {
71
- let left = this.parsePrimary()
72
- for (;;) {
73
- const token = this.peek()
74
- if (token?.type !== 'op') break
75
- // 百分号后缀
76
- if (token.op === '%') {
77
- if (PERCENT_BP <= minBp) break
78
- this.pos++
79
- left = { kind: 'percent', operand: left }
80
- continue
81
- }
82
- const bp = INFIX_BP[token.op]
83
- if (!bp || bp[0] <= minBp) break
84
- this.pos++
85
- const right = this.parseExpression(bp[1])
86
- left = { kind: 'binary', op: token.op as BinaryOperator, left, right }
87
- }
88
- return left
89
- }
90
-
91
- private parsePrimary(): AstNode {
92
- const token = this.next()
93
- if (!token) throw new FormulaParseError('表达式不完整')
94
-
95
- if (token.type === 'number') return { kind: 'number', value: token.value }
96
- if (token.type === 'string') return { kind: 'string', value: token.value }
97
-
98
- if (token.type === 'quoted-name') {
99
- // 带引号表名只能作为跨表引用前缀
100
- this.expectOp('!')
101
- return this.parseCellRefOrRange(token.name)
102
- }
103
-
104
- if (token.type === 'ident') {
105
- const name = token.name
106
- if (this.peekOp('(')) return this.parseCall(name)
107
- if (this.peekOp('!')) {
108
- this.pos++
109
- return this.parseCellRefOrRange(name)
110
- }
111
- const addr = parseCellRef(name)
112
- if (addr) return this.parseRangeTail({ sheet: undefined, addr })
113
- if (name.toUpperCase() === 'TRUE') return { kind: 'boolean', value: true }
114
- if (name.toUpperCase() === 'FALSE') return { kind: 'boolean', value: false }
115
- return { kind: 'name', name }
116
- }
117
-
118
- // 运算符位置:括号 / 一元 +-
119
- if (token.type === 'op') {
120
- if (token.op === '(') {
121
- const inner = this.parseExpression(0)
122
- this.expectOp(')')
123
- return inner
124
- }
125
- if (token.op === '-' || token.op === '+') {
126
- return { kind: 'unary', op: token.op, operand: this.parseExpression(UNARY_OPERAND_BP) }
127
- }
128
- }
129
- throw new FormulaParseError(`意外的 ${describeToken(token)}`)
130
- }
131
-
132
- /** 函数调用:ident 已消费,当前 token 为 '(' */
133
- private parseCall(name: string): AstNode {
134
- this.pos++ // 消费 '('
135
- const args: AstNode[] = []
136
- if (!this.peekOp(')')) {
137
- for (;;) {
138
- args.push(this.parseExpression(0))
139
- if (this.peekOp(',')) {
140
- this.pos++
141
- continue
142
- }
143
- break
144
- }
145
- }
146
- this.expectOp(')')
147
- return { kind: 'call', name, args }
148
- }
149
-
150
- /** 表名前缀(Sheet2! / 'My Sheet'!)之后的单元格或区域引用 */
151
- private parseCellRefOrRange(sheet: string): AstNode {
152
- const token = this.next()
153
- const addr = token?.type === 'ident' ? parseCellRef(token.name) : null
154
- if (!addr) {
155
- throw new FormulaParseError(`表名 "${sheet}" 后应为单元格引用,得到 ${describeToken(token)}`)
156
- }
157
- return this.parseRangeTail({ sheet, addr })
158
- }
159
-
160
- /** 单元格引用之后的可选区域后缀(:B9) */
161
- private parseRangeTail(ref: { sheet: string | undefined; addr: CellAddress }): AstNode {
162
- if (!this.peekOp(':')) return { kind: 'cell', sheet: ref.sheet, addr: ref.addr }
163
- this.pos++
164
- const token = this.next()
165
- const end = token?.type === 'ident' ? parseCellRef(token.name) : null
166
- if (!end) {
167
- throw new FormulaParseError(`区域 ":" 后应为单元格引用,得到 ${describeToken(token)}`)
168
- }
169
- return { kind: 'range', sheet: ref.sheet, range: createRange(ref.addr, end) }
170
- }
171
- }
172
-
173
- /** 按单元格引用形态解析(列限 3 字母);不匹配返回 null */
174
- function parseCellRef(name: string): CellAddress | null {
175
- if (!CELL_REF_RE.test(name)) return null
176
- return parseAddress(name)
177
- }
178
-
179
- function describeToken(token: FormulaToken | undefined): string {
180
- if (!token) return '输入末尾'
181
- switch (token.type) {
182
- case 'number':
183
- return `数字 ${token.value}`
184
- case 'string':
185
- return `字符串 "${token.value}"`
186
- case 'ident':
187
- return `名称 "${token.name}"`
188
- case 'quoted-name':
189
- return `表名 '${token.name}'`
190
- case 'op':
191
- return `"${token.op}"`
192
- }
193
- }
194
-
195
- /** 解析公式文本(不含 '=')→ AST;非法输入抛 FormulaParseError */
196
- export function parseFormula(text: string): AstNode {
197
- return new Parser(tokenizeFormula(text)).parse()
198
- }