multiplex/multiplex.js

View on GitHub
src/lib/collections/list.js

Summary

Maintainability
C
1 day
Test Coverage
import Collection from './collection';
import ReadOnlyCollection from './read-only-collection';
import ArrayIterator from '../iteration/iterator-array';
import Comparer from './comparer';

import isNumber from '../utils/is-number';
import isFunction from '../utils/is-function';
import assertNotNull from '../utils/assert-not-null';
import assertType from '../utils/assert-type';
import binarySearch from '../utils/binary-search';
import buffer from '../utils/buffer';
import bufferTo from '../utils/buffer-to';
import extend from '../utils/extend';
import count from '../utils/count';
import { ARRAY_PROTOTYPE } from '../utils/builtin-types';
import error, { ERROR_ARGUMENT_OUT_OF_RANGE } from '../utils/error';


/**
* Initializes a new instance of the List class.
* @param {Number|Iterable|...Object=} value The number of elements, Arbitrary number of arguments or the collection whose elements are copied to the new list.
*/
export default function List(value) {
    this.length = 0;

    if (arguments.length === 1) {
        if (isNumber(value)) {
            this.length = value;
        }

        else {
            this.addRange(value);
        }
    }
    else {
        this.addRange(arguments);
    }
}

extend(List, Collection, {
    /**
    * Adds an object to the end of the List.
    * @param {Object} item The object to be added to the end of the List.
    */
    add: function (item) {
        this[this.length++] = item;
    },

    /**
    * Adds the elements of the specified collection to the end of the List.
    * @param {Iterable} collection The collection whose elements should be added to the end of the List.
    */
    addRange: function (collection) {
        assertNotNull(collection);
        this.insertRange(this.length, collection);
    },

    /**
    * Returns a read-only wrapper for the current list.
    * @returns {ReadOnlyCollection}
    */
    asReadOnly: function () {
        return new ReadOnlyCollection(this);
    },

    /**
    * Searches a range of elements in the sorted List for an element using the specified comparer and returns the zero-based index of the element.
    * @param {Object} item The object to locate.
    * @param {Comparer=} comparer The Comparer implementation to use when comparing elements.
    * @param {Number=} index The zero-based starting index of the range to search.
    * @param {Number=} count The length of the range to search.
    * @returns {Number}
    */
    binarySearch: function (item, comparer, index, count) {
        index = index || 0;
        count = count || this.length;
        comparer = Comparer.from(comparer);

        return binarySearch(this, index, count, item, comparer.compare);
    },

    /**
    * Removes all items from the List.
    */
    clear: function () {
        shrinkList(this, 0);
    },

    /**
    * Gets the number of elements contained in the List.
    * @param {Function=} predicate A function to test each element for a condition. eg. function(item)
    * @returns {Number}
    */
    count: function (predicate) {
        return predicate ? count(this, predicate) : this.length;
    },

    /**
    * Determines whether the List contains a specific value.
    * @param {Object} item The object to locate in the List.
    * @returns {Boolean}
    */
    contains: function (item) {
        return this.indexOf(item) !== -1;
    },

    /**
    * Copies the elements of the List to an Array, starting at a particular Array index.
    * @param {Array} array The one-dimensional Array that is the destination of the elements copied from List.
    * @param {Number} arrayIndex The zero-based index in array at which copying begins.
    */
    copyTo: function (array, arrayIndex) {
        bufferTo(this, array, arrayIndex);
    },

    /**
    * Determines whether the List contains elements that match the conditions defined by the specified predicate.
    * @param {Function} match The predicate function that defines the conditions of the elements to search for. eg. function(item)
    * @returns {Boolean}
    */
    exists: function (match) {
        return this.findIndex(match) !== -1;
    },

    /**
    * Searches for an element that matches the conditions defined by the specified predicate, and returns the first occurrence within the entire List.
    * @param {Function} match The predicate function that defines the conditions of the elements to search for. eg. function(item)
    * @returns {Object}
    */
    find: function (match) {
        assertType(match, Function);

        for (var i = 0, len = this.length; i < len; i++) {
            if (match(this[i]) === true) {
                return this[i];
            }
        }
    },

    /**
    * Searches for an element that matches the conditions defined by the specified predicate,
    * and returns the zero-based index of the first occurrence within the range of elements
    * in the List that starts at the specified index and contains the specified number of elements.
    * @param {Number|Function} startIndexOrMatch The zero-based starting index of the search or the predicate function, eg. function(item)
    * @param {Number|Function=} countOrMatch The number of elements in the section to search or the predicate function, eg. function(item)
    * @param {Function=} match The predicate function that defines the conditions of the elements to search for, eg. function(item)
    * @returns {Number}
    */
    findIndex: function (startIndexOrMatch, countOrMatch, match) {
        var len = this.length,
            startIndex = isNumber(startIndexOrMatch) ? startIndexOrMatch : 0,
            count = isNumber(countOrMatch) ? countOrMatch : len - startIndex;

        match = isFunction(startIndexOrMatch) ? startIndexOrMatch : (isFunction(countOrMatch) ? countOrMatch : match);

        assertType(match, Function);
        validateListIndex(this, startIndex);

        while (count-- > 0 && startIndex < len) {
            if (match(this[startIndex]) === true) {
                return startIndex;
            }

            startIndex++;
        }

        return -1;
    },

    /**
    * Searches for an element that matches the conditions defined by the specified predicate,
    * and returns the last occurrence within the entire List.
    * @param {Function} match The predicate function that defines the conditions of the elements to search for. eg. function(item)
    * @returns {Object}
    */
    findLast: function (match) {
        assertType(match, Function);

        var len = this.length;
        while (len-- > 0) {
            if (match(this[len]) === true) {
                return this[len];
            }
        }

        return undefined;
    },

    /**
    * Searches for an element that matches the conditions defined by the specified predicate,
    * and returns the zero-based index  of the last occurrence within the range of elements
    * in the List that contains the specified number of elements and ends at the specified index.
    * @param {Number|Function} startIndexOrMatch The zero-based starting index of the search or the predicate, eg. function(item)
    * @param {Number|Function=} countOrMatch The number of elements in the section to search or the predicate, eg. function(item)
    * @param {Function=} match The predicate function that defines the conditions of the elements to search for, eg. function(item)
    * @returns {Number}
    */
    findLastIndex: function (startIndexOrMatch, countOrMatch, match) {
        var startIndex = isNumber(startIndexOrMatch) ? startIndexOrMatch : this.length - 1,
            count = isNumber(countOrMatch) ? countOrMatch : startIndex;

        match = isFunction(startIndexOrMatch) ? startIndexOrMatch : (isFunction(countOrMatch) ? countOrMatch : match);

        assertType(match, Function);
        validateListIndex(this, startIndex);

        while (count-- > 0 && startIndex > 0) {
            if (match(this[startIndex]) === true) {
                return startIndex;
            }

            startIndex--;
        }

        return -1;
    },

    /**
    * Retrieves all the elements that match the conditions defined by the specified predicate.
    * @param {Function} match The predicate function that defines the conditions of the elements to search for. eg. function(item)
    * @returns {List}
    */
    findAll: function (match) {
        assertType(match, Function);

        var arr = new Array(this.length),
            count = 0;

        for (var i = 0, len = this.length; i < len; i++) {
            if (match(this[i]) === true) {
                arr[count++] = this[i];
            }
        }

        arr.length = count;
        return new List(arr);
    },

    /**
    * Gets the element at the specified index.
    * @param {Number} index The zero-based index of the element to get.
    * @returns {Object}
    */
    get: function (index) {
        validateListIndex(this, index);
        return this[index];
    },

    /**
    * Creates a shallow copy of a range of elements in the source List.
    * @param {Number} index The zero-based List index at which the range starts.
    * @param {Number} count The number of elements in the range.
    * @returns {List}
    */
    getRange: function (index, count) {
        validateListIndex(this, index + count - 1);
        return new List(this.slice(index, index + count));
    },

    /**
    * Searches for the specified object and returns the zero-based index of the first occurrence within
    * the range of elements in the List that extends from the specified index to the last element.
    * @param {Object} item The object to locate in the List.
    * @param {Number=} index The zero-based starting index of the search. 0 (zero) is valid in an empty list.
    * @returns {Number}
    */
    indexOf: ARRAY_PROTOTYPE.indexOf,

    /**
    * Inserts an element into the List at the specified index.
    * @param {Number} index The zero-based index at which item should be inserted.
    * @param {Object} item The object to insert.
    */
    insert: function (index, item) {
        if (index !== this.length) {
            validateListIndex(this, index);
        }

        var len = ++this.length;

        while (len-- > index) {
            this[len] = this[len - 1];
        }

        this[index] = item;
    },

    /**
    * Inserts the elements of a collection into the List at the specified index.
    * @param {Number} index The zero-based index at which item should be inserted.
    * @param {Iterable} collection The collection whose elements should be inserted into the List.
    */
    insertRange: function (index, collection) {
        assertType(index, Number);
        assertNotNull(collection);

        if (index !== this.length) {
            validateListIndex(this, index);
        }

        var arr = buffer(collection),
            count = arr.length,
            len = this.length + count;

        this.length = len;

        while (len-- > index) {
            this[len] = this[len - count];
        }

        while (count-- > 0) {
            this[index + count] = arr[count];
        }
    },

    /**
    * Searches for the specified object and returns the zero-based index of the last occurrence
    * within the range of elements in the List that extends from the specified index to the last element.
    * @param {Object} item The object to locate in the List.
    * @param {Number=} index The zero-based starting index of the search. 0 (zero) is valid in an empty list.
    * @returns {Number}
    */
    lastIndexOf: ARRAY_PROTOTYPE.lastIndexOf,

    /**
    * Removes the first occurrence of a specific object from the List.
    * @param {Object} item The object to remove from the List.
    * @returns {Boolean}
    */
    remove: function (item) {
        var index = this.indexOf(item);

        if (index !== -1) {
            this.removeAt(index);
            return true;
        }

        return false;
    },

    /**
    * Removes all the elements that match the conditions defined by the specified predicate.
    * @param {Function} match The predicate function that defines the conditions of the elements to remove. eg. function(item)
    * @returns {Number}
    */
    removeAll: function (match) {
        assertType(match, Function);

        var freeIndex = 0,
            len = this.length;

        while (freeIndex < len && !match(this[freeIndex])) {
            freeIndex++;
        }

        if (freeIndex >= len) {
            return 0;
        }

        var current = freeIndex + 1;

        while (current < len) {
            while (current < len && match(this[current])) {
                current++;
            }

            if (current < len) {
                this[freeIndex++] = this[current++];
            }
        }

        shrinkList(this, freeIndex);
        return len - freeIndex;
    },

    /**
    * Removes the element at the specified index of the List.
    * @param {Number} index The zero-based index of the element to remove.
    */
    removeAt: function (index) {
        validateListIndex(this, index);

        var i = index,
            len = --this.length;

        for (; i < len; i++) {
            this[i] = this[i + 1];
        }

        delete this[len];
    },

    /**
    * Removes a range of elements from the List.
    * @param {Number} index The zero-based index of the element to remove.
    * @param {Number} count The number of elements to remove.
    */
    removeRange: function (index, count) {
        validateListIndex(this, index + count - 1);

        var len = this.length - count;

        for (; index < len; index++) {
            this[index] = this[index + count];
        }

        shrinkList(this, len);
    },

    /**
    * Reverses the order of the elements in the specified range.
    * @param {Number=} index The zero-based starting index of the range to reverse.
    * @param {Number=} count The number of elements in the range to reverse.
    */
    reverse: function (index, count) {
        index = index || 0;
        count = count || this.length;
        validateListIndex(this, index + count - 1);

        var arr = this.slice(index, index + count).reverse(),
            len = arr.length;

        while (len-- > 0) {
            this[len + index] = arr[len];
        }

        return this;
    },

    /**
    * Returns a shallow copy of a portion of the list into a new array object.
    * @param {Number=} begin Zero-based index at which to begin extraction.
    * @param {Number=} end Zero-based index at which to end extraction
    * @returns {Array}
    */
    slice: ARRAY_PROTOTYPE.slice,

    /**
    * Changes the content of the list by removing existing elements and/or adding new elements.
    * @param {Number} start Index at which to start changing the list.
    * @param {Number} deleteCount An integer indicating the number of old list elements to remove.
    * @param {Object...} items The elements to add to the list.
    * @returns {Array}
    */
    splice: ARRAY_PROTOTYPE.splice,

    /**
    * Sets the element at the specified index.
    * @param {Number} index The zero-based index of the element to set.
    * @param {Object} item The object to be added at the specified index.
    */
    set: function (index, value) {
        validateListIndex(this, index);
        this[index] = value;
    },

    /**
    * Sorts the elements in a range of elements in List using the specified comparer.
    * @param {Number|Function|Comparer} val The starting index, the comparison function or the Comparer.
    * @param {Number=} count The length of the range to sort.
    * @param {Comparer=} comparer The Comparer implementation to use when comparing elements.
    */
    sort: function (indexOrComparer, count, comparer) {
        var index = isNumber(indexOrComparer) ? indexOrComparer : 0,
            total = count || this.length - index,
            comparision = indexOrComparer === null ? null :
                (isFunction(indexOrComparer) ? indexOrComparer :
                    Comparer.from(comparer || indexOrComparer).compare);

        validateListIndex(this, index + total - 1);

        var arr = this.slice(index, index + total).sort(comparision),
            len = arr.length;

        while (len-- > 0) {
            this[len + index] = arr[len];
        }
    },

    /**
    * Copies the elements of the List to a new array.
    * @returns {Array}
    */
    toArray: function () {
        return this.slice();
    },

    /**
    * Determines whether every element in the List matches the conditions defined by the specified predicate.
    * @param {Function} match The Predicate function that defines the conditions to check against the elements, eg. function(item)
    * @returns {Boolean}
    */
    trueForAll: function (match) {
        assertType(match, Function);

        for (var i = 0, len = this.length; i < len; i++) {
            if (match(this[i]) === false) {
                return false;
            }
        }

        return true;
    },

    toString: function () {
        return '[List]';
    },

    '@@iterator': function () {
        return new ArrayIterator(this);
    }
});


/// helper method to shrink the list to lower size and delete upper indexes
function shrinkList(list, newSize) {
    var size = list.length;
    list.length = newSize;

    if (size < newSize) {
        do {
            delete list[newSize];
        }
        while (newSize-- < size);
    }
}


/// helper method to validate index against being out of range
function validateListIndex(list, index) {
    if (index < 0 || index >= list.length) {
        error(ERROR_ARGUMENT_OUT_OF_RANGE);
    }
}