<?xml version="1.0" encoding="UTF-8"?>
<commit>
  <added type="array"/>
  <modified type="array">
    <modified>
      <diff>@@ -49,13 +49,12 @@ SC.BENCHMARK_OBJECTS = NO;
 
 */
 SC.Object = function(noinit) { 
-	if (noinit === SC.Object._noinit_) return ;
+	if (noinit === SC.Object._noinit_) return this ;
 	var ret = SC.Object._init.apply(this,SC.$A(arguments)) ;
   return ret ;
 };
 
-Object.extend(SC.Object, 
-/** @scope SC.Object */ {
+SC.mixin(SC.Object, /** @scope SC.Object */ {
 
 	_noinit_: '__noinit__',
 	
@@ -86,7 +85,7 @@ Object.extend(SC.Object,
      
     // build function.  copy class methods on to it.
     var ret = function(noinit) { 
-      if (noinit &amp;&amp; (typeof(noinit) == 'string') &amp;&amp; (noinit == SC.Object._noinit_)) return ;
+      if (noinit &amp;&amp; (typeof(noinit) == 'string') &amp;&amp; (noinit == SC.Object._noinit_)) return this ;
       var ret = SC.Object._init.apply(this,SC.$A(arguments)); 
       return ret ;
     };</diff>
      <filename>foundation/object.js</filename>
    </modified>
    <modified>
      <diff>@@ -440,10 +440,11 @@ SC.Enumerable = {
     spec. Sorry.
     
     @params callback {Function} the callback to execute
-    @params target {Object} the target object to use
+    @params initialValue {Object} initial value for the reduce
+    @params reducerProperty {String} internal use only.  May not be available.
     @returns {Array} A filtered array.
   */
-  reduce: function(callback, initialValue) {
+  reduce: function(callback, initialValue, reducerProperty) {
     if (typeof callback !== &quot;function&quot;) throw new TypeError() ;
     var len = (this.get) ? this.get('length') : this.length ;
 
@@ -463,7 +464,7 @@ SC.Enumerable = {
         if (ret === undefined) {
           ret = next ;
         } else {
-          ret = callback.call(null, ret, next, idx, this);
+          ret = callback.call(null, ret, next, idx, this, reducerProperty);
         }
       }
       last = next ;
@@ -579,13 +580,16 @@ SC.Enumerable = {
 } ;
 
 // Build in a separate function to avoid unintential leaks through closures...
-SC._buildReducerFor = function(reducerKey) {
+SC._buildReducerFor = function(reducerKey, reducerProperty) {
   return function(key, value) {
     var reducer = this[reducerKey] ;
     if (SC.typeOf(reducer) !== T_FUNCTION) {
       return (this.unknownProperty) ? this.unknownProperty(key, value) : null;
     } else {
-      return this.reduce(reducer, null) ;
+      // Invoke the reduce method defined in enumerable instead of using the
+      // one implemented in the receiver.  The receiver might be a native 
+      // implementation that does not support reducerProperty.
+      return SC.Enumerable.reduce.call(this, reducer, null, reducerProperty) ;
     }
   }.property('[]') ;
 };
@@ -645,7 +649,12 @@ SC.Reducers = {
     if (key[0] !== '@') return undefined ; // not a reduced property
     
     // get the reducer key and the reducer
-    var reducerKey = &quot;reduce%@&quot;.fmt(key.slice(1,key.length).capitalize()) ; 
+    var matches = key.match(/^@([^(]*)(\(([^)]*)\))?$/) ;
+    if (!matches || matches.length &lt; 2) return undefined ; // no match
+    
+    var reducerKey = matches[1]; // = 'max' if key = '@max(balance)'
+    var reducerProperty = matches[3] ; // = 'balance' if key = '@max(balance)'
+    var reducerKey = &quot;reduce%@&quot;.fmt(reducerKey.capitalize()) ; 
     var reducer = this[reducerKey] ;
 
     // if there is no reduce function defined for this key, then we can't 
@@ -653,40 +662,94 @@ SC.Reducers = {
     if (SC.typeOf(reducer) !== T_FUNCTION) return undefined;
     
     // if we can't generate the property, just run reduce
-    if (generateProperty === NO) return this.reduce(reducer, null) ;
+    if (generateProperty === NO) {
+      return SC.Enumerable.reduce.call(this, reducer, null, reducerProperty) ;
+    }
 
     // ok, found the reducer.  Let's build the computed property and install
-    var func = SC._buildReducerFor(reducerKey);
-    var p = this.constructor.prototype ;
+    var func = SC._buildReducerFor(reducerKey, reducerProperty);
+    var p = (this._type) ? this._type : this.constructor.prototype ;
+    
     if (p) {
       p[key] = func ;
       this.registerDependentKey(key, '[]') ;
     }
     
     // and reduce anyway...
-    return this.reduce(reducer, null) ;
+    return SC.Enumerable.reduce.call(this, reducer, null, reducerProperty) ;
   },
   
   /** 
     Reducer for @max reduced property.
   */
-  reduceMax: function(previousValue, item, index, enumerable) {
+  reduceMax: function(previousValue, item, index, e, reducerProperty) {
+    if (reducerProperty &amp;&amp; item) {
+      item = (item.get) ? item.get(reducerProperty) : item[reducerProperty];
+    }
     if (previousValue == null) return item ;
     return (item &gt; previousValue) ? item : previousValue ;
   },
 
   /** 
+    Reducer for @maxObject reduced property.
+  */
+  reduceMaxObject: function(previousItem, item, index, e, reducerProperty) {
+    
+    // get the value for both the previous and current item.  If no
+    // reducerProperty was supplied, use the items themselves.
+    var previousValue = previousItem, itemValue = item ;
+    if (reducerProperty) {
+      if (item) {
+        itemValue = (item.get) ? item.get(reducerProperty) : item[reducerProperty] ;
+      }
+      
+      if (previousItem) {
+        previousItemValue = (previousItem.get) ? previousItem.get(reducerProperty) : previousItem[reducerProperty] ;
+      }
+    }
+    if (previousValue == null) return item ;
+    return (itemValue &gt; previousValue) ? item : previousItem ;
+  },
+
+  /** 
     Reducer for @min reduced property.
   */
-  reduceMin: function(previousValue, item, index, enumerable) {
+  reduceMin: function(previousValue, item, index, e, reducerProperty) {
+    if (reducerProperty &amp;&amp; item) {
+      item = (item.get) ? item.get(reducerProperty) : item[reducerProperty];
+    }
     if (previousValue == null) return item ;
     return (item &lt; previousValue) ? item : previousValue ;
   },
 
   /** 
+    Reducer for @maxObject reduced property.
+  */
+  reduceMinObject: function(previousItem, item, index, e, reducerProperty) {
+
+    // get the value for both the previous and current item.  If no
+    // reducerProperty was supplied, use the items themselves.
+    var previousValue = previousItem, itemValue = item ;
+    if (reducerProperty) {
+      if (item) {
+        itemValue = (item.get) ? item.get(reducerProperty) : item[reducerProperty] ;
+      }
+      
+      if (previousItem) {
+        previousItemValue = (previousItem.get) ? previousItem.get(reducerProperty) : previousItem[reducerProperty] ;
+      }
+    }
+    if (previousValue == null) return item ;
+    return (itemValue &lt; previousValue) ? item : previousItem ;
+  },
+
+  /** 
     Reducer for @average reduced property.
   */
-  reduceAverage: function(previousValue, item, index, e) {
+  reduceAverage: function(previousValue, item, index, e, reducerProperty) {
+    if (reducerProperty &amp;&amp; item) {
+      item = (item.get) ? item.get(reducerProperty) : item[reducerProperty];
+    }
     var ret = (previousValue || 0) + item ;
     var len = (e.get) ? e.get('length') : e.length;
     if (index &gt;= len-1) ret = ret / len; //avg after last item.
@@ -696,7 +759,10 @@ SC.Reducers = {
   /** 
     Reducer for @sum reduced property.
   */
-  reduceSum: function(previousValue, item, index, e) {
+  reduceSum: function(previousValue, item, index, e, reducerProperty) {
+    if (reducerProperty &amp;&amp; item) {
+      item = (item.get) ? item.get(reducerProperty) : item[reducerProperty];
+    }
     return (previousValue == null) ? item : previousValue + item ;
   }
 } ;
@@ -894,7 +960,7 @@ SC.mixin(Array.prototype, SC.Reducers) ;
       return ret ;
     },
 
-    reduce: function(callback, initialValue) {
+    reduce: function(callback, initialValue, reducerProperty) {
       if (typeof callback !== &quot;function&quot;) throw new TypeError() ;
       var len = this.length ;
 
@@ -912,7 +978,7 @@ SC.mixin(Array.prototype, SC.Reducers) ;
           if (ret === undefined) {
             ret = next ;
           } else {
-            ret = callback.call(null, ret, next, idx, this);
+            ret = callback.call(null, ret, next, idx, this, reducerProperty);
           }
         }
       }</diff>
      <filename>mixins/enumerable.js</filename>
    </modified>
    <modified>
      <diff>@@ -603,6 +603,13 @@ SC.Observable = {
 
     // otherwise, bind as a normal property
     } else {      
+      
+      // if you add an observer beginning with '@', then we might need to 
+      // create or register the property...
+      if (this.reducedProperty &amp;&amp; (key.charAt(0) === '@')) {
+        this.reducedProperty(key, undefined) ; // create if needed...
+      }
+      
       var observers = kvo.observers[key] = (kvo.observers[key] || []) ;
       var found = false; var loc = observers.length;
       while(!found &amp;&amp; --loc &gt;= 0) found = (observers[loc] == func) ;</diff>
      <filename>mixins/observable.js</filename>
    </modified>
    <modified>
      <diff>@@ -233,6 +233,91 @@ var EnumerableTests = {
   &quot;invokeWhile should call method on member objects until return does not match&quot;: function() {
     var ret = src.invokeWhile(&quot;OK&quot;, &quot;invokeWhileTest&quot;, &quot;item2&quot;) ;
     assertEqual(&quot;FAIL&quot;, ret, &quot;return value&quot;);
+  },
+  
+  &quot;get @min(balance) should return the minimum balance&quot;: function() {
+    assertEqual(1, src.get('@min(balance)')) ;
+  },
+  
+  &quot;get @max(balance) should return the maximum balance&quot;: function() {
+    assertEqual(4, src.get('@max(balance)')) ;
+  },
+  
+  &quot;get @minObject(balance) should return the record with min balance&quot;: function() {
+    assertEqual(src.objectAt(0), src.get('@minObject(balance)')) ;
+  },
+  
+  &quot;get @maxObject(balance) should return the record with the max balance&quot;: function() {
+    assertEqual(src.objectAt(0), src.get('@maxObject(balance)')) ;
+  },
+  
+  &quot;get @sum(balance) should return the sum of the balances.&quot;: function() {
+    assertEqual(1+2+3+4, src.get(&quot;@sum(balance)&quot;)) ;
+  },
+  
+  &quot;get @average(balance) should return the average of balances&quot;: function() {
+    assertEqual((1+2+3+4)/4, src.get(&quot;@average(balance)&quot;)) ;
+  },
+  
+  &quot;should invoke custom reducer&quot;: function() {
+    // install reducer method
+    src.reduceTest = reduceTestFunc ;
+    assertEqual(&quot;TEST&quot;, src.get(&quot;@test&quot;)) ;
+    assertEqual(&quot;prop&quot;, src.get(&quot;@test(prop)&quot;)) ;
+  },
+  &quot;should trigger observer of reduced prop when array changes once property retrieved once&quot;: function() {
+
+    // get the property...this will install the reducer property...
+    src.get(&quot;@max(balance)&quot;) ;
+    
+    // install observer
+    var observedValue = null ;
+    src.addObserver(&quot;@max(balance)&quot;, function() { 
+      observedValue = src.get(&quot;@max(balance)&quot;);
+    }) ;
+    
+    src.addProbe('[]') ;
+    src.addProbe('@max(balance)');
+    
+    // add record to array
+    src.pushObject({ 
+      first: &quot;John&quot;, 
+      gender: &quot;male&quot;, 
+      californian: NO, 
+      ready: YES, 
+      visited: &quot;Paris&quot;, 
+      balance: 5
+    }) ;
+    
+    //SC.NotificationQueue.flush() ; // force observers to trigger
+    
+    // observed value should now be set because the reduced property observer
+    // was triggered when we changed the array contents.
+    assertEqual(5, observedValue) ;
+  },
+  
+  &quot;should trigger observer of reduced prop when array changes - even if you never retrieved the property before&quot;: function() {
+    // install observer
+    var observedValue = null ;
+    src.addObserver(&quot;@max(balance)&quot;, function() { 
+      observedValue = src.get(&quot;@max(balance)&quot;);
+    }) ;
+    
+    // add record to array
+    src.pushObject({ 
+      first: &quot;John&quot;, 
+      gender: &quot;male&quot;, 
+      californian: NO, 
+      ready: YES, 
+      visited: &quot;Paris&quot;, 
+      balance: 5
+    }) ;
+    
+    //SC.NotificationQueue.flush() ; // force observers to trigger
+    
+    // observed value should now be set because the reduced property observer
+    // was triggered when we changed the array contents.
+    assertEqual(5, observedValue) ;
   }
   
 }; 
@@ -245,12 +330,30 @@ var DummyEnumerable = SC.Object.extend(SC.Enumerable, {
   
   objectAt: function(idx) { return this.content[idx]; },
   
-  nextObject: function(idx) { return this.content[idx]; }
+  nextObject: function(idx) { return this.content[idx]; },
+  
+  // add support for reduced properties.
+  unknownProperty: function(key, value) {
+    var ret = this.reducedProperty(key, value) ;
+    if (ret === undefined) {
+      if (value !== undefined) this[key] = value ;
+      ret = value ;
+    }
+    return ret ;
+  },
+  
+  pushObject: function(object) {
+    this.content.push(object) ;
+    this.enumerableContentDidChange() ;
+  }
+  
 }) ;
 
 var runFunc = function(a,b) { return ['DONE', a, b]; } ;
 var invokeWhileOK = function() { return &quot;OK&quot;; } ;
 var invokeWhileNotOK = function() { return &quot;FAIL&quot;; };
+var reduceTestFunc = function(prev, item, idx, e, pname) { return pname||'TEST'; } ;
+
 var CommonArray = [
   { first: &quot;Charles&quot;, 
     gender: &quot;male&quot;, 
@@ -259,7 +362,8 @@ var CommonArray = [
     visited: &quot;Prague&quot;, 
     doneTravelling: NO, 
     run: runFunc,
-    invokeWhileTest: invokeWhileOK 
+    invokeWhileTest: invokeWhileOK,
+    balance: 1
   },
 
   { first: &quot;Jenna&quot;, 
@@ -269,7 +373,8 @@ var CommonArray = [
     visited: &quot;Prague&quot;, 
     doneTravelling: NO, 
     run: runFunc,
-    invokeWhileTest: invokeWhileOK 
+    invokeWhileTest: invokeWhileOK,
+    balance: 2 
   },
   
   { first: &quot;Peter&quot;, 
@@ -279,8 +384,10 @@ var CommonArray = [
     visited: &quot;Prague&quot;, 
     doneTravelling: NO, 
     run: runFunc,
-    invokeWhileTest: invokeWhileNotOK 
+    invokeWhileTest: invokeWhileNotOK,
+    balance: 3 
   },
+  
   { first: &quot;Chris&quot;, 
     gender: &quot;male&quot;, 
     californian: NO, 
@@ -288,12 +395,13 @@ var CommonArray = [
     visited: &quot;Prague&quot;, 
     doneTravelling: NO, 
     run: runFunc,
-    invokeWhileTest: invokeWhileOK 
+    invokeWhileTest: invokeWhileOK,
+    balance: 4
   } ] ;
 
 Test.context(&quot;Real Array&quot;, SC.mixin(EnumerableTests, {
   
-  setup: function() { src = CommonArray; },
+  setup: function() { src = CommonArray.clone(); },
   
   teardown: function() { src = null ; }
 </diff>
      <filename>tests/mixins/enumerable.rhtml</filename>
    </modified>
  </modified>
  <removed type="array"/>
  <parents type="array">
    <parent>
      <id>543198e3102768087b074a71ceb2ec71da0ad760</id>
    </parent>
  </parents>
  <author>
    <name>Charles Jolley</name>
    <email>charles@sproutit.com</email>
  </author>
  <url>http://github.com/sproutit/sproutcore/commit/c3564ee7f5fbeec6d73064f998e1787544da955b</url>
  <id>c3564ee7f5fbeec6d73064f998e1787544da955b</id>
  <committed-date>2008-08-09T18:57:35-07:00</committed-date>
  <authored-date>2008-08-09T18:57:35-07:00</authored-date>
  <message>Add unit tests for reduced properties and fix bug they found</message>
  <tree>7eed598d11814ead50793b55c2dcd677e057460f</tree>
  <committer>
    <name>Charles Jolley</name>
    <email>charles@sproutit.com</email>
  </committer>
</commit>
