@veltra/sheet 2.0.1 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/report/binding-badge.js +76 -0
- package/dist/components/report/designer/cell-coords.js +46 -0
- package/dist/components/report/designer/col-widths.js +15 -0
- package/dist/components/report/designer/conditional-rules/helpers.js +139 -0
- package/dist/components/report/designer/drop-highlight-overlay.js +76 -0
- package/dist/components/report/designer/float-panel.js +385 -0
- package/dist/components/report/designer/role.js +17 -0
- package/dist/components/report/designer/rule-preview.js +33 -0
- package/dist/components/report/designer/rule-row.js +269 -0
- package/dist/components/report/designer/rules-dialog.js +135 -0
- package/dist/components/report/designer/topology-overlay.js +122 -0
- package/dist/components/report/designer/topology.js +64 -0
- package/dist/components/report/designer/use-grid-overlay.js +75 -0
- package/dist/components/report/designer-hub.js +177 -0
- package/dist/components/report/field-panel-helpers.js +33 -0
- package/dist/components/report/field-panel.js +132 -0
- package/dist/components/report/filter-bar.js +74 -0
- package/dist/components/report/hub-connection-form.js +145 -0
- package/dist/components/report/hub-dataset-editor.js +394 -0
- package/dist/components/report/report-designer.js +343 -0
- package/dist/components/report/report-viewer.js +111 -0
- package/dist/components/report/style.d.ts +17 -0
- package/dist/components/report/style.js +18 -0
- package/dist/components/report/style2.css +869 -0
- package/dist/components/report/use-report-designer.js +360 -0
- package/dist/components/report/use-report-viewer.js +50 -0
- package/dist/components/sheet/formula-bar.js +393 -0
- package/dist/components/sheet/formula-suggest-list.js +66 -0
- package/dist/components/sheet/import-file.js +140 -0
- package/dist/components/sheet/insert-count-menu-item.js +68 -0
- package/dist/components/sheet/insert-image.js +79 -0
- package/dist/components/sheet/popup-helpers.js +73 -0
- package/dist/components/sheet/popups/border-popup.js +75 -0
- package/dist/components/sheet/popups/border-preset-glyph.js +106 -0
- package/dist/components/sheet/popups/export-popup.js +51 -0
- package/dist/components/sheet/popups/fill-color-popup.js +37 -0
- package/dist/components/sheet/popups/find-popup.js +113 -0
- package/dist/components/sheet/popups/font-color-popup.js +37 -0
- package/dist/components/sheet/popups/font-size-popup.js +63 -0
- package/dist/components/sheet/popups/import.worker.d.ts +21 -0
- package/dist/components/sheet/popups/import.worker.js +41 -0
- package/dist/components/sheet/popups/insert-image-popup.js +38 -0
- package/dist/components/sheet/sheet-context-menu.js +200 -0
- package/dist/components/sheet/sheet-tabs.js +162 -0
- package/dist/components/sheet/sheet-toolbar.js +106 -0
- package/dist/components/sheet/sheet.js +198 -0
- package/dist/components/sheet/style.d.ts +11 -0
- package/dist/components/sheet/style.js +12 -0
- package/dist/components/sheet/style2.css +654 -0
- package/dist/components/sheet/use-find-replace.js +161 -0
- package/dist/components/sheet/use-formula-ref-select.js +37 -0
- package/dist/components/sheet/use-formula-suggest.js +126 -0
- package/dist/components/sheet/use-sheet-grid.js +188 -0
- package/dist/components/sheet/use-sheet-state.js +122 -0
- package/dist/components/sheet/use-sheet-tabs-bar.js +90 -0
- package/dist/components/sheet/use-tool-groups.js +42 -0
- package/dist/components/sheet/use-tool-popup.js +123 -0
- package/dist/components/sheet/use-toolbar-scroll.js +72 -0
- package/dist/index.d.ts +18 -26
- package/dist/index.js +14 -24
- package/dist/report/binding.d.ts +45 -0
- package/dist/report/binding.js +175 -0
- package/dist/report/connector.d.ts +66 -0
- package/dist/report/connector.js +159 -0
- package/dist/report/export-xlsx.d.ts +19 -0
- package/dist/report/export-xlsx.js +95 -0
- package/dist/report/filter-bar.d.ts +10 -0
- package/dist/report/filter-bar.js +31 -0
- package/dist/report/index.d.ts +12 -0
- package/dist/report/params.d.ts +13 -0
- package/dist/report/params.js +61 -0
- package/dist/report/render/aggregate.js +157 -0
- package/dist/report/render/builder.d.ts +12 -0
- package/dist/report/render/builder.js +139 -0
- package/dist/report/render/coordinate.d.ts +25 -0
- package/dist/report/render/coordinate.js +733 -0
- package/dist/report/render/index.d.ts +14 -0
- package/dist/report/render/index.js +21 -0
- package/dist/report/render/style-resolver.js +50 -0
- package/dist/report/render/template-index.d.ts +24 -0
- package/dist/report/render/template-index.js +137 -0
- package/dist/report/rules/evaluate.d.ts +25 -0
- package/dist/report/rules/evaluate.js +82 -0
- package/dist/report/rules/index.d.ts +2 -0
- package/dist/report/template.d.ts +59 -0
- package/dist/report/template.js +80 -0
- package/dist/report/types.d.ts +85 -0
- 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 +3 -1
- package/dist/types/report-designer.d.ts +36 -0
- package/dist/types/report-viewer.d.ts +38 -0
- package/dist/types/sheet.d.ts +22 -5
- package/package.json +10 -7
- package/src/components/report/__test__/conditional-rules-helpers.test.ts +87 -0
- package/src/components/report/__test__/field-panel-helpers.test.ts +65 -0
- package/src/components/report/__test__/float-panel-position.test.ts +45 -0
- package/src/components/report/__test__/report-designer.test.ts +388 -0
- package/src/components/report/__test__/report-viewer.test.ts +334 -0
- package/src/components/report/__test__/topology.test.ts +143 -0
- package/src/components/report/__test__/use-report-designer.test.ts +565 -0
- package/src/components/report/binding-badge.ts +73 -0
- package/src/components/report/designer/cell-coords.ts +94 -0
- package/src/components/report/designer/col-widths.ts +33 -0
- package/src/components/report/designer/conditional-rules/helpers.ts +186 -0
- package/src/components/report/designer/drop-highlight-overlay.vue +74 -0
- package/src/components/report/designer/float-panel.vue +387 -0
- package/src/components/report/designer/role.ts +32 -0
- package/src/components/report/designer/rule-preview.vue +40 -0
- package/src/components/report/designer/rule-row.vue +252 -0
- package/src/components/report/designer/rules-dialog.vue +126 -0
- package/src/components/report/designer/topology-overlay.vue +127 -0
- package/src/components/report/designer/topology.ts +124 -0
- package/src/components/report/designer/use-grid-overlay.ts +113 -0
- package/src/components/report/designer-hub.vue +198 -0
- package/src/components/report/field-panel-helpers.ts +44 -0
- package/src/components/report/field-panel.vue +136 -0
- package/src/components/report/filter-bar.vue +77 -0
- package/src/components/report/hub-connection-form.vue +124 -0
- package/src/components/report/hub-dataset-editor.vue +369 -0
- package/src/components/report/index.ts +2 -0
- package/src/components/report/report-designer.vue +356 -0
- package/src/components/report/report-viewer.vue +128 -0
- package/src/components/report/style-designer.scss +583 -0
- package/src/components/report/style-hub.scss +292 -0
- package/src/components/report/style-viewer.scss +104 -0
- package/src/components/report/style.scss +3 -0
- package/src/components/report/style.ts +20 -0
- package/src/components/report/use-report-designer.ts +547 -0
- package/src/components/report/use-report-viewer.ts +79 -0
- package/src/components/sheet/__test__/formula-bar.test.ts +474 -0
- package/src/components/sheet/__test__/import-file.test.ts +183 -0
- package/src/components/sheet/__test__/insert-count-menu-item.test.ts +123 -0
- package/src/components/sheet/__test__/insert-image-popup.test.ts +89 -0
- package/src/components/sheet/__test__/insert-image.test.ts +100 -0
- package/src/components/sheet/__test__/sheet-component.test.ts +993 -0
- package/src/components/sheet/__test__/sheet-context-menu.test.ts +206 -0
- package/src/components/sheet/__test__/use-formula-ref-select.test.ts +35 -0
- package/src/components/sheet/__test__/use-formula-suggest.test.ts +83 -0
- package/src/components/sheet/formula-bar.vue +473 -0
- package/src/components/sheet/formula-suggest-list.vue +64 -0
- package/src/components/sheet/import-file.ts +173 -0
- package/src/components/sheet/insert-count-menu-item.vue +69 -0
- package/src/components/sheet/insert-image.ts +90 -0
- package/src/components/sheet/parsing.ts +11 -0
- package/src/components/sheet/popup-helpers.ts +50 -0
- package/src/components/sheet/popups/border-popup.vue +93 -0
- package/src/components/sheet/popups/border-preset-glyph.vue +111 -0
- package/src/components/sheet/popups/export-popup.vue +52 -0
- package/src/components/sheet/popups/fill-color-popup.vue +39 -0
- package/src/components/sheet/popups/find-popup.vue +111 -0
- package/src/components/sheet/popups/font-color-popup.vue +39 -0
- package/src/components/sheet/popups/font-size-popup.vue +52 -0
- package/src/components/sheet/popups/import.worker.ts +66 -0
- package/src/components/sheet/popups/insert-image-popup.vue +39 -0
- package/src/components/sheet/sheet-context-menu.ts +229 -0
- package/src/components/sheet/sheet-tabs.vue +162 -0
- package/src/components/sheet/sheet-toolbar.vue +91 -0
- package/src/components/sheet/sheet.vue +211 -0
- package/src/components/sheet/style.scss +737 -0
- package/src/components/sheet/style.ts +13 -0
- package/src/components/sheet/use-find-replace.ts +200 -0
- package/src/components/sheet/use-formula-ref-select.ts +34 -0
- package/src/components/sheet/use-formula-suggest.ts +150 -0
- package/src/components/sheet/use-sheet-grid.ts +247 -0
- package/src/components/sheet/use-sheet-state.ts +161 -0
- package/src/components/sheet/use-sheet-tabs-bar.ts +115 -0
- package/src/components/sheet/use-tool-groups.ts +61 -0
- package/src/components/sheet/use-tool-popup.ts +150 -0
- package/src/components/sheet/use-toolbar-scroll.ts +95 -0
- package/src/env.d.ts +11 -0
- package/src/index.ts +39 -14
- package/src/report/__test__/binding.test.ts +218 -0
- package/src/report/__test__/connector.test.ts +218 -0
- package/src/report/__test__/export-xlsx.test.ts +118 -0
- package/src/report/__test__/filter-bar.test.ts +58 -0
- package/src/report/__test__/params.test.ts +156 -0
- package/src/report/__test__/render.test.ts +728 -0
- package/src/report/__test__/rules.test.ts +131 -0
- package/src/report/__test__/template.test.ts +355 -0
- package/src/report/binding.ts +243 -0
- package/src/report/connector.ts +205 -0
- package/src/report/export-xlsx.ts +118 -0
- package/src/report/filter-bar.ts +36 -0
- package/src/report/index.ts +13 -0
- package/src/report/params.ts +96 -0
- package/src/report/render/__test__/aggregate.test.ts +16 -0
- package/src/report/render/__test__/coordinate.test.ts +405 -0
- package/src/report/render/aggregate.ts +240 -0
- package/src/report/render/builder.ts +211 -0
- package/src/report/render/coordinate.ts +1239 -0
- package/src/report/render/index.ts +30 -0
- package/src/report/render/style-resolver.ts +68 -0
- package/src/report/render/template-index.ts +200 -0
- package/src/report/rules/evaluate.ts +113 -0
- package/src/report/rules/index.ts +7 -0
- package/src/report/template.ts +141 -0
- package/src/report/types.ts +96 -0
- 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 +2 -0
- package/src/types/report-designer.ts +36 -0
- package/src/types/report-viewer.ts +37 -0
- package/src/types/sheet.ts +26 -3
- 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 +0 -211
- package/dist/vue/sheet.js.map +0 -1
- package/dist/vue/sheet.vue.d.ts +0 -24
- package/dist/vue/style.d.ts +0 -1
- package/dist/vue/style.js +0 -2
- package/dist/vue/style2.css +0 -93
- 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
- package/src/vue/__test__/sheet-component.test.ts +0 -267
- package/src/vue/sheet.vue +0 -235
- package/src/vue/style.scss +0 -94
- package/src/vue/style.ts +0 -2
- /package/src/{vue → components/sheet}/index.ts +0 -0
|
@@ -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
|
-
}
|
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 公式分词器:数字 / 字符串 / 布尔 / 标识符(函数名、引用、表名)/ 带引号表名 / 运算符。
|
|
3
|
-
*
|
|
4
|
-
* 不做语义消歧:`A1`(引用)、`SUM`(函数名)、`Sheet2`(表名)统一产出 ident,
|
|
5
|
-
* 由 parser 依据后随 token(`(` / `!` / `:` / 运算位)判定。
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
/** 解析失败异常(Sheet 层捕获 → 单元格记为 #ERROR!) */
|
|
9
|
-
export class FormulaParseError extends Error {
|
|
10
|
-
constructor(message: string) {
|
|
11
|
-
super(message)
|
|
12
|
-
this.name = 'FormulaParseError'
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export type FormulaOperator =
|
|
17
|
-
| '+'
|
|
18
|
-
| '-'
|
|
19
|
-
| '*'
|
|
20
|
-
| '/'
|
|
21
|
-
| '^'
|
|
22
|
-
| '&'
|
|
23
|
-
| '%'
|
|
24
|
-
| '('
|
|
25
|
-
| ')'
|
|
26
|
-
| ','
|
|
27
|
-
| '!'
|
|
28
|
-
| ':'
|
|
29
|
-
| '='
|
|
30
|
-
| '<>'
|
|
31
|
-
| '<'
|
|
32
|
-
| '<='
|
|
33
|
-
| '>'
|
|
34
|
-
| '>='
|
|
35
|
-
|
|
36
|
-
export type FormulaToken =
|
|
37
|
-
| { type: 'number'; value: number }
|
|
38
|
-
| { type: 'string'; value: string }
|
|
39
|
-
/** 标识符:函数名 / 单元格引用形态 / 裸表名(含 $ 绝对引用写法);TRUE/FALSE 由 parser 归约为布尔 */
|
|
40
|
-
| { type: 'ident'; name: string }
|
|
41
|
-
/** 带单引号的表名('' 转义为字面单引号) */
|
|
42
|
-
| { type: 'quoted-name'; name: string }
|
|
43
|
-
| { type: 'op'; op: FormulaOperator }
|
|
44
|
-
|
|
45
|
-
const NUMBER_RE = /^(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?/
|
|
46
|
-
|
|
47
|
-
/** 标识符首字符:字母 / _ / $ / 非 ASCII(中文表名等) */
|
|
48
|
-
function isIdentStart(ch: string): boolean {
|
|
49
|
-
return /[A-Za-z_$]/.test(ch) || ch.charCodeAt(0) >= 0x80
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** 标识符后续字符:首字符集 + 数字 + . */
|
|
53
|
-
function isIdentPart(ch: string): boolean {
|
|
54
|
-
return /[A-Za-z0-9_.$]/.test(ch) || ch.charCodeAt(0) >= 0x80
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const TWO_CHAR_OPS = ['<>', '<=', '>='] as const
|
|
58
|
-
const ONE_CHAR_OPS = [
|
|
59
|
-
'+',
|
|
60
|
-
'-',
|
|
61
|
-
'*',
|
|
62
|
-
'/',
|
|
63
|
-
'^',
|
|
64
|
-
'&',
|
|
65
|
-
'%',
|
|
66
|
-
'(',
|
|
67
|
-
')',
|
|
68
|
-
',',
|
|
69
|
-
'!',
|
|
70
|
-
':',
|
|
71
|
-
'=',
|
|
72
|
-
'<',
|
|
73
|
-
'>'
|
|
74
|
-
] as const
|
|
75
|
-
|
|
76
|
-
/** 公式文本(不含 '=')→ token 序列;非法输入抛 FormulaParseError */
|
|
77
|
-
export function tokenizeFormula(text: string): FormulaToken[] {
|
|
78
|
-
const tokens: FormulaToken[] = []
|
|
79
|
-
let i = 0
|
|
80
|
-
while (i < text.length) {
|
|
81
|
-
const ch = text[i]!
|
|
82
|
-
// 空白忽略(不支持 Excel 的空格交集运算符)
|
|
83
|
-
if (ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r') {
|
|
84
|
-
i++
|
|
85
|
-
continue
|
|
86
|
-
}
|
|
87
|
-
// 数字
|
|
88
|
-
if ((ch >= '0' && ch <= '9') || (ch === '.' && text[i + 1]! >= '0' && text[i + 1]! <= '9')) {
|
|
89
|
-
const match = NUMBER_RE.exec(text.slice(i))
|
|
90
|
-
if (!match) throw new FormulaParseError(`非法数字(位置 ${i + 1})`)
|
|
91
|
-
tokens.push({ type: 'number', value: Number.parseFloat(match[0]) })
|
|
92
|
-
i += match[0].length
|
|
93
|
-
continue
|
|
94
|
-
}
|
|
95
|
-
// 字符串字面量("...","" 转义)
|
|
96
|
-
if (ch === '"') {
|
|
97
|
-
let value = ''
|
|
98
|
-
let j = i + 1
|
|
99
|
-
for (;;) {
|
|
100
|
-
if (j >= text.length) throw new FormulaParseError('字符串缺少结束引号')
|
|
101
|
-
if (text[j] === '"') {
|
|
102
|
-
if (text[j + 1] === '"') {
|
|
103
|
-
value += '"'
|
|
104
|
-
j += 2
|
|
105
|
-
continue
|
|
106
|
-
}
|
|
107
|
-
break
|
|
108
|
-
}
|
|
109
|
-
value += text[j]
|
|
110
|
-
j++
|
|
111
|
-
}
|
|
112
|
-
tokens.push({ type: 'string', value })
|
|
113
|
-
i = j + 1
|
|
114
|
-
continue
|
|
115
|
-
}
|
|
116
|
-
// 带引号表名('...','' 转义)
|
|
117
|
-
if (ch === "'") {
|
|
118
|
-
let name = ''
|
|
119
|
-
let j = i + 1
|
|
120
|
-
for (;;) {
|
|
121
|
-
if (j >= text.length) throw new FormulaParseError('表名缺少结束引号')
|
|
122
|
-
if (text[j] === "'") {
|
|
123
|
-
if (text[j + 1] === "'") {
|
|
124
|
-
name += "'"
|
|
125
|
-
j += 2
|
|
126
|
-
continue
|
|
127
|
-
}
|
|
128
|
-
break
|
|
129
|
-
}
|
|
130
|
-
name += text[j]
|
|
131
|
-
j++
|
|
132
|
-
}
|
|
133
|
-
tokens.push({ type: 'quoted-name', name })
|
|
134
|
-
i = j + 1
|
|
135
|
-
continue
|
|
136
|
-
}
|
|
137
|
-
// 标识符(函数名 / 引用 / 裸表名)
|
|
138
|
-
if (isIdentStart(ch)) {
|
|
139
|
-
let j = i + 1
|
|
140
|
-
while (j < text.length && isIdentPart(text[j]!)) j++
|
|
141
|
-
tokens.push({ type: 'ident', name: text.slice(i, j) })
|
|
142
|
-
i = j
|
|
143
|
-
continue
|
|
144
|
-
}
|
|
145
|
-
// 双字符运算符优先
|
|
146
|
-
const two = text.slice(i, i + 2)
|
|
147
|
-
if ((TWO_CHAR_OPS as readonly string[]).includes(two)) {
|
|
148
|
-
tokens.push({ type: 'op', op: two as FormulaOperator })
|
|
149
|
-
i += 2
|
|
150
|
-
continue
|
|
151
|
-
}
|
|
152
|
-
if ((ONE_CHAR_OPS as readonly string[]).includes(ch)) {
|
|
153
|
-
tokens.push({ type: 'op', op: ch as FormulaOperator })
|
|
154
|
-
i++
|
|
155
|
-
continue
|
|
156
|
-
}
|
|
157
|
-
throw new FormulaParseError(`无法识别的字符 "${ch}"(位置 ${i + 1})`)
|
|
158
|
-
}
|
|
159
|
-
return tokens
|
|
160
|
-
}
|