Select Git revision
diff.js 5.24 KiB
'use strict';
/**
* @author schukai GmbH
*/
import {assignToNamespace, Monster} from '../namespace.js';
import {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.16.1/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 {Diff} from 'https://cdn.jsdelivr.net/npm/@schukai/monster@1.16.1/dist/modules/data/diff.js';
* console.log(Diff(a, b))
* </script>
* ```
*
* @example
*
* import {Diff} from 'https://cdn.jsdelivr.net/npm/@schukai/monster@1.16.1/dist/modules/data/diff.js';
*
* // 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;
}
assignToNamespace('Monster.Data', Diff);
export {Monster, Diff}