@aiquants/virtualscroll 1.20.0 → 1.22.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +73 -0
- package/README.md +5 -3
- package/dist/ScrollBar.d.cts +123 -0
- package/dist/ScrollBar.d.ts +123 -0
- package/dist/ScrollBar.d.ts.map +1 -1
- package/dist/ScrollPane.d.cts +16 -0
- package/dist/ScrollPane.d.ts +16 -0
- package/dist/ScrollPane.d.ts.map +1 -1
- package/dist/TapScrollCircle.d.ts.map +1 -1
- package/dist/VirtualScroll.d.cts +9 -0
- package/dist/VirtualScroll.d.ts +9 -0
- package/dist/VirtualScroll.d.ts.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1558 -1436
- package/dist/styles/virtualscroll.css +1 -1
- package/dist/styles/virtualscroll.standalone.css +1 -1
- package/dist/tapScrollCircleSampleVisual.d.ts.map +1 -1
- package/dist/useFenwickMapTree.d.cts +43 -0
- package/dist/useFenwickMapTree.d.ts +43 -0
- package/dist/useFenwickMapTree.d.ts.map +1 -1
- package/package.json +4 -1
- package/src/ScrollBar.tsx +218 -33
- package/src/ScrollPane.tsx +366 -24
- package/src/TapScrollCircle.tsx +10 -0
- package/src/VirtualScroll.tsx +11 -1
- package/src/index.ts +1 -1
- package/src/styles/virtualscroll.css +3 -3
- package/src/tapScrollCircleSampleVisual.tsx +22 -5
- package/src/useFenwickMapTree.ts +95 -13
- package/src/ScrollBar.spec.tsx +0 -620
- package/src/ScrollPane.spec.tsx +0 -496
- package/src/TapScrollCircle.spec.tsx +0 -275
- package/src/VirtualScroll.spec.ts +0 -641
- package/src/cli.server.spec.ts +0 -137
- package/src/logger.spec.ts +0 -128
- package/src/useFenwickMapTree.huge.spec.ts +0 -388
- package/src/useFenwickMapTree.spec.ts +0 -1518
- package/src/useLruCache.spec.ts +0 -382
- package/src/utils.spec.ts +0 -39
package/src/cli.server.spec.ts
DELETED
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Regression tests for the CLI entry point: usage guidance must always be
|
|
3
|
-
* printed via console (never suppressed by the Logger's default WARN level),
|
|
4
|
-
* and importing the module must not trigger the CLI.
|
|
5
|
-
* CLI エントリポイントの回帰テスト。usage 案内が Logger の既定レベル (WARN) に
|
|
6
|
-
* 抑制されず常時 console 表示されること、および import 時に CLI が起動しないことの検証。
|
|
7
|
-
*/
|
|
8
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
|
|
9
|
-
|
|
10
|
-
// spawn / existsSync をモックし、実プロセス起動・実ファイル参照なしで CLI 分岐を検証する
|
|
11
|
-
vi.mock("node:child_process", async (importOriginal) => {
|
|
12
|
-
const actual = await importOriginal<typeof import("node:child_process")>()
|
|
13
|
-
const spawnMock = vi.fn(() => ({ on: vi.fn() }))
|
|
14
|
-
return {
|
|
15
|
-
...actual,
|
|
16
|
-
default: { ...actual, spawn: spawnMock },
|
|
17
|
-
spawn: spawnMock,
|
|
18
|
-
}
|
|
19
|
-
})
|
|
20
|
-
vi.mock("node:fs", async (importOriginal) => {
|
|
21
|
-
const actual = await importOriginal<typeof import("node:fs")>()
|
|
22
|
-
// interop で default 経由の名前解決が行われるため、default 側にも同一モックを載せる
|
|
23
|
-
const existsSyncMock = vi.fn(() => true)
|
|
24
|
-
return {
|
|
25
|
-
...actual,
|
|
26
|
-
default: { ...actual, existsSync: existsSyncMock },
|
|
27
|
-
existsSync: existsSyncMock,
|
|
28
|
-
}
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
import { spawn } from "node:child_process"
|
|
32
|
-
import { existsSync } from "node:fs"
|
|
33
|
-
import { runCli } from "./cli.server"
|
|
34
|
-
|
|
35
|
-
const USAGE_MESSAGE = "Usage: npx @aiquants/virtualscroll demo"
|
|
36
|
-
|
|
37
|
-
describe("cli.server", () => {
|
|
38
|
-
let consoleErrorSpy: ReturnType<typeof vi.spyOn>
|
|
39
|
-
let exitSpy: ReturnType<typeof vi.spyOn>
|
|
40
|
-
|
|
41
|
-
beforeEach(() => {
|
|
42
|
-
// console.error を捕捉して usage 案内の常時表示を検証する
|
|
43
|
-
consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {})
|
|
44
|
-
// process.exit は throw に差し替え、以降のコードへ進まない実挙動を再現する
|
|
45
|
-
exitSpy = vi.spyOn(process, "exit").mockImplementation(((code?: number) => {
|
|
46
|
-
throw new Error(`process.exit(${code})`)
|
|
47
|
-
}) as never)
|
|
48
|
-
vi.mocked(spawn).mockClear()
|
|
49
|
-
vi.mocked(existsSync).mockClear()
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
afterEach(() => {
|
|
53
|
-
consoleErrorSpy.mockRestore()
|
|
54
|
-
exitSpy.mockRestore()
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
it("prints unknown command and usage to console.error and exits 1", () => {
|
|
58
|
-
// 未知コマンドではコマンド名 + usage が console.error 直で表示される (Logger レベル非依存)
|
|
59
|
-
expect(() => runCli(["foo"])).toThrow("process.exit(1)")
|
|
60
|
-
const messages = consoleErrorSpy.mock.calls.map((call) => String(call[0]))
|
|
61
|
-
expect(messages).toContain("Unknown command: foo")
|
|
62
|
-
expect(messages).toContain(USAGE_MESSAGE)
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
it("prints usage without 'Unknown command: undefined' when no args are given", () => {
|
|
66
|
-
// 引数なしでは undefined を埋め込んだ Unknown command 行を出さず usage のみ表示する
|
|
67
|
-
expect(() => runCli([])).toThrow("process.exit(1)")
|
|
68
|
-
const messages = consoleErrorSpy.mock.calls.map((call) => String(call[0]))
|
|
69
|
-
expect(messages).toContain(USAGE_MESSAGE)
|
|
70
|
-
expect(messages.some((message) => message.includes("Unknown command"))).toBe(false)
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
it("prints source-checkout guidance via console.error and exits 1 when demo dir is missing", () => {
|
|
74
|
-
// demo ディレクトリ不在 (published tarball 相当) では案内を常時表示して exit 1 する
|
|
75
|
-
vi.mocked(existsSync).mockReturnValue(false)
|
|
76
|
-
expect(() => runCli(["demo"])).toThrow("process.exit(1)")
|
|
77
|
-
const messages = consoleErrorSpy.mock.calls.map((call) => String(call[0]))
|
|
78
|
-
expect(messages.some((message) => message.includes("only available in a source checkout"))).toBe(true)
|
|
79
|
-
expect(vi.mocked(spawn)).not.toHaveBeenCalled()
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
it("announces startup and spawns the demo server when demo dir exists", () => {
|
|
83
|
-
// demo ディレクトリが存在する場合は起動メッセージを常時表示し pnpm run dev を spawn する
|
|
84
|
-
vi.mocked(existsSync).mockReturnValue(true)
|
|
85
|
-
runCli(["demo"])
|
|
86
|
-
const messages = consoleErrorSpy.mock.calls.map((call) => String(call[0]))
|
|
87
|
-
expect(messages).toContain("Starting demo server...")
|
|
88
|
-
expect(vi.mocked(spawn)).toHaveBeenCalledWith("pnpm", ["run", "dev"], expect.objectContaining({ stdio: "inherit" }))
|
|
89
|
-
expect(exitSpy).not.toHaveBeenCalled()
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
it("propagates a non-zero child exit code to process.exitCode via the close handler", () => {
|
|
93
|
-
// 子プロセスが非ゼロ終了したら親の exitCode に伝播する (シェル連結/CI で失敗が成功扱いにならない)
|
|
94
|
-
const previousExitCode = process.exitCode
|
|
95
|
-
try {
|
|
96
|
-
vi.mocked(existsSync).mockReturnValue(true)
|
|
97
|
-
runCli(["demo"])
|
|
98
|
-
const onMock = vi.mocked(spawn).mock.results[0]?.value.on as ReturnType<typeof vi.fn>
|
|
99
|
-
const closeHandler = onMock.mock.calls.find((call) => call[0] === "close")?.[1] as (code: number | null) => void
|
|
100
|
-
expect(closeHandler).toBeTypeOf("function")
|
|
101
|
-
closeHandler(3)
|
|
102
|
-
expect(process.exitCode).toBe(3)
|
|
103
|
-
// code=null (シグナル終了等) は汎用の 1 にフォールバックする
|
|
104
|
-
process.exitCode = undefined
|
|
105
|
-
closeHandler(null)
|
|
106
|
-
expect(process.exitCode).toBe(1)
|
|
107
|
-
// 正常終了 (code=0) では exitCode を汚染しない
|
|
108
|
-
process.exitCode = undefined
|
|
109
|
-
closeHandler(0)
|
|
110
|
-
expect(process.exitCode).toBeUndefined()
|
|
111
|
-
} finally {
|
|
112
|
-
process.exitCode = previousExitCode
|
|
113
|
-
}
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
it("sets process.exitCode to 1 when the child emits an error (spawn failure)", () => {
|
|
117
|
-
// spawn 自体の失敗 (コマンド不在等) も非ゼロ終了として親へ伝播する
|
|
118
|
-
const previousExitCode = process.exitCode
|
|
119
|
-
try {
|
|
120
|
-
vi.mocked(existsSync).mockReturnValue(true)
|
|
121
|
-
runCli(["demo"])
|
|
122
|
-
const onMock = vi.mocked(spawn).mock.results[0]?.value.on as ReturnType<typeof vi.fn>
|
|
123
|
-
const errorHandler = onMock.mock.calls.find((call) => call[0] === "error")?.[1] as (err: Error) => void
|
|
124
|
-
expect(errorHandler).toBeTypeOf("function")
|
|
125
|
-
errorHandler(new Error("spawn pnpm ENOENT"))
|
|
126
|
-
expect(process.exitCode).toBe(1)
|
|
127
|
-
} finally {
|
|
128
|
-
process.exitCode = previousExitCode
|
|
129
|
-
}
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
it("does not run the CLI on import (direct-run guard)", () => {
|
|
133
|
-
// モジュール import 自体では spawn も exit も発生していない (トップレベルガードの検証)
|
|
134
|
-
// 上記テスト以外での呼び出しが無いことは各テストの mockClear + アサーションで担保される
|
|
135
|
-
expect(vi.mocked(spawn)).not.toHaveBeenCalled()
|
|
136
|
-
})
|
|
137
|
-
})
|
package/src/logger.spec.ts
DELETED
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Tests for Logger.
|
|
3
|
-
* Logger のテスト。
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { beforeEach, describe, expect, it, vi } from "vitest"
|
|
7
|
-
import { type ILogger, Logger, LogLevel } from "./logger"
|
|
8
|
-
|
|
9
|
-
const createImpl = (): ILogger => ({
|
|
10
|
-
debug: vi.fn(),
|
|
11
|
-
info: vi.fn(),
|
|
12
|
-
warn: vi.fn(),
|
|
13
|
-
error: vi.fn(),
|
|
14
|
-
})
|
|
15
|
-
|
|
16
|
-
describe("Logger", () => {
|
|
17
|
-
let impl: ILogger
|
|
18
|
-
|
|
19
|
-
beforeEach(() => {
|
|
20
|
-
impl = createImpl()
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
it("レベル抑制時は impl を呼ばない", () => {
|
|
24
|
-
const logger = new Logger(LogLevel.WARN, "[t]", impl)
|
|
25
|
-
logger.debug("hidden")
|
|
26
|
-
logger.info("hidden")
|
|
27
|
-
expect(impl.debug).not.toHaveBeenCalled()
|
|
28
|
-
expect(impl.info).not.toHaveBeenCalled()
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
it("有効レベルでは prefix 付きで出力する(後方互換)", () => {
|
|
32
|
-
const logger = new Logger(LogLevel.DEBUG, "[t]", impl)
|
|
33
|
-
logger.debug("hello", 1, 2)
|
|
34
|
-
expect(impl.debug).toHaveBeenCalledWith("[t] hello", 1, 2)
|
|
35
|
-
|
|
36
|
-
logger.warn({ a: 1 })
|
|
37
|
-
expect(impl.warn).toHaveBeenCalledWith("[t]", { a: 1 })
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
describe("isEnabled", () => {
|
|
41
|
-
it("現在のレベルで出力されるレベルのみ true", () => {
|
|
42
|
-
const logger = new Logger(LogLevel.WARN, "[t]", impl)
|
|
43
|
-
expect(logger.isEnabled(LogLevel.DEBUG)).toBe(false)
|
|
44
|
-
expect(logger.isEnabled(LogLevel.INFO)).toBe(false)
|
|
45
|
-
expect(logger.isEnabled(LogLevel.WARN)).toBe(true)
|
|
46
|
-
expect(logger.isEnabled(LogLevel.ERROR)).toBe(true)
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
it("静的インスタンスにも isEnabled がある", () => {
|
|
50
|
-
Logger.setLevel(LogLevel.ERROR)
|
|
51
|
-
expect(Logger.isEnabled(LogLevel.WARN)).toBe(false)
|
|
52
|
-
expect(Logger.isEnabled(LogLevel.ERROR)).toBe(true)
|
|
53
|
-
Logger.setLevel(LogLevel.WARN)
|
|
54
|
-
})
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
describe("遅延評価(サンク)引数", () => {
|
|
58
|
-
it("抑制時はサンクを一切呼ばない(オブジェクト生成/DOM 読み取りを回避)", () => {
|
|
59
|
-
const logger = new Logger(LogLevel.WARN, "[t]", impl)
|
|
60
|
-
const messageThunk = vi.fn(() => "expensive message")
|
|
61
|
-
const paramThunk = vi.fn(() => ({ heavy: true }))
|
|
62
|
-
|
|
63
|
-
logger.debug(messageThunk, paramThunk)
|
|
64
|
-
|
|
65
|
-
expect(messageThunk).not.toHaveBeenCalled()
|
|
66
|
-
expect(paramThunk).not.toHaveBeenCalled()
|
|
67
|
-
expect(impl.debug).not.toHaveBeenCalled()
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
it("有効時はサンクを解決して出力する", () => {
|
|
71
|
-
const logger = new Logger(LogLevel.DEBUG, "[t]", impl)
|
|
72
|
-
const messageThunk = vi.fn(() => "expensive message")
|
|
73
|
-
const paramThunk = vi.fn(() => ({ heavy: true }))
|
|
74
|
-
|
|
75
|
-
logger.debug(messageThunk, paramThunk)
|
|
76
|
-
|
|
77
|
-
expect(messageThunk).toHaveBeenCalledTimes(1)
|
|
78
|
-
expect(paramThunk).toHaveBeenCalledTimes(1)
|
|
79
|
-
expect(impl.debug).toHaveBeenCalledWith("[t] expensive message", { heavy: true })
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
it("非関数値はそのまま素通し(後方互換)", () => {
|
|
83
|
-
const logger = new Logger(LogLevel.INFO, "[t]", impl)
|
|
84
|
-
logger.info("plain", { a: 1 }, 3)
|
|
85
|
-
expect(impl.info).toHaveBeenCalledWith("[t] plain", { a: 1 }, 3)
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
it("class を渡してもクラッシュせず、元のクラス値をそのまま出力する", () => {
|
|
89
|
-
const logger = new Logger(LogLevel.ERROR, "[t]", impl)
|
|
90
|
-
class Foo {}
|
|
91
|
-
|
|
92
|
-
// class constructor は `new` なしで呼べないが、logger 呼び出しは throw しない
|
|
93
|
-
expect(() => logger.error("failed with handler:", Foo)).not.toThrow()
|
|
94
|
-
expect(impl.error).toHaveBeenCalledWith("[t] failed with handler:", Foo)
|
|
95
|
-
|
|
96
|
-
// message 位置に class を渡してもクラッシュしない
|
|
97
|
-
expect(() => logger.error(Foo)).not.toThrow()
|
|
98
|
-
expect(impl.error).toHaveBeenCalledWith("[t]", Foo)
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
it("thunk が throw しても例外を伝播させず、元の関数値を出力する", () => {
|
|
102
|
-
const logger = new Logger(LogLevel.DEBUG, "[t]", impl)
|
|
103
|
-
const badThunk = () => {
|
|
104
|
-
throw new Error("boom in thunk")
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// レベル有効時でもログ呼び出しは呼び出し元に例外を伝播させない
|
|
108
|
-
expect(() => logger.debug("state:", badThunk)).not.toThrow()
|
|
109
|
-
// 握り潰しではなく、元の関数値がログに出力される
|
|
110
|
-
expect(impl.debug).toHaveBeenCalledWith("[t] state:", badThunk)
|
|
111
|
-
|
|
112
|
-
// message 位置の thunk が throw するケースも同様
|
|
113
|
-
expect(() => logger.debug(badThunk)).not.toThrow()
|
|
114
|
-
expect(impl.debug).toHaveBeenCalledWith("[t]", badThunk)
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
it("抑制レベルでは throw する thunk も一切評価されない", () => {
|
|
118
|
-
const logger = new Logger(LogLevel.WARN, "[t]", impl)
|
|
119
|
-
const badThunk = vi.fn(() => {
|
|
120
|
-
throw new Error("boom in thunk")
|
|
121
|
-
})
|
|
122
|
-
|
|
123
|
-
expect(() => logger.debug(badThunk)).not.toThrow()
|
|
124
|
-
expect(badThunk).not.toHaveBeenCalled()
|
|
125
|
-
expect(impl.debug).not.toHaveBeenCalled()
|
|
126
|
-
})
|
|
127
|
-
})
|
|
128
|
-
})
|
|
@@ -1,388 +0,0 @@
|
|
|
1
|
-
import { act, renderHook } from "@testing-library/react"
|
|
2
|
-
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"
|
|
3
|
-
import { FenwickMapTree, useFenwickMapTree } from "./useFenwickMapTree.ts"
|
|
4
|
-
|
|
5
|
-
let warnSpy: ReturnType<typeof vi.spyOn> | undefined
|
|
6
|
-
|
|
7
|
-
beforeAll(() => {
|
|
8
|
-
warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {})
|
|
9
|
-
})
|
|
10
|
-
|
|
11
|
-
afterAll(() => {
|
|
12
|
-
warnSpy?.mockRestore()
|
|
13
|
-
warnSpy = undefined
|
|
14
|
-
})
|
|
15
|
-
|
|
16
|
-
const MAX_SAFE_BIG = BigInt(Number.MAX_SAFE_INTEGER)
|
|
17
|
-
const EXACT_SIZE = Number.MAX_SAFE_INTEGER
|
|
18
|
-
const SHRUNK_SIZE = Number(MAX_SAFE_BIG - 64n)
|
|
19
|
-
const HEAD_INDEX = 0
|
|
20
|
-
const SECOND_INDEX = 1
|
|
21
|
-
const MID_INDEX = Number(MAX_SAFE_BIG - 512n)
|
|
22
|
-
const BEFORE_MID_INDEX = MID_INDEX - 1
|
|
23
|
-
const PRE_TAIL_INDEX = Number(MAX_SAFE_BIG - 8n)
|
|
24
|
-
const BEFORE_PRE_TAIL_INDEX = PRE_TAIL_INDEX - 1
|
|
25
|
-
const TAIL_INDEX = Number(MAX_SAFE_BIG - 1n)
|
|
26
|
-
const BEFORE_TAIL_INDEX = TAIL_INDEX - 1
|
|
27
|
-
const SAMPLE_RANGE_NEAR_TAIL = {
|
|
28
|
-
from: Number(MAX_SAFE_BIG - 16n),
|
|
29
|
-
to: TAIL_INDEX,
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
describe("FenwickMapTree (最大安全整数近傍)", () => {
|
|
33
|
-
it("最大安全整数サイズで基礎的な取得ができる", () => {
|
|
34
|
-
const tree = new FenwickMapTree(EXACT_SIZE, 0)
|
|
35
|
-
|
|
36
|
-
expect(tree.getSize()).toBe(EXACT_SIZE)
|
|
37
|
-
expect(tree.getTotal()).toBe(0)
|
|
38
|
-
expect(tree.get(HEAD_INDEX)).toBe(0)
|
|
39
|
-
expect(tree.get(TAIL_INDEX)).toBe(0)
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
it("最大安全整数サイズで複数インデックスを更新しても累積和が安全に計算できる", () => {
|
|
43
|
-
const tree = new FenwickMapTree(EXACT_SIZE, 0)
|
|
44
|
-
|
|
45
|
-
tree.update(HEAD_INDEX, 5)
|
|
46
|
-
tree.update(SECOND_INDEX, 3)
|
|
47
|
-
tree.update(MID_INDEX, 4)
|
|
48
|
-
tree.update(PRE_TAIL_INDEX, 7)
|
|
49
|
-
tree.update(TAIL_INDEX, 9)
|
|
50
|
-
|
|
51
|
-
expect(tree.prefixSum(HEAD_INDEX).cumulative).toBe(5)
|
|
52
|
-
expect(tree.prefixSum(SECOND_INDEX).cumulative).toBe(8)
|
|
53
|
-
expect(tree.prefixSum(MID_INDEX).cumulative).toBe(12)
|
|
54
|
-
expect(tree.prefixSum(PRE_TAIL_INDEX).cumulative).toBe(19)
|
|
55
|
-
expect(tree.prefixSum(TAIL_INDEX).cumulative).toBe(28)
|
|
56
|
-
expect(tree.getTotal()).toBe(28)
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
it("最大安全整数サイズで findIndexAtOrAfter が閾値に対応する", () => {
|
|
60
|
-
const tree = new FenwickMapTree(EXACT_SIZE, 0)
|
|
61
|
-
|
|
62
|
-
tree.update(HEAD_INDEX, 5)
|
|
63
|
-
tree.update(SECOND_INDEX, 3)
|
|
64
|
-
tree.update(MID_INDEX, 4)
|
|
65
|
-
tree.update(PRE_TAIL_INDEX, 7)
|
|
66
|
-
tree.update(TAIL_INDEX, 9)
|
|
67
|
-
|
|
68
|
-
expect(tree.findIndexAtOrAfter(1).index).toBe(HEAD_INDEX)
|
|
69
|
-
expect(tree.findIndexAtOrAfter(8).index).toBe(SECOND_INDEX)
|
|
70
|
-
expect(tree.findIndexAtOrAfter(12).index).toBe(MID_INDEX)
|
|
71
|
-
expect(tree.findIndexAtOrAfter(19).index).toBe(PRE_TAIL_INDEX)
|
|
72
|
-
expect(tree.findIndexAtOrAfter(28).index).toBe(TAIL_INDEX)
|
|
73
|
-
expect(tree.findIndexAtOrAfter(29).index).toBe(-1)
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
it("最大安全整数サイズで findIndexAtOrBefore が閾値に対応する", () => {
|
|
77
|
-
const tree = new FenwickMapTree(EXACT_SIZE, 0)
|
|
78
|
-
|
|
79
|
-
tree.update(HEAD_INDEX, 5)
|
|
80
|
-
tree.update(SECOND_INDEX, 3)
|
|
81
|
-
tree.update(MID_INDEX, 4)
|
|
82
|
-
tree.update(PRE_TAIL_INDEX, 7)
|
|
83
|
-
tree.update(TAIL_INDEX, 9)
|
|
84
|
-
|
|
85
|
-
expect(tree.findIndexAtOrBefore(4).index).toBe(-1)
|
|
86
|
-
expect(tree.findIndexAtOrBefore(5).index).toBe(HEAD_INDEX)
|
|
87
|
-
expect(tree.findIndexAtOrBefore(8).index).toBe(BEFORE_MID_INDEX)
|
|
88
|
-
expect(tree.findIndexAtOrBefore(12).index).toBe(BEFORE_PRE_TAIL_INDEX)
|
|
89
|
-
expect(tree.findIndexAtOrBefore(19).index).toBe(BEFORE_TAIL_INDEX)
|
|
90
|
-
expect(tree.findIndexAtOrBefore(28).index).toBe(TAIL_INDEX)
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
it("巨大サイズで updates がまとめて適用される", () => {
|
|
94
|
-
const tree = new FenwickMapTree(EXACT_SIZE, 0)
|
|
95
|
-
|
|
96
|
-
tree.updates([
|
|
97
|
-
{ index: HEAD_INDEX, value: 6 },
|
|
98
|
-
{ index: MID_INDEX, value: 4 },
|
|
99
|
-
{ index: TAIL_INDEX, value: 8 },
|
|
100
|
-
])
|
|
101
|
-
|
|
102
|
-
expect(tree.get(HEAD_INDEX)).toBe(6)
|
|
103
|
-
expect(tree.prefixSum(MID_INDEX).cumulative).toBe(10)
|
|
104
|
-
expect(tree.prefixSum(TAIL_INDEX).cumulative).toBe(18)
|
|
105
|
-
expect(tree.getTotal()).toBe(18)
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
it("巨大サイズで updateDelta が差分を加算できる", () => {
|
|
109
|
-
const tree = new FenwickMapTree(EXACT_SIZE, 0)
|
|
110
|
-
|
|
111
|
-
tree.updateDelta(TAIL_INDEX, 5)
|
|
112
|
-
tree.updateDelta(TAIL_INDEX, 3)
|
|
113
|
-
tree.updateDelta(HEAD_INDEX, 4)
|
|
114
|
-
|
|
115
|
-
expect(tree.get(TAIL_INDEX)).toBe(8)
|
|
116
|
-
expect(tree.prefixSum(TAIL_INDEX).cumulative).toBe(12)
|
|
117
|
-
expect(tree.getTotal()).toBe(12)
|
|
118
|
-
})
|
|
119
|
-
|
|
120
|
-
it("巨大サイズで changeSize を縮小すると範囲外デルタが除去される", () => {
|
|
121
|
-
const tree = new FenwickMapTree(EXACT_SIZE, 0)
|
|
122
|
-
|
|
123
|
-
tree.update(HEAD_INDEX, 5)
|
|
124
|
-
tree.update(TAIL_INDEX, 9)
|
|
125
|
-
|
|
126
|
-
tree.changeSize(SHRUNK_SIZE)
|
|
127
|
-
|
|
128
|
-
expect(tree.getSize()).toBe(SHRUNK_SIZE)
|
|
129
|
-
expect(() => tree.get(TAIL_INDEX)).toThrow()
|
|
130
|
-
expect(tree.getTotal()).toBe(5)
|
|
131
|
-
})
|
|
132
|
-
|
|
133
|
-
it("巨大サイズで changeSize を拡大した後でも更新ができる", () => {
|
|
134
|
-
const tree = new FenwickMapTree(SHRUNK_SIZE, 0)
|
|
135
|
-
|
|
136
|
-
tree.update(HEAD_INDEX, 5)
|
|
137
|
-
|
|
138
|
-
tree.changeSize(EXACT_SIZE)
|
|
139
|
-
tree.update(TAIL_INDEX, 11)
|
|
140
|
-
|
|
141
|
-
expect(tree.getSize()).toBe(EXACT_SIZE)
|
|
142
|
-
expect(tree.get(TAIL_INDEX)).toBe(11)
|
|
143
|
-
expect(tree.prefixSum(TAIL_INDEX).cumulative).toBe(16)
|
|
144
|
-
})
|
|
145
|
-
|
|
146
|
-
it("巨大サイズで rebuildTree を呼んでも合計値が維持される", () => {
|
|
147
|
-
const tree = new FenwickMapTree(EXACT_SIZE, 0)
|
|
148
|
-
|
|
149
|
-
tree.update(HEAD_INDEX, 5)
|
|
150
|
-
tree.update(PRE_TAIL_INDEX, 7)
|
|
151
|
-
|
|
152
|
-
tree.rebuildTree()
|
|
153
|
-
|
|
154
|
-
expect(tree.get(HEAD_INDEX)).toBe(5)
|
|
155
|
-
expect(tree.get(PRE_TAIL_INDEX)).toBe(7)
|
|
156
|
-
expect(tree.getTotal()).toBe(12)
|
|
157
|
-
})
|
|
158
|
-
|
|
159
|
-
it("巨大サイズで valueFn による遅延具現化が機能する", () => {
|
|
160
|
-
const tree = new FenwickMapTree(
|
|
161
|
-
EXACT_SIZE,
|
|
162
|
-
(index) => {
|
|
163
|
-
if (index === HEAD_INDEX) {
|
|
164
|
-
return 4
|
|
165
|
-
}
|
|
166
|
-
if (index === TAIL_INDEX) {
|
|
167
|
-
return 6
|
|
168
|
-
}
|
|
169
|
-
return 0
|
|
170
|
-
},
|
|
171
|
-
{ sampleRange: { from: 0, to: 16 }, materialize: false },
|
|
172
|
-
)
|
|
173
|
-
|
|
174
|
-
const head = tree.get(HEAD_INDEX, { materializeOption: { materialize: true } })
|
|
175
|
-
const tail = tree.get(TAIL_INDEX, { materializeOption: { materialize: true, ranges: [SAMPLE_RANGE_NEAR_TAIL] } })
|
|
176
|
-
const total = tree.getTotal({ materializeOption: { materialize: true, ranges: [SAMPLE_RANGE_NEAR_TAIL] } })
|
|
177
|
-
|
|
178
|
-
expect(head).toBe(4)
|
|
179
|
-
expect(tail).toBe(6)
|
|
180
|
-
expect(total).toBe(10)
|
|
181
|
-
})
|
|
182
|
-
|
|
183
|
-
it("巨大サイズの +1 末尾追記でも delta と total が正確 (回帰: r3 監査所見)", () => {
|
|
184
|
-
const tree = new FenwickMapTree(SHRUNK_SIZE, 0)
|
|
185
|
-
tree.update(HEAD_INDEX, 5)
|
|
186
|
-
tree.update(SHRUNK_SIZE - 1, 7)
|
|
187
|
-
|
|
188
|
-
tree.changeSize(SHRUNK_SIZE + 1)
|
|
189
|
-
|
|
190
|
-
expect(tree.getSize()).toBe(SHRUNK_SIZE + 1)
|
|
191
|
-
expect(tree.getTotal()).toBe(12)
|
|
192
|
-
expect(tree.prefixSum(SHRUNK_SIZE).cumulative).toBe(12)
|
|
193
|
-
expect(tree.get(SHRUNK_SIZE)).toBe(0)
|
|
194
|
-
})
|
|
195
|
-
|
|
196
|
-
it("巨大サイズ (bigint 探索パス) の findIndexAtOrAfter は materialize:true でも契約を満たし連続呼び出しで安定する (回帰: r3 監査所見)", () => {
|
|
197
|
-
// size >= 2^30 で _findIndexLarge (bigint 二分探索) に委譲されるパスの検証
|
|
198
|
-
const size = 0x80000000 // 2^31
|
|
199
|
-
const valueFn = (index: number) => (index === 500 ? 10 : 100)
|
|
200
|
-
const tree = new FenwickMapTree(size, valueFn, { sampleRange: { from: 0, to: 99 }, materialize: true })
|
|
201
|
-
const opts = { materializeOption: { materialize: true, ranges: [{ from: 0, to: 99 }] } }
|
|
202
|
-
|
|
203
|
-
const first = tree.findIndexAtOrAfter(50100, opts)
|
|
204
|
-
const second = tree.findIndexAtOrAfter(50100, opts)
|
|
205
|
-
|
|
206
|
-
// AtOrAfter 契約: cumulative は target 以上
|
|
207
|
-
expect(first.cumulative).toBeGreaterThanOrEqual(50100)
|
|
208
|
-
// 同一引数の連続呼び出しは同一結果 (探索後の木変異によるドリフトがない)
|
|
209
|
-
expect(second.index).toBe(first.index)
|
|
210
|
-
expect(second.cumulative).toBe(first.cumulative)
|
|
211
|
-
})
|
|
212
|
-
|
|
213
|
-
it("巨大サイズ (bigint 探索パス) の未発見時は小サイズパスと同一契約で cumulative などが undefined になる (回帰: r4 監査所見)", () => {
|
|
214
|
-
// 旧実装は bestResult ?? lastResult のフォールバックにより、未発見 (index=-1) でも
|
|
215
|
-
// 最後にプローブした位置の cumulative/currentValue/safeIndex が載っていた
|
|
216
|
-
// (AtOrBefore の未発見時は cumulative > target という契約矛盾の値になる)。
|
|
217
|
-
const size = 0x40000000 // 2^30 (bigint 探索パスの下限)
|
|
218
|
-
const tree = new FenwickMapTree(size, 10)
|
|
219
|
-
|
|
220
|
-
// AtOrAfter: total を超える target は未発見
|
|
221
|
-
const after = tree.findIndexAtOrAfter(Number.MAX_SAFE_INTEGER)
|
|
222
|
-
expect(after.index).toBe(-1)
|
|
223
|
-
expect(after.cumulative).toBeUndefined()
|
|
224
|
-
expect(after.currentValue).toBeUndefined()
|
|
225
|
-
expect(after.safeIndex).toBeUndefined()
|
|
226
|
-
|
|
227
|
-
// AtOrBefore: どの累積和よりも小さい target は未発見
|
|
228
|
-
const before = tree.findIndexAtOrBefore(-1)
|
|
229
|
-
expect(before.index).toBe(-1)
|
|
230
|
-
expect(before.cumulative).toBeUndefined()
|
|
231
|
-
expect(before.currentValue).toBeUndefined()
|
|
232
|
-
expect(before.safeIndex).toBeUndefined()
|
|
233
|
-
|
|
234
|
-
// 小サイズパスと同一契約であることの対照確認
|
|
235
|
-
const small = new FenwickMapTree(1000, 10)
|
|
236
|
-
const smallAfter = small.findIndexAtOrAfter(Number.MAX_SAFE_INTEGER)
|
|
237
|
-
expect(smallAfter.index).toBe(-1)
|
|
238
|
-
expect(smallAfter.cumulative).toBeUndefined()
|
|
239
|
-
expect(smallAfter.safeIndex).toBeUndefined()
|
|
240
|
-
})
|
|
241
|
-
|
|
242
|
-
it("巨大サイズで materialize 範囲を指定して prefixSum が反映される", () => {
|
|
243
|
-
const tree = new FenwickMapTree(
|
|
244
|
-
EXACT_SIZE,
|
|
245
|
-
(index) => {
|
|
246
|
-
if (index === PRE_TAIL_INDEX) {
|
|
247
|
-
return 7
|
|
248
|
-
}
|
|
249
|
-
if (index === TAIL_INDEX) {
|
|
250
|
-
return 5
|
|
251
|
-
}
|
|
252
|
-
return 0
|
|
253
|
-
},
|
|
254
|
-
{ sampleRange: SAMPLE_RANGE_NEAR_TAIL, materialize: false },
|
|
255
|
-
)
|
|
256
|
-
|
|
257
|
-
const prefix = tree.prefixSum(TAIL_INDEX, { materializeOption: { materialize: true, ranges: [SAMPLE_RANGE_NEAR_TAIL] } })
|
|
258
|
-
|
|
259
|
-
expect(prefix.currentValue).toBe(5)
|
|
260
|
-
expect(prefix.cumulative).toBe(12)
|
|
261
|
-
expect(prefix.total).toBe(12)
|
|
262
|
-
})
|
|
263
|
-
})
|
|
264
|
-
|
|
265
|
-
describe("useFenwickMapTree (最大安全整数近傍)", () => {
|
|
266
|
-
it("最大安全整数サイズで同一パラメーターならインスタンスが安定する", () => {
|
|
267
|
-
const { result, rerender } = renderHook<FenwickMapTree, { size: number; value: number | ((index: number) => number) }>(({ size, value }) => useFenwickMapTree(size, value), {
|
|
268
|
-
initialProps: { size: EXACT_SIZE, value: 0 },
|
|
269
|
-
})
|
|
270
|
-
|
|
271
|
-
const first = result.current
|
|
272
|
-
|
|
273
|
-
rerender({ size: EXACT_SIZE, value: 0 })
|
|
274
|
-
|
|
275
|
-
expect(result.current).toBe(first)
|
|
276
|
-
})
|
|
277
|
-
|
|
278
|
-
it("最大安全整数サイズでサイズが変わっても同じインスタンスになる(安定性保証)", () => {
|
|
279
|
-
const { result, rerender } = renderHook<FenwickMapTree, { size: number; value: number | ((index: number) => number) }>(({ size, value }) => useFenwickMapTree(size, value), {
|
|
280
|
-
initialProps: { size: EXACT_SIZE, value: 0 },
|
|
281
|
-
})
|
|
282
|
-
|
|
283
|
-
const first = result.current
|
|
284
|
-
|
|
285
|
-
rerender({ size: SHRUNK_SIZE, value: 0 })
|
|
286
|
-
|
|
287
|
-
expect(result.current).toBe(first)
|
|
288
|
-
})
|
|
289
|
-
|
|
290
|
-
it("最大安全整数サイズで valueFn が変わっても同じインスタンスになる(安定性保証)", () => {
|
|
291
|
-
const { result, rerender } = renderHook<FenwickMapTree, { size: number; value: number | ((index: number) => number) }>(({ size, value }) => useFenwickMapTree(size, value), {
|
|
292
|
-
initialProps: { size: EXACT_SIZE, value: 0 },
|
|
293
|
-
})
|
|
294
|
-
|
|
295
|
-
const first = result.current
|
|
296
|
-
|
|
297
|
-
rerender({
|
|
298
|
-
size: EXACT_SIZE,
|
|
299
|
-
value: (index: number) => {
|
|
300
|
-
if (index === HEAD_INDEX) {
|
|
301
|
-
return 2
|
|
302
|
-
}
|
|
303
|
-
return 0
|
|
304
|
-
},
|
|
305
|
-
})
|
|
306
|
-
|
|
307
|
-
expect(result.current).toBe(first)
|
|
308
|
-
})
|
|
309
|
-
|
|
310
|
-
it("最大安全整数サイズで返却されたツリーが更新を反映する", () => {
|
|
311
|
-
const { result } = renderHook(() => useFenwickMapTree(EXACT_SIZE, 0))
|
|
312
|
-
|
|
313
|
-
act(() => {
|
|
314
|
-
result.current.update(HEAD_INDEX, 5)
|
|
315
|
-
result.current.update(TAIL_INDEX, 9)
|
|
316
|
-
})
|
|
317
|
-
|
|
318
|
-
expect(result.current.get(HEAD_INDEX)).toBe(5)
|
|
319
|
-
expect(result.current.get(TAIL_INDEX)).toBe(9)
|
|
320
|
-
expect(result.current.prefixSum(TAIL_INDEX).cumulative).toBe(14)
|
|
321
|
-
})
|
|
322
|
-
})
|
|
323
|
-
|
|
324
|
-
describe("FenwickMapTree (100 Billion items & 1 Trillion items)", () => {
|
|
325
|
-
const HUNDRED_BILLION = 100_000_000_000
|
|
326
|
-
|
|
327
|
-
it("works without infinite loop for 100 Billion items", () => {
|
|
328
|
-
// Initialize with default item size 20
|
|
329
|
-
const tree = new FenwickMapTree(HUNDRED_BILLION, 20)
|
|
330
|
-
|
|
331
|
-
// This operation triggers _findIndex or _findIndexLarge
|
|
332
|
-
// Before fix: loop around 2^31 -> hang
|
|
333
|
-
// After fix: uses _findIndexLarge -> success
|
|
334
|
-
|
|
335
|
-
// Target 1000.
|
|
336
|
-
// Index 49: cumulative = (49+1)*20 = 1000. 1000 >= 1000. Found.
|
|
337
|
-
const targetOffset = 20 * 50
|
|
338
|
-
const result = tree.findIndexAtOrAfter(targetOffset)
|
|
339
|
-
|
|
340
|
-
expect(result.index).toBe(49)
|
|
341
|
-
expect(result.cumulative).toBe(targetOffset)
|
|
342
|
-
})
|
|
343
|
-
|
|
344
|
-
it("works for size just related to bitwise boundary (3 Billion)", () => {
|
|
345
|
-
const THREE_BILLION = 3_000_000_000
|
|
346
|
-
const tree = new FenwickMapTree(THREE_BILLION, 10)
|
|
347
|
-
// 10 * 60 = 600.
|
|
348
|
-
// Index 59: (59+1)*10 = 600.
|
|
349
|
-
const result = tree.findIndexAtOrAfter(10 * 60)
|
|
350
|
-
expect(result.index).toBe(59)
|
|
351
|
-
})
|
|
352
|
-
|
|
353
|
-
it("(a) does not hang at the 2^30 bitwise-doubling boundary", () => {
|
|
354
|
-
// size = 2^30 はビット倍加ループが Int32 オーバーフローする中間域。
|
|
355
|
-
// 修正前: bit << 1 が -2^31 に化けて無限ループ (タイムアウト)。
|
|
356
|
-
const tree = new FenwickMapTree(0x40000000, 10) // 2^30 = 1,073,741,824
|
|
357
|
-
// index 9: cumulative = (9+1)*10 = 100 >= 100
|
|
358
|
-
const result = tree.findIndexAtOrAfter(100)
|
|
359
|
-
expect(result.index).toBe(9)
|
|
360
|
-
expect(result.cumulative).toBe(100)
|
|
361
|
-
})
|
|
362
|
-
|
|
363
|
-
it("(a) does not hang for sizes in [2^30, 2^31-1] (max 32-bit signed)", () => {
|
|
364
|
-
const tree = new FenwickMapTree(0x7fffffff, 10) // 2^31 - 1 = 2,147,483,647
|
|
365
|
-
const result = tree.findIndexAtOrAfter(100)
|
|
366
|
-
expect(result.index).toBe(9)
|
|
367
|
-
expect(result.cumulative).toBe(100)
|
|
368
|
-
|
|
369
|
-
const before = tree.findIndexAtOrBefore(100)
|
|
370
|
-
expect(before.index).toBe(9)
|
|
371
|
-
})
|
|
372
|
-
|
|
373
|
-
it("works for 1 Trillion items (1,000,000,000,000)", () => {
|
|
374
|
-
const TRILLION = 1_000_000_000_000
|
|
375
|
-
// Need to ensure total height fits in safe integer.
|
|
376
|
-
// MAX safe integer ~9 Quadrillion.
|
|
377
|
-
// 1 Trillion * 25 = 25 Trillion on safe side.
|
|
378
|
-
const itemSize = 25
|
|
379
|
-
const tree = new FenwickMapTree(TRILLION, itemSize)
|
|
380
|
-
|
|
381
|
-
const indexToFind = 500_000_000_000 // 500 Billion
|
|
382
|
-
const targetOffset = itemSize * (indexToFind + 1)
|
|
383
|
-
const result = tree.findIndexAtOrAfter(targetOffset)
|
|
384
|
-
|
|
385
|
-
expect(result.index).toBe(indexToFind)
|
|
386
|
-
expect(result.cumulative).toBe(targetOffset)
|
|
387
|
-
})
|
|
388
|
-
})
|