UNPKG

sugar

Version:

A Javascript library for working with native objects.

1,593 lines (1,408 loc) 294 kB
/* * Sugar Library vedge * * Freely distributable and licensed under the MIT-style license. * Copyright (c) 2012 Andrew Plummer * http://sugarjs.com/ * * ---------------------------- */ (function(){ /*** * @package Core * @description Internal utility and common methods. ***/ // A few optimizations for Google Closure Compiler will save us a couple kb in the release script. var object = Object, array = Array, regexp = RegExp, date = Date, string = String, number = Number, math = Math, Undefined; // The global context var globalContext = typeof global !== 'undefined' ? global : this; // defineProperty exists in IE8 but will error when trying to define a property on // native objects. IE8 does not have defineProperies, however, so this check saves a try/catch block. var definePropertySupport = object.defineProperty && object.defineProperties; // Class initializers and class helpers var ClassNames = 'Array,Boolean,Date,Function,Number,String,RegExp'.split(','); var isArray = buildClassCheck(ClassNames[0]); var isBoolean = buildClassCheck(ClassNames[1]); var isDate = buildClassCheck(ClassNames[2]); var isFunction = buildClassCheck(ClassNames[3]); var isNumber = buildClassCheck(ClassNames[4]); var isString = buildClassCheck(ClassNames[5]); var isRegExp = buildClassCheck(ClassNames[6]); function buildClassCheck(type) { return function(obj) { return className(obj) === '[object '+type+']'; } } function className(obj) { return object.prototype.toString.call(obj); } function initializeClasses() { initializeClass(object); iterateOverObject(ClassNames, function(i,name) { initializeClass(globalContext[name]); }); } function initializeClass(klass) { if(klass['SugarMethods']) return; defineProperty(klass, 'SugarMethods', {}); extend(klass, false, false, { 'restore': function() { var all = arguments.length === 0, methods = multiArgs(arguments); iterateOverObject(klass['SugarMethods'], function(name, m) { if(all || methods.indexOf(name) > -1) { defineProperty(m.instance ? klass.prototype : klass, name, m.method); } }); }, 'extend': function(methods, override, instance) { extend(klass, instance !== false, override, methods); } }); } // Class extending methods function extend(klass, instance, override, methods) { var extendee = instance ? klass.prototype : klass, original; initializeClass(klass); iterateOverObject(methods, function(name, method) { original = extendee[name]; if(typeof override === 'function') { method = wrapNative(extendee[name], method, override); } if(override !== false || !extendee[name]) { defineProperty(extendee, name, method); } // If the method is internal to Sugar, then store a reference so it can be restored later. klass['SugarMethods'][name] = { instance: instance, method: method, original: original }; }); } function extendSimilar(klass, instance, override, set, fn) { var methods = {}; set = isString(set) ? set.split(',') : set; set.forEach(function(name, i) { fn(methods, name, i); }); extend(klass, instance, override, methods); } function wrapNative(nativeFn, extendedFn, condition) { return function() { if(nativeFn && (condition === true || !condition.apply(this, arguments))) { return nativeFn.apply(this, arguments); } else { return extendedFn.apply(this, arguments); } } } function defineProperty(target, name, method) { if(definePropertySupport) { object.defineProperty(target, name, { 'value': method, 'configurable': true, 'enumerable': false, 'writable': true }); } else { target[name] = method; } } // Argument helpers function multiArgs(args, fn) { var result = [], i; for(i = 0; i < args.length; i++) { result.push(args[i]); if(fn) fn.call(args, args[i], i); } return result; } function flattenedArgs(obj, fn, from) { multiArgs(array.prototype.concat.apply([], array.prototype.slice.call(obj, from || 0)), fn); } function checkCallback(fn) { if(!fn || !fn.call) { throw new TypeError('Callback is not callable'); } } // General helpers function isDefined(o) { return o !== Undefined; } function isUndefined(o) { return o === Undefined; } // Object helpers function isObjectPrimitive(obj) { // Check for null return obj && typeof obj === 'object'; } function isObject(obj) { // === on the constructor is not safe across iframes // 'hasOwnProperty' ensures that the object also inherits // from Object, which is false for DOMElements in IE. return !!obj && className(obj) === '[object Object]' && 'hasOwnProperty' in obj; } function hasOwnProperty(obj, key) { return object['hasOwnProperty'].call(obj, key); } function iterateOverObject(obj, fn) { var key; for(key in obj) { if(!hasOwnProperty(obj, key)) continue; if(fn.call(obj, key, obj[key], obj) === false) break; } } function simpleMerge(target, source) { iterateOverObject(source, function(key) { target[key] = source[key]; }); return target; } // Hash definition function Hash(obj) { simpleMerge(this, obj); }; Hash.prototype.constructor = object; // Number helpers function getRange(start, stop, fn, step) { var arr = [], i = parseInt(start), down = step < 0; while((!down && i <= stop) || (down && i >= stop)) { arr.push(i); if(fn) fn.call(this, i); i += step || 1; } return arr; } function round(val, precision, method) { var fn = math[method || 'round']; var multiplier = math.pow(10, math.abs(precision || 0)); if(precision < 0) multiplier = 1 / multiplier; return fn(val * multiplier) / multiplier; } function ceil(val, precision) { return round(val, precision, 'ceil'); } function floor(val, precision) { return round(val, precision, 'floor'); } function padNumber(num, place, sign, base) { var str = math.abs(num).toString(base || 10); str = repeatString(place - str.replace(/\.\d+/, '').length, '0') + str; if(sign || num < 0) { str = (num < 0 ? '-' : '+') + str; } return str; } function getOrdinalizedSuffix(num) { if(num >= 11 && num <= 13) { return 'th'; } else { switch(num % 10) { case 1: return 'st'; case 2: return 'nd'; case 3: return 'rd'; default: return 'th'; } } } // String helpers // WhiteSpace/LineTerminator as defined in ES5.1 plus Unicode characters in the Space, Separator category. function getTrimmableCharacters() { return '\u0009\u000A\u000B\u000C\u000D\u0020\u00A0\u1680\u180E\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u2028\u2029\u3000\uFEFF'; } function repeatString(times, str) { return array(math.max(0, isDefined(times) ? times : 1) + 1).join(str || ''); } // RegExp helpers function getRegExpFlags(reg, add) { var flags = reg.toString().match(/[^/]*$/)[0]; if(add) { flags = (flags + add).split('').sort().join('').replace(/([gimy])\1+/g, '$1'); } return flags; } function escapeRegExp(str) { if(!isString(str)) str = string(str); return str.replace(/([\\/'*+?|()\[\]{}.^$])/g,'\\$1'); } // Specialized helpers // Used by Array#unique and Object.equal function stringify(thing, stack) { var type = typeof thing, thingIsObject, thingIsArray, klass, value, arr, key, i; // Return quickly if string to save cycles if(type === 'string') return thing; klass = object.prototype.toString.call(thing) thingIsObject = isObject(thing); thingIsArray = klass === '[object Array]'; if(thing != null && thingIsObject || thingIsArray) { // This method for checking for cyclic structures was egregiously stolen from // the ingenious method by @kitcambridge from the Underscore script: // https://github.com/documentcloud/underscore/issues/240 if(!stack) stack = []; // Allowing a step into the structure before triggering this // script to save cycles on standard JSON structures and also to // try as hard as possible to catch basic properties that may have // been modified. if(stack.length > 1) { i = stack.length; while (i--) { if (stack[i] === thing) { return 'CYC'; } } } stack.push(thing); value = string(thing.constructor); arr = thingIsArray ? thing : object.keys(thing).sort(); for(i = 0; i < arr.length; i++) { key = thingIsArray ? i : arr[i]; value += key + stringify(thing[key], stack); } stack.pop(); } else if(1 / thing === -Infinity) { value = '-0'; } else { value = string(thing && thing.valueOf ? thing.valueOf() : thing); } return type + klass + value; } function isEqual(a, b) { if(objectIsMatchedByValue(a) && objectIsMatchedByValue(b)) { return stringify(a) === stringify(b); } else { return a === b; } } function objectIsMatchedByValue(obj) { var klass = className(obj); return klass === '[object Date]' || klass === '[object Array]' || klass === '[object String]' || klass === '[object Number]' || klass === '[object RegExp]' || klass === '[object Boolean]' || klass === '[object Arguments]' || isObject(obj); } // Used by Array#at and String#at function entryAtIndex(arr, args, str) { var result = [], length = arr.length, loop = args[args.length - 1] !== false, r; multiArgs(args, function(index) { if(isBoolean(index)) return false; if(loop) { index = index % length; if(index < 0) index = length + index; } r = str ? arr.charAt(index) || '' : arr[index]; result.push(r); }); return result.length < 2 ? result[0] : result; } // Object class methods implemented as instance methods function buildObjectInstanceMethods(set, target) { extendSimilar(target, true, false, set, function(methods, name) { methods[name + (name === 'equal' ? 's' : '')] = function() { return object[name].apply(null, [this].concat(multiArgs(arguments))); } }); } initializeClasses(); /*** * @package ES5 * @description Shim methods that provide ES5 compatible functionality. This package can be excluded if you do not require legacy browser support (IE8 and below). * ***/ /*** * Object module * ***/ extend(object, false, false, { 'keys': function(obj) { var keys = []; if(!isObjectPrimitive(obj) && !isRegExp(obj) && !isFunction(obj)) { throw new TypeError('Object required'); } iterateOverObject(obj, function(key, value) { keys.push(key); }); return keys; } }); /*** * Array module * ***/ // ECMA5 methods function arrayIndexOf(arr, search, fromIndex, increment) { var length = arr.length, fromRight = increment == -1, start = fromRight ? length - 1 : 0, index = toIntegerWithDefault(fromIndex, start); if(index < 0) { index = length + index; } if((!fromRight && index < 0) || (fromRight && index >= length)) { index = start; } while((fromRight && index >= 0) || (!fromRight && index < length)) { if(arr[index] === search) { return index; } index += increment; } return -1; } function arrayReduce(arr, fn, initialValue, fromRight) { var length = arr.length, count = 0, defined = isDefined(initialValue), result, index; checkCallback(fn); if(length == 0 && !defined) { throw new TypeError('Reduce called on empty array with no initial value'); } else if(defined) { result = initialValue; } else { result = arr[fromRight ? length - 1 : count]; count++; } while(count < length) { index = fromRight ? length - count - 1 : count; if(index in arr) { result = fn(result, arr[index], index, arr); } count++; } return result; } function toIntegerWithDefault(i, d) { if(isNaN(i)) { return d; } else { return parseInt(i >> 0); } } function checkFirstArgumentExists(args) { if(args.length === 0) { throw new TypeError('First argument must be defined'); } } extend(array, false, false, { /*** * * @method Array.isArray(<obj>) * @returns Boolean * @short Returns true if <obj> is an Array. * @extra This method is provided for browsers that don't support it internally. * @example * * Array.isArray(3) -> false * Array.isArray(true) -> false * Array.isArray('wasabi') -> false * Array.isArray([1,2,3]) -> true * ***/ 'isArray': function(obj) { return isArray(obj); } }); extend(array, true, false, { /*** * @method every(<f>, [scope]) * @returns Boolean * @short Returns true if all elements in the array match <f>. * @extra [scope] is the %this% object. %all% is provided an alias. In addition to providing this method for browsers that don't support it natively, this method also implements @array_matching. * @example * + ['a','a','a'].every(function(n) { * return n == 'a'; * }); * ['a','a','a'].every('a') -> true * [{a:2},{a:2}].every({a:2}) -> true ***/ 'every': function(fn, scope) { var length = this.length, index = 0; checkFirstArgumentExists(arguments); while(index < length) { if(index in this && !fn.call(scope, this[index], index, this)) { return false; } index++; } return true; }, /*** * @method some(<f>, [scope]) * @returns Boolean * @short Returns true if any element in the array matches <f>. * @extra [scope] is the %this% object. %any% is provided as an alias. In addition to providing this method for browsers that don't support it natively, this method also implements @array_matching. * @example * + ['a','b','c'].some(function(n) { * return n == 'a'; * }); + ['a','b','c'].some(function(n) { * return n == 'd'; * }); * ['a','b','c'].some('a') -> true * [{a:2},{b:5}].some({a:2}) -> true ***/ 'some': function(fn, scope) { var length = this.length, index = 0; checkFirstArgumentExists(arguments); while(index < length) { if(index in this && fn.call(scope, this[index], index, this)) { return true; } index++; } return false; }, /*** * @method map(<map>, [scope]) * @returns Array * @short Maps the array to another array containing the values that are the result of calling <map> on each element. * @extra [scope] is the %this% object. In addition to providing this method for browsers that don't support it natively, this enhanced method also directly accepts a string, which is a shortcut for a function that gets that property (or invokes a function) on each element. * @example * + [1,2,3].map(function(n) { * return n * 3; * }); -> [3,6,9] * ['one','two','three'].map(function(n) { * return n.length; * }); -> [3,3,5] * ['one','two','three'].map('length') -> [3,3,5] ***/ 'map': function(fn, scope) { var length = this.length, index = 0, result = new Array(length); checkFirstArgumentExists(arguments); while(index < length) { if(index in this) { result[index] = fn.call(scope, this[index], index, this); } index++; } return result; }, /*** * @method filter(<f>, [scope]) * @returns Array * @short Returns any elements in the array that match <f>. * @extra [scope] is the %this% object. In addition to providing this method for browsers that don't support it natively, this method also implements @array_matching. * @example * + [1,2,3].filter(function(n) { * return n > 1; * }); * [1,2,2,4].filter(2) -> 2 * ***/ 'filter': function(fn, scope) { var length = this.length, index = 0, result = []; checkFirstArgumentExists(arguments); while(index < length) { if(index in this && fn.call(scope, this[index], index, this)) { result.push(this[index]); } index++; } return result; }, /*** * @method indexOf(<search>, [fromIndex]) * @returns Number * @short Searches the array and returns the first index where <search> occurs, or -1 if the element is not found. * @extra [fromIndex] is the index from which to begin the search. This method performs a simple strict equality comparison on <search>. It does not support enhanced functionality such as searching the contents against a regex, callback, or deep comparison of objects. For such functionality, use the %findIndex% method instead. * @example * * [1,2,3].indexOf(3) -> 1 * [1,2,3].indexOf(7) -> -1 * ***/ 'indexOf': function(search, fromIndex) { if(isString(this)) return this.indexOf(search, fromIndex); return arrayIndexOf(this, search, fromIndex, 1); }, /*** * @method lastIndexOf(<search>, [fromIndex]) * @returns Number * @short Searches the array and returns the last index where <search> occurs, or -1 if the element is not found. * @extra [fromIndex] is the index from which to begin the search. This method performs a simple strict equality comparison on <search>. * @example * * [1,2,1].lastIndexOf(1) -> 2 * [1,2,1].lastIndexOf(7) -> -1 * ***/ 'lastIndexOf': function(search, fromIndex) { if(isString(this)) return this.lastIndexOf(search, fromIndex); return arrayIndexOf(this, search, fromIndex, -1); }, /*** * @method forEach([fn], [scope]) * @returns Nothing * @short Iterates over the array, calling [fn] on each loop. * @extra This method is only provided for those browsers that do not support it natively. [scope] becomes the %this% object. * @example * * ['a','b','c'].forEach(function(a) { * // Called 3 times: 'a','b','c' * }); * ***/ 'forEach': function(fn, scope) { var length = this.length, index = 0; checkCallback(fn); while(index < length) { if(index in this) { fn.call(scope, this[index], index, this); } index++; } }, /*** * @method reduce(<fn>, [init]) * @returns Mixed * @short Reduces the array to a single result. * @extra If [init] is passed as a starting value, that value will be passed as the first argument to the callback. The second argument will be the first element in the array. From that point, the result of the callback will then be used as the first argument of the next iteration. This is often refered to as "accumulation", and [init] is often called an "accumulator". If [init] is not passed, then <fn> will be called n - 1 times, where n is the length of the array. In this case, on the first iteration only, the first argument will be the first element of the array, and the second argument will be the second. After that callbacks work as normal, using the result of the previous callback as the first argument of the next. This method is only provided for those browsers that do not support it natively. * * @example * + [1,2,3,4].reduce(function(a, b) { * return a - b; * }); + [1,2,3,4].reduce(function(a, b) { * return a - b; * }, 100); * ***/ 'reduce': function(fn, init) { return arrayReduce(this, fn, init); }, /*** * @method reduceRight([fn], [init]) * @returns Mixed * @short Identical to %Array#reduce%, but operates on the elements in reverse order. * @extra This method is only provided for those browsers that do not support it natively. * * * * * @example * + [1,2,3,4].reduceRight(function(a, b) { * return a - b; * }); * ***/ 'reduceRight': function(fn, init) { return arrayReduce(this, fn, init, true); } }); /*** * String module * ***/ function buildTrim() { var support = getTrimmableCharacters().match(/^\s+$/); try { string.prototype.trim.call([1]); } catch(e) { support = false; } extend(string, true, !support, { /*** * @method trim[Side]() * @returns String * @short Removes leading and/or trailing whitespace from the string. * @extra Whitespace is defined as line breaks, tabs, and any character in the "Space, Separator" Unicode category, conforming to the the ES5 spec. The standard %trim% method is only added when not fully supported natively. * * @set * trim * trimLeft * trimRight * * @example * * ' wasabi '.trim() -> 'wasabi' * ' wasabi '.trimLeft() -> 'wasabi ' * ' wasabi '.trimRight() -> ' wasabi' * ***/ 'trim': function() { return this.toString().trimLeft().trimRight(); }, 'trimLeft': function() { return this.replace(regexp('^['+getTrimmableCharacters()+']+'), ''); }, 'trimRight': function() { return this.replace(regexp('['+getTrimmableCharacters()+']+$'), ''); } }); } /*** * Function module * ***/ extend(Function, true, false, { /*** * @method bind(<scope>, [arg1], ...) * @returns Function * @short Binds <scope> as the %this% object for the function when it is called. Also allows currying an unlimited number of parameters. * @extra "currying" means setting parameters ([arg1], [arg2], etc.) ahead of time so that they are passed when the function is called later. If you pass additional parameters when the function is actually called, they will be added will be added to the end of the curried parameters. This method is provided for browsers that don't support it internally. * @example * + (function() { * return this; * }).bind('woof')(); -> returns 'woof'; function is bound with 'woof' as the this object. * (function(a) { * return a; * }).bind(1, 2)(); -> returns 2; function is bound with 1 as the this object and 2 curried as the first parameter * (function(a, b) { * return a + b; * }).bind(1, 2)(3); -> returns 5; function is bound with 1 as the this object, 2 curied as the first parameter and 3 passed as the second when calling the function * ***/ 'bind': function(scope) { var fn = this, args = multiArgs(arguments).slice(1), nop, bound; if(!isFunction(this)) { throw new TypeError('Function.prototype.bind called on a non-function'); } bound = function() { return fn.apply(fn.prototype && this instanceof fn ? this : scope, args.concat(multiArgs(arguments))); } bound.prototype = this.prototype; return bound; } }); /*** * Date module * ***/ /*** * @method toISOString() * @returns String * @short Formats the string to ISO8601 format. * @extra This will always format as UTC time. Provided for browsers that do not support this method. * @example * * Date.create().toISOString() -> ex. 2011-07-05 12:24:55.528Z * *** * @method toJSON() * @returns String * @short Returns a JSON representation of the date. * @extra This is effectively an alias for %toISOString%. Will always return the date in UTC time. Provided for browsers that do not support this method. * @example * * Date.create().toJSON() -> ex. 2011-07-05 12:24:55.528Z * ***/ extend(date, false, false, { /*** * @method Date.now() * @returns String * @short Returns the number of milliseconds since January 1st, 1970 00:00:00 (UTC time). * @extra Provided for browsers that do not support this method. * @example * * Date.now() -> ex. 1311938296231 * ***/ 'now': function() { return new date().getTime(); } }); function buildISOString() { var d = new date(date.UTC(1999, 11, 31)), target = '1999-12-31T00:00:00.000Z'; var support = d.toISOString && d.toISOString() === target; extendSimilar(date, true, !support, 'toISOString,toJSON', function(methods, name) { methods[name] = function() { return padNumber(this.getUTCFullYear(), 4) + '-' + padNumber(this.getUTCMonth() + 1, 2) + '-' + padNumber(this.getUTCDate(), 2) + 'T' + padNumber(this.getUTCHours(), 2) + ':' + padNumber(this.getUTCMinutes(), 2) + ':' + padNumber(this.getUTCSeconds(), 2) + '.' + padNumber(this.getUTCMilliseconds(), 3) + 'Z'; } }); } // Initialize buildTrim(); buildISOString(); /*** * @package Array * @dependency core * @description Array manipulation and traversal, "fuzzy matching" against elements, alphanumeric sorting and collation, enumerable methods on Object. * ***/ function multiMatch(el, match, scope, params) { var result = true; if(el === match) { // Match strictly equal values up front. return true; } else if(isRegExp(match) && isString(el)) { // Match against a regexp return regexp(match).test(el); } else if(isFunction(match)) { // Match against a filtering function return match.apply(scope, params); } else if(isObject(match) && isObjectPrimitive(el)) { // Match against a hash or array. iterateOverObject(match, function(key, value) { if(!multiMatch(el[key], match[key], scope, [el[key], el])) { result = false; } }); return result; } else { return isEqual(el, match); } } function transformArgument(el, map, context, mapArgs) { if(isUndefined(map)) { return el; } else if(isFunction(map)) { return map.apply(context, mapArgs || []); } else if(isFunction(el[map])) { return el[map].call(el); } else { return el[map]; } } // Basic array internal methods function arrayEach(arr, fn, startIndex, loop) { var length, index, i; if(startIndex < 0) startIndex = arr.length + startIndex; i = isNaN(startIndex) ? 0 : startIndex; length = loop === true ? arr.length + i : arr.length; while(i < length) { index = i % arr.length; if(!(index in arr)) { return iterateOverSparseArray(arr, fn, i, loop); } else if(fn.call(arr, arr[index], index, arr) === false) { break; } i++; } } function iterateOverSparseArray(arr, fn, fromIndex, loop) { var indexes = [], i; for(i in arr) { if(isArrayIndex(arr, i) && i >= fromIndex) { indexes.push(parseInt(i)); } } indexes.sort().each(function(index) { return fn.call(arr, arr[index], index, arr); }); return arr; } function isArrayIndex(arr, i) { return i in arr && toUInt32(i) == i && i != 0xffffffff; } function toUInt32(i) { return i >>> 0; } function arrayFind(arr, f, startIndex, loop, returnIndex) { var result, index; arrayEach(arr, function(el, i, arr) { if(multiMatch(el, f, arr, [el, i, arr])) { result = el; index = i; return false; } }, startIndex, loop); return returnIndex ? index : result; } function arrayUnique(arr, map) { var result = [], o = {}, transformed; arrayEach(arr, function(el, i) { transformed = map ? transformArgument(el, map, arr, [el, i, arr]) : el; if(!checkForElementInHashAndSet(o, transformed)) { result.push(el); } }) return result; } function arrayIntersect(arr1, arr2, subtract) { var result = [], o = {}; arr2.each(function(el) { checkForElementInHashAndSet(o, el); }); arr1.each(function(el) { var stringified = stringify(el), isReference = !objectIsMatchedByValue(el); // Add the result to the array if: // 1. We're subtracting intersections or it doesn't already exist in the result and // 2. It exists in the compared array and we're adding, or it doesn't exist and we're removing. if(elementExistsInHash(o, stringified, el, isReference) != subtract) { discardElementFromHash(o, stringified, el, isReference); result.push(el); } }); return result; } function arrayFlatten(arr, level, current) { level = level || Infinity; current = current || 0; var result = []; arrayEach(arr, function(el) { if(isArray(el) && current < level) { result = result.concat(arrayFlatten(el, level, current + 1)); } else { result.push(el); } }); return result; } function flatArguments(args) { var result = []; multiArgs(args, function(arg) { result = result.concat(arg); }); return result; } function elementExistsInHash(hash, key, element, isReference) { var exists = key in hash; if(isReference) { if(!hash[key]) { hash[key] = []; } exists = hash[key].indexOf(element) !== -1; } return exists; } function checkForElementInHashAndSet(hash, element) { var stringified = stringify(element), isReference = !objectIsMatchedByValue(element), exists = elementExistsInHash(hash, stringified, element, isReference); if(isReference) { hash[stringified].push(element); } else { hash[stringified] = element; } return exists; } function discardElementFromHash(hash, key, element, isReference) { var arr, i = 0; if(isReference) { arr = hash[key]; while(i < arr.length) { if(arr[i] === element) { arr.splice(i, 1); } else { i += 1; } } } else { delete hash[key]; } } // Support methods function getMinOrMax(obj, map, which, all) { var edge, result = [], max = which === 'max', min = which === 'min', isArray = Array.isArray(obj); iterateOverObject(obj, function(key) { var el = obj[key], test = transformArgument(el, map, obj, isArray ? [el, parseInt(key), obj] : []); if(isUndefined(test)) { throw new TypeError('Cannot compare with undefined'); } if(test === edge) { result.push(el); } else if(isUndefined(edge) || (max && test > edge) || (min && test < edge)) { result = [el]; edge = test; } }); if(!isArray) result = arrayFlatten(result, 1); return all ? result : result[0]; } // Alphanumeric collation helpers function collateStrings(a, b) { var aValue, bValue, aChar, bChar, aEquiv, bEquiv, index = 0, tiebreaker = 0; a = getCollationReadyString(a); b = getCollationReadyString(b); do { aChar = getCollationCharacter(a, index); bChar = getCollationCharacter(b, index); aValue = getCollationValue(aChar); bValue = getCollationValue(bChar); if(aValue === -1 || bValue === -1) { aValue = a.charCodeAt(index) || null; bValue = b.charCodeAt(index) || null; } aEquiv = aChar !== a.charAt(index); bEquiv = bChar !== b.charAt(index); if(aEquiv !== bEquiv && tiebreaker === 0) { tiebreaker = aEquiv - bEquiv; } index += 1; } while(aValue != null && bValue != null && aValue === bValue); if(aValue === bValue) return tiebreaker; return aValue < bValue ? -1 : 1; } function getCollationReadyString(str) { if(array[AlphanumericSortIgnoreCase]) { str = str.toLowerCase(); } return str.replace(array[AlphanumericSortIgnore], ''); } function getCollationCharacter(str, index) { var chr = str.charAt(index), eq = array[AlphanumericSortEquivalents] || {}; return eq[chr] || chr; } function getCollationValue(chr) { var order = array[AlphanumericSortOrder]; if(!chr) { return null; } else { return order.indexOf(chr); } } var AlphanumericSortOrder = 'AlphanumericSortOrder'; var AlphanumericSortIgnore = 'AlphanumericSortIgnore'; var AlphanumericSortIgnoreCase = 'AlphanumericSortIgnoreCase'; var AlphanumericSortEquivalents = 'AlphanumericSortEquivalents'; function buildEnhancements() { var callbackCheck = function() { var a = arguments; return a.length > 0 && !isFunction(a[0]); }; extendSimilar(array, true, callbackCheck, 'map,every,all,some,any,none,filter', function(methods, name) { methods[name] = function(f) { return this[name](function(el, index) { if(name === 'map') { return transformArgument(el, f, this, [el, index, this]); } else { return multiMatch(el, f, this, [el, index, this]); } }); } }); } function buildAlphanumericSort() { var order = 'AÁÀÂÃĄBCĆČÇDĎÐEÉÈĚÊËĘFGĞHıIÍÌİÎÏJKLŁMNŃŇÑOÓÒÔPQRŘSŚŠŞTŤUÚÙŮÛÜVWXYÝZŹŻŽÞÆŒØÕÅÄÖ'; var equiv = 'AÁÀÂÃÄ,CÇ,EÉÈÊË,IÍÌİÎÏ,OÓÒÔÕÖ,Sß,UÚÙÛÜ'; array[AlphanumericSortOrder] = order.split('').map(function(str) { return str + str.toLowerCase(); }).join(''); var equivalents = {}; arrayEach(equiv.split(','), function(set) { var equivalent = set.charAt(0); arrayEach(set.slice(1).split(''), function(chr) { equivalents[chr] = equivalent; equivalents[chr.toLowerCase()] = equivalent.toLowerCase(); }); }); array[AlphanumericSortIgnoreCase] = true; array[AlphanumericSortEquivalents] = equivalents; } extend(array, false, false, { /*** * * @method Array.create(<obj1>, <obj2>, ...) * @returns Array * @short Alternate array constructor. * @extra This method will create a single array by calling %concat% on all arguments passed. In addition to ensuring that an unknown variable is in a single, flat array (the standard constructor will create nested arrays, this one will not), it is also a useful shorthand to convert a function's arguments object into a standard array. * @example * * Array.create('one', true, 3) -> ['one', true, 3] * Array.create(['one', true, 3]) -> ['one', true, 3] + Array.create(function(n) { * return arguments; * }('howdy', 'doody')); * ***/ 'create': function() { var result = [], tmp; multiArgs(arguments, function(a) { if(isObjectPrimitive(a)) { try { tmp = array.prototype.slice.call(a, 0); if(tmp.length > 0) { a = tmp; } } catch(e) {}; } result = result.concat(a); }); return result; } }); extend(array, true, false, { /*** * @method find(<f>, [index] = 0, [loop] = false) * @returns Mixed * @short Returns the first element that matches <f>. * @extra <f> will match a string, number, array, object, or alternately test against a function or regex. Starts at [index], and will continue once from index = 0 if [loop] is true. This method implements @array_matching. * @example * + [{a:1,b:2},{a:1,b:3},{a:1,b:4}].find(function(n) { * return n['a'] == 1; * }); -> {a:1,b:3} * ['cuba','japan','canada'].find(/^c/, 2) -> 'canada' * ***/ 'find': function(f, index, loop) { return arrayFind(this, f, index, loop); }, /*** * @method findAll(<f>, [index] = 0, [loop] = false) * @returns Array * @short Returns all elements that match <f>. * @extra <f> will match a string, number, array, object, or alternately test against a function or regex. Starts at [index], and will continue once from index = 0 if [loop] is true. This method implements @array_matching. * @example * + [{a:1,b:2},{a:1,b:3},{a:2,b:4}].findAll(function(n) { * return n['a'] == 1; * }); -> [{a:1,b:3},{a:1,b:4}] * ['cuba','japan','canada'].findAll(/^c/) -> 'cuba','canada' * ['cuba','japan','canada'].findAll(/^c/, 2) -> 'canada' * ***/ 'findAll': function(f, index, loop) { var result = []; arrayEach(this, function(el, i, arr) { if(multiMatch(el, f, arr, [el, i, arr])) { result.push(el); } }, index, loop); return result; }, /*** * @method findIndex(<f>, [startIndex] = 0, [loop] = false) * @returns Number * @short Returns the index of the first element that matches <f> or -1 if not found. * @extra This method has a few notable differences to native %indexOf%. Although <f> will similarly match a primitive such as a string or number, it will also match deep objects and arrays that are not equal by reference (%===%). Additionally, if a function is passed it will be run as a matching function (similar to the behavior of %Array#filter%) rather than attempting to find that function itself by reference in the array. Starts at [index], and will continue once from index = 0 if [loop] is true. This method implements @array_matching. * @example * + [1,2,3,4].findIndex(3); -> 2 + [1,2,3,4].findIndex(function(n) { * return n % 2 == 0; * }); -> 1 + ['one','two','three'].findIndex(/th/); -> 2 * ***/ 'findIndex': function(f, startIndex, loop) { var index = arrayFind(this, f, startIndex, loop, true); return isUndefined(index) ? -1 : index; }, /*** * @method count(<f>) * @returns Number * @short Counts all elements in the array that match <f>. * @extra <f> will match a string, number, array, object, or alternately test against a function or regex. This method implements @array_matching. * @example * * [1,2,3,1].count(1) -> 2 * ['a','b','c'].count(/b/) -> 1 + [{a:1},{b:2}].count(function(n) { * return n['a'] > 1; * }); -> 0 * ***/ 'count': function(f) { if(isUndefined(f)) return this.length; return this.findAll(f).length; }, /*** * @method removeAt(<start>, [end]) * @returns Array * @short Removes element at <start>. If [end] is specified, removes the range between <start> and [end]. This method will change the array! If you don't intend the array to be changed use %clone% first. * @example * * ['a','b','c'].removeAt(0) -> ['b','c'] * [1,2,3,4].removeAt(1, 3) -> [1] * ***/ 'removeAt': function(start, end) { if(isUndefined(start)) return this; if(isUndefined(end)) end = start; for(var i = 0; i <= (end - start); i++) { this.splice(start, 1); } return this; }, /*** * @method include(<el>, [index]) * @returns Array * @short Adds <el> to the array. * @extra This is a non-destructive alias for %add%. It will not change the original array. * @example * * [1,2,3,4].include(5) -> [1,2,3,4,5] * [1,2,3,4].include(8, 1) -> [1,8,2,3,4] * [1,2,3,4].include([5,6,7]) -> [1,2,3,4,5,6,7] * ***/ 'include': function(el, index) { return this.clone().add(el, index); }, /*** * @method exclude([f1], [f2], ...) * @returns Array * @short Removes any element in the array that matches [f1], [f2], etc. * @extra This is a non-destructive alias for %remove%. It will not change the original array. This method implements @array_matching. * @example * * [1,2,3].exclude(3) -> [1,2] * ['a','b','c'].exclude(/b/) -> ['a','c'] + [{a:1},{b:2}].exclude(function(n) { * return n['a'] == 1; * }); -> [{b:2}] * ***/ 'exclude': function() { return array.prototype.remove.apply(this.clone(), arguments); }, /*** * @method clone() * @returns Array * @short Makes a shallow clone of the array. * @example * * [1,2,3].clone() -> [1,2,3] * ***/ 'clone': function() { return simpleMerge([], this); }, /*** * @method unique([map] = null) * @returns Array * @short Removes all duplicate elements in the array. * @extra [map] may be a function mapping the value to be uniqued on or a string acting as a shortcut. This is most commonly used when you have a key that ensures the object's uniqueness, and don't need to check all fields. This method will also correctly operate on arrays of objects. * @example * * [1,2,2,3].unique() -> [1,2,3] * [{foo:'bar'},{foo:'bar'}].unique() -> [{foo:'bar'}] + [{foo:'bar'},{foo:'bar'}].unique(function(obj){ * return obj.foo; * }); -> [{foo:'bar'}] * [{foo:'bar'},{foo:'bar'}].unique('foo') -> [{foo:'bar'}] * ***/ 'unique': function(map) { return arrayUnique(this, map); }, /*** * @method flatten([limit] = Infinity) * @returns Array * @short Returns a flattened, one-dimensional copy of the array. * @extra You can optionally specify a [limit], which will only flatten that depth. * @example * * [[1], 2, [3]].flatten() -> [1,2,3] * [['a'],[],'b','c'].flatten() -> ['a','b','c'] * ***/ 'flatten': function(limit) { return arrayFlatten(this, limit); }, /*** * @method union([a1], [a2], ...) * @returns Array * @short Returns an array containing all elements in all arrays with duplicates removed. * @extra This method will also correctly operate on arrays of objects. * @example * * [1,3,5].union([5,7,9]) -> [1,3,5,7,9] * ['a','b'].union(['b','c']) -> ['a','b','c'] * ***/ 'union': function() { return arrayUnique(this.concat(flatArguments(arguments))); }, /*** * @method intersect([a1], [a2], ...) * @returns Array * @short Returns an array containing the elements all arrays have in common. * @extra This method will also correctly operate on arrays of objects. * @example * * [1,3,5].intersect([5,7,9]) -> [5] * ['a','b'].intersect('b','c') -> ['b'] * ***/ 'intersect': function() { return arrayIntersect(this, flatArguments(arguments), false); }, /*** * @method subtract([a1], [a2], ...) * @returns Array * @short Subtracts from the array all elements in [a1], [a2], etc. * @extra This method will also correctly operate on arrays of objects. * @example * * [1,3,5].subtract([5,7,9]) -> [1,3] * [1,3,5].subtract([3],[5]) -> [1] * ['a','b'].subtract('b','c') -> ['a'] * ***/ 'subtract': function(a) { return arrayIntersect(this, flatArguments(arguments), true); }, /*** * @method at(<index>, [loop] = true) * @returns Mixed * @short Gets the element(s) at a given index. * @extra When [loop] is true, overshooting the end of the array (or the beginning) will begin counting from the other end. As an alternate syntax, passing multiple indexes will get the elements at those indexes. * @example * * [1,2,3].at(0) -> 1 * [1,2,3].at(2) -> 3 * [1,2,3].at(4) -> 2 * [1,2,3].at(4, false) -> null * [1,2,3].at(-1) -> 3 * [1,2,3].at(0,1) -> [1,2] * ***/ 'at': function() { return entryAtIndex(this, arguments); }, /*** * @method first([num] = 1) * @returns Mixed * @short Returns the first element(s) in the array. * @extra When <num> is passed, returns the first <num> elements in the array. * @example * * [1,2,3].first() -> 1 * [1,2,3].first(2) -> [1,2] * ***/ 'first': function(num) { if(isUndefined(num)) return this[0]; if(num < 0) num = 0; return this.slice(0, num); }, /*** * @method last([num] = 1) * @returns Mixed * @short Returns the last element(s) in the array. * @extra When <num> is passed, returns the last <num> elements in the array. * @example * * [1,2,3].last() -> 3 * [1,2,3].last(2) -> [2,3] * ***/ 'last': function(num) { if(isUndefined(num)) return this[this.length - 1]; var start = this.length - num < 0 ? 0 : this.length - num; return this.slice(start); }, /*** * @method from(<index>) * @returns Array * @short Returns a slice of the array from <index>. * @example * * [1,2,3].from(1) -> [2,3] * [1,2,3].from(2) -> [3] * ***/ 'from': function(num) { return this.slice(num); }, /*** * @method to(<index>) * @returns Array * @short Returns a slice of the array up to <index>. * @example * * [1,2,3].to(1) -> [1] * [1,2,3].to(2) -> [1,2] * ***/ 'to': function(num) { if(isUndefined(num)) num = this.length; return this.slice(0, num); }, /*** * @method min([map], [all] = false) * @returns Mixed * @short Returns the element in the array with the lowest value. * @extra [map] may be a function mapping the value to be checked or a string acting as a shortcut. If [all] is true, will return all min values in an array. * @example * * [1,2,3].min() -> 1 * ['fee','fo','fum'].min('length') -> 'fo' * ['fee','fo','fum'].min('length', true) -> ['fo'] + ['fee','fo','fum'].min(function(n) { * return n.length; * }); -> ['fo'] + [{a:3,a:2}].min(function(n) { * return n['a']; * }); -> [{a:2}] * ***/ 'min': function(map, all) { return getMinOrMax(this, map, 'min', all); }, /*** * @method max([map], [all] = false) * @returns Mixed * @short Returns the element in the array with the greatest value. * @extra [map] may be a function mapping the value to be checked or a string acting as a shortcut. If [all] is true, will return all max values in an array. * @example * * [1,2,3].max() -> 3 * ['fee','fo','fum'].max('length') -> 'fee' * ['fee','fo','fum'].max('length', true) -> ['fee'] + [{a:3,a:2}].max(function(n) { * return n['a']; * }); -> {a:3} * ***/ 'max': function(map, all) { return getMinOrMax(this, map, 'max', all); }, /*** * @method least([map]) * @returns Array * @short Returns the elements in the array with the least commonly occuring value. * @extra [map] may be a function mapping the value to be checked or a string acting as a shortcut. * @example * * [3,2,2].least() -> [3] * ['fe','fo','fum'].least('length') -> ['fum'] + [{age:35,name:'ken'},{age:12,name:'bob'},{age:12,name:'ted'}].least(function(n) { * return n.age; * }); -> [{age:35,name:'ken'}] * ***/ 'least': function(map, all) { return getMinOrMax(this.groupBy.apply(this, [map]), 'length', 'min', all); }, /*** * @method most([map]) * @returns Array * @short Returns the elements in the array with the most commonly occuring value. * @extra [map] may be a function mapping the value to be checked or a string acting as a shortcut. * @example * * [3,2,2].most() -> [2] * ['fe','fo','fum'].most('length') -> ['fe','fo'] + [{age:35,name:'ken'},{age:12,name:'bob'},{age:12,name:'ted'}].most(function(n) { * return n.age; * }); -> [{age:12,name:'bob'},{age:12,name:'ted'}] * ***/ 'most': function(map, all) { return getMinOrMax(this.groupBy.apply(this, [map]), 'length', 'max', all); }, /*** * @method sum([map]) * @returns Number * @short Sums all values in the array. * @extra [map] may be a function mapping the value to be summed or a string acting as a shortcut. * @example * * [1,2,2].sum() -> 5 + [{age:35},{age:12},{age:12}].sum(function(n) { * return n.age; * }); -> 59 * [{age:35},{age:12},{age:12}].sum('age') -> 59 * ***/ 'sum': function(map) { var arr = map ? this.map(map) : this; return arr.length > 0 ? arr.reduce(function(a,b) { return a + b; }) : 0; }, /*** * @method average([map]) * @returns Number * @short Averages all values in the array. * @extra [map] may be a function mapping the