Skip to content

Commit

Permalink
Add MultiIndex data structure
Browse files Browse the repository at this point in the history
  • Loading branch information
p2004a committed Jan 5, 2025
1 parent 9dca071 commit db383f2
Show file tree
Hide file tree
Showing 2 changed files with 147 additions and 0 deletions.
47 changes: 47 additions & 0 deletions src/multiIndex.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { test, suite } from 'node:test';
import assert from 'node:assert/strict';
import { MultiIndex } from './multiIndex.js';

suite('MultiIndex', () => {
test('set', () => {
const mi = new MultiIndex({ a1: '', a2: 0, a3: '' });
assert.equal(mi.set({ a1: 'a1.1', a2: 1, a3: 'a3.1' }), true);
assert.equal(mi.set({ a1: 'a1.2', a2: 2, a3: 'a3.2' }), true);
assert.equal(mi.set({ a1: 'a1.2', a2: 2, a3: 'a3.2' }), false);
assert.throws(() => {
mi.set({ a1: 'a1.2', a2: 2, a3: 'a3.2_prime' });
});
assert.equal(mi.size, 2);
});

test('get', () => {
const mi = new MultiIndex({ a1: '', a2: 0, a3: '' });
mi.set({ a1: 'a1.1', a2: 1, a3: 'a3.1' });

assert.equal(mi.get('a1', 'a1.1')?.a3, 'a3.1');
assert.equal(mi.get('a2', 1)?.a1, 'a1.1');
assert.equal(mi.get('a3', 'a3.1')?.a1, 'a1.1');

assert.equal(mi.get('a1', 'non-existent'), undefined);
});

test('has', () => {
const mi = new MultiIndex({ a1: '', a2: 0 });
mi.set({ a1: 'a1.1', a2: 1 });

assert.equal(mi.has('a1', 'a1.1'), true);
assert.equal(mi.has('a1', 'non-existent'), false);
});

test('delete', () => {
const mi = new MultiIndex({ a1: '', a2: 0 });
mi.set({ a1: 'a1.1', a2: 1 });
mi.set({ a1: 'a1.2', a2: 2 });
assert.equal(mi.size, 2);

assert.equal(mi.delete('a1', 'a1.1'), true);
assert.ok(!mi.hasAny({ a1: 'a1.1', a2: 1 }));
assert.ok(mi.hasAll({ a1: 'a1.2', a2: 2 }));
assert.equal(mi.size, 1);
});
});
100 changes: 100 additions & 0 deletions src/multiIndex.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/**
* MultiIndex is a n-way mapping between values. In mathematical sense,
* it's a bijection between n-sets of values.
*
* Each of the sets has a name and type: modeled by the template type argument:
*
* {
* 'set_name1': type
* 'set_name2': some_type2
* }
*
* The multi-index supports adding new elements and looking up values from all
* the sets given the value from only one of them.
*/
export class MultiIndex<K extends object> {
private m: { [key in keyof K]: Map<K[key], K> };

/**
* Constructs a new MultiIndex.
*
* @param idx Any index key. It's not added to the multi-index, just needed
* because of the type erasure.
*/
constructor(private idx: K) {
this.m = {} as typeof this.m;
for (const k in idx) {
this.m[k] = new Map();
}
}

/**
* Tests if any of the values from the index key are already in the set.
*/
hasAny(idx: K): boolean {
for (const k in idx) {
if (this.m[k].has(idx[k])) {
return true;
}
}
return false;
}

/**
* Tests if the whole index key is already in the set.
*/
hasAll(idx: K): boolean {
for (const k in this.m) {
const v = this.m[k].get(idx[k]);
if (v) {
for (const k in idx) {
if (v[k] != idx[k]) {
return false;
}
}
return true;
}
break;
}
return false;
}

set(idx: K): boolean {
if (this.hasAll(idx)) {
return false;
}
if (this.hasAny(idx)) {
throw new Error('Trying to set incompatible index key');
}
for (const k in idx) {
this.m[k].set(idx[k], idx);
}
return true;
}

get<U extends keyof K>(set: U, key: K[U]): K | undefined {
return this.m[set].get(key);
}

has<U extends keyof K>(set: U, key: K[U]): boolean {
return this.m[set].has(key);
}

delete<U extends keyof K>(set: U, key: K[U]): boolean {
const k = this.m[set].get(key);
if (!k) {
return false;
}
for (const i in k) {
this.m[i].delete(k[i]);
}
return true;
}

get size(): number {
for (const k in this.m) {
return this.m[k].size;
}
return 0;
}
}

0 comments on commit db383f2

Please sign in to comment.