API Docs for: 3.11.0-git
Show:

File: src/sm-indexed-map/js/indexed-map.js

/*jshint es3: true, globalstrict: true, indent: 4 */

/**
Provides the `Y.IndexedMap` data structure.

@module gallery-sm-indexed-map
@main gallery-sm-indexed-map
**/

/**
An ordered, indexed hash map data structure. Like `Y.Map` and Array got together
and had a beautiful baby.

Provides constant time key-based lookups as well as numerical index-based
lookups, splicing, and other array-like functionality. Keys may be any value,
including objects.

The `Y.IndexedMap` class extends `Y.Map`. See that module's documentation for
more details on Map fundamentals.

@class IndexedMap
@extends Map
@constructor
@param {Array[]|Map} [entries] Array or Map of entries to add to this map. If an
    array, then each entry should itself be an array in which the first item is
    the key and the second item is the value for that entry.

@param {Object} [options] Options.

    @param {Boolean} [options.autoStamp=false] If `true`, objects used as keys
        will be automatically stamped with a unique id as the value of the
        property defined by the `objectIdName` option ("_yuid" by default) if
        that property isn't already set. This will result in much faster lookups
        for object keys.

    @param {String} [options.objectIdName="_yuid"] Name of a property whose
        string value should be used as the unique key when present on an object
        that's given as a key. This will significantly speed up lookups of
        object-based keys that define this property.
**/

"use strict";

var protoSlice = Array.prototype.slice;

function IndexedMap() {
    Y.Map.apply(this, arguments);
}

