@aiquants/virtualscroll 1.18.5 → 1.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -13
- package/dist/ScrollPane.d.cts +2 -0
- package/dist/ScrollPane.d.ts +2 -0
- package/dist/ScrollPane.d.ts.map +1 -1
- package/dist/VirtualScroll.d.cts +2 -0
- package/dist/VirtualScroll.d.ts +2 -0
- package/dist/VirtualScroll.d.ts.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1115 -1112
- package/dist/styles/virtualscroll.css +1 -1
- package/dist/styles/virtualscroll.standalone.css +3 -0
- package/package.json +6 -4
- package/src/ScrollBar.spec.tsx +620 -0
- package/src/ScrollBar.tsx +1397 -0
- package/src/ScrollPane.spec.tsx +482 -0
- package/src/ScrollPane.tsx +913 -0
- package/src/TapScrollCircle.spec.tsx +275 -0
- package/src/TapScrollCircle.tsx +363 -0
- package/src/VirtualScroll.spec.ts +623 -0
- package/src/VirtualScroll.tsx +1891 -0
- package/src/cli.server.spec.ts +137 -0
- package/src/cli.server.ts +110 -0
- package/src/index.ts +24 -0
- package/src/logger.spec.ts +128 -0
- package/src/logger.ts +229 -0
- package/src/styles/components.entry.css +7 -0
- package/src/styles/standalone.entry.css +11 -0
- package/src/styles/virtualscroll.css +298 -0
- package/src/tapScrollCircleSampleVisual.tsx +74 -0
- package/src/useFenwickMapTree.huge.spec.ts +388 -0
- package/src/useFenwickMapTree.spec.ts +1518 -0
- package/src/useFenwickMapTree.ts +1368 -0
- package/src/useHeightCache.ts +32 -0
- package/src/useLruCache.spec.ts +382 -0
- package/src/useLruCache.ts +301 -0
- package/src/utils.spec.ts +39 -0
- package/src/utils.ts +16 -0
|
@@ -0,0 +1,1518 @@
|
|
|
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
|
+
})
|