├── .gitignore ├── .travis.yml ├── README.md ├── bench.js ├── index.js ├── package.json └── test ├── decode.js ├── encode.js └── integration.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.8" 4 | - "0.10" 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # binary-sortable-hash 2 | 3 | Hash arrays of numbers into a binary string from which you can reconstruct the 4 | original values, with configurable precision loss. The generated hashes 5 | **sort well**, so similar input values cause large shared prefixes in hashes. (as seen in [geohashing](http://en.wikipedia.org/wiki/Geohash)) 6 | 7 | ```js 8 | sortable.encode([0]) === '011111111111111111111111111111111111111111111111111111111111'; 9 | sortable.decode(011111111111111111111111111111111111111111111111111111111111, 1) === -8.673617379884035e-17 10 | sortable.encode([10, 11, -10]) === '110001001001110110011001100100001011100110001001100110011011'; 11 | ``` 12 | 13 | [![build status](https://secure.travis-ci.org/rt2zz/binary-sortable-hash.png)](http://travis-ci.org/rt2zz/binary-sortable-hash) 14 | 15 | ## Usage 16 | 17 | Hash a lat/lon array representing Röcken Germany `{lat: 51.2408, lon: 12.1161}` and then restore it, using different 18 | hash sizes. 19 | 20 | ```js 21 | var sortable = require('sortable-hash'); 22 | 23 | var rocken = {lat: 51.2408, lon: 12.1161} 24 | //normalize scalars to +-100 25 | var normalized = [100*rocken.lat/180, 100*rocken.lon/90] 26 | 27 | var hash = sortable.encode(normalized) 28 | // => '110010010010000100101111010001010001001110011100001100111111' 29 | 30 | var decoded = sortable.decode(hash, 2); 31 | var restoredRockenCoords = { lat: decoded[0]*180/100, lon: decoded[1]*90/100 } 32 | // => { lat: 51.24080015346408, lon: 12.116099959239364 } 33 | 34 | var lowerFidelityHash = sortable.encode(normalized, 10) 35 | // => 1100100100 36 | var decoded = sortable.decode(lowerFidelityHash, 2); 37 | var restoredRockenCoords = { lat: decoded[0]*180/100, lon: decoded[1]*90/100 } 38 | // => { lat: 50.625, lon: 14.0625 } 39 | 40 | var hexadecimalHash = parseInt(hash, 2).toString(16) 41 | // => c9212f45139c300 (limited precision beyond 53bits) 42 | ``` 43 | 44 | **note:** If you need precision when converting integers above 53 bits consider using [bigint](https://github.com/substack/node-bigint) 45 | 46 | ## API 47 | 48 | ### sortable.encode(values[, options]) 49 | 50 | Hash the array `values`, which may only contain Numbers in the range of 51 | `[-100, 100]`. 52 | 53 | `options` can either be an object with these possible keys: 54 | 55 | * `precision`: Number of bits (read: length) of the resulting binary hash 56 | 57 | or a Number, in which case it sets `options.precision`. 58 | 59 | ### sortable.decode(string, options) 60 | 61 | Decode `string` into an Array of Numbers. 62 | 63 | `options` can either be an object with these possible keys: 64 | 65 | * `num`: number of elements initially passed to `hash.encode`. (required) 66 | 67 | Or a Number, in which case it sets `options.num` 68 | 69 | ## Installation 70 | 71 | With [npm](http://npmjs.org) do: 72 | 73 | ```bash 74 | $ npm install binary-sortable-hash 75 | ``` 76 | 77 | ## Kudos 78 | 79 | This is the idea of [geohashes](http://en.wikipedia.org/wiki/Geohash) 80 | generalized for use with all numeric data and numbers of input fields. 81 | 82 | binary-sortable-hash is a derivative of the awesome [sortable-hash](https://github.com/juliangruber/sortable-hash) by @juliangruber 83 | -------------------------------------------------------------------------------- /bench.js: -------------------------------------------------------------------------------- 1 | var hash = require('./'); 2 | var ben = require('ben'); 3 | 4 | var ms = ben(100000, function () { 5 | hash.encode([10, 10, 10], {precision: 60}); 6 | }); 7 | console.log('Encode: %s ms/op', ms); 8 | 9 | // bin to dec 10 | var decms = ben(100000, function () { 11 | parseInt(hash.encode([10, 10, 10]), 2); 12 | }); 13 | console.log('Encode to Dec: %s ms/op', decms); 14 | 15 | //bin to dec to 32 16 | var ttms = ben(100000, function () { 17 | parseInt(hash.encode([10, 10, 10]), 2).toString(32); 18 | }); 19 | console.log('Encode to Dec to 32: %s ms/op', ttms); 20 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var Hash = {} 2 | 3 | Hash.decode = function (hash, opts) { 4 | if (typeof opts == 'undefined') throw new Error('`num` or `opts` argument required') 5 | if (typeof opts == 'number') { 6 | opts = { num: opts } 7 | } 8 | 9 | var num = opts.num 10 | 11 | var ranges = [] 12 | for (var i = 0; i < num; i++) { 13 | ranges[i] = [-100, 100] 14 | } 15 | var id = 0 16 | 17 | for (var i = 0; i < hash.length; i++) { 18 | var range = ranges[id++ % ranges.length] 19 | var bit = hash[i] 20 | range[bit^1] = avg(range) 21 | } 22 | 23 | var averaged = [] 24 | for (var i = 0; i < ranges.length; i++) { 25 | averaged[i] = avg(ranges[i]) 26 | } 27 | return averaged 28 | } 29 | 30 | Hash.encode = function (values, opts) { 31 | if (typeof opts == 'number') { 32 | opts = { precision: opts } 33 | } 34 | if (!opts) opts = {} 35 | 36 | var precision = opts.precision || 60 37 | 38 | var ranges = [] 39 | for (var i = 0; i < values.length; i++) { 40 | if (values[i] < -100 || values[i] > 100) { 41 | throw new Error('accepted input range: [-100, 100]') 42 | } 43 | ranges[i] = [-100, 100] 44 | } 45 | 46 | var hash = '' 47 | var i = 0 48 | 49 | for(var j = 0; j < precision; j++) { 50 | var arg = i++ % values.length 51 | var range = ranges[arg] 52 | var value = values[arg] 53 | var mid = avg(range) 54 | 55 | var bit = value > mid ? 1 : 0 56 | range[bit^1] = mid 57 | 58 | //@TODO performance test of string concat vs number adding 59 | hash += bit 60 | } 61 | 62 | return hash 63 | } 64 | 65 | function avg (r) { 66 | return (r[0] + r[1]) / 2 67 | } 68 | 69 | module.exports = Hash 70 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "binary-sortable-hash", 3 | "description": "Hash an array of numbers into a sortable(ish) binary number.", 4 | "version": "0.0.1", 5 | "repository": { 6 | "type": "git", 7 | "url": "git://github.com/rt2zz/binary-sortable-hash.git" 8 | }, 9 | "homepage": "git://github.com/rt2zz/binary-sortable-hash.git", 10 | "main": "index.js", 11 | "scripts": { 12 | "test": "tape test/*.js" 13 | }, 14 | "dependencies": {}, 15 | "devDependencies": { 16 | "ben": "0.0.0", 17 | "tape": "~1.0.4" 18 | }, 19 | "keywords": [ 20 | "hash", 21 | "geohash", 22 | "sortable" 23 | ], 24 | "license": "MIT" 25 | } 26 | -------------------------------------------------------------------------------- /test/decode.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var decode = require('..').decode 3 | 4 | test('decode', function (t) { 5 | t.plan(2) 6 | 7 | t.throws(decode.bind(null, 'numValues required')) 8 | 9 | t.deepEqual(i(decode('11000100100111011001100110010000', 3)), [10, 11, -10]) 10 | }) 11 | 12 | function i (arr) { 13 | return arr.map(function (e) { 14 | return Math.round(e) 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /test/encode.js: -------------------------------------------------------------------------------- 1 | var test = require('tape'); 2 | var encode = require('..').encode; 3 | 4 | test('encode', function (t) { 5 | t.plan(2); 6 | 7 | t.throws(encode.bind(null, [-200])); 8 | 9 | t.equal(encode([10, 11, -10]), '110001001001110110011001100100001011100110001001100110011011'); 10 | }); 11 | -------------------------------------------------------------------------------- /test/integration.js: -------------------------------------------------------------------------------- 1 | var test = require('tape'); 2 | var encode = require('..').encode; 3 | var decode = require('..').decode; 4 | 5 | test('integration', function (t) { 6 | t.plan(9); 7 | 8 | t.deepEqual(i(decode(encode([10]), 1)), [10]); 9 | t.deepEqual(i(decode(encode([10], 32), 1)), [10]); 10 | t.deepEqual(i(decode(encode([10], 64), 1)), [10]); 11 | 12 | t.deepEqual(i(decode(encode([10, -10]), 2)), [10, -10]); 13 | t.deepEqual(i(decode(encode([10, -10], 32), 2)), [10, -10]); 14 | t.deepEqual(i(decode(encode([10, -10], 64), 2)), [10, -10]); 15 | 16 | t.deepEqual(i(decode(encode([10, -10, 10]), 3)), [10, -10, 10]); 17 | t.deepEqual(i(decode(encode([10, -10, 10], 32), 3)), [10, -10, 10]); 18 | t.deepEqual(i(decode(encode([10, -10, 10], 64), 3)), [10, -10, 10]); 19 | }); 20 | 21 | function i (arr) { 22 | return arr.map(function (e) { 23 | return Math.round(e); 24 | }); 25 | } 26 | --------------------------------------------------------------------------------