#

API Docs for: v2.11.1
Show: Inherited Protected Private Deprecated

# File: packages/ember-runtime/lib/computed/reduce_computed_macros.js

    /**
    @module ember
    @submodule ember-runtime
    */

    import { EmptyObject, guidFor } from 'ember-utils';
    import {
      assert,
      get,
      Error as EmberError,
      ComputedProperty,
      computed,
      addObserver,
      removeObserver,
      isNone,
      getProperties,
      WeakMap
    } from 'ember-metal';
    import compare from '../compare';
    import { isArray } from '../utils';
    import { A as emberA } from '../system/native_array';

    function reduceMacro(dependentKey, callback, initialValue) {
      return computed(`${dependentKey}.[]`, function() {
        let arr = get(this, dependentKey);

        if (arr === null || typeof arr !== 'object') { return initialValue; }

        return arr.reduce((previousValue, currentValue, index, array) => {
          return callback.call(this, previousValue, currentValue, index, array);
        }, initialValue);
      }).readOnly();
    }

    function arrayMacro(dependentKey, callback) {
      // This is a bit ugly
      let propertyName;
      if (/@each/.test(dependentKey)) {
        propertyName = dependentKey.replace(/\.@each.*$/, '');
      } else {
        propertyName = dependentKey;
        dependentKey += '.[]';
      }

      return computed(dependentKey, function() {
        let value = get(this, propertyName);
        if (isArray(value)) {
          return emberA(callback.call(this, value));
        } else {
          return emberA();
        }
      }).readOnly();
    }

    function multiArrayMacro(dependentKeys, callback) {
      let args = dependentKeys.map(key => `${key}.[]`);

      args.push(function() {
        return emberA(callback.call(this, dependentKeys));
      });

      return computed.apply(this, args).readOnly();
    }

    /**
      A computed property that returns the sum of the values
      in the dependent array.

      @method sum
      @for Ember.computed
      @param {String} dependentKey
      @return {Ember.ComputedProperty} computes the sum of all values in the dependentKey's array
      @since 1.4.0
      @public
    */
    export function sum(dependentKey) {
      return reduceMacro(dependentKey, (sum, item) => sum + item, 0);
    }

    /**
      A computed property that calculates the maximum value in the
      dependent array. This will return `-Infinity` when the dependent
      array is empty.

      ```javascript
      let Person = Ember.Object.extend({
        childAges: Ember.computed.mapBy('children', 'age'),
        maxChildAge: Ember.computed.max('childAges')
      });

      let lordByron = Person.create({ children: [] });

      lordByron.get('maxChildAge'); // -Infinity
      lordByron.get('children').pushObject({
        name: 'Augusta Ada Byron', age: 7
      });
      lordByron.get('maxChildAge'); // 7
      lordByron.get('children').pushObjects([{
        name: 'Allegra Byron',
        age: 5
      }, {
        name: 'Elizabeth Medora Leigh',
        age: 8
      }]);
      lordByron.get('maxChildAge'); // 8
      ```

      If the types of the arguments are not numbers,
      they will be converted to numbers and the type
      of the return value will always be `Number`.
      For example, the max of a list of Date objects will be
      the highest timestamp as a `Number`.
      This behavior is consistent with `Math.max`.

      @method max
      @for Ember.computed
      @param {String} dependentKey
      @return {Ember.ComputedProperty} computes the largest value in the dependentKey's array
      @public
    */
    export function max(dependentKey) {
      return reduceMacro(dependentKey, (max, item) => Math.max(max, item), -Infinity);
    }

    /**
      A computed property that calculates the minimum value in the
      dependent array. This will return `Infinity` when the dependent
      array is empty.

      ```javascript
      let Person = Ember.Object.extend({
        childAges: Ember.computed.mapBy('children', 'age'),
        minChildAge: Ember.computed.min('childAges')
      });

      let lordByron = Person.create({ children: [] });

      lordByron.get('minChildAge'); // Infinity
      lordByron.get('children').pushObject({
        name: 'Augusta Ada Byron', age: 7
      });
      lordByron.get('minChildAge'); // 7
      lordByron.get('children').pushObjects([{
        name: 'Allegra Byron',
        age: 5
      }, {
        name: 'Elizabeth Medora Leigh',
        age: 8
      }]);
      lordByron.get('minChildAge'); // 5
      ```

      If the types of the arguments are not numbers,
      they will be converted to numbers and the type
      of the return value will always be `Number`.
      For example, the min of a list of Date objects will be
      the lowest timestamp as a `Number`.
      This behavior is consistent with `Math.min`.

      @method min
      @for Ember.computed
      @param {String} dependentKey
      @return {Ember.ComputedProperty} computes the smallest value in the dependentKey's array
      @public
    */
    export function min(dependentKey) {
      return reduceMacro(dependentKey, (min, item) => Math.min(min, item), Infinity);
    }

    /**
      Returns an array mapped via the callback

      The callback method you provide should have the following signature.
      `item` is the current item in the iteration.
      `index` is the integer index of the current item in the iteration.

      ```javascript
      function(item, index);
      ```

      Example

      ```javascript
      let Hamster = Ember.Object.extend({
        excitingChores: Ember.computed.map('chores', function(chore, index) {
          return chore.toUpperCase() + '!';
        })
      });

      let hamster = Hamster.create({
        chores: ['clean', 'write more unit tests']
      });

      hamster.get('excitingChores'); // ['CLEAN!', 'WRITE MORE UNIT TESTS!']
      ```

      @method map
      @for Ember.computed
      @param {String} dependentKey
      @param {Function} callback
      @return {Ember.ComputedProperty} an array mapped via the callback
      @public
    */
    export function map(dependentKey, callback) {
      return arrayMacro(dependentKey, function(value) {
        return value.map(callback, this);
      });
    }

    /**
      Returns an array mapped to the specified key.

      ```javascript
      let Person = Ember.Object.extend({
        childAges: Ember.computed.mapBy('children', 'age')
      });

      let lordByron = Person.create({ children: [] });

      lordByron.get('childAges'); // []
      lordByron.get('children').pushObject({ name: 'Augusta Ada Byron', age: 7 });
      lordByron.get('childAges'); // [7]
      lordByron.get('children').pushObjects([{
        name: 'Allegra Byron',
        age: 5
      }, {
        name: 'Elizabeth Medora Leigh',
        age: 8
      }]);
      lordByron.get('childAges'); // [7, 5, 8]
      ```

      @method mapBy
      @for Ember.computed
      @param {String} dependentKey
      @param {String} propertyKey
      @return {Ember.ComputedProperty} an array mapped to the specified key
      @public
    */
    export function mapBy(dependentKey, propertyKey) {
      assert(
        'Ember.computed.mapBy expects a property string for its second argument, ' +
        'perhaps you meant to use "map"',
        typeof propertyKey === 'string'
      );

      return map(`${dependentKey}.@each.${propertyKey}`, item => get(item, propertyKey));
    }

    /**
      Filters the array by the callback.

      The callback method you provide should have the following signature.
      `item` is the current item in the iteration.
      `index` is the integer index of the current item in the iteration.
      `array` is the dependant array itself.

      ```javascript
      function(item, index, array);
      ```

      ```javascript
      let Hamster = Ember.Object.extend({
        remainingChores: Ember.computed.filter('chores', function(chore, index, array) {
          return !chore.done;
        })
      });

      let hamster = Hamster.create({
        chores: [
          { name: 'cook', done: true },
          { name: 'clean', done: true },
          { name: 'write more unit tests', done: false }
        ]
      });

      hamster.get('remainingChores'); // [{name: 'write more unit tests', done: false}]
      ```

      @method filter
      @for Ember.computed
      @param {String} dependentKey
      @param {Function} callback
      @return {Ember.ComputedProperty} the filtered array
      @public
    */
    export function filter(dependentKey, callback) {
      return arrayMacro(dependentKey, function(value) {
        return value.filter(callback, this);
      });
    }

    /**
      Filters the array by the property and value

      ```javascript
      let Hamster = Ember.Object.extend({
        remainingChores: Ember.computed.filterBy('chores', 'done', false)
      });

      let hamster = Hamster.create({
        chores: [
          { name: 'cook', done: true },
          { name: 'clean', done: true },
          { name: 'write more unit tests', done: false }
        ]
      });

      hamster.get('remainingChores'); // [{ name: 'write more unit tests', done: false }]
      ```

      @method filterBy
      @for Ember.computed
      @param {String} dependentKey
      @param {String} propertyKey
      @param {*} value
      @return {Ember.ComputedProperty} the filtered array
      @public
    */
    export function filterBy(dependentKey, propertyKey, value) {
      let callback;

      if (arguments.length === 2) {
        callback = (item) => get(item, propertyKey);
      } else {
        callback = (item) => get(item, propertyKey) === value;
      }

      return filter(`${dependentKey}.@each.${propertyKey}`, callback);
    }

    /**
      A computed property which returns a new array with all the unique
      elements from one or more dependent arrays.

      Example

      ```javascript
      let Hamster = Ember.Object.extend({
        uniqueFruits: Ember.computed.uniq('fruits')
      });

      let hamster = Hamster.create({
        fruits: [
          'banana',
          'grape',
          'kale',
          'banana'
        ]
      });

      hamster.get('uniqueFruits'); // ['banana', 'grape', 'kale']
      ```

      @method uniq
      @for Ember.computed
      @param {String} propertyKey*
      @return {Ember.ComputedProperty} computes a new array with all the
      unique elements from the dependent array
      @public
    */
    export function uniq(...args) {
      return multiArrayMacro(args, function(dependentKeys) {
        let uniq = emberA();

        dependentKeys.forEach(dependentKey => {
          let value = get(this, dependentKey);
          if (isArray(value)) {
            value.forEach(item => {
              if (uniq.indexOf(item) === -1) {
                uniq.push(item);
              }
            });
          }
        });

        return uniq;
      });
    }

    /**
      A computed property which returns a new array with all the unique
      elements from an array, with uniqueness determined by specific key.

      Example

      ```javascript
      let Hamster = Ember.Object.extend({
        uniqueFruits: Ember.computed.uniqBy('fruits', 'id')
      });
      let hamster = Hamster.create({
        fruits: [
          { id: 1, 'banana' },
          { id: 2, 'grape' },
          { id: 3, 'peach' },
          { id: 1, 'banana' }
        ]
      });
      hamster.get('uniqueFruits'); // [ { id: 1, 'banana' }, { id: 2, 'grape' }, { id: 3, 'peach' }]
      ```

      @method uniqBy
      @for Ember.computed
      @param {String} dependentKey
      @param {String} propertyKey
      @return {Ember.ComputedProperty} computes a new array with all the
      unique elements from the dependent array
      @public
    */
    export function uniqBy(dependentKey, propertyKey) {
      return computed(`${dependentKey}.[]`, function() {
        let uniq = emberA();
        let seen = new EmptyObject();
        let list = get(this, dependentKey);
        if (isArray(list)) {
          list.forEach(item => {
            let guid = guidFor(get(item, propertyKey));
            if (!(guid in seen)) {
              seen[guid] = true;
              uniq.push(item);
            }
          });
        }
        return uniq;
      }).readOnly();
    }

    /**
      Alias for [Ember.computed.uniq](/api/#method_computed_uniq).

      @method union
      @for Ember.computed
      @param {String} propertyKey*
      @return {Ember.ComputedProperty} computes a new array with all the
      unique elements from the dependent array
      @public
    */
    export let union = uniq;

    /**
      A computed property which returns a new array with all the duplicated
      elements from two or more dependent arrays.

      Example

      ```javascript
      let obj = Ember.Object.extend({
        friendsInCommon: Ember.computed.intersect('adaFriends', 'charlesFriends')
      }).create({
        adaFriends: ['Charles Babbage', 'John Hobhouse', 'William King', 'Mary Somerville'],
        charlesFriends: ['William King', 'Mary Somerville', 'Ada Lovelace', 'George Peacock']
      });

      obj.get('friendsInCommon'); // ['William King', 'Mary Somerville']
      ```

      @method intersect
      @for Ember.computed
      @param {String} propertyKey*
      @return {Ember.ComputedProperty} computes a new array with all the
      duplicated elements from the dependent arrays
      @public
    */
    export function intersect(...args) {
      return multiArrayMacro(args, function(dependentKeys) {
        let arrays = dependentKeys.map(dependentKey => {
          let array = get(this, dependentKey);

          return isArray(array) ? array : [];
        });

        let results = arrays.pop().filter(candidate => {
          for (let i = 0; i < arrays.length; i++) {
            let found = false;
            let array = arrays[i];
            for (let j = 0; j < array.length; j++) {
              if (array[j] === candidate) {
                found = true;
                break;
              }
            }

            if (found === false) { return false; }
          }

          return true;
        });

        return emberA(results);
      });
    }

    /**
      A computed property which returns a new array with all the
      properties from the first dependent array that are not in the second
      dependent array.

      Example

      ```javascript
      let Hamster = Ember.Object.extend({
        likes: ['banana', 'grape', 'kale'],
        wants: Ember.computed.setDiff('likes', 'fruits')
      });

      let hamster = Hamster.create({
        fruits: [
          'grape',
          'kale',
        ]
      });

      hamster.get('wants'); // ['banana']
      ```

      @method setDiff
      @for Ember.computed
      @param {String} setAProperty
      @param {String} setBProperty
      @return {Ember.ComputedProperty} computes a new array with all the
      items from the first dependent array that are not in the second
      dependent array
      @public
    */
    export function setDiff(setAProperty, setBProperty) {
      if (arguments.length !== 2) {
        throw new EmberError('setDiff requires exactly two dependent arrays.');
      }

      return computed(`${setAProperty}.[]`, `${setBProperty}.[]`, function() {
        let setA = this.get(setAProperty);
        let setB = this.get(setBProperty);

        if (!isArray(setA)) { return emberA(); }
        if (!isArray(setB)) { return emberA(setA); }

        return setA.filter(x => setB.indexOf(x) === -1);
      }).readOnly();
    }

    /**
      A computed property that returns the array of values
      for the provided dependent properties.

      Example

      ```javascript
      let Hamster = Ember.Object.extend({
        clothes: Ember.computed.collect('hat', 'shirt')
      });

      let hamster = Hamster.create();

      hamster.get('clothes'); // [null, null]
      hamster.set('hat', 'Camp Hat');
      hamster.set('shirt', 'Camp Shirt');
      hamster.get('clothes'); // ['Camp Hat', 'Camp Shirt']
      ```

      @method collect
      @for Ember.computed
      @param {String} dependentKey*
      @return {Ember.ComputedProperty} computed property which maps
      values of all passed in properties to an array.
      @public
    */
    export function collect(...dependentKeys) {
      return multiArrayMacro(dependentKeys, function() {
        let properties = getProperties(this, dependentKeys);
        let res = emberA();
        for (let key in properties) {
          if (properties.hasOwnProperty(key)) {
            if (isNone(properties[key])) {
              res.push(null);
            } else {
              res.push(properties[key]);
            }
          }
        }
        return res;
      });
    }

    /**
      A computed property which returns a new array with all the
      properties from the first dependent array sorted based on a property
      or sort function.

      The callback method you provide should have the following signature:

      ```javascript
      function(itemA, itemB);
      ```

      - `itemA` the first item to compare.
      - `itemB` the second item to compare.

      This function should return negative number (e.g. `-1`) when `itemA` should come before
      `itemB`. It should return positive number (e.g. `1`) when `itemA` should come after
      `itemB`. If the `itemA` and `itemB` are equal this function should return `0`.

      Therefore, if this function is comparing some numeric values, simple `itemA - itemB` or
      `itemA.get( 'foo' ) - itemB.get( 'foo' )` can be used instead of series of `if`.

      Example

      ```javascript
      let ToDoList = Ember.Object.extend({
        // using standard ascending sort
        todosSorting: ['name'],
        sortedTodos: Ember.computed.sort('todos', 'todosSorting'),

        // using descending sort
        todosSortingDesc: ['name:desc'],
        sortedTodosDesc: Ember.computed.sort('todos', 'todosSortingDesc'),

        // using a custom sort function
        priorityTodos: Ember.computed.sort('todos', function(a, b){
          if (a.priority > b.priority) {
            return 1;
          } else if (a.priority < b.priority) {
            return -1;
          }

          return 0;
        })
      });

      let todoList = ToDoList.create({todos: [
        { name: 'Unit Test', priority: 2 },
        { name: 'Documentation', priority: 3 },
        { name: 'Release', priority: 1 }
      ]});

      todoList.get('sortedTodos');      // [{ name:'Documentation', priority:3 }, { name:'Release', priority:1 }, { name:'Unit Test', priority:2 }]
      todoList.get('sortedTodosDesc');  // [{ name:'Unit Test', priority:2 }, { name:'Release', priority:1 }, { name:'Documentation', priority:3 }]
      todoList.get('priorityTodos');    // [{ name:'Release', priority:1 }, { name:'Unit Test', priority:2 }, { name:'Documentation', priority:3 }]
      ```

      @method sort
      @for Ember.computed
      @param {String} itemsKey
      @param {String or Function} sortDefinition a dependent key to an
      array of sort properties (add `:desc` to the arrays sort properties to sort descending) or a function to use when sorting
      @return {Ember.ComputedProperty} computes a new sorted array based
      on the sort property array or callback function
      @public
    */
    export function sort(itemsKey, sortDefinition) {
      assert(
        'Ember.computed.sort requires two arguments: an array key to sort and ' +
        'either a sort properties key or sort function',
        arguments.length === 2
      );

      if (typeof sortDefinition === 'function') {
        return customSort(itemsKey, sortDefinition);
      } else {
        return propertySort(itemsKey, sortDefinition);
      }
    }

    function customSort(itemsKey, comparator) {
      return arrayMacro(itemsKey, function(value) {
        return value.slice().sort((x, y) => comparator.call(this, x, y));
      });
    }

    // This one needs to dynamically set up and tear down observers on the itemsKey
    // depending on the sortProperties
    function propertySort(itemsKey, sortPropertiesKey) {
      let cp = new ComputedProperty(function(key) {
        let itemsKeyIsAtThis = (itemsKey === '@this');
        let sortProperties = get(this, sortPropertiesKey);

        assert(
          `The sort definition for '${key}' on ${this} must be a function or an array of strings`,
          isArray(sortProperties) && sortProperties.every(s => typeof s === 'string')
        );

        let normalizedSortProperties = normalizeSortProperties(sortProperties);

        // Add/remove property observers as required.
        let activeObserversMap = cp._activeObserverMap || (cp._activeObserverMap = new WeakMap());
        let activeObservers = activeObserversMap.get(this);

        if (activeObservers) {
          activeObservers.forEach(args => removeObserver.apply(null, args));
        }

        function sortPropertyDidChange() {
          this.notifyPropertyChange(key);
        }

        activeObservers = normalizedSortProperties.map(([prop]) => {
          let path = itemsKeyIsAtThis ? `@each.${prop}` : `${itemsKey}.@each.${prop}`;
          let args = [this, path, sortPropertyDidChange];
          addObserver.apply(null, args);
          return args;
        });

        activeObserversMap.set(this, activeObservers);

        // Sort and return the array.
        let items = itemsKeyIsAtThis ? this : get(this, itemsKey);

        if (isArray(items)) {
          return sortByNormalizedSortProperties(items, normalizedSortProperties);
        } else {
          return emberA();
        }
      });

      cp._activeObserverMap = undefined;

      return cp.property(`${sortPropertiesKey}.[]`).readOnly();
    }

    function normalizeSortProperties(sortProperties) {
      return sortProperties.map(p => {
        let [prop, direction] = p.split(':');
        direction = direction || 'asc';

        return [prop, direction];
      });
    }

    function sortByNormalizedSortProperties(items, normalizedSortProperties) {
      return emberA(items.slice().sort((itemA, itemB) => {
        for (let i = 0; i < normalizedSortProperties.length; i++) {
          let [prop, direction] = normalizedSortProperties[i];
          let result = compare(get(itemA, prop), get(itemB, prop));
          if (result !== 0) {
            return (direction === 'desc') ? (-1 * result) : result;
          }
        }

        return 0;
      }));
    }