data/diff.js

'use strict';

/**
 * @author schukai GmbH
 */


import {Monster, isArray, isObject} from "../types/is.js";

/**
 * With the diff function you can perform the change of one object to another. The result shows the changes of the second object to the first object.
 *
 * The operator `add` means that something has been added to the second object. `delete` means that something has been deleted from the second object compared to the first object.
 *
 * you can call the method via the monster namespace `Monster.Data.Diff()`.
 *
 * ```
 * <script type="module">
 * import {Monster} from 'https://cdn.jsdelivr.net/npm/@schukai/monster@1.6.0/dist/modules/data/diff.js';
 * console.log(Monster.Data.Diff(a, b))
 * </script>
 * ```
 *
 * Alternatively, you can also integrate this function individually.
 *
 * ```
 * <script type="module">
 * import {Pipe} from 'https://cdn.jsdelivr.net/npm/@schukai/monster@1.6.0/dist/modules/data/diff.js';
 * console.log(Diff(a, b))
 * </script>
 * ```
 *
 * given are two objects x and y.
 *
 * ```
 * let x = {
 *     a: 1,
 *     b: "Hello!"
 * }
 *
 *  let y = {
 *     a: 2,
 *     c: true
 * }
 * ```
 *
 * These two objects can be compared with each other.
 *
 * ```
 *  console.log(Diff(x, y));
 * ```
 *
 * the result is then the following
 *
 * ```
 *  [
 *  {
 *         operator: 'update',
 *         path: [ 'a' ],
 *         first: { value: 1, type: 'number' },
 *         second: { value: 2, type: 'number' }
 *     },
 *  {
 *         operator: 'delete',
 *         path: [ 'b' ],
 *         first: { value: 'Hello!', type: 'string' }
 *     },
 *  {
 *         operator: 'add',
 *         path: [ 'c' ],
 *         second: { value: true, type: 'boolean' }
 *     }
 *  ]
 * ```
 *
 * @param {*} first
 * @param {*} second
 * @return {array}
 * @since 1.6.0
 * @copyright schukai GmbH
 * @memberOf Monster/Data
 */
function Diff(first, second) {
    return doDiff(first, second)
}

/**
 * @private
 * @param a
 * @param b
 * @param type
 * @return {Set<string>|Set<number>}
 */
function getKeys(a, b, type) {
    if (isArray(type)) {
        const keys = a.length > b.length ? new Array(a.length) : new Array(b.length);
        keys.fill(0);
        return new Set(keys.map((_, i) => i));
    }

    return new Set(Object.keys(a).concat(Object.keys(b)));
}

/**
 * @private
 * @param a
 * @param b
 * @param path
 * @param diff
 * @return {array}
 */
function doDiff(a, b, path, diff) {

    let typeA = typeof a
    let typeB = typeof b

    const currPath = path || [];
    const currDiff = diff || [];

    if (typeA === typeB && typeA === 'object') { // array is object too

        getKeys(a, b, typeA).forEach((v) => {

            if (!(Object.prototype.hasOwnProperty.call(a, v))) {
                currDiff.push(buildResult(a[v], b[v], 'add', currPath.concat(v)));
            } else if (!(Object.prototype.hasOwnProperty.call(b, v))) {
                currDiff.push(buildResult(a[v], b[v], 'delete', currPath.concat(v)));
            } else {
                doDiff(a[v], b[v], currPath.concat(v), currDiff);
            }
        });

    } else {

        const o = getOperator(a, b, typeA, typeB);
        if (o !== undefined) {
            currDiff.push(buildResult(a, b, o, path));
        }

    }

    return currDiff;

}

/**
 *
 * @param {*} a
 * @param {*} b
 * @param {string} operator
 * @param {array} path
 * @return {{path: array, operator: string}}
 * @private
 */
function buildResult(a, b, operator, path) {

    const result = {
        operator,
        path,
    };

    if (operator !== 'add') {
        result.first = {
            value: a,
            type: typeof a
        };

        if (isObject(a)) {
            const name = Object.getPrototypeOf(a)?.constructor?.name;
            if (name !== undefined) {
                result.first.instance = name;
            }
        }
    }

    if (operator === 'add' || operator === 'update') {
        result.second = {
            value: b,
            type: typeof b
        };

        if (isObject(b)) {
            const name = Object.getPrototypeOf(b)?.constructor?.name;
            if (name !== undefined) {
                result.second.instance = name;
            }
        }

    }

    return result;
}

/**
 * @private
 * @param {*} a
 * @param {*} b
 * @return {boolean}
 */
function isNotEqual(a, b) {

    if (typeof a !== typeof b) {
        return true;
    }

    if (a instanceof Date && b instanceof Date) {
        return a.getTime() !== b.getTime();
    }

    return a !== b;
}

/**
 * @private
 * @param {*} a
 * @param {*} b
 * @return {string|undefined}
 */
function getOperator(a, b) {

    /**
     * @type {string|undefined}
     */
    let operator;

    /**
     * @type {string}
     */
    let typeA = typeof a;

    /**
     * @type {string}
     */
    let typeB = typeof b;

    if (typeA === 'undefined' && typeB !== 'undefined') {
        operator = 'add';
    } else if (typeA !== 'undefined' && typeB === 'undefined') {
        operator = 'delete';
    } else if (isNotEqual(a, b)) {
        operator = 'update';
    }

    return operator;

}

Monster.assignToNamespace('Monster.Data', Diff);
export {Monster, Diff}