@veltra/sheet 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/dist/core/address.d.ts +60 -0
  2. package/dist/core/address.js +142 -0
  3. package/dist/core/address.js.map +1 -0
  4. package/dist/core/cell-store.d.ts +61 -0
  5. package/dist/core/cell-store.js +135 -0
  6. package/dist/core/cell-store.js.map +1 -0
  7. package/dist/core/command/default-registry.d.ts +10 -0
  8. package/dist/core/command/default-registry.js +18 -0
  9. package/dist/core/command/default-registry.js.map +1 -0
  10. package/dist/core/command/history.d.ts +64 -0
  11. package/dist/core/command/history.js +127 -0
  12. package/dist/core/command/history.js.map +1 -0
  13. package/dist/core/command/merge-cells.d.ts +21 -0
  14. package/dist/core/command/merge-cells.js +74 -0
  15. package/dist/core/command/merge-cells.js.map +1 -0
  16. package/dist/core/command/registry.d.ts +17 -0
  17. package/dist/core/command/registry.js +26 -0
  18. package/dist/core/command/registry.js.map +1 -0
  19. package/dist/core/command/set-cell-formula.d.ts +17 -0
  20. package/dist/core/command/set-cell-formula.js +29 -0
  21. package/dist/core/command/set-cell-formula.js.map +1 -0
  22. package/dist/core/command/set-cell-value.d.ts +22 -0
  23. package/dist/core/command/set-cell-value.js +35 -0
  24. package/dist/core/command/set-cell-value.js.map +1 -0
  25. package/dist/core/command/types.d.ts +68 -0
  26. package/dist/core/events.js +29 -0
  27. package/dist/core/events.js.map +1 -0
  28. package/dist/core/formula/ast.d.ts +58 -0
  29. package/dist/core/formula/ast.js +38 -0
  30. package/dist/core/formula/ast.js.map +1 -0
  31. package/dist/core/formula/dependency-graph.d.ts +70 -0
  32. package/dist/core/formula/dependency-graph.js +269 -0
  33. package/dist/core/formula/dependency-graph.js.map +1 -0
  34. package/dist/core/formula/errors.d.ts +25 -0
  35. package/dist/core/formula/errors.js +36 -0
  36. package/dist/core/formula/errors.js.map +1 -0
  37. package/dist/core/formula/evaluator.d.ts +41 -0
  38. package/dist/core/formula/evaluator.js +154 -0
  39. package/dist/core/formula/evaluator.js.map +1 -0
  40. package/dist/core/formula/functions.d.ts +30 -0
  41. package/dist/core/formula/functions.js +209 -0
  42. package/dist/core/formula/functions.js.map +1 -0
  43. package/dist/core/formula/parser.d.ts +7 -0
  44. package/dist/core/formula/parser.js +208 -0
  45. package/dist/core/formula/parser.js.map +1 -0
  46. package/dist/core/formula/tokenizer.d.ts +37 -0
  47. package/dist/core/formula/tokenizer.js +146 -0
  48. package/dist/core/formula/tokenizer.js.map +1 -0
  49. package/dist/core/merge-manager.d.ts +64 -0
  50. package/dist/core/merge-manager.js +111 -0
  51. package/dist/core/merge-manager.js.map +1 -0
  52. package/dist/core/selection.d.ts +31 -0
  53. package/dist/core/selection.js +62 -0
  54. package/dist/core/selection.js.map +1 -0
  55. package/dist/core/sheet.d.ts +105 -0
  56. package/dist/core/sheet.js +202 -0
  57. package/dist/core/sheet.js.map +1 -0
  58. package/dist/core/workbook.d.ts +43 -0
  59. package/dist/core/workbook.js +77 -0
  60. package/dist/core/workbook.js.map +1 -0
  61. package/dist/grid/sheet-grid.d.ts +59 -0
  62. package/dist/grid/sheet-grid.js +230 -0
  63. package/dist/grid/sheet-grid.js.map +1 -0
  64. package/dist/index.d.ts +28 -0
  65. package/dist/index.js +25 -0
  66. package/dist/tools/builtin.js +72 -0
  67. package/dist/tools/builtin.js.map +1 -0
  68. package/dist/tools/context.d.ts +57 -0
  69. package/dist/tools/context.js +43 -0
  70. package/dist/tools/context.js.map +1 -0
  71. package/dist/tools/registry.d.ts +68 -0
  72. package/dist/tools/registry.js +77 -0
  73. package/dist/tools/registry.js.map +1 -0
  74. package/dist/types/index.d.ts +2 -0
  75. package/dist/types/sheet.d.ts +43 -0
  76. package/dist/vue/index.d.ts +2 -0
  77. package/dist/vue/sheet.js +185 -0
  78. package/dist/vue/sheet.js.map +1 -0
  79. package/dist/vue/sheet.vue.d.ts +24 -0
  80. package/dist/vue/style.d.ts +1 -0
  81. package/dist/vue/style.js +1 -0
  82. package/dist/vue/style2.css +93 -0
  83. package/package.json +37 -0
  84. package/src/core/__test__/address.test.ts +113 -0
  85. package/src/core/__test__/cell-store.test.ts +97 -0
  86. package/src/core/__test__/command.test.ts +377 -0
  87. package/src/core/__test__/formula-adversarial.test.ts +265 -0
  88. package/src/core/__test__/formula-engine.test.ts +470 -0
  89. package/src/core/__test__/formula-functions.test.ts +178 -0
  90. package/src/core/__test__/formula-parser.test.ts +160 -0
  91. package/src/core/__test__/history.test.ts +225 -0
  92. package/src/core/__test__/merge-manager.test.ts +106 -0
  93. package/src/core/__test__/sheet.test.ts +142 -0
  94. package/src/core/address.ts +166 -0
  95. package/src/core/cell-store.ts +173 -0
  96. package/src/core/command/default-registry.ts +15 -0
  97. package/src/core/command/history.ts +167 -0
  98. package/src/core/command/index.ts +7 -0
  99. package/src/core/command/merge-cells.ts +74 -0
  100. package/src/core/command/registry.ts +30 -0
  101. package/src/core/command/set-cell-formula.ts +29 -0
  102. package/src/core/command/set-cell-value.ts +38 -0
  103. package/src/core/command/types.ts +73 -0
  104. package/src/core/events.ts +37 -0
  105. package/src/core/formula/ast.ts +66 -0
  106. package/src/core/formula/dependency-graph.ts +329 -0
  107. package/src/core/formula/errors.ts +41 -0
  108. package/src/core/formula/evaluator.ts +219 -0
  109. package/src/core/formula/functions.ts +266 -0
  110. package/src/core/formula/index.ts +7 -0
  111. package/src/core/formula/parser.ts +198 -0
  112. package/src/core/formula/tokenizer.ts +160 -0
  113. package/src/core/merge-manager.ts +158 -0
  114. package/src/core/selection.ts +66 -0
  115. package/src/core/sheet.ts +257 -0
  116. package/src/core/workbook.ts +99 -0
  117. package/src/env.d.ts +7 -0
  118. package/src/grid/__test__/canvas-mock.ts +49 -0
  119. package/src/grid/__test__/setup.ts +3 -0
  120. package/src/grid/__test__/sheet-grid-formula.test.ts +140 -0
  121. package/src/grid/__test__/sheet-grid.test.ts +177 -0
  122. package/src/grid/sheet-grid.ts +283 -0
  123. package/src/index.ts +16 -0
  124. package/src/tools/__test__/builtin.test.ts +128 -0
  125. package/src/tools/__test__/context.test.ts +157 -0
  126. package/src/tools/__test__/registry.test.ts +102 -0
  127. package/src/tools/builtin.ts +75 -0
  128. package/src/tools/context.ts +106 -0
  129. package/src/tools/index.ts +2 -0
  130. package/src/tools/registry.ts +129 -0
  131. package/src/types/index.ts +1 -0
  132. package/src/types/sheet.ts +41 -0
  133. package/src/vue/__test__/sheet-component.test.ts +267 -0
  134. package/src/vue/index.ts +1 -0
  135. package/src/vue/sheet.vue +195 -0
  136. package/src/vue/style.scss +94 -0
  137. package/src/vue/style.ts +1 -0
