Source: utils/object.js

/**
 * @namespace qui.utils.object
 */

import * as ArrayUtils from './array.js'


/**
 * Use Object's `hasOwnProperty` and never rely on whatever `hasOwnProperty` incoming objects have.
 * @private
 * @param {Object} obj
 * @param {String} prop
 * @returns {Boolean}
 */
function hasOwnProperty(obj, prop) {
    return Object.prototype.hasOwnProperty.call(obj, prop)
}

/**
 * Tell if a value is a pure object.
 * @param {*} value value to test
 * @returns {Boolean}
 */
export function isObject(value) {
    return (value != null) && (value.constructor === Object)
}

/**
 * Create an object from a list of entries. Each entry is an array of two elements: the key and its associated value.
 * @alias qui.utils.object.fromEntries
 * @param {Array[]} entries
 * @returns {Object}
 */
export function fromEntries(entries) {
    let obj = {}
    entries.forEach(function (entry) {
        obj[entry[0]] = entry[1]
    })

    return obj
}

/**
 * Search an object for an entry that matches a condition and return the corresponding key.
 * @alias qui.utils.object.findKey
 * @param {Object} obj
 * @param {Function} func function that implements the search condition; it is called with value and key as arguments
 * @param {*} [thisArg] optional `this` argument to be used when calling `func`
 * @returns {String} the matched key, or `undefined` if no entry matched
 */
export function findKey(obj, func, thisArg = null) {
    let entry = Object.entries(obj).find(function (entry) {
        if (func.call(thisArg, entry[1], entry[0])) {
            return true
        }
    })

    if (entry) {
        return entry[0]
    }

    return undefined
}

/**
 * Search an object for an entry that matches a condition and return the corresponding value.
 * @alias qui.utils.object.findValue
 * @param {Object} obj
 * @param {Function} func function that implements the search condition; it is called with key and value as arguments
 * @param {*} [thisArg] optional `this` argument to be used when calling `func`
 * @returns {String} the matched value, or `undefined` if no entry matched
 */
export function findValue(obj, func, thisArg = null) {
    let entry = Object.entries(obj).find(function (entry) {
        if (func.call(thisArg, entry[0], entry[1])) {
            return true
        }
    })

    if (entry) {
        return entry[1]
    }

    return undefined
}

/**
 * Parse an object, calling a function for each entry.
 * @alias qui.utils.object.forEach
 * @param {Object} obj
 * @param {Function} func function to be called for each entry; it is called with the entry (array of key and value) as
 * argument
 * @param {*} [thisArg] optional `this` argument to be used when calling `func`
 * @param {Function} [sortKeyFunc] optional function used to extract key from each entry when sorting entries; no
 * sorting is performed unless this function is supplied
 */
export function forEach(obj, func, thisArg = null, sortKeyFunc = null) {
    let entries = Object.entries(obj)
    if (sortKeyFunc) {
        ArrayUtils.sortKey(entries, sortKeyFunc)
    }
    entries.forEach(function (entry) {
        func.call(thisArg, entry[0], entry[1])
    })
}

/**
 * Filter object entries. This function is similar to Array's `filter` method, but applied on objects.
 * @alias qui.utils.object.filter
 * @param {Object} obj
 * @param {Function} func a function called with each key and value as arguments; only entries for which this function
 * returns a true value will be kept
 * @param {*} [thisArg] optional `this` argument to be used when calling `func`
 * @returns {Object} the filtered object
 */
export function filter(obj, func, thisArg = null) {
    return fromEntries(Object.entries(obj).filter(entry => func.call(thisArg, entry[0], entry[1])))
}

/**
 * Map object entries. This function is similar to Array's `map` method, but applied on objects.
 * @alias qui.utils.object.map
 * @param {Object} obj
 * @param {Function} func a function called with each key and value as arguments; it is expected to return an array with
 * two elements, the mapped key and value
 * @param {*} [thisArg] optional `this` argument to be used when calling `func`
 * @returns {Object} the mapped object
 */
export function map(obj, func, thisArg = null) {
    return fromEntries(Object.entries(obj).map(entry => func.call(thisArg, entry[0], entry[1])))
}

/**
 * Map object keys.
 * @alias qui.utils.object.mapKey
 * @param {Object} obj
 * @param {Function} func a function called with each key and value as arguments; it is expected to return the mapped
 * key
 * @param {*} [thisArg] optional `this` argument to be used when calling `func`
 * @returns {Object} the mapped object
 */
