Source: utils/array.js

/**
 * @namespace qui.utils.array
 */

function makeKeySortFunc(desc) {
    return function (p1, p2) {
        let k1 = p1.key
        let k2 = p2.key

        if ((desc && k1 > k2) || (!desc && k1 < k2)) {
            return -1
        }
        if ((desc && k1 < k2) || (!desc && k1 > k2)) {
            return 1
        }
        else {
            return 0
        }
    }
}

/* The sort runs over element-key pairs, so the key function runs once per element instead of twice for every
 * comparison the sort makes. Sorting 448 items in random order took 6408 key extractions; this brings it to 448. */
function sortPairs(array, func, desc, thisArg, sortFunc) {
    /* Each element carries its own key instead of the two being matched up in a Map, which would file 0 and -0 under
     * one entry. Undefined entries and holes are held back and restored at the end, as Array#sort leaves them. */
    let length = array.length
    let pairs = []
    let undefinedCount = 0

    array.forEach(function (e) {
        if (e === undefined) {
            undefinedCount++
        }
        else {
            pairs.push({element: e, key: func.call(thisArg, e)})
        }
    })

    sortFunc(pairs, makeKeySortFunc(desc))

    array.length = 0
    pairs.forEach(p => array.push(p.element))
    while (undefinedCount--) {
        array.push(undefined)
    }
    array.length = length

    return array
}

function merge(left, right, compareFunc) {
    let result = []

    while (left.length > 0 || right.length > 0) {
        if (left.length > 0 && right.length > 0) {
            if (compareFunc(left[0], right[0]) <= 0) {
                result.push(left[0])
                left = left.slice(1)
            }
            else {
                result.push(right[0])
                right = right.slice(1)
            }
        }
        else if (left.length > 0) {
            result.push(left[0])
            left = left.slice(1)
        }
        else if (right.length > 0) {
            result.push(right[0])
            right = right.slice(1)
        }
    }

    return result
}

function sorted(array, compareFunc) {
    let length = array.length
    let middle = Math.floor(length / 2)

    compareFunc = compareFunc || function (left, right) {
        if (left < right) {
            return -1
        }
        else if (left === right) {
            return 0
        }
        else {
            return 1
        }
    }

    if (length < 2) {
        return array
    }

    return merge(
        sorted(array.slice(0, middle), compareFunc),
        sorted(array.slice(middle, length), compareFunc),
        compareFunc
    )
}


/**
 * Perform a stable sort on an array, *in place*, using a comparison function.
 * @alias qui.utils.array.stableSort
 * @param {Array} array
 * @param {Function} compareFunc comparison function; takes two elements as parameters and returns `-1, `0` or `1`
 * @returns {Array} the array
 */
export function stableSort(array, compareFunc) {
    /* Slower, but such is life */
    let result = sorted(array, compareFunc).slice()

    /* Replace all the elements in array with the sorted ones */
    array.length = 0
    Array.prototype.splice.apply(array, [0, 0].concat(result))

    return array
}

/**
 * Perform a sort on an array, *in place*, using a key extraction function.
 *
 * As with `Array#sort`, `undefined` entries and holes end up at the end of the array, in that order, and the key
 * extraction function is not called for them. It is called once per remaining element.
 * @alias qui.utils.array.sortKey
 * @param {Array} array
 * @param {Function} func key extraction function; will be called with each element as parameter and is expected to
 * return the comparison key
 * @param {Boolean} [desc] whether to do a descending sort (defaults to `false`)
 * @param {*} [thisArg] optional argument to use as `this` when calling the key extraction function.
 * @returns {Array} the array
 */
export function sortKey(array, func, desc = false, thisArg = null) {
    return sortPairs(array, func, desc, thisArg, (pairs, compareFunc) => pairs.sort(compareFunc))
}

/**
 * Perform a stable sort on an array, *in place*, using a key extraction function.
 *
 * As with `Array#sort`, `undefined` entries and holes end up at the end of the array, in that order, and the key
 * extraction function is not called for them. It is called once per remaining element.
 * @alias qui.utils.array.stableSortKey
 * @param {Array} array
 * @param {Function} func key extraction function; will be called with each element as parameter and is expected to
 * return the comparison key
 * @param {Boolean} [desc] whether to do a descending sort (defaults to `false`)
 * @param {*} [thisArg] optional argument to use as `this` when calling the key extraction function.
 * @returns {Array} the array
 */
export function stableSortKey(array, func, desc = false, thisArg = null) {
    return sortPairs(array, func, desc, thisArg, stableSort)
}

/**
 * Generate an array based on a range of numbers.
 * @alias qui.utils.array.range
 * @param {Number} start inclusive range start
 * @param {Number} stop exclusive range stop
 * @param {Number} [step] range step (defaults to `1`)
 * @returns {Number[]}
 */
export function range(start, stop, step = 1) {
    let array = []
    for (let i = start; i < stop; i += step) {
        array.push(i)
    }

    return array
}

/**
 * Return an array of distinct elements found in an input array.
 * @alias qui.utils.array.distinct
 * @param {Array} array
 * @param {Function} [equalsFunc] a comparison function (defaults to the `===` operator)
 * @param {*} [thisArg] optional argument to be used as `this` when calling `equalsFunc`
 * @returns {Array} the distinct elements array
 */
export function distinct(array, equalsFunc = null, thisArg = null) {
    let uniqueElements = []

    if (!equalsFunc) {
        equalsFunc = (a, b) => a === b
    }

    array.forEach(function (element) {
        let found = uniqueElements.find(function (elem) {
            return equalsFunc.call(thisArg, elem, element)
        })

        if (found === undefined) {
            uniqueElements.push(element)
        }
    })

    return uniqueElements
}

/**
 * Remove all occurrences of an element from an array, in place.
 * @alias qui.utils.array.remove
 * @param {Array} array
 * @param {*} element the element to remove
 */
export function remove(array, element) {
    let index
    while ((index = array.indexOf(element)) >= 0) {
        array.splice(index, 1)
    }
}

/**
 * Remove all occurrences of a set of elements from an array, in place.
 * @alias qui.utils.array.removeMany
 * @param {Array} array
 * @param {Array} elements the set of elements to remove
 */
export function removeMany(array, elements) {
    let index
    elements.forEach(function (element) {
        while ((index = array.indexOf(element)) >= 0) {
            array.splice(index, 1)
        }
    })
}