@@ -0,0 +1,470 @@
1
+ import { describe, expect, it, vi } from 'vitest'
2
+
3
+ import { parseRange, type CellAddress } from '../address'
4
+ import { Sheet } from '../sheet'
5
+ import { Workbook } from '../workbook'
6
+
7
+ const A1 = { row: 0, col: 0 }
8
+ const B1 = { row: 0, col: 1 }
9
+ const C1 = { row: 0, col: 2 }
10
+ const D1 = { row: 0, col: 3 }
11
+
12
+ describe('依赖重算', () => {
13
+ it('改 A1 → 依赖它的 =A1*2 重算', () => {
14
+ const sheet = new Sheet()
15
+ sheet.setCellValue(A1, 3)
16
+ sheet.setCellFormula(B1, '=A1*2')
17
+ expect(sheet.getCellData(B1)).toMatchObject({ v: 6 })
18
+
19
+ sheet.setCellValue(A1, 10)
20
+ expect(sheet.getCellData(B1)).toMatchObject({ v: 20 })
21
+ })
22
+
23
+ it('间接依赖按拓扑序重算:A1 → B1(=A1*2) → C1(=B1+1)', () => {
24
+ const sheet = new Sheet()
25
+ sheet.setCellValue(A1, 1)
26
+ sheet.setCellFormula(B1, '=A1*2')
27
+ sheet.setCellFormula(C1, '=B1+1')
28
+ expect(sheet.getCellData(C1)).toMatchObject({ v: 3 })
29
+
30
+ sheet.setCellValue(A1, 5)
31
+ // 若顺序错误(C1 先于 B1 重算)会拿到旧 B1 的 2+1=3
32
+ expect(sheet.getCellData(B1)).toMatchObject({ v: 10 })
33
+ expect(sheet.getCellData(C1)).toMatchObject({ v: 11 })
34
+ })
35
+
36
+ it('菱形依赖(D1=B1+C1,B1/C1 同依赖 A1)只算一次且结果正确', () => {
37
+ const sheet = new Sheet()
38
+ sheet.setCellValue(A1, 2)
39
+ sheet.setCellFormula(B1, '=A1*2')
40
+ sheet.setCellFormula(C1, '=A1*3')
41
+ sheet.setCellFormula(D1, '=B1+C1')
42
+ expect(sheet.getCellData(D1)).toMatchObject({ v: 10 })
43
+
44
+ sheet.setCellValue(A1, 4)
45
+ expect(sheet.getCellData(D1)).toMatchObject({ v: 20 })
46
+ })
47
+
48
+ it('连续改值,每次重算结果一致', () => {
49
+ const sheet = new Sheet()
50
+ sheet.setCellFormula(B1, '=A1*2')
51
+ sheet.setCellFormula(C1, '=B1+1')
52
+ for (const [input, b, c] of [
53
+ [1, 2, 3],
54
+ [2, 4, 5],
55
+ [3, 6, 7],
56
+ [2, 4, 5],
57
+ [0, 0, 1]
58
+ ] as const) {
59
+ sheet.setCellValue(A1, input)
60
+ expect(sheet.getCellData(B1)).toMatchObject({ v: b })
61
+ expect(sheet.getCellData(C1)).toMatchObject({ v: c })
62
+ }
63
+ })
64
+
65
+ it('重算收敛时值未变的格不产生派生变更(不触发 cell-change)', () => {
66
+ const sheet = new Sheet()
67
+ sheet.setCellFormula(B1, '=A1*0+1')
68
+ expect(sheet.getCellData(B1)).toMatchObject({ v: 1 })
69
+
70
+ const handler = vi.fn()
71
+ sheet.on('cell-change', handler)
72
+ sheet.setCellValue(A1, 99)
73
+ // 只有 A1 自身变更;B1 收敛(恒为 1)不应有事件
74
+ expect(handler).toHaveBeenCalledTimes(1)
75
+ expect(handler).toHaveBeenCalledWith({ addr: A1 })
76
+ })
77
+ })
78
+
79
+ describe('循环引用', () => {
80
+ it('A1==B1+1, B1==A1+1 → 双格 #CYCLE!;打破循环自动恢复', () => {
81
+ const sheet = new Sheet()
82
+ sheet.setCellFormula(A1, '=B1+1')
83
+ sheet.setCellFormula(B1, '=A1+1')
84
+ expect(sheet.getCellData(A1)).toMatchObject({ v: '#CYCLE!', t: 'e' })
85
+ expect(sheet.getCellData(B1)).toMatchObject({ v: '#CYCLE!', t: 'e' })
86
+
87
+ // 打破循环:B1 改为常量 → A1 恢复
88
+ sheet.setCellValue(B1, 5)
89
+ expect(sheet.getCellData(A1)).toMatchObject({ v: 6, t: 'n' })
90
+ expect(sheet.getCellData(B1)).toMatchObject({ v: 5 })
91
+ })
92
+
93
+ it('环外格引用环上格 → 错误传播(自身不入环)', () => {
94
+ const sheet = new Sheet()
95
+ sheet.setCellFormula(A1, '=B1+1')
96
+ sheet.setCellFormula(B1, '=A1+1')
97
+ sheet.setCellFormula(C1, '=A1*2')
98
+ expect(sheet.getCellData(C1)).toMatchObject({ v: '#CYCLE!', t: 'e' })
99
+
100
+ // 打破循环后环外格一并恢复
101
+ sheet.setCellValue(B1, 2)
102
+ expect(sheet.getCellData(A1)).toMatchObject({ v: 3 })
103
+ expect(sheet.getCellData(C1)).toMatchObject({ v: 6 })
104
+ })
105
+
106
+ it('自引用 A1==A1+1 → #CYCLE!,改常量后恢复', () => {
107
+ const sheet = new Sheet()
108
+ sheet.setCellFormula(A1, '=A1+1')
109
+ expect(sheet.getCellData(A1)).toMatchObject({ v: '#CYCLE!', t: 'e' })
110
+
111
+ sheet.setCellValue(A1, 10)
112
+ expect(sheet.getCellData(A1)).toMatchObject({ v: 10 })
113
+ })
114
+
115
+ it('跨表循环:Sheet1!A1 ↔ Sheet2!B1 → 双格 #CYCLE!,打破后恢复', () => {
116
+ const workbook = new Workbook()
117
+ const sheet1 = workbook.activeSheet
118
+ const sheet2 = workbook.addSheet('Sheet2')
119
+ sheet1.setCellFormula(A1, '=Sheet2!B1+1')
120
+ sheet2.setCellFormula(B1, '=Sheet1!A1+1')
121
+ expect(sheet1.getCellData(A1)).toMatchObject({ v: '#CYCLE!', t: 'e' })
122
+ expect(sheet2.getCellData(B1)).toMatchObject({ v: '#CYCLE!', t: 'e' })
123
+
124
+ sheet2.setCellValue(B1, 3)
125
+ expect(sheet1.getCellData(A1)).toMatchObject({ v: 4 })
126
+ })
127
+
128
+ it('循环期间其它无关变更不影响环状态', () => {
129
+ const sheet = new Sheet()
130
+ sheet.setCellFormula(A1, '=B1+1')
131
+ sheet.setCellFormula(B1, '=A1+D1')
132
+ sheet.setCellFormula(C1, '=A1+1')
133
+ // A1 → B1 → A1 成环(D1 是环上引用);C1 环外
134
+ expect(sheet.getCellData(A1)).toMatchObject({ v: '#CYCLE!' })
135
+ expect(sheet.getCellData(B1)).toMatchObject({ v: '#CYCLE!' })
136
+ expect(sheet.getCellData(C1)).toMatchObject({ v: '#CYCLE!' })
137
+
138
+ // 改 D1(环的输入)→ 重算后仍是环
139
+ sheet.setCellValue(D1, 10)
140
+ expect(sheet.getCellData(A1)).toMatchObject({ v: '#CYCLE!' })
141
+ expect(sheet.getCellData(B1)).toMatchObject({ v: '#CYCLE!' })
142
+ })
143
+ })
144
+
145
+ describe('跨表引用', () => {
146
+ it('Sheet2 值变更 → Sheet1 引用格实时重算;undo 一并还原', () => {
147
+ const workbook = new Workbook()
148
+ const sheet1 = workbook.activeSheet
149
+ const sheet2 = workbook.addSheet('Sheet2')
150
+ sheet2.setCellValue(A1, 10)
151
+ sheet1.setCellFormula(B1, '=Sheet2!A1*2')
152
+ expect(sheet1.getCellData(B1)).toMatchObject({ v: 20 })
153
+
154
+ sheet2.setCellValue(A1, 7)
155
+ expect(sheet1.getCellData(B1)).toMatchObject({ v: 14 })
156
+
157
+ // 变更发生在 Sheet2,单次 undo 同时还原 Sheet2 源格与 Sheet1 派生格
158
+ expect(sheet2.undo()).toBe(true)
159
+ expect(sheet2.getCellData(A1)).toMatchObject({ v: 10 })
160
+ expect(sheet1.getCellData(B1)).toMatchObject({ v: 20 })
161
+ })
162
+
163
+ it('引用不存在的 sheet → #REF!', () => {
164
+ const sheet = new Sheet()
165
+ sheet.setCellFormula(A1, '=Nope!B2')
166
+ expect(sheet.getCellData(A1)).toMatchObject({ v: '#REF!', t: 'e' })
167
+ sheet.setCellFormula(B1, "=SUM('No Sheet'!A1:B2)")
168
+ expect(sheet.getCellData(B1)).toMatchObject({ v: '#REF!', t: 'e' })
169
+ })
170
+
171
+ it("跨表区域:'S2'!A1:B2 聚合", () => {
172
+ const workbook = new Workbook()
173
+ const sheet1 = workbook.activeSheet
174
+ const s2 = workbook.addSheet('S2')
175
+ s2.setCellValue(A1, 1)
176
+ s2.setCellValue({ row: 1, col: 1 }, 2)
177
+ sheet1.setCellFormula(A1, "=SUM('S2'!A1:B2)")
178
+ expect(sheet1.getCellData(A1)).toMatchObject({ v: 3 })
179
+
180
+ // 区域内空格变更也触发重算
181
+ s2.setCellValue({ row: 0, col: 1 }, 10)
182
+ expect(sheet1.getCellData(A1)).toMatchObject({ v: 13 })
183
+ })
184
+
185
+ it('删除被引用的 sheet:缓存仍旧值,下次重算 → #REF!(已知限制行为)', () => {
186
+ const workbook = new Workbook()
187
+ const sheet1 = workbook.activeSheet
188
+ const sheet2 = workbook.addSheet('Sheet2')
189
+ sheet2.setCellValue(A1, 10)
190
+ sheet1.setCellFormula(B1, '=Sheet2!A1+D1')
191
+ sheet1.setCellValue(D1, 1)
192
+ expect(sheet1.getCellData(B1)).toMatchObject({ v: 11 })
193
+
194
+ workbook.removeSheet('Sheet2')
195
+ expect(workbook.formulaGraph.getSheet('Sheet2')).toBeUndefined()
196
+ // 删除本身不触发重算(已知限制):缓存仍旧值
197
+ expect(sheet1.getCellData(B1)).toMatchObject({ v: 11 })
198
+ // 任意触发源变更 → 重算时表已不存在 → #REF!
199
+ sheet1.setCellValue(D1, 2)
200
+ expect(sheet1.getCellData(B1)).toMatchObject({ v: '#REF!', t: 'e' })
201
+ })
202
+ })
203
+
204
+ describe('错误值体系', () => {
205
+ it('三类错误:除零 #DIV/0!、类型错误 #VALUE!、未知函数 #NAME?', () => {
206
+ const sheet = new Sheet()
207
+ sheet.setCellFormula(A1, '=1/0')
208
+ expect(sheet.getCellData(A1)).toMatchObject({ v: '#DIV/0!', t: 'e' })
209
+ sheet.setCellFormula(B1, '=1+"abc"')
210
+ expect(sheet.getCellData(B1)).toMatchObject({ v: '#VALUE!', t: 'e' })
211
+ sheet.setCellFormula(C1, '=NOSUCHFN(1)')
212
+ expect(sheet.getCellData(C1)).toMatchObject({ v: '#NAME?', t: 'e' })
213
+ })
214
+
215
+ it('0 作除数与 0 的负幂 → #DIV/0!;非法幂 → #VALUE!', () => {
216
+ const sheet = new Sheet()
217
+ sheet.setCellFormula(A1, '=0/0')
218
+ expect(sheet.getCellData(A1)).toMatchObject({ v: '#DIV/0!' })
219
+ sheet.setCellFormula(B1, '=0^-1')
220
+ expect(sheet.getCellData(B1)).toMatchObject({ v: '#DIV/0!' })
221
+ sheet.setCellFormula(C1, '=(-1)^0.5')
222
+ expect(sheet.getCellData(C1)).toMatchObject({ v: '#VALUE!' })
223
+ })
224
+
225
+ it('错误沿依赖链传播,源修复后恢复', () => {
226
+ const sheet = new Sheet()
227
+ sheet.setCellFormula(A1, '=1/0')
228
+ sheet.setCellFormula(B1, '=A1*2')
229
+ sheet.setCellFormula(C1, '=B1+1')
230
+ expect(sheet.getCellData(B1)).toMatchObject({ v: '#DIV/0!' })
231
+ expect(sheet.getCellData(C1)).toMatchObject({ v: '#DIV/0!' })
232
+
233
+ sheet.setCellValue(A1, 5)
234
+ expect(sheet.getCellData(B1)).toMatchObject({ v: 10 })
235
+ expect(sheet.getCellData(C1)).toMatchObject({ v: 11 })
236
+ })
237
+
238
+ it('区域直接作为公式结果 → #VALUE!', () => {
239
+ const sheet = new Sheet()
240
+ sheet.setCellValue(A1, 1)
241
+ sheet.setCellFormula(B1, '=A1:A3')
242
+ expect(sheet.getCellData(B1)).toMatchObject({ v: '#VALUE!', t: 'e' })
243
+ })
244
+ })
245
+
246
+ describe('undo/redo 集成', () => {
247
+ it('改值波及 10 格公式链 → 单次 undo 全部还原', () => {
248
+ const sheet = new Sheet()
249
+ sheet.setCellValue(A1, 1)
250
+ // B1..K1 共 10 格链:B1=A1+1, C1=B1+1, …
251
+ const chain: CellAddress[] = []
252
+ for (let col = 1; col <= 10; col++) {
253
+ const prev = { row: 0, col: col - 1 }
254
+ const addr = { row: 0, col }
255
+ sheet.setCellFormula(addr, `=${String.fromCharCode(64 + col)}1+1`)
256
+ chain.push(addr)
257
+ }
258
+ expect(sheet.getCellData(chain[9]!)).toMatchObject({ v: 11 })
259
+ sheet.history.clear()
260
+
261
+ sheet.setCellValue(A1, 100)
262
+ expect(sheet.getCellData(chain[9]!)).toMatchObject({ v: 110 })
263
+ // 源变更 + 10 格派生 = 一个 undo 单元
264
+ expect(sheet.history.undoSize).toBe(1)
265
+
266
+ expect(sheet.undo()).toBe(true)
267
+ expect(sheet.getCellData(A1)).toMatchObject({ v: 1 })
268
+ for (const [i, addr] of chain.entries()) {
269
+ expect(sheet.getCellData(addr)).toMatchObject({ v: i + 2 })
270
+ }
271
+
272
+ expect(sheet.redo()).toBe(true)
273
+ expect(sheet.getCellData(chain[9]!)).toMatchObject({ v: 110 })
274
+ })
275
+
276
+ it('undo 公式输入:公式、缓存与波及变更一步还原', () => {
277
+ const sheet = new Sheet()
278
+ sheet.setCellValue(A1, 5)
279
+ sheet.setCellFormula(B1, '=A1*2')
280
+ sheet.setCellFormula(C1, '=B1+1')
281
+ sheet.history.clear()
282
+
283
+ sheet.setCellFormula(D1, '=C1*10')
284
+ expect(sheet.getCellData(D1)).toMatchObject({ v: 110 })
285
+
286
+ sheet.undo()
287
+ expect(sheet.getCellData(D1)).toBeUndefined()
288
+
289
+ sheet.redo()
290
+ expect(sheet.getCellData(D1)).toMatchObject({ f: 'C1*10', v: 110 })
291
+ })
292
+
293
+ it('undo 公式删除 → 依赖图复原,再次改值照常重算', () => {
294
+ const sheet = new Sheet()
295
+ sheet.setCellValue(A1, 5)
296
+ sheet.setCellFormula(B1, '=A1*2')
297
+ sheet.setCellFormula(C1, '=B1+1')
298
+ const nodesBefore = sheet.formulaGraph.nodeCount
299
+
300
+ // 删除 B1 的公式(写常量覆盖)→ C1 按 B1 新值重算
301
+ sheet.setCellValue(B1, 0)
302
+ expect(sheet.getCellData(B1)).toMatchObject({ v: 0 })
303
+ expect(sheet.getCellData(C1)).toMatchObject({ v: 1 })
304
+ expect(sheet.formulaGraph.nodeCount).toBe(nodesBefore - 1)
305
+
306
+ // undo 删除 → 公式、缓存、派生值、图节点全部复原
307
+ sheet.undo()
308
+ expect(sheet.getCellData(B1)).toMatchObject({ f: 'A1*2', v: 10 })
309
+ expect(sheet.getCellData(C1)).toMatchObject({ v: 11 })
310
+ expect(sheet.formulaGraph.nodeCount).toBe(nodesBefore)
311
+
312
+ // 图确实复原:再改 A1 仍触发 B1/C1 重算
313
+ sheet.setCellValue(A1, 7)
314
+ expect(sheet.getCellData(B1)).toMatchObject({ v: 14 })
315
+ expect(sheet.getCellData(C1)).toMatchObject({ v: 15 })
316
+ })
317
+ })
318
+
319
+ describe('公式与合并单元格(对抗场景)', () => {
320
+ it('引用合并格:锚点有值、被覆盖格按空(原始存储语义)', () => {
321
+ const sheet = new Sheet()
322
+ sheet.setCellValue({ row: 1, col: 1 }, 10) // B2
323
+ sheet.mergeCells(parseRange('B2:C3')!)
324
+
325
+ sheet.setCellFormula(D1, '=B2')
326
+ expect(sheet.getCellData(D1)).toMatchObject({ v: 10 })
327
+ // C3 是被覆盖格 → 空 → 0
328
+ sheet.setCellFormula({ row: 0, col: 4 }, '=C3+1')
329
+ expect(sheet.getCellData({ row: 0, col: 4 })).toMatchObject({ v: 1 })
330
+ // 区域含合并:只锚点计数一次
331
+ sheet.setCellFormula({ row: 0, col: 5 }, '=SUM(B2:C3)')
332
+ expect(sheet.getCellData({ row: 0, col: 5 })).toMatchObject({ v: 10 })
333
+ })
334
+
335
+ it('公式格作为合并锚点:合并后公式保留且继续重算', () => {
336
+ const sheet = new Sheet()
337
+ sheet.setCellValue(A1, 5)
338
+ sheet.setCellFormula({ row: 1, col: 1 }, '=A1+1') // B2
339
+ sheet.mergeCells(parseRange('B2:C3')!)
340
+
341
+ expect(sheet.getCellData({ row: 1, col: 1 })).toMatchObject({ f: 'A1+1', v: 6 })
342
+ sheet.setCellValue(A1, 7)
343
+ expect(sheet.getCellData({ row: 1, col: 1 })).toMatchObject({ v: 8 })
344
+ })
345
+
346
+ it('被覆盖格上的公式被合并清除:节点移除 + 依赖者重算', () => {
347
+ const sheet = new Sheet()
348
+ sheet.setCellValue({ row: 1, col: 1 }, 10) // B2 锚点
349
+ sheet.setCellFormula({ row: 2, col: 2 }, '=B2+1') // C3 将被覆盖
350
+ sheet.setCellFormula(D1, '=C3*2')
351
+ expect(sheet.getCellData(D1)).toMatchObject({ v: 22 })
352
+ const nodesBefore = sheet.formulaGraph.nodeCount
353
+
354
+ sheet.mergeCells(parseRange('B2:C3')!)
355
+ // C3 数据被清空 → 公式节点移除;D1 按 C3 空值重算
356
+ expect(sheet.getCellData({ row: 2, col: 2 })).toBeUndefined()
357
+ expect(sheet.formulaGraph.nodeCount).toBe(nodesBefore - 1)
358
+ expect(sheet.getCellData(D1)).toMatchObject({ v: 0 })
359
+
360
+ // undo 合并 → C3 公式恢复,D1 回到 22
361
+ sheet.undo()
362
+ expect(sheet.getCellData({ row: 2, col: 2 })).toMatchObject({ f: 'B2+1', v: 11 })
363
+ expect(sheet.getCellData(D1)).toMatchObject({ v: 22 })
364
+ })
365
+ })
366
+
367
+ describe('公式生命周期(对抗场景)', () => {
368
+ it('删除公式后依赖图清理:原依赖格改动不再触发重算', () => {
369
+ const sheet = new Sheet()
370
+ sheet.setCellValue(A1, 5)
371
+ sheet.setCellFormula(B1, '=A1*2')
372
+ sheet.setCellFormula(C1, '=B1+1')
373
+ expect(sheet.formulaGraph.nodeCount).toBe(2)
374
+
375
+ // 清空 B1(删除公式)→ C1 按空值重算
376
+ sheet.setCellValue(B1, null)
377
+ expect(sheet.getCellData(B1)).toBeUndefined()
378
+ expect(sheet.getCellData(C1)).toMatchObject({ v: 1 })
379
+ expect(sheet.formulaGraph.nodeCount).toBe(1)
380
+
381
+ // A1 改动不再有任何依赖者
382
+ const handler = vi.fn()
383
+ sheet.on('cell-change', handler)
384
+ sheet.setCellValue(A1, 99)
385
+ expect(handler).toHaveBeenCalledTimes(1)
386
+ expect(sheet.getCellData(C1)).toMatchObject({ v: 1 })
387
+ })
388
+
389
+ it('公式改公式:依赖边切换(旧依赖不再触发,新依赖生效)', () => {
390
+ const sheet = new Sheet()
391
+ sheet.setCellValue(A1, 1)
392
+ sheet.setCellValue(B1, 10)
393
+ sheet.setCellFormula(C1, '=A1+1')
394
+ expect(sheet.getCellData(C1)).toMatchObject({ v: 2 })
395
+
396
+ sheet.setCellFormula(C1, '=B1+1')
397
+ expect(sheet.getCellData(C1)).toMatchObject({ v: 11 })
398
+
399
+ // 旧依赖 A1 改动不再影响 C1
400
+ sheet.setCellValue(A1, 100)
401
+ expect(sheet.getCellData(C1)).toMatchObject({ v: 11 })
402
+ // 新依赖 B1 生效
403
+ sheet.setCellValue(B1, 20)
404
+ expect(sheet.getCellData(C1)).toMatchObject({ v: 21 })
405
+ })
406
+
407
+ it('同公式重复输入 = 无操作(不入历史)', () => {
408
+ const sheet = new Sheet()
409
+ sheet.setCellValue(A1, 1)
410
+ sheet.setCellFormula(B1, '=A1*2')
411
+ const depth = sheet.history.undoSize
412
+
413
+ sheet.setCellFormula(B1, '=A1*2')
414
+ expect(sheet.history.undoSize).toBe(depth)
415
+ expect(sheet.getCellData(B1)).toMatchObject({ v: 2 })
416
+ })
417
+
418
+ it("'=' 单独输入 = 清除单元格", () => {
419
+ const sheet = new Sheet()
420
+ sheet.setCellValue(A1, 5)
421
+ sheet.setCellValue(A1, '=')
422
+ expect(sheet.getCellData(A1)).toBeUndefined()
423
+ // 空格输入 '=' 也是无操作
424
+ expect(sheet.canUndo).toBe(true) // 仍有上面两条历史
425
+ sheet.setCellValue(B1, '=')
426
+ expect(sheet.getCellData(B1)).toBeUndefined()
427
+ })
428
+
429
+ it('A1:A1 单格区域', () => {
430
+ const sheet = new Sheet()
431
+ sheet.setCellValue(A1, 42)
432
+ sheet.setCellFormula(B1, '=SUM(A1:A1)')
433
+ expect(sheet.getCellData(B1)).toMatchObject({ v: 42 })
434
+ })
435
+
436
+ it('公式引用空区域 → SUM 为 0、COUNT 为 0、AVERAGE #DIV/0!', () => {
437
+ const sheet = new Sheet()
438
+ sheet.setCellFormula(A1, '=SUM(D1:E9)')
439
+ sheet.setCellFormula(B1, '=COUNT(D1:E9)')
440
+ sheet.setCellFormula(C1, '=AVERAGE(D1:E9)')
441
+ expect(sheet.getCellData(A1)).toMatchObject({ v: 0 })
442
+ expect(sheet.getCellData(B1)).toMatchObject({ v: 0 })
443
+ expect(sheet.getCellData(C1)).toMatchObject({ v: '#DIV/0!' })
444
+ })
445
+
446
+ it('批量写入(粘贴场景):重算派生并入同一 undo 单元', () => {
447
+ const sheet = new Sheet()
448
+ sheet.setCellFormula(C1, '=A1+B1')
449
+ sheet.history.clear()
450
+
451
+ sheet.setCells([
452
+ { addr: A1, data: { v: 1, t: 'n' } },
453
+ { addr: B1, data: { v: 2, t: 'n' } }
454
+ ])
455
+ expect(sheet.getCellData(C1)).toMatchObject({ v: 3 })
456
+ expect(sheet.history.undoSize).toBe(1)
457
+
458
+ sheet.undo()
459
+ expect(sheet.getCellData(A1)).toBeUndefined()
460
+ expect(sheet.getCellData(B1)).toBeUndefined()
461
+ expect(sheet.getCellData(C1)).toMatchObject({ v: 0 })
462
+ })
463
+
464
+ it('setCell 直接写带 f 的 CellData 同样走重算', () => {
465
+ const sheet = new Sheet()
466
+ sheet.setCellValue(A1, 6)
467
+ sheet.setCell(B1, { f: 'A1*2' })
468
+ expect(sheet.getCellData(B1)).toMatchObject({ f: 'A1*2', v: 12, t: 'n' })
469
+ })
470
+ })
@@ -0,0 +1,178 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import type { CellAddress } from '../address'
4
+ import { Sheet } from '../sheet'
5
+
6
+ const A1 = { row: 0, col: 0 }
7
+
8
+ /** 写入公式并读回计算缓存值 */
9
+ function calcValue(sheet: Sheet, formula: string, addr: CellAddress = { row: 20, col: 20 }) {
10
+ sheet.setCellFormula(addr, formula)
11
+ return sheet.getCellData(addr)
12
+ }
13
+
14
+ describe('函数集:SUM / AVERAGE / MAX / MIN', () => {
15
+ it('SUM:直接参数求和、数字文本强转、非法文本 #VALUE!', () => {
16
+ const sheet = new Sheet()
17
+ expect(calcValue(sheet, '=SUM(1,2,3)')).toMatchObject({ v: 6 })
18
+ expect(calcValue(sheet, '=SUM("5",3)')).toMatchObject({ v: 8 })
19
+ expect(calcValue(sheet, '=SUM("abc")')).toMatchObject({ v: '#VALUE!', t: 'e' })
20
+ })
21
+
22
+ it('SUM:区域内文本/布尔忽略,空格按 0', () => {
23
+ const sheet = new Sheet()
24
+ sheet.setCellValue(A1, 2)
25
+ sheet.setCellValue({ row: 1, col: 0 }, 'text')
26
+ sheet.setCellValue({ row: 2, col: 0 }, true)
27
+ expect(calcValue(sheet, '=SUM(A1:A5)')).toMatchObject({ v: 2 })
28
+ // 全空区域 → 0
29
+ expect(calcValue(sheet, '=SUM(C1:C9)')).toMatchObject({ v: 0 })
30
+ })
31
+
32
+ it('AVERAGE:均值;无数字 → #DIV/0!', () => {
33
+ const sheet = new Sheet()
34
+ expect(calcValue(sheet, '=AVERAGE(2,4)')).toMatchObject({ v: 3 })
35
+ expect(calcValue(sheet, '=AVERAGE(C1:C9)')).toMatchObject({ v: '#DIV/0!', t: 'e' })
36
+ sheet.setCellValue(A1, 2)
37
+ sheet.setCellValue({ row: 1, col: 0 }, 'text')
38
+ // 区域内文本忽略,只按数字格求均值
39
+ expect(calcValue(sheet, '=AVERAGE(A1:A2)')).toMatchObject({ v: 2 })
40
+ })
41
+
42
+ it('MAX / MIN:极值;无数字 → 0', () => {
43
+ const sheet = new Sheet()
44
+ expect(calcValue(sheet, '=MAX(1,9,3)')).toMatchObject({ v: 9 })
45
+ expect(calcValue(sheet, '=MIN(1,-9,3)')).toMatchObject({ v: -9 })
46
+ expect(calcValue(sheet, '=MAX(C1:C9)')).toMatchObject({ v: 0 })
47
+ expect(calcValue(sheet, '=MIN(C1:C9)')).toMatchObject({ v: 0 })
48
+ })
49
+ })
50
+
51
+ describe('函数集:COUNT / COUNTA', () => {
52
+ it('COUNT 只数数字(区域内文本/空格不计;直接参数可强转即计)', () => {
53
+ const sheet = new Sheet()
54
+ sheet.setCellValue(A1, 1)
55
+ sheet.setCellValue({ row: 1, col: 0 }, 'text')
56
+ sheet.setCellValue({ row: 2, col: 0 }, true)
57
+ expect(calcValue(sheet, '=COUNT(A1:A5)')).toMatchObject({ v: 1 })
58
+ expect(calcValue(sheet, '=COUNT(1,"x","5",TRUE)')).toMatchObject({ v: 3 })
59
+ })
60
+
61
+ it('COUNTA 数非空(文本/布尔/空串结果都计,空格不计)', () => {
62
+ const sheet = new Sheet()
63
+ sheet.setCellValue(A1, 1)
64
+ sheet.setCellValue({ row: 1, col: 0 }, 'text')
65
+ sheet.setCellValue({ row: 2, col: 0 }, true)
66
+ expect(calcValue(sheet, '=COUNTA(A1:A5)')).toMatchObject({ v: 3 })
67
+ })
68
+ })
69
+
70
+ describe('函数集:IF / AND / OR / NOT', () => {
71
+ it('IF:分支选择;缺省 else → FALSE;条件非法 → #VALUE!', () => {
72
+ const sheet = new Sheet()
73
+ expect(calcValue(sheet, '=IF(1>0,"y","n")')).toMatchObject({ v: 'y' })
74
+ expect(calcValue(sheet, '=IF(0,1,2)')).toMatchObject({ v: 2 })
75
+ expect(calcValue(sheet, '=IF(1=1,7)')).toMatchObject({ v: 7 })
76
+ expect(calcValue(sheet, '=IF(0,7)')).toMatchObject({ v: false, t: 'b' })
77
+ expect(calcValue(sheet, '=IF("abc",1,2)')).toMatchObject({ v: '#VALUE!', t: 'e' })
78
+ })
79
+
80
+ it('IF 惰性求值:未选分支的除零不产生错误', () => {
81
+ const sheet = new Sheet()
82
+ expect(calcValue(sheet, '=IF(FALSE,1/0,2)')).toMatchObject({ v: 2 })
83
+ expect(calcValue(sheet, '=IF(TRUE,1,1/0)')).toMatchObject({ v: 1 })
84
+ // 条件本身是错误则传播
85
+ expect(calcValue(sheet, '=IF(1/0,1,2)')).toMatchObject({ v: '#DIV/0!', t: 'e' })
86
+ })
87
+
88
+ it('AND / OR / NOT:布尔逻辑与强转', () => {
89
+ const sheet = new Sheet()
90
+ expect(calcValue(sheet, '=AND(1,TRUE)')).toMatchObject({ v: true })
91
+ expect(calcValue(sheet, '=AND(1,0)')).toMatchObject({ v: false })
92
+ expect(calcValue(sheet, '=OR(0,FALSE)')).toMatchObject({ v: false })
93
+ expect(calcValue(sheet, '=OR(0,1)')).toMatchObject({ v: true })
94
+ expect(calcValue(sheet, '=NOT(0)')).toMatchObject({ v: true })
95
+ expect(calcValue(sheet, '=NOT(TRUE)')).toMatchObject({ v: false })
96
+ // 无有效操作数 / 非法文本 → #VALUE!
97
+ expect(calcValue(sheet, '=AND(C1:C9)')).toMatchObject({ v: '#VALUE!', t: 'e' })
98
+ expect(calcValue(sheet, '=OR("x")')).toMatchObject({ v: '#VALUE!', t: 'e' })
99
+ expect(calcValue(sheet, '=NOT("x")')).toMatchObject({ v: '#VALUE!', t: 'e' })
100
+ // 区域内只取布尔格
101
+ sheet.setCellValue(A1, true)
102
+ sheet.setCellValue({ row: 1, col: 0 }, 5)
103
+ expect(calcValue(sheet, '=AND(A1:A2)')).toMatchObject({ v: true })
104
+ })
105
+ })
106
+
107
+ describe('函数集:ROUND / ABS / CONCATENATE', () => {
108
+ it('ROUND:精度、负数位数、远离零取整', () => {
109
+ const sheet = new Sheet()
110
+ expect(calcValue(sheet, '=ROUND(3.14159,2)')).toMatchObject({ v: 3.14 })
111
+ expect(calcValue(sheet, '=ROUND(2.5,0)')).toMatchObject({ v: 3 })
112
+ expect(calcValue(sheet, '=ROUND(-2.5,0)')).toMatchObject({ v: -3 })
113
+ expect(calcValue(sheet, '=ROUND(1234.567,-2)')).toMatchObject({ v: 1200 })
114
+ // 浮点表示误差补偿(2.675 实际存储为 2.67499…)
115
+ expect(calcValue(sheet, '=ROUND(2.675,2)')).toMatchObject({ v: 2.68 })
116
+ })
117
+
118
+ it('ABS:绝对值;非法参数 → #VALUE!', () => {
119
+ const sheet = new Sheet()
120
+ expect(calcValue(sheet, '=ABS(-3)')).toMatchObject({ v: 3 })
121
+ expect(calcValue(sheet, '=ABS("x")')).toMatchObject({ v: '#VALUE!', t: 'e' })
122
+ })
123
+
124
+ it('CONCATENATE:文本拼接(数字/布尔强转,区域展开,空格按空串)', () => {
125
+ const sheet = new Sheet()
126
+ expect(calcValue(sheet, '=CONCATENATE("a",1,TRUE)')).toMatchObject({ v: 'a1TRUE' })
127
+ sheet.setCellValue(A1, 'x')
128
+ sheet.setCellValue({ row: 2, col: 0 }, 'z')
129
+ // A2 为空格 → 空串
130
+ expect(calcValue(sheet, '=CONCATENATE(A1:A3)')).toMatchObject({ v: 'xz' })
131
+ })
132
+ })
133
+
134
+ describe('函数集:通用语义', () => {
135
+ it('嵌套函数与多层括号', () => {
136
+ const sheet = new Sheet()
137
+ sheet.setCellValue(A1, 1)
138
+ sheet.setCellValue({ row: 1, col: 0 }, 2)
139
+ sheet.setCellValue({ row: 2, col: 0 }, 3)
140
+ sheet.setCellValue({ row: 0, col: 1 }, 8)
141
+ sheet.setCellValue({ row: 0, col: 2 }, 4)
142
+ expect(calcValue(sheet, '=SUM(A1:A3, MAX(B1, MIN(C1, 10))) * 2')).toMatchObject({ v: 28 })
143
+ expect(calcValue(sheet, '=((1+2)*(3+4))/(7-2)')).toMatchObject({ v: 4.2 })
144
+ })
145
+
146
+ it('参数个数非法 → #VALUE!;未知函数 → #NAME?', () => {
147
+ const sheet = new Sheet()
148
+ expect(calcValue(sheet, '=IF(1)')).toMatchObject({ v: '#VALUE!', t: 'e' })
149
+ expect(calcValue(sheet, '=ROUND(1)')).toMatchObject({ v: '#VALUE!', t: 'e' })
150
+ expect(calcValue(sheet, '=ABS(1,2)')).toMatchObject({ v: '#VALUE!', t: 'e' })
151
+ expect(calcValue(sheet, '=SUM()')).toMatchObject({ v: '#VALUE!', t: 'e' })
152
+ expect(calcValue(sheet, '=NOSUCHFN(1)')).toMatchObject({ v: '#NAME?', t: 'e' })
153
+ })
154
+
155
+ it('函数名大小写不敏感', () => {
156
+ const sheet = new Sheet()
157
+ expect(calcValue(sheet, '=sum(1,2)')).toMatchObject({ v: 3 })
158
+ expect(calcValue(sheet, '=If(1,2,3)')).toMatchObject({ v: 2 })
159
+ })
160
+
161
+ it('区域内错误值传播', () => {
162
+ const sheet = new Sheet()
163
+ sheet.setCellFormula(A1, '=1/0')
164
+ sheet.setCellValue({ row: 1, col: 0 }, 5)
165
+ expect(calcValue(sheet, '=SUM(A1:A2)')).toMatchObject({ v: '#DIV/0!', t: 'e' })
166
+ })
167
+
168
+ it('稀疏性:大区域内只有少量格参与迭代', () => {
169
+ const sheet = new Sheet()
170
+ sheet.setCellValue(A1, 1)
171
+ sheet.setCellValue({ row: 9999, col: 0 }, 2)
172
+ // A1:A10000 只有 2 个真实格;结果与迭代规模无关
173
+ expect(sheet.store.size).toBe(2)
174
+ expect(calcValue(sheet, '=SUM(A1:A10000)')).toMatchObject({ v: 3 })
175
+ expect(calcValue(sheet, '=COUNT(A1:A10000)')).toMatchObject({ v: 2 })
176
+ expect(calcValue(sheet, '=AVERAGE(A1:A10000)')).toMatchObject({ v: 1.5 })
177
+ })
178
+ })