master
1
2 'use strict';
3
4 /***
5 * @package Array
6 * @dependency core
7 * @description Array manipulation and traversal, "fuzzy matching" against elements, alphanumeric sorting and collation, enumerable methods on Object.
8 *
9 ***/
10
11
12 function regexMatcher(reg) {
13 reg = regexp(reg);
14 return function (el) {
15 return reg.test(el);
16 }
17 }
18
19 function dateMatcher(d) {
20 var ms = d.getTime();
21 return function (el) {
22 return !!(el && el.getTime) && el.getTime() === ms;
23 }
24 }
25
26 function functionMatcher(fn) {
27 return function (el, i, arr) {
28 // Return true up front if match by reference
29 return el === fn || fn.call(this, el, i, arr);
30 }
31 }
32
33 function invertedArgsFunctionMatcher(fn) {
34 return function (value, key, obj) {
35 // Return true up front if match by reference
36 return value === fn || fn.call(obj, key, value, obj);
37 }
38 }
39
40 function fuzzyMatcher(obj, isObject) {
41 var matchers = {};
42 return function (el, i, arr) {
43 var key;
44 if(!isObjectType(el)) {
45 return false;
46 }
47 for(key in obj) {
48 matchers[key] = matchers[key] || getMatcher(obj[key], isObject);
49 if(matchers[key].call(arr, el[key], i, arr) === false) {
50 return false;
51 }
52 }
53 return true;
54 }
55 }
56
57 function defaultMatcher(f) {
58 return function (el) {
59 return el === f || isEqual(el, f);
60 }
61 }
62
63 function getMatcher(f, isObject) {
64 if(isPrimitiveType(f)) {
65 // Do nothing and fall through to the
66 // default matcher below.
67 } else if(isRegExp(f)) {
68 // Match against a regexp
69 return regexMatcher(f);
70 } else if(isDate(f)) {
71 // Match against a date. isEqual below should also
72 // catch this but matching directly up front for speed.
73 return dateMatcher(f);
74 } else if(isFunction(f)) {
75 // Match against a filtering function
76 if(isObject) {
77 return invertedArgsFunctionMatcher(f);
78 } else {
79 return functionMatcher(f);
80 }
81 } else if(isPlainObject(f)) {
82 // Match against a fuzzy hash or array.
83 return fuzzyMatcher(f, isObject);
84 }
85 // Default is standard isEqual
86 return defaultMatcher(f);
87 }
88
89 function transformArgument(el, map, context, mapArgs) {
90 if(!map) {
91 return el;
92 } else if(map.apply) {
93 return map.apply(context, mapArgs || []);
94 } else if(isFunction(el[map])) {
95 return el[map].call(el);
96 } else {
97 return el[map];
98 }
99 }
100
101 // Basic array internal methods
102
103 function arrayEach(arr, fn, startIndex, loop) {
104 var index, i, length = +arr.length;
105 if(startIndex < 0) startIndex = arr.length + startIndex;
106 i = isNaN(startIndex) ? 0 : startIndex;
107 if(loop === true) {
108 length += i;
109 }
110 while(i < length) {
111 index = i % arr.length;
112 if(!(index in arr)) {
113 return iterateOverSparseArray(arr, fn, i, loop);
114 } else if(fn.call(arr, arr[index], index, arr) === false) {
115 break;
116 }
117 i++;
118 }
119 }
120
121 function iterateOverSparseArray(arr, fn, fromIndex, loop) {
122 var indexes = [], i;
123 for(i in arr) {
124 if(isArrayIndex(arr, i) && i >= fromIndex) {
125 indexes.push(parseInt(i));
126 }
127 }
128 indexes.sort().each(function(index) {
129 return fn.call(arr, arr[index], index, arr);
130 });
131 return arr;
132 }
133
134 function isArrayIndex(arr, i) {
135 return i in arr && toUInt32(i) == i && i != 0xffffffff;
136 }
137
138 function toUInt32(i) {
139 return i >>> 0;
140 }
141
142 function arrayFind(arr, f, startIndex, loop, returnIndex, context) {
143 var result, index, matcher;
144 if(arr.length > 0) {
145 matcher = getMatcher(f);
146 arrayEach(arr, function(el, i) {
147 if(matcher.call(context, el, i, arr)) {
148 result = el;
149 index = i;
150 return false;
151 }
152 }, startIndex, loop);
153 }
154 return returnIndex ? index : result;
155 }
156
157 function arrayUnique(arr, map) {
158 var result = [], o = {}, transformed;
159 arrayEach(arr, function(el, i) {
160 transformed = map ? transformArgument(el, map, arr, [el, i, arr]) : el;
161 if(!checkForElementInHashAndSet(o, transformed)) {
162 result.push(el);
163 }
164 })
165 return result;
166 }
167
168 function arrayIntersect(arr1, arr2, subtract) {
169 var result = [], o = {};
170 arr2.each(function(el) {
171 checkForElementInHashAndSet(o, el);
172 });
173 arr1.each(function(el) {
174 var stringified = stringify(el),
175 isReference = !objectIsMatchedByValue(el);
176 // Add the result to the array if:
177 // 1. We're subtracting intersections or it doesn't already exist in the result and
178 // 2. It exists in the compared array and we're adding, or it doesn't exist and we're removing.
179 if(elementExistsInHash(o, stringified, el, isReference) !== subtract) {
180 discardElementFromHash(o, stringified, el, isReference);
181 result.push(el);
182 }
183 });
184 return result;
185 }
186
187 function arrayFlatten(arr, level, current) {
188 level = level || Infinity;
189 current = current || 0;
190 var result = [];
191 arrayEach(arr, function(el) {
192 if(isArray(el) && current < level) {
193 result = result.concat(arrayFlatten(el, level, current + 1));
194 } else {
195 result.push(el);
196 }
197 });
198 return result;
199 }
200
201 function isArrayLike(obj) {
202 return hasProperty(obj, 'length') && !isString(obj) && !isPlainObject(obj);
203 }
204
205 function isArgumentsObject(obj) {
206 // .callee exists on Arguments objects in < IE8
207 return hasProperty(obj, 'length') && (className(obj) === '[object Arguments]' || !!obj.callee);
208 }
209
210 function flatArguments(args) {
211 var result = [];
212 multiArgs(args, function(arg) {
213 result = result.concat(arg);
214 });
215 return result;
216 }
217
218 function elementExistsInHash(hash, key, element, isReference) {
219 var exists = key in hash;
220 if(isReference) {
221 if(!hash[key]) {
222 hash[key] = [];
223 }
224 exists = hash[key].indexOf(element) !== -1;
225 }
226 return exists;
227 }
228
229 function checkForElementInHashAndSet(hash, element) {
230 var stringified = stringify(element),
231 isReference = !objectIsMatchedByValue(element),
232 exists = elementExistsInHash(hash, stringified, element, isReference);
233 if(isReference) {
234 hash[stringified].push(element);
235 } else {
236 hash[stringified] = element;
237 }
238 return exists;
239 }
240
241 function discardElementFromHash(hash, key, element, isReference) {
242 var arr, i = 0;
243 if(isReference) {
244 arr = hash[key];
245 while(i < arr.length) {
246 if(arr[i] === element) {
247 arr.splice(i, 1);
248 } else {
249 i += 1;
250 }
251 }
252 } else {
253 delete hash[key];
254 }
255 }
256
257 // Support methods
258
259 function getMinOrMax(obj, map, which, all) {
260 var el,
261 key,
262 edge,
263 test,
264 result = [],
265 max = which === 'max',
266 min = which === 'min',
267 isArray = array.isArray(obj);
268 for(key in obj) {
269 if(!obj.hasOwnProperty(key)) continue;
270 el = obj[key];
271 test = transformArgument(el, map, obj, isArray ? [el, parseInt(key), obj] : []);
272 if(isUndefined(test)) {
273 throw new TypeError('Cannot compare with undefined');
274 }
275 if(test === edge) {
276 result.push(el);
277 } else if(isUndefined(edge) || (max && test > edge) || (min && test < edge)) {
278 result = [el];
279 edge = test;
280 }
281 }
282 if(!isArray) result = arrayFlatten(result, 1);
283 return all ? result : result[0];
284 }
285
286
287 // Alphanumeric collation helpers
288
289 function collateStrings(a, b) {
290 var aValue, bValue, aChar, bChar, aEquiv, bEquiv, index = 0, tiebreaker = 0;
291
292 var sortIgnore = array[AlphanumericSortIgnore];
293 var sortIgnoreCase = array[AlphanumericSortIgnoreCase];
294 var sortEquivalents = array[AlphanumericSortEquivalents];
295 var sortOrder = array[AlphanumericSortOrder];
296 var naturalSort = array[AlphanumericSortNatural];
297
298 a = getCollationReadyString(a, sortIgnore, sortIgnoreCase);
299 b = getCollationReadyString(b, sortIgnore, sortIgnoreCase);
300
301 do {
302
303 aChar = getCollationCharacter(a, index, sortEquivalents);
304 bChar = getCollationCharacter(b, index, sortEquivalents);
305 aValue = getSortOrderIndex(aChar, sortOrder);
306 bValue = getSortOrderIndex(bChar, sortOrder);
307
308 if(aValue === -1 || bValue === -1) {
309 aValue = a.charCodeAt(index) || null;
310 bValue = b.charCodeAt(index) || null;
311 if(naturalSort && codeIsNumeral(aValue) && codeIsNumeral(bValue)) {
312 aValue = stringToNumber(a.slice(index));
313 bValue = stringToNumber(b.slice(index));
314 }
315 } else {
316 aEquiv = aChar !== a.charAt(index);
317 bEquiv = bChar !== b.charAt(index);
318 if(aEquiv !== bEquiv && tiebreaker === 0) {
319 tiebreaker = aEquiv - bEquiv;
320 }
321 }
322 index += 1;
323 } while(aValue != null && bValue != null && aValue === bValue);
324 if(aValue === bValue) return tiebreaker;
325 return aValue - bValue;
326 }
327
328 function getCollationReadyString(str, sortIgnore, sortIgnoreCase) {
329 if(!isString(str)) str = string(str);
330 if(sortIgnoreCase) {
331 str = str.toLowerCase();
332 }
333 if(sortIgnore) {
334 str = str.replace(sortIgnore, '');
335 }
336 return str;
337 }
338
339 function getCollationCharacter(str, index, sortEquivalents) {
340 var chr = str.charAt(index);
341 return sortEquivalents[chr] || chr;
342 }
343
344 function getSortOrderIndex(chr, sortOrder) {
345 if(!chr) {
346 return null;
347 } else {
348 return sortOrder.indexOf(chr);
349 }
350 }
351
352 var AlphanumericSort = 'AlphanumericSort';
353 var AlphanumericSortOrder = 'AlphanumericSortOrder';
354 var AlphanumericSortIgnore = 'AlphanumericSortIgnore';
355 var AlphanumericSortIgnoreCase = 'AlphanumericSortIgnoreCase';
356 var AlphanumericSortEquivalents = 'AlphanumericSortEquivalents';
357 var AlphanumericSortNatural = 'AlphanumericSortNatural';
358
359
360
361 function buildEnhancements() {
362 var nativeMap = array.prototype.map;
363 var callbackCheck = function() {
364 var args = arguments;
365 return args.length > 0 && !isFunction(args[0]);
366 };
367 extendSimilar(array, true, callbackCheck, 'every,all,some,filter,any,none,find,findIndex', function(methods, name) {
368 var nativeFn = array.prototype[name]
369 methods[name] = function(f) {
370 var matcher = getMatcher(f);
371 return nativeFn.call(this, function(el, index) {
372 return matcher(el, index, this);
373 });
374 }
375 });
376 extend(array, true, callbackCheck, {
377 'map': function(f) {
378 return nativeMap.call(this, function(el, index) {
379 return transformArgument(el, f, this, [el, index, this]);
380 });
381 }
382 });
383 }
384
385 function buildAlphanumericSort() {
386 var order = 'AÁÀÂÃĄBCĆČÇDĎÐEÉÈĚÊËĘFGĞHıIÍÌİÎÏJKLŁMNŃŇÑOÓÒÔPQRŘSŚŠŞTŤUÚÙŮÛÜVWXYÝZŹŻŽÞÆŒØÕÅÄÖ';
387 var equiv = 'AÁÀÂÃÄ,CÇ,EÉÈÊË,IÍÌİÎÏ,OÓÒÔÕÖ,Sß,UÚÙÛÜ';
388 array[AlphanumericSortOrder] = order.split('').map(function(str) {
389 return str + str.toLowerCase();
390 }).join('');
391 var equivalents = {};
392 arrayEach(equiv.split(','), function(set) {
393 var equivalent = set.charAt(0);
394 arrayEach(set.slice(1).split(''), function(chr) {
395 equivalents[chr] = equivalent;
396 equivalents[chr.toLowerCase()] = equivalent.toLowerCase();
397 });
398 });
399 array[AlphanumericSortNatural] = true;
400 array[AlphanumericSortIgnoreCase] = true;
401 array[AlphanumericSortEquivalents] = equivalents;
402 }
403
404 extend(array, false, true, {
405
406 /***
407 *
408 * @method Array.create(<obj1>, <obj2>, ...)
409 * @returns Array
410 * @short Alternate array constructor.
411 * @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.
412 * @example
413 *
414 * Array.create('one', true, 3) -> ['one', true, 3]
415 * Array.create(['one', true, 3]) -> ['one', true, 3]
416 + Array.create(function(n) {
417 * return arguments;
418 * }('howdy', 'doody'));
419 *
420 ***/
421 'create': function() {
422 var result = [];
423 multiArgs(arguments, function(a) {
424 if(isArgumentsObject(a) || isArrayLike(a)) {
425 a = array.prototype.slice.call(a, 0);
426 }
427 result = result.concat(a);
428 });
429 return result;
430 }
431
432 });
433
434 extend(array, true, false, {
435
436 /***
437 * @method find(<f>, [context] = undefined)
438 * @returns Mixed
439 * @short Returns the first element that matches <f>.
440 * @extra [context] is the %this% object if passed. When <f> is a function, will use native implementation if it exists. <f> will also match a string, number, array, object, or alternately test against a function or regex. This method implements @array_matching.
441 * @example
442 *
443 + [{a:1,b:2},{a:1,b:3},{a:1,b:4}].find(function(n) {
444 * return n['a'] == 1;
445 * }); -> {a:1,b:3}
446 * ['cuba','japan','canada'].find(/^c/) -> 'cuba'
447 *
448 ***/
449 'find': function(f, context) {
450 checkCallback(f);
451 return arrayFind(this, f, 0, false, false, context);
452 },
453
454 /***
455 * @method findIndex(<f>, [context] = undefined)
456 * @returns Number
457 * @short Returns the index of the first element that matches <f> or -1 if not found.
458 * @extra [context] is the %this% object if passed. When <f> is a function, will use native implementation if it exists. <f> will also match a string, number, array, object, or alternately test against a function or regex. This method implements @array_matching.
459 *
460 * @example
461 *
462 + [1,2,3,4].findIndex(function(n) {
463 * return n % 2 == 0;
464 * }); -> 1
465 + [1,2,3,4].findIndex(3); -> 2
466 + ['one','two','three'].findIndex(/t/); -> 1
467 *
468 ***/
469 'findIndex': function(f, context) {
470 var index;
471 checkCallback(f);
472 index = arrayFind(this, f, 0, false, true, context);
473 return isUndefined(index) ? -1 : index;
474 }
475
476 });
477
478 extend(array, true, true, {
479
480 /***
481 * @method findFrom(<f>, [index] = 0, [loop] = false)
482 * @returns Array
483 * @short Returns any element that matches <f>, beginning from [index].
484 * @extra <f> will match a string, number, array, object, or alternately test against a function or regex. Will continue from index = 0 if [loop] is true. This method implements @array_matching.
485 * @example
486 *
487 * ['cuba','japan','canada'].findFrom(/^c/, 2) -> 'canada'
488 *
489 ***/
490 'findFrom': function(f, index, loop) {
491 return arrayFind(this, f, index, loop);
492 },
493
494 /***
495 * @method findIndexFrom(<f>, [index] = 0, [loop] = false)
496 * @returns Array
497 * @short Returns the index of any element that matches <f>, beginning from [index].
498 * @extra <f> will match a string, number, array, object, or alternately test against a function or regex. Will continue from index = 0 if [loop] is true. This method implements @array_matching.
499 * @example
500 *
501 * ['cuba','japan','canada'].findIndexFrom(/^c/, 2) -> 2
502 *
503 ***/
504 'findIndexFrom': function(f, index, loop) {
505 var index = arrayFind(this, f, index, loop, true);
506 return isUndefined(index) ? -1 : index;
507 },
508
509 /***
510 * @method findAll(<f>, [index] = 0, [loop] = false)
511 * @returns Array
512 * @short Returns all elements that match <f>.
513 * @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.
514 * @example
515 *
516 + [{a:1,b:2},{a:1,b:3},{a:2,b:4}].findAll(function(n) {
517 * return n['a'] == 1;
518 * }); -> [{a:1,b:3},{a:1,b:4}]
519 * ['cuba','japan','canada'].findAll(/^c/) -> 'cuba','canada'
520 * ['cuba','japan','canada'].findAll(/^c/, 2) -> 'canada'
521 *
522 ***/
523 'findAll': function(f, index, loop) {
524 var result = [], matcher;
525 if(this.length > 0) {
526 matcher = getMatcher(f);
527 arrayEach(this, function(el, i, arr) {
528 if(matcher(el, i, arr)) {
529 result.push(el);
530 }
531 }, index, loop);
532 }
533 return result;
534 },
535
536 /***
537 * @method count(<f>)
538 * @returns Number
539 * @short Counts all elements in the array that match <f>.
540 * @extra <f> will match a string, number, array, object, or alternately test against a function or regex. This method implements @array_matching.
541 * @example
542 *
543 * [1,2,3,1].count(1) -> 2
544 * ['a','b','c'].count(/b/) -> 1
545 + [{a:1},{b:2}].count(function(n) {
546 * return n['a'] > 1;
547 * }); -> 0
548 *
549 ***/
550 'count': function(f) {
551 if(isUndefined(f)) return this.length;
552 return this.findAll(f).length;
553 },
554
555 /***
556 * @method removeAt(<start>, [end])
557 * @returns Array
558 * @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.
559 * @example
560 *
561 * ['a','b','c'].removeAt(0) -> ['b','c']
562 * [1,2,3,4].removeAt(1, 3) -> [1]
563 *
564 ***/
565 'removeAt': function(start, end) {
566 if(isUndefined(start)) return this;
567 if(isUndefined(end)) end = start;
568 this.splice(start, end - start + 1);
569 return this;
570 },
571
572 /***
573 * @method include(<el>, [index])
574 * @returns Array
575 * @short Adds <el> to the array.
576 * @extra This is a non-destructive alias for %add%. It will not change the original array.
577 * @example
578 *
579 * [1,2,3,4].include(5) -> [1,2,3,4,5]
580 * [1,2,3,4].include(8, 1) -> [1,8,2,3,4]
581 * [1,2,3,4].include([5,6,7]) -> [1,2,3,4,5,6,7]
582 *
583 ***/
584 'include': function(el, index) {
585 return this.clone().add(el, index);
586 },
587
588 /***
589 * @method exclude([f1], [f2], ...)
590 * @returns Array
591 * @short Removes any element in the array that matches [f1], [f2], etc.
592 * @extra This is a non-destructive alias for %remove%. It will not change the original array. This method implements @array_matching.
593 * @example
594 *
595 * [1,2,3].exclude(3) -> [1,2]
596 * ['a','b','c'].exclude(/b/) -> ['a','c']
597 + [{a:1},{b:2}].exclude(function(n) {
598 * return n['a'] == 1;
599 * }); -> [{b:2}]
600 *
601 ***/
602 'exclude': function() {
603 return array.prototype.remove.apply(this.clone(), arguments);
604 },
605
606 /***
607 * @method clone()
608 * @returns Array
609 * @short Makes a shallow clone of the array.
610 * @example
611 *
612 * [1,2,3].clone() -> [1,2,3]
613 *
614 ***/
615 'clone': function() {
616 return simpleMerge([], this);
617 },
618
619 /***
620 * @method unique([map] = null)
621 * @returns Array
622 * @short Removes all duplicate elements in the array.
623 * @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.
624 * @example
625 *
626 * [1,2,2,3].unique() -> [1,2,3]
627 * [{foo:'bar'},{foo:'bar'}].unique() -> [{foo:'bar'}]
628 + [{foo:'bar'},{foo:'bar'}].unique(function(obj){
629 * return obj.foo;
630 * }); -> [{foo:'bar'}]
631 * [{foo:'bar'},{foo:'bar'}].unique('foo') -> [{foo:'bar'}]
632 *
633 ***/
634 'unique': function(map) {
635 return arrayUnique(this, map);
636 },
637
638 /***
639 * @method flatten([limit] = Infinity)
640 * @returns Array
641 * @short Returns a flattened, one-dimensional copy of the array.
642 * @extra You can optionally specify a [limit], which will only flatten that depth.
643 * @example
644 *
645 * [[1], 2, [3]].flatten() -> [1,2,3]
646 * [['a'],[],'b','c'].flatten() -> ['a','b','c']
647 *
648 ***/
649 'flatten': function(limit) {
650 return arrayFlatten(this, limit);
651 },
652
653 /***
654 * @method union([a1], [a2], ...)
655 * @returns Array
656 * @short Returns an array containing all elements in all arrays with duplicates removed.
657 * @extra This method will also correctly operate on arrays of objects.
658 * @example
659 *
660 * [1,3,5].union([5,7,9]) -> [1,3,5,7,9]
661 * ['a','b'].union(['b','c']) -> ['a','b','c']
662 *
663 ***/
664 'union': function() {
665 return arrayUnique(this.concat(flatArguments(arguments)));
666 },
667
668 /***
669 * @method intersect([a1], [a2], ...)
670 * @returns Array
671 * @short Returns an array containing the elements all arrays have in common.
672 * @extra This method will also correctly operate on arrays of objects.
673 * @example
674 *
675 * [1,3,5].intersect([5,7,9]) -> [5]
676 * ['a','b'].intersect('b','c') -> ['b']
677 *
678 ***/
679 'intersect': function() {
680 return arrayIntersect(this, flatArguments(arguments), false);
681 },
682
683 /***
684 * @method subtract([a1], [a2], ...)
685 * @returns Array
686 * @short Subtracts from the array all elements in [a1], [a2], etc.
687 * @extra This method will also correctly operate on arrays of objects.
688 * @example
689 *
690 * [1,3,5].subtract([5,7,9]) -> [1,3]
691 * [1,3,5].subtract([3],[5]) -> [1]
692 * ['a','b'].subtract('b','c') -> ['a']
693 *
694 ***/
695 'subtract': function(a) {
696 return arrayIntersect(this, flatArguments(arguments), true);
697 },
698
699 /***
700 * @method at(<index>, [loop] = true)
701 * @returns Mixed
702 * @short Gets the element(s) at a given index.
703 * @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.
704 * @example
705 *
706 * [1,2,3].at(0) -> 1
707 * [1,2,3].at(2) -> 3
708 * [1,2,3].at(4) -> 2
709 * [1,2,3].at(4, false) -> null
710 * [1,2,3].at(-1) -> 3
711 * [1,2,3].at(0,1) -> [1,2]
712 *
713 ***/
714 'at': function() {
715 return getEntriesForIndexes(this, arguments);
716 },
717
718 /***
719 * @method first([num] = 1)
720 * @returns Mixed
721 * @short Returns the first element(s) in the array.
722 * @extra When <num> is passed, returns the first <num> elements in the array.
723 * @example
724 *
725 * [1,2,3].first() -> 1
726 * [1,2,3].first(2) -> [1,2]
727 *
728 ***/
729 'first': function(num) {
730 if(isUndefined(num)) return this[0];
731 if(num < 0) num = 0;
732 return this.slice(0, num);
733 },
734
735 /***
736 * @method last([num] = 1)
737 * @returns Mixed
738 * @short Returns the last element(s) in the array.
739 * @extra When <num> is passed, returns the last <num> elements in the array.
740 * @example
741 *
742 * [1,2,3].last() -> 3
743 * [1,2,3].last(2) -> [2,3]
744 *
745 ***/
746 'last': function(num) {
747 if(isUndefined(num)) return this[this.length - 1];
748 var start = this.length - num < 0 ? 0 : this.length - num;
749 return this.slice(start);
750 },
751
752 /***
753 * @method from(<index>)
754 * @returns Array
755 * @short Returns a slice of the array from <index>.
756 * @example
757 *
758 * [1,2,3].from(1) -> [2,3]
759 * [1,2,3].from(2) -> [3]
760 *
761 ***/
762 'from': function(num) {
763 return this.slice(num);
764 },
765
766 /***
767 * @method to(<index>)
768 * @returns Array
769 * @short Returns a slice of the array up to <index>.
770 * @example
771 *
772 * [1,2,3].to(1) -> [1]
773 * [1,2,3].to(2) -> [1,2]
774 *
775 ***/
776 'to': function(num) {
777 if(isUndefined(num)) num = this.length;
778 return this.slice(0, num);
779 },
780
781 /***
782 * @method min([map], [all] = false)
783 * @returns Mixed
784 * @short Returns the element in the array with the lowest value.
785 * @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.
786 * @example
787 *
788 * [1,2,3].min() -> 1
789 * ['fee','fo','fum'].min('length') -> 'fo'
790 * ['fee','fo','fum'].min('length', true) -> ['fo']
791 + ['fee','fo','fum'].min(function(n) {
792 * return n.length;
793 * }); -> ['fo']
794 + [{a:3,a:2}].min(function(n) {
795 * return n['a'];
796 * }); -> [{a:2}]
797 *
798 ***/
799 'min': function(map, all) {
800 return getMinOrMax(this, map, 'min', all);
801 },
802
803 /***
804 * @method max([map], [all] = false)
805 * @returns Mixed
806 * @short Returns the element in the array with the greatest value.
807 * @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.
808 * @example
809 *
810 * [1,2,3].max() -> 3
811 * ['fee','fo','fum'].max('length') -> 'fee'
812 * ['fee','fo','fum'].max('length', true) -> ['fee']
813 + [{a:3,a:2}].max(function(n) {
814 * return n['a'];
815 * }); -> {a:3}
816 *
817 ***/
818 'max': function(map, all) {
819 return getMinOrMax(this, map, 'max', all);
820 },
821
822 /***
823 * @method least([map])
824 * @returns Array
825 * @short Returns the elements in the array with the least commonly occuring value.
826 * @extra [map] may be a function mapping the value to be checked or a string acting as a shortcut.
827 * @example
828 *
829 * [3,2,2].least() -> [3]
830 * ['fe','fo','fum'].least('length') -> ['fum']
831 + [{age:35,name:'ken'},{age:12,name:'bob'},{age:12,name:'ted'}].least(function(n) {
832 * return n.age;
833 * }); -> [{age:35,name:'ken'}]
834 *
835 ***/
836 'least': function(map, all) {
837 return getMinOrMax(this.groupBy.apply(this, [map]), 'length', 'min', all);
838 },
839
840 /***
841 * @method most([map])
842 * @returns Array
843 * @short Returns the elements in the array with the most commonly occuring value.
844 * @extra [map] may be a function mapping the value to be checked or a string acting as a shortcut.
845 * @example
846 *
847 * [3,2,2].most() -> [2]
848 * ['fe','fo','fum'].most('length') -> ['fe','fo']
849 + [{age:35,name:'ken'},{age:12,name:'bob'},{age:12,name:'ted'}].most(function(n) {
850 * return n.age;
851 * }); -> [{age:12,name:'bob'},{age:12,name:'ted'}]
852 *
853 ***/
854 'most': function(map, all) {
855 return getMinOrMax(this.groupBy.apply(this, [map]), 'length', 'max', all);
856 },
857
858 /***
859 * @method sum([map])
860 * @returns Number
861 * @short Sums all values in the array.
862 * @extra [map] may be a function mapping the value to be summed or a string acting as a shortcut.
863 * @example
864 *
865 * [1,2,2].sum() -> 5
866 + [{age:35},{age:12},{age:12}].sum(function(n) {
867 * return n.age;
868 * }); -> 59
869 * [{age:35},{age:12},{age:12}].sum('age') -> 59
870 *
871 ***/
872 'sum': function(map) {
873 var arr = map ? this.map(map) : this;
874 return arr.length > 0 ? arr.reduce(function(a,b) { return a + b; }) : 0;
875 },
876
877 /***
878 * @method average([map])
879 * @returns Number
880 * @short Gets the mean average for all values in the array.
881 * @extra [map] may be a function mapping the value to be averaged or a string acting as a shortcut.
882 * @example
883 *
884 * [1,2,3].average() -> 2
885 + [{age:35},{age:11},{age:11}].average(function(n) {
886 * return n.age;
887 * }); -> 19
888 * [{age:35},{age:11},{age:11}].average('age') -> 19
889 *
890 ***/
891 'average': function(map) {
892 var arr = map ? this.map(map) : this;
893 return arr.length > 0 ? arr.sum() / arr.length : 0;
894 },
895
896 /***
897 * @method inGroups(<num>, [padding])
898 * @returns Array
899 * @short Groups the array into <num> arrays.
900 * @extra [padding] specifies a value with which to pad the last array so that they are all equal length.
901 * @example
902 *
903 * [1,2,3,4,5,6,7].inGroups(3) -> [ [1,2,3], [4,5,6], [7] ]
904 * [1,2,3,4,5,6,7].inGroups(3, 'none') -> [ [1,2,3], [4,5,6], [7,'none','none'] ]
905 *
906 ***/
907 'inGroups': function(num, padding) {
908 var pad = arguments.length > 1;
909 var arr = this;
910 var result = [];
911 var divisor = ceil(this.length / num);
912 simpleRepeat(num, function(i) {
913 var index = i * divisor;
914 var group = arr.slice(index, index + divisor);
915 if(pad && group.length < divisor) {
916 simpleRepeat(divisor - group.length, function() {
917 group = group.add(padding);
918 });
919 }
920 result.push(group);
921 });
922 return result;
923 },
924
925 /***
926 * @method inGroupsOf(<num>, [padding] = null)
927 * @returns Array
928 * @short Groups the array into arrays of <num> elements each.
929 * @extra [padding] specifies a value with which to pad the last array so that they are all equal length.
930 * @example
931 *
932 * [1,2,3,4,5,6,7].inGroupsOf(4) -> [ [1,2,3,4], [5,6,7] ]
933 * [1,2,3,4,5,6,7].inGroupsOf(4, 'none') -> [ [1,2,3,4], [5,6,7,'none'] ]
934 *
935 ***/
936 'inGroupsOf': function(num, padding) {
937 var result = [], len = this.length, arr = this, group;
938 if(len === 0 || num === 0) return arr;
939 if(isUndefined(num)) num = 1;
940 if(isUndefined(padding)) padding = null;
941 simpleRepeat(ceil(len / num), function(i) {
942 group = arr.slice(num * i, num * i + num);
943 while(group.length < num) {
944 group.push(padding);
945 }
946 result.push(group);
947 });
948 return result;
949 },
950
951 /***
952 * @method isEmpty()
953 * @returns Boolean
954 * @short Returns true if the array is empty.
955 * @extra This is true if the array has a length of zero, or contains only %undefined%, %null%, or %NaN%.
956 * @example
957 *
958 * [].isEmpty() -> true
959 * [null,undefined].isEmpty() -> true
960 *
961 ***/
962 'isEmpty': function() {
963 return this.compact().length == 0;
964 },
965
966 /***
967 * @method sortBy(<map>, [desc] = false)
968 * @returns Array
969 * @short Sorts the array by <map>.
970 * @extra <map> may be a function, a string acting as a shortcut, or blank (direct comparison of array values). [desc] will sort the array in descending order. When the field being sorted on is a string, the resulting order will be determined by an internal collation algorithm that is optimized for major Western languages, but can be customized. For more information see @array_sorting.
971 * @example
972 *
973 * ['world','a','new'].sortBy('length') -> ['a','new','world']
974 * ['world','a','new'].sortBy('length', true) -> ['world','new','a']
975 + [{age:72},{age:13},{age:18}].sortBy(function(n) {
976 * return n.age;
977 * }); -> [{age:13},{age:18},{age:72}]
978 *
979 ***/
980 'sortBy': function(map, desc) {
981 var arr = this.clone();
982 arr.sort(function(a, b) {
983 var aProperty, bProperty, comp;
984 aProperty = transformArgument(a, map, arr, [a]);
985 bProperty = transformArgument(b, map, arr, [b]);
986 if(isString(aProperty) && isString(bProperty)) {
987 comp = collateStrings(aProperty, bProperty);
988 } else if(aProperty < bProperty) {
989 comp = -1;
990 } else if(aProperty > bProperty) {
991 comp = 1;
992 } else {
993 comp = 0;
994 }
995 return comp * (desc ? -1 : 1);
996 });
997 return arr;
998 },
999
1000 /***
1001 * @method randomize()
1002 * @returns Array
1003 * @short Returns a copy of the array with the elements randomized.
1004 * @extra Uses Fisher-Yates algorithm.
1005 * @example
1006 *
1007 * [1,2,3,4].randomize() -> [?,?,?,?]
1008 *
1009 ***/
1010 'randomize': function() {
1011 var arr = this.concat(), i = arr.length, j, x;
1012 while(i) {
1013 j = (math.random() * i) | 0;
1014 x = arr[--i];
1015 arr[i] = arr[j];
1016 arr[j] = x;
1017 }
1018 return arr;
1019 },
1020
1021 /***
1022 * @method zip([arr1], [arr2], ...)
1023 * @returns Array
1024 * @short Merges multiple arrays together.
1025 * @extra This method "zips up" smaller arrays into one large whose elements are "all elements at index 0", "all elements at index 1", etc. Useful when you have associated data that is split over separated arrays. If the arrays passed have more elements than the original array, they will be discarded. If they have fewer elements, the missing elements will filled with %null%.
1026 * @example
1027 *
1028 * [1,2,3].zip([4,5,6]) -> [[1,2], [3,4], [5,6]]
1029 * ['Martin','John'].zip(['Luther','F.'], ['King','Kennedy']) -> [['Martin','Luther','King'], ['John','F.','Kennedy']]
1030 *
1031 ***/
1032 'zip': function() {
1033 var args = multiArgs(arguments);
1034 return this.map(function(el, i) {
1035 return [el].concat(args.map(function(k) {
1036 return (i in k) ? k[i] : null;
1037 }));
1038 });
1039 },
1040
1041 /***
1042 * @method sample([num])
1043 * @returns Mixed
1044 * @short Returns a random element from the array.
1045 * @extra If [num] is passed, will return [num] samples from the array.
1046 * @example
1047 *
1048 * [1,2,3,4,5].sample() -> // Random element
1049 * [1,2,3,4,5].sample(3) -> // Array of 3 random elements
1050 *
1051 ***/
1052 'sample': function(num) {
1053 var arr = this.randomize();
1054 return arguments.length > 0 ? arr.slice(0, num) : arr[0];
1055 },
1056
1057 /***
1058 * @method each(<fn>, [index] = 0, [loop] = false)
1059 * @returns Array
1060 * @short Runs <fn> against each element in the array. Enhanced version of %Array#forEach%.
1061 * @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.
1062 * @example
1063 *
1064 * [1,2,3,4].each(function(n) {
1065 * // Called 4 times: 1, 2, 3, 4
1066 * });
1067 * [1,2,3,4].each(function(n) {
1068 * // Called 4 times: 3, 4, 1, 2
1069 * }, 2, true);
1070 *
1071 ***/
1072 'each': function(fn, index, loop) {
1073 arrayEach(this, fn, index, loop);
1074 return this;
1075 },
1076
1077 /***
1078 * @method add(<el>, [index])
1079 * @returns Array
1080 * @short Adds <el> to the array.
1081 * @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.
1082 * @example
1083 *
1084 * [1,2,3,4].add(5) -> [1,2,3,4,5]
1085 * [1,2,3,4].add([5,6,7]) -> [1,2,3,4,5,6,7]
1086 * [1,2,3,4].insert(8, 1) -> [1,8,2,3,4]
1087 *
1088 ***/
1089 'add': function(el, index) {
1090 if(!isNumber(number(index)) || isNaN(index)) index = this.length;
1091 array.prototype.splice.apply(this, [index, 0].concat(el));
1092 return this;
1093 },
1094
1095 /***
1096 * @method remove([f1], [f2], ...)
1097 * @returns Array
1098 * @short Removes any element in the array that matches [f1], [f2], etc.
1099 * @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. This method implements @array_matching.
1100 * @example
1101 *
1102 * [1,2,3].remove(3) -> [1,2]
1103 * ['a','b','c'].remove(/b/) -> ['a','c']
1104 + [{a:1},{b:2}].remove(function(n) {
1105 * return n['a'] == 1;
1106 * }); -> [{b:2}]
1107 *
1108 ***/
1109 'remove': function() {
1110 var arr = this;
1111 multiArgs(arguments, function(f) {
1112 var i = 0, matcher = getMatcher(f);
1113 while(i < arr.length) {
1114 if(matcher(arr[i], i, arr)) {
1115 arr.splice(i, 1);
1116 } else {
1117 i++;
1118 }
1119 }
1120 });
1121 return arr;
1122 },
1123
1124 /***
1125 * @method compact([all] = false)
1126 * @returns Array
1127 * @short Removes all instances of %undefined%, %null%, and %NaN% from the array.
1128 * @extra If [all] is %true%, all "falsy" elements will be removed. This includes empty strings, 0, and false.
1129 * @example
1130 *
1131 * [1,null,2,undefined,3].compact() -> [1,2,3]
1132 * [1,'',2,false,3].compact() -> [1,'',2,false,3]
1133 * [1,'',2,false,3].compact(true) -> [1,2,3]
1134 *
1135 ***/
1136 'compact': function(all) {
1137 var result = [];
1138 arrayEach(this, function(el, i) {
1139 if(isArray(el)) {
1140 result.push(el.compact());
1141 } else if(all && el) {
1142 result.push(el);
1143 } else if(!all && el != null && el.valueOf() === el.valueOf()) {
1144 result.push(el);
1145 }
1146 });
1147 return result;
1148 },
1149
1150 /***
1151 * @method groupBy(<map>, [fn])
1152 * @returns Object
1153 * @short Groups the array by <map>.
1154 * @extra Will return an object with keys equal to the grouped values. <map> may be a mapping function, or a string acting as a shortcut. Optionally calls [fn] for each group.
1155 * @example
1156 *
1157 * ['fee','fi','fum'].groupBy('length') -> { 2: ['fi'], 3: ['fee','fum'] }
1158 + [{age:35,name:'ken'},{age:15,name:'bob'}].groupBy(function(n) {
1159 * return n.age;
1160 * }); -> { 35: [{age:35,name:'ken'}], 15: [{age:15,name:'bob'}] }
1161 *
1162 ***/
1163 'groupBy': function(map, fn) {
1164 var arr = this, result = {}, key;
1165 arrayEach(arr, function(el, index) {
1166 key = transformArgument(el, map, arr, [el, index, arr]);
1167 if(!result[key]) result[key] = [];
1168 result[key].push(el);
1169 });
1170 if(fn) {
1171 iterateOverObject(result, fn);
1172 }
1173 return result;
1174 },
1175
1176 /***
1177 * @method none(<f>)
1178 * @returns Boolean
1179 * @short Returns true if none of the elements in the array match <f>.
1180 * @extra <f> will match a string, number, array, object, or alternately test against a function or regex. This method implements @array_matching.
1181 * @example
1182 *
1183 * [1,2,3].none(5) -> true
1184 * ['a','b','c'].none(/b/) -> false
1185 + [{a:1},{b:2}].none(function(n) {
1186 * return n['a'] > 1;
1187 * }); -> true
1188 *
1189 ***/
1190 'none': function() {
1191 return !this.any.apply(this, arguments);
1192 }
1193
1194
1195 });
1196
1197
1198 // Aliases
1199
1200 extend(array, true, true, {
1201
1202 /***
1203 * @method all()
1204 * @alias every
1205 *
1206 ***/
1207 'all': array.prototype.every,
1208
1209 /*** @method any()
1210 * @alias some
1211 *
1212 ***/
1213 'any': array.prototype.some,
1214
1215 /***
1216 * @method insert()
1217 * @alias add
1218 *
1219 ***/
1220 'insert': array.prototype.add
1221
1222 });
1223
1224
1225 /***
1226 * Object module
1227 * Enumerable methods on objects
1228 *
1229 ***/
1230
1231 function keysWithObjectCoercion(obj) {
1232 return object.keys(coercePrimitiveToObject(obj));
1233 }
1234
1235 /***
1236 * @method [enumerable](<obj>)
1237 * @returns Boolean
1238 * @short Enumerable methods in the Array package are also available to the Object class. They will perform their normal operations for every property in <obj>.
1239 * @extra In cases where a callback is used, instead of %element, index%, the callback will instead be passed %key, value%. Enumerable methods are also available to extended objects as instance methods.
1240 *
1241 * @set
1242 * each
1243 * map
1244 * any
1245 * all
1246 * none
1247 * count
1248 * find
1249 * findAll
1250 * reduce
1251 * isEmpty
1252 * sum
1253 * average
1254 * min
1255 * max
1256 * least
1257 * most
1258 *
1259 * @example
1260 *
1261 * Object.any({foo:'bar'}, 'bar') -> true
1262 * Object.extended({foo:'bar'}).any('bar') -> true
1263 * Object.isEmpty({}) -> true
1264 + Object.map({ fred: { age: 52 } }, 'age'); -> { fred: 52 }
1265 *
1266 ***/
1267
1268 function buildEnumerableMethods(names, mapping) {
1269 extendSimilar(object, false, true, names, function(methods, name) {
1270 methods[name] = function(obj, arg1, arg2) {
1271 var result, coerced = keysWithObjectCoercion(obj), matcher;
1272 if(!mapping) {
1273 matcher = getMatcher(arg1, true);
1274 }
1275 result = array.prototype[name].call(coerced, function(key) {
1276 var value = obj[key];
1277 if(mapping) {
1278 return transformArgument(value, arg1, obj, [key, value, obj]);
1279 } else {
1280 return matcher(value, key, obj);
1281 }
1282 }, arg2);
1283 if(isArray(result)) {
1284 // The method has returned an array of keys so use this array
1285 // to build up the resulting object in the form we want it in.
1286 result = result.reduce(function(o, key, i) {
1287 o[key] = obj[key];
1288 return o;
1289 }, {});
1290 }
1291 return result;
1292 };
1293 });
1294 buildObjectInstanceMethods(names, Hash);
1295 }
1296
1297 function exportSortAlgorithm() {
1298 array[AlphanumericSort] = collateStrings;
1299 }
1300
1301 extend(object, false, true, {
1302
1303 'map': function(obj, map) {
1304 var result = {}, key, value;
1305 for(key in obj) {
1306 if(!hasOwnProperty(obj, key)) continue;
1307 value = obj[key];
1308 result[key] = transformArgument(value, map, obj, [key, value, obj]);
1309 }
1310 return result;
1311 },
1312
1313 'reduce': function(obj) {
1314 var values = keysWithObjectCoercion(obj).map(function(key) {
1315 return obj[key];
1316 });
1317 return values.reduce.apply(values, multiArgs(arguments, null, 1));
1318 },
1319
1320 'each': function(obj, fn) {
1321 checkCallback(fn);
1322 iterateOverObject(obj, fn);
1323 return obj;
1324 },
1325
1326 /***
1327 * @method size(<obj>)
1328 * @returns Number
1329 * @short Returns the number of properties in <obj>.
1330 * @extra %size% is available as an instance method on extended objects.
1331 * @example
1332 *
1333 * Object.size({ foo: 'bar' }) -> 1
1334 *
1335 ***/
1336 'size': function (obj) {
1337 return keysWithObjectCoercion(obj).length;
1338 }
1339
1340 });
1341
1342 var EnumerableFindingMethods = 'any,all,none,count,find,findAll,isEmpty'.split(',');
1343 var EnumerableMappingMethods = 'sum,average,min,max,least,most'.split(',');
1344 var EnumerableOtherMethods = 'map,reduce,size'.split(',');
1345 var EnumerableMethods = EnumerableFindingMethods.concat(EnumerableMappingMethods).concat(EnumerableOtherMethods);
1346
1347 buildEnhancements();
1348 buildAlphanumericSort();
1349 buildEnumerableMethods(EnumerableFindingMethods);
1350 buildEnumerableMethods(EnumerableMappingMethods, true);
1351 buildObjectInstanceMethods(EnumerableOtherMethods, Hash);
1352 exportSortAlgorithm();
1353