sort.js

/**
 * @class Utility class that provides methods to sort data from Arrays.
 * These methods are intended to be used as compareFunctions by the standard 
 * JavaScript {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort sort} function.
 * @hideconstructor
 * 
 * @copyright (c) 2021 TLF Research Ltd.
 */
function Sort() { }

// Parses a string like 'foo[34]' into 'foo' and 34
const PARSE_PROPERTY = /([^\[]+)(?:\[(\d+)\])?/;

/**
 * Specifies to sort using a numeric comparison.
 * @const
 */
Sort.NUMERIC = true;

/**
 * Returns a sort function that will sort an object array in-place into <em>ascending</em> order using the given properties.
 *
 * @param {string|array} properties  Object properties to use for the sort - to select a specific property from an array 
 * specify the index in the string e.g. 'property[0]' to use the first element.
 * @param {boolean} [numeric=false] If true use a numeric (not string) comparison
 * @param {object} [substitution] Allows values to be substituted with new ones before doing the compare. 
 * The substituted value is *only* used for the comparison; the original value remains in the sorted array.
 * 
 * @example
 * // a becomes [{"prop": "1"}, {"prop": "2"}, {"prop": "10"}, {"prop": null}]
 * const a = [{"prop": "1"}, {"prop": null}, {"prop": "10"}, {"prop": "2"}];
 * a.sort(Sort.Asc("prop", Sort.NUMERIC, {"null": "9999"}));
 *
 * @return {Function}
 */
Sort.Asc = function (properties, numeric, substitution) {
    return Sort.sort(properties, numeric, substitution, -1, 1);
};


/**
 * Returns a sort function that will sort an object array in-place into <em>descending</em> order using the given properties.
 *
 * @param {string|array} properties  Object properties to use for the sort - to select a specific property from an array 
 * specify the index in the string e.g. 'property[0]' to use the first element.
 * @param {boolean} [numeric=false] If true use a numeric (not string) comparison
 * @param {object} [substitution] Allows values to be substituted with new ones before doing the compare.
 * The substituted value is *only* used for the comparison; the original value remains in the sorted array.
 *
 * @example
 * // a becomes [{"prop": null}, {"prop": "10"}, {"prop": "2"}, {"prop": "1"}]
 * const a = [{"prop": "1"}, {"prop": null}, {"prop": "10"}, {"prop": "2"}];
 * a.sort(Sort.Desc("prop", Sort.NUMERIC, {"null": "9999"}));
 * 
 * @return {Function}
 */
Sort.Desc = function (properties, numeric, substitution) {
    return Sort.sort(properties, numeric, substitution, 1, -1);
};

/**
 * Sort the given object array so that its values are in the same order as the desired order.
 *
 * @param {array} orig Original object array to be sorted - is not changed. 
 * @param {string|null} property Object property to order by. If set to null it will sort using the original's raw values. In this case original shoud be a simple array of scalar values.
 * @param {array} desired Desired order of array elements.
 * Any values in 'orig' that are not in 'desired' will be appended to the end of the result in their original order.
 *
 * @example
 * // a becomes [{foo:"1"}, {foo:"2"}, {foo:"6"}, {foo:"5"}]
 * const a = Sort.Specific([{foo:"6"}, {foo:"5"}, {foo:"2"}, {foo:"1"}], "foo", ["1", "2"]);
 * 
 * @return {array} Sorted array
 */
Sort.Specific = function (orig, property, desired = []) {
    const clone = JSON.parse(JSON.stringify(orig));
    const result = [];
    desired.forEach(function (want) {
        const idx = clone.map(val => property === null ? val : val[property]).indexOf(want);
        if (idx !== -1) {
            result.push(...clone.slice(idx, idx + 1));
            clone.splice(idx, 1);
        }
    })

    return [...result, ...clone];
}

Sort.sort = function (properties, numeric, substitution, lessThan, greaterThan) {

    if (!Array.isArray(properties)) {
        properties = [properties];
    }

    return compare;

    function compare(a, b, i) {
        if (typeof i === 'undefined') {
            i = 0;
        }

        const [_, property, idx] = properties[i].match(PARSE_PROPERTY);

        let valA, valB;

        if (typeof idx === 'undefined') {
            valA = a[property];
            valB = b[property];
        } else {
            valA = a[property][idx];
            valB = b[property][idx];
        }

        if (substitution && typeof substitution[valA] !== 'undefined') {
            valA = substitution[valA];
        }

        if (substitution && typeof substitution[valB] !== 'undefined') {
            valB = substitution[valB];
        }

        let testA = numeric ? Number(valA) : String(valA);
        let testB = numeric ? Number(valB) : String(valB);

        if (testA === testB) {
            return ++i >= properties.length ? 0 : compare(a, b, i);
        } else {
            return testA < testB ? lessThan : greaterThan;
        }
    }
};