Skip to content

Commit cbdc497

Browse files
broofasindresorhusShynRou
committed
Add BigInt support and Unicode handling (#9)
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com> Co-authored-by: Dustin Hagemeier <dustin@commit.international>
1 parent 7a72892 commit cbdc497

File tree

5 files changed

+121
-24
lines changed

5 files changed

+121
-24
lines changed

index.d.ts

+2-5
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,12 @@ declare const fnv1a: {
99
import fnv1a = require('@sindresorhus/fnv1a');
1010
1111
fnv1a('🦄🌈');
12-
//=> 582881315
12+
//=> 2868248295
1313
```
1414
*/
1515
(string: string): number;
1616

17-
// TODO: remove this in the next major version, refactor the whole definition to:
18-
// declare function fnv1a(string: string): number;
19-
// export = fnv1a;
20-
default: typeof fnv1a;
17+
bigInt(string: string): BigInt;
2118
};
2219

2320
export = fnv1a;

index.js

+70-9
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,81 @@
11
'use strict';
2-
const OFFSET_BASIS_32 = 2166136261;
32

4-
const fnv1a = string => {
5-
let hash = OFFSET_BASIS_32;
3+
// FNV_PRIMES and FNV_OFFSETS from
4+
// http://www.isthe.com/chongo/tech/comp/fnv/index.html#FNV-param
5+
//
6+
// Defining these as strings instead of BigInt literals avoids syntax errors on
7+
// legacy platforms.
68

7-
for (let i = 0; i < string.length; i++) {
8-
hash ^= string.charCodeAt(i);
9+
const FNV_PRIMES = {
10+
32: '16777619',
11+
64: '1099511628211',
12+
128: '309485009821345068724781371',
13+
256: '374144419156711147060143317175368453031918731002211',
14+
512: '35835915874844867368919076489095108449946327955754392558399825615420669938882575126094039892345713852759',
15+
1024: '5016456510113118655434598811035278955030765345404790744303017523831112055108147451509157692220295382716162651878526895249385292291816524375083746691371804094271873160484737966720260389217684476157468082573'
16+
};
17+
18+
const FNV_OFFSETS = {
19+
32: '2166136261',
20+
64: '14695981039346656037',
21+
128: '144066263297769815596495629667062367629',
22+
256: '100029257958052580907070968620625704837092796014241193945225284501741471925557',
23+
512: '9659303129496669498009435400716310466090418745672637896108374329434462657994582932197716438449813051892206539805784495328239340083876191928701583869517785',
24+
1024: '14197795064947621068722070641403218320880622795441933960878474914617582723252296732303717722150864096521202355549365628174669108571814760471015076148029755969804077320157692458563003215304957150157403644460363550505412711285966361610267868082893823963790439336411086884584107735010676915'
25+
};
26+
27+
// Legacy implementation for 32-bit + Number types, older systems that don't
28+
// support BigInt
29+
function fnv1a(str) {
30+
// Handle unicode code points > 0x7f
31+
let hash = Number(FNV_OFFSETS[32]);
32+
let unicoded = false;
33+
for (let i = 0; i < str.length; i++) {
34+
let v = str.charCodeAt(i);
35+
// Non-ASCII char triggers unicode escape logic
36+
if (v > 0x7F && !unicoded) {
37+
str = unescape(encodeURIComponent(str));
38+
v = str.charCodeAt(i);
39+
unicoded = true;
40+
}
941

10-
// 32-bit FNV prime: 2**24 + 2**8 + 0x93 = 16777619
11-
// Using bitshift for accuracy and performance. Numbers in JS suck.
42+
hash ^= v;
1243
hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
1344
}
1445

1546
return hash >>> 0;
16-
};
47+
}
48+
49+
function bigInt(str, {size} = {size: 32}) {
50+
if (typeof (BigInt) === 'undefined') {
51+
throw new TypeError('BigInt is not supported');
52+
}
53+
54+
if (!FNV_PRIMES[size]) {
55+
throw new Error('`size` must be one of 32, 64, 128, 256, 512, or 1024');
56+
}
57+
58+
let hash = BigInt(FNV_OFFSETS[size]);
59+
const prime = BigInt(FNV_PRIMES[size]);
60+
61+
// Handle unicode code points > 0x7f
62+
let unicoded = false;
63+
for (let i = 0; i < str.length; i++) {
64+
let v = str.charCodeAt(i);
65+
// Non-ASCII char triggers unicode escape logic
66+
if (v > 0x7F && !unicoded) {
67+
str = unescape(encodeURIComponent(str));
68+
v = str.charCodeAt(i);
69+
unicoded = true;
70+
}
71+
72+
hash ^= BigInt(v);
73+
hash = BigInt.asUintN(size, hash * prime);
74+
}
75+
76+
return hash;
77+
}
1778

1879
module.exports = fnv1a;
19-
// TODO: remove this in the next major version, refactor the whole definition to:
80+
module.exports.bigInt = bigInt;
2081
module.exports.default = fnv1a;

package.json

+8-3
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,13 @@
3636
"vo"
3737
],
3838
"devDependencies": {
39-
"ava": "^1.4.1",
40-
"tsd": "^0.7.1",
41-
"xo": "^0.24.0"
39+
"ava": "2.4.0",
40+
"tsd": "0.11.0",
41+
"xo": "0.25.3"
42+
},
43+
"xo": {
44+
"globals": [
45+
"BigInt"
46+
]
4247
}
4348
}

readme.md

+16-3
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,31 @@ FNV hashes are designed to be fast while maintaining a low collision rate. The F
1313
$ npm install @sindresorhus/fnv1a
1414
```
1515

16-
1716
## Usage
1817

18+
### fnv1a(string)
19+
1920
```js
2021
const fnv1a = require('@sindresorhus/fnv1a');
2122

2223
fnv1a('🦄🌈');
23-
//=> 582881315
24+
//=> 2868248295
2425
```
2526

26-
It returns the hash as a positive integer.
27+
It returns the hash as a 32-bit positive Number.
28+
29+
### fnv1a.bigInt(string, [{size}])
30+
31+
On systems that support BigInt, this method may be called to generate larger hashes. This method throws if `BigInt` is not available, however.
32+
33+
```js
34+
const fnv1a = require('@sindresorhus/fnv1a');
35+
36+
fnv1a.bigInt('hello world', {size: 128});
37+
//=> 143667438548887148232425432707801491127n
38+
```
2739

40+
It returns the hash as a `size`-bit positive BigInt.
2841

2942
## Related
3043

test.js

+25-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import test from 'ava';
2-
import fnv1a from '.';
2+
import fnv1a, {bigInt} from '.';
33

4-
test('main', t => {
4+
test('default', t => {
5+
// Test 32-bit for various strings
56
t.is(fnv1a(''), 2166136261);
6-
t.is(fnv1a('🦄🌈'), 582881315);
7-
87
t.is(fnv1a('h'), 3977000791);
98
t.is(fnv1a('he'), 1547363254);
109
t.is(fnv1a('hel'), 179613742);
@@ -17,5 +16,27 @@ test('main', t => {
1716
t.is(fnv1a('hello worl'), 2767971961);
1817
t.is(fnv1a('hello world'), 3582672807);
1918

19+
// Bigger test
2020
t.is(fnv1a('Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium.'), 2964896417);
21+
22+
// Verify unicode handling against values from https://www.tools4noobs.com/online_tools/hash/
23+
t.is(fnv1a('🦄🌈'), 0xAAF5FEE7);
24+
t.is(fnv1a('\u{0000}\u{0080}\u{0100}\u{0180}\u{0250}\u{02b0}\u{0300}\u{0370}\u{0400}\u{0500}\u{0530}\u{0590}\u{0600}\u{0700}\u{0780}\u{0900}\u{0980}\u{0a00}\u{0a80}\u{0b00}\u{0b80}\u{0c00}\u{0c80}\u{0d00}\u{0d80}\u{0e00}\u{0e80}\u{0f00}\u{1000}\u{10a0}\u{1100}\u{1200}\u{13a0}\u{1400}\u{1680}\u{16a0}\u{1700}\u{1720}\u{1740}\u{1760}\u{1780}\u{1800}\u{1900}\u{1950}\u{19e0}\u{1d00}\u{1e00}\u{1f00}\u{2000}\u{2070}\u{20a0}\u{20d0}\u{2100}\u{2150}\u{2190}\u{2200}\u{2300}\u{2400}\u{2440}\u{2460}\u{2500}\u{2580}\u{25a0}\u{2600}\u{2700}\u{27c0}\u{27f0}\u{2800}\u{2900}\u{2980}\u{2a00}\u{2b00}\u{2e80}\u{2f00}\u{2ff0}\u{3000}\u{3040}\u{30a0}\u{3100}\u{3130}\u{3190}\u{31a0}\u{31f0}\u{3200}\u{3300}\u{3400}\u{4dc0}\u{4e00}\u{a000}\u{a490}\u{ac00}\u{d800}\u{dc00}\u{e000}\u{f900}\u{fb00}\u{fb50}\u{fe00}\u{fe20}\u{fe30}\u{fe50}\u{fe70}\u{ff00}\u{fff0}\u{10000}\u{10080}\u{10100}\u{10300}\u{10330}\u{10380}\u{10400}\u{10450}\u{10480}\u{10800}\u{1d000}\u{1d100}\u{1d300}\u{1d400}\u{20000}\u{2f800}\u{e0000}\u{e0100}'), 0x983FDF05);
2125
});
26+
27+
// If BigInt support available ...
28+
if (typeof (BigInt) === 'undefined') {
29+
console.warn('BigInt not supported - skipping fnv1a.bigInt() tests');
30+
} else {
31+
test('bigInt()', t => {
32+
// Sanity check larger hashes against values from
33+
// https://fnvhash.github.io/fnv-calculator-online/
34+
35+
t.is(bigInt('hello world', {size: 32}), BigInt('0xd58b3fa7'));
36+
t.is(bigInt('hello world', {size: 64}), BigInt('0x779a65e7023cd2e7'));
37+
t.is(bigInt('hello world', {size: 128}), BigInt('0x6c155799fdc8eec4b91523808e7726b7'));
38+
t.is(bigInt('hello world', {size: 256}), BigInt('0xecc3cf2e0edfccd3d87f21ec0883aad4db43eead66ce09eb4a97e04e1a184527'));
39+
t.is(bigInt('hello world', {size: 512}), BigInt('0x2b9c19ec56ccf98da0f227cc82bfaacbd8350928bd2ceacae7bc8aa13e747f5c43ca4e2e98fc25e94e4e805675545ee95a3b968c0acfaecb90aea2fdbcd4de0f'));
40+
t.is(bigInt('hello world', {size: 1024}), BigInt('0x3fa9d253e52ae80105b382c80a01e27a53d7bc1d201efb47b38f4d6e465489829d7d272127d20e1076129c00000000000000000000000000000000000000000000000000000000000000000000000000000253eb20f42a7228af9022d9f35ece5bb71e40fcd8717b80d164ab921709996e5c43aae801418e878cddf968d4616f'));
41+
});
42+
}

0 commit comments

Comments
 (0)