export function mapKey(obj, func, thisArg = null) {
    return fromEntries(Object.entries(obj).map(entry => [func.call(thisArg, entry[0], entry[1]), entry[1]]))
}

/**
 * Map object values.
 * @alias qui.utils.object.mapValue
 * @param {Object} obj
 * @param {Function} func a function called with each value and key as arguments; it is expected to return the mapped
 * value
 * @param {*} [thisArg] optional `this` argument to be used when calling `func`
 * @returns {Object} the mapped object
 */
export function mapValue(obj, func, thisArg = null) {
    return fromEntries(Object.entries(obj).map(entry => [entry[0], func.call(thisArg, entry[1], entry[0])]))
}

/**
 * Assign values to an object, *in place*, but only if corresponding keys are missing.
 * @alias qui.utils.object.assignDefault
 * @param {Object} dest
 * @param {Object} src
 * @returns {Object} the given `dest` object
 */
export function assignDefault(dest, src) {
    Object.keys(src).forEach(function (key) {
        if (key in dest) {
            return
        }

        dest[key] = src[key]
    })

    return dest
}

/**
 * Combine two or more objects into a single object by merging their keys and values.
 * @alias qui.utils.object.combine
 * @param {...Object} objs objects to combine
 * @returns {Object} the combined object
 */
export function combine(...objs) {
    let combined = {}
    objs.forEach(function (obj) {
        Object.keys(obj).forEach(function (key) {
            combined[key] = obj[key]
        })
    })

    return combined
}

/**
 * Clone an object by copying its entries into a new object. Optionally recurse into inner objects and arrays.
 * @alias qui.utils.object.copy
 * @param {Object} orig original object to copy
 * @param {Boolean} [deep] set to `true` to perform a *deep* copy (defaults to `false`)
 * @returns {Object}
 */
export function copy(orig, deep = false) {
    if (deep) {
        let type = typeof orig
        if (orig === undefined || orig === null ||
            type === 'number' || type === 'string' ||
            type === 'boolean' || type === 'function') {

            return orig
        }
        else if (Array.isArray(orig)) {
            return orig.map(e => copy(e, /* deep = */ true))
        }
        else if (orig.constructor === Object) { /* Plain object */
            return map(orig, (k, v) => [k, copy(v, /* deep = */ true)])
        }
        else {
            return orig
        }
    }
    else {
        return fromEntries(Object.entries(orig))
    }
}

/**
 * Remove a key from an object, returning it.
 * @alias qui.utils.object.pop
 * @param {Object} obj
 * @param {String} key
 * @param {*} [def] an optional default value to return if key is missing
 * @returns {*}
 */
export function pop(obj, key, def = null) {
    if (!(key in obj)) {
        return def
    }

    let value = obj[key]
    delete obj[key]

    return value
}

/**
 * Insert a value into an object, but only if it's not already present, and return it.
 * @alias qui.utils.object.setDefault
 * @param {Object} obj
 * @param {String} key
 * @param {*} value
 * @returns {*}
 */
export function setDefault(obj, key, value) {
    if (key in obj) {
        return obj[key]
    }

    return (obj[key] = value)
}

/**
 * A deep equals() operator that will recursively compare any non-primitive objects to validate the equality.
 * @alias qui.utils.object.deepEquals
 * @param {Object} obj1 the first object of the comparison
 * @param {Object} obj2 the second object of the comparison
 * @returns {Boolean} `true` if the two objects are deeply equal, `false`otherwise
 */
export function deepEquals(obj1, obj2) {
    if (obj1 === obj2) {
        return true
    }

    if (typeof obj1 !== typeof obj2) {
        return false
    }

    if (Array.isArray(obj1)) {
        if (!Array.isArray(obj2)) {
            return false
        }

        if (obj1.length !== obj2.length) {
            return false
        }

        for (let i = 0; i < obj1.length; i++) {
            if (!deepEquals(obj1[i], obj2[i])) {
                return false
            }
        }

        return true
    }
    else if (obj1 instanceof Object) {
        if (!(obj2 instanceof Object)) {
            return false
        }

        for (let key in obj1) {
            if (hasOwnProperty(obj1, key)) {
                if (!hasOwnProperty(obj2, key)) {
                    return false
                }

                if (!deepEquals(obj1[key], obj2[key])) {
                    return false
                }
            }
        }

        for (let key in obj2) {
            if (hasOwnProperty(obj2, key)) {
                if (!hasOwnProperty(obj1, key)) {
                    return false
                }
            }
        }

        return true
    }
    else {
        return false
    }
}