Skip to content

Commit c423d64

Browse files
chore(tables): own fractional-indexing in-house, drop runtime dep
1 parent 24a6086 commit c423d64

5 files changed

Lines changed: 494 additions & 6 deletions

File tree

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
/**
2+
* @vitest-environment node
3+
*
4+
* Differential test: runs the in-house port side by side with the upstream
5+
* `fractional-indexing` package over exhaustive + randomized inputs and asserts
6+
* byte-identical output. The package is kept as a devDependency solely as this
7+
* oracle. Delete this file (and the dep) once we no longer want the comparison.
8+
*/
9+
import {
10+
generateKeyBetween as oracleKeyBetween,
11+
generateNKeysBetween as oracleNKeysBetween,
12+
} from 'fractional-indexing'
13+
import { describe, expect, it } from 'vitest'
14+
import {
15+
generateKeyBetween,
16+
generateNKeysBetween,
17+
} from '@/lib/fractional-indexing/fractional-indexing'
18+
19+
/** Deterministic LCG (Numerical Recipes constants) — no test-only dependency. */
20+
function makeRng(seed: number): () => number {
21+
let state = seed >>> 0
22+
return () => {
23+
state = (Math.imul(state, 1664525) + 1013904223) >>> 0
24+
return state / 0x100000000
25+
}
26+
}
27+
28+
function randInt(rng: () => number, maxExclusive: number): number {
29+
return Math.floor(rng() * maxExclusive)
30+
}
31+
32+
/** Length of a key's integer part from its head char (a..z → 2..27, A..Z → 27..2). */
33+
function integerPartLength(head: string): number {
34+
if (head >= 'a' && head <= 'z') return head.charCodeAt(0) - 'a'.charCodeAt(0) + 2
35+
if (head >= 'A' && head <= 'Z') return 'Z'.charCodeAt(0) - head.charCodeAt(0) + 2
36+
throw new Error(`unexpected head: ${head}`)
37+
}
38+
39+
/** Compare both impls for `(a, b)`: same return value, or both throw. */
40+
function expectKeyParity(a: string | null, b: string | null): string | null {
41+
let mine: string | undefined
42+
let mineThrew = false
43+
try {
44+
mine = generateKeyBetween(a, b)
45+
} catch {
46+
mineThrew = true
47+
}
48+
49+
let theirs: string | undefined
50+
let theirsThrew = false
51+
try {
52+
theirs = oracleKeyBetween(a, b)
53+
} catch {
54+
theirsThrew = true
55+
}
56+
57+
expect(mineThrew).toBe(theirsThrew)
58+
if (!mineThrew) {
59+
expect(mine).toBe(theirs)
60+
return mine as string
61+
}
62+
return null
63+
}
64+
65+
describe('fractional-indexing in-house port ≡ upstream', () => {
66+
it('matches known anchor values from the algorithm', () => {
67+
expect(generateKeyBetween(null, null)).toBe('a0')
68+
expect(generateKeyBetween('a0', null)).toBe('a1')
69+
expect(generateKeyBetween(null, 'a0')).toBe('Zz')
70+
expect(generateKeyBetween('a0', 'a1')).toBe('a0V')
71+
// All match the oracle too.
72+
expect(generateKeyBetween(null, null)).toBe(oracleKeyBetween(null, null))
73+
expect(generateKeyBetween('a0', 'a1')).toBe(oracleKeyBetween('a0', 'a1'))
74+
})
75+
76+
it('matches over exhaustive ordered pairs from a fixed key pool', () => {
77+
// Build a sorted pool by chaining appends, then test every ordered pair
78+
// plus both open ends.
79+
const pool: string[] = []
80+
let last: string | null = null
81+
for (let i = 0; i < 40; i++) {
82+
last = generateKeyBetween(last, null)
83+
pool.push(last)
84+
}
85+
const ends: Array<string | null> = [null, ...pool]
86+
for (const a of ends) {
87+
for (const b of ends) {
88+
// Only feed ordered, distinct bounds to the "happy path"; the parity
89+
// helper also asserts both throw together for the invalid ones.
90+
expectKeyParity(a, b)
91+
}
92+
}
93+
})
94+
95+
it('matches while building a list via random-position inserts', () => {
96+
for (const seed of [1, 7, 42, 1337, 99999]) {
97+
const rng = makeRng(seed)
98+
const keys: string[] = []
99+
for (let step = 0; step < 400; step++) {
100+
const pos = randInt(rng, keys.length + 1)
101+
const a = pos === 0 ? null : keys[pos - 1]
102+
const b = pos === keys.length ? null : keys[pos]
103+
const key = expectKeyParity(a, b)
104+
expect(key).not.toBeNull()
105+
keys.splice(pos, 0, key as string)
106+
}
107+
// List stayed strictly sorted throughout.
108+
for (let i = 1; i < keys.length; i++) {
109+
expect(keys[i - 1] < keys[i]).toBe(true)
110+
}
111+
}
112+
})
113+
114+
it('matches generateNKeysBetween across open/closed ranges and counts', () => {
115+
const rng = makeRng(2024)
116+
for (let trial = 0; trial < 200; trial++) {
117+
// A random ordered (a, b) window inside a freshly built run.
118+
const run = generateNKeysBetween(null, null, 12)
119+
const i = randInt(rng, run.length)
120+
const j = randInt(rng, run.length)
121+
const lo = Math.min(i, j)
122+
const hi = Math.max(i, j)
123+
const n = randInt(rng, 8)
124+
125+
const a = run[lo]
126+
const b = lo === hi ? null : run[hi]
127+
expect(generateNKeysBetween(a, b, n)).toEqual(oracleNKeysBetween(a, b, n))
128+
}
129+
// Edge counts and open ends.
130+
expect(generateNKeysBetween(null, null, 0)).toEqual(oracleNKeysBetween(null, null, 0))
131+
expect(generateNKeysBetween(null, null, 1)).toEqual(oracleNKeysBetween(null, null, 1))
132+
expect(generateNKeysBetween(null, null, 50)).toEqual(oracleNKeysBetween(null, null, 50))
133+
expect(generateNKeysBetween('a0', null, 25)).toEqual(oracleNKeysBetween('a0', null, 25))
134+
expect(generateNKeysBetween(null, 'a0', 25)).toEqual(oracleNKeysBetween(null, 'a0', 25))
135+
})
136+
137+
it('matches across integer-length rollover on long append/prepend runs', () => {
138+
// A long append run forces incrementInteger to roll the integer part
139+
// through multiple heads and lengths (a→…→z→null path); a long prepend run
140+
// exercises decrementInteger symmetrically. The random test above stays in
141+
// head 'a'/'Z' length-2, so these cover the branchy carry/borrow code.
142+
const lengths = new Set<number>()
143+
let appendKey: string | null = null
144+
for (let i = 0; i < 5000; i++) {
145+
const mine = generateKeyBetween(appendKey, null)
146+
expect(mine).toBe(oracleKeyBetween(appendKey, null))
147+
appendKey = mine
148+
lengths.add(integerPartLength(mine[0]))
149+
}
150+
let prependKey: string | null = null
151+
for (let i = 0; i < 5000; i++) {
152+
const mine = generateKeyBetween(null, prependKey)
153+
expect(mine).toBe(oracleKeyBetween(null, prependKey))
154+
prependKey = mine
155+
lengths.add(integerPartLength(mine[0]))
156+
}
157+
// Confirm we actually crossed integer-length boundaries (not just length 2).
158+
expect([...lengths].some((l) => l > 2)).toBe(true)
159+
})
160+
161+
it('matches deep same-spot inserts (long fractions)', () => {
162+
// Repeatedly inserting between the same two neighbors grows the fraction
163+
// without bound — exercises the recursive midpoint + common-prefix path.
164+
let lo = generateKeyBetween(null, null)
165+
let hi = generateKeyBetween(lo, null)
166+
let maxLen = 0
167+
for (let i = 0; i < 2000; i++) {
168+
const mine = generateKeyBetween(lo, hi)
169+
expect(mine).toBe(oracleKeyBetween(lo, hi))
170+
// Alternate which side we keep so the fraction deepens on both ends.
171+
if (i % 2 === 0) hi = mine
172+
else lo = mine
173+
maxLen = Math.max(maxLen, mine.length)
174+
}
175+
expect(maxLen).toBeGreaterThan(10)
176+
})
177+
178+
it('throws on invalid keys and inverted bounds in both impls', () => {
179+
const bad: Array<[string | null, string | null]> = [
180+
['a1', 'a0'], // inverted
181+
['a0', 'a0'], // equal
182+
['', null], // empty key
183+
['a00', null], // trailing zero in fraction
184+
['1', null], // invalid head
185+
]
186+
for (const [a, b] of bad) {
187+
let mineThrew = false
188+
let theirsThrew = false
189+
try {
190+
generateKeyBetween(a, b)
191+
} catch {
192+
mineThrew = true
193+
}
194+
try {
195+
oracleKeyBetween(a, b)
196+
} catch {
197+
theirsThrew = true
198+
}
199+
expect(mineThrew).toBe(true)
200+
expect(mineThrew).toBe(theirsThrew)
201+
}
202+
})
203+
})

0 commit comments

Comments
 (0)