@schukai/monster
Version:
Monster is a simple library for creating fast, robust and lightweight websites.
186 lines (162 loc) • 4.1 kB
JavaScript
/**
* Copyright © schukai GmbH and all contributing authors, {{copyRightYear}}. All rights reserved.
* Node module: @schukai/monster
*
* This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3).
* The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html
*
* For those who do not wish to adhere to the AGPLv3, a commercial license is available.
* Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms.
* For more information about purchasing a commercial license, please contact schukai GmbH.
*
* SPDX-License-Identifier: AGPL-3.0
*/
import { isArray, isObject } from "../types/is.mjs";
import { typeOf } from "../types/typeof.mjs";
export { diff };
/**
* 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.
*
* @externalExample ../../example/data/diff.mjs
* @param {*} first
* @param {*} second
* @return {array}
* @license AGPLv3
* @since 1.6.0
* @copyright schukai GmbH
*/
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) {
const typeA = typeOf(a);
const typeB = typeOf(b);
const currPath = path || [];
const currDiff = diff || [];
if (typeA === typeB && (typeA === "object" || typeA === "array")) {
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}
*/
const typeA = typeof a;
/**
* @type {string}
*/
const 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;
}