1 // ==========================================================================
  2 // Project:   SproutCore Costello - Property Observing Library
  3 // Copyright: ©2006-2011 Strobe Inc. and contributors.
  4 //            Portions ©2008-2011 Apple Inc. All rights reserved.
  5 // License:   Licensed under MIT license (see license.js)
  6 // ==========================================================================
  7 
  8 sc_require('core');
  9 sc_require('ext/function');
 10 sc_require('system/enumerator');
 11 
 12 /*global Prototype */
 13 
 14 /**
 15   @class
 16 
 17   This mixin defines the common interface implemented by enumerable objects
 18   in SproutCore.  Most of these methods follow the standard Array iteration
 19   API defined up to JavaScript 1.8 (excluding language-specific features that
 20   cannot be emulated in older versions of JavaScript).
 21 
 22   This mixin is applied automatically to the Array class on page load, so you
 23   can use any of these methods on simple arrays.  If Array already implements
 24   one of these methods, the mixin will not override them.
 25 
 26   Writing Your Own Enumerable
 27   -----
 28 
 29   To make your own custom class enumerable, you need two items:
 30 
 31   1. You must have a length property.  This property should change whenever
 32      the number of items in your enumerable object changes.  If you using this
 33      with an SC.Object subclass, you should be sure to change the length
 34      property using set().
 35 
 36   2. You must implement nextObject().  See documentation.
 37 
 38   Once you have these two methods implemented, apply the SC.Enumerable mixin
 39   to your class and you will be able to enumerate the contents of your object
 40   like any other collection.
 41 
 42   Using SproutCore Enumeration with Other Libraries
 43   -----
 44 
 45   Many other libraries provide some kind of iterator or enumeration like
 46   facility.  This is often where the most common API conflicts occur.
 47   SproutCore's API is designed to be as friendly as possible with other
 48   libraries by implementing only methods that mostly correspond to the
 49   JavaScript 1.8 API.
 50 
 51   @since SproutCore 1.0
 52 */
 53 SC.Enumerable = /** @scope SC.Enumerable.prototype */{
 54 
 55   /**
 56     Walk like a duck.
 57 
 58     @type Boolean
 59   */
 60   isEnumerable: YES,
 61 
 62   /**
 63     Implement this method to make your class enumerable.
 64 
 65     This method will be called repeatedly during enumeration.  The index value
 66     will always begin with 0 and increment monotonically.  You don't have to
 67     rely on the index value to determine what object to return, but you should
 68     always check the value and start from the beginning when you see the
 69     requested index is 0.
 70 
 71     The previousObject is the object that was returned from the last call
 72     to nextObject for the current iteration.  This is a useful way to
 73     manage iteration if you are tracing a linked list, for example.
 74 
 75     Finally the context parameter will always contain a hash you can use as
 76     a "scratchpad" to maintain any other state you need in order to iterate
 77     properly.  The context object is reused and is not reset between
 78     iterations so make sure you setup the context with a fresh state whenever
 79     the index parameter is 0.
 80 
 81     Generally iterators will continue to call nextObject until the index
 82     reaches the your current length-1.  If you run out of data before this
 83     time for some reason, you should simply return undefined.
 84 
 85     The default implementation of this method simply looks up the index.
 86     This works great on any Array-like objects.
 87 
 88     @param {Number} index the current index of the iteration
 89     @param {Object} previousObject the value returned by the last call to nextObject.
 90     @param {Object} context a context object you can use to maintain state.
 91     @returns {Object} the next object in the iteration or undefined
 92   */
 93   nextObject: function (index, previousObject, context) {
 94     return this.objectAt ? this.objectAt(index) : this[index];
 95   },
 96 
 97   /**
 98     Helper method returns the first object from a collection.  This is usually
 99     used by bindings and other parts of the framework to extract a single
100     object if the enumerable contains only one item.
101 
102     If you override this method, you should implement it so that it will
103     always return the same value each time it is called.  If your enumerable
104     contains only one object, this method should always return that object.
105     If your enumerable is empty, this method should return undefined.
106 
107     This property is observable if the enumerable supports it.  Examples
108     of enumerables where firstObject is observable include SC.Array,
109     SC.ManyArray and SC.SparseArray.  To implement a custom enumerable where
110     firstObject is observable, see {@link #enumerableContentDidChange}.
111 
112     @see #enumerableContentDidChange
113 
114     @returns {Object} the object or undefined
115   */
116   firstObject: function () {
117     if (this.get('length') === 0) return undefined;
118     if (this.objectAt) return this.objectAt(0); // support arrays out of box
119 
120     // handle generic enumerables
121     var context = SC.Enumerator._popContext(), ret;
122     ret = this.nextObject(0, null, context);
123     context = SC.Enumerator._pushContext(context);
124     return ret;
125   }.property().cacheable(),
126 
127   /**
128     Helper method returns the last object from a collection.
129 
130     This property is observable if the enumerable supports it.  Examples
131     of enumerables where lastObject is observable include SC.Array,
132     SC.ManyArray and SC.SparseArray.  To implement a custom enumerable where
133     lastObject is observable, see {@link #enumerableContentDidChange}.
134 
135     @see #enumerableContentDidChange
136 
137     @returns {Object} the object or undefined
138   */
139   lastObject: function () {
140     var len = this.get('length');
141     if (len === 0) return undefined;
142     if (this.objectAt) return this.objectAt(len - 1); // support arrays out of box
143   }.property().cacheable(),
144 
145   /**
146     Returns a new enumerator for this object.  See SC.Enumerator for
147     documentation on how to use this object.  Enumeration is an alternative
148     to using one of the other iterators described here.
149 
150     @returns {SC.Enumerator} an enumerator for the receiver
151   */
152   enumerator: function () { return SC.Enumerator.create(this); },
153 
154   /**
155     Iterates through the enumerable, calling the passed function on each
156     item.  This method corresponds to the forEach() method defined in
157     JavaScript 1.6.
158 
159     The callback method you provide should have the following signature (all
160     parameters are optional):
161 
162           function (item, index, enumerable);
163 
164     - *item* is the current item in the iteration.
165     - *index* is the current index in the iteration
166     - *enumerable* is the enumerable object itself.
167 
168     Note that in addition to a callback, you can also pass an optional target
169     object that will be set as "this" on the context.  This is a good way
170     to give your iterator function access to the current object.
171 
172     @param {Function} callback the callback to execute
173     @param {Object} target the target object to use
174     @returns {Object} this
175   */
176   forEach: function (callback, target) {
177     if (typeof callback !== "function") throw new TypeError();
178     var len = this.get ? this.get('length') : this.length;
179     if (target === undefined) target = null;
180 
181     var last = null;
182     var context = SC.Enumerator._popContext();
183     for (var idx = 0; idx < len;idx++) {
184       var next = this.nextObject(idx, last, context);
185       callback.call(target, next, idx, this);
186       last = next;
187     }
188     last = null;
189     context = SC.Enumerator._pushContext(context);
190     return this;
191   },
192 
193   /**
194     Retrieves the named value on each member object.  This is more efficient
195     than using one of the wrapper methods defined here.  Objects that
196     implement SC.Observable will use the get() method, otherwise the property
197     will be accessed directly.
198 
199     @param {String} key the key to retrieve
200     @returns {Array} extracted values
201   */
202   getEach: function (key) {
203     return this.map(function (next) {
204       return next ? (next.get ? next.get(key) : next[key]) : null;
205     }, this);
206   },
207 
208   /**
209     Sets the value on the named property for each member.  This is more
210     efficient than using other methods defined on this helper.  If the object
211     implements SC.Observable, the value will be changed to set(), otherwise
212     it will be set directly.  null objects are skipped.
213 
214     @param {String} key the key to set
215     @param {Object} value the object to set
216     @returns {Object} receiver
217   */
218   setEach: function (key, value) {
219     this.forEach(function (next) {
220       if (next) {
221         if (next.set) next.set(key, value);
222         else next[key] = value;
223       }
224     }, this);
225     return this;
226   },
227 
228   /**
229     Maps all of the items in the enumeration to another value, returning
230     a new array.  This method corresponds to map() defined in JavaScript 1.6.
231 
232     The callback method you provide should have the following signature (all
233     parameters are optional):
234 
235         function (item, index, enumerable);
236 
237     - *item* is the current item in the iteration.
238     - *index* is the current index in the iteration
239     - *enumerable* is the enumerable object itself.
240 
241     It should return the mapped value.
242 
243     Note that in addition to a callback, you can also pass an optional target
244     object that will be set as "this" on the context.  This is a good way
245     to give your iterator function access to the current object.
246 
247     @param {Function} callback the callback to execute
248     @param {Object} target the target object to use
249     @returns {Array} The mapped array.
250   */
251   map: function (callback, target) {
252     if (typeof callback !== "function") throw new TypeError();
253     var len = this.get ? this.get('length') : this.length;
254     if (target === undefined) target = null;
255 
256     var ret  = [];
257     var last = null;
258     var context = SC.Enumerator._popContext();
259     for (var idx = 0; idx < len;idx++) {
260       var next = this.nextObject(idx, last, context);
261       ret[idx] = callback.call(target, next, idx, this);
262       last = next;
263     }
264     last = null;
265     context = SC.Enumerator._pushContext(context);
266     return ret;
267   },
268 
269   /**
270     Similar to map, this specialized function returns the value of the named
271     property on all items in the enumeration.
272 
273     @param {String} key name of the property
274     @returns {Array} The mapped array.
275   */
276   mapProperty: function (key) {
277     return this.map(function (next) {
278       return next ? (next.get ? next.get(key) : next[key]) : null;
279     });
280   },
281 
282   /**
283     Returns an array with all of the items in the enumeration that the passed
284     function returns YES for. This method corresponds to filter() defined in
285     JavaScript 1.6.
286 
287     The callback method you provide should have the following signature (all
288     parameters are optional):
289 
290           function (item, index, enumerable);
291 
292     - *item* is the current item in the iteration.
293     - *index* is the current index in the iteration
294     - *enumerable* is the enumerable object itself.
295 
296     It should return the YES to include the item in the results, NO otherwise.
297 
298     Note that in addition to a callback, you can also pass an optional target
299     object that will be set as "this" on the context.  This is a good way
300     to give your iterator function access to the current object.
301 
302     @param {Function} callback the callback to execute
303     @param {Object} target the target object to use
304     @returns {Array} A filtered array.
305   */
306   filter: function (callback, target) {
307     if (typeof callback !== "function") throw new TypeError();
308     var len = this.get ? this.get('length') : this.length;
309     if (target === undefined) target = null;
310 
311     var ret  = [];
312     var last = null;
313     var context = SC.Enumerator._popContext();
314     for (var idx = 0; idx < len;idx++) {
315       var next = this.nextObject(idx, last, context);
316       if (callback.call(target, next, idx, this)) ret.push(next);
317       last = next;
318     }
319     last = null;
320     context = SC.Enumerator._pushContext(context);
321     return ret;
322   },
323 
324   /**
325     Returns an array sorted by the value of the passed key parameters.
326     null objects will be sorted first.  You can pass either an array of keys
327     or multiple parameters which will act as key names
328 
329     @param {String} key one or more key names
330     @returns {Array}
331   */
332   sortProperty: function (key) {
333     var keys = (typeof key === SC.T_STRING) ? arguments : key,
334         len  = keys.length,
335         src;
336 
337     // get the src array to sort
338     if (this instanceof Array) src = this;
339     else {
340       src = [];
341       this.forEach(function (i) { src.push(i); });
342     }
343 
344     if (!src) return [];
345     return src.sort(function (a, b) {
346       var idx, key, aValue, bValue, ret = 0;
347 
348       for (idx = 0; ret === 0 && idx < len; idx++) {
349         key = keys[idx];
350         aValue = a ? (a.get ? a.get(key) : a[key]) : null;
351         bValue = b ? (b.get ? b.get(key) : b[key]) : null;
352         ret = SC.compare(aValue, bValue);
353       }
354       return ret;
355     });
356   },
357 
358 
359   /**
360     Returns an array with just the items with the matched property.  You
361     can pass an optional second argument with the target value.  Otherwise
362     this will match any property that evaluates to true.
363 
364     Note: null, undefined, false and the empty string all evaulate to false.
365 
366     @param {String} key the property to test
367     @param {String} value optional value to test against.
368     @returns {Array} filtered array
369   */
370   filterProperty: function (key, value) {
371     var len = this.get ? this.get('length') : this.length,
372         ret = [],
373         last = null,
374         context = SC.Enumerator._popContext(),
375         idx, item, cur;
376     // Although the code for value and no value are almost identical, we want to make as many decisions outside
377     // the loop as possible.
378     if (value === undefined) {
379       for (idx = 0; idx < len; idx++) {
380         item = this.nextObject(idx, last, context);
381         cur = item ? (item.get ? item.get(key) : item[key]) : null;
382         if (!!cur) ret.push(item);
383         last = item;
384       }
385     } else {
386       for (idx = 0; idx < len; idx++) {
387         item = this.nextObject(idx, last, context);
388         cur = item ? (item.get ? item.get(key) : item[key]) : null;
389         if (SC.isEqual(cur, value)) ret.push(item);
390         last = item;
391       }
392     }
393     last = null;
394     context = SC.Enumerator._pushContext(context);
395     return ret;
396   },
397 
398   /**
399     Returns the first item in the array for which the callback returns YES.
400     This method works similar to the filter() method defined in JavaScript 1.6
401     except that it will stop working on the array once a match is found.
402 
403     The callback method you provide should have the following signature (all
404     parameters are optional):
405 
406           function (item, index, enumerable);
407 
408     - *item* is the current item in the iteration.
409     - *index* is the current index in the iteration
410     - *enumerable* is the enumerable object itself.
411 
412     It should return the YES to include the item in the results, NO otherwise.
413 
414     Note that in addition to a callback, you can also pass an optional target
415     object that will be set as "this" on the context.  This is a good way
416     to give your iterator function access to the current object.
417 
418     @param {Function} callback the callback to execute
419     @param {Object} target the target object to use
420     @returns {Object} Found item or null.
421   */
422   find: function (callback, target) {
423     var len = this.get ? this.get('length') : this.length;
424     if (target === undefined) target = null;
425 
426     var last = null, next, found = NO, ret = null;
427     var context = SC.Enumerator._popContext();
428     for (var idx = 0; idx < len && !found;idx++) {
429       next = this.nextObject(idx, last, context);
430       if (found = callback.call(target, next, idx, this)) ret = next;
431       last = next;
432     }
433     next = last = null;
434     context = SC.Enumerator._pushContext(context);
435     return ret;
436   },
437 
438   /**
439     Returns an the first item with a property matching the passed value.  You
440     can pass an optional second argument with the target value.  Otherwise
441     this will match any property that evaluates to true.
442 
443     This method works much like the more generic find() method.
444 
445     @param {String} key the property to test
446     @param {String} value optional value to test against.
447     @returns {Object} found item or null
448   */
449   findProperty: function (key, value) {
450     var len = this.get ? this.get('length') : this.length;
451     var found = NO, ret = null, last = null, next, cur;
452     var context = SC.Enumerator._popContext();
453     for (var idx = 0; idx < len && !found;idx++) {
454       next = this.nextObject(idx, last, context);
455       cur = next ? (next.get ? next.get(key) : next[key]) : null;
456       found = (value === undefined) ? !!cur : SC.isEqual(cur, value);
457       if (found) ret = next;
458       last = next;
459     }
460     last = next = null;
461     context = SC.Enumerator._pushContext(context);
462     return ret;
463   },
464 
465   /**
466     Returns YES if the passed function returns YES for every item in the
467     enumeration.  This corresponds with the every() method in JavaScript 1.6.
468 
469     The callback method you provide should have the following signature (all
470     parameters are optional):
471 
472           function (item, index, enumerable);
473 
474     - *item* is the current item in the iteration.
475     - *index* is the current index in the iteration
476     - *enumerable* is the enumerable object itself.
477 
478     It should return the YES or NO.
479 
480     Note that in addition to a callback, you can also pass an optional target
481     object that will be set as "this" on the context.  This is a good way
482     to give your iterator function access to the current object.
483 
484     Example Usage:
485 
486           if (people.every(isEngineer)) { Paychecks.addBigBonus(); }
487 
488     @param {Function} callback the callback to execute
489     @param {Object} target the target object to use
490     @returns {Boolean}
491   */
492   every: function (callback, target) {
493     if (typeof callback !== "function") throw new TypeError();
494     var len = this.get ? this.get('length') : this.length;
495     if (target === undefined) target = null;
496 
497     var ret  = YES;
498     var last = null;
499     var context = SC.Enumerator._popContext();
500     for (var idx = 0;ret && (idx < len);idx++) {
501       var next = this.nextObject(idx, last, context);
502       if (!callback.call(target, next, idx, this)) ret = NO;
503       last = next;
504     }
505     last = null;
506     context = SC.Enumerator._pushContext(context);
507     return ret;
508   },
509 
510   /**
511     Returns YES if the passed property resolves to true for all items in the
512     enumerable.  This method is often simpler/faster than using a callback.
513 
514     @param {String} key the property to test
515     @param {String} value optional value to test against.
516     @returns {Array} filtered array
517   */
518   everyProperty: function (key, value) {
519     var len = this.get ? this.get('length') : this.length;
520     var ret  = YES;
521     var last = null;
522     var context = SC.Enumerator._popContext();
523     for (var idx = 0;ret && (idx < len);idx++) {
524       var next = this.nextObject(idx, last, context);
525       var cur = next ? (next.get ? next.get(key) : next[key]) : null;
526       ret = (value === undefined) ? !!cur : SC.isEqual(cur, value);
527       last = next;
528     }
529     last = null;
530     context = SC.Enumerator._pushContext(context);
531     return ret;
532   },
533 
534 
535   /**
536     Returns YES if the passed function returns true for any item in the
537     enumeration. This corresponds with the every() method in JavaScript 1.6.
538 
539     The callback method you provide should have the following signature (all
540     parameters are optional):
541 
542           function (item, index, enumerable);
543 
544     - *item* is the current item in the iteration.
545     - *index* is the current index in the iteration
546     - *enumerable* is the enumerable object itself.
547 
548     It should return the YES to include the item in the results, NO otherwise.
549 
550     Note that in addition to a callback, you can also pass an optional target
551     object that will be set as "this" on the context.  This is a good way
552     to give your iterator function access to the current object.
553 
554     Usage Example:
555 
556           if (people.some(isManager)) { Paychecks.addBiggerBonus(); }
557 
558     @param {Function} callback the callback to execute
559     @param {Object} target the target object to use
560     @returns {Boolean} YES
561   */
562   some: function (callback, target) {
563     if (typeof callback !== "function") throw new TypeError();
564     var len = this.get ? this.get('length') : this.length;
565     if (target === undefined) target = null;
566 
567     var ret  = NO;
568     var last = null;
569     var context = SC.Enumerator._popContext();
570     for (var idx = 0;(!ret) && (idx < len);idx++) {
571       var next = this.nextObject(idx, last, context);
572       if (callback.call(target, next, idx, this)) ret = YES;
573       last = next;
574     }
575     last = null;
576     context = SC.Enumerator._pushContext(context);
577     return ret;
578   },
579 
580   /**
581     Returns YES if the passed property resolves to true for any item in the
582     enumerable.  This method is often simpler/faster than using a callback.
583 
584     @param {String} key the property to test
585     @param {String} value optional value to test against.
586     @returns {Boolean} YES
587   */
588   someProperty: function (key, value) {
589     var len = this.get ? this.get('length') : this.length;
590     var ret  = NO;
591     var last = null;
592     var context = SC.Enumerator._popContext();
593     for (var idx = 0; !ret && (idx < len); idx++) {
594       var next = this.nextObject(idx, last, context);
595       var cur = next ? (next.get ? next.get(key) : next[key]) : null;
596       ret = (value === undefined) ? !!cur : SC.isEqual(cur, value);
597       last = next;
598     }
599     last = null;
600     context = SC.Enumerator._pushContext(context);
601     return ret;  // return the invert
602   },
603 
604   /**
605     This will combine the values of the enumerator into a single value. It
606     is a useful way to collect a summary value from an enumeration.  This
607     corresponds to the reduce() method defined in JavaScript 1.8.
608 
609     The callback method you provide should have the following signature (all
610     parameters are optional):
611 
612           function (previousValue, item, index, enumerable);
613 
614     - *previousValue* is the value returned by the last call to the iterator.
615     - *item* is the current item in the iteration.
616     - *index* is the current index in the iteration
617     - *enumerable* is the enumerable object itself.
618 
619     Return the new cumulative value.
620 
621     In addition to the callback you can also pass an initialValue.  An error
622     will be raised if you do not pass an initial value and the enumerator is
623     empty.
624 
625     Note that unlike the other methods, this method does not allow you to
626     pass a target object to set as this for the callback.  It's part of the
627     spec. Sorry.
628 
629     @param {Function} callback the callback to execute
630     @param {Object} initialValue initial value for the reduce
631     @param {String} reducerProperty internal use only.  May not be available.
632     @returns {Object} The reduced value.
633   */
634   reduce: function (callback, initialValue, reducerProperty) {
635     if (typeof callback !== "function") throw new TypeError();
636     var len = this.get ? this.get('length') : this.length;
637 
638     // no value to return if no initial value & empty
639     if (len === 0 && initialValue === undefined) throw new TypeError();
640 
641     var ret  = initialValue;
642     var last = null;
643     var context = SC.Enumerator._popContext();
644     for (var idx = 0; idx < len;idx++) {
645       var next = this.nextObject(idx, last, context);
646 
647       // while ret is still undefined, just set the first value we get as ret.
648       // this is not the ideal behavior actually but it matches the FireFox
649       // implementation... :(
650       if (next !== null) {
651         if (ret === undefined) {
652           ret = next;
653         } else {
654           ret = callback.call(null, ret, next, idx, this, reducerProperty);
655         }
656       }
657       last = next;
658     }
659     last = null;
660     context = SC.Enumerator._pushContext(context);
661 
662     // uh oh...we never found a value!
663     if (ret === undefined) throw new TypeError();
664     return ret;
665   },
666 
667   /**
668     Invokes the named method on every object in the receiver that
669     implements it.  This method corresponds to the implementation in
670     Prototype 1.6.
671 
672     @param {String} methodName the name of the method
673     @param {Object...} args optional arguments to pass as well.
674     @returns {Array} return values from calling invoke.
675   */
676   invoke: function (methodName) {
677     var len = this.get ? this.get('length') : this.length;
678     if (len <= 0) return []; // nothing to invoke....
679 
680     var idx;
681 
682     // collect the arguments
683     var args = [];
684     var alen = arguments.length;
685     if (alen > 1) {
686       for (idx = 1; idx < alen; idx++) args.push(arguments[idx]);
687     }
688 
689     // call invoke
690     var ret = [];
691     var last = null;
692     var context = SC.Enumerator._popContext();
693     for (idx = 0; idx < len; idx++) {
694       var next = this.nextObject(idx, last, context);
695       var method = next ? next[methodName] : null;
696       if (method) ret[idx] = method.apply(next, args);
697       last = next;
698     }
699     last = null;
700     context = SC.Enumerator._pushContext(context);
701     return ret;
702   },
703 
704   /**
705     Invokes the passed method and optional arguments on the receiver elements
706     as long as the methods return value matches the target value.  This is
707     a useful way to attempt to apply changes to a collection of objects unless
708     or until one fails.
709 
710     @param {Object} targetValue the target return value
711     @param {String} methodName the name of the method
712     @param {Object...} args optional arguments to pass as well.
713     @returns {Array} return values from calling invoke.
714   */
715   invokeWhile: function (targetValue, methodName) {
716     var len = this.get ? this.get('length') : this.length;
717     if (len <= 0) return null; // nothing to invoke....
718 
719     var idx;
720 
721     // collect the arguments
722     var args = [];
723     var alen = arguments.length;
724     if (alen > 2) {
725       for (idx = 2; idx < alen; idx++) args.push(arguments[idx]);
726     }
727 
728     // call invoke
729     var ret = targetValue;
730     var last = null;
731     var context = SC.Enumerator._popContext();
732     for (idx = 0; (ret === targetValue) && (idx < len); idx++) {
733       var next = this.nextObject(idx, last, context);
734       var method = next ? next[methodName] : null;
735       if (method) ret = method.apply(next, args);
736       last = next;
737     }
738     last = null;
739     context = SC.Enumerator._pushContext(context);
740     return ret;
741   },
742 
743   /**
744     Simply converts the enumerable into a genuine array.  The order, of
745     course, is not gauranteed.  Corresponds to the method implemented by
746     Prototype.
747 
748     @returns {Array} the enumerable as an array.
749   */
750   toArray: function () {
751     var ret = [];
752     this.forEach(function (o) { ret.push(o); }, this);
753     return ret;
754   },
755 
756   /**
757     Converts an enumerable into a matrix, with inner arrays grouped based
758     on a particular property of the elements of the enumerable.
759 
760     @param {String} key the property to test
761     @returns {Array} matrix of arrays
762   */
763   groupBy: function (key) {
764     var len = this.get ? this.get('length') : this.length,
765         ret = [],
766         last = null,
767         context = SC.Enumerator._popContext(),
768         grouped = [],
769         keyValues = [],
770         idx, next, cur;
771 
772     for (idx = 0; idx < len;idx++) {
773       next = this.nextObject(idx, last, context);
774       cur = next ? (next.get ? next.get(key) : next[key]) : null;
775       if (SC.none(grouped[cur])) {
776         grouped[cur] = [];
777         keyValues.push(cur);
778       }
779       grouped[cur].push(next);
780       last = next;
781     }
782     last = null;
783     context = SC.Enumerator._pushContext(context);
784 
785     for (idx = 0, len = keyValues.length; idx < len; idx++) {
786       ret.push(grouped[keyValues[idx]]);
787     }
788     return ret;
789   }
790 
791 };
792 
793 // Build in a separate function to avoid unintentional leaks through closures...
794 SC._buildReducerFor = function (reducerKey, reducerProperty) {
795   return function (key, value) {
796     var reducer = this[reducerKey];
797     if (SC.typeOf(reducer) !== SC.T_FUNCTION) {
798       return this.unknownProperty ? this.unknownProperty(key, value) : null;
799     } else {
800       // Invoke the reduce method defined in enumerable instead of using the
801       // one implemented in the receiver.  The receiver might be a native
802       // implementation that does not support reducerProperty.
803       var ret = SC.Enumerable.reduce.call(this, reducer, null, reducerProperty);
804       return ret;
805     }
806   }.property('[]');
807 };
808 
809 /** @class */
810 SC.Reducers = /** @scope SC.Reducers.prototype */ {
811   /**
812     This property will trigger anytime the enumerable's content changes.
813     You can observe this property to be notified of changes to the enumerables
814     content.
815 
816     For plain enumerables, this property is read only.  SC.Array overrides
817     this method.
818 
819     @type SC.Array
820   */
821   '[]': function (key, value) { return this; }.property(),
822 
823   /**
824     Invoke this method when the contents of your enumerable has changed.
825     This will notify any observers watching for content changes.  If you are
826     implementing an ordered enumerable (such as an Array), also pass the
827     start and length values so that it can be used to notify range observers.
828     Passing start and length values will also ensure that the computed
829     properties `firstObject` and `lastObject` are updated.
830 
831     @param {Number} [start] start offset for the content change
832     @param {Number} [length] length of change
833     @param {Number} [deltas] if you added or removed objects, the delta change
834     @returns {Object} receiver
835   */
836   enumerableContentDidChange: function (start, length, deltas) {
837     // If the start & length are provided, we can also indicate if the firstObject
838     // or lastObject properties changed, thus making them independently observable.
839     if (!SC.none(start)) {
840       if (start === 0) this.notifyPropertyChange('firstObject');
841       if (!SC.none(length) && start + length >= this.get('length') - 1) this.notifyPropertyChange('lastObject');
842     }
843 
844     this.notifyPropertyChange('[]');
845   },
846 
847   /**
848     Call this method from your unknownProperty() handler to implement
849     automatic reduced properties.  A reduced property is a property that
850     collects its contents dynamically from your array contents.  Reduced
851     properties always begin with "@".  Getting this property will call
852     reduce() on your array with the function matching the key name as the
853     processor.
854 
855     The return value of this will be either the return value from the
856     reduced property or undefined, which means this key is not a reduced
857     property.  You can call this at the top of your unknownProperty handler
858     like so:
859 
860       unknownProperty: function (key, value) {
861         var ret = this.handleReduceProperty(key, value);
862         if (ret === undefined) {
863           // process like normal
864         }
865       }
866 
867     @param {String} key the reduce property key
868 
869     @param {Object} value a value or undefined.
870 
871     @param {Boolean} generateProperty only set to false if you do not want
872       an optimized computed property handler generated for this.  Not common.
873 
874     @returns {Object} the reduced property or undefined
875   */
876   reducedProperty: function (key, value, generateProperty) {
877 
878     if (!key || typeof key !== SC.T_STRING || key.charAt(0) !== '@') return undefined; // not a reduced property
879 
880     // get the reducer key and the reducer
881     var matches = key.match(/^@([^(]*)(\(([^)]*)\))?$/);
882     if (!matches || matches.length < 2) return undefined; // no match
883 
884     var reducerKey = matches[1]; // = 'max' if key = '@max(balance)'
885     var reducerProperty = matches[3]; // = 'balance' if key = '@max(balance)'
886     reducerKey = "reduce" + reducerKey.slice(0, 1).toUpperCase() + reducerKey.slice(1);
887     var reducer = this[reducerKey];
888 
889     // if there is no reduce function defined for this key, then we can't
890     // build a reducer for it.
891     if (SC.typeOf(reducer) !== SC.T_FUNCTION) return undefined;
892 
893     // if we can't generate the property, just run reduce
894     if (generateProperty === NO) {
895       return SC.Enumerable.reduce.call(this, reducer, null, reducerProperty);
896     }
897 
898     // ok, found the reducer.  Let's build the computed property and install
899     var func = SC._buildReducerFor(reducerKey, reducerProperty);
900     var p = this.constructor.prototype;
901 
902     if (p) {
903       p[key] = func;
904 
905       // add the function to the properties array so that new instances
906       // will have their dependent key registered.
907       var props = p._properties || [];
908       props.push(key);
909       p._properties = props;
910       this.registerDependentKey(key, '[]');
911     }
912 
913     // and reduce anyway...
914     return SC.Enumerable.reduce.call(this, reducer, null, reducerProperty);
915   },
916 
917   /**
918     Reducer for @max reduced property.
919 
920     @param {Object} previousValue The previous value in the enumerable
921     @param {Object} item The current value in the enumerable
922     @param {Number} index The index of the current item in the enumerable
923     @param {String} reducerProperty (Optional) The property in the enumerable being reduced
924 
925     @returns {Object} reduced value
926   */
927   reduceMax: function (previousValue, item, index, e, reducerProperty) {
928     if (reducerProperty && item) {
929       item = item.get ? item.get(reducerProperty) : item[reducerProperty];
930     }
931     if (previousValue === null) return item;
932     return (item > previousValue) ? item : previousValue;
933   },
934 
935   /**
936     Reduces an enumerable to the max of the items in the enumerable. If
937     reducerProperty is passed, it will reduce that property. Otherwise, it will
938     reduce the item itself.
939 
940     @param {Object} previousValue The previous value in the enumerable
941     @param {Object} item The current value in the enumerable
942     @param {Number} index The index of the current item in the enumerable
943     @param {String} reducerProperty (Optional) The property in the enumerable being reduced
944 
945     @returns {Object} reduced value
946   */
947   reduceMaxObject: function (previousItem, item, index, e, reducerProperty) {
948 
949     // get the value for both the previous and current item.  If no
950     // reducerProperty was supplied, use the items themselves.
951     var previousValue = previousItem, itemValue = item;
952     if (reducerProperty) {
953       if (item) {
954         itemValue = item.get ? item.get(reducerProperty) : item[reducerProperty];
955       }
956 
957       if (previousItem) {
958         previousValue = previousItem.get ? previousItem.get(reducerProperty) : previousItem[reducerProperty];
959       }
960     }
961     if (previousValue === null) return item;
962     return (itemValue > previousValue) ? item : previousItem;
963   },
964 
965   /**
966     Reduces an enumerable to the min of the items in the enumerable. If
967     reducerProperty is passed, it will reduce that property. Otherwise, it will
968     reduce the item itself.
969 
970     @param {Object} previousValue The previous value in the enumerable
971     @param {Object} item The current value in the enumerable
972     @param {Number} index The index of the current item in the enumerable
973     @param {String} reducerProperty (Optional) The property in the enumerable being reduced
974 
975     @returns {Object} reduced value
976   */
977   reduceMin: function (previousValue, item, index, e, reducerProperty) {
978     if (reducerProperty && item) {
979       item = item.get ? item.get(reducerProperty) : item[reducerProperty];
980     }
981     if (previousValue === null) return item;
982     return (item < previousValue) ? item : previousValue;
983   },
984 
985   /**
986     Reduces an enumerable to the max of the items in the enumerable. If
987     reducerProperty is passed, it will reduce that property. Otherwise, it will
988     reduce the item itself.
989 
990     @param {Object} previousValue The previous value in the enumerable
991     @param {Object} item The current value in the enumerable
992     @param {Number} index The index of the current item in the enumerable
993     @param {String} reducerProperty (Optional) The property in the enumerable being reduced
994 
995     @returns {Object} reduced value
996   */
997   reduceMinObject: function (previousItem, item, index, e, reducerProperty) {
998 
999     // get the value for both the previous and current item.  If no
1000     // reducerProperty was supplied, use the items themselves.
1001     var previousValue = previousItem, itemValue = item;
1002     if (reducerProperty) {
1003       if (item) {
1004         itemValue = item.get ? item.get(reducerProperty) : item[reducerProperty];
1005       }
1006 
1007       if (previousItem) {
1008         previousValue = previousItem.get ? previousItem.get(reducerProperty) : previousItem[reducerProperty];
1009       }
1010     }
1011     if (previousValue === null) return item;
1012     return (itemValue < previousValue) ? item : previousItem;
1013   },
1014 
1015   /**
1016     Reduces an enumerable to the average of the items in the enumerable. If
1017     reducerProperty is passed, it will reduce that property. Otherwise, it will
1018     reduce the item itself.
1019 
1020     @param {Object} previousValue The previous value in the enumerable
1021     @param {Object} item The current value in the enumerable
1022     @param {Number} index The index of the current item in the enumerable
1023     @param {String} reducerProperty (Optional) The property in the enumerable being reduced
1024 
1025     @returns {Object} reduced value
1026   */
1027   reduceAverage: function (previousValue, item, index, e, reducerProperty) {
1028     if (reducerProperty && item) {
1029       item = item.get ? item.get(reducerProperty) : item[reducerProperty];
1030     }
1031     var ret = (previousValue || 0) + item;
1032     var len = e.get ? e.get('length') : e.length;
1033     if (index >= len - 1) ret = ret / len; //avg after last item.
1034     return ret;
1035   },
1036 
1037   /**
1038     Reduces an enumerable to the sum of the items in the enumerable. If
1039     reducerProperty is passed, it will reduce that property. Otherwise, it will
1040     reduce the item itself.
1041 
1042     @param {Object} previousValue The previous value in the enumerable
1043     @param {Object} item The current value in the enumerable
1044     @param {Number} index The index of the current item in the enumerable
1045     @param {String} reducerProperty (Optional) The property in the enumerable being reduced
1046 
1047     @returns {Object} reduced value
1048   */
1049   reduceSum: function (previousValue, item, index, e, reducerProperty) {
1050     if (reducerProperty && item) {
1051       item = item.get ? item.get(reducerProperty) : item[reducerProperty];
1052     }
1053     return (previousValue === null) ? item : previousValue + item;
1054   }
1055 };
1056 
1057 // Apply reducers...
1058 SC.mixin(SC.Enumerable, SC.Reducers);
1059 SC.mixin(Array.prototype, SC.Reducers);
1060 Array.prototype.isEnumerable = YES;
1061 
1062 // ......................................................
1063 // ARRAY SUPPORT
1064 //
1065 
1066 // Implement the same enhancements on Array.  We use specialized methods
1067 // because working with arrays are so common.
1068 (function () {
1069 
1070   // These methods will be applied even if they already exist b/c we do it
1071   // better.
1072   var alwaysMixin = {
1073 
1074     // this is supported so you can get an enumerator.  The rest of the
1075     // methods do not use this just to squeeze every last ounce of perf as
1076     // possible.
1077     nextObject: SC.Enumerable.nextObject,
1078     enumerator: SC.Enumerable.enumerator,
1079     firstObject: SC.Enumerable.firstObject,
1080     lastObject: SC.Enumerable.lastObject,
1081     sortProperty: SC.Enumerable.sortProperty,
1082 
1083     // see above...
1084     mapProperty: function (key) {
1085       var len = this.length;
1086       var ret  = [];
1087       for (var idx = 0; idx < len; idx++) {
1088         var next = this[idx];
1089         ret[idx] = next ? (next.get ? next.get(key) : next[key]) : null;
1090       }
1091       return ret;
1092     },
1093 
1094     filterProperty: function (key, value) {
1095       var len = this.length,
1096           ret = [],
1097           idx, item, cur;
1098       // Although the code for value and no value are almost identical, we want to make as many decisions outside
1099       // the loop as possible.
1100       if (value === undefined) {
1101         for (idx = 0; idx < len; idx++) {
1102           item = this[idx];
1103           cur = item ? (item.get ? item.get(key) : item[key]) : null;
1104           if (!!cur) ret.push(item);
1105         }
1106       } else {
1107         for (idx = 0; idx < len; idx++) {
1108           item = this[idx];
1109           cur = item ? (item.get ? item.get(key) : item[key]) : null;
1110           if (SC.isEqual(cur, value)) ret.push(item);
1111         }
1112       }
1113       return ret;
1114     },
1115     //returns a matrix
1116     groupBy: function (key) {
1117       var len = this.length,
1118           ret = [],
1119           grouped = [],
1120           keyValues = [],
1121           idx, next, cur;
1122 
1123       for (idx = 0; idx < len; idx++) {
1124         next = this[idx];
1125         cur = next ? (next.get ? next.get(key) : next[key]) : null;
1126         if (SC.none(grouped[cur])) { grouped[cur] = []; keyValues.push(cur); }
1127         grouped[cur].push(next);
1128       }
1129 
1130       for (idx = 0, len = keyValues.length; idx < len; idx++) {
1131         ret.push(grouped[keyValues[idx]]);
1132       }
1133       return ret;
1134     },
1135 
1136     find: function (callback, target) {
1137       if (typeof callback !== "function") throw new TypeError();
1138       var len = this.length;
1139       if (target === undefined) target = null;
1140 
1141       var next, ret = null, found = NO;
1142       for (var idx = 0; idx < len && !found; idx++) {
1143         next = this[idx];
1144         if (found = callback.call(target, next, idx, this)) ret = next;
1145       }
1146       next = null;
1147       return ret;
1148     },
1149 
1150     findProperty: function (key, value) {
1151       var len = this.length;
1152       var next, cur, found = NO, ret = null;
1153       for (var idx = 0; idx < len && !found; idx++) {
1154         cur = (next = this[idx]) ? (next.get ? next.get(key): next[key]):null;
1155         found = (value === undefined) ? !!cur : SC.isEqual(cur, value);
1156         if (found) ret = next;
1157       }
1158       next = null;
1159       return ret;
1160     },
1161 
1162     everyProperty: function (key, value) {
1163       var len = this.length;
1164       var ret  = YES;
1165       for (var idx = 0; ret && (idx < len); idx++) {
1166         var next = this[idx];
1167         var cur = next ? (next.get ? next.get(key) : next[key]) : null;
1168         ret = (value === undefined) ? !!cur : SC.isEqual(cur, value);
1169       }
1170       return ret;
1171     },
1172 
1173     someProperty: function (key, value) {
1174       var len = this.length;
1175       var ret  = NO;
1176       for (var idx = 0; !ret && (idx < len); idx++) {
1177         var next = this[idx];
1178         var cur = next ? (next.get ? next.get(key) : next[key]) : null;
1179         ret = (value === undefined) ? !!cur : SC.isEqual(cur, value);
1180       }
1181       return ret;  // return the invert
1182     },
1183 
1184     invoke: function (methodName) {
1185       var len = this.length;
1186       if (len <= 0) return []; // nothing to invoke....
1187 
1188       var idx;
1189 
1190       // collect the arguments
1191       var args = [];
1192       var alen = arguments.length;
1193       if (alen > 1) {
1194         for (idx = 1; idx < alen; idx++) args.push(arguments[idx]);
1195       }
1196 
1197       // call invoke
1198       var ret = [];
1199       for (idx = 0; idx < len; idx++) {
1200         var next = this[idx];
1201         var method = next ? next[methodName] : null;
1202         if (method) ret[idx] = method.apply(next, args);
1203       }
1204       return ret;
1205     },
1206 
1207     invokeWhile: function (targetValue, methodName) {
1208       var len = this.length;
1209       if (len <= 0) return null; // nothing to invoke....
1210 
1211       var idx;
1212 
1213       // collect the arguments
1214       var args = [];
1215       var alen = arguments.length;
1216       if (alen > 2) {
1217         for (idx = 2; idx < alen; idx++) args.push(arguments[idx]);
1218       }
1219 
1220       // call invoke
1221       var ret = targetValue;
1222       for (idx = 0; (ret === targetValue) && (idx < len); idx++) {
1223         var next = this[idx];
1224         var method = next ? next[methodName] : null;
1225         if (method) ret = method.apply(next, args);
1226       }
1227       return ret;
1228     },
1229 
1230     toArray: function () {
1231       var len = this.length;
1232       if (len <= 0) return []; // nothing to invoke....
1233 
1234       // call invoke
1235       var ret = [];
1236       for (var idx = 0; idx < len; idx++) {
1237         var next = this[idx];
1238         ret.push(next);
1239       }
1240       return ret;
1241     },
1242 
1243     getEach: function (key) {
1244       var ret = [];
1245       var len = this.length;
1246       for (var idx = 0; idx < len; idx++) {
1247         var obj = this[idx];
1248         ret[idx] = obj ? (obj.get ? obj.get(key) : obj[key]) : null;
1249       }
1250       return ret;
1251     },
1252 
1253     setEach: function (key, value) {
1254       var len = this.length;
1255       for (var idx = 0; idx < len; idx++) {
1256         var obj = this[idx];
1257         if (obj) {
1258           if (obj.set) {
1259             obj.set(key, value);
1260           } else obj[key] = value;
1261         }
1262       }
1263       return this;
1264     }
1265 
1266   };
1267 
1268   // These methods will only be applied if they are not already defined b/c
1269   // the browser is probably getting it.
1270   var mixinIfMissing = {
1271 
1272     // QUESTION: The lack of DRY is burning my eyes [YK]
1273     forEach: function (callback, target) {
1274       if (typeof callback !== "function") throw new TypeError();
1275 
1276       // QUESTION: Is this necessary?
1277       if (target === undefined) target = null;
1278 
1279       for (var i = 0, l = this.length; i < l; i++) {
1280         var next = this[i];
1281         callback.call(target, next, i, this);
1282       }
1283       return this;
1284     },
1285 
1286     map: function (callback, target) {
1287       if (typeof callback !== "function") throw new TypeError();
1288 
1289       if (target === undefined) target = null;
1290 
1291       var ret = [];
1292       for (var i = 0, l = this.length; i < l; i++) {
1293         var next = this[i];
1294         ret[i] = callback.call(target, next, i, this);
1295       }
1296       return ret;
1297     },
1298 
1299     filter: function (callback, target) {
1300       if (typeof callback !== "function") throw new TypeError();
1301 
1302       if (target === undefined) target = null;
1303 
1304       var ret = [];
1305       for (var i = 0, l = this.length; i < l; i++) {
1306         var next = this[i];
1307         if (callback.call(target, next, i, this)) ret.push(next);
1308       }
1309       return ret;
1310     },
1311 
1312     every: function (callback, target) {
1313       if (typeof callback !== "function") throw new TypeError();
1314       var len = this.length;
1315       if (target === undefined) target = null;
1316 
1317       var ret  = YES;
1318       for (var idx = 0; ret && (idx < len); idx++) {
1319         var next = this[idx];
1320         if (!callback.call(target, next, idx, this)) ret = NO;
1321       }
1322       return ret;
1323     },
1324 
1325     some: function (callback, target) {
1326       if (typeof callback !== "function") throw new TypeError();
1327       var len = this.length;
1328       if (target === undefined) target = null;
1329 
1330       var ret  = NO;
1331       for (var idx = 0; (!ret) && (idx < len); idx++) {
1332         var next = this[idx];
1333         if (callback.call(target, next, idx, this)) ret = YES;
1334       }
1335       return ret;
1336     },
1337 
1338     reduce: function (callback, initialValue, reducerProperty) {
1339       if (typeof callback !== "function") throw new TypeError();
1340       var len = this.length;
1341 
1342       // no value to return if no initial value & empty
1343       if (len === 0 && initialValue === undefined) throw new TypeError();
1344 
1345       var ret  = initialValue;
1346       for (var idx = 0; idx < len; idx++) {
1347         var next = this[idx];
1348 
1349         // while ret is still undefined, just set the first value we get as
1350         // ret. this is not the ideal behavior actually but it matches the
1351         // FireFox implementation... :(
1352         if (next !== null) {
1353           if (ret === undefined) {
1354             ret = next;
1355           } else {
1356             ret = callback.call(null, ret, next, idx, this, reducerProperty);
1357           }
1358         }
1359       }
1360 
1361       // uh oh...we never found a value!
1362       if (ret === undefined) throw new TypeError();
1363       return ret;
1364     }
1365   };
1366 
1367   // Apply methods if missing...
1368   for (var key in mixinIfMissing) {
1369     if (!mixinIfMissing.hasOwnProperty(key)) continue;
1370 
1371     // The mixinIfMissing methods should be applied if they are not defined.
1372     // If Prototype 1.6 is included, some of these methods will be defined
1373     // already, but we want to override them anyway in this special case
1374     // because our version is faster and functionally identical.
1375     if (!Array.prototype[key] || ((typeof Prototype === 'object') && Prototype.Version.match(/^1\.6/))) {
1376       Array.prototype[key] = mixinIfMissing[key];
1377     }
1378   }
1379 
1380   // Apply other methods...
1381   SC.mixin(Array.prototype, alwaysMixin);
1382 
1383 })();
1384 
1385