@veltra/sheet 2.0.1 → 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 -26
- package/dist/index.js +3 -23
- 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 -164
- package/dist/vue/style.d.ts +11 -1
- package/dist/vue/style.js +10 -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 -7
- package/src/env.d.ts +11 -0
- package/src/index.ts +203 -14
- 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 +161 -186
- package/src/vue/style.scss +646 -3
- package/src/vue/style.ts +11 -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/fill.d.ts +0 -34
- package/dist/core/fill.js +0 -320
- package/dist/core/fill.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 -116
- package/dist/core/sheet.js +0 -224
- 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 -81
- package/dist/grid/sheet-grid.js +0 -331
- package/dist/grid/sheet-grid.js.map +0 -1
- package/dist/grid/vtable-theme.js +0 -80
- package/dist/grid/vtable-theme.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__/fill.test.ts +0 -140
- 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/fill.ts +0 -314
- 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 -282
- 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 -275
- package/src/grid/sheet-grid.ts +0 -407
- package/src/grid/vtable-theme.ts +0 -61
|
@@ -1,209 +0,0 @@
|
|
|
1
|
-
import { formulaError, isFormulaError } from "./errors.js";
|
|
2
|
-
import { coerceToBoolean, coerceToNumber, coerceToText } from "./evaluator.js";
|
|
3
|
-
//#region src/core/formula/functions.ts
|
|
4
|
-
const registry = /* @__PURE__ */ new Map();
|
|
5
|
-
/** 注册函数(名称大小写不敏感;同名覆盖,供扩展/自定义函数) */
|
|
6
|
-
function registerFormulaFunction(name, def) {
|
|
7
|
-
registry.set(name.toUpperCase(), def);
|
|
8
|
-
}
|
|
9
|
-
/** 查询函数(大小写不敏感) */
|
|
10
|
-
function getFormulaFunction(name) {
|
|
11
|
-
return registry.get(name.toUpperCase());
|
|
12
|
-
}
|
|
13
|
-
/** 求值器回调:名称解析 + 参数个数校验 + 按 kind 分发 */
|
|
14
|
-
function invokeFormulaFunction(name, nodes, evalNode) {
|
|
15
|
-
const def = getFormulaFunction(name);
|
|
16
|
-
if (!def) return formulaError("#NAME?");
|
|
17
|
-
if (def.minArgs !== void 0 && nodes.length < def.minArgs) return formulaError("#VALUE!");
|
|
18
|
-
if (def.maxArgs !== void 0 && nodes.length > def.maxArgs) return formulaError("#VALUE!");
|
|
19
|
-
if (def.kind === "lazy") return def.impl(nodes, evalNode);
|
|
20
|
-
return def.impl(nodes.map(evalNode));
|
|
21
|
-
}
|
|
22
|
-
function flattenArgs(args) {
|
|
23
|
-
const out = [];
|
|
24
|
-
for (const arg of args) if (Array.isArray(arg)) for (const value of arg) out.push({
|
|
25
|
-
value,
|
|
26
|
-
fromRange: true
|
|
27
|
-
});
|
|
28
|
-
else out.push({
|
|
29
|
-
value: arg,
|
|
30
|
-
fromRange: false
|
|
31
|
-
});
|
|
32
|
-
return out;
|
|
33
|
-
}
|
|
34
|
-
/** 收集数字:区域内只取数字格;直接参数强转(非法/错误 → 传播) */
|
|
35
|
-
function collectNumbers(args) {
|
|
36
|
-
const numbers = [];
|
|
37
|
-
for (const { value, fromRange } of flattenArgs(args)) {
|
|
38
|
-
if (isFormulaError(value)) return value;
|
|
39
|
-
if (fromRange) {
|
|
40
|
-
if (typeof value === "number") numbers.push(value);
|
|
41
|
-
continue;
|
|
42
|
-
}
|
|
43
|
-
const n = coerceToNumber(value);
|
|
44
|
-
if (isFormulaError(n)) return n;
|
|
45
|
-
numbers.push(n);
|
|
46
|
-
}
|
|
47
|
-
return numbers;
|
|
48
|
-
}
|
|
49
|
-
/** 收集布尔:区域内只取布尔格;直接参数强转(非法文本/错误 → 传播);空值跳过 */
|
|
50
|
-
function collectBooleans(args) {
|
|
51
|
-
const booleans = [];
|
|
52
|
-
for (const { value, fromRange } of flattenArgs(args)) {
|
|
53
|
-
if (isFormulaError(value)) return value;
|
|
54
|
-
if (fromRange) {
|
|
55
|
-
if (typeof value === "boolean") booleans.push(value);
|
|
56
|
-
continue;
|
|
57
|
-
}
|
|
58
|
-
if (value === null) continue;
|
|
59
|
-
const b = coerceToBoolean(value);
|
|
60
|
-
if (isFormulaError(b)) return b;
|
|
61
|
-
booleans.push(b);
|
|
62
|
-
}
|
|
63
|
-
return booleans;
|
|
64
|
-
}
|
|
65
|
-
registerFormulaFunction("SUM", {
|
|
66
|
-
minArgs: 1,
|
|
67
|
-
impl(args) {
|
|
68
|
-
const numbers = collectNumbers(args);
|
|
69
|
-
if (isFormulaError(numbers)) return numbers;
|
|
70
|
-
let sum = 0;
|
|
71
|
-
for (const n of numbers) sum += n;
|
|
72
|
-
return sum;
|
|
73
|
-
}
|
|
74
|
-
});
|
|
75
|
-
registerFormulaFunction("AVERAGE", {
|
|
76
|
-
minArgs: 1,
|
|
77
|
-
impl(args) {
|
|
78
|
-
const numbers = collectNumbers(args);
|
|
79
|
-
if (isFormulaError(numbers)) return numbers;
|
|
80
|
-
if (numbers.length === 0) return formulaError("#DIV/0!");
|
|
81
|
-
let sum = 0;
|
|
82
|
-
for (const n of numbers) sum += n;
|
|
83
|
-
return sum / numbers.length;
|
|
84
|
-
}
|
|
85
|
-
});
|
|
86
|
-
registerFormulaFunction("MAX", {
|
|
87
|
-
minArgs: 1,
|
|
88
|
-
impl(args) {
|
|
89
|
-
const numbers = collectNumbers(args);
|
|
90
|
-
if (isFormulaError(numbers)) return numbers;
|
|
91
|
-
return numbers.length === 0 ? 0 : Math.max(...numbers);
|
|
92
|
-
}
|
|
93
|
-
});
|
|
94
|
-
registerFormulaFunction("MIN", {
|
|
95
|
-
minArgs: 1,
|
|
96
|
-
impl(args) {
|
|
97
|
-
const numbers = collectNumbers(args);
|
|
98
|
-
if (isFormulaError(numbers)) return numbers;
|
|
99
|
-
return numbers.length === 0 ? 0 : Math.min(...numbers);
|
|
100
|
-
}
|
|
101
|
-
});
|
|
102
|
-
registerFormulaFunction("COUNT", {
|
|
103
|
-
minArgs: 1,
|
|
104
|
-
impl(args) {
|
|
105
|
-
let count = 0;
|
|
106
|
-
for (const { value, fromRange } of flattenArgs(args)) {
|
|
107
|
-
if (isFormulaError(value)) return value;
|
|
108
|
-
if (fromRange) {
|
|
109
|
-
if (typeof value === "number") count++;
|
|
110
|
-
continue;
|
|
111
|
-
}
|
|
112
|
-
if (!isFormulaError(coerceToNumber(value))) count++;
|
|
113
|
-
}
|
|
114
|
-
return count;
|
|
115
|
-
}
|
|
116
|
-
});
|
|
117
|
-
registerFormulaFunction("COUNTA", {
|
|
118
|
-
minArgs: 1,
|
|
119
|
-
impl(args) {
|
|
120
|
-
let count = 0;
|
|
121
|
-
for (const { value, fromRange } of flattenArgs(args)) {
|
|
122
|
-
if (!fromRange && isFormulaError(value)) return value;
|
|
123
|
-
if (value === null) continue;
|
|
124
|
-
count++;
|
|
125
|
-
}
|
|
126
|
-
return count;
|
|
127
|
-
}
|
|
128
|
-
});
|
|
129
|
-
registerFormulaFunction("IF", {
|
|
130
|
-
kind: "lazy",
|
|
131
|
-
minArgs: 2,
|
|
132
|
-
maxArgs: 3,
|
|
133
|
-
impl(nodes, evalNode) {
|
|
134
|
-
const cond = coerceToBoolean(evalNode(nodes[0]));
|
|
135
|
-
if (isFormulaError(cond)) return cond;
|
|
136
|
-
if (cond) return evalNode(nodes[1]);
|
|
137
|
-
return nodes[2] ? evalNode(nodes[2]) : false;
|
|
138
|
-
}
|
|
139
|
-
});
|
|
140
|
-
registerFormulaFunction("AND", {
|
|
141
|
-
minArgs: 1,
|
|
142
|
-
impl(args) {
|
|
143
|
-
const booleans = collectBooleans(args);
|
|
144
|
-
if (isFormulaError(booleans)) return booleans;
|
|
145
|
-
if (booleans.length === 0) return formulaError("#VALUE!");
|
|
146
|
-
return booleans.every(Boolean);
|
|
147
|
-
}
|
|
148
|
-
});
|
|
149
|
-
registerFormulaFunction("OR", {
|
|
150
|
-
minArgs: 1,
|
|
151
|
-
impl(args) {
|
|
152
|
-
const booleans = collectBooleans(args);
|
|
153
|
-
if (isFormulaError(booleans)) return booleans;
|
|
154
|
-
if (booleans.length === 0) return formulaError("#VALUE!");
|
|
155
|
-
return booleans.some(Boolean);
|
|
156
|
-
}
|
|
157
|
-
});
|
|
158
|
-
registerFormulaFunction("NOT", {
|
|
159
|
-
minArgs: 1,
|
|
160
|
-
maxArgs: 1,
|
|
161
|
-
impl(args) {
|
|
162
|
-
const b = coerceToBoolean(args[0]);
|
|
163
|
-
if (isFormulaError(b)) return b;
|
|
164
|
-
return !b;
|
|
165
|
-
}
|
|
166
|
-
});
|
|
167
|
-
registerFormulaFunction("ROUND", {
|
|
168
|
-
minArgs: 2,
|
|
169
|
-
maxArgs: 2,
|
|
170
|
-
impl(args) {
|
|
171
|
-
const n = coerceToNumber(args[0]);
|
|
172
|
-
if (isFormulaError(n)) return n;
|
|
173
|
-
const d = coerceToNumber(args[1]);
|
|
174
|
-
if (isFormulaError(d)) return d;
|
|
175
|
-
const factor = 10 ** Math.trunc(d);
|
|
176
|
-
if (!Number.isFinite(factor)) return n;
|
|
177
|
-
if (factor === 0) return 0;
|
|
178
|
-
const shifted = n * factor;
|
|
179
|
-
return Math.sign(shifted) * Math.round(Math.abs(shifted) + 1e-12) / factor;
|
|
180
|
-
}
|
|
181
|
-
});
|
|
182
|
-
registerFormulaFunction("ABS", {
|
|
183
|
-
minArgs: 1,
|
|
184
|
-
maxArgs: 1,
|
|
185
|
-
impl(args) {
|
|
186
|
-
const n = coerceToNumber(args[0]);
|
|
187
|
-
if (isFormulaError(n)) return n;
|
|
188
|
-
return Math.abs(n);
|
|
189
|
-
}
|
|
190
|
-
});
|
|
191
|
-
registerFormulaFunction("CONCATENATE", {
|
|
192
|
-
minArgs: 1,
|
|
193
|
-
impl(args) {
|
|
194
|
-
let text = "";
|
|
195
|
-
for (const arg of args) {
|
|
196
|
-
const values = Array.isArray(arg) ? arg : [arg];
|
|
197
|
-
for (const value of values) {
|
|
198
|
-
const piece = coerceToText(value);
|
|
199
|
-
if (isFormulaError(piece)) return piece;
|
|
200
|
-
text += piece;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
return text;
|
|
204
|
-
}
|
|
205
|
-
});
|
|
206
|
-
//#endregion
|
|
207
|
-
export { getFormulaFunction, invokeFormulaFunction, registerFormulaFunction };
|
|
208
|
-
|
|
209
|
-
//# sourceMappingURL=functions.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"functions.js","names":[],"sources":["../../../src/core/formula/functions.ts"],"sourcesContent":["import type { AstNode } from './ast'\nimport { formulaError, isFormulaError, type FormulaError } from './errors'\nimport {\n coerceToBoolean,\n coerceToNumber,\n coerceToText,\n type EvalValue,\n type ScalarValue\n} from './evaluator'\n\n/**\n * 函数注册表(可扩展)+ 基础函数集。\n *\n * 参数形态约定:区域引用求值为数组(只含稀疏存在的格),直接参数为标量。\n * 聚合函数据此区分 Excel 语义:区域内的文本/布尔被忽略,直接参数则强转(非法 → #VALUE!)。\n * lazy 函数自行求值参数(IF 的短路分支,未选分支的副作用/错误不产生)。\n */\n\nexport type FormulaFunction =\n | { kind?: 'normal'; minArgs?: number; maxArgs?: number; impl: (args: EvalValue[]) => EvalValue }\n | {\n kind: 'lazy'\n minArgs?: number\n maxArgs?: number\n impl: (nodes: AstNode[], evalNode: (node: AstNode) => EvalValue) => EvalValue\n }\n\nconst registry = new Map<string, FormulaFunction>()\n\n/** 注册函数(名称大小写不敏感;同名覆盖,供扩展/自定义函数) */\nexport function registerFormulaFunction(name: string, def: FormulaFunction): void {\n registry.set(name.toUpperCase(), def)\n}\n\n/** 查询函数(大小写不敏感) */\nexport function getFormulaFunction(name: string): FormulaFunction | undefined {\n return registry.get(name.toUpperCase())\n}\n\n/** 求值器回调:名称解析 + 参数个数校验 + 按 kind 分发 */\nexport function invokeFormulaFunction(\n name: string,\n nodes: AstNode[],\n evalNode: (node: AstNode) => EvalValue\n): EvalValue {\n const def = getFormulaFunction(name)\n if (!def) return formulaError('#NAME?')\n if (def.minArgs !== undefined && nodes.length < def.minArgs) return formulaError('#VALUE!')\n if (def.maxArgs !== undefined && nodes.length > def.maxArgs) return formulaError('#VALUE!')\n if (def.kind === 'lazy') return def.impl(nodes, evalNode)\n return def.impl(nodes.map(evalNode))\n}\n\n// ─── 内部工具 ────────────────────────────────────────────────\n\ninterface FlatArg {\n value: ScalarValue | FormulaError\n /** 来自区域展开(区别于直接参数:区域内文本/布尔被聚合函数忽略) */\n fromRange: boolean\n}\n\nfunction flattenArgs(args: EvalValue[]): FlatArg[] {\n const out: FlatArg[] = []\n for (const arg of args) {\n if (Array.isArray(arg)) {\n for (const value of arg) out.push({ value, fromRange: true })\n } else {\n out.push({ value: arg, fromRange: false })\n }\n }\n return out\n}\n\n/** 收集数字:区域内只取数字格;直接参数强转(非法/错误 → 传播) */\nfunction collectNumbers(args: EvalValue[]): number[] | FormulaError {\n const numbers: number[] = []\n for (const { value, fromRange } of flattenArgs(args)) {\n if (isFormulaError(value)) return value\n if (fromRange) {\n if (typeof value === 'number') numbers.push(value)\n continue\n }\n const n = coerceToNumber(value)\n if (isFormulaError(n)) return n\n numbers.push(n)\n }\n return numbers\n}\n\n/** 收集布尔:区域内只取布尔格;直接参数强转(非法文本/错误 → 传播);空值跳过 */\nfunction collectBooleans(args: EvalValue[]): boolean[] | FormulaError {\n const booleans: boolean[] = []\n for (const { value, fromRange } of flattenArgs(args)) {\n if (isFormulaError(value)) return value\n if (fromRange) {\n if (typeof value === 'boolean') booleans.push(value)\n continue\n }\n if (value === null) continue\n const b = coerceToBoolean(value)\n if (isFormulaError(b)) return b\n booleans.push(b)\n }\n return booleans\n}\n\n// ─── 基础函数集 ─────────────────────────────────────────────\n\nregisterFormulaFunction('SUM', {\n minArgs: 1,\n impl(args) {\n const numbers = collectNumbers(args)\n if (isFormulaError(numbers)) return numbers\n let sum = 0\n for (const n of numbers) sum += n\n return sum\n }\n})\n\nregisterFormulaFunction('AVERAGE', {\n minArgs: 1,\n impl(args) {\n const numbers = collectNumbers(args)\n if (isFormulaError(numbers)) return numbers\n if (numbers.length === 0) return formulaError('#DIV/0!')\n let sum = 0\n for (const n of numbers) sum += n\n return sum / numbers.length\n }\n})\n\nregisterFormulaFunction('MAX', {\n minArgs: 1,\n impl(args) {\n const numbers = collectNumbers(args)\n if (isFormulaError(numbers)) return numbers\n return numbers.length === 0 ? 0 : Math.max(...numbers)\n }\n})\n\nregisterFormulaFunction('MIN', {\n minArgs: 1,\n impl(args) {\n const numbers = collectNumbers(args)\n if (isFormulaError(numbers)) return numbers\n return numbers.length === 0 ? 0 : Math.min(...numbers)\n }\n})\n\nregisterFormulaFunction('COUNT', {\n minArgs: 1,\n impl(args) {\n let count = 0\n for (const { value, fromRange } of flattenArgs(args)) {\n if (isFormulaError(value)) return value\n if (fromRange) {\n if (typeof value === 'number') count++\n continue\n }\n // 直接参数:可强转为数字即计数(含 TRUE/FALSE、数字文本)\n if (!isFormulaError(coerceToNumber(value))) count++\n }\n return count\n }\n})\n\nregisterFormulaFunction('COUNTA', {\n minArgs: 1,\n impl(args) {\n let count = 0\n for (const { value, fromRange } of flattenArgs(args)) {\n // 直接错误参数传播;区域内的错误格照常计数\n if (!fromRange && isFormulaError(value)) return value\n if (value === null) continue\n count++\n }\n return count\n }\n})\n\nregisterFormulaFunction('IF', {\n kind: 'lazy',\n minArgs: 2,\n maxArgs: 3,\n impl(nodes, evalNode) {\n const cond = coerceToBoolean(evalNode(nodes[0]!))\n if (isFormulaError(cond)) return cond\n if (cond) return evalNode(nodes[1]!)\n return nodes[2] ? evalNode(nodes[2]) : false\n }\n})\n\nregisterFormulaFunction('AND', {\n minArgs: 1,\n impl(args) {\n const booleans = collectBooleans(args)\n if (isFormulaError(booleans)) return booleans\n if (booleans.length === 0) return formulaError('#VALUE!')\n return booleans.every(Boolean)\n }\n})\n\nregisterFormulaFunction('OR', {\n minArgs: 1,\n impl(args) {\n const booleans = collectBooleans(args)\n if (isFormulaError(booleans)) return booleans\n if (booleans.length === 0) return formulaError('#VALUE!')\n return booleans.some(Boolean)\n }\n})\n\nregisterFormulaFunction('NOT', {\n minArgs: 1,\n maxArgs: 1,\n impl(args) {\n const b = coerceToBoolean(args[0]!)\n if (isFormulaError(b)) return b\n return !b\n }\n})\n\nregisterFormulaFunction('ROUND', {\n minArgs: 2,\n maxArgs: 2,\n impl(args) {\n const n = coerceToNumber(args[0]!)\n if (isFormulaError(n)) return n\n const d = coerceToNumber(args[1]!)\n if (isFormulaError(d)) return d\n const factor = 10 ** Math.trunc(d)\n // 位数超出 double 精度时四舍五入是恒等/归零\n if (!Number.isFinite(factor)) return n\n if (factor === 0) return 0\n // 远离零方向取整;1e-12 补偿浮点表示误差(如 2.675 存为 2.67499…)\n const shifted = n * factor\n const rounded = Math.sign(shifted) * Math.round(Math.abs(shifted) + 1e-12)\n return rounded / factor\n }\n})\n\nregisterFormulaFunction('ABS', {\n minArgs: 1,\n maxArgs: 1,\n impl(args) {\n const n = coerceToNumber(args[0]!)\n if (isFormulaError(n)) return n\n return Math.abs(n)\n }\n})\n\nregisterFormulaFunction('CONCATENATE', {\n minArgs: 1,\n impl(args) {\n let text = ''\n for (const arg of args) {\n const values = Array.isArray(arg) ? arg : [arg]\n for (const value of values) {\n const piece = coerceToText(value)\n if (isFormulaError(piece)) return piece\n text += piece\n }\n }\n return text\n }\n})\n"],"mappings":"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"}
|
|
@@ -1,208 +0,0 @@
|
|
|
1
|
-
import { createRange, parseAddress } from "../address.js";
|
|
2
|
-
import { FormulaParseError, tokenizeFormula } from "./tokenizer.js";
|
|
3
|
-
//#region src/core/formula/parser.ts
|
|
4
|
-
/**
|
|
5
|
-
* Pratt parser:token 序列 → AST。
|
|
6
|
-
*
|
|
7
|
-
* 优先级(同 Excel,低到高):比较运算 < `&` < `+ -` < `* /` < 一元 `+ -` < `^`(右结合)< `%`。
|
|
8
|
-
* 一元运算紧于幂次是 Excel 的特有行为:-2^2 = (-2)^2 = 4。
|
|
9
|
-
* 引用消歧:ident 后随 `(` → 函数调用;后随 `!` → 表名前缀;匹配单元格形态 → 引用;
|
|
10
|
-
* TRUE/FALSE → 布尔字面量;其余 → 未知名称节点(求值 #NAME?)。
|
|
11
|
-
*/
|
|
12
|
-
/** 中缀运算符绑定力 [左, 右];右结合运算符右绑定力 = 左 - 1 */
|
|
13
|
-
const INFIX_BP = {
|
|
14
|
-
"=": [1, 1],
|
|
15
|
-
"<>": [1, 1],
|
|
16
|
-
"<": [1, 1],
|
|
17
|
-
"<=": [1, 1],
|
|
18
|
-
">": [1, 1],
|
|
19
|
-
">=": [1, 1],
|
|
20
|
-
"&": [2, 2],
|
|
21
|
-
"+": [3, 3],
|
|
22
|
-
"-": [3, 3],
|
|
23
|
-
"*": [4, 4],
|
|
24
|
-
"/": [4, 4],
|
|
25
|
-
"^": [6, 5]
|
|
26
|
-
};
|
|
27
|
-
/** 百分号后缀绑定力(最高) */
|
|
28
|
-
const PERCENT_BP = 7;
|
|
29
|
-
/** 一元 +/- 操作数的绑定力:紧于 ^(6),松于 %(7) */
|
|
30
|
-
const UNARY_OPERAND_BP = 6;
|
|
31
|
-
/** 单元格引用形态(列限 3 字母;超出按未知名称处理,如 Sheet2) */
|
|
32
|
-
const CELL_REF_RE = /^\$?[A-Za-z]{1,3}\$?[1-9]\d*$/;
|
|
33
|
-
var Parser = class {
|
|
34
|
-
tokens;
|
|
35
|
-
pos = 0;
|
|
36
|
-
constructor(tokens) {
|
|
37
|
-
this.tokens = tokens;
|
|
38
|
-
}
|
|
39
|
-
parse() {
|
|
40
|
-
const node = this.parseExpression(0);
|
|
41
|
-
if (this.pos < this.tokens.length) throw new FormulaParseError(`意外的多余输入: ${describeToken(this.tokens[this.pos])}`);
|
|
42
|
-
return node;
|
|
43
|
-
}
|
|
44
|
-
peek() {
|
|
45
|
-
return this.tokens[this.pos];
|
|
46
|
-
}
|
|
47
|
-
next() {
|
|
48
|
-
return this.tokens[this.pos++];
|
|
49
|
-
}
|
|
50
|
-
peekOp(op) {
|
|
51
|
-
const token = this.peek();
|
|
52
|
-
return token?.type === "op" && token.op === op;
|
|
53
|
-
}
|
|
54
|
-
expectOp(op) {
|
|
55
|
-
if (!this.peekOp(op)) throw new FormulaParseError(`期望 "${op}",得到 ${describeToken(this.peek())}`);
|
|
56
|
-
this.pos++;
|
|
57
|
-
}
|
|
58
|
-
parseExpression(minBp) {
|
|
59
|
-
let left = this.parsePrimary();
|
|
60
|
-
for (;;) {
|
|
61
|
-
const token = this.peek();
|
|
62
|
-
if (token?.type !== "op") break;
|
|
63
|
-
if (token.op === "%") {
|
|
64
|
-
if (PERCENT_BP <= minBp) break;
|
|
65
|
-
this.pos++;
|
|
66
|
-
left = {
|
|
67
|
-
kind: "percent",
|
|
68
|
-
operand: left
|
|
69
|
-
};
|
|
70
|
-
continue;
|
|
71
|
-
}
|
|
72
|
-
const bp = INFIX_BP[token.op];
|
|
73
|
-
if (!bp || bp[0] <= minBp) break;
|
|
74
|
-
this.pos++;
|
|
75
|
-
const right = this.parseExpression(bp[1]);
|
|
76
|
-
left = {
|
|
77
|
-
kind: "binary",
|
|
78
|
-
op: token.op,
|
|
79
|
-
left,
|
|
80
|
-
right
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
return left;
|
|
84
|
-
}
|
|
85
|
-
parsePrimary() {
|
|
86
|
-
const token = this.next();
|
|
87
|
-
if (!token) throw new FormulaParseError("表达式不完整");
|
|
88
|
-
if (token.type === "number") return {
|
|
89
|
-
kind: "number",
|
|
90
|
-
value: token.value
|
|
91
|
-
};
|
|
92
|
-
if (token.type === "string") return {
|
|
93
|
-
kind: "string",
|
|
94
|
-
value: token.value
|
|
95
|
-
};
|
|
96
|
-
if (token.type === "quoted-name") {
|
|
97
|
-
this.expectOp("!");
|
|
98
|
-
return this.parseCellRefOrRange(token.name);
|
|
99
|
-
}
|
|
100
|
-
if (token.type === "ident") {
|
|
101
|
-
const name = token.name;
|
|
102
|
-
if (this.peekOp("(")) return this.parseCall(name);
|
|
103
|
-
if (this.peekOp("!")) {
|
|
104
|
-
this.pos++;
|
|
105
|
-
return this.parseCellRefOrRange(name);
|
|
106
|
-
}
|
|
107
|
-
const addr = parseCellRef(name);
|
|
108
|
-
if (addr) return this.parseRangeTail({
|
|
109
|
-
sheet: void 0,
|
|
110
|
-
addr
|
|
111
|
-
});
|
|
112
|
-
if (name.toUpperCase() === "TRUE") return {
|
|
113
|
-
kind: "boolean",
|
|
114
|
-
value: true
|
|
115
|
-
};
|
|
116
|
-
if (name.toUpperCase() === "FALSE") return {
|
|
117
|
-
kind: "boolean",
|
|
118
|
-
value: false
|
|
119
|
-
};
|
|
120
|
-
return {
|
|
121
|
-
kind: "name",
|
|
122
|
-
name
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
if (token.type === "op") {
|
|
126
|
-
if (token.op === "(") {
|
|
127
|
-
const inner = this.parseExpression(0);
|
|
128
|
-
this.expectOp(")");
|
|
129
|
-
return inner;
|
|
130
|
-
}
|
|
131
|
-
if (token.op === "-" || token.op === "+") return {
|
|
132
|
-
kind: "unary",
|
|
133
|
-
op: token.op,
|
|
134
|
-
operand: this.parseExpression(UNARY_OPERAND_BP)
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
throw new FormulaParseError(`意外的 ${describeToken(token)}`);
|
|
138
|
-
}
|
|
139
|
-
/** 函数调用:ident 已消费,当前 token 为 '(' */
|
|
140
|
-
parseCall(name) {
|
|
141
|
-
this.pos++;
|
|
142
|
-
const args = [];
|
|
143
|
-
if (!this.peekOp(")")) for (;;) {
|
|
144
|
-
args.push(this.parseExpression(0));
|
|
145
|
-
if (this.peekOp(",")) {
|
|
146
|
-
this.pos++;
|
|
147
|
-
continue;
|
|
148
|
-
}
|
|
149
|
-
break;
|
|
150
|
-
}
|
|
151
|
-
this.expectOp(")");
|
|
152
|
-
return {
|
|
153
|
-
kind: "call",
|
|
154
|
-
name,
|
|
155
|
-
args
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
/** 表名前缀(Sheet2! / 'My Sheet'!)之后的单元格或区域引用 */
|
|
159
|
-
parseCellRefOrRange(sheet) {
|
|
160
|
-
const token = this.next();
|
|
161
|
-
const addr = token?.type === "ident" ? parseCellRef(token.name) : null;
|
|
162
|
-
if (!addr) throw new FormulaParseError(`表名 "${sheet}" 后应为单元格引用,得到 ${describeToken(token)}`);
|
|
163
|
-
return this.parseRangeTail({
|
|
164
|
-
sheet,
|
|
165
|
-
addr
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
/** 单元格引用之后的可选区域后缀(:B9) */
|
|
169
|
-
parseRangeTail(ref) {
|
|
170
|
-
if (!this.peekOp(":")) return {
|
|
171
|
-
kind: "cell",
|
|
172
|
-
sheet: ref.sheet,
|
|
173
|
-
addr: ref.addr
|
|
174
|
-
};
|
|
175
|
-
this.pos++;
|
|
176
|
-
const token = this.next();
|
|
177
|
-
const end = token?.type === "ident" ? parseCellRef(token.name) : null;
|
|
178
|
-
if (!end) throw new FormulaParseError(`区域 ":" 后应为单元格引用,得到 ${describeToken(token)}`);
|
|
179
|
-
return {
|
|
180
|
-
kind: "range",
|
|
181
|
-
sheet: ref.sheet,
|
|
182
|
-
range: createRange(ref.addr, end)
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
};
|
|
186
|
-
/** 按单元格引用形态解析(列限 3 字母);不匹配返回 null */
|
|
187
|
-
function parseCellRef(name) {
|
|
188
|
-
if (!CELL_REF_RE.test(name)) return null;
|
|
189
|
-
return parseAddress(name);
|
|
190
|
-
}
|
|
191
|
-
function describeToken(token) {
|
|
192
|
-
if (!token) return "输入末尾";
|
|
193
|
-
switch (token.type) {
|
|
194
|
-
case "number": return `数字 ${token.value}`;
|
|
195
|
-
case "string": return `字符串 "${token.value}"`;
|
|
196
|
-
case "ident": return `名称 "${token.name}"`;
|
|
197
|
-
case "quoted-name": return `表名 '${token.name}'`;
|
|
198
|
-
case "op": return `"${token.op}"`;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
/** 解析公式文本(不含 '=')→ AST;非法输入抛 FormulaParseError */
|
|
202
|
-
function parseFormula(text) {
|
|
203
|
-
return new Parser(tokenizeFormula(text)).parse();
|
|
204
|
-
}
|
|
205
|
-
//#endregion
|
|
206
|
-
export { parseFormula };
|
|
207
|
-
|
|
208
|
-
//# sourceMappingURL=parser.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"parser.js","names":[],"sources":["../../../src/core/formula/parser.ts"],"sourcesContent":["import { createRange, parseAddress, type CellAddress } from '../address'\nimport type { AstNode, BinaryOperator } from './ast'\nimport { FormulaParseError, tokenizeFormula, type FormulaToken } from './tokenizer'\n\n/**\n * Pratt parser:token 序列 → AST。\n *\n * 优先级(同 Excel,低到高):比较运算 < `&` < `+ -` < `* /` < 一元 `+ -` < `^`(右结合)< `%`。\n * 一元运算紧于幂次是 Excel 的特有行为:-2^2 = (-2)^2 = 4。\n * 引用消歧:ident 后随 `(` → 函数调用;后随 `!` → 表名前缀;匹配单元格形态 → 引用;\n * TRUE/FALSE → 布尔字面量;其余 → 未知名称节点(求值 #NAME?)。\n */\n\n/** 中缀运算符绑定力 [左, 右];右结合运算符右绑定力 = 左 - 1 */\nconst INFIX_BP: Record<string, readonly [number, number]> = {\n '=': [1, 1],\n '<>': [1, 1],\n '<': [1, 1],\n '<=': [1, 1],\n '>': [1, 1],\n '>=': [1, 1],\n '&': [2, 2],\n '+': [3, 3],\n '-': [3, 3],\n '*': [4, 4],\n '/': [4, 4],\n '^': [6, 5]\n}\n/** 百分号后缀绑定力(最高) */\nconst PERCENT_BP = 7\n/** 一元 +/- 操作数的绑定力:紧于 ^(6),松于 %(7) */\nconst UNARY_OPERAND_BP = 6\n\n/** 单元格引用形态(列限 3 字母;超出按未知名称处理,如 Sheet2) */\nconst CELL_REF_RE = /^\\$?[A-Za-z]{1,3}\\$?[1-9]\\d*$/\n\nclass Parser {\n private pos = 0\n\n constructor(private readonly tokens: FormulaToken[]) {}\n\n parse(): AstNode {\n const node = this.parseExpression(0)\n if (this.pos < this.tokens.length) {\n throw new FormulaParseError(`意外的多余输入: ${describeToken(this.tokens[this.pos]!)}`)\n }\n return node\n }\n\n private peek(): FormulaToken | undefined {\n return this.tokens[this.pos]\n }\n\n private next(): FormulaToken | undefined {\n return this.tokens[this.pos++]\n }\n\n private peekOp(op: string): boolean {\n const token = this.peek()\n return token?.type === 'op' && token.op === op\n }\n\n private expectOp(op: string): void {\n if (!this.peekOp(op)) {\n throw new FormulaParseError(`期望 \"${op}\",得到 ${describeToken(this.peek())}`)\n }\n this.pos++\n }\n\n private parseExpression(minBp: number): AstNode {\n let left = this.parsePrimary()\n for (;;) {\n const token = this.peek()\n if (token?.type !== 'op') break\n // 百分号后缀\n if (token.op === '%') {\n if (PERCENT_BP <= minBp) break\n this.pos++\n left = { kind: 'percent', operand: left }\n continue\n }\n const bp = INFIX_BP[token.op]\n if (!bp || bp[0] <= minBp) break\n this.pos++\n const right = this.parseExpression(bp[1])\n left = { kind: 'binary', op: token.op as BinaryOperator, left, right }\n }\n return left\n }\n\n private parsePrimary(): AstNode {\n const token = this.next()\n if (!token) throw new FormulaParseError('表达式不完整')\n\n if (token.type === 'number') return { kind: 'number', value: token.value }\n if (token.type === 'string') return { kind: 'string', value: token.value }\n\n if (token.type === 'quoted-name') {\n // 带引号表名只能作为跨表引用前缀\n this.expectOp('!')\n return this.parseCellRefOrRange(token.name)\n }\n\n if (token.type === 'ident') {\n const name = token.name\n if (this.peekOp('(')) return this.parseCall(name)\n if (this.peekOp('!')) {\n this.pos++\n return this.parseCellRefOrRange(name)\n }\n const addr = parseCellRef(name)\n if (addr) return this.parseRangeTail({ sheet: undefined, addr })\n if (name.toUpperCase() === 'TRUE') return { kind: 'boolean', value: true }\n if (name.toUpperCase() === 'FALSE') return { kind: 'boolean', value: false }\n return { kind: 'name', name }\n }\n\n // 运算符位置:括号 / 一元 +-\n if (token.type === 'op') {\n if (token.op === '(') {\n const inner = this.parseExpression(0)\n this.expectOp(')')\n return inner\n }\n if (token.op === '-' || token.op === '+') {\n return { kind: 'unary', op: token.op, operand: this.parseExpression(UNARY_OPERAND_BP) }\n }\n }\n throw new FormulaParseError(`意外的 ${describeToken(token)}`)\n }\n\n /** 函数调用:ident 已消费,当前 token 为 '(' */\n private parseCall(name: string): AstNode {\n this.pos++ // 消费 '('\n const args: AstNode[] = []\n if (!this.peekOp(')')) {\n for (;;) {\n args.push(this.parseExpression(0))\n if (this.peekOp(',')) {\n this.pos++\n continue\n }\n break\n }\n }\n this.expectOp(')')\n return { kind: 'call', name, args }\n }\n\n /** 表名前缀(Sheet2! / 'My Sheet'!)之后的单元格或区域引用 */\n private parseCellRefOrRange(sheet: string): AstNode {\n const token = this.next()\n const addr = token?.type === 'ident' ? parseCellRef(token.name) : null\n if (!addr) {\n throw new FormulaParseError(`表名 \"${sheet}\" 后应为单元格引用,得到 ${describeToken(token)}`)\n }\n return this.parseRangeTail({ sheet, addr })\n }\n\n /** 单元格引用之后的可选区域后缀(:B9) */\n private parseRangeTail(ref: { sheet: string | undefined; addr: CellAddress }): AstNode {\n if (!this.peekOp(':')) return { kind: 'cell', sheet: ref.sheet, addr: ref.addr }\n this.pos++\n const token = this.next()\n const end = token?.type === 'ident' ? parseCellRef(token.name) : null\n if (!end) {\n throw new FormulaParseError(`区域 \":\" 后应为单元格引用,得到 ${describeToken(token)}`)\n }\n return { kind: 'range', sheet: ref.sheet, range: createRange(ref.addr, end) }\n }\n}\n\n/** 按单元格引用形态解析(列限 3 字母);不匹配返回 null */\nfunction parseCellRef(name: string): CellAddress | null {\n if (!CELL_REF_RE.test(name)) return null\n return parseAddress(name)\n}\n\nfunction describeToken(token: FormulaToken | undefined): string {\n if (!token) return '输入末尾'\n switch (token.type) {\n case 'number':\n return `数字 ${token.value}`\n case 'string':\n return `字符串 \"${token.value}\"`\n case 'ident':\n return `名称 \"${token.name}\"`\n case 'quoted-name':\n return `表名 '${token.name}'`\n case 'op':\n return `\"${token.op}\"`\n }\n}\n\n/** 解析公式文本(不含 '=')→ AST;非法输入抛 FormulaParseError */\nexport function parseFormula(text: string): AstNode {\n return new Parser(tokenizeFormula(text)).parse()\n}\n"],"mappings":"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"}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
//#region src/core/formula/tokenizer.d.ts
|
|
2
|
-
/**
|
|
3
|
-
* 公式分词器:数字 / 字符串 / 布尔 / 标识符(函数名、引用、表名)/ 带引号表名 / 运算符。
|
|
4
|
-
*
|
|
5
|
-
* 不做语义消歧:`A1`(引用)、`SUM`(函数名)、`Sheet2`(表名)统一产出 ident,
|
|
6
|
-
* 由 parser 依据后随 token(`(` / `!` / `:` / 运算位)判定。
|
|
7
|
-
*/
|
|
8
|
-
/** 解析失败异常(Sheet 层捕获 → 单元格记为 #ERROR!) */
|
|
9
|
-
declare class FormulaParseError extends Error {
|
|
10
|
-
constructor(message: string);
|
|
11
|
-
}
|
|
12
|
-
type FormulaOperator = '+' | '-' | '*' | '/' | '^' | '&' | '%' | '(' | ')' | ',' | '!' | ':' | '=' | '<>' | '<' | '<=' | '>' | '>=';
|
|
13
|
-
type FormulaToken = {
|
|
14
|
-
type: 'number';
|
|
15
|
-
value: number;
|
|
16
|
-
} | {
|
|
17
|
-
type: 'string';
|
|
18
|
-
value: string;
|
|
19
|
-
} |
|
|
20
|
-
/** 标识符:函数名 / 单元格引用形态 / 裸表名(含 $ 绝对引用写法);TRUE/FALSE 由 parser 归约为布尔 */
|
|
21
|
-
{
|
|
22
|
-
type: 'ident';
|
|
23
|
-
name: string;
|
|
24
|
-
} |
|
|
25
|
-
/** 带单引号的表名('' 转义为字面单引号) */
|
|
26
|
-
{
|
|
27
|
-
type: 'quoted-name';
|
|
28
|
-
name: string;
|
|
29
|
-
} | {
|
|
30
|
-
type: 'op';
|
|
31
|
-
op: FormulaOperator;
|
|
32
|
-
};
|
|
33
|
-
/** 公式文本(不含 '=')→ token 序列;非法输入抛 FormulaParseError */
|
|
34
|
-
declare function tokenizeFormula(text: string): FormulaToken[];
|
|
35
|
-
//#endregion
|
|
36
|
-
export { FormulaOperator, FormulaParseError, FormulaToken, tokenizeFormula };
|
|
37
|
-
//# sourceMappingURL=tokenizer.d.ts.map
|
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
//#region src/core/formula/tokenizer.ts
|
|
2
|
-
/**
|
|
3
|
-
* 公式分词器:数字 / 字符串 / 布尔 / 标识符(函数名、引用、表名)/ 带引号表名 / 运算符。
|
|
4
|
-
*
|
|
5
|
-
* 不做语义消歧:`A1`(引用)、`SUM`(函数名)、`Sheet2`(表名)统一产出 ident,
|
|
6
|
-
* 由 parser 依据后随 token(`(` / `!` / `:` / 运算位)判定。
|
|
7
|
-
*/
|
|
8
|
-
/** 解析失败异常(Sheet 层捕获 → 单元格记为 #ERROR!) */
|
|
9
|
-
var FormulaParseError = class extends Error {
|
|
10
|
-
constructor(message) {
|
|
11
|
-
super(message);
|
|
12
|
-
this.name = "FormulaParseError";
|
|
13
|
-
}
|
|
14
|
-
};
|
|
15
|
-
const NUMBER_RE = /^(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?/;
|
|
16
|
-
/** 标识符首字符:字母 / _ / $ / 非 ASCII(中文表名等) */
|
|
17
|
-
function isIdentStart(ch) {
|
|
18
|
-
return /[A-Za-z_$]/.test(ch) || ch.charCodeAt(0) >= 128;
|
|
19
|
-
}
|
|
20
|
-
/** 标识符后续字符:首字符集 + 数字 + . */
|
|
21
|
-
function isIdentPart(ch) {
|
|
22
|
-
return /[A-Za-z0-9_.$]/.test(ch) || ch.charCodeAt(0) >= 128;
|
|
23
|
-
}
|
|
24
|
-
const TWO_CHAR_OPS = [
|
|
25
|
-
"<>",
|
|
26
|
-
"<=",
|
|
27
|
-
">="
|
|
28
|
-
];
|
|
29
|
-
const ONE_CHAR_OPS = [
|
|
30
|
-
"+",
|
|
31
|
-
"-",
|
|
32
|
-
"*",
|
|
33
|
-
"/",
|
|
34
|
-
"^",
|
|
35
|
-
"&",
|
|
36
|
-
"%",
|
|
37
|
-
"(",
|
|
38
|
-
")",
|
|
39
|
-
",",
|
|
40
|
-
"!",
|
|
41
|
-
":",
|
|
42
|
-
"=",
|
|
43
|
-
"<",
|
|
44
|
-
">"
|
|
45
|
-
];
|
|
46
|
-
/** 公式文本(不含 '=')→ token 序列;非法输入抛 FormulaParseError */
|
|
47
|
-
function tokenizeFormula(text) {
|
|
48
|
-
const tokens = [];
|
|
49
|
-
let i = 0;
|
|
50
|
-
while (i < text.length) {
|
|
51
|
-
const ch = text[i];
|
|
52
|
-
if (ch === " " || ch === " " || ch === "\n" || ch === "\r") {
|
|
53
|
-
i++;
|
|
54
|
-
continue;
|
|
55
|
-
}
|
|
56
|
-
if (ch >= "0" && ch <= "9" || ch === "." && text[i + 1] >= "0" && text[i + 1] <= "9") {
|
|
57
|
-
const match = NUMBER_RE.exec(text.slice(i));
|
|
58
|
-
if (!match) throw new FormulaParseError(`非法数字(位置 ${i + 1})`);
|
|
59
|
-
tokens.push({
|
|
60
|
-
type: "number",
|
|
61
|
-
value: Number.parseFloat(match[0])
|
|
62
|
-
});
|
|
63
|
-
i += match[0].length;
|
|
64
|
-
continue;
|
|
65
|
-
}
|
|
66
|
-
if (ch === "\"") {
|
|
67
|
-
let value = "";
|
|
68
|
-
let j = i + 1;
|
|
69
|
-
for (;;) {
|
|
70
|
-
if (j >= text.length) throw new FormulaParseError("字符串缺少结束引号");
|
|
71
|
-
if (text[j] === "\"") {
|
|
72
|
-
if (text[j + 1] === "\"") {
|
|
73
|
-
value += "\"";
|
|
74
|
-
j += 2;
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
break;
|
|
78
|
-
}
|
|
79
|
-
value += text[j];
|
|
80
|
-
j++;
|
|
81
|
-
}
|
|
82
|
-
tokens.push({
|
|
83
|
-
type: "string",
|
|
84
|
-
value
|
|
85
|
-
});
|
|
86
|
-
i = j + 1;
|
|
87
|
-
continue;
|
|
88
|
-
}
|
|
89
|
-
if (ch === "'") {
|
|
90
|
-
let name = "";
|
|
91
|
-
let j = i + 1;
|
|
92
|
-
for (;;) {
|
|
93
|
-
if (j >= text.length) throw new FormulaParseError("表名缺少结束引号");
|
|
94
|
-
if (text[j] === "'") {
|
|
95
|
-
if (text[j + 1] === "'") {
|
|
96
|
-
name += "'";
|
|
97
|
-
j += 2;
|
|
98
|
-
continue;
|
|
99
|
-
}
|
|
100
|
-
break;
|
|
101
|
-
}
|
|
102
|
-
name += text[j];
|
|
103
|
-
j++;
|
|
104
|
-
}
|
|
105
|
-
tokens.push({
|
|
106
|
-
type: "quoted-name",
|
|
107
|
-
name
|
|
108
|
-
});
|
|
109
|
-
i = j + 1;
|
|
110
|
-
continue;
|
|
111
|
-
}
|
|
112
|
-
if (isIdentStart(ch)) {
|
|
113
|
-
let j = i + 1;
|
|
114
|
-
while (j < text.length && isIdentPart(text[j])) j++;
|
|
115
|
-
tokens.push({
|
|
116
|
-
type: "ident",
|
|
117
|
-
name: text.slice(i, j)
|
|
118
|
-
});
|
|
119
|
-
i = j;
|
|
120
|
-
continue;
|
|
121
|
-
}
|
|
122
|
-
const two = text.slice(i, i + 2);
|
|
123
|
-
if (TWO_CHAR_OPS.includes(two)) {
|
|
124
|
-
tokens.push({
|
|
125
|
-
type: "op",
|
|
126
|
-
op: two
|
|
127
|
-
});
|
|
128
|
-
i += 2;
|
|
129
|
-
continue;
|
|
130
|
-
}
|
|
131
|
-
if (ONE_CHAR_OPS.includes(ch)) {
|
|
132
|
-
tokens.push({
|
|
133
|
-
type: "op",
|
|
134
|
-
op: ch
|
|
135
|
-
});
|
|
136
|
-
i++;
|
|
137
|
-
continue;
|
|
138
|
-
}
|
|
139
|
-
throw new FormulaParseError(`无法识别的字符 "${ch}"(位置 ${i + 1})`);
|
|
140
|
-
}
|
|
141
|
-
return tokens;
|
|
142
|
-
}
|
|
143
|
-
//#endregion
|
|
144
|
-
export { FormulaParseError, tokenizeFormula };
|
|
145
|
-
|
|
146
|
-
//# sourceMappingURL=tokenizer.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"tokenizer.js","names":[],"sources":["../../../src/core/formula/tokenizer.ts"],"sourcesContent":["/**\n * 公式分词器:数字 / 字符串 / 布尔 / 标识符(函数名、引用、表名)/ 带引号表名 / 运算符。\n *\n * 不做语义消歧:`A1`(引用)、`SUM`(函数名)、`Sheet2`(表名)统一产出 ident,\n * 由 parser 依据后随 token(`(` / `!` / `:` / 运算位)判定。\n */\n\n/** 解析失败异常(Sheet 层捕获 → 单元格记为 #ERROR!) */\nexport class FormulaParseError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'FormulaParseError'\n }\n}\n\nexport type FormulaOperator =\n | '+'\n | '-'\n | '*'\n | '/'\n | '^'\n | '&'\n | '%'\n | '('\n | ')'\n | ','\n | '!'\n | ':'\n | '='\n | '<>'\n | '<'\n | '<='\n | '>'\n | '>='\n\nexport type FormulaToken =\n | { type: 'number'; value: number }\n | { type: 'string'; value: string }\n /** 标识符:函数名 / 单元格引用形态 / 裸表名(含 $ 绝对引用写法);TRUE/FALSE 由 parser 归约为布尔 */\n | { type: 'ident'; name: string }\n /** 带单引号的表名('' 转义为字面单引号) */\n | { type: 'quoted-name'; name: string }\n | { type: 'op'; op: FormulaOperator }\n\nconst NUMBER_RE = /^(\\d+\\.?\\d*|\\.\\d+)([eE][+-]?\\d+)?/\n\n/** 标识符首字符:字母 / _ / $ / 非 ASCII(中文表名等) */\nfunction isIdentStart(ch: string): boolean {\n return /[A-Za-z_$]/.test(ch) || ch.charCodeAt(0) >= 0x80\n}\n\n/** 标识符后续字符:首字符集 + 数字 + . */\nfunction isIdentPart(ch: string): boolean {\n return /[A-Za-z0-9_.$]/.test(ch) || ch.charCodeAt(0) >= 0x80\n}\n\nconst TWO_CHAR_OPS = ['<>', '<=', '>='] as const\nconst ONE_CHAR_OPS = [\n '+',\n '-',\n '*',\n '/',\n '^',\n '&',\n '%',\n '(',\n ')',\n ',',\n '!',\n ':',\n '=',\n '<',\n '>'\n] as const\n\n/** 公式文本(不含 '=')→ token 序列;非法输入抛 FormulaParseError */\nexport function tokenizeFormula(text: string): FormulaToken[] {\n const tokens: FormulaToken[] = []\n let i = 0\n while (i < text.length) {\n const ch = text[i]!\n // 空白忽略(不支持 Excel 的空格交集运算符)\n if (ch === ' ' || ch === '\\t' || ch === '\\n' || ch === '\\r') {\n i++\n continue\n }\n // 数字\n if ((ch >= '0' && ch <= '9') || (ch === '.' && text[i + 1]! >= '0' && text[i + 1]! <= '9')) {\n const match = NUMBER_RE.exec(text.slice(i))\n if (!match) throw new FormulaParseError(`非法数字(位置 ${i + 1})`)\n tokens.push({ type: 'number', value: Number.parseFloat(match[0]) })\n i += match[0].length\n continue\n }\n // 字符串字面量(\"...\",\"\" 转义)\n if (ch === '\"') {\n let value = ''\n let j = i + 1\n for (;;) {\n if (j >= text.length) throw new FormulaParseError('字符串缺少结束引号')\n if (text[j] === '\"') {\n if (text[j + 1] === '\"') {\n value += '\"'\n j += 2\n continue\n }\n break\n }\n value += text[j]\n j++\n }\n tokens.push({ type: 'string', value })\n i = j + 1\n continue\n }\n // 带引号表名('...','' 转义)\n if (ch === \"'\") {\n let name = ''\n let j = i + 1\n for (;;) {\n if (j >= text.length) throw new FormulaParseError('表名缺少结束引号')\n if (text[j] === \"'\") {\n if (text[j + 1] === \"'\") {\n name += \"'\"\n j += 2\n continue\n }\n break\n }\n name += text[j]\n j++\n }\n tokens.push({ type: 'quoted-name', name })\n i = j + 1\n continue\n }\n // 标识符(函数名 / 引用 / 裸表名)\n if (isIdentStart(ch)) {\n let j = i + 1\n while (j < text.length && isIdentPart(text[j]!)) j++\n tokens.push({ type: 'ident', name: text.slice(i, j) })\n i = j\n continue\n }\n // 双字符运算符优先\n const two = text.slice(i, i + 2)\n if ((TWO_CHAR_OPS as readonly string[]).includes(two)) {\n tokens.push({ type: 'op', op: two as FormulaOperator })\n i += 2\n continue\n }\n if ((ONE_CHAR_OPS as readonly string[]).includes(ch)) {\n tokens.push({ type: 'op', op: ch as FormulaOperator })\n i++\n continue\n }\n throw new FormulaParseError(`无法识别的字符 \"${ch}\"(位置 ${i + 1})`)\n }\n return tokens\n}\n"],"mappings":";;;;;;;;AAQA,IAAa,oBAAb,cAAuC,MAAM;CAC3C,YAAY,SAAiB;EAC3B,MAAM,OAAO;EACb,KAAK,OAAO;CACd;AACF;AA+BA,MAAM,YAAY;;AAGlB,SAAS,aAAa,IAAqB;CACzC,OAAO,aAAa,KAAK,EAAE,KAAK,GAAG,WAAW,CAAC,KAAK;AACtD;;AAGA,SAAS,YAAY,IAAqB;CACxC,OAAO,iBAAiB,KAAK,EAAE,KAAK,GAAG,WAAW,CAAC,KAAK;AAC1D;AAEA,MAAM,eAAe;CAAC;CAAM;CAAM;AAAI;AACtC,MAAM,eAAe;CACnB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;;AAGA,SAAgB,gBAAgB,MAA8B;CAC5D,MAAM,SAAyB,CAAC;CAChC,IAAI,IAAI;CACR,OAAO,IAAI,KAAK,QAAQ;EACtB,MAAM,KAAK,KAAK;EAEhB,IAAI,OAAO,OAAO,OAAO,OAAQ,OAAO,QAAQ,OAAO,MAAM;GAC3D;GACA;EACF;EAEA,IAAK,MAAM,OAAO,MAAM,OAAS,OAAO,OAAO,KAAK,IAAI,MAAO,OAAO,KAAK,IAAI,MAAO,KAAM;GAC1F,MAAM,QAAQ,UAAU,KAAK,KAAK,MAAM,CAAC,CAAC;GAC1C,IAAI,CAAC,OAAO,MAAM,IAAI,kBAAkB,WAAW,IAAI,EAAE,EAAE;GAC3D,OAAO,KAAK;IAAE,MAAM;IAAU,OAAO,OAAO,WAAW,MAAM,EAAE;GAAE,CAAC;GAClE,KAAK,MAAM,EAAE,CAAC;GACd;EACF;EAEA,IAAI,OAAO,MAAK;GACd,IAAI,QAAQ;GACZ,IAAI,IAAI,IAAI;GACZ,SAAS;IACP,IAAI,KAAK,KAAK,QAAQ,MAAM,IAAI,kBAAkB,WAAW;IAC7D,IAAI,KAAK,OAAO,MAAK;KACnB,IAAI,KAAK,IAAI,OAAO,MAAK;MACvB,SAAS;MACT,KAAK;MACL;KACF;KACA;IACF;IACA,SAAS,KAAK;IACd;GACF;GACA,OAAO,KAAK;IAAE,MAAM;IAAU;GAAM,CAAC;GACrC,IAAI,IAAI;GACR;EACF;EAEA,IAAI,OAAO,KAAK;GACd,IAAI,OAAO;GACX,IAAI,IAAI,IAAI;GACZ,SAAS;IACP,IAAI,KAAK,KAAK,QAAQ,MAAM,IAAI,kBAAkB,UAAU;IAC5D,IAAI,KAAK,OAAO,KAAK;KACnB,IAAI,KAAK,IAAI,OAAO,KAAK;MACvB,QAAQ;MACR,KAAK;MACL;KACF;KACA;IACF;IACA,QAAQ,KAAK;IACb;GACF;GACA,OAAO,KAAK;IAAE,MAAM;IAAe;GAAK,CAAC;GACzC,IAAI,IAAI;GACR;EACF;EAEA,IAAI,aAAa,EAAE,GAAG;GACpB,IAAI,IAAI,IAAI;GACZ,OAAO,IAAI,KAAK,UAAU,YAAY,KAAK,EAAG,GAAG;GACjD,OAAO,KAAK;IAAE,MAAM;IAAS,MAAM,KAAK,MAAM,GAAG,CAAC;GAAE,CAAC;GACrD,IAAI;GACJ;EACF;EAEA,MAAM,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;EAC/B,IAAK,aAAmC,SAAS,GAAG,GAAG;GACrD,OAAO,KAAK;IAAE,MAAM;IAAM,IAAI;GAAuB,CAAC;GACtD,KAAK;GACL;EACF;EACA,IAAK,aAAmC,SAAS,EAAE,GAAG;GACpD,OAAO,KAAK;IAAE,MAAM;IAAM,IAAI;GAAsB,CAAC;GACrD;GACA;EACF;EACA,MAAM,IAAI,kBAAkB,YAAY,GAAG,OAAO,IAAI,EAAE,EAAE;CAC5D;CACA,OAAO;AACT"}
|