Skip to content
Merged
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
15 changes: 15 additions & 0 deletions docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,21 @@ new Trie<string>( data, {
} );
```

### A Quick Word on Hashing
<p>This Trie implementation employs a bespoke HashMap implementation under the hood to enhance data retrieval. It applies a default hashing algorithm as part of this HashMap.</p>
<p>For data objects with <code>hashCode</code> property, that <code>hashCode</code> property will be used instead. Both constant <code>hashCode</code> property and <code>hashCode</code> method types are recognized.</p>

```tsx
new Trie([
[
{ ..., hashCode(){ return <some value> } },
{ ..., hashCode: <some value> },
...
],
...
]);
```

## Properties

### isEmpty
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,5 @@
"test:watch": "jest --updateSnapshot --watchAll"
},
"types": "dist/index.d.ts",
"version": "0.1.2"
"version": "0.2.0"
}
62 changes: 60 additions & 2 deletions src/main.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import type { TrieableNode } from './main';
import type { EqualityFn, TrieableNode } from './main';

import {
afterAll,
beforeAll,
describe,
expect,
jest,
test
} from '@jest/globals';

Expand All @@ -18,6 +19,7 @@ import {
} from './test-artifacts/test-data';

import Trie, {
ChildNodes,
LT_TYPES_MSG,
LT_ARG_TYPES_MISMATCH_MSG,
Node,
Expand Down Expand Up @@ -333,7 +335,7 @@ describe( 'Trie class', () => {
typeDesc, data, expectedErrorMsg
) => {
expect(() => new Trie<unknown>( data, { sorted: true }) ).toThrow( expectedErrorMsg );
const lessThanMatcher = jest.fn().mockReturnValue( true );
const lessThanMatcher = jest.fn().mockReturnValue( true ) as EqualityFn;
new Trie( data, { lessThanMatcher, sorted: true });
expect( lessThanMatcher ).toHaveBeenCalled();
} );
Expand Down Expand Up @@ -876,4 +878,60 @@ describe( 'Trie class', () => {

} );

describe( "deference to data's own hashCode property whenever present", () => {
class Int {
v : number;
constructor( v ) { this.v = v }
}
class TNodes extends ChildNodes<Int> {
getCodes() { return this.codes }
getKeys() { return this.keys }
constructor() { super( ( a, b ) => a.v === ( b as Int ).v ) }
indexOf( data : Int ) { return this.keys.findIndex( k => this.isEqualValue( data, k ) ) }
protected _optForKeyLocator( key: Int ) { return true }
}
test( 'if hashCode function, resolve and use the computed value', () => {
const hashMock = jest.fn();
class TestInt extends Int {
hashCode() {
hashMock();
return this.v;
}
}
const getInt = v => new Node( new TestInt( v ) as Int );
const childNodes = new TNodes();
const zeroInt = getInt( 0 );
childNodes.set( getInt( 5 ) );
childNodes.set( getInt( 32 ) );
childNodes.set( zeroInt );
childNodes.set( getInt( -1 ) );
childNodes.set( zeroInt );
expect( hashMock ).toHaveBeenCalledTimes( 5 );
hashMock.mockClear();
expect( hashMock ).not.toHaveBeenCalled();
childNodes.set( getInt( 88 ) );
childNodes.set( getInt( 64 ) );
expect( hashMock ).toHaveBeenCalledTimes( 2 );
expect( childNodes.getCodes() ).toHaveLength( 6 );
expect( new Set( childNodes.getCodes() ).size ).toBe( 6 );
} );

test( 'if constant hashCode, simple use the value as-is', () => {
class TestInt extends Int { hashCode = 1024 }
const getInt = v => new Node( new TestInt( v ) as Int );
const childNodes = new TNodes();
const zeroInt = getInt( 0 );
childNodes.set( getInt( 5 ) );
childNodes.set( getInt( 32 ) );
childNodes.set( zeroInt );
childNodes.set( getInt( -1 ) );
childNodes.set( zeroInt );
childNodes.set( getInt( 88 ) );
childNodes.set( getInt( 64 ) );
expect( childNodes.getCodes() ).toHaveLength( 6 );
expect( childNodes.getCodes().every( c => c === 1024 ) ).toBe( true );
} );

} );

} );
63 changes: 39 additions & 24 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -569,7 +569,7 @@ export class Node<T = unknown> {

Node.prototype[ Symbol.toStringTag ] = NODE_DESC;

abstract class ChildNodes<T = unknown> {
export abstract class ChildNodes<T = unknown> {
protected codes : Array<number>;
protected keys : Array<T>;
protected buckets : Array<Array<[Node<T>, number]>>; // [node, keys Index]
Expand All @@ -593,14 +593,6 @@ abstract class ChildNodes<T = unknown> {
}
return this._get( key, code );
}
abstract indexOf( data : T ) : number;
remove( node : Node<T> ) {
const { code, bucketIndex } = this._optForKeyLocator( node.data )
? this._getEntryByKeyIndex( this.indexOf( node.data ) )
: this._getEntry( node.data )
this._splice( code, bucketIndex );
}
set( node : Node<T> ) { this._set( node ) }
list() {
const nodes : Array<Node<T>> = [];
for( let codes = this.codes, cLen = codes.length, c = 0; c < cLen; c++ ) {
Expand All @@ -613,6 +605,14 @@ abstract class ChildNodes<T = unknown> {
}
return nodes;
}
abstract indexOf( data : T ) : number;
remove( node : Node<T> ) {
const { code, bucketIndex } = this._optForKeyLocator( node.data )
? this._getEntryByKeyIndex( this.indexOf( node.data ) )
: this._getEntry( node.data )
this._splice( code, bucketIndex );
}
set( node : Node<T> ) { this._set( node ) }
protected _get( key : T, code : number = null ) {
const { value } = this._getEntry( key, code );
if( value === null ) { return null }
Expand Down Expand Up @@ -732,36 +732,51 @@ class SortedChildNodes<T = unknown> extends ChildNodes<T> {
function robustHash<T>( key : T ) { return Math.abs( runHash( key ) ) }
function runHash<T>(
key : T,
hash = 0,
visited : Array<unknown> = []
) {
if( key === null ) { return hash }
if( key === null ) { return 0 }
switch( typeof key ) {
case 'string': {
for( let k = ( key as string ).length; k--; ) {
// Multiply by prime and use bitwise OR to ensure 32-bit int.
hash = ( hash * 31 + ( key as string ).charCodeAt( k ) ) | 0;
}
return hash;
};
case 'number': return hash + ( key as number | 0 );
case 'boolean': return hash + ( key ? 1 : 0 );
case 'undefined': return hash;
case 'string': return stringHash( key as string );
case 'number': return key as number | 0;
case 'boolean': return key ? 1 : 0;
case 'function': return stringHash( key.toString() )
case 'undefined': return 0;
case 'symbol': return stringHash( key.description );
}
{
const _key = key as Record<string, unknown>
if( 'hashCode' in _key ) {
return runHash(
typeof _key.hashCode === 'function'
? _key.hashCode()
: _key.hashCode
);
}
}
const { desc, index } = bSearch( key, visited );
if( desc === Compared.EQ ) { return hash }
if( desc === Compared.EQ ) { return 0 }
visited.splice( index + ( desc === Compared.LT ? 1 : 0 ), 0, key );
let hash = 0
if( Array.isArray( key ) ) {
for( let k = key.length; k--; ) {
hash = runHash( key[ k ], hash, visited );
hash += runHash( key[ k ], visited );
}
return hash;
}
for( let keys = Object.keys( key ).sort(), k = keys.length; k--; ) {
hash = runHash( key[ keys[ k ] ], hash, visited );
if( keys[ k ] === 'hashCode' ) { continue }
hash += runHash( key[ keys[ k ] ], visited );
}
return hash;
}
function stringHash( key : string ) {
let hash = 0;
for( let k = ( key as string ).length; k--; ) {
// Multiply by prime and use bitwise OR to ensure 32-bit int.
hash = ( hash * 31 + ( key as string ).charCodeAt( k ) ) | 0;
}
return hash;
};

function bSearch<T = unknown>(
needle : T,
Expand Down