API Docs for:
Show:

File: yui3-gallery/src/gallery-weighted-list/js/weighted-list.js

/**
 * @module gallery-weighted-list
 */
(function (Y) {
    'use strict';

    var _Alea = Y.Alea,
        _Array = Y.Array,
        _Math = Math,

        _each = _Array.each,
        _floor = _Math.floor,
        _isFunction = Y.Lang.isFunction,
        _iterate = _Array.iterate,
        _map = _Array.map,
        _random = _Alea ? new _Alea().random : _Math.random,
        _reduce = _Array.reduce,
        _some = _Array.some,

        /**
         * @class WeightedList
         * @constructor
         */
        _class = function () {
            this._array = [];
        };

    _class.prototype = {
        /**
         * Add a value to the weighted list.
         * @method add
         * @param {Any} value
         * @param {Number} [weight] Optional.  Defaults to 1.
         * @return {Number} The index of the item that was added.
         */
        add: function (value, weight) {
            var me = this,

                array = me._array;

            if (!weight && weight !== 0) {
                weight = 1;
            }

            array.push({
                value: value,
                weight: weight
            });

            delete me._sum;
            delete me._totals;

            return array.length - 1;
        },
        /**
         * Dedupes a weighted list of string values, returning a new weighted
         * list that is guaranteed to contain only one copy of a given string
         * value.  This method differs from the unique method in that it's
         * optimized for use only with string values, whereas unique may be used
         * with other types (but is slower).  Using dedupe with non-string
         * values may result in unexpected behavior.
         * @method dedupe
         * @param {String} [mode] Optional.  If the original weighted list contains
         * duplicate values with different weights, the mode specifies how those
         * weights get transferred to the new weighted list.  mode may be one of
         * the following values:
         * <dl>
         *     <dt>
         *         'first'
         *     </dt>
         *     <dd>
         *         Use the first weight that is found.  Ignore all others.
         *     </dd>
         *     <dt>
         *         'sum'
         *     </dt>
         *     <dd>
         *         Use the sum of all weights that are found.  This is the
         *         default mode.
         *     </dd>
         * </dl>
         * @return {WeightedList}
         */
        dedupe: function (mode) {
            var array = this._array,
                i,
                index,
                item,
                itemValue,
                length = array.length,
                object = {},
                other = new _class();

            if (!mode) {
                mode = 'sum';
            }

            for (i = 0; i < length; i += 1) {
                item = array[i];
                itemValue = item.value;

                if (!object.hasOwnProperty(itemValue)) {
                    object[itemValue] = other.add(itemValue, item.weight);
                } else if (mode === 'sum') {
                    index = object[itemValue];
                    other.updateWeight(index, other.weight(index) + item.weight);
                }
            }

            return other;
        },
        /**
         * Executes the supplied function for each value in the weighted list.
         * @method each
         * @chainable
         * @param {Function} fn The function to execute for each value in the
         * weighted list.  The first argument passed to this function will be
         * the value.  The second argument passed to this function will be the
         * value's index.  The third argument passed to this function will be
         * the value's weight.
         * @param {Any} [context] Optional.  The context the function is called with.
         */
        each: function (fn, context) {
            _each(this._array, function (item, index) {
                fn.call(context, item.value, index, item.weight);
            });

            return this;
        },
        /**
         * Executes the supplied function for each value in the weighted list.
         * Iteration stops if the supplied function does not return a truthy
         * value.
         * @method every
         * @param {Function} fn The function to execute for each value in the
         * weighted list.  The first argument passed to this function will be
         * the value.  The second argument passed to this function will be the
         * value's index.  The third argument passed to this function will be
         * the value's weight.
         * @param {Any} [context] Optional.  The context the function is called with.
         * @return {Boolean} true if every value in the weighted list returns
         * true from the supplied function, false otherwise.
         */
        every: function (fn, context) {
            return !_some(this._array, function (item, index) {
                return !fn.call(context, item.value, index, item.weight);
            });
        },
        /**
         * Executes the supplied function for each value in the weighted list.
         * Returns a new weighted list containing the values for which the
         * supplied function returned a truthy value.  The values in the new
         * weighted list will retain the same weights they had in the original
         * weighted list.
         * @method filter
         * @param {Function} fn The function to execute for each value in the
         * weighted list.  The first argument passed to this function will be
         * the value.  The second argument passed to this function will be the
         * value's index.  The third argument passed to this function will be
         * the value's weight.
         * @param {Any} [context] Optional.  The context the function is called with.
         * @return {WeightedList}
         */
        filter: function (fn, context) {
            var other = new _class();

            _each(this._array, function (item, index) {
                var itemValue = item.value,
                    itemWeight = item.weight;

                if (fn.call(context, itemValue, index, itemWeight)) {
                    other.add(itemValue, itemWeight);
                }
            });

            return other;
        },
        /**
         * Executes the supplied function for each value in the weighted list,
         * searching for the first value that matches the supplied function.
         * @method find
         * @param {Function} fn The function to execute for each value in the
         * weighted list.  The first argument passed to this function will be
         * the value.  The second argument passed to this function will be the
         * value's index.  The third argument passed to this function will be
         * the value's weight.  Iteration is stopped as soon as this function
         * returns true.
         * @param {Any} [context] Optional.  The context the function is called with.
         * @return {Any} The found value is returned or null is returned if no value
         * was found.
         */
        find: function (fn, context) {
            var found = null;

            _some(this._array, function (item, index) {
                var itemValue = item.value;

                if (fn.call(context, itemValue, index, item.weight)) {
                    found = itemValue;
                    return true;
                }
            });

            return found;
        },
        /**
         * Iterates over a weighted list, returning a new weighted list with all
         * the values that match the supplied regular expression.  The values in
         * the new weighted list will retain the same weights they had in the
         * original weighted list.
         * @method grep
         * @param {RegExp} pattern Regular expression to test against each item.
         * @return {WeightedList}
         */
        grep: function (pattern) {
            var other = new _class();

            _each(this._array, function (item) {
                var itemValue = item.value;

                if (pattern.test(itemValue)) {
                    other.add(itemValue, item.weight);
                }
            });

            return other;
        },
        /**
         * Returns the index of the first value in the weighted list that is
         * equal (using a strict equality check) to the specified value, or -1
         * if the value isn't found.
         * @method indexOf
         * @param {Any} value
         * @param {Number} [from] Optional.  The index at which to begin the
         * search.  Defaults to 0.
         * @return {Number}
         */
        indexOf: function (value, from) {
            var me = this,

                array = me._array,
                found = -1;

            from = from || 0;

            if (from < 0) {
                from += array.length;
            }

            _some(array, function (item, index) {
                if (index < from) {
                    return false;
                }

                if (me.itemsAreEqual(item.value, value)) {
                    found = index;
                    return true;
                }
            });

            return found;
        },
        /**
         * Executes a named method on each value in a weighted list of objects.
         * Values in the weighted list that do not have a function by that name
         * will be skipped.
         * @method invoke
         * @param {String} methodName
         * @return {WeightedList} A new weighted list is returned containing the
         * return values from each method.  The values in the new weighted list
         * will retain the same weights they had in the original weighted list.
         */
        invoke: function (methodName) {
            var args = _Array(arguments, 1, true),
                other = new _class();

            _each(this._array, function (item) {
                var itemValue = item.value,
                    method = itemValue && itemValue[methodName];

                other.add(_isFunction(method) ? method.apply(itemValue, args) : null, item.weight);
            });

            return other;
        },
        /**
         * Returns true if the weighted list is empty.
         * @method isEmpty
         * @return {Boolean}
         */
        isEmpty: function () {
            return !this._array.length;
        },
        /**
         * Gets an item by index from the weighted list if an index is supplied.
         * If an index is not supplied, an item is selected by weighted random
         * distribution.
         * @method item
         * @param {Number} [index] Optional.
         * @return {Object}  The item is returned or null is returned if the
         * given index does not exist.  A returned item will be an object with
         * the following properties:
         * <dl>
         *     <dt>
         *         index
         *     </dt>
         *     <dd>
         *         This item's index.
         *     </dd>
         *     <dt>
         *         value
         *     </dt>
         *     <dd>
         *         This item's value.
         *     </dd>
         *     <dt>
         *         weight
         *     </dt>
         *     <dd>
         *         This item's weight.
         *     </dd>
         * </dl>
         */
        item: function (index) {
            if (!index && index !== 0) {
                index = this._randomIndex();
            }

            var item = this._array[index];
            return item ? {
                index: index,
                value: item.value,
                weight: item.weight
            } : null;
        },
        /**
         * Default comparator for values stored in this weighted list.  Used by
         * the indexOf, lastIndexOf, and remove methods.
         * @method itemsAreEqual
         * @param {Any} a
         * @param {Any} b
         * @return {Boolean}
         */
        itemsAreEqual: function (a, b) {
            return a === b;
        },
        /**
         * Returns the index of the last value in the weighted list that is
         * equal (using a strict equality check) to the specified value, or -1
         * if the value isn't found.
         * @method lastIndexOf
         * @param {Any} value
         * @param {Number} [from] Optional.  The index at which to begin the
         * search.  Defaults to the last index in the weighted list.
         * @return {Number}
         */
        lastIndexOf: function (value, from) {
            var me = this,

                array = me._array,
                arrayLength = array.length,
                found = -1;

            if (!from && from !== 0) {
                from = arrayLength - 1;
            }

            if (from < 0) {
                from += array.length;
            }

            _iterate(array, -1, function (item, index) {
                if (index > from) {
                    return false;
                }

                if (me.itemsAreEqual(item.value, value)) {
                    found = index;
                    return true;
                }
            });

            return found;
        },
        /**
         * Executes the supplied function for each value in the weighted list
         * and returns a new weighted list containing all the values returned by
         * the supplied function.  The values in the new weighted list will
         * retain the same weights they had in the original weighted list.
         * @method map
         * @param {Function} fn The function to execute for each value in the
         * weighted list.  The first argument passed to this function will be
         * the value.  The second argument passed to this function will be the
         * value's index.  The third argument passed to this function will be
         * the value's weight.
         * @param {Any} [context] Optional.  The context the function is called with.
         * @return {WeightedList}
         */
        map: function (fn, context) {
            var other = new _class();

            _each(this._array, function (item, index) {
                var itemWeight = item.weight;

                other.add(fn.call(context, item.value, index, itemWeight), item.weight);
            });

            return other;
        },
        /**
         * Partitions a weighted list into two new weighted lists, one with the
         * values for which the supplied function returns true, and one with the
         * values for which the function returns false.  The values in the new
         * weighted lists will retain the same weights they had in the original
         * weighted list.
         * @method partition
         * @param {Function} fn The function to execute for each value in the
         * weighted list.  The first argument passed to this function will be
         * the value.  The second argument passed to this function will be the
         * value's index.  The third argument passed to this function will be
         * the value's weight.
         * @param {Any} [context] Optional.  The context the function is called with.
         * @return {Object} An object with two properties: matches and rejects.
         * Each is a weighted list containing the items that were selected or
         * rejected by the test function.
         */
        partition: function (fn, context) {
            var matches = new _class(),
                rejects = new _class();

            _each(this._array, function (item, index) {
                var itemValue = item.value,
                    itemWeight = item.weight;

                (fn.call(context, itemValue, index, itemWeight) ? matches : rejects).add(itemValue, itemWeight);
            });

            return {
                matches: matches,
                rejects: rejects
            };
        },
        /**
         * Executes the supplied function for each value in the weighted list,
         * "folding" the weighted list into a single value.
         * @method reduce
         * @param {Any} initialValue
         * @param {Function} fn The function to execute for each value in the
         * weighted list.  The first argument passed to this function will be
         * the value returned from the previous iteration or the initial value
         * if this is the first iteration.  The second argument passed to this
         * function will be the current value in the weighted list.  The third
         * argument passed to this function will be the current value's index.
         * The fourth argument passed to this function will be the current
         * value's weight.
         * @param {Any} [context] Optional.  The context the function is called with.
         * @return {Any} Final result from iteratively applying the given function to
         * each value in the weighted list.
         */
        reduce: function (initialValue, fn, context) {
            return _reduce(this._array, initialValue, function (value, item, index) {
                return fn.call(context, value, item.value, index, item.weight);
            });
        },
        /**
         * The inverse of the filter method.  Executes the supplied function for
         * each value in the weighted list.  Returns a new weighted list
         * containing the values for which the supplied function returned false.
         * The values in the new weighted list will retain the same weights they
         * had in the original weighted list.
         * @method reject
         * @param {Function} fn The function to execute for each value in the
         * weighted list.  The first argument passed to this function will be
         * the value.  The second argument passed to this function will be the
         * value's index.  The third argument passed to this function will be
         * the value's weight.
         * @param {Any} [context] Optional.  The context the function is called with.
         * @return {WeightedList}
         */
        reject: function (fn, context) {
            var other = new _class();

            _each(this._array, function (item, index) {
                var itemValue = item.value,
                    itemWeight = item.weight;

                if (!fn.call(context, itemValue, index, itemWeight)) {
                    other.add(itemValue, itemWeight);
                }
            });

            return other;
        },
        /**
         * Removes the first or all occurrences of a value from the weighted
         * list.  This may cause remaining values to be reindexed.
         * @method remove
         * @param {Any} value
         * @param {Boolean} [all] Optional.  If true, removes all occurances.
         * @return {Number} The number of items that were removed.
         */
        remove: function (value, all) {
            var me = this,

                array = me._array,
                count = 0,
                i = array.length - 1;

            for (; i >= 0; i -= 1) {
                if (me.itemsAreEqual(value, me.value(i))) {
                    array.splice(i, 1);
                    count += 1;

                    if (!all) {
                        break;
                    }
                }
            }

            if (count) {
                delete me._sum;
                delete me._totals;
            }

            return count;
        },
        /**
         * Removes a value from the weighted list by index.  This may cause
         * remaining values to be reindexed.
         * @method removeIndex
         * @chainable
         * @param {Number} index
         */
        removeIndex: function (index) {
            var me = this;

            me._array.splice(index, 1);
            delete me._sum;
            delete me._totals;

            return me;
        },
        /**
         * Returns the number of values in the weighted list.
         * @method size
         * @return {Number}
         */
        size: function () {
            return this._array.length;
        },
        /**
         * Executes the supplied function for each value in the weighted list.
         * Returning a truthy value from the function will stop the processing
         * of remaining values.
         * @method some
         * @param {Function} fn The function to execute for each value in the
         * weighted list.  The first argument passed to this function will be
         * the value.  The second argument passed to this function will be the
         * value's index.  The third argument passed to this function will be
         * the value's weight.
         * @param {Any} [context] Optional.  The context the function is called with.
         * @return {Boolean} true if the function returns a truthy value on any
         * of the values in the weighted list; false otherwise.
         */
        some: function (fn, context) {
            return _some(this._array, function (item, index) {
                return fn.call(context, item.value, index, item.weight);
            });
        },
        /**
         * Change the value and weight of a value that is already in the
         * weighted list.
         * @method update
         * @chainable
         * @param {Number} index
         * @param {Any} value
         * @param {Number} weight
         */
        update: function (index, value, weight) {
            var me = this;

            if (!weight && weight !== 0) {
                weight = 1;
            }

            me._array[index] = {
                value: value,
                weight: weight
            };

            delete me._sum;
            delete me._totals;

            return me;
        },
        /**
         * Change the value of a value that is already in the weighted list.
         * @method updateValue
         * @chainable
         * @param {Number} index
         * @param {Any} value
         */
        updateValue: function (index, value) {
            return this.update(index, value, this.weight(index));
        },
        /**
         * Change the weight of a value that is already in the weighted list.
         * @method updateWeight
         * @chainable
         * @param {Number} index
         * @param {Number} weight
         */
        updateWeight: function (index, weight) {
            return this.update(index, this.value(index), weight);
        },
        /**
         * Returns a copy of the weighted list with duplicate value removed.
         * @method unique
         * @param {String} [mode] Optional.  If the original weighted list contains
         * duplicate values with different weights, the mode specifies how those
         * weights get transferred to the new weighted list.  mode may be one of
         * the following values:
         * <dl>
         *     <dt>
         *         'first'
         *     </dt>
         *     <dd>
         *         Use the first weight that is found.  Ignore all others.
         *     </dd>
         *     <dt>
         *         'sum'
         *     </dt>
         *     <dd>
         *         Use the sum of all weights that are found.  This is the
         *         default mode.
         *     </dd>
         * </dl>
         * @return {WeightedList}
         */
        unique: function (mode) {
            if (!mode) {
                mode = 'sum';
            }

            var other = new _class();

            _each(this._array, function (item) {
                var itemValue = item.value;

                if (!_some(other._array, function (otherItem, index) {
                    if (otherItem.value === itemValue) {
                        if (mode === 'sum') {
                            other.updateWeight(index, otherItem.weight + item.weight);
                        }

                        return true;
                    }
                })) {
                    other.add(itemValue, item.weight);
                }
            });

            return other;
        },
        /**
         * Provides an array of values.
         * @method toArray
         * @return {Array}
         */
        toArray: function () {
            return _map(this._array, function (item) {
                return item.value;
            });
        },
        /**
         * Provides an array of values for JSON.stringify.
         * @method toJSON
         * @return {Array}
         */
        toJSON: function () {
            return this.toArray();
        },
        /**
         * @method toString
         * @return {String}
         */
        toString: function () {
            return this.toArray().toString();
        },
        /**
         * Gets a value by index from the weighted list if an index is supplied.
         * If an index is not supplied, a value is selected by weighted random
         * distribution.
         * @method value
         * @param {Number} [index] Optional.
         * @return {Any} The value is returned or null is returned if the given index
         * does not exist.
         */
        value: function (index) {
            if (!index && index !== 0) {
                index = this._randomIndex();
            }

            var item = this._array[index];
            return item ? item.value : null;
        },
        /**
         * Gets a value's weight by index from the weighted list if an index is
         * supplied.  If an index is not supplied, a value is selected by
         * weighted random distribution.
         * @method weight
         * @param {Number} [index] Optional.
         * @return {Number} The weight is returned or null is returned if the
         * given index does not exist.
         */
        weight: function (index) {
            if (!index && index !== 0) {
                index = this._randomIndex();
            }

            var item = this._array[index];
            return item ? item.weight : null;
        },
        /**
         * Returns an index by weighted random distribution.
         * @method _randomIndex
         * @protected
         * @return {Number}
         */
        _randomIndex: function () {
            var maximumIndex,
                me = this,
                middleIndex,
                minimumIndex = 0,
                random,
                sum = me._sum,
                total,
                totals = me._totals;

            if (!sum || !totals) {
                me._update();

                sum = me._sum;
                totals = me._totals;

                if (!sum || !totals || !totals.length) {
                    return null;
                }
            }

            maximumIndex = totals.length - 1;
            random = _random() * sum;

            while (maximumIndex >= minimumIndex) {
                middleIndex = (maximumIndex + minimumIndex) / 2;

                if (middleIndex < 0) {
                    middleIndex = 0;
                } else {
                    middleIndex = _floor(middleIndex);
                }

                total = totals[middleIndex];

                if (random === total) {
                    middleIndex += 1;
                    break;
                } else if (random < total) {
                    if (middleIndex && random > totals[middleIndex - 1]) {
                        break;
                    }

                    maximumIndex = middleIndex - 1;
                } else if (random > total) {
                    minimumIndex = middleIndex + 1;
                }
            }

            return middleIndex;
        },
        /**
         * Updates chached data for achieving weighted random distribution.
         * @method _update
         * @chainable
         * @protected
         */
        _update: function () {
            var me = this,
                sum = 0,
                totals = [];

            _each(me._array, function (item) {
                sum += item.weight;
                totals.push(sum);
            });

            me._sum = sum;
            me._totals = totals;

            return me;
        }
    };

    Y.WeightedList = _class;
}(Y));