master
Raw Download raw file
   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