@aiquants/virtualscroll 1.19.3 → 1.22.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 (43) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README.md +9 -6
  3. package/dist/ScrollBar.d.cts +123 -0
  4. package/dist/ScrollBar.d.ts +123 -0
  5. package/dist/ScrollBar.d.ts.map +1 -1
  6. package/dist/ScrollPane.d.cts +19 -1
  7. package/dist/ScrollPane.d.ts +19 -1
  8. package/dist/ScrollPane.d.ts.map +1 -1
  9. package/dist/VirtualScroll.d.cts +9 -0
  10. package/dist/VirtualScroll.d.ts +9 -0
  11. package/dist/VirtualScroll.d.ts.map +1 -1
  12. package/dist/_headFenwick.d.cts +450 -0
  13. package/dist/_headFenwick.d.ts +451 -0
  14. package/dist/_headFenwick.d.ts.map +1 -0
  15. package/dist/index.cjs +1 -1
  16. package/dist/index.d.cts +1 -1
  17. package/dist/index.d.ts +1 -1
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +1470 -1360
  20. package/dist/styles/virtualscroll.css +1 -1
  21. package/dist/styles/virtualscroll.standalone.css +1 -1
  22. package/dist/tapScrollCircleSampleVisual.d.ts.map +1 -1
  23. package/dist/useFenwickMapTree.d.cts +43 -0
  24. package/dist/useFenwickMapTree.d.ts +43 -0
  25. package/dist/useFenwickMapTree.d.ts.map +1 -1
  26. package/package.json +4 -1
  27. package/src/ScrollBar.tsx +211 -33
  28. package/src/ScrollPane.tsx +252 -35
  29. package/src/VirtualScroll.tsx +11 -1
  30. package/src/index.ts +1 -1
  31. package/src/styles/virtualscroll.css +3 -3
  32. package/src/tapScrollCircleSampleVisual.tsx +22 -5
  33. package/src/useFenwickMapTree.ts +95 -13
  34. package/src/ScrollBar.spec.tsx +0 -620
  35. package/src/ScrollPane.spec.tsx +0 -482
  36. package/src/TapScrollCircle.spec.tsx +0 -275
  37. package/src/VirtualScroll.spec.ts +0 -641
  38. package/src/cli.server.spec.ts +0 -137
  39. package/src/logger.spec.ts +0 -128
  40. package/src/useFenwickMapTree.huge.spec.ts +0 -388
  41. package/src/useFenwickMapTree.spec.ts +0 -1518
  42. package/src/useLruCache.spec.ts +0 -382
  43. package/src/utils.spec.ts +0 -39
