/** vim: et:ts=2:sw=2:sts=2 * @license (c) 2017 Ribose Inc. * * IdPacker is a library provides encoding support for Number collections (Arrays or Objects). * * * Usage: * * The encoding algorithm encodes a Number collection (in form of Array or Object) into another String. * * @param collection - Number collection (in form of Array or Object) * @param windowSize - Maximunm length of the binary encodedString in binary form (default: 10) * @param isExcludeNull - Exclude the object keys with null value (default: true) * @param outputCharset - Control how the output look like. CANNOT CONTAIN DUPLICATED characters (default: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-") * * IdPacker.encodeHashKeys(collection, windowSize, isExcludeNull, outputCharset); * * * Examples: * * // encode the items in an Array * var collection = [5, 6, 21, 23, 25]; * IdPacker.encodeHashKeys(collection); * => "_E~C_O.V" * * // encode the keys of an Object * var collection = { * 5: "a", * 6: "b", * 21: "c", * 23: "d", * 25: "e" * }; * IdPacker.encodeHashKeys(collection); * => "_E~C_O.V" * * * The encoding algorithm: * * The algorithm first sorts the collection in ascending order. Then, it decomposes * the sorted collection into multiple parts starting from 1. Finally, each part is * encoded into one of the following three encodedString forms: * * 1) Spaces encodedString * A encoded text with prefix '_', represents continuous space. * * 2) Range encodedString * A encoded text with prefix '~', represents continuous number. * * 3) Binary encodedString * A encoded text with prefix '.', represents arbitrary distributed numbers. * * In each part, the encodedString after the prefix is a value from a base-X number system, where * X is the length of IdPacker.outputCharset (overridable). * * Take the encoded text "_E~C_O.V" (original collection: [5, 6, 21, 23, 25]) as an example. * It consists of 4 parts: * * 1)_E * 4 continuous space (0-4) * * 2) ~C * 2 continuous number (5,6) * * 3) _O * 14 continuous space (7-20) * * 4) .V * Convert 'V' from the base-X number system to base-10 number system gives '21', * convert '21' from the base-10 number system to base-2 (binary) number system gives '101'. * '1' and '0' represent 'number' and 'space', respectively. Therefore, '101' denotes 21, 23, 25. */ (function (factory) { 'use strict'; /* AMD. Register as an anonymous module. */ if (typeof define === 'function' && define.amd) { define(factory); } /* Node/CommonJS */ else if (typeof exports === 'object') { module.exports = factory(); } /* Browser globals */ else { window.IdPacker = factory(); } }(function() { 'use strict'; return { spacesEncodedStringPrefix: '_', binaryEncodedStringPrefix: '.', rangeEncodedStringPrefix: '~', windowSize: 10, isExcludeNull: true, outputCharset: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-", range: function(start, length) { /** convert arguments to numbers */ if (typeof start != 'number') { start = parseInt(start, 10); } if (typeof length != 'number') { length = parseInt(length, 10); } this.start = !$.isNumeric(start) ? 0 : start; this.length = !$.isNumeric(length) ? 0 : length; // range functions /** * var range = IdPacker.range(0, 5); * range.clone(); * => IdPacker.range(0, 5) */ this.clone = function() { return IdPacker.range(this.start, this.length); }; /** * var range = IdPacker.range(0, 5); * range.end(); * => 4 */ this.end = function() { return this.start + this.length - 1; }; /** * var range = IdPacker.range(3, 5); * range.roundToWindow(5); * => IdPacker.range(0, 10) * * range.roundToWindow(2); * => IdPacker.range(2, 6) */ this.roundToWindow = function(window_size) { var start = Math.floor(this.start / window_size) * window_size; var length = Math.ceil((this.start + this.length - start) / window_size) * window_size; return IdPacker.range(start, length); }; /** * var range = IdPacker.range(0, 5); * range.contain(3); * => true * * range.contain(8); * => false */ this.contain = function(n) { return n >= this.start && n <= this.end(); }; // range-to-range functions /** * var range1 = IdPacker.range(0, 2); * var range2 = IdPacker.range(2, 2); * var range3 = IdPacker.range(3, 2); * range1.touch(range2); * => true * * range1.touch(range3); * => false * * range2.touch(range3); * => true */ this.touch = function(range) { var start = Math.max(this.start, range.start); var end = Math.min(this.end(), range.end()); var length = end - start + 1; return length >= 0; }; /** * var range1 = IdPacker.range(2, 2); * var range2 = IdPacker.range(3, 2); * range1.intersect(range2); * => IdPacker.range(3, 1) */ this.intersect = function(range) { var start = Math.max(this.start, range.start); var end = Math.min(this.end(), range.end()); var length = end - start + 1; return (length > 0) ? IdPacker.range(start, length) : null; }; /** * var range1 = IdPacker.range(2, 2); * var range2 = IdPacker.range(2, 5); * var range3 = IdPacker.range(0, 5); * range1.isSubsetOf(range3); * => true * * range2.isSubsetOf(range3); * => false */ this.isSubsetOf = function(range) { return range.contain(this.start) && range.contain(this.end()); }; /** * var range1 = IdPacker.range(0, 5); * var range2 = IdPacker.range(2, 5); * range1.union(range2); * => IdPacker.range(0, 7) */ this.union = function(range) { var start = Math.min(this.start, range.start); var end = Math.max(this.end(), range.end()); var length = end - start + 1; return IdPacker.range(start, length); }; /** * var range1 = IdPacker.range(0, 5); * var range2 = IdPacker.range(2, 5); * range1.minus(range2); * => IdPacker.range(0, 2) */ this.minus = function(range) { var intersectedRange = this.intersect(range); if (this.equal(intersectedRange)) { return []; } else if (this.start < intersectedRange.start && this.end() > intersectedRange.end()) { return [(IdPacker.range(this.start, intersectedRange.start - this.start)), (IdPacker.range(intersectedRange.end() + 1, this.end() - intersectedRange.end()))]; } else if (this.start < intersectedRange.start) { return [IdPacker.range(this.start, intersectedRange.start - this.start)]; } else { return [IdPacker.range(intersectedRange.end() + 1, this.end() - intersectedRange.end())]; } }; /** * var range1 = IdPacker.range(0, 2); * var range2 = IdPacker.range(0, 2); * var range3 = IdPacker.range(2, 2); * range1.equal(range2); * => true * * range1.equal(range3); * => false */ this.equal = function (range) { return this.start == range.start && this.length == range.length; }; // range-to-ranges functions /** * var range1 = IdPacker.range(0, 2); * var range2 = IdPacker.range(2, 2); * var ranges = [IdPacker.range(0, 3), IdPacker.range(5, 3)]; * range1.hasFullRange(ranges); * => true * * range2.hasFullRange(ranges); * => false */ this.hasFullRange = function(ranges) { for(var i = 0, l = ranges.length; i < l; i++) { if (this.isSubsetOf(ranges[i])) { return true; } } return false; }; /** * var range = IdPacker.range(2, 5); * var ranges = [IdPacker.range(0, 3), IdPacker.range(5, 3)]; * range.getOverlapRanges(ranges); * => [IdPacker.range(2, 1), IdPacker.range(5, 2)] */ this.getOverlapRanges = function(ranges) { var overlapRanges = []; for(var i = 0, l = ranges.length; i < l; i++) { var intersectedRange = this.intersect(ranges[i]); if (intersectedRange !== null) { overlapRanges.push(intersectedRange); } } return overlapRanges; }; /** * var range = IdPacker.range(2, 5); * var ranges = [IdPacker.range(0, 3), IdPacker.range(5, 3)]; * range.getMissingRanges(ranges); * => [IdPacker.range(3, 2)] */ this.getMissingRanges = function(ranges) { var overlapRanges = this.getOverlapRanges(ranges); var missingRanges = []; var missingRange = this; for(var i = 0, ol = overlapRanges.length; i < ol; i++) { var currentMissingRanges = missingRange.minus(overlapRanges[i]); for (var j = 0, cl = currentMissingRanges.length-1; j < cl; j++) { missingRanges.push(currentMissingRanges[j]); } missingRange = currentMissingRanges[currentMissingRanges.length-1]; } if (missingRange) { missingRanges.push(missingRange); } return missingRanges; }; /** * var range = IdPacker.range(2, 5); * var ranges = [IdPacker.range(0, 3), IdPacker.range(5, 3)]; * range.addToRanges(ranges); * => [IdPacker.range(0, 8)] */ this.addToRanges = function(ranges){ if (! ranges.length) { ranges.push(this); return ranges; } var i, l; var overlapRanges = []; var accumulate; for (i = 0, l = ranges.length; i < l; i++) { if (this.touch(ranges[i])) { overlapRanges.push(i); } } if (overlapRanges.length === 0) { for (i = 0, l = ranges.length; i < l; i++) { if (this.start < ranges[i].start) { Array.prototype.splice.apply(ranges, [i, 0].concat(this)); return ranges; } } ranges.push(this); return ranges; } accumulate = this; for (i = 0, l = overlapRanges.length; i < l; i++) { accumulate = accumulate.union(ranges[overlapRanges[i]]); } Array.prototype.splice.apply( ranges, [overlapRanges[0], overlapRanges.length].concat(accumulate) ); return ranges; }; /** * var range = IdPacker.range(2, 5); * var ranges = [IdPacker.range(0, 3), IdPacker.range(5, 3)]; * range.deleteFromRanges(ranges); * => [IdPacker.range(0, 2), IdPacker.range(7, 1)] * * @param {Array(Range)} ranges * @return {Array(Range)} */ this.deleteFromRanges = function(ranges) { var i, l; var overlapRanges = []; var accumulate = []; for (i = 0, l = ranges.length; i < l; i++) { if (this.intersect(ranges[i]) !== null) { overlapRanges.push(i); } } if (overlapRanges.length === 0) { return ranges; } accumulate = []; for (i = 0, l = overlapRanges.length; i < l; i++) { accumulate = accumulate.concat(ranges[overlapRanges[i]].minus(this)); } Array.prototype.splice.apply( ranges, [overlapRanges[0], overlapRanges.length].concat(accumulate) ); return ranges; }; }, /** * [1,2,3,6,7,8] * => [IdPacker.range(start: 1 length: 3), IdPacker.range(start: 6 length: 3)] */ convertNumbersToRanges: function (numbers) { var ranges = []; if (numbers.length > 0) { var range = IdPacker.range(numbers[0], 1); for (var i = 1; i < numbers.length; i++) { if (numbers[i] == numbers[i-1]+1) { range.length = range.length + 1; } else { ranges.push(range); range = IdPacker.range(numbers[i], 1); } } ranges.push(range); } return ranges; }, /** * [IdPacker.range(start: 1 length: 3), IdPacker.range(start: 6 length: 3)] * => "11100111" */ convertRangesToBinaryNumber: function (ranges) { var binaryNumber = ''; var i, j; for (i = 0; i < ranges.length; i++) { if (i > 0) { for (j = ranges[i].start; j > ranges[i-1].end() + 1; j--) { binaryNumber += '0'; } } for (j = 0; j < ranges[i].length; j++) { binaryNumber += '1'; } } return binaryNumber; }, /** * [5, 6, 21, 23, 25] * => "_E~C_O.V" */ encodeHashKeys: function(hash, windowSize, isExcludeNull, outputCharset) { // set default values windowSize = typeof windowSize === "number" ? windowSize : this.windowSize; isExcludeNull = typeof isExcludeNull === "boolean" ? isExcludeNull : this.isExcludeNull; outputCharset = typeof outputCharset === "string" ? outputCharset : this.outputCharset; var encodedHashKeys = ''; var hashKeys = hash instanceof Array ? hash : []; // convert keys in Object into array if collection is an Object if (! (hash instanceof Array)) { var hashKey; for (hashKey in hash) { if (!isExcludeNull || isExcludeNull && hash[hashKey]) { hashKeys.push(parseInt(hashKey, 10)); } } if (! hashKeys.length) { return ''; } } // sort the collection in ascending order hashKeys.sort(function(a,b) { return a - b; }); var ranges = this.convertNumbersToRanges(hashKeys); var prevEnd = 0; var currStart = 1; var spaces = 0; var groupWithPrev = false; var rangesToGroup = []; var binaryNumber = ''; var decimalNumber = 0; var encodedString = ''; for (var i = 0; i < ranges.length; i++) { spaces = ranges[i].start - prevEnd; if (groupWithPrev) { if (ranges[i].end() - currStart + 1 == windowSize) { rangesToGroup.push(ranges[i]); binaryNumber = this.convertRangesToBinaryNumber(rangesToGroup); decimalNumber = this.convertBinaryNumberToDecimalNumber(binaryNumber); encodedString = this.binaryPrefix + this.encodeDecimalNumber(decimalNumber, outputCharset); encodedHashKeys += encodedString; rangesToGroup = []; groupWithPrev = false; } else if (ranges[i].end() - currStart + 1 >= windowSize) { if (rangesToGroup.length == 1) { encodedString = this.rangePrefix + this.encodeDecimalNumber(rangesToGroup[0].length, outputCharset); encodedHashKeys += encodedString; } else { binaryNumber = this.convertRangesToBinaryNumber(rangesToGroup); decimalNumber = this.convertBinaryNumberToDecimalNumber(binaryNumber); encodedString = this.binaryPrefix + this.encodeDecimalNumber(decimalNumber, outputCharset); encodedHashKeys += encodedString; } rangesToGroup = []; encodedString = this.spacesPrefix + this.encodeDecimalNumber(spaces, outputCharset); encodedHashKeys += encodedString; if (ranges[i].length >= windowSize) { encodedString = this.rangePrefix + this.encodeDecimalNumber(ranges[i].length, outputCharset); encodedHashKeys += encodedString; groupWithPrev = false; } else { rangesToGroup.push(ranges[i]); currStart = ranges[i].start; groupWithPrev = true; } } else { rangesToGroup.push(ranges[i]); } } else { if (spaces >= 0) { encodedString = this.spacesPrefix + this.encodeDecimalNumber(spaces, outputCharset); encodedHashKeys += encodedString; } if (ranges[i].length >= windowSize) { encodedString = this.rangePrefix + this.encodeDecimalNumber(ranges[i].length, outputCharset); encodedHashKeys += encodedString; } else { rangesToGroup.push(ranges[i]); currStart = ranges[i].start; groupWithPrev = true; } } prevEnd = ranges[i].end(); } if (rangesToGroup.length == 1) { encodedString = this.rangePrefix + this.encodeDecimalNumber(rangesToGroup[0].length, outputCharset); encodedHashKeys += encodedString; } else if (rangesToGroup.length > 0) { binaryNumber = this.convertRangesToBinaryNumber(rangesToGroup); decimalNumber = this.convertBinaryNumberToDecimalNumber(binaryNumber); encodedString = this.binaryPrefix + this.encodeDecimalNumber(decimalNumber, outputCharset); encodedHashKeys += encodedString; } return encodedHashKeys; }, encodeInteger: function (n) { return this.encodeDecimalNumber(n); }, /** * "10101" * => 21 */ convertBinaryNumberToDecimalNumber: function(binaryNumber) { var decimalNumber = 0; for (var i = 0; i < binaryNumber.length; i++) { decimalNumber = decimalNumber + Math.pow(2, binaryNumber.length - i - 1) * parseInt(binaryNumber.charAt(i), 10); } return decimalNumber; }, /** * 5 * => F" */ encodeDecimalNumber: function(decimalNumber, outputCharset) { if (typeof outputCharset == 'undefined' || outputCharset == null) { outputCharset = this.outputCharset; } var encodedNumber = ""; var base = outputCharset.length; var quotient = decimalNumber; var remainder; while (true) { remainder = quotient % base; encodedNumber = outputCharset.charAt(remainder) + encodedNumber; quotient = (quotient - remainder) / base; if (quotient == 0) { break; } } return encodedNumber; } }; }));