From 1052d16ff1b0c34f66e134e484e4dd2602331e43 Mon Sep 17 00:00:00 2001 From: Lumi Pakkanen Date: Wed, 10 Apr 2024 15:28:17 +0300 Subject: [PATCH] Implement HashMap similar to Python's dict Implement an abstract base class for values hashable by their numeric value and distinguishable by strict equality. Implement HashMap as an associative data structure for hashable values. Implement HashSet as a collection of hashable values. Replace FractionSet implementation with HashSet. --- src/__tests__/index.spec.ts | 23 ++++- src/fraction.ts | 18 +++- src/hashable.ts | 201 ++++++++++++++++++++++++++++++++++++ src/index.ts | 47 +-------- 4 files changed, 239 insertions(+), 50 deletions(-) create mode 100644 src/hashable.ts diff --git a/src/__tests__/index.spec.ts b/src/__tests__/index.spec.ts index 1ebb389..f548b1a 100644 --- a/src/__tests__/index.spec.ts +++ b/src/__tests__/index.spec.ts @@ -1,7 +1,6 @@ import {describe, it, expect} from 'vitest'; import { Fraction, - FractionSet, arraysEqual, binomial, ceilPow2, @@ -20,6 +19,7 @@ import { norm, valueToCents, } from '../index'; +import {HashMap, HashSet} from '../hashable'; describe('Array equality tester', () => { it('works on integer arrays', () => { @@ -207,7 +207,7 @@ describe('Farey sequence generator', () => { }); it('agrees with the brute force method', () => { - const everything = new FractionSet(); + const everything = new HashSet(); const N = Math.floor(Math.random() * 50) + 1; for (let d = 1; d <= N; ++d) { for (let n = 0; n <= d; ++n) { @@ -254,7 +254,7 @@ describe('Farey interior generator', () => { }); it('agrees with the brute force method', () => { - const everything = new FractionSet(); + const everything = new HashSet(); const N = Math.floor(Math.random() * 50) + 1; for (let d = 1; d <= N; ++d) { for (let n = 1; n < d; ++n) { @@ -382,3 +382,20 @@ describe('Constant structure checker with a margin of equivalence', () => { expect(hasMarginConstantStructure([1199, 1200], 2)).toBe(false); }); }); + +describe('Fractions as hashmap keys', () => { + it('can override values using equivalent keys', () => { + const hashMap = new HashMap([ + [new Fraction(3, 2), 'fif'], + [new Fraction(2), 'octave'], + ]); + hashMap.set(new Fraction('5/4'), 'third'); + hashMap.set(new Fraction(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'], + ]); + }); +}); diff --git a/src/fraction.ts b/src/fraction.ts index 8500f8c..2f3a09a 100644 --- a/src/fraction.ts +++ b/src/fraction.ts @@ -3,6 +3,7 @@ import {valueToCents} from './conversion'; import {PRIMES} from './primes'; +import {Hashable} from './hashable'; export type UnsignedFraction = {n: number; d: number}; @@ -76,7 +77,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 */ @@ -85,6 +86,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'); @@ -831,6 +833,20 @@ export class Fraction { } } + /** + * Check if two {@link Fraction} instances represent the same rational number. + * 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: Fraction) { + const {s, n, d} = other; + return this.s === s && this.n === n && this.d === d; + } + /** * Check if two rational numbers are divisible * (i.e. this is an integer multiple of other) diff --git a/src/hashable.ts b/src/hashable.ts new file mode 100644 index 0000000..562645b --- /dev/null +++ b/src/hashable.ts @@ -0,0 +1,201 @@ +/** + * A hashable type expected to remain immutable so that it can be used as a dictionary / hash map key. + */ +export abstract class Hashable { + abstract strictEquals(other: Hashable): boolean; + + abstract valueOf(): number; +} + +/** + * Python style dictionary implementation using hashable keys. + */ +export class HashMap implements Map { + private map: Map; + + constructor(entries?: Iterable<[Key, Value]>) { + this.map = new Map(); + if (entries === undefined) { + return; + } + for (const [key, value] of entries) { + this.set(key, value); + } + } + + set(key: Key, value: Value) { + const hash = key.valueOf(); + const subEntries = this.map.get(hash) ?? []; + for (let i = 0; i < subEntries.length; ++i) { + const [existing] = subEntries[i]; + if (key.strictEquals(existing)) { + subEntries[i] = [key, value]; + return this; + } + } + subEntries.push([key, value]); + this.map.set(hash, subEntries); + return this; + } + + get(key: Key) { + const hash = key.valueOf(); + const subEntries = this.map.get(hash); + if (subEntries === undefined) { + return undefined; + } + for (const [existing, value] of subEntries) { + if (existing.strictEquals(key)) { + return value; + } + } + return undefined; + } + + clear(): void { + this.map.clear(); + } + + delete(key: Key): boolean { + const hash = key.valueOf(); + const subEntries = this.map.get(hash); + if (subEntries === undefined) { + return false; + } + for (let i = 0; i < subEntries.length; ++i) { + const [existing] = subEntries[i]; + if (existing.strictEquals(key)) { + subEntries.splice(i, 1); + if (!subEntries.length) { + this.map.delete(hash); + } + return true; + } + } + return false; + } + + has(key: Key): boolean { + const hash = key.valueOf(); + const subEntries = this.map.get(hash); + if (subEntries === undefined) { + return false; + } + for (const [existing] of subEntries) { + if (existing.strictEquals(key)) { + return true; + } + } + return false; + } + + get size(): number { + let total = 0; + for (const subEntries of this.map) { + total += subEntries.length; + } + return total; + } + + *entries(): IterableIterator<[Key, Value]> { + for (const subEntries of this.map.values()) { + for (const entry of subEntries) { + yield entry; + } + } + } + + *keys(): IterableIterator { + for (const subEntries of this.map.values()) { + for (const [key] of subEntries) { + yield key; + } + } + } + + *values(): IterableIterator { + for (const subEntries of this.map.values()) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + for (const [_, value] of subEntries) { + yield value; + } + } + } + + *[Symbol.iterator]() { + yield* this.entries(); + } + + get [Symbol.toStringTag]() { + return 'HashMap'; + } + + forEach( + callbackfn: (value: Value, key: Key, map: HashMap) => void, + thisArg?: any + ): void { + callbackfn = callbackfn.bind(thisArg); + for (const subEntries of this.map.values()) { + for (const [key, value] of subEntries) { + callbackfn(value, key, this); + } + } + } +} + +/** + * Python style set implementation using hashable values. + */ +export class HashSet implements Set { + private hashmap: HashMap; + constructor(values?: Iterable) { + this.hashmap = new HashMap(); + if (values === undefined) { + return; + } + for (const value of values) { + this.hashmap.set(value, undefined); + } + } + add(value: T) { + this.hashmap.set(value, undefined); + return this; + } + clear(): void { + this.hashmap.clear(); + } + delete(value: T): boolean { + return this.hashmap.delete(value); + } + has(value: T): boolean { + return this.hashmap.has(value); + } + get size() { + return this.hashmap.size; + } + *entries(): IterableIterator<[T, T]> { + for (const key of this.hashmap.keys()) { + yield [key, key]; + } + } + *keys(): IterableIterator { + yield* this.hashmap.keys(); + } + *values(): IterableIterator { + yield* this.hashmap.keys(); + } + *[Symbol.iterator]() { + yield* this.hashmap.keys(); + } + + get [Symbol.toStringTag]() { + return 'HashSet'; + } + forEach( + callbackfn: (value: T, value2: T, set: Set) => void, + thisArg?: any + ): void { + callbackfn = callbackfn.bind(thisArg); + this.hashmap.forEach((_, key) => callbackfn(key, key, this)); + } +} diff --git a/src/index.ts b/src/index.ts index 2e914f8..908f5e1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,6 +6,7 @@ export * from './conversion'; export * from './combinations'; export * from './monzo'; export * from './approximation'; +export * from './hashable'; export interface AnyArray { [key: number]: any; @@ -124,52 +125,6 @@ export function iteratedEuclid(params: Iterable) { return coefs; } -/** - * Collection of unique fractions. - */ -export class FractionSet extends Set { - /** - * Check `value` membership. - * @param value Value to check for membership. - * @returns A boolean asserting whether an element is present with the given value in the `FractionSet` object or not. - */ - has(value: Fraction) { - for (const other of this) { - if (other.equals(value)) { - return true; - } - } - return false; - } - - /** - * Appends `value` to the `FractionSet` object. - * @param value Value to append. - * @returns The `FractionSet` object with added value. - */ - add(value: Fraction) { - if (this.has(value)) { - return this; - } - super.add(value); - return this; - } - - /** - * Removes the element associated to the `value`. - * @param value Value to remove. - * @returns A boolean asserting whether an element was successfully removed or not. `FractionSet.prototype.has(value)` will return `false` afterwards. - */ - delete(value: Fraction) { - for (const other of this) { - if (other.equals(value)) { - return super.delete(other); - } - } - return false; - } -} - // https://stackoverflow.com/a/37716142 // step 1: a basic LUT with a few steps of Pascal's triangle const BINOMIALS = [