@@ -1,1518 +0,0 @@
1
- /**
2
- * @file Tests for FenwickMapTree and useFenwickMapTree.
3
- * @file FenwickMapTree と useFenwickMapTree のテスト。
4
- */
5
- import { act, renderHook } from "@testing-library/react"
6
- import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"
7
- import { FenwickMapTree, useFenwickMapTree } from "./useFenwickMapTree.ts"
8
-
9
- let warnSpy: ReturnType<typeof vi.spyOn> | undefined
10
-
11
- beforeAll(() => {
12
- warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {})
13
- })
14
-
15
- afterAll(() => {
16
- warnSpy?.mockRestore()
17
- warnSpy = undefined
18
- })
19
-
20
- describe("FenwickMapTree", () => {
21
- describe("コンストラクタと初期化", () => {
22
- it("固定値で正しく初期化されるべき", () => {
23
- const tree = new FenwickMapTree(10, 20)
24
- expect(tree.getSize()).toBe(10)
25
- expect(tree.get(0)).toBe(20)
26
- expect(tree.get(9)).toBe(20)
27
- expect(tree.prefixSum(0).cumulative).toBe(20)
28
- expect(tree.prefixSum(4).cumulative).toBe(100)
29
- expect(tree.getTotal()).toBe(200)
30
- })
31
-
32
- it("関数で正しく初期化されるべき", () => {
33
- const valueFn = (index: number) => (index + 1) * 10
34
- const tree = new FenwickMapTree(10, valueFn)
35
- expect(tree.getSize()).toBe(10)
36
- expect(tree.getTotal()).toBe(55 * 10) // 中央値の 55 * 10 になる。
37
- expect(tree.prefixSum(5).total).toBe(55 * 10) // prefixSum は materialize しない
38
- // get は materialize しない。中央値の 55 になる。
39
- expect(tree.get(5)).toBe(55)
40
- // prefixSum は materialize しない
41
- expect(tree.prefixSum(2).cumulative).toBe(55 + 55 + 55)
42
- // 具現化して値を確認
43
- expect(tree.get(5, { materializeOption: { materialize: true } })).toBe(60) // 6 * 10
44
- expect(tree.getTotal()).toBe(550 + 5)
45
- // 具現化して接頭辞和を確認
46
- expect(tree.prefixSum(4, { materializeOption: { materialize: true } }).cumulative).toBe(55 + 55 + 55 + 55 + 50)
47
- expect(tree.prefixSum(4, { materializeOption: { materialize: true } }).total).toBe(55 + 55 + 55 + 55 + 50 + 60 + 55 + 55 + 55 + 55) // 具現化後の合計値も確認
48
- // 具現化して接頭辞和を確認
49
- expect(tree.prefixSum(5, { materializeOption: { materialize: true } }).cumulative).toBe(55 + 55 + 55 + 55 + 50 + 60)
50
- expect(tree.prefixSum(5, { materializeOption: { materialize: true } }).total).toBe(55 + 55 + 55 + 55 + 50 + 60 + 55 + 55 + 55 + 55) // 具現化後の合計値も確認
51
- // 具現化して接頭辞和を確認
52
- expect(tree.prefixSum(7, { materializeOption: { materialize: true } }).cumulative).toBe(55 + 55 + 55 + 55 + 50 + 60 + 55 + 80)
53
- // 具現化後の合計値も確認
54
- expect(tree.getTotal()).toBe(55 + 55 + 55 + 55 + 50 + 60 + 55 + 80 + 55 + 55)
55
- // 具現化後の接頭辞和も確認
56
- expect(
57
- tree.prefixSum(9, {
58
- materializeOption: {
59
- materialize: true,
60
- ranges: [
61
- { from: 0, to: 1 },
62
- { from: 2, to: 2 },
63
- ],
64
- },
65
- }).cumulative,
66
- ).toBe(10 + 20 + 30 + 55 + 50 + 60 + 55 + 80 + 55 + 100)
67
- })
68
-
69
- it("関数とサンプリングオプションで初期化されるべき - サンプリング範囲が全体と一致", () => {
70
- const valueFn = (i: number) => (i < 5 ? 10 : 20)
71
- const tree = new FenwickMapTree(10, valueFn, { sampleRange: { from: 0, to: 9 } })
72
- // サンプリング範囲の値は [10, 10, 10, 10, 10, 20, 20, 20, 20, 20]
73
- // 合計は 150, 中央値は 15。baseValue は 15 になる。
74
- // get は具現化しない限り baseValue を返す
75
- expect(tree.get(0)).toBe(15)
76
- expect(tree.get(4)).toBe(15)
77
- expect(tree.get(5)).toBe(15)
78
- expect(tree.get(9)).toBe(15)
79
- // 具現化すると valueFn の値が返る
80
- expect(tree.get(0, { materializeOption: { materialize: true } })).toBe(10)
81
- expect(tree.get(9, { materializeOption: { materialize: true } })).toBe(20)
82
- // getTotal は valueFn を全範囲で評価した合計値
83
- expect(tree.getTotal()).toBe(150)
84
- })
85
-
86
- it("関数とサンプリングオプションで初期化されるべき - サンプリング範囲が一部", () => {
87
- const valueFn = (i: number) => (i < 5 ? 10 : 100) // 0-4: 10, 5-9: 100
88
- // 最初の5要素のみサンプリング
89
- const tree = new FenwickMapTree(10, valueFn, { sampleRange: { from: 0, to: 4 } })
90
- // サンプリング範囲の値は [10, 10, 10, 10, 10]
91
- // baseValue は 10 になる。
92
- expect(tree.get(0)).toBe(10) // サンプリング範囲内 (baseValue)
93
- expect(tree.get(9)).toBe(10) // サンプリング範囲外 (baseValue)
94
- // 具現化すると正しい値が返る
95
- expect(tree.get(9, { materializeOption: { materialize: true } })).toBe(100)
96
- // getTotal は valueFn を全範囲で評価した合計値
97
- expect(tree.getTotal()).toBe(5 * 10 + 4 * 10 + 100)
98
- })
99
-
100
- it("関数とサンプリングオプションで初期化されるべき - サンプリングで baseValue が偏る場合", () => {
101
- const valueFn = (i: number) => (i === 0 ? 100 : 10) // ほとんどが10
102
- // 全範囲をサンプリングすると、最頻値は 10 になるため baseValue は 10 になる
103
- const tree = new FenwickMapTree(10, valueFn, { sampleRange: { from: 0, to: 9 } })
104
- // get は具現化しない限り baseValue を返す
105
- expect(tree.get(5)).toBe(10)
106
- // 具現化
107
- expect(tree.get(0, { materializeOption: { materialize: true } })).toBe(100)
108
- expect(tree.get(1, { materializeOption: { materialize: true } })).toBe(10)
109
- // 合計値は valueFn を全範囲で評価した合計値になる
110
- const expectedTotal = 100 + 9 * 10
111
- expect(tree.getTotal({ materializeOption: { materialize: true } })).toBe(expectedTotal)
112
- })
113
-
114
- it("関数とサンプリングオプションで初期化されるべき - サンプリングサイズが小さい - 初期具現化なし", () => {
115
- const valueFn = (i: number) => 100 + i
116
- // 2要素だけサンプリング。サンプリング範囲はデフォルトで具現化されない
117
- const tree = new FenwickMapTree(20, valueFn, { sampleRange: { from: 5, to: 6 } })
118
- // サンプル値: 105, 106。 baseValue = Math.floor((105+106)/2) = 105
119
- expect(tree.get(0)).toBe(105)
120
- // 具現化
121
- expect(tree.get(0, { materializeOption: { materialize: true } })).toBe(100)
122
- // 合計値
123
- // baseValue が 105 になるため、具現化されていない 19 個の要素は 105 として扱われる。
124
- // 具現化済みの1要素 (index:0) と合わせて、合計は 19 * 105 + 100 = 2095 となる。
125
- const expectedTotal = 19 * 105 + 100
126
- expect(tree.getTotal()).toBe(expectedTotal) // 2095
127
- })
128
-
129
- it("関数とサンプリングオプションで初期化されるべき - サンプリングサイズが小さい - 初期具現化あり", () => {
130
- const valueFn = (i: number) => 100 + i
131
- // 2要素だけサンプリング。サンプリング範囲はデフォルトで具現化されない
132
- const tree = new FenwickMapTree(20, valueFn, { sampleRange: { from: 5, to: 6 }, materialize: true })
133
- // サンプル値: 105, 106。 baseValue = Math.floor((105+106)/2) = 105
134
- expect(tree.get(0)).toBe(105)
135
- // 具現化
136
- expect(tree.get(0, { materializeOption: { materialize: true } })).toBe(100)
137
- // 合計値
138
- // baseValue が 105 になるため、具現化されていない 17 個の要素は 105 として扱われる。
139
- // 具現化済みの3要素 (5, 6, 0) と合わせて、合計は 17 * 105 + 105 + 106 + 100 = 2096 となる。
140
- const expectedTotal = 17 * 105 + 105 + 106 + 100
141
- expect(tree.getTotal()).toBe(expectedTotal) // 2096
142
- })
143
-
144
- it("サイズ 0 で正しく初期化されるべき", () => {
145
- const tree = new FenwickMapTree(0, 10)
146
- expect(tree.getSize()).toBe(0)
147
- expect(tree.getTotal()).toBe(0)
148
- expect(tree.prefixSum(-1).cumulative).toBe(0)
149
- expect(tree.findIndexAtOrAfter(10).index).toBe(-1)
150
- })
151
- })
152
-
153
- describe("update と updates", () => {
154
- it("単一の値を正しく更新するべき", () => {
155
- const tree = new FenwickMapTree(10, 10) // total = 100
156
- tree.update(5, 20) // change = +10
157
- expect(tree.get(5)).toBe(20)
158
- expect(tree.getTotal()).toBe(110)
159
- expect(tree.prefixSum(5).cumulative).toBe(5 * 10 + 20)
160
- })
161
-
162
- it("複数の値を正しく更新するべき", () => {
163
- const tree = new FenwickMapTree(10, 10) // total = 100
164
- tree.updates([
165
- { index: 2, value: 5 }, // change = -5
166
- { index: 8, value: 15 }, // change = +5
167
- ])
168
- expect(tree.get(2)).toBe(5)
169
- expect(tree.get(8)).toBe(15)
170
- expect(tree.getTotal()).toBe(100)
171
- expect(tree.prefixSum(8).cumulative).toBe(7 * 10 + 5 + 15)
172
- })
173
-
174
- it("関数で初期化されたツリーを正しく更新するべき", () => {
175
- const tree = new FenwickMapTree(10, (i) => i) // baseValue は 4, total は 40
176
- tree.update(5, 10) // 元の値は baseValue(4), change = +6
177
- expect(tree.get(5)).toBe(10)
178
- expect(tree.getTotal()).toBe(46) // 40 + 6
179
- })
180
- })
181
-
182
- describe("prefixSum", () => {
183
- it("接頭辞和を正しく計算するべき", () => {
184
- const tree = new FenwickMapTree(10, (i) => i + 1) // 1, 2, ..., 10
185
- expect(tree.prefixSum(2).cumulative).toBe(5 + 5 + 5)
186
- expect(tree.prefixSum(9).cumulative).toBe(5 * 10)
187
- })
188
-
189
- it("更新後の接頭辞和を正しく計算するべき", () => {
190
- const tree = new FenwickMapTree(10, 10)
191
- tree.update(5, 0)
192
- expect(tree.prefixSum(4).cumulative).toBe(50)
193
- expect(tree.prefixSum(5).cumulative).toBe(50)
194
- expect(tree.prefixSum(6).cumulative).toBe(60)
195
- })
196
-
197
- it("範囲外のインデックスは正しく処理されるべき", () => {
198
- const tree = new FenwickMapTree(10, 10)
199
- expect(tree.prefixSum(-1).cumulative).toBe(0)
200
- expect(tree.prefixSum(10).cumulative).toBe(100) // size-1 に丸められる
201
- })
202
-
203
- it("materialize オプションが正しく動作するべき", () => {
204
- const valueFn = vi.fn((index: number) => index * 10)
205
- const tree = new FenwickMapTree(10, valueFn)
206
-
207
- // 具現化なし
208
- tree.prefixSum(5)
209
- expect(valueFn).toHaveBeenCalledTimes(10)
210
-
211
- // 具現化あり
212
- tree.prefixSum(5, { materializeOption: { materialize: true } })
213
- // 5 のみ具現化される
214
- expect(valueFn).toHaveBeenCalledTimes(11)
215
- })
216
- })
217
-
218
- describe("getTotal", () => {
219
- it("初期状態の合計値を正しく計算するべき", () => {
220
- const tree = new FenwickMapTree(10, 15)
221
- expect(tree.getTotal()).toBe(150)
222
- })
223
-
224
- it("関数で初期化されたツリーの合計値を正しく計算するべき", () => {
225
- const tree = new FenwickMapTree(10, (i) => i)
226
- expect(tree.getTotal()).toBe(4 * 10)
227
- })
228
-
229
- it("更新後の合計値を正しく計算するべき", () => {
230
- const tree = new FenwickMapTree(10, 10)
231
- tree.update(0, 20)
232
- tree.update(9, 0)
233
- expect(tree.getTotal()).toBe(100)
234
- })
235
- })
236
-
237
- describe("changeSize", () => {
238
- it("サイズを大きくするべき", () => {
239
- const tree = new FenwickMapTree(5, 10) // total = 50
240
- tree.changeSize(10)
241
- expect(tree.getSize()).toBe(10)
242
- expect(tree.getTotal()).toBe(100)
243
- expect(tree.get(9)).toBe(10)
244
- })
245
-
246
- it("サイズを小さくするべき", () => {
247
- const tree = new FenwickMapTree(10, 10) // total = 100
248
- tree.update(8, 20) // total = 110
249
- tree.changeSize(5)
250
- expect(tree.getSize()).toBe(5)
251
- // サイズ縮小により、[0, 4] の範囲の要素のみが残る。
252
- // インデックス 8 の更新は失われるため、合計値は 5 * 10 = 50 になる。
253
- expect(tree.getTotal()).toBe(50)
254
- expect(() => tree.get(8)).toThrow()
255
- })
256
-
257
- it("関数で初期化されたツリーのサイズを小さくするべき", () => {
258
- // valueFn で初期化。サンプル範囲は [0, 4]。値は [0, 1, 2, 3, 4]。
259
- // 平均は 2 なので baseValue は 2。初期 total は 2 * 5 = 10。
260
- const tree = new FenwickMapTree(5, (i) => i)
261
- expect(tree.getTotal()).toBe(10)
262
-
263
- // インデックス 4 の値を 10 に更新。元の値は baseValue(2) なので、差分は +8。
264
- // total は 10 + 8 = 18 になる。
265
- tree.update(4, 10)
266
- expect(tree.getTotal()).toBe(18)
267
-
268
- // サイズを 3 に縮小。インデックス 4 の更新は失われる。
269
- // 新しいツリーは size=3, baseValue=2 で再計算される。
270
- // total は 2 * 3 = 6 になる。
271
- tree.changeSize(3)
272
- expect(tree.getSize()).toBe(3)
273
- expect(tree.getTotal()).toBe(6)
274
- // 縮小後の範囲の値を具現化して確認
275
- expect(tree.get(0, { materializeOption: { materialize: true } })).toBe(0)
276
- expect(tree.get(1, { materializeOption: { materialize: true } })).toBe(1)
277
- expect(tree.get(2, { materializeOption: { materialize: true } })).toBe(2)
278
- // 具現化後の合計値は 0 + 1 + 2 = 3
279
- expect(tree.getTotal()).toBe(3)
280
- })
281
-
282
- it("関数で初期化されたツリーのサイズを大きくするべき", () => {
283
- // valueFn で初期化。baseValue は 2。total は 2 * 5 = 10。
284
- const tree = new FenwickMapTree(5, (i) => i)
285
- tree.update(2, 10) // 元の値は baseValue(2)。差分 +8。total = 18。
286
- expect(tree.getTotal()).toBe(18)
287
-
288
- // サイズを 8 に拡大。
289
- // 既存の要素 [0-4] は維持され、新しい要素 [5-7] が baseValue(2) で追加される。
290
- // total は 18 + 3 * 2 = 24 になる。
291
- tree.changeSize(8)
292
- expect(tree.getSize()).toBe(8)
293
- expect(tree.getTotal()).toBe(24)
294
- // 維持された更新済みの値を確認
295
- expect(tree.get(2)).toBe(10)
296
- // 新しく追加された値を確認
297
- expect(tree.get(7)).toBe(2)
298
- // 新しい要素を具現化
299
- expect(tree.get(7, { materializeOption: { materialize: true } })).toBe(7)
300
- // 具現化による差分 (7 - 2 = 5) が total に反映される。
301
- expect(tree.getTotal()).toBe(24 + 5)
302
- })
303
-
304
- it("サイズ 0 に変更してから再度大きくするべき", () => {
305
- const tree = new FenwickMapTree(5, 10)
306
- tree.update(3, 20) // total = 60
307
- tree.changeSize(0)
308
- expect(tree.getSize()).toBe(0)
309
- expect(tree.getTotal()).toBe(0)
310
-
311
- tree.changeSize(3)
312
- expect(tree.getSize()).toBe(3)
313
- // 新しい要素は baseValue(10) で初期化される
314
- expect(tree.getTotal()).toBe(30)
315
- expect(tree.get(0)).toBe(10)
316
- })
317
-
318
- it("サイズを拡大・縮小を繰り返すべき", () => {
319
- const tree = new FenwickMapTree(5, 10) // size: 5, total: 50
320
- tree.update(1, 20) // total: 60
321
-
322
- // 1. 拡大
323
- tree.changeSize(10) // size: 10, total: 60 + 5*10 = 110
324
- expect(tree.getSize()).toBe(10)
325
- expect(tree.getTotal()).toBe(110)
326
- tree.update(8, 30) // total: 110 - 10 + 30 = 130
327
- expect(tree.getTotal()).toBe(130)
328
-
329
- // 2. 縮小
330
- tree.changeSize(7) // size: 7
331
- // インデックス 8 の更新は失われる。
332
- // total はインデックス 1 の更新 (20) + 6 * 10 = 80
333
- expect(tree.getSize()).toBe(7)
334
- expect(tree.getTotal()).toBe(80)
335
- expect(() => tree.get(8)).toThrow()
336
- expect(tree.get(1)).toBe(20)
337
-
338
- // 3. 再度拡大
339
- tree.changeSize(12) // size: 12
340
- // total は 80 + 5 * 10 = 130
341
- expect(tree.getSize()).toBe(12)
342
- expect(tree.getTotal()).toBe(130)
343
- expect(tree.get(11)).toBe(10)
344
- })
345
-
346
- it("関数で初期化されたツリーでサイズを拡大・縮小を繰り返すべき", () => {
347
- const valueFn = (i: number) => 100 + i
348
- const tree = new FenwickMapTree(10, valueFn) // baseValue: 104
349
- // total: 104 * 10 = 1040
350
-
351
- // 1. 拡大
352
- tree.changeSize(15) // total: 1040 + 5 * 104 = 1560
353
- expect(tree.getSize()).toBe(15)
354
- expect(tree.getTotal()).toBe(1560)
355
-
356
- // 2. 具現化と更新
357
- expect(tree.get(12, { materializeOption: { materialize: true } })).toBe(112)
358
- // total: 1560 - 104 + 112 = 1568
359
- expect(tree.getTotal()).toBe(1568)
360
- tree.update(5, 200) // 元の値は baseValue(104), 差分 +96
361
- // total: 1568 + 96 = 1664
362
- expect(tree.getTotal()).toBe(1664)
363
-
364
- // 3. 縮小
365
- tree.changeSize(8)
366
- // インデックス 12 の具現化と更新は失われる。
367
- // インデックス 5 の更新は残る。
368
- // 新しい total は、baseValue(104) * 7 + 更新された値(200) = 728 + 200 = 928
369
- expect(tree.getSize()).toBe(8)
370
- expect(tree.getTotal()).toBe(928)
371
-
372
- // 4. 再度拡大
373
- tree.changeSize(10)
374
- // total: 928 + 2 * 104 = 1136
375
- expect(tree.getSize()).toBe(10)
376
- expect(tree.getTotal()).toBe(1136)
377
- // 失われたインデックス 8 の値は baseValue に戻る
378
- expect(tree.get(8)).toBe(104)
379
- })
380
- })
381
-
382
- describe("findIndexAtOrAfter", () => {
383
- const tree = new FenwickMapTree(10, 10) // 各要素10, 累積和は 10, 20, ..., 100
384
- tree.update(5, 20) // 累積和は ..., 50, 70, 80, ...
385
-
386
- it("findIndexAtOrAfter が正しいインデックスを見つけるべき", () => {
387
- expect(tree.findIndexAtOrAfter(25).index).toBe(2)
388
- expect(tree.findIndexAtOrAfter(30).index).toBe(2)
389
- expect(tree.findIndexAtOrAfter(70).index).toBe(5)
390
- expect(tree.findIndexAtOrAfter(1).index).toBe(0)
391
- expect(tree.findIndexAtOrAfter(121).index).toBe(-1)
392
- })
393
-
394
- it("findIndexAtOrAfter の境界値テスト(findLast相当の検証)", () => {
395
- // findLast(25) は index 1 を返すべきだったが、findIndexAtOrAfter では境界値を確認
396
- // 累積和: [10, 20, 30, 40, 50, 70, 80, 90, 100, 110]
397
-
398
- // 25 以上の最初のインデックスは 2 (累積和30)
399
- expect(tree.findIndexAtOrAfter(25).index).toBe(2)
400
- // 24 以上の最初のインデックスは 2 (累積和30)
401
- expect(tree.findIndexAtOrAfter(24).index).toBe(2)
402
- // 20 以上の最初のインデックスは 1 (累積和20)
403
- expect(tree.findIndexAtOrAfter(20).index).toBe(1)
404
- // 21 以上の最初のインデックスは 2 (累積和30)
405
- expect(tree.findIndexAtOrAfter(21).index).toBe(2)
406
- })
407
-
408
- it("findIndexAtOrAfter の境界値テスト(より多くのパターン)", () => {
409
- // 元の findLast のテストパターンを findIndexAtOrAfter で検証
410
- // findLast(69) は index 4 を返すべきだった -> 69以上は index 5
411
- expect(tree.findIndexAtOrAfter(69).index).toBe(5)
412
- expect(tree.findIndexAtOrAfter(70).index).toBe(5)
413
- expect(tree.findIndexAtOrAfter(71).index).toBe(6)
414
-
415
- // findLast(1) は index -1 を返すべきだった -> 1以上は index 0
416
- expect(tree.findIndexAtOrAfter(1).index).toBe(0)
417
- expect(tree.findIndexAtOrAfter(0).index).toBe(0)
418
-
419
- // findLast(121) は index 9 を返すべきだった -> 121以上は存在しない
420
- expect(tree.findIndexAtOrAfter(121).index).toBe(-1)
421
- expect(tree.findIndexAtOrAfter(110).index).toBe(9)
422
- expect(tree.findIndexAtOrAfter(111).index).toBe(-1)
423
- })
424
-
425
- it("findIndexAtOrAfter を total 値で呼び出すと末尾のインデックスを返すこと", () => {
426
- const total = tree.getTotal()
427
- const result = tree.findIndexAtOrAfter(total)
428
- expect(result.index).toBe(tree.getSize() - 1)
429
- const offset = total - tree.prefixSum(result.index - 1).cumulative
430
- expect(offset).toBe(tree.get(result.index))
431
- })
432
- })
433
-
434
- describe("エッジケース", () => {
435
- it("範囲外のインデックスで get を呼ぶとエラーをスローするべき", () => {
436
- const tree = new FenwickMapTree(10, 10)
437
- expect(() => tree.get(10)).toThrow("Index out of bounds")
438
- expect(() => tree.get(-1)).toThrow("Index out of bounds")
439
- })
440
-
441
- it("範囲外のインデックスで update を呼ぶとエラーをスローするべき", () => {
442
- const tree = new FenwickMapTree(10, 10)
443
- // updateDeltas 内でエラーがスローされる
444
- expect(() => tree.update(10, 10)).toThrow("Index 10 out of bounds")
445
- })
446
-
447
- it("負の値を update しようとするとエラーをスローするべき", () => {
448
- const tree = new FenwickMapTree(10, 10)
449
- expect(() => tree.update(5, -10)).toThrow("Value cannot be negative.")
450
- })
451
- })
452
-
453
- describe("複雑なシナリオと連続操作", () => {
454
- it("更新、サイズ拡大、再更新後に正しい状態を保つべき", () => {
455
- const tree = new FenwickMapTree(5, 10) // size: 5, total: 50
456
- tree.update(2, 20) // [10, 10, 20, 10, 10], total: 60
457
- expect(tree.getTotal()).toBe(60)
458
-
459
- tree.changeSize(8) // size: 8, [10, 10, 20, 10, 10, 10, 10, 10]
460
- expect(tree.getSize()).toBe(8)
461
- expect(tree.getTotal()).toBe(90) // [10, 10, 20, 10, 10, 10, 10, 10]
462
-
463
- tree.update(7, 30) // [10, 10, 20, 10, 10, 10, 10, 30], total: 110
464
- expect(tree.get(7)).toBe(30)
465
- expect(tree.getTotal()).toBe(110)
466
- expect(tree.prefixSum(7).cumulative).toBe(110)
467
- expect(tree.prefixSum(3).cumulative).toBe(50)
468
- })
469
-
470
- it("更新、サイズ縮小、再更新後に正しい状態を保つべき", () => {
471
- const tree = new FenwickMapTree(10, (i) => i) // 0..9, total: 40, baseValue: 4
472
- tree.updates([
473
- { index: 8, value: 20 }, // 元の値は baseValue(4), 差分 +16
474
- { index: 9, value: 30 }, // 元の値は baseValue(4), 差分 +26
475
- ]) // total: 40 + 16 + 26 = 82
476
- tree.changeSize(5) // 4,4,4,4,5, total: 20
477
- expect(tree.getTotal()).toBe(20)
478
-
479
- // サイズ縮小: size=5
480
- // baseValue は 4 のまま。インデックス 8, 9 の更新は破棄される。
481
- // 新しい total は 4 * 5 = 20
482
- tree.changeSize(5)
483
- expect(tree.getSize()).toBe(5)
484
- expect(tree.getTotal()).toBe(20)
485
- // 累積和の確認: [4, 4, 4, 4, 4] -> 合計 20
486
- expect(tree.prefixSum(4).cumulative).toBe(20)
487
-
488
- // 再更新: index=0 の値を 10 に
489
- // 元の値は baseValue(4), 差分 +6
490
- // 新しい total は 20 + 6 = 26
491
- tree.update(0, 10)
492
- expect(tree.getTotal()).toBe(26)
493
- // 累積和の確認: [10, 4, 4, 4, 4] -> 合計 26
494
- expect(tree.prefixSum(4).cumulative).toBe(26)
495
- })
496
-
497
- it("find, updateDelta, resize, getTotal, reset の連続操作が正しく動作するべき", () => {
498
- const valueFn = vi.fn((i: number) => 10 + i) // 10, 11, 12, ...
499
- // サンプル値は [10...19]、中央値は (14+15)/2=14.5 -> baseValue=14
500
- const tree = new FenwickMapTree(10, valueFn)
501
- let total = 14 * 10 // 140
502
- expect(tree.getTotal()).toBe(total)
503
-
504
- // 1. 一部を具現化
505
- // コンストラクタで10回、prefixSumで1回呼ばれる
506
- expect(tree.prefixSum(3, { materializeOption: { materialize: true } }).cumulative).toBe(14 + 14 + 14 + 13) // 3 のみ具現化
507
- expect(valueFn).toHaveBeenCalledTimes(11)
508
- total += 13 - 14 // 140 - 1 = 139
509
- expect(tree.getTotal()).toBe(total)
510
-
511
- // 2. 具現化済みと未具現化のインデックスを更新
512
- tree.updates([
513
- // index 2 は未具現化。元の値は baseValue(14)。差分は 5 - 14 = -9
514
- { index: 2, value: 5 },
515
- // index 8 は未具現化。元の値は baseValue(14)。差分は 25 - 14 = +11
516
- { index: 8, value: 25 },
517
- ])
518
- total += 5 - 14 + (25 - 14) // 139 - 9 + 11 = 141
519
- expect(tree.getTotal()).toBe(total)
520
-
521
- // 3. サイズ変更
522
- tree.changeSize(12)
523
- expect(tree.getSize()).toBe(12)
524
- // 新しい要素 (index 10, 11) は baseValue(14) で埋められる
525
- total += 2 * 14 // 141 + 28 = 169
526
- expect(tree.getTotal()).toBe(total)
527
-
528
- // 4. 再更新
529
- tree.update(11, 50) // baseValue(14) -> 50, diff: +36
530
- total += 50 - 14 // 169 + 36 = 205
531
- expect(tree.getTotal()).toBe(total)
532
-
533
- // 5. サイズ縮小
534
- tree.changeSize(5)
535
- expect(tree.getSize()).toBe(5)
536
- // 削除される範囲: [5, 11]
537
- // 削除される更新: index 8 (25), index 11 (50)
538
- // 残る範囲: [0, 4]
539
- // 残る更新: index 2 (5)
540
- // 残る具現化: index 3 (13)
541
- //
542
- // 再計算:
543
- // index 0,1,4 は baseValue (14)
544
- // index 2 は更新済み (5)
545
- // index 3 は具現化済み (13)
546
- // total = 14 * 3 + 5 + 13 = 42 + 18 = 60
547
- expect(tree.getTotal()).toBe(60)
548
- })
549
-
550
- it("遅延初期化と複数操作を組み合わせても正しく動作するべき", () => {
551
- const valueFn = vi.fn((i: number) => i * 10)
552
- // 初期化: size=10, valueFn=(i)=>i*10
553
- // サンプル値は [0, 10, ..., 90] で全てユニークなため、中央値が baseValue になる。
554
- // baseValue = (40 + 50) / 2 = 45
555
- const tree = new FenwickMapTree(10, valueFn)
556
- // 初期 total は baseValue * size = 45 * 10 = 450
557
- expect(tree.getTotal()).toBe(450)
558
-
559
- // 1. 一部を具現化
560
- // コンストラクタで10回、prefixSumで1回呼ばれる
561
- expect(tree.prefixSum(3, { materializeOption: { materialize: true } }).cumulative).toBe(45 + 45 + 45 + 30) // 3 のみ具現化, -15
562
- expect(valueFn).toHaveBeenCalledTimes(11)
563
- // 具現化による差分が total に反映される
564
- expect(tree.getTotal()).toBe(450 - 15)
565
-
566
- // 2. 具現化済みと未具現化のインデックスを更新
567
- tree.updates([
568
- // index 2 は未具現化。元の値は baseValue(45)。差分は 25 - 45 = -20
569
- { index: 2, value: 25 },
570
- // index 8 は未具現化。元の値は baseValue(45)。差分は 85 - 45 = +40
571
- { index: 8, value: 85 },
572
- ])
573
- // total = 435 - 20 + 40 = 455
574
- expect(tree.getTotal()).toBe(455)
575
-
576
- // 3. サイズ変更
577
- tree.changeSize(12)
578
- expect(tree.getSize()).toBe(12)
579
- // 新しい要素 (index 10, 11) は baseValue(45) で埋められる
580
- // total = 455 + 2 * 45 = 545
581
- expect(tree.getTotal()).toBe(545)
582
- expect(tree.get(11)).toBe(45) // 新しい要素は baseValue
583
-
584
- // 4. 再更新
585
- tree.update(11, 100) // baseValue(45) -> 100, diff: +55
586
- // total = 545 + 55 = 600
587
- expect(tree.getTotal()).toBe(600)
588
-
589
- // 5. サイズ縮小 (更新した値が含まれるように)
590
- tree.changeSize(10)
591
- expect(tree.getSize()).toBe(10)
592
- // 削除される範囲: [10, 11]
593
- // 削除される更新: index 12 (100)
594
- // 残る範囲: [0, 9]
595
- // 残る具現化: index 0 (0), index 1 (1), ..., index 9 (9), total = 45
596
- tree.changeSize(10)
597
- expect(tree.getSize()).toBe(10)
598
- expect(tree.getTotal()).toBe(455) // [45,45,25,30,45,45,45,45,85,45] = 455
599
- })
600
- })
601
-
602
- describe("rebuildTree と calculateAccumulatedError", () => {
603
- it("reset が valueFn でツリーを正しく再構築するべき", () => {
604
- const tree = new FenwickMapTree(10, 10)
605
- tree.update(5, 20)
606
-
607
- // reset を呼び出すと、新しい size と valueFn で再初期化される
608
- // サンプル値 [0,1,2,3,4] から baseValue=2 が計算される
609
- tree.reset(5, (i) => i)
610
-
611
- expect(tree.getSize()).toBe(5)
612
- // baseValue は 2 なので、合計は 2 * 5 = 10
613
- expect(tree.getTotal()).toBe(10)
614
- // get(4) は baseValue を返す
615
- expect(tree.get(4)).toBe(2)
616
- // get(0) は baseValue を返す
617
- expect(tree.get(0)).toBe(2)
618
- })
619
-
620
- it("calculateAccumulatedError が状態の不整合を正しく検出するべき", () => {
621
- const tree = new FenwickMapTree(10, 10)
622
- tree.update(5, 20)
623
-
624
- // reset を呼び出すと、新しい size と valueFn で再初期化される
625
- // サンプル値 [0,1,2,3,4] から baseValue=2 が計算され、
626
- // サンプル値は即座に具現化される
627
- tree.reset(5, (i) => i, { materialize: true })
628
-
629
- expect(tree.getSize()).toBe(5)
630
- // baseValue は 2 だが、全要素が具現化されているため、合計は 0+1+2+3+4=10
631
- expect(tree.getTotal()).toBe(10)
632
- // get(4) は具現化された値 4 を返す
633
- expect(tree.get(4)).toBe(4)
634
- // get(0) は具現化された値 0 を返す
635
- expect(tree.get(0)).toBe(0)
636
- })
637
-
638
- it("calculateAccumulatedError が具現化による誤差を正しく計算するべき", () => {
639
- // valueFn のサンプル値は [0,1,2,3,4], baseValue は 2
640
- const tree = new FenwickMapTree(5, (i) => i)
641
- expect(tree.getTotal()).toBe(10) // 2 * 5 = 10
642
- // この時点では具現化された値はないので、誤差は 0
643
- expect(tree.calculateAccumulatedError()).toBe(0)
644
-
645
- // index 3 を具現化: valueFn(3) = 3。元の値は baseValue(2)
646
- tree.get(3, { materializeOption: { materialize: true } })
647
- expect(tree.getTotal()).toBe(11) // 10 + 1 = 11
648
- // 実際の合計値は 2+2+2+3+2 = 11。
649
- // 具現化された値の合計は 3。
650
- // 未具現化の値の合計は 2 * 4 = 8。
651
- // 誤差 = 0
652
- expect(tree.calculateAccumulatedError()).toBe(0)
653
-
654
- // index 0 を具現化: valueFn(0) = 0。元の値は baseValue(2)。差分は 0 - 2 = -2
655
- tree.get(0, { materializeOption: { materialize: true } })
656
- expect(tree.getTotal()).toBe(9) // 11 - 2 = 9
657
- // 誤差 = 0
658
- expect(tree.calculateAccumulatedError()).toBe(0)
659
- })
660
-
661
- it("rebuildTree 後に誤差がリセットされるべき", () => {
662
- const tree = new FenwickMapTree(5, (i) => i) // baseValue=2
663
- tree.get(3, { materializeOption: { materialize: true } }) // total = 11, error = 0
664
- expect(tree.calculateAccumulatedError()).toBe(0)
665
-
666
- // rebuildTree を呼び出す
667
- tree.rebuildTree()
668
- expect(tree.getTotal()).toBe(11) // 具現化された値は維持される
669
- expect(tree.calculateAccumulatedError()).toBe(0)
670
- // 以前具現化された値
671
- expect(tree.get(3)).toBe(3)
672
- })
673
-
674
- it("意図的な不整合を calculateAccumulatedError が検出するべき", () => {
675
- const tree = new FenwickMapTree(5, 10)
676
- // 内部の total を手動で変更する
677
- expect(tree.getTotal()).toBe(50)
678
- // @ts-expect-error - private property access for testing
679
- expect(tree.getTotal()).toBe(tree.total)
680
- // 期待される誤差は 0
681
- expect(tree.calculateAccumulatedError()).toBe(0)
682
-
683
- // rebuildTree を実行
684
- tree.rebuildTree()
685
- expect(tree.getTotal()).toBe(50)
686
- expect(tree.calculateAccumulatedError()).toBe(0)
687
- })
688
- })
689
-
690
- describe("サイズ変更", () => {
691
- it("updateDelta が正しく差分を更新するべき", () => {
692
- const tree = new FenwickMapTree(10, 100) // total: 1000
693
- // インデックス 5 の値を +20 する
694
- tree.updateDelta(5, 20)
695
- expect(tree.get(5)).toBe(120)
696
- expect(tree.getTotal()).toBe(1020)
697
- // さらに -5 する
698
- tree.updateDelta(5, -5)
699
- expect(tree.get(5)).toBe(115)
700
- expect(tree.getTotal()).toBe(1015)
701
- expect(tree.prefixSum(4).cumulative).toBe(500)
702
- expect(tree.prefixSum(5).cumulative).toBe(500 + 115)
703
- })
704
-
705
- it("find, updateDelta, resize, getTotal, reset の連続操作が正しく動作するべき", () => {
706
- const valueFn = vi.fn((i: number) => 10 + i) // 10, 11, 12, ...
707
- // サンプル値は [10...29]、中央値は (19+20)/2=19.5 -> baseValue=19
708
- const tree = new FenwickMapTree(20, valueFn)
709
- const baseValue = 19
710
- let total = baseValue * 20 // 380
711
- expect(tree.getTotal()).toBe(total)
712
-
713
- // 1. findIndexAtOrAfter を呼び、一部の値を具現化させる
714
- // この操作により、total が変化する
715
- const { index, total: newTotal } = tree.findIndexAtOrAfter(100, { materializeOption: { materialize: true } }) // 18*5=90, 19*6=114
716
- expect(index).toBe(5) // index=6 が最初の具現化値, size 19 -> 15
717
- expect(newTotal).toBe(376) // ?
718
- total = tree.getTotal() // findIndexAtOrAfter 後の実際の total を取得
719
- expect(total).toBe(newTotal)
720
- const initialCalls = valueFn.mock.calls.length
721
- expect(initialCalls).toBeGreaterThan(0)
722
-
723
- // 2. 未具現化のインデックスに対して updateDelta を実行
724
- // updateDelta は現在の値(未具現化なら baseValue)に差分を加える
725
- // 元の値は baseValue(19), 更新後は 19 + 5 = 24
726
- expect(tree.getTotal()).toBe(376)
727
- tree.updateDelta(15, 5)
728
- expect(tree.get(15)).toBe(24)
729
- total += 5
730
- expect(tree.getTotal()).toBe(total) // 368 + 5 = 373
731
- // get で具現化すると、valueFn(15)=25 になる。
732
- // ただし、updateDelta ですでに delta が 5 になっているので、
733
- // 具現化の際に delta は valueFn(15) - baseValue = 25 - 19 = 6 に上書きされる。
734
- // 最終的な total の変化は 6 - 5 = +1
735
- total += 1
736
- expect(tree.get(15, { materializeOption: { materialize: true } })).toBe(25)
737
- expect(tree.getTotal()).toBe(total) // 373 + 1 = 374
738
- expect(valueFn).toHaveBeenCalledWith(15)
739
-
740
- // 3. サイズを縮小し、更新した要素を削除
741
- tree.changeSize(10)
742
- // 縮小後の合計値は、0-9 の範囲で具現化された値の合計になる
743
- const materializedTotalAfterShrink = tree.getTotal({ materializeOption: { materialize: true, ranges: [{ from: 0, to: 9 }] } })
744
- expect(tree.getSize()).toBe(10)
745
- expect(tree.getTotal()).toBe(materializedTotalAfterShrink)
746
- total = materializedTotalAfterShrink
747
-
748
- // 4. 再びサイズを拡大
749
- tree.changeSize(15)
750
- expect(tree.getSize()).toBe(15)
751
- // 新しい要素 (インデックス 10-14) は baseValue (19) を持つと仮定される
752
- total += 5 * baseValue
753
- expect(tree.getTotal()).toBe(total)
754
-
755
- // 5. 新しく追加された範囲を具現化・更新
756
- tree.get(12, { materializeOption: { materialize: true } }) // value: 10+12=22, diff: 22-19=+3
757
- total += 3 // 299 + 3 = 302
758
- expect(tree.getTotal()).toBe(total)
759
- tree.update(13, 100) // baseValue(19) -> 100, diff: +81
760
- total += 81 // 302 + 81 = 383
761
- expect(tree.getTotal()).toBe(total)
762
-
763
- // 6. サイズを縮小 (15 -> 7)
764
- // 削除される範囲: [7, 14]
765
- // 削除される更新: index 8 (50), index 13 (100)
766
- // 削除される具現化: index 12 (34)
767
- // 残る範囲: [0, 6]
768
- // 残る具現化: index 0 (10), index 1 (11), index 2 (12), index 3 (13), index 4 (14), index 5 (15), index 6 (16), total = 91
769
- tree.changeSize(7)
770
- expect(tree.getSize()).toBe(7)
771
- expect(tree.getTotal()).toBe(91)
772
-
773
- // 7. 最終確認
774
- expect(tree.get(3)).toBe(13)
775
- expect(tree.get(4)).toBe(14)
776
- expect(tree.get(6)).toBe(16)
777
- })
778
-
779
- it("サイズ変更を繰り返した後に findFirst が正しい値を返すか", () => {
780
- // 1. 初期化
781
- const tree = new FenwickMapTree(10, 10)
782
- expect(tree.getTotal()).toBe(100)
783
-
784
- // 2. 拡大と更新
785
- tree.changeSize(20) // size: 20, total: 200
786
- tree.update(15, 50) // total: 200 - 10 + 50 = 240
787
- expect(tree.getTotal()).toBe(240)
788
-
789
- // 3. 縮小と更新
790
- tree.changeSize(10) // size: 10, total: 100 (update at 15 is lost)
791
- tree.update(5, 5) // total: 100 - 10 + 5 = 95
792
- expect(tree.getTotal()).toBe(95)
793
-
794
- // 4. 再拡大
795
- tree.changeSize(15) // size: 15, total: 95 + 5 * 10 = 145
796
- expect(tree.getTotal()).toBe(145)
797
-
798
- // 5. findIndexAtOrAfter の検証
799
- // 累積和: [10, 20, 30, 40, 50, 55, 65, 75, 85, 95, 105, 115, 125, 135, 145]
800
- // prefixSum(4) = 50
801
- // prefixSum(5) = 55
802
- const result1 = tree.findIndexAtOrAfter(52)
803
- expect(result1.index).toBe(5)
804
- const offset1 = 52 - tree.prefixSum(result1.index - 1).cumulative
805
- expect(offset1).toBe(2) // 52 - 50 = 2
806
- expect(result1.total).toBe(145)
807
-
808
- const result2 = tree.findIndexAtOrAfter(55)
809
- expect(result2.index).toBe(5)
810
- const offset2 = 55 - tree.prefixSum(result2.index - 1).cumulative
811
- expect(offset2).toBe(5) // 55 - 50 = 5
812
- expect(result2.total).toBe(145)
813
-
814
- // prefixSum(9) = 95
815
- // prefixSum(10) = 105
816
- const result3 = tree.findIndexAtOrAfter(100)
817
- expect(result3.index).toBe(10)
818
- const offset3 = 100 - tree.prefixSum(result3.index - 1).cumulative
819
- expect(offset3).toBe(5) // 100 - 95 = 5
820
- expect(result3.total).toBe(145)
821
-
822
- // 範囲外
823
- const result4 = tree.findIndexAtOrAfter(200)
824
- expect(result4.index).toBe(-1)
825
-
826
- // 合計値と一致
827
- const total = tree.getTotal()
828
- const result5 = tree.findIndexAtOrAfter(total)
829
- expect(result5.index).toBe(14) // 最後のインデックス
830
- const offset5 = total - tree.prefixSum(result5.index - 1).cumulative
831
- expect(offset5).toBe(10) // 145 - 135 = 10
832
- expect(result5.total).toBe(145)
833
- })
834
-
835
- it("巨大なサイズでサイズ変更を繰り返しても total が正確であるべき (固定値)", () => {
836
- const initialSize = 100_000_000 // 1億
837
- const value = 10
838
- const tree = new FenwickMapTree(initialSize, value)
839
-
840
- // 1. 初期状態の確認
841
- expect(tree.getSize()).toBe(initialSize)
842
- expect(tree.getTotal()).toBe(initialSize * value)
843
-
844
- // 2. サイズを縮小
845
- const smallerSize = 80_000_000 // 8千万
846
- tree.changeSize(smallerSize)
847
- expect(tree.getSize()).toBe(smallerSize)
848
- expect(tree.getTotal()).toBe(smallerSize * value)
849
-
850
- // 3. 値を更新
851
- tree.update(100, 50) // 元の値は 10, 差分 +40
852
- expect(tree.getTotal()).toBe(smallerSize * value + 40)
853
-
854
- // 4. サイズを拡大
855
- const largerSize = 120_000_000 // 1億2千万
856
- tree.changeSize(largerSize)
857
- expect(tree.getSize()).toBe(largerSize)
858
- // 既存の合計値 + 新しく追加された要素の合計値
859
- // (smallerSize * value + 40) + (largerSize - smallerSize) * value
860
- // = largerSize * value + 40
861
- expect(tree.getTotal()).toBe(largerSize * value + 40)
862
-
863
- // 5. 再度サイズを縮小 (更新した値が含まれるように)
864
- const finalSize = 90_000_000 // 9千万
865
- tree.changeSize(finalSize)
866
- expect(tree.getSize()).toBe(finalSize)
867
- // 更新した値は残る
868
- expect(tree.getTotal()).toBe(finalSize * value + 40)
869
- expect(tree.get(100)).toBe(50)
870
-
871
- // 6. 再度サイズを縮小 (更新した値が削除されるように)
872
- const verySmallSize = 50
873
- tree.changeSize(verySmallSize)
874
- expect(tree.getSize()).toBe(verySmallSize)
875
- // 更新した値は削除される
876
- expect(tree.getTotal()).toBe(verySmallSize * value)
877
- expect(() => tree.get(100)).toThrow()
878
- })
879
-
880
- it("巨大なサイズでサイズ変更を繰り返しても total が正確であるべき (関数)", () => {
881
- const initialSize = 50_000_000 // 5千万
882
- const valueFn = (i: number) => 10 + (i % 20) // 10から29の周期的
883
- // サンプル範囲はデフォルトで [0, 999]
884
- // 値は 10..29, 10..29 ...
885
- // baseValue は (10+29)/2 = 19.5 -> 19
886
- const tree = new FenwickMapTree(initialSize, valueFn)
887
- const baseValue = 19
888
-
889
- // 1. 初期状態の確認
890
- expect(tree.getSize()).toBe(initialSize)
891
- expect(tree.getTotal()).toBe(initialSize * baseValue)
892
-
893
- // 2. サイズを縮小
894
- const smallerSize = 40_000_000 // 4千万
895
- tree.changeSize(smallerSize)
896
- expect(tree.getSize()).toBe(smallerSize)
897
- expect(tree.getTotal()).toBe(smallerSize * baseValue)
898
-
899
- // 3. 値を更新 (未具現化)
900
- const updateIndex1 = 1_000_000
901
- const newValue1 = 100
902
- const originalValue1 = baseValue
903
- const diff1 = newValue1 - originalValue1
904
- tree.update(updateIndex1, newValue1)
905
- expect(tree.getTotal()).toBe(smallerSize * baseValue + diff1)
906
-
907
- // 4. 値を具現化してから更新
908
- const updateIndex2 = 2_000_000
909
- const originalValue2 = valueFn(updateIndex2) // 10 + 0 = 10
910
- const materializedValue2 = tree.get(updateIndex2, { materializeOption: { materialize: true } })
911
- expect(materializedValue2).toBe(originalValue2)
912
- const totalAfterMaterialize = smallerSize * baseValue + diff1 + (originalValue2 - baseValue)
913
-
914
- expect(tree.getTotal()).toBe(totalAfterMaterialize)
915
-
916
- const newValue2 = 200
917
- const diff2 = newValue2 - originalValue2
918
- tree.update(updateIndex2, newValue2)
919
- expect(tree.getTotal()).toBe(totalAfterMaterialize + diff2)
920
-
921
- // 5. サイズを拡大
922
- const largerSize = 60_000_000 // 6千万
923
- const currentTotal = tree.getTotal()
924
- tree.changeSize(largerSize)
925
- expect(tree.getSize()).toBe(largerSize)
926
- const expectedTotalAfterLarge = currentTotal + (largerSize - smallerSize) * baseValue
927
- expect(tree.getTotal()).toBe(expectedTotalAfterLarge)
928
-
929
- // 6. 再度サイズを縮小 (更新した値が削除されるように)
930
- const finalSize = 500_000
931
- tree.changeSize(finalSize)
932
- expect(tree.getSize()).toBe(finalSize)
933
- // 更新はすべて範囲外になる
934
- // total は baseValue * finalSize になるはずだが、
935
- // updateIndex2 の具現化が残っている可能性がある。
936
- // ここでは、具現化済みの値がリセットされないことを前提に、
937
- // 縮小後の total を再計算する。
938
- // updateIndex2 は削除されたので、その具現化の影響は消える。
939
- // よって、baseValue * finalSize になる。
940
- expect(tree.getTotal()).toBe(finalSize * baseValue)
941
- expect(() => tree.get(updateIndex1)).toThrow()
942
- expect(() => tree.get(updateIndex2)).toThrow()
943
- })
944
- })
945
- })
946
-
947
- describe("FenwickMapTree (回帰: 監査所見)", () => {
948
- it("(c) setValueFn に数値を渡すと total が baseValue と deltas に整合する", () => {
949
- const tree = new FenwickMapTree(10, 40) // total = 400
950
- expect(tree.getTotal()).toBe(400)
951
-
952
- tree.setValueFn(50, { reset: false })
953
-
954
- // baseValue が 50 に差し替わり、deltas は空なので total は 500 になる
955
- expect(tree.getTotal()).toBe(500)
956
- expect(tree.prefixSum(9).cumulative).toBe(500)
957
- expect(tree.calculateAccumulatedError()).toBe(0)
958
- })
959
-
960
- it("(c) setValueFn 数値化後も既存 delta を保持しつつ total が整合する", () => {
961
- const tree = new FenwickMapTree(4, 10) // total = 40
962
- tree.update(1, 30) // delta[1] = 20, total = 60
963
-
964
- tree.setValueFn(100, { reset: false })
965
-
966
- // baseValue=100, delta[1]=20 -> value(1)=120, total = 100*4 + 20 = 420
967
- expect(tree.get(1)).toBe(120)
968
- expect(tree.getTotal()).toBe(420)
969
- expect(tree.prefixSum(3).cumulative).toBe(420)
970
- expect(tree.calculateAccumulatedError()).toBe(0)
971
- })
972
-
973
- it("(d) updates で同一 index が重複しても差分が二重適用されない (baseValue=0)", () => {
974
- const tree = new FenwickMapTree(10, 0)
975
-
976
- tree.updates([
977
- { index: 5, value: 10 },
978
- { index: 5, value: 10 },
979
- ])
980
-
981
- expect(tree.get(5)).toBe(10)
982
- expect(tree.getTotal()).toBe(10)
983
- expect(tree.calculateAccumulatedError()).toBe(0)
984
- })
985
-
986
- it("(d) updates で同一 index の異なる値は最後の値が採用される (baseValue=8)", () => {
987
- const tree = new FenwickMapTree(10, 8) // total = 80
988
-
989
- tree.updates([
990
- { index: 5, value: 10 },
991
- { index: 5, value: 12 },
992
- ])
993
-
994
- expect(tree.get(5)).toBe(12)
995
- // 期待 total = 80 - 8 + 12 = 84
996
- expect(tree.getTotal()).toBe(84)
997
- expect(tree.calculateAccumulatedError()).toBe(0)
998
- })
999
-
1000
- it("(f) updateDeltas は範囲外 index があると先行分を適用せず throw する (アトミック)", () => {
1001
- const tree = new FenwickMapTree(10, 0)
1002
-
1003
- expect(() =>
1004
- tree.updateDeltas([
1005
- { index: 0, change: 5 },
1006
- { index: 99, change: 5 },
1007
- ]),
1008
- ).toThrow()
1009
-
1010
- // index 0 への +5 は適用されていないこと
1011
- expect(tree.get(0)).toBe(0)
1012
- expect(tree.getTotal()).toBe(0)
1013
- expect(tree.calculateAccumulatedError()).toBe(0)
1014
- })
1015
-
1016
- it("(g) 空の木 (size=0) の get / prefixSum は throw せず中立値を返す", () => {
1017
- const tree = new FenwickMapTree(0, 10)
1018
-
1019
- expect(() => tree.get(0)).not.toThrow()
1020
- expect(tree.get(0)).toBe(0)
1021
- expect(tree.prefixSum(0).cumulative).toBe(0)
1022
- expect(tree.prefixSum(0).currentValue).toBe(0)
1023
- expect(tree.findIndexAtOrAfter(10).index).toBe(-1)
1024
-
1025
- // 非空の木では範囲外 index は従来どおり throw すること
1026
- const nonEmpty = new FenwickMapTree(3, 10)
1027
- expect(() => nonEmpty.get(5)).toThrow()
1028
- })
1029
-
1030
- it("(e) materialize ranges の from が負でも total が破損しない", () => {
1031
- const tree = new FenwickMapTree(10, (i) => (i === -1 ? 50 : 10), { materialize: false })
1032
-
1033
- // 負の from を含む range を渡しても -1 は具現化されない
1034
- const prefix = tree.prefixSum(9, { materializeOption: { materialize: true, ranges: [{ from: -1, to: 3 }] } })
1035
-
1036
- expect(prefix.cumulative).toBe(100)
1037
- expect(tree.getTotal({ materializeOption: { materialize: true, ranges: [{ from: -1, to: 3 }] } })).toBe(100)
1038
- expect(tree.calculateAccumulatedError()).toBe(0)
1039
- })
1040
-
1041
- it("(L528) findIndexAtOrAfter は具現化 range 指定時に連続呼び出しでも結果が揺れない", () => {
1042
- const tree = new FenwickMapTree(10, (i) => (i === 0 ? 100 : 10))
1043
-
1044
- const opts = { materializeOption: { materialize: true, ranges: [{ from: 0, to: 5 }] } }
1045
- const first = tree.findIndexAtOrAfter(15, opts)
1046
- const second = tree.findIndexAtOrAfter(15, opts)
1047
-
1048
- // 具現化により row0=100 >= 15 なので index 0 が一貫して返る
1049
- expect(first.index).toBe(0)
1050
- expect(first.cumulative).toBe(100)
1051
- expect(second.index).toBe(first.index)
1052
- expect(second.cumulative).toBe(first.cumulative)
1053
- })
1054
-
1055
- it("(b) 末尾伸長 (changeSize 拡大) で既存 delta が新ノードへ正しく伝播する", () => {
1056
- const tree = new FenwickMapTree(4, 10)
1057
- tree.update(0, 50) // delta[0] = 40, total = 80
1058
-
1059
- tree.changeSize(8)
1060
-
1061
- // index0=50, 残り 7 件は baseValue 10
1062
- expect(tree.get(0)).toBe(50)
1063
- expect(tree.get(7)).toBe(10)
1064
- expect(tree.getTotal()).toBe(120)
1065
- expect(tree.prefixSum(7).cumulative).toBe(120)
1066
- expect(tree.calculateAccumulatedError()).toBe(0)
1067
- })
1068
- })
1069
-
1070
- describe("FenwickMapTree (回帰: baseValue 分散サンプリング / 末尾追記)", () => {
1071
- it("先頭が非代表的な高さでも baseValue が多数派に寄り total が実測に近い", () => {
1072
- // 先頭 200px × 101 (ヒーロー/見出し) + 40px × 100,000 (多数派)。
1073
- // 旧実装 (先頭 0..99 の連続サンプリング) では全サンプルが 200 になり baseValue=200、
1074
- // 未具現化の全アイテムを 200px と見積もって total が実測の約 5 倍に膨らんでいた。
1075
- const HERO_COUNT = 101
1076
- const NORMAL_COUNT = 100_000
1077
- const size = HERO_COUNT + NORMAL_COUNT
1078
- const valueFn = (i: number) => (i < HERO_COUNT ? 200 : 40)
1079
- const tree = new FenwickMapTree(size, valueFn)
1080
-
1081
- // baseValue は多数派 (40) 側に決まる。未具現化行は baseValue を返す。
1082
- expect(tree.get(50_000)).toBe(40)
1083
- // 具現化していないので total = baseValue * size = 40 * size
1084
- expect(tree.getTotal()).toBe(40 * size)
1085
-
1086
- // 実測総高さ (200*101 + 40*100000) に対して 1% 以内。
1087
- const actualTotal = 200 * HERO_COUNT + 40 * NORMAL_COUNT
1088
- expect(Math.abs(tree.getTotal() - actualTotal) / actualTotal).toBeLessThan(0.01)
1089
- // 旧実装の推定 (200 * size) からは大きく離れていることも確認 (多数派が採用された証拠)。
1090
- expect(tree.getTotal()).toBeLessThan(200 * size * 0.5)
1091
- })
1092
-
1093
- it("均一高さでは分散サンプリングでも baseValue が不変", () => {
1094
- // 一様リストは全サンプルが同一なので、しきい値超えの大サイズでも推定値は不変。
1095
- const size = 10_000
1096
- const tree = new FenwickMapTree(size, () => 33)
1097
- expect(tree.get(0)).toBe(33)
1098
- expect(tree.get(size - 1)).toBe(33)
1099
- expect(tree.getTotal()).toBe(33 * size)
1100
- })
1101
-
1102
- it("周期的な高さ (20 行周期) でもストライドのエイリアシングで baseValue が偏らない", () => {
1103
- // stride が周期の約数だと 1 剰余類へ潰れて baseValue が偏る。奇数化により回避される。
1104
- const size = 50_000_000
1105
- const valueFn = (i: number) => 10 + (i % 20) // 10..29 の周期
1106
- const tree = new FenwickMapTree(size, valueFn)
1107
- // 全剰余をまたぐため中央値 (=19) に落ち着く。10 や 29 のような端の値には偏らない。
1108
- expect(tree.get(0)).toBe(19)
1109
- expect(tree.getTotal()).toBe(19 * size)
1110
- })
1111
-
1112
- it("末尾追記 (changeSize 拡大) 後もオフセットと total が正確", () => {
1113
- const tree = new FenwickMapTree(4, 10) // 一様 10, total = 40
1114
- tree.update(1, 25) // delta[1] = 15, total = 55
1115
-
1116
- tree.changeSize(6) // 末尾に 2 件を baseValue(10) で追記
1117
-
1118
- expect(tree.getSize()).toBe(6)
1119
- // 既存行の対応が保たれる
1120
- expect(tree.get(1)).toBe(25)
1121
- // 追記された行は baseValue
1122
- expect(tree.get(4)).toBe(10)
1123
- expect(tree.get(5)).toBe(10)
1124
- // total = 55 + 2 * 10 = 75
1125
- expect(tree.getTotal()).toBe(75)
1126
- // 各インデックスまでの累積オフセットが正確
1127
- expect(tree.prefixSum(0).cumulative).toBe(10)
1128
- expect(tree.prefixSum(1).cumulative).toBe(35) // 10 + 25
1129
- expect(tree.prefixSum(3).cumulative).toBe(55) // 10 + 25 + 10 + 10
1130
- expect(tree.prefixSum(5).cumulative).toBe(75)
1131
- expect(tree.calculateAccumulatedError()).toBe(0)
1132
- })
1133
- })
1134
-
1135
- describe("FenwickMapTree (回帰: r3 監査所見)", () => {
1136
- it("小数 baseValue の末尾 +1 追記でも total が prefixSum と厳密一致し整合検査が誤発火しない", () => {
1137
- // 増分 total 更新 (+= baseValue * Δ) は浮動小数点の分配則不成立により
1138
- // prefixSum の走査結果と厳密不一致になり、追記のたびに偽の
1139
- // "Inconsistent Fenwick Tree state" エラーを出していた回帰の再現。
1140
- const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {})
1141
- try {
1142
- // 7 値周期の小数高さ: 各値の頻度 20% 以下のため _modeOrMedian は生の小数中央値を
1143
- // baseValue に採用する (FP 乖離の再現条件)
1144
- const valueFn = (i: number) => 24.7 + (i % 7) * 0.13
1145
- const tree = new FenwickMapTree(200, valueFn)
1146
- for (let size = 201; size <= 260; size++) {
1147
- tree.changeSize(size)
1148
- const last = tree.prefixSum(size - 1)
1149
- // total キャッシュと prefixSum 走査は厳密一致すること
1150
- expect(last.cumulative).toBe(last.total)
1151
- }
1152
- expect(errorSpy).not.toHaveBeenCalled()
1153
- } finally {
1154
- errorSpy.mockRestore()
1155
- }
1156
- })
1157
-
1158
- it("ランダム差分: update と伸長を混在させた木が reset 再構築した木と全点一致する", () => {
1159
- // 決定的な擬似乱数 (LCG) で再現可能にする
1160
- let seed = 123456789
1161
- const rand = () => {
1162
- seed = (seed * 1664525 + 1013904223) >>> 0
1163
- return seed / 0x100000000
1164
- }
1165
- const base = 20
1166
- let size = 120
1167
- const tree = new FenwickMapTree(size, base)
1168
- const expected = new Map<number, number>()
1169
- for (let step = 0; step < 300; step++) {
1170
- if (rand() < 0.5) {
1171
- const index = Math.floor(rand() * size)
1172
- const value = Math.floor(rand() * 50)
1173
- tree.update(index, value)
1174
- expected.set(index, value)
1175
- } else {
1176
- // 1〜8 件の末尾追記 (2 冪境界も自然に跨ぐ)
1177
- size += 1 + Math.floor(rand() * 8)
1178
- tree.changeSize(size)
1179
- }
1180
- }
1181
- // 同じ値を一括適用して再構築した木と全点一致すること
1182
- const rebuilt = new FenwickMapTree(size, base)
1183
- rebuilt.updates([...expected.entries()].map(([index, value]) => ({ index, value })))
1184
- let cumulative = 0
1185
- for (let i = 0; i < size; i++) {
1186
- cumulative += expected.get(i) ?? base
1187
- expect(tree.prefixSum(i).cumulative).toBe(cumulative)
1188
- expect(rebuilt.prefixSum(i).cumulative).toBe(cumulative)
1189
- }
1190
- expect(tree.getTotal()).toBe(cumulative)
1191
- expect(rebuilt.getTotal()).toBe(cumulative)
1192
- expect(tree.calculateAccumulatedError()).toBe(0)
1193
- })
1194
-
1195
- it("2 冪境界を跨ぐ +1 伸長でも既存 delta が新規有効化ノードへ正しく伝播する", () => {
1196
- for (const boundary of [128, 256, 512]) {
1197
- const tree = new FenwickMapTree(boundary - 1, 10)
1198
- tree.update(0, 60) // delta +50
1199
- tree.update(boundary - 2, 35) // delta +25
1200
- tree.changeSize(boundary) // 2 冪ノード (boundary) が新規有効化される
1201
- tree.changeSize(boundary + 1)
1202
- const expectedTotal = 10 * (boundary + 1) + 50 + 25
1203
- expect(tree.prefixSum(boundary).cumulative).toBe(expectedTotal)
1204
- expect(tree.getTotal()).toBe(expectedTotal)
1205
- expect(tree.calculateAccumulatedError()).toBe(0)
1206
- }
1207
- })
1208
-
1209
- it("小リストの reset は負の sampleRange をクランプし、負 index の valueFn 呼び出しと NaN 汚染を起こさない", () => {
1210
- // 配列添字ベースの valueFn (負 index では undefined を返す実運用形)
1211
- const heights = Array.from({ length: 50 }, () => 30)
1212
- const calls: number[] = []
1213
- const valueFn = (i: number) => {
1214
- calls.push(i)
1215
- return heights[i]
1216
- }
1217
- const tree = new FenwickMapTree(50, valueFn, { sampleRange: { from: -5, to: 10 }, materialize: true })
1218
- // 負 index で valueFn が呼ばれないこと
1219
- expect(calls.every((i) => i >= 0 && i < 50)).toBe(true)
1220
- // baseValue / total が NaN 汚染されないこと
1221
- expect(Number.isFinite(tree.getTotal())).toBe(true)
1222
- expect(tree.getTotal()).toBe(50 * 30)
1223
- expect(tree.get(0)).toBe(30)
1224
- expect(tree.calculateAccumulatedError()).toBe(0)
1225
- })
1226
-
1227
- it("findIndexAtOrAfter は materialize:true + ranges でも契約 (cumulative >= target) を満たし連続呼び出しで安定する", () => {
1228
- // 探索後の prefixSum 内強制具現化が木を変異させ、返却済み cumulative が
1229
- // 古い状態のままになる契約違反 (50010 < 50100) の再現条件。
1230
- const valueFn = (i: number) => (i === 500 ? 10 : 100)
1231
- const tree = new FenwickMapTree(1000, valueFn, { sampleRange: { from: 0, to: 99 }, materialize: true })
1232
- const opts = { materializeOption: { materialize: true, ranges: [{ from: 0, to: 99 }] } }
1233
-
1234
- const first = tree.findIndexAtOrAfter(50100, opts)
1235
- const second = tree.findIndexAtOrAfter(50100, opts)
1236
- const third = tree.findIndexAtOrAfter(50100, opts)
1237
-
1238
- // AtOrAfter 契約: cumulative は target 以上
1239
- expect(first.cumulative).toBeGreaterThanOrEqual(50100)
1240
- // 同一引数の連続呼び出しは同一結果 (ドリフトしない)
1241
- expect(second.index).toBe(first.index)
1242
- expect(second.cumulative).toBe(first.cumulative)
1243
- expect(third.index).toBe(first.index)
1244
- expect(third.cumulative).toBe(first.cumulative)
1245
- })
1246
-
1247
- it("findIndexAtOrBefore も materialize:true で契約 (cumulative <= target) を満たし連続呼び出しで安定する", () => {
1248
- const valueFn = (i: number) => (i === 500 ? 10 : 100)
1249
- const tree = new FenwickMapTree(1000, valueFn, { sampleRange: { from: 0, to: 99 }, materialize: true })
1250
- const opts = { materializeOption: { materialize: true, ranges: [{ from: 0, to: 99 }] } }
1251
-
1252
- const first = tree.findIndexAtOrBefore(50100, opts)
1253
- const second = tree.findIndexAtOrBefore(50100, opts)
1254
-
1255
- expect(first.cumulative).toBeLessThanOrEqual(50100)
1256
- expect(second.index).toBe(first.index)
1257
- expect(second.cumulative).toBe(first.cumulative)
1258
- })
1259
-
1260
- it("updates / updateDeltas は非有限値 (NaN/Infinity) を throw で拒否し木を汚染しない", () => {
1261
- const tree = new FenwickMapTree(10, 10) // total = 100
1262
- expect(() => tree.updates([{ index: 1, value: Number.NaN }])).toThrow()
1263
- expect(() => tree.updates([{ index: 1, value: Number.POSITIVE_INFINITY }])).toThrow()
1264
- expect(() => tree.updateDeltas([{ index: 1, change: Number.NaN }])).toThrow()
1265
- expect(() => tree.updateDelta(1, Number.NEGATIVE_INFINITY)).toThrow()
1266
- // 拒否後も木は無傷であること (アトミック性)
1267
- expect(tree.getTotal()).toBe(100)
1268
- expect(tree.prefixSum(9).cumulative).toBe(100)
1269
- expect(tree.get(1)).toBe(10)
1270
- expect(tree.calculateAccumulatedError()).toBe(0)
1271
- })
1272
-
1273
- it("updates は途中に NaN があると先行分も適用しない (アトミック)", () => {
1274
- const tree = new FenwickMapTree(10, 10) // total = 100
1275
- expect(() =>
1276
- tree.updates([
1277
- { index: 0, value: 50 },
1278
- { index: 1, value: Number.NaN },
1279
- ]),
1280
- ).toThrow()
1281
- expect(tree.get(0)).toBe(10)
1282
- expect(tree.getTotal()).toBe(100)
1283
- })
1284
- })
1285
-
1286
- describe("FenwickMapTree (回帰: r4 監査所見)", () => {
1287
- it("100 < size < 200 でも後半の多数派が baseValue に採用される (stride=1 盲点の回帰)", () => {
1288
- // 旧実装は stride = floor(size / 100) が size=101..199 で 1 になり (奇数のため +1 補正も
1289
- // 効かない)、先頭 100 件しかサンプリングせず後半の多数派を見落としていた。
1290
- // ceil 化により必ず全域を跨ぐことを size = 101 / 150 / 199 で検証する。
1291
-
1292
- // size=199: 先頭 99 件が 200px、後半 100 件が 40px (多数派は後半)
1293
- const tree199 = new FenwickMapTree(199, (i) => (i < 99 ? 200 : 40))
1294
- expect(tree199.get(150)).toBe(40) // 未具現化行は baseValue を返す
1295
- expect(tree199.getTotal()).toBe(40 * 199)
1296
-
1297
- // size=150: 先頭 50 件が 200px、後半 100 件が 40px
1298
- const tree150 = new FenwickMapTree(150, (i) => (i < 50 ? 200 : 40))
1299
- expect(tree150.get(100)).toBe(40)
1300
- expect(tree150.getTotal()).toBe(40 * 150)
1301
-
1302
- // size=101: 先頭 40 件が 200px、後半 61 件が 40px
1303
- const tree101 = new FenwickMapTree(101, (i) => (i < 40 ? 200 : 40))
1304
- expect(tree101.get(100)).toBe(40)
1305
- expect(tree101.getTotal()).toBe(40 * 101)
1306
- })
1307
-
1308
- it("具現化経路 (get/prefixSum の materialize) は valueFn の非有限値を throw で拒否し木を汚染しない", () => {
1309
- // index 250 だけ NaN を返す valueFn (250 は stride=3 のサンプル点に含まれない)
1310
- const tree = new FenwickMapTree(300, (i) => (i === 250 ? Number.NaN : 30))
1311
- // サンプリングは NaN に当たらず baseValue = 30 が確立する
1312
- expect(tree.get(250)).toBe(30) // 未具現化なら baseValue
1313
- expect(tree.getTotal()).toBe(30 * 300)
1314
-
1315
- // NaN 行の具現化は変異前に throw する (update 系と対称)
1316
- expect(() => tree.get(250, { materializeOption: { materialize: true } })).toThrow()
1317
- expect(() => tree.prefixSum(299, { materializeOption: { materialize: true, ranges: [{ from: 240, to: 260 }] } })).toThrow()
1318
-
1319
- // 拒否後も木は無傷 (total / prefixSum が NaN 汚染されない)
1320
- expect(tree.getTotal()).toBe(30 * 300)
1321
- expect(tree.prefixSum(299).cumulative).toBe(30 * 300)
1322
- expect(tree.calculateAccumulatedError()).toBe(0)
1323
- })
1324
-
1325
- it("reset のサンプリングで valueFn が非有限値を返すと throw し、木は直前の状態を無傷に保つ (半構築を残さない)", () => {
1326
- const tree = new FenwickMapTree(10, 10)
1327
- tree.update(3, 25) // total = 115
1328
-
1329
- // 小リストパス (サンプル範囲の検証で throw)
1330
- expect(() => tree.reset(5, () => Number.NaN)).toThrow()
1331
- // 大リストパス (ストライドサンプリングの検証で throw)
1332
- expect(() => tree.reset(1000, () => Number.POSITIVE_INFINITY)).toThrow()
1333
-
1334
- // 旧状態がそのまま残ること
1335
- expect(tree.getSize()).toBe(10)
1336
- expect(tree.getTotal()).toBe(115)
1337
- expect(tree.get(3)).toBe(25)
1338
- expect(tree.calculateAccumulatedError()).toBe(0)
1339
- })
1340
-
1341
- it("updateDelta / updateDeltas は適用後の実効値が負になる change を throw で拒否する", () => {
1342
- const tree = new FenwickMapTree(10, 10) // baseValue = 10, total = 100
1343
-
1344
- // 単発で実効値が負になる change
1345
- expect(() => tree.updateDelta(2, -30)).toThrow()
1346
- // バッチ内累積で負になる change (各単体では実効値 4 だが累積で -2)
1347
- expect(() =>
1348
- tree.updateDeltas([
1349
- { index: 3, change: -6 },
1350
- { index: 3, change: -6 },
1351
- ]),
1352
- ).toThrow()
1353
-
1354
- // 拒否後も木は無傷であること (アトミック性)
1355
- expect(tree.getTotal()).toBe(100)
1356
- expect(tree.get(2)).toBe(10)
1357
- expect(tree.get(3)).toBe(10)
1358
-
1359
- // 実効値 0 ちょうどまでは許容される (updates の value=0 許容と対称)
1360
- tree.updateDelta(2, -10)
1361
- expect(tree.get(2)).toBe(0)
1362
- expect(tree.getTotal()).toBe(90)
1363
-
1364
- // 累積和の単調性が保たれ、探索契約が壊れないこと
1365
- // 累積和: [10, 20, 20, 30, ...]
1366
- expect(tree.findIndexAtOrAfter(15).index).toBe(1)
1367
- expect(tree.findIndexAtOrBefore(15).index).toBe(0)
1368
- })
1369
-
1370
- it("update 系は小数 / NaN の index を throw で拒否し、get / prefixSum も不正 index で木を汚染しない", () => {
1371
- const tree = new FenwickMapTree(10, 10)
1372
-
1373
- // 書き込み経路: 小数/NaN index は throw (NaN は範囲比較を素通りするため明示検証)
1374
- expect(() => tree.update(2.5, 20)).toThrow("Index must be an integer")
1375
- expect(() => tree.updateDelta(Number.NaN, 5)).toThrow("Index must be an integer")
1376
-
1377
- // get: 小数/NaN index は範囲外と同様に throw
1378
- expect(() => tree.get(2.5)).toThrow("Index out of bounds")
1379
- expect(() => tree.get(Number.NaN)).toThrow("Index out of bounds")
1380
-
1381
- // prefixSum: 小数は切り捨て、NaN は負 index と同じ中立値へ縮退 (具現化キーを作らない)
1382
- expect(tree.prefixSum(2.5, { materializeOption: { materialize: true } }).safeIndex).toBe(2)
1383
- expect(tree.prefixSum(2.5).cumulative).toBe(30)
1384
- expect(tree.prefixSum(Number.NaN).cumulative).toBe(0)
1385
- expect(tree.prefixSum(Number.NaN).safeIndex).toBe(0)
1386
-
1387
- // 木は無傷であること
1388
- expect(tree.getTotal()).toBe(100)
1389
- expect(tree.prefixSum(9).cumulative).toBe(100)
1390
- expect(tree.calculateAccumulatedError()).toBe(0)
1391
- })
1392
-
1393
- it("changeSize は非有限サイズを throw で拒否し、小数は切り捨て・負値は 0 に縮退する", () => {
1394
- const tree = new FenwickMapTree(10, 10)
1395
-
1396
- expect(() => tree.changeSize(Number.NaN)).toThrow()
1397
- expect(() => tree.changeSize(Number.POSITIVE_INFINITY)).toThrow()
1398
- // 拒否後も木は無傷
1399
- expect(tree.getSize()).toBe(10)
1400
- expect(tree.getTotal()).toBe(100)
1401
-
1402
- // 小数は切り捨てて末尾伸長
1403
- tree.changeSize(12.9)
1404
- expect(tree.getSize()).toBe(12)
1405
- expect(tree.getTotal()).toBe(120)
1406
-
1407
- // 負値は 0 に縮退 (reset の正規化と同一規則)
1408
- tree.changeSize(-3)
1409
- expect(tree.getSize()).toBe(0)
1410
- expect(tree.getTotal()).toBe(0)
1411
- })
1412
-
1413
- it("コンストラクタ / reset / フックは NaN・小数の size を安全に正規化する", () => {
1414
- // コンストラクタ: NaN size は 0 に縮退
1415
- const tree = new FenwickMapTree(Number.NaN, 10)
1416
- expect(tree.getSize()).toBe(0)
1417
- expect(tree.getTotal()).toBe(0)
1418
-
1419
- // reset: 小数 size は切り捨て
1420
- tree.reset(7.9, 10)
1421
- expect(tree.getSize()).toBe(7)
1422
- expect(tree.getTotal()).toBe(70)
1423
-
1424
- // フック: Math.max(0, NaN) = NaN の素通りが遮断される
1425
- const { result } = renderHook(() => useFenwickMapTree(Number.NaN, 10))
1426
- expect(result.current.getSize()).toBe(0)
1427
- expect(result.current.getTotal()).toBe(0)
1428
- })
1429
-
1430
- it("小リスト reset で from > to の逆転 sampleRange は既定範囲へフォールバックし baseValue=0 に縮退しない", () => {
1431
- const tree = new FenwickMapTree(20, () => 30, { sampleRange: { from: 10, to: 5 } })
1432
- expect(tree.get(0)).toBe(30)
1433
- expect(tree.getTotal()).toBe(20 * 30)
1434
- })
1435
- })
1436
-
1437
- describe("useFenwickMapTree", () => {
1438
- it("props が変わらない限りインスタンスは安定しているべき", () => {
1439
- const { result, rerender } = renderHook<FenwickMapTree, { size: number; valueOrFn: number | ((i: number) => number) }>(({ size, valueOrFn }) => useFenwickMapTree(size, valueOrFn), {
1440
- initialProps: { size: 10, valueOrFn: 20 },
1441
- })
1442
- const firstInstance = result.current
1443
- rerender({ size: 10, valueOrFn: 20 })
1444
- expect(result.current).toBe(firstInstance)
1445
- })
1446
-
1447
- it("size が変わっても同じインスタンスになる(安定性保証)", () => {
1448
- const { result, rerender } = renderHook<FenwickMapTree, { size: number; valueOrFn: number | ((i: number) => number) }>(({ size, valueOrFn }) => useFenwickMapTree(size, valueOrFn), {
1449
- initialProps: { size: 10, valueOrFn: 20 },
1450
- })
1451
- const firstInstance = result.current
1452
- rerender({ size: 20, valueOrFn: 20 })
1453
- expect(result.current).toBe(firstInstance)
1454
- expect(result.current.getSize()).toBe(20)
1455
- })
1456
-
1457
- it("valueOrFn が変わっても同じインスタンスになる(安定性保証)", () => {
1458
- const { result, rerender } = renderHook<FenwickMapTree, { size: number; valueOrFn: number | ((i: number) => number) }>(({ size, valueOrFn }) => useFenwickMapTree(size, valueOrFn), {
1459
- initialProps: { size: 10, valueOrFn: 20 },
1460
- })
1461
- const firstInstance = result.current
1462
- const newValueFn = (i: number) => i
1463
- rerender({ size: 10, valueOrFn: newValueFn })
1464
- expect(result.current).toBe(firstInstance)
1465
- })
1466
-
1467
- it("フックから返されたツリーが正しく動作するべき", () => {
1468
- const { result } = renderHook(() => useFenwickMapTree(5, 10))
1469
- const tree = result.current
1470
-
1471
- expect(tree.getTotal()).toBe(50)
1472
-
1473
- act(() => {
1474
- tree.update(2, 20)
1475
- })
1476
-
1477
- expect(tree.get(2)).toBe(20)
1478
- expect(tree.getTotal()).toBe(60)
1479
- })
1480
-
1481
- it("resetOnValueFnChange=false でも size 0 -> N と valueFn 変更の同時発生で total が確立する (回帰: r3 監査所見)", () => {
1482
- // 「itemCount=0 でマウント -> データ到着で itemCount>0 + getItemHeight 新識別子」の
1483
- // ローディングフロー。旧実装は baseValue=0 のまま増分伸長し total=0 で固着していた。
1484
- const options = { resetOnValueFnChange: false }
1485
- const fn1 = (_i: number) => 30
1486
- const fn2 = (_i: number) => 30
1487
- const { result, rerender } = renderHook<FenwickMapTree, { size: number; valueOrFn: number | ((i: number) => number) }>(({ size, valueOrFn }) => useFenwickMapTree(size, valueOrFn, options), {
1488
- initialProps: { size: 0, valueOrFn: fn1 },
1489
- })
1490
- expect(result.current.getTotal()).toBe(0)
1491
-
1492
- // size 0 -> 200 と valueFn の識別子変更が同一レンダーで発生
1493
- rerender({ size: 200, valueOrFn: fn2 })
1494
-
1495
- expect(result.current.getSize()).toBe(200)
1496
- // reset に倒れて baseValue がサンプリングされ、total が確立すること
1497
- expect(result.current.getTotal()).toBe(200 * 30)
1498
- expect(result.current.get(5)).toBe(30)
1499
- })
1500
-
1501
- it("resetOnValueFnChange=false で size 0 のまま valueFn が変わっても安全 (0 -> N reset の前提統一)", () => {
1502
- const options = { resetOnValueFnChange: false }
1503
- const fn1 = (_i: number) => 30
1504
- const fn2 = (_i: number) => 40
1505
- const fn3 = (_i: number) => 40
1506
- const { result, rerender } = renderHook<FenwickMapTree, { size: number; valueOrFn: number | ((i: number) => number) }>(({ size, valueOrFn }) => useFenwickMapTree(size, valueOrFn, options), {
1507
- initialProps: { size: 0, valueOrFn: fn1 },
1508
- })
1509
-
1510
- // size 0 のまま valueFn だけ変更 (伸長なし) -> total 0 のままで正しい
1511
- rerender({ size: 0, valueOrFn: fn2 })
1512
- expect(result.current.getTotal()).toBe(0)
1513
-
1514
- // その後の 0 -> N 伸長 (valueFn も新識別子) で total が確立すること
1515
- rerender({ size: 100, valueOrFn: fn3 })
1516
- expect(result.current.getTotal()).toBe(100 * 40)
1517
- })
1518
- })