feat: conformance tests pass for first time

Change implementation to exponentially increase search space at each
level.
This commit is contained in:
nobody 2025-12-12 21:05:29 -08:00
commit 546d6deb69
Signed by: GrocerPublishAgent
GPG key ID: D460CD54A9E3AB86
13 changed files with 1852 additions and 102 deletions

View file

@ -1,9 +1,65 @@
const ALPHABET =
"-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz";
const MAX_LEVEL_EXPONENT = 8;
function maxValueForLevel(level: number): number {
const exp = Math.min(level + 1, MAX_LEVEL_EXPONENT);
return Math.pow(64, exp) - 1;
}
function charsForLevel(level: number): number {
return Math.min(level + 1, MAX_LEVEL_EXPONENT);
}
function idToNumbers(id: string): number[] {
const nums = id.split("").map((char) => ALPHABET.indexOf(char));
return nums;
const numbers: number[] = [];
let pos = 0;
let level = 0;
while (pos < id.length) {
const numChars = charsForLevel(level);
if (pos + numChars > id.length) {
throw new Error(
`Invalid sort key length ${id.length}. Expected triangular number (1, 3, 6, 10, ...)`
);
}
let value = 0;
for (let i = 0; i < numChars; i++) {
const char = id[pos + i];
const digit = ALPHABET.indexOf(char);
if (digit === -1) {
throw new Error(
`Invalid character '${char}' in sort key. Expected characters from alphabet: ${ALPHABET}`
);
}
value = value * 64 + digit;
}
numbers.push(value);
pos += numChars;
level++;
}
return numbers;
}
function numbersToId(numbers: number[]): string {
let result = "";
for (let level = 0; level < numbers.length; level++) {
const numChars = charsForLevel(level);
const chars: string[] = [];
let v = numbers[level];
for (let i = 0; i < numChars; i++) {
chars.push(ALPHABET[v % 64]);
v = Math.floor(v / 64);
}
result += chars.reverse().join("");
}
return result;
}
type Maybe<T> = T | null;
@ -13,7 +69,6 @@ function isNone<T>(value: Maybe<T>): value is null {
}
export class LSEQ {
// true = allocate near min, false = allocate near max
private strategies: boolean[];
private random: () => number;
@ -22,50 +77,157 @@ export class LSEQ {
this.strategies = [random() < 0.5];
}
public alloc(before: Maybe<string>, after: Maybe<string>): string {
// Convert to numeric arrays, using boundary values for null
const p = isNone(before) ? [0] : idToNumbers(before);
const q = isNone(after) ? [63] : idToNumbers(after);
private genRange(min: number, max: number): number {
return min + Math.floor(this.random() * (max - min));
}
alloc(before: Maybe<string>, after: Maybe<string>): string {
const p = isNone(before) ? [] : idToNumbers(before);
const q = isNone(after) ? [] : idToNumbers(after);
// Walk through digits looking for space
let depth = 0;
const result = [];
// eslint-disable-next-line no-constant-condition
const result: number[] = [];
while (true) {
const pVal = depth < p.length ? p[depth] : 0;
const qVal = depth < q.length ? q[depth] : 63;
const pVal = p[depth] ?? 0;
const qUpper = q[depth];
const levelMax = maxValueForLevel(depth);
const interval = qVal - pVal;
const minAlloc = pVal + 1;
const maxAlloc =
qUpper !== undefined ? Math.max(0, qUpper - 1) : levelMax;
// If we have space between values at this depth
if (interval > 1) {
// Pick a value in the available range
const range = interval - 1;
const addVal = 1 + Math.floor(this.random() * range);
let newValue;
if (this.strategies[depth]) {
newValue = pVal + addVal;
} else {
newValue = qVal - addVal;
}
// Take the prefix from p up to depth and append our new value
if (minAlloc <= maxAlloc) {
const range = maxAlloc - minAlloc + 1;
const offset = this.genRange(0, range);
const newValue = this.strategies[depth]
? minAlloc + offset
: maxAlloc - offset;
result.push(newValue);
return result.map((n) => ALPHABET[n]).join("");
return numbersToId(result);
}
result.push(pVal);
// If values are the same or adjacent at this depth,
// continue to next depth
depth++;
if (depth > this.strategies.length) {
if (depth >= this.strategies.length) {
this.strategies.push(this.random() < 0.5);
}
}
}
}
const USABLE_SPACE: number[] = [
64 - 1,
4096 - 1,
262144 - 1,
16777216 - 1,
1073741824 - 1,
68719476736 - 1,
4398046511104 - 1,
281474976710656 - 1,
];
export class EvenSpacingIterator implements Iterator<number> {
private remainingItems: number;
private spaceSize: number;
private nextItem: number;
private stepSizeInteger: number;
private stepSizeError: number;
private errorAccumulator: number;
private constructor(
remainingItems: number,
spaceSize: number,
stepSizeInteger: number,
stepSizeError: number
) {
this.remainingItems = remainingItems;
this.spaceSize = spaceSize;
this.nextItem = 1;
this.stepSizeInteger = stepSizeInteger;
this.stepSizeError = stepSizeError;
this.errorAccumulator = 0;
}
static create(totalItems: number): { k: number; iterator: EvenSpacingIterator } {
if (totalItems === 0) {
throw new Error("Too many items to allocate");
}
let k = 0;
let spaceSize = 0;
for (let index = 0; index < USABLE_SPACE.length; index++) {
const size = USABLE_SPACE[index];
if (size >= totalItems) {
k = index + 1;
spaceSize = size;
break;
}
}
if (k === 0) {
throw new Error("Too many items to allocate");
}
const stepSize = spaceSize / totalItems;
const stepSizeInteger = Math.floor(stepSize);
const stepSizeError = stepSize - stepSizeInteger;
return {
k,
iterator: new EvenSpacingIterator(
totalItems,
spaceSize,
stepSizeInteger,
stepSizeError
),
};
}
static positionToKey(k: number, position: number): string {
const result: number[] = [];
for (let i = 0; i < k - 1; i++) {
result.push(0);
}
if (k > 0) {
result.push(position);
}
return numbersToId(result);
}
next(): IteratorResult<number> {
if (this.remainingItems === 0) {
return { done: true, value: undefined };
}
if (this.nextItem > this.spaceSize) {
return { done: true, value: undefined };
}
const currentPosition = this.nextItem;
this.remainingItems--;
this.nextItem += this.stepSizeInteger;
this.errorAccumulator += this.stepSizeError;
if (this.errorAccumulator >= 1.0) {
this.nextItem++;
this.errorAccumulator -= 1.0;
}
return { done: false, value: currentPosition };
}
[Symbol.iterator](): Iterator<number> {
return this;
}
}
export function compareLSEQ(a: string, b: string): number {
if (a === b) return 0;
return a < b ? -1 : 1;