Y.IndexedMap = Y.extend(IndexedMap, Y.Map, {
    // -- Public Methods -------------------------------------------------------

    /**
    Returns the entry at the given _index_, or `undefined` if not found.

    An entry is an array with two items, the first being a key and the second a
    value associated with that key.

    @example

        var map = new Y.IndexedMap([
            ['a', 'airwolf'],
            ['b', 'battlestar galactica']
        ]);

        map.entryAt(1); // => ['b', 'battlestar galactica']
        map.entryAt(5); // => undefined

    @method entryAt
    @param {Number} index Index to look up.
    @return {Array|undefined} Entry at the given _index_, or `undefined` if not
        found.
    **/
    entryAt: function (index) {
        if (index in this._mapKeys) {
            return [this._mapKeys[index], this._mapValues[index]];
        }
    },

    /**
    Returns the numerical index of the entry with the given _key_, or `-1` if
    not found.

    This is a very efficient operation with string keys, but may be slower with
    non-string keys.

    @example

        var map = new Y.IndexedMap([
            ['a', 'airwolf'],
            ['b', 'battlestar galactica']
        ]);

        map.indexOfKey('b'); // => 1
        map.indexOfKey('z'); // => -1

    @method indexOfKey
    @param {Mixed} key Key to look up.
    @return {Number} Index of the entry with the given _key_, or `-1` if not
        found.
    @protected
    **/
    indexOfKey: function (key) {
        return Y.Map.prototype._indexOfKey.call(this, key);
    },

    /**
    Returns the key at the given _index_, or `undefined` if not found.

    @example

        var map = new Y.IndexedMap([
            ['a', 'airwolf'],
            ['b', 'battlestar galactica']
        ]);

        map.keyAt(0); // => 'a'
        map.keyAt(5); // => undefined

    @method keyAt
    @param {Number} index Index to look up.
    @return {Mixed} Key at the given _index_, or `undefined` if not found.
    **/
    keyAt: function (index) {
        return this._mapKeys[index];
    },

    /**
    Removes the last entry from this map and returns it.

    @example

        var map = new Y.IndexedMap([
            ['a', 'airwolf'],
            ['b', 'battlestar galactica']
        ]);

        map.pop(); // => ['b', 'battlestar galactica']
        map.pop(); // => ['a', 'airwolf']
        map.pop(); // => undefined

    @method pop
    @return {Array} Entry that was removed, or `undefined` if this map is empty.
    **/
    pop: function () {
        var len = this._mapKeys.length,

            entry,
            i;

        if (len) {
            i     = len - 1;
            entry = [this._mapKeys[i], this._mapValues[i]];

            this._removeMapEntry(i);
            this._updateMapSize();

            return entry;
        }
    },

    /**
    Appends the given entry or entries to the end of this map and returns the
    map's new size.

    Any entries that already exist in this map with the same keys as the new
    entries will be removed to make way for the new entries, regardless of their
    position. If this map has been sorted, you may need to re-sort it after
    appending new entries.

    @example

        var map = new Y.IndexedMap();

        map.push(
            ['a', 'airwolf'],
            ['b', 'battlestar galactica']
        ); // => 2

        map.get('a'); // => 'airwolf'

    @method push
    @param {Array} entry* One or more entries to append to this map.
    @return {Number} New size of this map.
    **/
    push: function () {
        var args = protoSlice.call(arguments);

        args.unshift(this._mapKeys.length, 0);
        this.splice.apply(this, args);

        return this.size;
    },

    /**
    Removes the first entry from this map and returns it. This will cause the
    indices of subsequent entries in the map to be adjusted.

    @example

        var map = new Y.IndexedMap([
            ['a', 'airwolf'],
            ['b', 'battlestar galactica']
        ]);

        map.shift(); // => ['a', 'airwolf']
        map.shift(); // => ['b', 'battlestar galactica']
        map.shift(); // => undefined

    @method shift
    @return {Array} Entry that was removed, or `undefined` if this map is empty.
    **/
    shift: function () {
        if (this._mapKeys.length) {
            return this.splice(0, 1)[0];
        }
    },

    /**
    Returns a new IndexedMap containing a shallow copy of a portion of this map.

    The returned map will be created using the same options and constructor as
    this map.

    This method does not remove any entries or modify this map in any way.

    @example

        var map = new Y.IndexedMap([
            ['a', 'airwolf'],
            ['m', 'manimal'],
            ['s', 'space: 1999']
        ]);

        map.slice(1, 2);
        // => new IndexedMap containing ['m', 'manimal']

        map.slice(1);
        // => new IndexedMap containing ['m', 'manimal'] and ['s', 'space: 1999']

        map.slice(-1);
        // => new IndexedMap containing ['s', 'space: 1999']

        map.slice(0, -1);
        // => new IndexedMap containing ['a', 'airwolf'] and ['m', 'manimal']

        map.slice();
        // => new IndexedMap containing all three entries (effectively a copy)

    @method slice
    @param {Number} [begin] Zero-based index at which to begin extracting
        entries. If _begin_ is negative, it will be treated as relative to the
        end of the map. If _begin_ is omitted, it defaults to `0`.
    @param {Number} [end] Zero-based index at which to end extraction. `slice()`
        extracts entries up to but not including _end_. If _end is negative, it
        will be treated as relative to the end of the map. If _end_ is omitted,
        all elements between _begin_ and the end of the map will be extracted.
    @return {IndexedMap} New IndexedMap containing a shallow copy of the
        specified portion of this map.
    **/
    slice: function (begin, end) {
        var entries = [],

            keys,
            values;

        // Some browsers (guess which ones!) don't like it if you pass an
        // `undefined` end argument.
        if (typeof end === 'undefined') {
            keys   = this._mapKeys.slice(begin);
            values = this._mapValues.slice(begin);
        } else {
            keys   = this._mapKeys.slice(begin, end);
            values = this._mapValues.slice(begin, end);
        }

        for (var i = 0, len = keys.length; i < len; ++i) {
            entries[i] = [keys[i], values[i]];
        }

        return new this.constructor(entries, this._mapOptions);
    },

    /**
    Sorts the entries in this map.

    If a _callback_ is not given, entries will be sorted by converting their
    values to strings and comparing the values in lexicographic order (a simple
    ASCII sort).

    If a _callback_ is given, entries will be sorted according to the return
    value of the callback. The two entries (_a_ and _b_) passed to the callback
    are sorted based on the value the callback returns.

    The callback should return `-1` (or any negative number) to sort _a_ before
    _b_, `0` if _a_ and _b_ are equal, `1` (or any positive number) to sort _b_
    before _a_.

    @example

        var map = new Y.IndexedMap([
            [0, 'manimal'],
            [1, 'space: 1999'],
            [2, 'airwolf']
        ]);

        // ASCII sort by string values (the default).
        map.sort();

        map.entries();
        // => [
        //     [2, 'airwolf'],
        //     [0, 'manimal'],
        //     [1, 'space: 1999']
        // ]

        // Numerical sort by key (a custom sort).
        map.sort(function (a, b) {
            return a[0] - b[0];
        });

        map.entries();
        // => [
        //     [0, 'manimal'],
        //     [1, 'space: 1999']
        //     [2, 'airwolf'],
        // ]

        // Reverse ASCII sort by value (a custom sort).
        map.sort(function (a, b) {
            var aValue = a[1].toString(),
                bValue = b[1].toString();

            if (bValue === aValue) {
                return 0;
            }

            return bValue < aValue ? -1 : 1;
        });

        map.entries();
        // => [
        //     [1, 'space: 1999']
        //     [0, 'manimal'],
        //     [2, 'airwolf'],
        // ]

    @method sort
    @param {Function} [callback] Sort comparator callback. Receives two entriess
        as arguments, and should return an integer indicating whether the first
        is less than (`-1`), equal to (`0`), or greater than (`1`) the second.

        @param {Array} callback.a First entry to compare. An entry is an array
            in which the first element is the key and the second element is the
            value.
        @param {Array} callback.b Second entry to compare.

    @chainable
    **/
    sort: function (callback) {
        var entries = this.entries(),
            entry;

        entries.sort(callback || this._defaultSortFn);
        this.clear();

        for (var i = 0, len = entries.length; i < len; ++i) {
            entry = entries[i];
            this.set(entry[0], entry[1]);
        }

        return this;
    },

    /**
    Changes the contents of this map, adding new entries while removing old
    entries.

    If one or more new entries are specified, they will be added to this map
    starting at _start_.

    Any entries that already exist in this map with the same keys as the new
    entries will be removed to make way for the new entries, regardless of their
    position. If this map has been sorted, you may need to re-sort it after
    splicing in new entries.

    @example

        var map = new Y.IndexedMap([
            ['a', 'airwolf'],
            ['m', 'manimal'],
            ['s', 'space: 1999']
        ]);

        // Insert 'battlestar galactica' between 'airwolf' and 'manimal'.
        map.splice(1, 0, ['b', 'battlestar galactica']); // => []

        // Remove 'manimal'.
        map.splice(2, 1); // => [['m', 'manimal']]

        // Insert 'mission: impossible' and 'star trek' after
        // `battlestar galactica`. Since 'star trek' reuses the 's' key, the
        // 'space: 1999' entry will be removed.
        map.splice(2, 0, ['m', 'mission: impossible'], ['s', 'star trek']);
        // => [['s', 'space: 1999']]

        // Remove everything after 'airwolf', because who needs those shows
        // when you've got a show about a badass helicopter?
        map.splice(1);
        // => [['b', 'battlestar galactica'], ['m', 'mission: impossible'],
        //     ['s', 'star trek']]

    @method splice
    @param {Number} start Index at which to start changing this map. A negative
        value will result in a position relative to the end of the map.
    @param {Number} [count] Number of entries to remove, starting at _start_. If
        _count_ is `0`, no entries will be removed. If no `count` parameter is
        given, all entries after _start_ will be removed.
    @param {Array} [entry*] Zero or more new entries to add to this map. Each
        entry must be an array in which the first element is the key and the
        second element is the value to add.
    @return {Array[]} An array of entries that were removed, if any.
    **/
    splice: function (start, count) {
        var mapKeys        = this._mapKeys,
            mapValues      = this._mapValues,
            removedEntries = [],

            actualStart,
            i,
            len;

        // Determine the actual start index, since `start` may be relative.
        if (start < 0) {
            actualStart = Math.max(0, mapKeys.length + start);
        } else {
            actualStart = Math.min(mapKeys.length, start || 0);
        }

        // If `start` was given but `count` wasn't given, remove all entries
        // from `start` to the end of the map.
        if (typeof count === 'undefined' && Y.Lang.isNumber(start)) {
            count = mapKeys.length - actualStart;
        }

        start || (start = 0);

        // Remove keys and values.
        if (count) {
            var removedKeys   = mapKeys.splice(start, count),
                removedValues = mapValues.splice(start, count);

            // Remove key-to-index mappings for keys that were removed.
            for (i = 0, len = removedKeys.length; i < len; ++i) {
                removedEntries[i] = [removedKeys[i], removedValues[i]];
            }

            if (removedEntries.length) {
                this._isIndexStale = true;
            }
        }

        // Add new entries (if any).
        var newEntries = protoSlice.call(arguments, 2);

        if (newEntries.length) {
            var keyIndex,
                newEntry,
                newKey,
                newValue;

            // Splice new entries in after the start index.
            for (i = 0, len = newEntries.length; i < len; ++i) {
                newEntry = newEntries[i];

                if (newEntry.length !== 2) {
                    throw new TypeError('Invalid map entry: ' +
                        newEntry.toString());
                }

                newKey   = newEntry[0];
                newValue = newEntry[1];
                keyIndex = this.indexOfKey(newKey);

                if (keyIndex >= 0) {
                    // The key already exists in this map, so we have to remove
                    // it before we can reuse it in a new spliced entry.
                    removedEntries.push([mapKeys[keyIndex],
                        mapValues[keyIndex]]);

                    this._removeMapEntry(keyIndex);

                    // If the removed key preceded `actualStart`, adjust
                    // `actualStart` to account for the difference.
                    if (keyIndex < actualStart) {
                        actualStart -= 1;
                    }
                }

                // Add the new entry to the map.
                if (actualStart === mapKeys.length) {
                    // If `actualStart` is at the end of the map, we can append
                    // things without needing to reindex the map.
                    mapKeys[actualStart]   = newKey;
                    mapValues[actualStart] = newValue;

                    this._indexMapKey(actualStart, newKey);
                } else {
                    // `actualStart` is not at the end of the map, so we'll need
                    // to reindex the map.
                    mapKeys.splice(actualStart, 0, newKey);
                    mapValues.splice(actualStart, 0, newValue);

                    this._isIndexStale = true;
                }

                // Increment `actualStart` so that subsequent new entries are
                // added at the correct position.
                actualStart += 1;
            }
        }

        this._updateMapSize();

        return removedEntries;
    },

    /**
    Prepends the given entry or entries to the beginning of this map and returns
    the map's new size.

    Any entries that already exist in this map with the same keys as the new
    entries will be removed to make way for the new entries, regardless of their
    position. If this map has been sorted, you may need to re-sort it after
    prepending new entries.

    @example

        var map = new Y.IndexedMap([
            ['m', 'manimal'],
            ['s', 'space: 1999']
        ]);

        map.unshift(
            ['a', 'airwolf'],
            ['b', 'battlestar galactica']
        ); // => 2

        map.entries();
        // => [
        //     ['a', 'airwolf'],
        //     ['b', 'battlestar galactica'],
        //     ['m', 'manimal'],
        //     ['s', 'space: 1999']
        // ]

    @method unshift
    @param {Array} entry* One or more entries to append to this map.
    @return {Number} New size of this map.
    **/
    unshift: function () {
        var args = protoSlice.call(arguments);

        args.unshift(0, 0);
        this.splice.apply(this, args);

        return this.size;
    },

    /**
    Returns the value at the given _index_, or `undefined` if not found.

    @example

        var map = new Y.IndexedMap([
            ['a', 'airwolf'],
            ['b', 'battlestar galactica']
        ]);

        map.valueAt(0); // => 'airwolf'
        map.valueAt(5); // => undefined

    @method valueAt
    @param {Number} index Index to look up.
    @return {Mixed} Value at the given _index_, or `undefined` if not found.
    **/
    valueAt: function (index) {
        return this._mapValues[index];
    },

    // -- Protected Methods ----------------------------------------------------

    /**
    Default sort comparator used when `sort()` is called without a callback.

    @method _defaultSortFn
    @param {Array} a First entry to compare.
    @param {Array} b Second entry to compare.
    @return {Number} Result of the comparison.
    @protected
    **/
    _defaultSortFn: function (a, b) {
        var aValue = a[1].toString(),
            bValue = b[1].toString();

        return (aValue < bValue ? -1 : (aValue > bValue ? 1 : 0));
    }
});