├── .travis.yml ├── LICENSE ├── README.md ├── chars.js ├── codec ├── number.js └── object.js ├── index.js ├── package-lock.json ├── package.json └── test ├── bench.js ├── es5.test.js ├── index.js ├── number.test.js └── object.test.js /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.6 4 | - 0.8 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Dominic Tarr 2 | 3 | Permission is hereby granted, free of charge, 4 | to any person obtaining a copy of this software and 5 | associated documentation files (the "Software"), to 6 | deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, 8 | merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom 10 | the Software is furnished to do so, 11 | subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 18 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR 20 | ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # charwise 2 | 3 | like bytewise, except as strings. 4 | 5 | codec for js values (including arrays) that preserves lexiographic 6 | sort order when encoded. (the order is compatible with [bytewise](https://github.com/deanlandolt/bytewise) and thus indexeddb and couchdb, 7 | but the encoded format is different) 8 | 9 | The api provided follows [the level codec standard](https://github.com/level/codec#encoding-format) 10 | so this encoding can easily be used with [level](https://github.com/level) 11 | and [flumedb](https://github.com/flumedb) 12 | 13 | ## motivation 14 | 15 | for building indexes on top of leveldb, bytewise is great! 16 | it lets you structure you keys and reason about how they 17 | will be ordered in a very simple and reliable way. 18 | 19 | But bytewise is too slow! it's slow enough to have quite visible 20 | effects on a bulk load on a reasonable sized database with a couple 21 | of indexes. 22 | (i.e. 100k [secure-scuttlebutt](https://github.com/ssbc/secure-scuttlebutt) messages with indexes, measured by [bench-ssb](https://github.com/ssbc/bench-ssb)) 23 | 24 | ## stability: experimental 25 | 26 | Expect breaking changes to encoded format. We are still making 27 | breaking changes if necessary to improve performance. 28 | 29 | (although, [codec api](https://github.com/level/codec#encoding-format) is fully stable and will not change) 30 | 31 | ## simple benchmark 32 | 33 | run a simple benchmark for one second, encoding & decoding ops 34 | in one second. 35 | 36 | ``` 37 | # name, ops, multiplier 38 | bytewise encode 35661 39 | charwise encode 131366 x3.6 40 | bytewise decode 107571 41 | charwise decode 144557 x1.3 42 | ``` 43 | 44 | It was easy to make charwise faster than bytewise when 45 | it was only a partial implementation, but once correct escaping 46 | and nested arrays where added it got slow. 47 | 48 | But then [@PaulBlanche](https://github.com/PaulBlanche) 49 | [had the genious idea](https://github.com/dominictarr/charwise/pull/7) 50 | of encoding items in an array with their depth inside the array. 51 | This supports deeply nested arrays or shallowly nested arrays 52 | with only one pass escaping the items. This made encoding much faster 53 | again! 54 | 55 | ## Examples 56 | 57 | ```js 58 | const level = require('level') 59 | const charwise = require('charwise') 60 | 61 | const db = level('./db8', { 62 | keyEncoding: charwise 63 | }) 64 | 65 | await db.batch([ 66 | { type: 'put', key: ['users', 2], value: 'example' }, 67 | { type: 'put', key: ['users', 10], value: 'example2' } 68 | ]) 69 | 70 | const userStream = db.createStream({ 71 | gte: ['users', charwise.LO], 72 | lte: ['users', charwise.HI] 73 | }) 74 | 75 | // This will print ['users', 2], ['users', 10] 76 | // If you dont use charwise its sorted numerically and would 77 | // print ['users', 10] , ['users', 2] 78 | userStream.on('data', console.log) 79 | ``` 80 | 81 | 82 | ## License 83 | 84 | MIT 85 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /chars.js: -------------------------------------------------------------------------------- 1 | 2 | var s = '' 3 | for(var i = 0; i < 256; i++) 4 | s+=String.fromCharCode(i) 5 | console.log(s) 6 | -------------------------------------------------------------------------------- /codec/number.js: -------------------------------------------------------------------------------- 1 | // Number is encoded in scientific notation. 2 | // A Number is composed of an exponent and a mantissa. The exponent isan integer 3 | // in [-324, 308] and the mantissa is a decimal in ]-10, 10[. 4 | // First we encode the sign as N or P, then E marking the start of the exponent. 5 | // The exponent is offseted of 500 to be positive and startpadded to 3 chars. 6 | // We endpad mantissa with enough zero to exceed mantissa precision. 7 | // Then negative numbers' mantissa and exponent are flipped (nines' complement) 8 | 9 | exports.encode = function (number) { 10 | if (isNaN(number)) { return "DaN"; } 11 | if (number === 0) { return "FE 0M0"; } 12 | if (number === Infinity) { return "FF"; } 13 | if (number === -Infinity) { return "DD"; } 14 | 15 | var splitScientificNotation = number.toExponential().split('e'); 16 | var exponent = Number(splitScientificNotation[1]) + 500; 17 | var mantissa = splitScientificNotation[0] + (splitScientificNotation[0].indexOf('.') === -1 ? '.' : '') + '0'.repeat(20); 18 | var encoded = 'E' + padStart(String(exponent), 3) + 'M' + String(mantissa); 19 | if (number > 0) { 20 | return 'F' + encoded; 21 | } else { 22 | return 'D' + flip(encoded); 23 | } 24 | } 25 | 26 | exports.decode = function (encoded) { 27 | if (encoded === 'DaN') { return NaN; } 28 | if (encoded === 'FF') { return Infinity; } 29 | if (encoded === 'DD') { return -Infinity; } 30 | 31 | var isNegative = encoded[0] === 'D'; 32 | var splitEncoded = (isNegative ? flip(encoded) : encoded).slice(2).split('M'); 33 | return Number((isNegative ? '-':'') + splitEncoded[1] + 'e' + String(Number(splitEncoded[0])-500)); 34 | } 35 | 36 | function flip(number) { 37 | var flipped = ''; 38 | for (var i = 0; i < number.length; i++) { 39 | var digit = number[i]; 40 | if (isNaN(Number(digit)) || digit === ' ') { 41 | if (digit !== '-') { flipped += digit; } 42 | } else { 43 | flipped += String(9 - Number(digit)); 44 | } 45 | } 46 | return flipped; 47 | } 48 | 49 | function padStart (str, count) { 50 | return (' ').repeat(count - str.length).substr(0,count) + str; 51 | }; 52 | -------------------------------------------------------------------------------- /codec/object.js: -------------------------------------------------------------------------------- 1 | var dictEscape = { '?': '?@', '!': '??', '"': '?%' }; 2 | function escape(str) { 3 | if (!/[!"]/.test(str)) { return str; } 4 | return str.replace(/[\?!"]/g, function (match) { 5 | return dictEscape[match]; 6 | }); 7 | 8 | } 9 | 10 | var dictUnescape = { '?@': '?', '??': '!', '?%': '"' }; 11 | function unescape(str) { 12 | if (!/\?[%\?@]/.test(str)) { return str; } 13 | return str.replace(/\?[%\?@]/g, function (match) { 14 | return dictUnescape[match]; 15 | }); 16 | } 17 | 18 | exports.factory = function (codec) { 19 | 20 | return { 21 | encode: encode, 22 | decode: decode 23 | }; 24 | 25 | function encode(array) { 26 | if (array === null) { return 'A'; } 27 | if (!Array.isArray(array)) { throw new Error('can only encode arrays'); } 28 | var l = array.length; 29 | if (l == 0) { return 'K!'; } 30 | 31 | var s = encodeItem(array[0]); 32 | for (var i = 1; i < l; i++) { 33 | s += '"' + encodeItem(array[i]); 34 | } 35 | 36 | return 'K'+ s + '!'; 37 | } 38 | 39 | function encodeItem(item) { 40 | if (typeof item === 'object') { 41 | return encode(item); 42 | } 43 | return escape(codec.encode(item)); 44 | } 45 | 46 | function decode(encoded) { 47 | if (encoded === 'A') { return null; } 48 | if (encoded === 'K!') { return []; } 49 | var items = encoded.split('"'); 50 | 51 | var pointers = [[]]; 52 | var array; 53 | var depth = 0; 54 | 55 | var l = items.length; 56 | for (var i = 0; i < l; i++) { 57 | var item = items[i]; 58 | var itemLength = item.length; 59 | 60 | var open = 0; 61 | while (item[open] == 'K') { open++; } 62 | 63 | var close = 0; 64 | while (item[itemLength-close - 1] == '!') { close++; } 65 | 66 | var content = item.slice(open, itemLength-close); 67 | 68 | var newdepth = depth + open; 69 | for (var j = depth; j < newdepth; j++) { 70 | pointers[j + 1] = []; 71 | pointers[j].push(pointers[j + 1]); 72 | depth = newdepth; 73 | array = pointers[depth]; 74 | } 75 | 76 | if (content.length !== 0) { 77 | array.push(codec.decode(unescape(content))); 78 | } 79 | 80 | var newdepth = depth - close; 81 | for (var j = newdepth; j < depth; j++) { 82 | pointers[j + 1] = []; 83 | depth = newdepth; 84 | array = pointers[depth]; 85 | } 86 | 87 | } 88 | return pointers[0][0]; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var number = require('./codec/number.js'); 2 | var object = require('./codec/object.js'); 3 | 4 | var flip = exports.flip = function (n) { 5 | var s = n.toString() 6 | var f = '' 7 | for(var i in s) { 8 | f += s[i] == '.' ? '.' : (9 - +s[i]) 9 | } 10 | return f 11 | } 12 | 13 | function round (n) { 14 | return n < 0 ? Math.ceil(n) : Math.floor(n) 15 | } 16 | 17 | function fraction (f) { 18 | return f - round(f) 19 | } 20 | 21 | exports.number = number; 22 | 23 | exports.string = { 24 | encode: function (s) { 25 | //we'll need to escape the separators 26 | if(!/\x00|\x01/.test(s)) 27 | return 'J'+s 28 | else { 29 | return 'J'+s.replace(/\x01/g, '\x01\x01').replace(/\x00/g, '\x01') 30 | } 31 | }, 32 | decode: function (s) { 33 | if('J' === s[0]) 34 | return s.substring(1) //TODO, unescape things... 35 | } 36 | } 37 | 38 | exports.encode = function (t) { 39 | return exports[typeof t].encode(t) 40 | } 41 | 42 | exports.decode = function (s) { 43 | if(s === '') return s 44 | 45 | if(!decoders[s[0]]) 46 | throw new Error('no decoder for:'+JSON.stringify(s)) 47 | return decoders[s[0]](s) 48 | } 49 | 50 | exports.object = object.factory(exports); 51 | 52 | exports.boolean = { 53 | encode: function (b) { 54 | return b ? 'C' : 'B' 55 | }, 56 | decode: function (b) { 57 | return 'C' === b 58 | } 59 | } 60 | 61 | exports.undefined = { 62 | encode: function (b) { 63 | return 'L' 64 | }, 65 | decode: function () { 66 | return undefined 67 | } 68 | } 69 | 70 | var decoders = { 71 | A: exports.object.decode, //null 72 | B: exports.boolean.decode, // false 73 | C: exports.boolean.decode, // true 74 | D: exports.number.decode, // number 75 | F: exports.number.decode, // number 76 | // G Date 77 | // H Date 78 | // I Buffer 79 | J: exports.string.decode, // String 80 | K: exports.object.decode, // Array 81 | L: exports.undefined.decode, // undefined 82 | } 83 | 84 | exports.LO = null 85 | exports.HI = undefined 86 | 87 | 88 | //for leveldb, request strings 89 | exports.buffer = false 90 | exports.type = 'charwise' 91 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "charwise", 3 | "version": "3.0.1", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "balanced-match": { 8 | "version": "1.0.0", 9 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 10 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", 11 | "dev": true 12 | }, 13 | "brace-expansion": { 14 | "version": "1.1.8", 15 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", 16 | "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", 17 | "dev": true, 18 | "requires": { 19 | "balanced-match": "1.0.0", 20 | "concat-map": "0.0.1" 21 | } 22 | }, 23 | "bytewise": { 24 | "version": "1.1.0", 25 | "resolved": "https://registry.npmjs.org/bytewise/-/bytewise-1.1.0.tgz", 26 | "integrity": "sha1-HRPL/3F65xWAlKqIGzXQgbOHJT4=", 27 | "dev": true, 28 | "requires": { 29 | "bytewise-core": "1.2.3", 30 | "typewise": "1.0.3" 31 | } 32 | }, 33 | "bytewise-core": { 34 | "version": "1.2.3", 35 | "resolved": "https://registry.npmjs.org/bytewise-core/-/bytewise-core-1.2.3.tgz", 36 | "integrity": "sha1-P7QQx+kVWOsasiqCg0V3qmvWHUI=", 37 | "dev": true, 38 | "requires": { 39 | "typewise-core": "1.2.0" 40 | } 41 | }, 42 | "concat-map": { 43 | "version": "0.0.1", 44 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 45 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 46 | "dev": true 47 | }, 48 | "deep-equal": { 49 | "version": "1.0.1", 50 | "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", 51 | "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=", 52 | "dev": true 53 | }, 54 | "define-properties": { 55 | "version": "1.1.2", 56 | "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.2.tgz", 57 | "integrity": "sha1-g6c/L+pWmJj7c3GTyPhzyvbUXJQ=", 58 | "dev": true, 59 | "requires": { 60 | "foreach": "2.0.5", 61 | "object-keys": "1.0.11" 62 | } 63 | }, 64 | "defined": { 65 | "version": "1.0.0", 66 | "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", 67 | "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=", 68 | "dev": true 69 | }, 70 | "es-abstract": { 71 | "version": "1.7.0", 72 | "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.7.0.tgz", 73 | "integrity": "sha1-363ndOAb/Nl/lhgCmMRJyGI/uUw=", 74 | "dev": true, 75 | "requires": { 76 | "es-to-primitive": "1.1.1", 77 | "function-bind": "1.1.0", 78 | "is-callable": "1.1.3", 79 | "is-regex": "1.0.4" 80 | } 81 | }, 82 | "es-to-primitive": { 83 | "version": "1.1.1", 84 | "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.1.1.tgz", 85 | "integrity": "sha1-RTVSSKiJeQNLZ5Lhm7gfK3l13Q0=", 86 | "dev": true, 87 | "requires": { 88 | "is-callable": "1.1.3", 89 | "is-date-object": "1.0.1", 90 | "is-symbol": "1.0.1" 91 | } 92 | }, 93 | "for-each": { 94 | "version": "0.3.2", 95 | "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.2.tgz", 96 | "integrity": "sha1-LEBFC5NI6X8oEyJZO6lnBLmr1NQ=", 97 | "dev": true, 98 | "requires": { 99 | "is-function": "1.0.1" 100 | } 101 | }, 102 | "foreach": { 103 | "version": "2.0.5", 104 | "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", 105 | "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=", 106 | "dev": true 107 | }, 108 | "fs.realpath": { 109 | "version": "1.0.0", 110 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 111 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 112 | "dev": true 113 | }, 114 | "function-bind": { 115 | "version": "1.1.0", 116 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.0.tgz", 117 | "integrity": "sha1-FhdnFMgBeY5Ojyz391KUZ7tKV3E=", 118 | "dev": true 119 | }, 120 | "glob": { 121 | "version": "7.1.2", 122 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", 123 | "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", 124 | "dev": true, 125 | "requires": { 126 | "fs.realpath": "1.0.0", 127 | "inflight": "1.0.6", 128 | "inherits": "2.0.3", 129 | "minimatch": "3.0.4", 130 | "once": "1.4.0", 131 | "path-is-absolute": "1.0.1" 132 | } 133 | }, 134 | "has": { 135 | "version": "1.0.1", 136 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.1.tgz", 137 | "integrity": "sha1-hGFzP1OLCDfJNh45qauelwTcLyg=", 138 | "dev": true, 139 | "requires": { 140 | "function-bind": "1.1.0" 141 | } 142 | }, 143 | "inflight": { 144 | "version": "1.0.6", 145 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 146 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 147 | "dev": true, 148 | "requires": { 149 | "once": "1.4.0", 150 | "wrappy": "1.0.2" 151 | } 152 | }, 153 | "inherits": { 154 | "version": "2.0.3", 155 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 156 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", 157 | "dev": true 158 | }, 159 | "is-callable": { 160 | "version": "1.1.3", 161 | "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.3.tgz", 162 | "integrity": "sha1-hut1OSgF3cM69xySoO7fdO52BLI=", 163 | "dev": true 164 | }, 165 | "is-date-object": { 166 | "version": "1.0.1", 167 | "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", 168 | "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", 169 | "dev": true 170 | }, 171 | "is-function": { 172 | "version": "1.0.1", 173 | "resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.1.tgz", 174 | "integrity": "sha1-Es+5i2W1fdPRk6MSH19uL0N2ArU=", 175 | "dev": true 176 | }, 177 | "is-regex": { 178 | "version": "1.0.4", 179 | "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", 180 | "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", 181 | "dev": true, 182 | "requires": { 183 | "has": "1.0.1" 184 | } 185 | }, 186 | "is-symbol": { 187 | "version": "1.0.1", 188 | "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.1.tgz", 189 | "integrity": "sha1-PMWfAAJRlLarLjjbrmaJJWtmBXI=", 190 | "dev": true 191 | }, 192 | "minimatch": { 193 | "version": "3.0.4", 194 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 195 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 196 | "dev": true, 197 | "requires": { 198 | "brace-expansion": "1.1.8" 199 | } 200 | }, 201 | "minimist": { 202 | "version": "1.2.0", 203 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", 204 | "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", 205 | "dev": true 206 | }, 207 | "object-inspect": { 208 | "version": "1.3.0", 209 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.3.0.tgz", 210 | "integrity": "sha512-OHHnLgLNXpM++GnJRyyhbr2bwl3pPVm4YvaraHrRvDt/N3r+s/gDVHciA7EJBTkijKXj61ssgSAikq1fb0IBRg==", 211 | "dev": true 212 | }, 213 | "object-keys": { 214 | "version": "1.0.11", 215 | "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.11.tgz", 216 | "integrity": "sha1-xUYBd4rVYPEULODgG8yotW0TQm0=", 217 | "dev": true 218 | }, 219 | "once": { 220 | "version": "1.4.0", 221 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 222 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 223 | "dev": true, 224 | "requires": { 225 | "wrappy": "1.0.2" 226 | } 227 | }, 228 | "path-is-absolute": { 229 | "version": "1.0.1", 230 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 231 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 232 | "dev": true 233 | }, 234 | "path-parse": { 235 | "version": "1.0.5", 236 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", 237 | "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=", 238 | "dev": true 239 | }, 240 | "resolve": { 241 | "version": "1.4.0", 242 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.4.0.tgz", 243 | "integrity": "sha512-aW7sVKPufyHqOmyyLzg/J+8606v5nevBgaliIlV7nUpVMsDnoBGV/cbSLNjZAg9q0Cfd/+easKVKQ8vOu8fn1Q==", 244 | "dev": true, 245 | "requires": { 246 | "path-parse": "1.0.5" 247 | } 248 | }, 249 | "resumer": { 250 | "version": "0.0.0", 251 | "resolved": "https://registry.npmjs.org/resumer/-/resumer-0.0.0.tgz", 252 | "integrity": "sha1-8ej0YeQGS6Oegq883CqMiT0HZ1k=", 253 | "dev": true, 254 | "requires": { 255 | "through": "2.3.8" 256 | } 257 | }, 258 | "string.prototype.trim": { 259 | "version": "1.1.2", 260 | "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.1.2.tgz", 261 | "integrity": "sha1-0E3iyJ4Tf019IG8Ia17S+ua+jOo=", 262 | "dev": true, 263 | "requires": { 264 | "define-properties": "1.1.2", 265 | "es-abstract": "1.7.0", 266 | "function-bind": "1.1.0" 267 | } 268 | }, 269 | "tape": { 270 | "version": "4.8.0", 271 | "resolved": "https://registry.npmjs.org/tape/-/tape-4.8.0.tgz", 272 | "integrity": "sha512-TWILfEnvO7I8mFe35d98F6T5fbLaEtbFTG/lxWvid8qDfFTxt19EBijWmB4j3+Hoh5TfHE2faWs73ua+EphuBA==", 273 | "dev": true, 274 | "requires": { 275 | "deep-equal": "1.0.1", 276 | "defined": "1.0.0", 277 | "for-each": "0.3.2", 278 | "function-bind": "1.1.0", 279 | "glob": "7.1.2", 280 | "has": "1.0.1", 281 | "inherits": "2.0.3", 282 | "minimist": "1.2.0", 283 | "object-inspect": "1.3.0", 284 | "resolve": "1.4.0", 285 | "resumer": "0.0.0", 286 | "string.prototype.trim": "1.1.2", 287 | "through": "2.3.8" 288 | } 289 | }, 290 | "through": { 291 | "version": "2.3.8", 292 | "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", 293 | "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", 294 | "dev": true 295 | }, 296 | "typewise": { 297 | "version": "1.0.3", 298 | "resolved": "https://registry.npmjs.org/typewise/-/typewise-1.0.3.tgz", 299 | "integrity": "sha1-EGeTZUCvl5N8xdz5kiSG6fooRlE=", 300 | "dev": true, 301 | "requires": { 302 | "typewise-core": "1.2.0" 303 | } 304 | }, 305 | "typewise-core": { 306 | "version": "1.2.0", 307 | "resolved": "https://registry.npmjs.org/typewise-core/-/typewise-core-1.2.0.tgz", 308 | "integrity": "sha1-l+uRgFx/VdL5QXSPpQ0xXZke8ZU=", 309 | "dev": true 310 | }, 311 | "typewiselite": { 312 | "version": "1.0.0", 313 | "resolved": "https://registry.npmjs.org/typewiselite/-/typewiselite-1.0.0.tgz", 314 | "integrity": "sha1-yIgvobsQksBgBal/NO9chQjjZk4=", 315 | "dev": true 316 | }, 317 | "wrappy": { 318 | "version": "1.0.2", 319 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 320 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 321 | "dev": true 322 | } 323 | } 324 | } 325 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "charwise", 3 | "description": "encode/decode with same encoded sort order as bytewise", 4 | "version": "3.0.1", 5 | "homepage": "https://github.com/dominictarr/charwise", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/dominictarr/charwise.git" 9 | }, 10 | "dependencies": {}, 11 | "devDependencies": { 12 | "bytewise": "^1.1.0", 13 | "check-ecmascript-version-compatibility": "^0.1.1", 14 | "deep-equal": "^1.0.1", 15 | "glob": "^7.1.2", 16 | "tape": "^4.8.0", 17 | "typewiselite": "^1.0.0" 18 | }, 19 | "scripts": { 20 | "test": "set -e; for t in test/*.js; do node $t; done" 21 | }, 22 | "author": "Dominic Tarr (http://dominictarr.com)", 23 | "license": "MIT" 24 | } 25 | -------------------------------------------------------------------------------- /test/bench.js: -------------------------------------------------------------------------------- 1 | var bytewise = require('bytewise') 2 | var charwise = require('../') 3 | 4 | var randString = function() { 5 | var a = ' abcdefghijklmnopqrstuvwxyz'; 6 | var str = ''; 7 | for (var i = 0; i < 4 + Math.random() * 10; i++) { 8 | str += a[Math.floor(Math.random() * a.length)]; 9 | } 10 | return str; 11 | } 12 | var randArray = function (opt, depth) { 13 | depth = depth || 0; 14 | var length = Math.random() < (depth === 0 ? 0 : 0.5) ? 0 : Math.ceil(Math.random() * Math.max(0, opt.length)); 15 | var array = [] 16 | for(var i = 0; i < length; i++) { 17 | if (opt.depth != 0 && Math.random() < Math.max(0, opt.depth-depth)/opt.depth) { 18 | array.push(randArray(opt, depth + 1)); 19 | } else { 20 | var dice = Math.floor(Math.random() * 5) 21 | if(dice === 0) { 22 | array.push(randString()) 23 | } 24 | if(dice === 1) { 25 | array.push(Math.random() > 0.5 ? true : false); 26 | } 27 | if(dice === 2) { 28 | array.push(Math.random() > 0.5 ? null : undefined); 29 | } 30 | if(dice === 3) { 31 | array.push((Math.random()*2 - 1)); 32 | } 33 | if(dice === 4) { 34 | array.push((Math.random()*2 - 1)*1e10); 35 | } 36 | } 37 | } 38 | return array; 39 | } 40 | 41 | var items = []; 42 | for(var i = 0; i < 100; i++) { 43 | items.push(randArray({ 44 | depth: 0, 45 | length: 8 46 | })); 47 | } 48 | 49 | function bench (fn, items, samples) { 50 | var time = process.hrtime(); 51 | var iter = 0; 52 | var l = items.length; 53 | while(process.hrtime(time)[0] < 1) { 54 | fn(items[iter%l]); 55 | iter++; 56 | } 57 | return iter; 58 | 59 | } 60 | 61 | var bencode = bench(bytewise.encode, items); 62 | console.log('bytewise encode', Math.floor(bencode)); 63 | 64 | var cencode = bench(charwise.encode, items); 65 | console.log('charwise encode', Math.floor(cencode), 'x' + Math.floor(cencode / bencode * 10)/10); 66 | 67 | var bdecode = bench(bytewise.decode, items.map(function(item) { return bytewise.encode(item); })); 68 | console.log('bytewise decode', Math.floor(bdecode)); 69 | 70 | var cdecode = bench(charwise.decode, items.map(function(item) { return charwise.encode(item); })); 71 | console.log('charwise decode', Math.floor(cdecode), 'x' + Math.floor(cdecode / bdecode * 10)/10); 72 | -------------------------------------------------------------------------------- /test/es5.test.js: -------------------------------------------------------------------------------- 1 | var tape = require('tape') 2 | var glob = require('glob') 3 | var check = require('check-ecmascript-version-compatibility'); 4 | 5 | tape('ES5 compliant', function (t) { 6 | var onAllFilesDone = function (files) { 7 | var count = 0; 8 | return function (file) { 9 | return function (err) { 10 | if (err) { 11 | t.fail('in ('+file + ') : ' + err.message); 12 | }; 13 | count++; 14 | if (files.length === count) { 15 | t.pass(); 16 | t.end(); 17 | } 18 | } 19 | } 20 | } 21 | glob('./!(node_modules)/*.js', function (err, files) { 22 | console.log(files); 23 | var doneForFile = onAllFilesDone(files); 24 | files.forEach(function (file) { 25 | check(file, doneForFile(file)); 26 | }) 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | var tape = require('tape') 2 | var compare = require('typewiselite') 3 | 4 | var codec = require('../') 5 | 6 | tape('numbers', function (t) { 7 | 8 | var expected = [1,2,0,-1, 10, 1000, 949939434, -2, -30, 0.5, -0.72, -0.00001, 1000.00102120] 9 | 10 | function test (n) { 11 | var s = codec.number.encode(n) 12 | t.equal(typeof s, 'string') 13 | t.equal(codec.number.decode(s), n) 14 | return s 15 | } 16 | 17 | var actual = expected.map(test) 18 | 19 | expected.sort(function (a, b) { return a - b }) 20 | console.log(expected) 21 | actual.sort() //default to sort as strings 22 | console.log(actual) 23 | t.deepEqual(actual.map(codec.number.decode), expected) 24 | t.end() 25 | 26 | }) 27 | 28 | function cmp (a, b) { 29 | return a - b 30 | } 31 | // 32 | //function neg (e) { 33 | // return -e 34 | //} 35 | 36 | tape('flip', function (t) { 37 | var ns = [1,2,3,4,13,34,35, 50,100] 38 | console.log(ns.map(codec.flip)) 39 | console.log(ns.map(codec.flip).map(codec.flip).map(Number)) 40 | t.deepEqual(ns.map(codec.flip).map(codec.flip).map(Number), ns) 41 | // t.deepEqual( 42 | // ns.map(codec.flip).sort().map(codec.flip).map(Number), 43 | // ns.map(neg).sort(cmp).map(neg) 44 | // ) 45 | t.end() 46 | }) 47 | 48 | var data = [ 49 | 1, 0, 8234, 90321, -12, -34, 50 | // 0.1, -1.0032e29, 51 | 'hello', 'bye', 52 | true, 53 | false, 54 | null, 55 | undefined 56 | ].sort(compare) 57 | 58 | tape('order', function (t) { 59 | var strings = data.map(codec.encode).sort() 60 | t.deepEqual( 61 | strings.map(codec.decode), 62 | data 63 | ) 64 | t.end() 65 | }) 66 | 67 | 68 | tape('array', function (t) { 69 | var a = [] 70 | // for(var i = 0; i encode(0.999), 'orders well with number before') 46 | t.assert(encode(1) < encode(1.001), 'orders well with number after') 47 | t.end(); 48 | }); 49 | 50 | tape("Number: -1", function (t) { 51 | t.equals(decode(encode(-1)), -1, 'decode inverses encode'); 52 | t.assert(encode(-1) > encode(-1.001), 'orders well with number before') 53 | t.assert(encode(-1) < encode(-0.999), 'orders well with number after') 54 | t.end(); 55 | }); 56 | 57 | tape("Number: 0", function (t) { 58 | t.equals(decode(encode(0)), 0, 'decode inverses encode'); 59 | t.assert(encode(0) > encode(-Number.MIN_VALUE), 'orders well with number before') 60 | t.assert(encode(0) < encode(Number.MIN_VALUE), 'orders well with number after') 61 | t.end(); 62 | }); 63 | 64 | tape("Number: Infinity", function (t) { 65 | t.equals(decode(encode(Infinity)), Infinity, 'decode inverses encode'); 66 | t.assert(encode(Infinity) > encode(Number.MAX_VALUE), 'orders well with number before') 67 | t.end(); 68 | }); 69 | 70 | tape("Number: Infinity", function (t) { 71 | t.equals(decode(encode(-Infinity)), -Infinity, 'decode inverses encode'); 72 | t.assert(encode(-Infinity) < encode(Number.MAX_VALUE), 'orders well with number after') 73 | t.end(); 74 | }); 75 | 76 | tape("Number: random pairs", function (t) { 77 | var i; 78 | for (i = 0; i < 1000; i++) { 79 | var a = randQ(); 80 | var b = randQ(); 81 | if (decode(encode(a)) !== a) { 82 | t.equals(decode(encode(a)), a, 'decode(encode(' + a + ')) !== ' + a); 83 | break; 84 | } 85 | if (decode(encode(b)) !== b) { 86 | t.equals(decode(encode(b)), b, 'decode(encode(' + b + ')) !== ' + b); 87 | break; 88 | } 89 | if(a < b && encode(a) >= encode(b)) { 90 | t.fail('encode(' + a + ') >= encode(' + b + ')'); 91 | break; 92 | } 93 | if(a > b && encode(a) <= encode(b)) { 94 | t.fail('encode(' + a + ') =< encode(' + b + ')'); 95 | break; 96 | } 97 | if(a === b && encode(a) !== encode(b)) { 98 | t.fail('encode(' + a + ') !== encode(' + b + ')'); 99 | break; 100 | } 101 | } 102 | if (i === 1000) { 103 | t.pass('All random pairs validated') 104 | } 105 | t.end(); 106 | }); 107 | 108 | tape("Number: random pairs of close numbers", function (t) { 109 | var i; 110 | for (i = 0; i < 100; i++) { 111 | var a = Math.floor(rand(-10,10)); 112 | var b = a + (Math.random() > 0.5 ? 1 : -1)*0.000000000000001; 113 | if (decode(encode(a)) !== a) { 114 | t.equals(decode(encode(a)), a, 'decode(encode(' + a + ')) !== ' + a); 115 | break; 116 | } 117 | if (decode(encode(b)) !== b) { 118 | t.equals(decode(encode(b)), b, 'decode(encode(' + b + ')) !== ' + b); 119 | break; 120 | } 121 | if(a < b && encode(a) >= encode(b)) { 122 | t.fail('encode(' + a + ') >= encode(' + b + ')'); 123 | break; 124 | } 125 | if(a > b && encode(a) <= encode(b)) { 126 | t.fail('encode(' + a + ') =< encode(' + b + ')'); 127 | break; 128 | } 129 | if(a === b && encode(a) !== encode(b)) { 130 | t.fail('encode(' + a + ') !== encode(' + b + ')'); 131 | break; 132 | } 133 | } 134 | if (i === 100) { 135 | t.pass('All random pairs validated') 136 | } 137 | t.end(); 138 | }); 139 | -------------------------------------------------------------------------------- /test/object.test.js: -------------------------------------------------------------------------------- 1 | var tape = require('tape') 2 | var codec = require('../codec/object.js').factory(require('../index.js')); 3 | var same = require('deep-equal'); 4 | var bytewise = require('bytewise').encode; 5 | var encode = codec.encode; 6 | var decode = codec.decode; 7 | 8 | var words = ['foo', 'bar', 'hello!world', 'OSfail!Shard!A'] 9 | 10 | var randArray = function (size) { 11 | var array = []; 12 | for (var i = 0; i < Math.ceil(Math.random() * size); i++) { 13 | if (Math.random() > 0.5) { array.push(Math.random() > 0.5 ? true : false); } 14 | if (Math.random() > 0.5) { array.push(Math.random() > 0.5 ? undefined : null); } 15 | if (Math.random() > 0.5) { array.push((Math.random() - 0.5) * 4); } 16 | if (Math.random() > 0.5) { array.push(words[Math.floor(Math.random() * words.length)]); } 17 | if (Math.random() > 0.5) { array.push(randArray(size-1)); } 18 | } 19 | return array; 20 | } 21 | 22 | tape("Object: 1000 random array", function (t) { 23 | var i; 24 | for (i = 0; i < 1000; i++) { 25 | var array1 = randArray(3); 26 | var array2 = randArray(3); 27 | if (!same(decode(encode(array1)), array1)) { 28 | t.equals(decode(encode(array1)), array1, 'decode(encode(' + array1 + ')) !== ' + array1); 29 | break; 30 | } 31 | if (!same(decode(encode(array2)), array2)) { 32 | t.equals(decode(encode(array2)), array2, 'decode(encode(' + array2 + ')) !== ' + array2); 33 | break; 34 | } 35 | if (bytewise(array1) < bytewise(array2) && encode(array1) >= encode(array2)) { 36 | t.fail('encode(' + array1 + ') >= encode(' + array2 + ')'); 37 | break; 38 | } 39 | if (bytewise(array1) > bytewise(array2) && encode(array1) <= encode(array2)) { 40 | t.fail('encode(' + array1 + ') >= encode(' + array2 + ')'); 41 | break; 42 | } 43 | if (bytewise(array1) === bytewise(array2) && encode(array1) === encode(array2)) { 44 | t.fail('encode(' + array1 + ') !== encode(' + array2 + ')'); 45 | break; 46 | } 47 | } 48 | if (i === 1000) { 49 | t.pass('All random array validated') 50 | } 51 | t.end(); 52 | }); 53 | 54 | tape('edge cases', function (t) { 55 | var examples = [ 56 | [[[]]], 57 | ['hello\\', 'world'], 58 | ['hello!', 'world!'], 59 | ['hello?@!', 'world!'], 60 | ['hello@?@!', 'world!'] 61 | ] 62 | examples.forEach(function (a) { 63 | t.deepEqual(decode(encode(a)), a) 64 | }) 65 | t.deepEqual(decode(encode(examples)), examples) 66 | t.end() 67 | }) 68 | --------------------------------------------------------------------------------