@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.
- package/dist/index.d.ts +4 -25
- package/dist/index.js +3 -22
- package/dist/tools/builtin.js +279 -8
- package/dist/tools/context.d.ts +54 -11
- package/dist/tools/context.js +28 -5
- package/dist/tools/download.js +68 -0
- package/dist/tools/export.worker.d.ts +16 -0
- package/dist/tools/export.worker.js +29 -0
- package/dist/tools/registry.d.ts +7 -4
- package/dist/tools/registry.js +1 -3
- package/dist/types/index.d.ts +1 -0
- package/dist/types/sheet.d.ts +6 -5
- package/dist/vue/formula-bar.js +393 -0
- package/dist/vue/formula-suggest-list.js +66 -0
- package/dist/vue/import-file.js +140 -0
- package/dist/vue/insert-count-menu-item.js +68 -0
- package/dist/vue/insert-image.js +79 -0
- package/dist/vue/popup-helpers.js +73 -0
- package/dist/vue/popups/border-popup.js +75 -0
- package/dist/vue/popups/border-preset-glyph.js +106 -0
- package/dist/vue/popups/export-popup.js +51 -0
- package/dist/vue/popups/fill-color-popup.js +37 -0
- package/dist/vue/popups/find-popup.js +113 -0
- package/dist/vue/popups/font-color-popup.js +37 -0
- package/dist/vue/popups/font-size-popup.js +63 -0
- package/dist/vue/popups/import.worker.d.ts +21 -0
- package/dist/vue/popups/import.worker.js +41 -0
- package/dist/vue/popups/insert-image-popup.js +38 -0
- package/dist/vue/sheet-context-menu.js +200 -0
- package/dist/vue/sheet-tabs.js +162 -0
- package/dist/vue/sheet-toolbar.js +106 -0
- package/dist/vue/sheet.js +144 -138
- package/dist/vue/style.d.ts +11 -1
- package/dist/vue/style.js +11 -0
- package/dist/vue/style2.css +564 -3
- package/dist/vue/use-find-replace.js +161 -0
- package/dist/vue/use-formula-ref-select.js +37 -0
- package/dist/vue/use-formula-suggest.js +126 -0
- package/dist/vue/use-sheet-grid.js +180 -0
- package/dist/vue/use-sheet-state.js +122 -0
- package/dist/vue/use-sheet-tabs-bar.js +90 -0
- package/dist/vue/use-tool-groups.js +42 -0
- package/dist/vue/use-tool-popup.js +123 -0
- package/dist/vue/use-toolbar-scroll.js +72 -0
- package/package.json +12 -6
- package/src/env.d.ts +14 -0
- package/src/index.ts +203 -13
- package/src/tools/__test__/builtin.test.ts +208 -66
- package/src/tools/__test__/context.test.ts +62 -2
- package/src/tools/builtin.ts +350 -6
- package/src/tools/context.ts +89 -11
- package/src/tools/download.ts +82 -0
- package/src/tools/export.worker.ts +61 -0
- package/src/tools/registry.ts +16 -1
- package/src/types/index.ts +6 -0
- package/src/types/sheet.ts +5 -3
- package/src/vue/__test__/formula-bar.test.ts +455 -0
- package/src/vue/__test__/import-file.test.ts +183 -0
- package/src/vue/__test__/insert-count-menu-item.test.ts +123 -0
- package/src/vue/__test__/insert-image-popup.test.ts +89 -0
- package/src/vue/__test__/insert-image.test.ts +100 -0
- package/src/vue/__test__/sheet-component.test.ts +716 -27
- package/src/vue/__test__/sheet-context-menu.test.ts +206 -0
- package/src/vue/__test__/use-formula-ref-select.test.ts +35 -0
- package/src/vue/__test__/use-formula-suggest.test.ts +83 -0
- package/src/vue/formula-bar.vue +473 -0
- package/src/vue/formula-suggest-list.vue +64 -0
- package/src/vue/import-file.ts +173 -0
- package/src/vue/insert-count-menu-item.vue +69 -0
- package/src/vue/insert-image.ts +90 -0
- package/src/vue/parsing.ts +11 -0
- package/src/vue/popup-helpers.ts +50 -0
- package/src/vue/popups/border-popup.vue +93 -0
- package/src/vue/popups/border-preset-glyph.vue +111 -0
- package/src/vue/popups/export-popup.vue +52 -0
- package/src/vue/popups/fill-color-popup.vue +39 -0
- package/src/vue/popups/find-popup.vue +111 -0
- package/src/vue/popups/font-color-popup.vue +39 -0
- package/src/vue/popups/font-size-popup.vue +52 -0
- package/src/vue/popups/import.worker.ts +66 -0
- package/src/vue/popups/insert-image-popup.vue +39 -0
- package/src/vue/sheet-context-menu.ts +229 -0
- package/src/vue/sheet-tabs.vue +162 -0
- package/src/vue/sheet-toolbar.vue +91 -0
- package/src/vue/sheet.vue +163 -148
- package/src/vue/style.scss +646 -3
- package/src/vue/style.ts +12 -0
- package/src/vue/use-find-replace.ts +200 -0
- package/src/vue/use-formula-ref-select.ts +34 -0
- package/src/vue/use-formula-suggest.ts +150 -0
- package/src/vue/use-sheet-grid.ts +243 -0
- package/src/vue/use-sheet-state.ts +161 -0
- package/src/vue/use-sheet-tabs-bar.ts +115 -0
- package/src/vue/use-tool-groups.ts +61 -0
- package/src/vue/use-tool-popup.ts +150 -0
- package/src/vue/use-toolbar-scroll.ts +95 -0
- package/dist/core/address.d.ts +0 -60
- package/dist/core/address.js +0 -142
- package/dist/core/address.js.map +0 -1
- package/dist/core/cell-store.d.ts +0 -61
- package/dist/core/cell-store.js +0 -135
- package/dist/core/cell-store.js.map +0 -1
- package/dist/core/command/default-registry.d.ts +0 -10
- package/dist/core/command/default-registry.js +0 -18
- package/dist/core/command/default-registry.js.map +0 -1
- package/dist/core/command/history.d.ts +0 -64
- package/dist/core/command/history.js +0 -127
- package/dist/core/command/history.js.map +0 -1
- package/dist/core/command/merge-cells.d.ts +0 -21
- package/dist/core/command/merge-cells.js +0 -74
- package/dist/core/command/merge-cells.js.map +0 -1
- package/dist/core/command/registry.d.ts +0 -17
- package/dist/core/command/registry.js +0 -26
- package/dist/core/command/registry.js.map +0 -1
- package/dist/core/command/set-cell-formula.d.ts +0 -17
- package/dist/core/command/set-cell-formula.js +0 -29
- package/dist/core/command/set-cell-formula.js.map +0 -1
- package/dist/core/command/set-cell-value.d.ts +0 -22
- package/dist/core/command/set-cell-value.js +0 -35
- package/dist/core/command/set-cell-value.js.map +0 -1
- package/dist/core/command/types.d.ts +0 -68
- package/dist/core/events.js +0 -29
- package/dist/core/events.js.map +0 -1
- package/dist/core/formula/ast.d.ts +0 -58
- package/dist/core/formula/ast.js +0 -38
- package/dist/core/formula/ast.js.map +0 -1
- package/dist/core/formula/dependency-graph.d.ts +0 -70
- package/dist/core/formula/dependency-graph.js +0 -269
- package/dist/core/formula/dependency-graph.js.map +0 -1
- package/dist/core/formula/errors.d.ts +0 -25
- package/dist/core/formula/errors.js +0 -36
- package/dist/core/formula/errors.js.map +0 -1
- package/dist/core/formula/evaluator.d.ts +0 -41
- package/dist/core/formula/evaluator.js +0 -154
- package/dist/core/formula/evaluator.js.map +0 -1
- package/dist/core/formula/functions.d.ts +0 -30
- package/dist/core/formula/functions.js +0 -209
- package/dist/core/formula/functions.js.map +0 -1
- package/dist/core/formula/parser.d.ts +0 -7
- package/dist/core/formula/parser.js +0 -208
- package/dist/core/formula/parser.js.map +0 -1
- package/dist/core/formula/tokenizer.d.ts +0 -37
- package/dist/core/formula/tokenizer.js +0 -146
- package/dist/core/formula/tokenizer.js.map +0 -1
- package/dist/core/merge-manager.d.ts +0 -64
- package/dist/core/merge-manager.js +0 -111
- package/dist/core/merge-manager.js.map +0 -1
- package/dist/core/selection.d.ts +0 -31
- package/dist/core/selection.js +0 -62
- package/dist/core/selection.js.map +0 -1
- package/dist/core/sheet.d.ts +0 -105
- package/dist/core/sheet.js +0 -202
- package/dist/core/sheet.js.map +0 -1
- package/dist/core/workbook.d.ts +0 -43
- package/dist/core/workbook.js +0 -77
- package/dist/core/workbook.js.map +0 -1
- package/dist/grid/sheet-grid.d.ts +0 -59
- package/dist/grid/sheet-grid.js +0 -230
- package/dist/grid/sheet-grid.js.map +0 -1
- package/dist/tools/builtin.js.map +0 -1
- package/dist/tools/context.js.map +0 -1
- package/dist/tools/registry.js.map +0 -1
- package/dist/vue/index.d.ts +0 -2
- package/dist/vue/sheet.js.map +0 -1
- package/dist/vue/sheet.vue.d.ts +0 -24
- package/src/core/__test__/address.test.ts +0 -113
- package/src/core/__test__/cell-store.test.ts +0 -97
- package/src/core/__test__/command.test.ts +0 -377
- package/src/core/__test__/formula-adversarial.test.ts +0 -265
- package/src/core/__test__/formula-engine.test.ts +0 -470
- package/src/core/__test__/formula-functions.test.ts +0 -178
- package/src/core/__test__/formula-parser.test.ts +0 -160
- package/src/core/__test__/history.test.ts +0 -225
- package/src/core/__test__/merge-manager.test.ts +0 -106
- package/src/core/__test__/sheet.test.ts +0 -142
- package/src/core/address.ts +0 -166
- package/src/core/cell-store.ts +0 -173
- package/src/core/command/default-registry.ts +0 -15
- package/src/core/command/history.ts +0 -167
- package/src/core/command/index.ts +0 -7
- package/src/core/command/merge-cells.ts +0 -74
- package/src/core/command/registry.ts +0 -30
- package/src/core/command/set-cell-formula.ts +0 -29
- package/src/core/command/set-cell-value.ts +0 -38
- package/src/core/command/types.ts +0 -73
- package/src/core/events.ts +0 -37
- package/src/core/formula/ast.ts +0 -66
- package/src/core/formula/dependency-graph.ts +0 -329
- package/src/core/formula/errors.ts +0 -41
- package/src/core/formula/evaluator.ts +0 -219
- package/src/core/formula/functions.ts +0 -266
- package/src/core/formula/index.ts +0 -7
- package/src/core/formula/parser.ts +0 -198
- package/src/core/formula/tokenizer.ts +0 -160
- package/src/core/merge-manager.ts +0 -158
- package/src/core/selection.ts +0 -66
- package/src/core/sheet.ts +0 -257
- package/src/core/workbook.ts +0 -99
- package/src/grid/__test__/canvas-mock.ts +0 -49
- package/src/grid/__test__/setup.ts +0 -3
- package/src/grid/__test__/sheet-grid-formula.test.ts +0 -140
- package/src/grid/__test__/sheet-grid.test.ts +0 -177
- 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,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
|
-
}
|