├── package.json ├── test └── basic.js ├── LICENSE ├── sigmund.js ├── README.md └── bench.js /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sigmund", 3 | "version": "1.0.1", 4 | "description": "Quick and dirty signatures for Objects.", 5 | "main": "sigmund.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "dependencies": {}, 10 | "devDependencies": { 11 | "tap": "~0.3.0" 12 | }, 13 | "scripts": { 14 | "test": "tap test/*.js", 15 | "bench": "node bench.js" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git://github.com/isaacs/sigmund" 20 | }, 21 | "keywords": [ 22 | "object", 23 | "signature", 24 | "key", 25 | "data", 26 | "psychoanalysis" 27 | ], 28 | "author": "Isaac Z. Schlueter (http://blog.izs.me/)", 29 | "license": "ISC" 30 | } 31 | -------------------------------------------------------------------------------- /test/basic.js: -------------------------------------------------------------------------------- 1 | var test = require('tap').test 2 | var sigmund = require('../sigmund.js') 3 | 4 | 5 | // occasionally there are duplicates 6 | // that's an acceptable edge-case. JSON.stringify and util.inspect 7 | // have some collision potential as well, though less, and collision 8 | // detection is expensive. 9 | var hash = '{abc/def/g{0h1i2{jkl' 10 | var obj1 = {a:'b',c:/def/,g:['h','i',{j:'',k:'l'}]} 11 | var obj2 = {a:'b',c:'/def/',g:['h','i','{jkl']} 12 | 13 | var obj3 = JSON.parse(JSON.stringify(obj1)) 14 | obj3.c = /def/ 15 | obj3.g[2].cycle = obj3 16 | var cycleHash = '{abc/def/g{0h1i2{jklcycle' 17 | 18 | test('basic', function (t) { 19 | t.equal(sigmund(obj1), hash) 20 | t.equal(sigmund(obj2), hash) 21 | t.equal(sigmund(obj3), cycleHash) 22 | t.end() 23 | }) 24 | 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The ISC License 2 | 3 | Copyright (c) Isaac Z. Schlueter and Contributors 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR 15 | IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /sigmund.js: -------------------------------------------------------------------------------- 1 | module.exports = sigmund 2 | function sigmund (subject, maxSessions) { 3 | maxSessions = maxSessions || 10; 4 | var notes = []; 5 | var analysis = ''; 6 | var RE = RegExp; 7 | 8 | function psychoAnalyze (subject, session) { 9 | if (session > maxSessions) return; 10 | 11 | if (typeof subject === 'function' || 12 | typeof subject === 'undefined') { 13 | return; 14 | } 15 | 16 | if (typeof subject !== 'object' || !subject || 17 | (subject instanceof RE)) { 18 | analysis += subject; 19 | return; 20 | } 21 | 22 | if (notes.indexOf(subject) !== -1 || session === maxSessions) return; 23 | 24 | notes.push(subject); 25 | analysis += '{'; 26 | Object.keys(subject).forEach(function (issue, _, __) { 27 | // pseudo-private values. skip those. 28 | if (issue.charAt(0) === '_') return; 29 | var to = typeof subject[issue]; 30 | if (to === 'function' || to === 'undefined') return; 31 | analysis += issue; 32 | psychoAnalyze(subject[issue], session + 1); 33 | }); 34 | } 35 | psychoAnalyze(subject, 0); 36 | return analysis; 37 | } 38 | 39 | // vim: set softtabstop=4 shiftwidth=4: 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sigmund 2 | 3 | Quick and dirty signatures for Objects. 4 | 5 | This is like a much faster `deepEquals` comparison, which returns a 6 | string key suitable for caches and the like. 7 | 8 | ## Usage 9 | 10 | ```javascript 11 | function doSomething (someObj) { 12 | var key = sigmund(someObj, maxDepth) // max depth defaults to 10 13 | var cached = cache.get(key) 14 | if (cached) return cached 15 | 16 | var result = expensiveCalculation(someObj) 17 | cache.set(key, result) 18 | return result 19 | } 20 | ``` 21 | 22 | The resulting key will be as unique and reproducible as calling 23 | `JSON.stringify` or `util.inspect` on the object, but is much faster. 24 | In order to achieve this speed, some differences are glossed over. 25 | For example, the object `{0:'foo'}` will be treated identically to the 26 | array `['foo']`. 27 | 28 | Also, just as there is no way to summon the soul from the scribblings 29 | of a cocaine-addled psychoanalyst, there is no way to revive the object 30 | from the signature string that sigmund gives you. In fact, it's 31 | barely even readable. 32 | 33 | As with `util.inspect` and `JSON.stringify`, larger objects will 34 | produce larger signature strings. 35 | 36 | Because sigmund is a bit less strict than the more thorough 37 | alternatives, the strings will be shorter, and also there is a 38 | slightly higher chance for collisions. For example, these objects 39 | have the same signature: 40 | 41 | var obj1 = {a:'b',c:/def/,g:['h','i',{j:'',k:'l'}]} 42 | var obj2 = {a:'b',c:'/def/',g:['h','i','{jkl']} 43 | 44 | Like a good Freudian, sigmund is most effective when you already have 45 | some understanding of what you're looking for. It can help you help 46 | yourself, but you must be willing to do some work as well. 47 | 48 | Cycles are handled, and cyclical objects are silently omitted (though 49 | the key is included in the signature output.) 50 | 51 | The second argument is the maximum depth, which defaults to 10, 52 | because that is the maximum object traversal depth covered by most 53 | insurance carriers. 54 | -------------------------------------------------------------------------------- /bench.js: -------------------------------------------------------------------------------- 1 | // different ways to id objects 2 | // use a req/res pair, since it's crazy deep and cyclical 3 | 4 | // sparseFE10 and sigmund are usually pretty close, which is to be expected, 5 | // since they are essentially the same algorithm, except that sigmund handles 6 | // regular expression objects properly. 7 | 8 | 9 | var http = require('http') 10 | var util = require('util') 11 | var sigmund = require('./sigmund.js') 12 | var sreq, sres, creq, cres, test 13 | 14 | http.createServer(function (q, s) { 15 | sreq = q 16 | sres = s 17 | sres.end('ok') 18 | this.close(function () { setTimeout(function () { 19 | start() 20 | }, 200) }) 21 | }).listen(1337, function () { 22 | creq = http.get({ port: 1337 }) 23 | creq.on('response', function (s) { cres = s }) 24 | }) 25 | 26 | function start () { 27 | test = [sreq, sres, creq, cres] 28 | // test = sreq 29 | // sreq.sres = sres 30 | // sreq.creq = creq 31 | // sreq.cres = cres 32 | 33 | for (var i in exports.compare) { 34 | console.log(i) 35 | var hash = exports.compare[i]() 36 | console.log(hash) 37 | console.log(hash.length) 38 | console.log('') 39 | } 40 | 41 | require('bench').runMain() 42 | } 43 | 44 | function customWs (obj, md, d) { 45 | d = d || 0 46 | var to = typeof obj 47 | if (to === 'undefined' || to === 'function' || to === null) return '' 48 | if (d > md || !obj || to !== 'object') return ('' + obj).replace(/[\n ]+/g, '') 49 | 50 | if (Array.isArray(obj)) { 51 | return obj.map(function (i, _, __) { 52 | return customWs(i, md, d + 1) 53 | }).reduce(function (a, b) { return a + b }, '') 54 | } 55 | 56 | var keys = Object.keys(obj) 57 | return keys.map(function (k, _, __) { 58 | return k + ':' + customWs(obj[k], md, d + 1) 59 | }).reduce(function (a, b) { return a + b }, '') 60 | } 61 | 62 | function custom (obj, md, d) { 63 | d = d || 0 64 | var to = typeof obj 65 | if (to === 'undefined' || to === 'function' || to === null) return '' 66 | if (d > md || !obj || to !== 'object') return '' + obj 67 | 68 | if (Array.isArray(obj)) { 69 | return obj.map(function (i, _, __) { 70 | return custom(i, md, d + 1) 71 | }).reduce(function (a, b) { return a + b }, '') 72 | } 73 | 74 | var keys = Object.keys(obj) 75 | return keys.map(function (k, _, __) { 76 | return k + ':' + custom(obj[k], md, d + 1) 77 | }).reduce(function (a, b) { return a + b }, '') 78 | } 79 | 80 | function sparseFE2 (obj, maxDepth) { 81 | var seen = [] 82 | var soFar = '' 83 | function ch (v, depth) { 84 | if (depth > maxDepth) return 85 | if (typeof v === 'function' || typeof v === 'undefined') return 86 | if (typeof v !== 'object' || !v) { 87 | soFar += v 88 | return 89 | } 90 | if (seen.indexOf(v) !== -1 || depth === maxDepth) return 91 | seen.push(v) 92 | soFar += '{' 93 | Object.keys(v).forEach(function (k, _, __) { 94 | // pseudo-private values. skip those. 95 | if (k.charAt(0) === '_') return 96 | var to = typeof v[k] 97 | if (to === 'function' || to === 'undefined') return 98 | soFar += k + ':' 99 | ch(v[k], depth + 1) 100 | }) 101 | soFar += '}' 102 | } 103 | ch(obj, 0) 104 | return soFar 105 | } 106 | 107 | function sparseFE (obj, maxDepth) { 108 | var seen = [] 109 | var soFar = '' 110 | function ch (v, depth) { 111 | if (depth > maxDepth) return 112 | if (typeof v === 'function' || typeof v === 'undefined') return 113 | if (typeof v !== 'object' || !v) { 114 | soFar += v 115 | return 116 | } 117 | if (seen.indexOf(v) !== -1 || depth === maxDepth) return 118 | seen.push(v) 119 | soFar += '{' 120 | Object.keys(v).forEach(function (k, _, __) { 121 | // pseudo-private values. skip those. 122 | if (k.charAt(0) === '_') return 123 | var to = typeof v[k] 124 | if (to === 'function' || to === 'undefined') return 125 | soFar += k 126 | ch(v[k], depth + 1) 127 | }) 128 | } 129 | ch(obj, 0) 130 | return soFar 131 | } 132 | 133 | function sparse (obj, maxDepth) { 134 | var seen = [] 135 | var soFar = '' 136 | function ch (v, depth) { 137 | if (depth > maxDepth) return 138 | if (typeof v === 'function' || typeof v === 'undefined') return 139 | if (typeof v !== 'object' || !v) { 140 | soFar += v 141 | return 142 | } 143 | if (seen.indexOf(v) !== -1 || depth === maxDepth) return 144 | seen.push(v) 145 | soFar += '{' 146 | for (var k in v) { 147 | // pseudo-private values. skip those. 148 | if (k.charAt(0) === '_') continue 149 | var to = typeof v[k] 150 | if (to === 'function' || to === 'undefined') continue 151 | soFar += k 152 | ch(v[k], depth + 1) 153 | } 154 | } 155 | ch(obj, 0) 156 | return soFar 157 | } 158 | 159 | function noCommas (obj, maxDepth) { 160 | var seen = [] 161 | var soFar = '' 162 | function ch (v, depth) { 163 | if (depth > maxDepth) return 164 | if (typeof v === 'function' || typeof v === 'undefined') return 165 | if (typeof v !== 'object' || !v) { 166 | soFar += v 167 | return 168 | } 169 | if (seen.indexOf(v) !== -1 || depth === maxDepth) return 170 | seen.push(v) 171 | soFar += '{' 172 | for (var k in v) { 173 | // pseudo-private values. skip those. 174 | if (k.charAt(0) === '_') continue 175 | var to = typeof v[k] 176 | if (to === 'function' || to === 'undefined') continue 177 | soFar += k + ':' 178 | ch(v[k], depth + 1) 179 | } 180 | soFar += '}' 181 | } 182 | ch(obj, 0) 183 | return soFar 184 | } 185 | 186 | 187 | function flatten (obj, maxDepth) { 188 | var seen = [] 189 | var soFar = '' 190 | function ch (v, depth) { 191 | if (depth > maxDepth) return 192 | if (typeof v === 'function' || typeof v === 'undefined') return 193 | if (typeof v !== 'object' || !v) { 194 | soFar += v 195 | return 196 | } 197 | if (seen.indexOf(v) !== -1 || depth === maxDepth) return 198 | seen.push(v) 199 | soFar += '{' 200 | for (var k in v) { 201 | // pseudo-private values. skip those. 202 | if (k.charAt(0) === '_') continue 203 | var to = typeof v[k] 204 | if (to === 'function' || to === 'undefined') continue 205 | soFar += k + ':' 206 | ch(v[k], depth + 1) 207 | soFar += ',' 208 | } 209 | soFar += '}' 210 | } 211 | ch(obj, 0) 212 | return soFar 213 | } 214 | 215 | exports.compare = 216 | { 217 | // 'custom 2': function () { 218 | // return custom(test, 2, 0) 219 | // }, 220 | // 'customWs 2': function () { 221 | // return customWs(test, 2, 0) 222 | // }, 223 | 'JSON.stringify (guarded)': function () { 224 | var seen = [] 225 | return JSON.stringify(test, function (k, v) { 226 | if (typeof v !== 'object' || !v) return v 227 | if (seen.indexOf(v) !== -1) return undefined 228 | seen.push(v) 229 | return v 230 | }) 231 | }, 232 | 233 | 'flatten 10': function () { 234 | return flatten(test, 10) 235 | }, 236 | 237 | // 'flattenFE 10': function () { 238 | // return flattenFE(test, 10) 239 | // }, 240 | 241 | 'noCommas 10': function () { 242 | return noCommas(test, 10) 243 | }, 244 | 245 | 'sparse 10': function () { 246 | return sparse(test, 10) 247 | }, 248 | 249 | 'sparseFE 10': function () { 250 | return sparseFE(test, 10) 251 | }, 252 | 253 | 'sparseFE2 10': function () { 254 | return sparseFE2(test, 10) 255 | }, 256 | 257 | sigmund: function() { 258 | return sigmund(test, 10) 259 | }, 260 | 261 | 262 | // 'util.inspect 1': function () { 263 | // return util.inspect(test, false, 1, false) 264 | // }, 265 | // 'util.inspect undefined': function () { 266 | // util.inspect(test) 267 | // }, 268 | // 'util.inspect 2': function () { 269 | // util.inspect(test, false, 2, false) 270 | // }, 271 | // 'util.inspect 3': function () { 272 | // util.inspect(test, false, 3, false) 273 | // }, 274 | // 'util.inspect 4': function () { 275 | // util.inspect(test, false, 4, false) 276 | // }, 277 | // 'util.inspect Infinity': function () { 278 | // util.inspect(test, false, Infinity, false) 279 | // } 280 | } 281 | 282 | /** results 283 | **/ 284 | --------------------------------------------------------------------------------