UNPKG

pouchdb-collate

Version:

Collation functions for PouchDB map/reduce

386 lines (354 loc) 10.4 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); function pad(str, padWith, upToLength) { var padding = ''; var targetLength = upToLength - str.length; /* istanbul ignore next */ while (padding.length < targetLength) { padding += padWith; } return padding; } function padLeft(str, padWith, upToLength) { var padding = pad(str, padWith, upToLength); return padding + str; } var MIN_MAGNITUDE = -324; // verified by -Number.MIN_VALUE var MAGNITUDE_DIGITS = 3; // ditto var SEP = ''; // set to '_' for easier debugging function collate(a, b) { if (a === b) { return 0; } a = normalizeKey(a); b = normalizeKey(b); var ai = collationIndex(a); var bi = collationIndex(b); if ((ai - bi) !== 0) { return ai - bi; } switch (typeof a) { case 'number': return a - b; case 'boolean': return a < b ? -1 : 1; case 'string': return stringCollate(a, b); } return Array.isArray(a) ? arrayCollate(a, b) : objectCollate(a, b); } // couch considers null/NaN/Infinity/-Infinity === undefined, // for the purposes of mapreduce indexes. also, dates get stringified. function normalizeKey(key) { switch (typeof key) { case 'undefined': return null; case 'number': if (key === Infinity || key === -Infinity || isNaN(key)) { return null; } return key; case 'object': var origKey = key; if (Array.isArray(key)) { var len = key.length; key = new Array(len); for (var i = 0; i < len; i++) { key[i] = normalizeKey(origKey[i]); } /* istanbul ignore next */ } else if (key instanceof Date) { return key.toJSON(); } else if (key !== null) { // generic object key = {}; for (var k in origKey) { if (Object.prototype.hasOwnProperty.call(origKey, k)) { var val = origKey[k]; if (typeof val !== 'undefined') { key[k] = normalizeKey(val); } } } } } return key; } function indexify(key) { if (key !== null) { switch (typeof key) { case 'boolean': return key ? 1 : 0; case 'number': return numToIndexableString(key); case 'string': // We've to be sure that key does not contain \u0000 // Do order-preserving replacements: // 0 -> 1, 1 // 1 -> 1, 2 // 2 -> 2, 2 /* eslint-disable no-control-regex */ return key .replace(/\u0002/g, '\u0002\u0002') .replace(/\u0001/g, '\u0001\u0002') .replace(/\u0000/g, '\u0001\u0001'); /* eslint-enable no-control-regex */ case 'object': var isArray = Array.isArray(key); var arr = isArray ? key : Object.keys(key); var i = -1; var len = arr.length; var result = ''; if (isArray) { while (++i < len) { result += toIndexableString(arr[i]); } } else { while (++i < len) { var objKey = arr[i]; result += toIndexableString(objKey) + toIndexableString(key[objKey]); } } return result; } } return ''; } // convert the given key to a string that would be appropriate // for lexical sorting, e.g. within a database, where the // sorting is the same given by the collate() function. function toIndexableString(key) { var zero = '\u0000'; key = normalizeKey(key); return collationIndex(key) + SEP + indexify(key) + zero; } function parseNumber(str, i) { var originalIdx = i; var num; var zero = str[i] === '1'; if (zero) { num = 0; i++; } else { var neg = str[i] === '0'; i++; var numAsString = ''; var magAsString = str.substring(i, i + MAGNITUDE_DIGITS); var magnitude = parseInt(magAsString, 10) + MIN_MAGNITUDE; /* istanbul ignore next */ if (neg) { magnitude = -magnitude; } i += MAGNITUDE_DIGITS; while (true) { var ch = str[i]; if (ch === '\u0000') { break; } else { numAsString += ch; } i++; } numAsString = numAsString.split('.'); if (numAsString.length === 1) { num = parseInt(numAsString, 10); } else { /* istanbul ignore next */ num = parseFloat(numAsString[0] + '.' + numAsString[1]); } /* istanbul ignore next */ if (neg) { num = num - 10; } /* istanbul ignore next */ if (magnitude !== 0) { // parseFloat is more reliable than pow due to rounding errors // e.g. Number.MAX_VALUE would return Infinity if we did // num * Math.pow(10, magnitude); num = parseFloat(num + 'e' + magnitude); } } return {num: num, length : i - originalIdx}; } // move up the stack while parsing // this function moved outside of parseIndexableString for performance function pop(stack, metaStack) { var obj = stack.pop(); if (metaStack.length) { var lastMetaElement = metaStack[metaStack.length - 1]; if (obj === lastMetaElement.element) { // popping a meta-element, e.g. an object whose value is another object metaStack.pop(); lastMetaElement = metaStack[metaStack.length - 1]; } var element = lastMetaElement.element; var lastElementIndex = lastMetaElement.index; if (Array.isArray(element)) { element.push(obj); } else if (lastElementIndex === stack.length - 2) { // obj with key+value var key = stack.pop(); element[key] = obj; } else { stack.push(obj); // obj with key only } } } function parseIndexableString(str) { var stack = []; var metaStack = []; // stack for arrays and objects var i = 0; /*eslint no-constant-condition: ["error", { "checkLoops": false }]*/ while (true) { var collationIndex = str[i++]; if (collationIndex === '\u0000') { if (stack.length === 1) { return stack.pop(); } else { pop(stack, metaStack); continue; } } switch (collationIndex) { case '1': stack.push(null); break; case '2': stack.push(str[i] === '1'); i++; break; case '3': var parsedNum = parseNumber(str, i); stack.push(parsedNum.num); i += parsedNum.length; break; case '4': var parsedStr = ''; /*eslint no-constant-condition: ["error", { "checkLoops": false }]*/ while (true) { var ch = str[i]; if (ch === '\u0000') { break; } parsedStr += ch; i++; } // perform the reverse of the order-preserving replacement // algorithm (see above) /* eslint-disable no-control-regex */ parsedStr = parsedStr.replace(/\u0001\u0001/g, '\u0000') .replace(/\u0001\u0002/g, '\u0001') .replace(/\u0002\u0002/g, '\u0002'); /* eslint-enable no-control-regex */ stack.push(parsedStr); break; case '5': var arrayElement = { element: [], index: stack.length }; stack.push(arrayElement.element); metaStack.push(arrayElement); break; case '6': var objElement = { element: {}, index: stack.length }; stack.push(objElement.element); metaStack.push(objElement); break; /* istanbul ignore next */ default: throw new Error( 'bad collationIndex or unexpectedly reached end of input: ' + collationIndex); } } } function arrayCollate(a, b) { var len = Math.min(a.length, b.length); for (var i = 0; i < len; i++) { var sort = collate(a[i], b[i]); if (sort !== 0) { return sort; } } return (a.length === b.length) ? 0 : (a.length > b.length) ? 1 : -1; } function stringCollate(a, b) { // See: https://github.com/daleharvey/pouchdb/issues/40 // This is incompatible with the CouchDB implementation, but its the // best we can do for now return (a === b) ? 0 : ((a > b) ? 1 : -1); } function objectCollate(a, b) { var ak = Object.keys(a), bk = Object.keys(b); var len = Math.min(ak.length, bk.length); for (var i = 0; i < len; i++) { // First sort the keys var sort = collate(ak[i], bk[i]); if (sort !== 0) { return sort; } // if the keys are equal sort the values sort = collate(a[ak[i]], b[bk[i]]); if (sort !== 0) { return sort; } } return (ak.length === bk.length) ? 0 : (ak.length > bk.length) ? 1 : -1; } // The collation is defined by erlangs ordered terms // the atoms null, true, false come first, then numbers, strings, // arrays, then objects // null/undefined/NaN/Infinity/-Infinity are all considered null function collationIndex(x) { var id = ['boolean', 'number', 'string', 'object']; var idx = id.indexOf(typeof x); //false if -1 otherwise true, but fast!!!!1 if (~idx) { if (x === null) { return 1; } if (Array.isArray(x)) { return 5; } return idx < 3 ? (idx + 2) : (idx + 3); } /* istanbul ignore next */ if (Array.isArray(x)) { return 5; } } // conversion: // x yyy zz...zz // x = 0 for negative, 1 for 0, 2 for positive // y = exponent (for negative numbers negated) moved so that it's >= 0 // z = mantisse function numToIndexableString(num) { if (num === 0) { return '1'; } // convert number to exponential format for easier and // more succinct string sorting var expFormat = num.toExponential().split(/e\+?/); var magnitude = parseInt(expFormat[1], 10); var neg = num < 0; var result = neg ? '0' : '2'; // first sort by magnitude // it's easier if all magnitudes are positive var magForComparison = ((neg ? -magnitude : magnitude) - MIN_MAGNITUDE); var magString = padLeft((magForComparison).toString(), '0', MAGNITUDE_DIGITS); result += SEP + magString; // then sort by the factor var factor = Math.abs(parseFloat(expFormat[0])); // [1..10) /* istanbul ignore next */ if (neg) { // for negative reverse ordering factor = 10 - factor; } var factorStr = factor.toFixed(20); // strip zeros from the end factorStr = factorStr.replace(/\.?0+$/, ''); result += SEP + factorStr; return result; } exports.collate = collate; exports.normalizeKey = normalizeKey; exports.toIndexableString = toIndexableString; exports.parseIndexableString = parseIndexableString;