sugar
Version:
A Javascript library for working with native objects.
1,466 lines (1,344 loc) • 127 kB
JavaScript
// Google Closure Compiler will output a wrapping function here.
(function() {
// 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, Undefined;
// 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;
function extend(klass, instance, override, methods) {
var extendee = instance ? klass.prototype : klass;
initializeClass(klass, instance, methods);
iterateOverObject(methods, function(name, method) {
if(typeof override === 'function') {
defineProperty(extendee, name, wrapNative(extendee[name], method, override));
} else if(override === true || !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 };
});
}
function initializeClass(klass) {
if(klass.SugarMethods) return;
defineProperty(klass, 'SugarMethods', {});
defineProperty(klass, 'restore', function() {
var all = arguments.length === 0, methods = multiArgs(arguments);
iterateOverObject(klass['SugarMethods'], function(name, m) {
if(all || existsInArray(methods, name)) {
defineProperty(m.instance ? klass.prototype : klass, name, m.method);
}
});
});
defineProperty(klass, 'extend', function(methods, override, instance) {
if(klass === object && arguments.length === 0) {
mapObjectPrototypeMethods();
} else {
extend(klass, instance !== false, 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;
}
}
function iterateOverObject(obj, fn) {
var count = 0;
for(var key in obj) {
if(!obj.hasOwnProperty(key)) continue;
fn.call(obj, key, obj[key], count);
count++;
}
}
function equal(a, b, stack) {
var primitive = object.prototype.toString.call(a).match(/\[object (\w+)\]/)[1];
if(a === b) {
return a !== 0 || 1 / a === 1 / b;
} else if(isNull(a) || isUndefined(a) || isNull(b) || isUndefined(b)) {
return false;
} else if(primitive == 'RegExp') {
return a.ignoreCase == b.ignoreCase &&
a.multiline == b.multiline &&
a.source == b.source &&
a.global == b.global;
} else if(primitive == 'Array' || primitive == 'Object') {
// This method for checking for cyclic structures was egregiously stolen from
// the ingenious method by @kitcambridge from the Underscore script... ref:
// https://github.com/documentcloud/underscore/issues/240
var length = stack.length;
while (length--) {
if (stack[length] == a) return true;
}
stack.push(a);
for(var key in a) {
if(!a.hasOwnProperty(key)) continue;
if(!b.hasOwnProperty(key) || !equal(a[key], b[key], stack)) {
return false;
}
}
stack.pop();
return object.keys(a).length === object.keys(b).length &&
a.constructor === b.constructor &&
a.length === b.length;
} else {
return isClass(b, primitive) && a.valueOf() === b.valueOf();
}
}
function multiMatch(el, match, scope, params) {
var result = true;
if(el === match) {
// Match strictly equal values up front.
return true;
} else if(object.isRegExp(match)) {
// Match against a regexp
return regexp(match).test(el);
} else if(object.isFunction(match)) {
// Match against a filtering function
return match.apply(scope, [el].concat(params));
} else if(object.isObject(match) && object.isObject(el)) {
// Match against a hash or array.
iterateOverObject(match, function(key, value) {
if(!multiMatch(el[key], match[key], scope, params)) {
result = false;
}
});
return !object.isEmpty(match) && result;
} else {
return object.equal(el, match);
}
}
function transformArgument(el, map, context, mapArgs) {
if(isUndefined(map)) {
return el;
} else if(object.isFunction(map)) {
return map.apply(context, mapArgs || []);
} else if(object.isFunction(el[map])) {
return el[map].call(el);
} else {
return el[map];
}
}
function getArgs(args, index) {
return Array.prototype.slice.call(args, index);
}
function multiArgs(args, fn, flatten, index) {
args = getArgs(args);
if(flatten !== false) args = arrayFlatten(args);
arrayEach(args, fn || function(){}, index);
return args;
}
// Used for both arrays and strings
function entryAtIndex(arr, args, str) {
var result = [], length = arr.length, loop = args[args.length - 1] !== false, r;
multiArgs(args, function(index) {
if(object.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 module
*
* Much thanks to kangax for his informative aricle about how problems with instanceof and constructor
* http://perfectionkills.com/instanceof-considered-harmful-or-how-to-write-a-robust-isarray/
*
***/
function isClass(obj, str) {
return object.prototype.toString.call(obj) === '[object '+str+']';
}
function isObjectPrimitive(o) {
return typeof o == 'object';
}
function isNull(o) {
return o === null;
}
function isUndefined(o) {
return o === Undefined;
}
function isDefined(o) {
return o !== Undefined;
}
function mergeObject(target, source, deep, resolve) {
if(isObjectPrimitive(source)) {
iterateOverObject(source, function(key, val) {
var prop = target[key], conflict = isDefined(prop), isArray = object.isArray(val);
if(deep === true && (isArray || object.isObject(val))) {
if(!prop) prop = isArray ? [] : {};
mergeObject(prop, val, deep);
} else if(conflict && object.isFunction(resolve)) {
prop = resolve.call(source, key, target[key], source[key])
} else if(!conflict || (conflict && resolve !== false)) {
prop = source[key];
}
target[key] = prop;
});
}
return target;
}
function setParamsObject(obj, param, value, deep) {
var reg = /^(.+?)(\[.*\])$/, isArray, match, allKeys, key;
if(deep !== false && (match = param.match(reg))) {
key = match[1];
allKeys = match[2].replace(/^\[|\]$/g, '').split('][');
arrayEach(allKeys, function(k) {
isArray = !k || k.match(/^\d+$/);
if(!key && object.isArray(obj)) key = obj.length;
if(!obj[key]) {
obj[key] = isArray ? [] : {};
}
obj = obj[key];
key = k;
});
if(!key && isArray) key = obj.length.toString();
setParamsObject(obj, key, value);
} else if(value.match(/^[\d.]+$/)) {
obj[param] = parseFloat(value);
} else if(value === 'true') {
obj[param] = true;
} else if(value === 'false') {
obj[param] = false;
} else {
obj[param] = value;
}
}
function Hash(obj) {
var self = this;
iterateOverObject(obj, function(key, value) {
self[key] = value;
});
}
Hash.prototype.constructor = object;
/***
* @method is[Type](<obj>)
* @returns Boolean
* @short Returns true if <obj> is an object of that type.
* @extra %isObject% will return false on anything that is not an object literal, including instances of inherited classes. Note also that %isNaN% will ONLY return true if the object IS %NaN%. It does not mean the same as browser native %isNaN%, which returns true for anything that is "not a number". Type methods are available as instance methods on extended objects and when using Object.extend().
* @example
*
* Object.isArray([1,2,3]) -> true
* Object.isDate(3) -> false
* Object.isRegExp(/wasabi/) -> true
* Object.isObject({ broken:'wear' }) -> true
*
***
* @method isArray()
* @set isType
***
* @method isBoolean()
* @set isType
***
* @method isDate()
* @set isType
***
* @method isFunction()
* @set isType
***
* @method isNumber()
* @set isType
***
* @method isString()
* @set isType
***
* @method isRegExp()
* @set isType
***/
function buildTypeMethods() {
var methods = {};
arrayEach(['Array','Boolean','Date','Function','Number','String','RegExp'], function(type) {
methods['is' + type] = function(obj) {
return isClass(obj, type);
}
});
extend(Object, false, false, methods);
}
function buildInstanceMethods(set, target) {
var methods = {};
arrayEach(set, function(name) {
methods[name + (name === 'equal' ? 's' : '')] = function() {
return Object[name].apply(null, [this].concat(getArgs(arguments)));
}
});
extend(target, true, false, methods);
}
function buildObject() {
buildTypeMethods();
buildInstanceMethods(['keys','values','each','merge','isEmpty','clone','equal','watch','tap'], Hash);
}
function mapObjectPrototypeMethods() {
buildInstanceMethods(Object.keys(Object['SugarMethods']).remove('extended', 'fromQueryString'), Object);
}
extend(object, false, true, {
/***
* @method watch(<obj>, <prop>, <fn>)
* @returns Nothing
* @short Watches a property of <obj> and runs <fn> when it changes.
* @extra <fn> is passed three arguments: the property <prop>, the old value, and the new value. The return value of [fn] will be set as the new value. This method is useful for things such as validating or cleaning the value when it is set. Warning: this method WILL NOT work in browsers that don't support %Object.defineProperty%. This notably includes IE 8 and below, and Opera. This is the only method in Sugar that is not fully compatible with all browsers. %watch% is available as an instance method on extended objects and when using Object.extend().
* @example
*
* Object.watch({ foo: 'bar' }, 'foo', function(prop, oldVal, newVal) {
* // Will be run when the property 'foo' is set on the object.
* });
* Object.extended().watch({ foo: 'bar' }, 'foo', function(prop, oldVal, newVal) {
* // Will be run when the property 'foo' is set on the object.
* });
*
***/
'watch': function(obj, prop, fn) {
if(!definePropertySupport) return;
var value = obj[prop];
object.defineProperty(obj, prop, {
'get': function() {
return value;
},
'set': function(to) {
value = fn.call(obj, prop, value, to);
},
'enumerable': true,
'configurable': true
});
}
});
extend(object, false, false, {
/***
* @method Object.extended(<obj> = {})
* @returns Extended object
* @short Creates a new object, equivalent to %new Object()% or %{}%, but with extended methods.
* @extra See extended objects for more.
* @example
*
* Object.extended()
* Object.extended({ happy:true, pappy:false }).keys() -> ['happy','pappy']
* Object.extended({ happy:true, pappy:false }).values() -> [true, false]
*
***/
'extended': function(obj) {
return new Hash(obj);
},
/***
* @method isObject()
* @set isType
***/
'isObject': function(obj) {
if(isNull(obj) || isUndefined(obj)) {
return false;
} else {
return isClass(obj, 'Object') && obj.constructor === object;
}
},
/***
* @method isNaN()
* @set isType
***/
'isNaN': function(obj) {
// This is only true of NaN
return object.isNumber(obj) && obj.valueOf() !== obj.valueOf();
},
/***
* @method each(<obj>, [fn])
* @returns Object
* @short Iterates over each property in <obj> calling [fn] on each iteration.
* @extra %each% is available as an instance method on extended objects and when using Object.extend().
* @example
*
* Object.each({ broken:'wear' }, function(key, value) {
* // Iterates over each key/value pair.
* });
* Object.extended({ broken:'wear' }).each(function(key, value) {
* // Iterates over each key/value pair.
* });
*
***/
'each': function(obj, fn) {
if(fn) {
iterateOverObject(obj, function(k,v) {
fn.call(obj, k, v, obj);
});
}
return obj;
},
/***
* @method merge(<target>, <source>, [resolve] = true)
* @returns Merged object
* @short Merges all the properties of <source> into <target>.
* @extra Properties of <source> will win in the case of conflicts, unless [resolve] is %false%. [resolve] can also be a function that resolves the conflict. In this case it will be passed 3 arguments, %key%, %targetVal%, and %sourceVal%, with the context set to <source>. This will allow you to solve conflict any way you want, ie. adding two numbers together, etc. %merge% is available as an instance method on extended objects and when using Object.extend().
* @example
*
* Object.merge({a:1},{b:2}) -> { a:1, b:2 }
* Object.merge({a:1},{a:2}, false) -> { a:1 }
+ Object.merge({a:1},{a:2}, function(key, a, b) {
* return a + b;
* }); -> { a:3 }
* Object.extended({a:1}).merge({b:2}) -> { a:1, b:2 }
*
***/
'merge': function(target, merge, resolve) {
return mergeObject(target, merge, true, resolve);
},
/***
* @method isEmpty(<obj>)
* @returns Boolean
* @short Returns true if <obj> is empty.
* @extra %isEmpty% is available as an instance method on extended objects and when using Object.extend().
* @example
*
* Object.isEmpty({}) -> true
* Object.isEmpty({foo:'bar'}) -> false
* Object.extended({foo:'bar'}).isEmpty() -> false
*
***/
'isEmpty': function(obj) {
if(!isObjectPrimitive(obj) || isNull(obj)) return !(obj && obj.length > 0);
return object.keys(obj).length == 0;
},
/***
* @method equal(<a>, <b>)
* @returns Boolean
* @short Returns true if <a> and <b> are equal.
* @extra %empty% is available as an instance method as "equals" (note the "s") on extended objects and when using Object.extend().
* @example
*
* Object.equal({a:2}, {a:2}) -> true
* Object.equal({a:2}, {a:3}) -> false
* Object.extended({a:2}).equals({a:3}) -> false
*
***/
'equal': function(a, b) {
return equal(a, b, []);
},
/***
* @method values(<obj>, [fn])
* @returns Array
* @short Returns an array containing the values in <obj>. Optionally calls [fn] for each value.
* @extra Returned values are in no particular order. %values% is available as an instance method on extended objects and when using Object.extend().
* @example
*
* Object.values({ broken: 'wear' }) -> ['wear']
* Object.values({ broken: 'wear' }, function(value) {
* // Called once for each value.
* });
* Object.extended({ broken: 'wear' }).values() -> ['wear']
*
***/
'values': function(obj, fn) {
var values = [];
iterateOverObject(obj, function(k,v) {
values.push(v);
if(fn) fn.call(obj,v);
});
return values;
},
/***
* @method clone(<obj> = {}, [deep] = false)
* @returns Cloned object
* @short Creates a clone (copy) of <obj>.
* @extra Default is a shallow clone, unless [deep] is true. %clone% is available as an instance method on extended objects and when using Object.extend().
* @example
*
* Object.clone({foo:'bar'}) -> { foo: 'bar' }
* Object.clone() -> {}
* Object.extended({foo:'bar'}).clone() -> { foo: 'bar' }
*
***/
'clone': function(obj, deep) {
if(!isObjectPrimitive(obj) || isNull(obj)) return obj;
var target = Object.isFunction(obj.keys) ? Object.extended() : {};
return mergeObject(target, obj, deep);
},
/***
* @method Object.fromQueryString(<str>, [deep] = true)
* @returns Object
* @short Converts the query string of a URL into an object.
* @extra If [deep] is %false%, conversion will only accept shallow params (ie. no object or arrays with %[]% syntax) as these are not universally supported.
* @example
*
* Object.fromQueryString('foo=bar&broken=wear') -> { foo: 'bar', broken: 'wear' }
* Object.fromQueryString('foo[]=1&foo[]=2') -> { foo: [1,2] }
*
***/
'fromQueryString': function(str, deep) {
var result = object.extended(), split;
str = str && str.toString ? str.toString() : '';
str.replace(/^.*?\?/, '').unescapeURL().split('&').each(function(p) {
var split = p.split('=');
if(split.length !== 2) return;
setParamsObject(result, split[0], split[1], deep);
});
return result;
},
/***
* @method tap(<obj>, <fn>)
* @returns Object
* @short Runs <fn> and returns <obj>.
* @extra A string can also be used as a shortcut to a method. This method is used to run an intermediary function in the middle of method chaining. As a standalone method on the Object class it doesn't have too much use. The power of %tap% comes when using extended objects or modifying the Object prototype with Object.extend().
* @example
*
* Object.extend();
* [2,4,6].map(Math.exp).tap(function(){ arr.pop(); }).map(Math.round); -> [7,55]
* [2,4,6].map(Math.exp).tap('pop').map(Math.round); -> [7,55]
*
***/
'tap': function(obj, fn) {
transformArgument(obj, fn, obj, [obj]);
return obj;
}
});
extend(object, false, function() { return arguments.length > 1; }, {
/***
* @method keys(<obj>, [fn])
* @returns Array
* @short Returns an array containing the keys in <obj>. Optionally calls [fn] for each key.
* @extra This method is provided for browsers that don't support it natively, and additionally is enhanced to accept the callback [fn]. Returned keys are in no particular order. %keys% is available as an instance method on extended objects and when using Object.extend().
* @example
*
* Object.keys({ broken: 'wear' }) -> ['broken']
* Object.keys({ broken: 'wear' }, function(key, value) {
* // Called once for each key.
* });
* Object.extended({ broken: 'wear' }).keys() -> ['broken']
*
***/
'keys': function(obj, fn) {
if(isNull(obj) || (!isObjectPrimitive(obj) && !object.isRegExp(obj) && !object.isFunction(obj))) {
throw new TypeError('Object required');
}
var keys = [];
iterateOverObject(obj, function(key, value) {
keys.push(key);
if(fn) fn.call(obj, key, value);
});
return keys;
}
});
/***
* Array module
*
***/
// Basic array internal methods
function arrayEach(arr, fn, startIndex, loop, sparse) {
var length, index, i;
checkCallback(fn);
if(startIndex < 0) startIndex = arr.length + startIndex;
i = toIntegerWithDefault(startIndex, 0);
length = loop === true ? arr.length + i : arr.length;
while(i < length) {
index = i % arr.length;
if(!(index in arr) && sparse === true) {
return iterateOverSparseArray(arr, fn, i, loop);
} else if(fn.call(arr, arr[index], index, arr) === false) {
break;
}
i++;
}
}
function arrayFind(arr, f, startIndex, loop, returnIndex) {
var result, index;
arrayEach(arr, function(el, i, arr) {
if(multiMatch(el, f, arr, [i, arr])) {
result = el;
index = i;
return false;
}
}, startIndex, loop);
return returnIndex ? index : result;
}
function existsInArray(arr, obj) {
return arr.any(function(el) {
return object.equal(obj, el);
});
}
function arrayUnique(arr, map) {
var prop, test = function(el) { return transformArgument(el, map, arr, [el]) === prop; };
return arr.reduce(function(result, next, index) {
prop = transformArgument(next, map, arr, [next, index, arr]);
if(result.none(map ? test : next)) result.push(next);
return result;
}, []);
}
function arrayFlatten(arr, level, current) {
level = level || Infinity;
current = current || 0;
var result = [];
arrayEach(arr, function(el) {
if(object.isArray(el) && current < level) {
result = result.concat(arrayFlatten(el, level, current + 1));
} else {
result.push(el);
}
});
return result;
}
function arrayIntersect(arr1, arr2, subtract) {
var result = [];
arr1.each(function(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((subtract || !existsInArray(result, el)) && subtract != existsInArray(arr2, el)) {
result.push(el);
}
});
return result;
}
// 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.call(Undefined, result, arr[index], index, arr);
}
count++;
}
return result;
}
function toIntegerWithDefault(i, d) {
if(isNaN(i)) {
return d;
} else {
return parseInt(i >> 0);
}
}
function isArrayIndex(arr, i) {
return i in arr && toUInt32(i) == i && i != 0xffffffff;
}
function toUInt32(i) {
return i >>> 0;
}
function checkCallback(fn) {
if(!fn || !fn.call) {
throw new TypeError('Callback is not callable');
}
}
function checkFirstArgumentExists(args) {
if(args.length === 0) {
throw new TypeError('First argument must be defined');
}
}
// Support methods
function iterateOverSparseArray(arr, fn, fromIndex, loop) {
var indexes = [], i;
for(i in arr) {
if(isArrayIndex(arr, i) && i >= fromIndex) {
indexes.push(i.toNumber());
}
}
indexes.sort().each(function(index) {
return fn.call(arr, arr[index], index, arr);
});
return arr;
}
function getMinOrMax(obj, map, which, isArray) {
var max = which === 'max', min = which === 'min';
var edge = max ? -Infinity : Infinity;
var result = [];
iterateOverObject(obj, function(key) {
var entry = obj[key];
var test = transformArgument(entry, map, obj, isArray? [entry, key.toNumber(), obj] : []);
if(isUndefined(test)) {
return;
} else if(test === edge) {
result.push(entry);
} else if((max && test > edge) || (min && test < edge)) {
result = [entry];
edge = test;
}
});
return result;
}
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(obj) {
var result = [];
multiArgs(arguments, function(a) {
if(a && a.callee) a = getArgs(a);
result = result.concat(a);
});
return result;
},
/***
*
* @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 isClass(obj, 'Array');
}
});
extend(array, true, function() { var a = arguments; return a.length > 0 && !object.isFunction(a[0]); }, {
/***
* @method every(<f>, [scope])
* @returns Boolean
* @short Returns true if all elements in the array match <f>.
* @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 strings, numbers, deep objects, and arrays for <f>. %all% is provided an alias.
* @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(f, scope) {
var length = this.length, index = 0;
checkFirstArgumentExists(arguments);
while(index < length) {
if(index in this && !multiMatch(this[index], f, scope, [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. In addition to providing this method for browsers that don't support it natively, this enhanced method also directly accepts strings, numbers, deep objects, and arrays for <f>. %any% and %has% are provided as aliases.
* @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(f, scope) {
var length = this.length, index = 0;
checkFirstArgumentExists(arguments);
while(index < length) {
if(index in this && multiMatch(this[index], f, scope, [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. %collect% is provided as an alias.
* @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(map, scope) {
var length = this.length, index = 0, el, result = new Array(length);
checkFirstArgumentExists(arguments);
while(index < length) {
if(index in this) {
el = this[index];
result[index] = transformArgument(el, map, scope, [el, 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 enhanced method also directly accepts strings, numbers, deep objects, and arrays for <f>.
* @example
*
+ [1,2,3].filter(function(n) {
* return n > 1;
* });
* [1,2,2,4].filter(2) -> 2
*
***/
'filter': function(f, scope) {
var length = this.length, index = 0, result = [];
checkFirstArgumentExists(arguments);
while(index < length) {
if(index in this && multiMatch(this[index], f, scope, [index, this])) {
result.push(this[index]);
}
index++;
}
return result;
}
});
extend(array, true, false, {
/***
* @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 %find% method instead.
* @example
*
* [1,2,3].indexOf(3) -> 1
* [1,2,3].indexOf(7) -> -1
*
***/
'indexOf': function(search, fromIndex) {
if(object.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(object.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 By default this method calls [fn] n - 1 times, where n is the length of the array. On the first call it is passed the first and second elements in the array. The result of that callback will then be passed into the next iteration until it reaches the end, where the accumulated value will be returned as the final result. If [init] is passed, it will call [fn] one extra time in the beginning passing in [init] along with the first element. 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 Reduces the array to a single result by stepping through it from the right.
* @extra By default this method calls [fn] n - 1 times, where n is the length of the array. On the first call it is passed the last and second to last elements in the array. The result of that callback will then be passed into the next iteration until it reaches the beginning, where the accumulated value will be returned as the final result. If [init] is passed, it will call [fn] one extra time in the beginning passing in [init] along with the last element. 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);
},
/***
* @method each(<fn>, [index] = 0, [loop] = false)
* @returns Array
* @short Runs <fn> against elements in the array. Enhanced version of %Array#forEach%.
* @extra Parameters passed to <fn> are identical to %forEach%, ie. the first parameter is the current element, second parameter is the current index, and third parameter is the array itself. If <fn> returns %false% at any time it will break out of the loop. Once %each% finishes, it will return the array. If [index] is passed, <fn> will begin at that index and work its way to the end. If [loop] is true, it will then start over from the beginning of the array and continue until it reaches [index] - 1.
* @example
*
* [1,2,3,4].each(function(n) {
* // Called 4 times: 1, 2, 3, 4
* });
* [1,2,3,4].each(function(n) {
* // Called 4 times: 3, 4, 1, 2
* }, 2, true);
*
***/
'each': function(fn, index, loop) {
arrayEach(this, fn, index, loop, true);
return this;
},
/***
* @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.
* @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.
* @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, [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. Finally, a regexp will be matched against elements in the array, presumed to be strings. Starts at [index], and will continue once from index = 0 if [loop] is true.
* @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 isDefined(index) ? index : -1;
},
/***
* @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.
* @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 none(<f>)
* @returns Boolean
* @short Returns true if none of the elements in the array match <f>.
* @extra <f> will match a string, number, array, object, or alternately test against a function or regex.
* @example
*
* [1,2,3].none(5) -> true
* ['a','b','c'].none(/b/) -> false
+ [{a:1},{b:2}].none(function(n) {
* return n['a'] > 1;
* }); -> true
*
***/
'none': function() {
return !this.any.apply(this, arguments);
},
/***
* @method remove([f1], [f2], ...)
* @returns Array
* @short Removes any element in the array that matches [f1], [f2], etc.
* @extra Will match a string, number, array, object, or alternately test against a function or regex. This method will change the array! Use %exclude% for a non-destructive alias.
* @example
*
* [1,2,3].remove(3) -> [1,2]
* ['a','b','c'].remove(/b/) -> ['a','c']
+ [{a:1},{b:2}].remove(function(n) {
* return n['a'] == 1;
* }); -> [{b:2}]
*
***/
'remove': function() {
var i, arr = this;
multiArgs(arguments, function(f) {
i = 0;
while(i < arr.length) {
if(multiMatch(arr[i], f, arr, [i, arr])) {
arr.splice(i, 1);
} else {
i++;
}
}
}, false);
return arr;
},
/***
* @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 add(<el>, [index])
* @returns Array
* @short Adds <el> to the array.
* @extra If [index] is specified, it will add at [index], otherwise adds to the end of the array. %add% behaves like %concat% in that if <el> is an array it will be joined, not inserted. This method will change the array! Use %include% for a non-destructive alias. Also, %insert% is provided as an alias that reads better when using an index.
* @example
*
* [1,2,3,4].add(5) -> [1,2,3,4,5]
* [1,2,3,4].add([5,6,7]) -> [1,2,3,4,5,6,7]
* [1,2,3,4].insert(8, 1) -> [1,8,2,3,4]
*
***/
'add': function(el, index) {
if(!object.isNumber(number(index)) || isNaN(index) || index == -1) index = this.length;
else if(index < -1) index += 1;
array.prototype.splice.apply(this, [index, 0].concat(el));
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.
* @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 Clones the array.
* @example
*
* [1,2,3].clone() -> [1,2,3]
*
***/
'clone': function() {
return mergeObject([], 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.
* @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 union([a1], [a2], ...)
* @returns Array
* @short Returns an array containing all elements in all arrays with duplicates removed.
* @example
*
* [1,3,5].union([5,7,9]) -> [1,3,5,7,9]
* ['a','b'].union(['b','c']) -> ['a','b','c']
*
***/
'union': function() {
var arr = this;
multiArgs(arguments, function(a) {
arr = arr.concat(a);
});
return arrayUnique(arr);
},
/***
* @method intersect([a1], [a2], ...)
* @returns Array
* @short Returns an array containing the elements all arrays have in common.
* @example
*
* [1,3,5].intersect([5,7,9]) -> [5]
* ['a','b'].intersect('b','c') -> ['b']
*
***/
'intersect': function() {
return arrayIntersect(this, multiArgs(arguments), false);
},
/***
* @method subtract([a1], [a2], ...)
* @returns Array
* @short Subtracts from the array all elements in [a1], [a2], etc.
* @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, multiArgs(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])
* @returns Array
* @short Returns the elements 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.
* @example
*
* [1,2,3].min() -> [1]
* ['fee','fo','fum'].min('length') -> ['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) {
return arrayUnique(getMinOrMax(this, map, 'min', true));
},
/***
* @method max(<map>)
* @returns Array
* @short Returns the elements 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.
* @example
*
* [1,2,3].max() -> [3]
* ['fee','fo','fum'].max('length') -> ['fee','fum']
+ [{a:3,a:2}].max(function(n) {
* return n['a'];
* }); -> [{a:3}]
*
***/
'max': function(map) {
return arrayUnique(getMinOrMax(this, map, 'max', true));
},
/***
* @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() {
var result = arrayFlatten(getMinOrMax(this.groupBy.apply(this, arguments), 'length', 'min'));
return result.length === this.length ? [] : arrayUnique(result);
},
/***
* @method most(<map>)
* @returns Array
* @short Returns the elements in the array with the most commonly occuring value.
* @extra <map> m