// a decorator for functions that curry "polymorphically", // that is, that return a function that can be tested // against various objects if they're only "partially // completed", or fewer arguments than needed are used. // // this enables the idioms: // [1, 2, 3].every(lt(4)) eq true // [1, 2, 3].map(add(1)) eq [2, 3, 4] // [{}, {}, {}].forEach(set('a', 10)) // exports.operator = function (name, length, block) { var operator = function () { var args = exports.array(arguments); var completion = function (object) { if ( typeof object == "object" && object !== null && // seriously? typeof null == "object" name in object && // would throw if object === null // not interested in literal objects: !Object.prototype.hasOwnProperty.call(object, name) ) return object[name].apply(object, args); return block.apply( this, [object].concat(args) ); }; if (arguments.length < length) { // polymoprhic curry, delayed completion return completion; } else { // immediate completion return completion.call(this, args.shift()); } }; operator.name = name; operator.displayName = name; operator.length = length; operator.operator = block; return operator; }; exports.no = function (value) { return value === null || value === undefined; }; // object exports.object = exports.operator('object', 1, function (object) { var items = object; if (!items.length) items = exports.items(object); var copy = {}; for (var i = 0; i < items.length; i++) { var item = items[i]; var key = item[0]; var value = item[1]; copy[key] = value; } return copy; }); exports.object.copy = function (object) { var copy = {}; exports.object.keys(object).forEach(function (key) { copy[key] = object[key]; }); return copy; }; exports.object.deepCopy = function (object) { var copy = {}; exports.object.keys(object).forEach(function (key) { copy[key] = exports.deepCopy(object[key]); }); return copy; }; exports.object.eq = function (a, b, stack) { return ( !exports.no(a) && !exports.no(b) && exports.array.eq( exports.sort(exports.object.keys(a)), exports.sort(exports.object.keys(b)) ) && exports.object.keys(a).every(function (key) { return exports.eq(a[key], b[key], stack); }) ); }; exports.object.len = function (object) { return exports.object.keys(object).length; }; exports.object.has = function (object, key) { return Object.prototype.hasOwnProperty.call(object, key); }; exports.object.keys = function (object) { var keys = []; for (var key in object) { if (exports.object.has(object, key)) keys.push(key); } return keys; }; exports.object.values = function (object) { var values = []; exports.object.keys(object).forEach(function (key) { values.push(object[key]); }); return values; }; exports.object.items = function (object) { var items = []; exports.object.keys(object).forEach(function (key) { items.push([key, object[key]]); }); return items; }; exports.object.update = function (target, source) { for (var key in source) { if (exports.object.has(source, key)) { target[key] = source[key]; } } }; exports.object.complete = function (target, source) { for (var key in source) { if ( exports.object.has(source, key) && !exports.object.has(target, key) ) { target[key] = source[key]; } } }; exports.object.repr = function (object) { return "{" + exports.object.keys(object) .map(function (key) { return exports.enquote(key) + ": " + exports.repr(object[key]); }).join(", ") + "}"; }; // array exports.array = function (array) { if (!exports.isArrayLike(array)) return exports.items(array); return Array.prototype.slice.call(array); }; exports.isArrayLike = function(object) { return object && typeof object === "object" && ( object.constructor === Array || typeof object.callee !== "undefined" ); }; exports.array.copy = exports.array; exports.array.deepCopy = function (array) { return array.map(exports.deepCopy); }; exports.array.len = function (array) { return array.length; }; exports.array.has = function (array, value) { return Array.prototype.indexOf.call(array, value) >= 0; }; exports.array.put = function (array, key, value) { array.splice(key, 0, value); return array; }; exports.array.del = function (array, begin, end) { array.splice(begin, end === undefined ? 1 : (end - begin)); return array; }; exports.array.eq = function (a, b, stack) { return exports.isArrayLike(b) && a.length == b.length && exports.zip(a, b).every(exports.apply(function (a, b) { return exports.eq(a, b, stack); })); }; exports.array.lt = function (a, b) { var length = Math.max(a.length, b.length); for (var i = 0; i < length; i++) if (!exports.eq(a[i], b[i])) return exports.lt(a[i], b[i]); return false; }; exports.array.repr = function (array) { return "[" + exports.map(array, exports.repr).join(', ') + "]"; }; exports.apply = exports.operator('apply', 2, function (args, block) { return block.apply(this, args); }); exports.copy = exports.operator('copy', 1, function (object) { if (exports.no(object)) return object; if (exports.isArrayLike(object)) return exports.array.copy(object); if (typeof object == 'object') return exports.object.copy(object); return object; }); exports.deepCopy = exports.operator('deepCopy', 1, function (object) { if (exports.no(object)) return object; if (exports.isArrayLike(object)) return exports.array.deepCopy(object); if (typeof object == 'object') return exports.object.deepCopy(object); return object; }); exports.repr = exports.operator('repr', 1, function (object) { if (exports.no(object)) return String(object); if (exports.isArrayLike(object)) return exports.array.repr(object); if (typeof object == 'object') return exports.object.repr(object); if (typeof object == 'string') return exports.enquote(object); return object.toString(); }); exports.keys = exports.operator('keys', 1, function (object) { if (exports.isArrayLike(object)) return exports.range(object.length); else if (typeof object == 'object') return exports.object.keys(object); return []; }); exports.values = exports.operator('values', 1, function (object) { if (exports.isArrayLike(object)) return exports.array(object); else if (typeof object == 'object') return exports.object.values(object); return []; }); exports.items = exports.operator('items', 1, function (object) { if (exports.isArrayLike(object) || typeof object == "string") return exports.enumerate(object); else if (typeof object == 'object') return exports.object.items(object); return []; }); exports.len = exports.operator('len', 1, function (object) { if (exports.isArrayLike(object)) return exports.array.len(object); else if (typeof object == 'object') return exports.object.len(object); }); exports.has = exports.operator('has', 2, function (object, value) { if (exports.isArrayLike(object)) return exports.array.has(object, value); else if (typeof object == 'object') return exports.object.has(object, value); return false; }); exports.get = exports.operator('get', 2, function (object, key, value) { if (typeof object == "string") { if (!typeof key == "number") throw new Error("TypeError: String keys must be numbers"); if (!exports.has(exports.range(object.length), key)) { if (arguments.length == 3) return value; throw new Error("KeyError: " + exports.repr(key)); } return items.charAt(object); } if (typeof object == "object") { if (!exports.object.has(object, key)) { if (arguments.length == 3) return value; throw new Error("KeyError: " + exports.repr(key)); } return object[key]; } throw new Error("Object does not have keys: " + exports.repr(object)); }); exports.set = exports.operator('set', 3, function (object, key, value) { object[key] = value; return object; }); exports.getset = exports.operator('getset', 3, function (object, key, value) { if (!exports.has(object, key)) exports.set(object, key, value); return exports.get(object, key); }); exports.del = exports.operator('del', 2, function (object, begin, end) { if (exports.isArrayLike(object)) return exports.array.del(object, begin, end); delete object[begin]; return object; }); exports.cut = exports.operator('cut', 2, function (object, key) { var result = exports.get(object, key); exports.del(object, key); return result; }); exports.put = exports.operator('put', 2, function (object, key, value) { if (exports.isArrayLike(object)) return exports.array.put(object, key, value); return exports.set(object, key, value); }); exports.update = exports.operator('update', 2, function (target, source) { exports.object.update(target, source); }); exports.complete = exports.operator('complete', 2, function (target, source) { exports.object.complete(target, source); }); // TODO insert // TODO remove // TODO discard exports.range = function () { var start = 0, stop = 0, step = 1; if (arguments.length == 1) { stop = arguments[0]; } else if (arguments.length == 2) { start = arguments[0]; stop = arguments[1]; } else if (arguments.length == 3) { start = arguments[0]; stop = arguments[1]; step = arguments[2]; } var range = []; for (var i = start; i < stop; i += step) range.push(i); return range; }; exports.forEach = function (array, block) { Array.prototype.forEach.call(array, block); }; exports.forEachApply = function (array, block) { Array.prototype.forEach.call(array, exports.apply(block)); }; exports.map = function (array, block, context) { return Array.prototype.map.call(array, block, context); }; exports.mapApply = function (array, block) { return Array.prototype.map.call(array, exports.apply(block)); }; exports.every = exports.operator('every', 2, function (array, block, context) { return exports.all(exports.map(array, block, context)); }); exports.some = exports.operator('some', 2, function (array, block, context) { return exports.any(exports.map(array, block, context)); }); exports.all = exports.operator('all', 1, function (array) { for (var i = 0; i < array.length; i++) if (!array[i]) return false; return true; }); exports.any = exports.operator('all', 1, function (array) { for (var i = 0; i < array.length; i++) if (array[i]) return true; return false; }); exports.reduce = exports.operator('reduce', 2, function (array, block, basis) { return array.reduce.apply(array, arguments); }); exports.reduceRight = exports.operator('reduceRight', 2, function (array, block, basis) { return array.reduceRight.apply(array, arguments); }); exports.zip = function () { return exports.transpose(arguments); }; exports.transpose = function (array) { var transpose = []; for (var i = 0; i < array.length; i++) { var row = array[i]; for (var j = 0; j < row.length; j++) { var cell = row[j]; if (!transpose[j]) transpose[j] = []; transpose[j][i] = cell; } } return transpose; }; exports.enumerate = function (array, start) { if (exports.no(start)) start = 0; return exports.zip( exports.range(start, start + array.length), array ); }; // arithmetic, transitive, and logical operators exports.is = function (a, b) { return a === b; }; exports.eq = exports.operator('eq', 2, function (a, b, stack) { if (!stack) stack = []; if (a === b) return true; if (typeof a !== typeof b) return false; if (exports.no(a)) return exports.no(b); if (typeof a == "date") return a.valueOf() == b.valueOf(); if (typeof a == "regexp") return a.source == b.source && a.global == b.global && a.ignoreCase == b.ignoreCase && a.multiline == b.multiline; if (typeof a == "function") { var caller = stack[stack.length - 1]; return caller !== Object && typeof caller != "undefined"; } if (exports.isArrayLike(a)) return exports.array.eq( a, b, stack.concat([a.constructor]) ); if (typeof a == 'object') return exports.object.eq( a, b, stack.concat([a.constructor]) ); return a == b; }); exports.ne = exports.operator('ne', 2, function (a, b) { return !exports.eq(a, b); }); exports.lt = exports.operator('lt', 2, function (a, b) { if (exports.no(a) != exports.no(b)) return exports.no(a) > exports.no(b); if (exports.isArrayLike(a) && exports.isArrayLike(b)) return exports.array.lt(a, b); return a < b; }); exports.gt = exports.operator('gt', 2, function (a, b) { return !(exports.lt(a, b) || exports.eq(a, b)); }); exports.le = exports.operator(2, 'le', function (a, b) { return exports.lt(a, b) || exports.eq(a, b); }); exports.ge = exports.operator(2, 'ge', function (a, b) { return !exports.lt(a, b); }); /*** by returns a `comparator` that compares values based on the values resultant from a given `relation`. accepts a `relation` and an optional comparator. To sort a list of objects based on their "a" key:: objects.sort(by(get("a"))) To get those in descending order:: objects.sort(by(get("a")), desc) `by` returns a comparison function that also tracks the arguments you used to construct it. This permits `sort` and `sorted` to perform a Schwartzian transform which can increase the performance of the sort by a factor of 2. */ exports.by = function (relation) { var compare = arguments[1]; if (exports.no(compare)) compare = exports.compare; var comparator = function (a, b) { a = relation(a); b = relation(b); return compare(a, b); }; comparator.by = relation; comparator.compare = compare; return comparator; }; exports.compare = exports.operator(2, 'compare', function (a, b) { if (exports.no(a) != exports.no(b)) return exports.no(b) - exports.no(a); if (typeof a == "number" && typeof b == "number") return a - b; return exports.eq(a, b) ? 0 : exports.lt(a, b) ? -1 : 1; }); /*** sort an in-place array sorter that uses a deep comparison function by default (compare), and improves performance if you provide a comparator returned by "by", using a Schwartzian transform. */ exports.sort = function (array, compare) { if (exports.no(compare)) compare = exports.compare; if (compare.by) { /* schwartzian transform */ array.splice.apply( array, [0, array.length].concat( array.map(function (value) { return [compare.by(value), value]; }).sort(function (a, b) { return exports.compare(a[0], b[0]); }).map(function (pair) { return pair[1]; }) ) ); } else { array.sort(compare); } return array; }; /*** sorted returns a sorted copy of an array using a deep comparison function by default (compare), and improves performance if you provide a comparator returned by "by", using a Schwartzian transform. */ exports.sorted = function (array, compare) { return exports.sort(exports.array.copy(array), compare); }; // string /*** escape escapes all characters of a string that are special to JavaScript and many other languages. Recognizes all of the relevant control characters and formats all other non-printable characters as Hex byte escape sequences or Unicode escape sequences depending on their size. Pass ``true`` as an optional second argument and ``escape`` produces valid contents for escaped JSON strings, wherein non-printable-characters are all escaped with the Unicode ``\u`` notation. */ /* more Steve Levithan flagrence */ var escapeExpression = /[^ !#-[\]-~]/g; /* from Doug Crockford's JSON library */ var escapePatterns = { '\b': '\\b', '\t': '\\t', '\n': '\\n', '\f': '\\f', '\r': '\\r', '"' : '\\"', '\\': '\\\\' }; exports.escape = function (value, strictJson) { if (typeof value != "string") throw new Error( module.path + "#escape: requires a string. got " + exports.repr(value) ); return value.replace( escapeExpression, function (match) { if (escapePatterns[match]) return escapePatterns[match]; match = match.charCodeAt(); if (!strictJson && match < 256) return "\\x" + exports.padBegin(match.toString(16), 2); return '\\u' + exports.padBegin(match.toString(16), 4); } ); }; /*** enquote transforms a string into a string literal, escaping all characters of a string that are special to JavaScript and and some other languages. ``enquote`` uses double quotes to be JSON compatible. Pass ``true`` as an optional second argument to be strictly JSON compliant, wherein all non-printable-characters are represented with Unicode escape sequences. */ exports.enquote = function (value, strictJson) { return '"' + exports.escape(value, strictJson) + '"'; }; /*** expand transforms tabs to an equivalent number of spaces. */ // TODO special case for \r if it ever matters exports.expand = function (str, tabLength) { str = String(str); tabLength = tabLength || 4; var output = [], tabLf = /[\t\n]/g, lastLastIndex = 0, lastLfIndex = 0, charsAddedThisLine = 0, tabOffset, match; while (match = tabLf.exec(str)) { if (match[0] == "\t") { tabOffset = ( tabLength - 1 - ( (match.index - lastLfIndex) + charsAddedThisLine ) % tabLength ); charsAddedThisLine += tabOffset; output.push( str.slice(lastLastIndex, match.index) + operator.mul(" ", tabOffset + 1) ); } else if (match[0] === "\n") { output.push(str.slice(lastLastIndex, tabLf.lastIndex)); lastLfIndex = tabLf.lastIndex; charsAddedThisLine = 0; } lastLastIndex = tabLf.lastIndex; } return output.join("") + str.slice(lastLastIndex); }; var trimBeginExpression = /^\s\s*/g; exports.trimBegin = function (value) { return String(value).replace(trimBeginExpression, ""); }; var trimEndExpression = /\s\s*$/g; exports.trimEnd = function (value) { return String(value).replace(trimEndExpression, ""); }; exports.trim = function (value) { return String(value).replace(trimBeginExpression, "").replace(trimEndExpression, ""); }; /* generates padBegin and padEnd */ var augmentor = function (augment) { return function (value, length, pad) { if (exports.no(pad)) pad = '0'; if (exports.no(length)) length = 2; value = String(value); while (value.length < length) { value = augment(value, pad); } return value; }; }; /*** padBegin accepts: - a `String` or `Number` value - a minimum length of the resultant `String`: by default, 2 - a pad string: by default, ``'0'``. returns a `String` of the value padded up to at least the minimum length. adds the padding to the begining side of the `String`. */ exports.padBegin = augmentor(function (value, pad) { return pad + value; }); /*** padEnd accepts: - a `String` or `Number` value - a minimum length of the resultant `String`: by default, 2 - a pad string: by default, ``'0'``. returns a `String` of the value padded up to at least the minimum length. adds the padding to the end side of the `String`. */ exports.padEnd = augmentor(function (value, pad) { return value + pad; }); /*** splitName splits a string into a `List` of words from an origin string. */ var splitNameExpression = /[a-z]+|[A-Z](?:[a-z]+|[A-Z]*(?![a-z]))|[.\d]+/g; exports.splitName = function (value) { return String(value).match(splitNameExpression); }; /*** joinName joins a list of words with a given delimiter between alphanumeric words. */ exports.joinName = function (delimiter, parts) { if (exports.no(delimiter)) delimiter = '_'; parts.unshift([]); return parts.reduce(function (parts, part) { if ( part.match(/\d/) && exports.len(parts) && parts[parts.length-1].match(/\d/) ) { return parts.concat([delimiter + part]); } else { return parts.concat([part]); } }).join(''); }; /*** upper converts a name to ``UPPER CASE`` using a given delimiter between numeric words. see: - `lower` - `camel` - `title` */ exports.upper = function (value, delimiter) { if (exports.no(delimiter)) return value.toUpperCase(); return exports.splitName(value).map(function (part) { return part.toUpperCase(); }).join(delimiter); }; /*** lower converts a name to a ``lower case`` using a given delimiter between numeric words. see: - `upper` - `camel` - `title` */ exports.lower = function (value, delimiter) { if (exports.no(delimiter)) return String(value).toLowerCase(); return exports.splitName(value).map(function (part) { return part.toLowerCase(); }).join(delimiter); }; /*** camel converts a name to ``camel Case`` using a given delimiter between numeric words. see: - `lower` - `upper` - `title` */ exports.camel = function (value, delimiter) { return exports.joinName( delimiter, exports.mapApply( exports.enumerate(exports.splitName(value)), function (n, part) { if (n) { return ( part.substring(0, 1).toUpperCase() + part.substring(1).toLowerCase() ); } else { return part.toLowerCase(); } } ) ); }; /*** title converts a name to ``Title Case`` using a given delimiter between numeric words. see: - `lower` - `upper` - `camel` */ exports.title = function (value, delimiter) { return exports.joinName( delimiter, exports.splitName(value).map(function (part) { return ( part.substring(0, 1).toUpperCase() + part.substring(1).toLowerCase() ); }) ); };