Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement HashMap similar to Python's dict #19

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,5 +54,8 @@
},
"engines": {
"node": ">=10.6.0"
},
"dependencies": {
"ts-essentials": "^9.4.2"
}
}
73 changes: 73 additions & 0 deletions src/__tests__/hashable.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import {Primitive} from 'ts-essentials';
import {HashMap, Hashable} from '../hashable';
import {PRIMES} from '../primes';
import {Monzo, monzosEqual} from '../monzo';
import {describe, it, expect} from 'vitest';

/**
* Partial implementation of a rational number capable of holding very large values with a limited number of prime factors.
*/
class ImmutableMonzo extends Hashable {
vector: Readonly<Monzo>;

constructor(vector: Readonly<Monzo>) {
super();
this.vector = vector;
}

valueOf(): number {
let value = 1;
for (let i = 0; i < this.vector.length; ++i) {
value *= PRIMES[i] ** this.vector[i];
}
return value;
}

strictEquals(other: Hashable | Primitive): boolean {
if (other instanceof ImmutableMonzo) {
return monzosEqual(this.vector, other.vector);
}
return false;
}
}

function M(vector: Readonly<Monzo>) {
return Object.freeze(new ImmutableMonzo(Object.freeze(vector)));
}

describe('Hash-map / dictionary', () => {
it('can set / get keys that hash to the same value', () => {
const thirty = M([1, 1, 1]);
const big = M([100000]);
const alsoBig = M([-1, 1000]);
expect(thirty.valueOf()).toBe(30);
expect(big.valueOf()).toBe(alsoBig.valueOf());

const map = new HashMap<number | ImmutableMonzo, string>([
[30, 'number'],
[thirty, 'thirty'],
[big, 'biig'], // Oops typo. To be overriden.
]);

map.set(alsoBig, 'also big');
map.set(M([100000]), 'big');

expect(map.get(30)).toBe('number');
expect(map.get(M([1, 1, 1]))).toBe('thirty');
expect(map.get(M([100000]))).toBe('big');
expect(map.get(M([-1, 1000]))).toBe('also big');

expect(map.size).toBe(4);
});

it('can be cleared', () => {
const map = new HashMap();
map.set(3, 4);
expect(map.size).toBe(1);
map.clear();
expect(map.size).toBe(0);
expect(map.get(3)).toBe(undefined);
});

// TODO: Rest of the methods of HashMap
});
61 changes: 56 additions & 5 deletions src/__tests__/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import {describe, it, expect} from 'vitest';
import {
Fraction,
FractionSet,
arraysEqual,
binomial,
ceilPow2,
Expand All @@ -19,7 +18,10 @@ import {
iteratedEuclid,
norm,
valueToCents,
F,
} from '../index';
import {HashMap, HashSet, Hashable} from '../hashable';
import {DeepReadonly, Primitive} from 'ts-essentials';

describe('Array equality tester', () => {
it('works on integer arrays', () => {
Expand Down Expand Up @@ -207,11 +209,11 @@ describe('Farey sequence generator', () => {
});

it('agrees with the brute force method', () => {
const everything = new FractionSet();
const everything = new HashSet<DeepReadonly<Fraction>>();
const N = Math.floor(Math.random() * 50) + 1;
for (let d = 1; d <= N; ++d) {
for (let n = 0; n <= d; ++n) {
everything.add(new Fraction(n, d));
everything.add(F(n, d));
}
}
const brute = Array.from(everything);
Expand Down Expand Up @@ -254,11 +256,11 @@ describe('Farey interior generator', () => {
});

it('agrees with the brute force method', () => {
const everything = new FractionSet();
const everything = new HashSet<DeepReadonly<Fraction>>();
const N = Math.floor(Math.random() * 50) + 1;
for (let d = 1; d <= N; ++d) {
for (let n = 1; n < d; ++n) {
everything.add(new Fraction(n, d));
everything.add(F(n, d));
}
}
const brute = Array.from(everything);
Expand Down Expand Up @@ -382,3 +384,52 @@ describe('Constant structure checker with a margin of equivalence', () => {
expect(hasMarginConstantStructure([1199, 1200], 2)).toBe(false);
});
});

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A good idea would be adding tests to cover every method of HashMap (but probably not necessary HashSet) because there are usually two main paths for Hashable and primitive types, so it’s good to at least be confident they don’t diverge in behavior.

describe('Fractions as hashmap keys', () => {
it('can override values using equivalent keys', () => {
const hashMap = new HashMap([
[F(3, 2), 'fif'],
[F(2), 'octave'],
]);
hashMap.set(F('5/4'), 'third');
hashMap.set(F(1.5), 'fifth');
const entries = Array.from(hashMap).sort((a, b) => a[0].compare(b[0]));
expect(entries).toEqual([
[{s: 1, n: 5, d: 4}, 'third'],
[{s: 1, n: 3, d: 2}, 'fifth'],
[{s: 1, n: 2, d: 1}, 'octave'],
]);
});

it('plays nicely with mixed keys and mixed data', () => {
class OtherHashable extends Hashable {
name: string;
constructor(name: string) {
super();
this.name = name;
}
valueOf(): string {
return this.name;
}
strictEquals(other: Hashable | Primitive): boolean {
if (other instanceof OtherHashable) {
return this.name === other.name;
}
return false;
}
}
const hashMap = new HashMap<
number | string | Fraction | OtherHashable,
string | number | boolean
>([
[1, 'one'],
[F(1), 'other one'],
['1', 1],
[Object.freeze(new OtherHashable('1')), true],
]);
expect(hashMap.get(1)).toBe('one');
expect(hashMap.get(F('7/7'))).toBe('other one');
expect(hashMap.get('1')).toBe(1);
expect(hashMap.get(Object.freeze(new OtherHashable('1')))).toBe(true);
});
});
36 changes: 35 additions & 1 deletion src/fraction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

import {valueToCents} from './conversion';
import {PRIMES} from './primes';
import {Hashable} from './hashable';
import {DeepReadonly, Primitive} from 'ts-essentials';

export type UnsignedFraction = {n: number; d: number};

Expand Down Expand Up @@ -76,7 +78,7 @@ export function mmod(a: number, b: number) {
* new Fraction("13e-3"); // scientific notation
* ```
*/
export class Fraction {
export class Fraction extends Hashable {
/** Sign: +1, 0 or -1 */
s: number;
/** Numerator */
Expand All @@ -85,6 +87,7 @@ export class Fraction {
d: number;

constructor(numerator: FractionValue, denominator?: number) {
super();
if (denominator !== undefined) {
if (typeof numerator !== 'number') {
throw new Error('Numerator must be a number when denominator is given');
Expand Down Expand Up @@ -831,6 +834,24 @@ export class Fraction {
}
}

/**
* Check if two {@link Fraction} instances represent the same rational number.
* Returns `false` for other {@link Hashable} objects.
* Examples:
* ```ts
* new Fraction("19.7").strictEquals(new Fraction("98/5")) // false
* new Fraction("19.6").strictEquals(new Fraction("98/5")) // true
* new Fraction("19.5").strictEquals(new Fraction("98/5")) // false
* ```
**/
strictEquals(other: Hashable | Primitive) {
if (other instanceof Fraction) {
const {s, n, d} = other;
return this.s === s && this.n === n && this.d === d;
}
return false;
}

/**
* Check if two rational numbers are divisible
* (i.e. this is an integer multiple of other)
Expand Down Expand Up @@ -1114,3 +1135,16 @@ export class Fraction {
return other_.pow(exponent);
}
}

/**
* Construct an immutable {@link Fraction} instance suitable for a key in a {@link HashMap} or a value in {@link HashSet}.
* @param numerator Integer numerator, floating point value, string or another {@link Fraction} to convert.
* @param denominator Integer denominator (defaults to `1`).
* @returns An immutable rational number.
*/
export function F(
numerator: FractionValue,
denominator?: number
): DeepReadonly<Fraction> {
return Object.freeze(new Fraction(numerator, denominator));
}
Loading
Loading