Skip to content

Commit

Permalink
Implement HashMap similar to Python's dict
Browse files Browse the repository at this point in the history
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<Fraction>.
  • Loading branch information
frostburn committed Apr 10, 2024
1 parent 0536280 commit 1052d16
Show file tree
Hide file tree
Showing 4 changed files with 239 additions and 50 deletions.
23 changes: 20 additions & 3 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 @@ -20,6 +19,7 @@ import {
norm,
valueToCents,
} from '../index';
import {HashMap, HashSet} from '../hashable';

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

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

it('agrees with the brute force method', () => {
const everything = new FractionSet();
const everything = new HashSet<Fraction>();
const N = Math.floor(Math.random() * 50) + 1;
for (let d = 1; d <= N; ++d) {
for (let n = 1; n < d; ++n) {
Expand Down Expand Up @@ -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'],
]);
});
});
18 changes: 17 additions & 1 deletion src/fraction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import {valueToCents} from './conversion';
import {PRIMES} from './primes';
import {Hashable} from './hashable';

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

Expand Down Expand Up @@ -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 */
Expand All @@ -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');
Expand Down Expand Up @@ -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)
Expand Down
201 changes: 201 additions & 0 deletions src/hashable.ts
Original file line number Diff line number Diff line change
@@ -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<Key extends Hashable, Value> implements Map<Key, Value> {
private map: Map<number, [Key, Value][]>;

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<Key> {
for (const subEntries of this.map.values()) {
for (const [key] of subEntries) {
yield key;
}
}
}

*values(): IterableIterator<Value> {
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<Key, Value>) => 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<T extends Hashable> implements Set<T> {
private hashmap: HashMap<T, undefined>;
constructor(values?: Iterable<T>) {
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<T> {
yield* this.hashmap.keys();
}
*values(): IterableIterator<T> {
yield* this.hashmap.keys();
}
*[Symbol.iterator]() {
yield* this.hashmap.keys();
}

get [Symbol.toStringTag]() {
return 'HashSet';
}
forEach(
callbackfn: (value: T, value2: T, set: Set<T>) => void,
thisArg?: any
): void {
callbackfn = callbackfn.bind(thisArg);
this.hashmap.forEach((_, key) => callbackfn(key, key, this));
}
}
47 changes: 1 addition & 46 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -124,52 +125,6 @@ export function iteratedEuclid(params: Iterable<number>) {
return coefs;
}

/**
* Collection of unique fractions.
*/
export class FractionSet extends Set<Fraction> {
/**
* 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 = [
Expand Down

0 comments on commit 1052d16

Please sign in to comment.