feat: conformance tests pass for first time
Change implementation to exponentially increase search space at each level.
This commit is contained in:
parent
31c454a78c
commit
546d6deb69
13 changed files with 1852 additions and 102 deletions
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue