From fee7ae6f7b4edb01779f9f60295d82f96596f034 Mon Sep 17 00:00:00 2001 From: DoneDeal0 Date: Wed, 11 Jan 2023 15:42:16 +0100 Subject: [PATCH] feat: add buymeacoffee link + discardArrayOrder option --- README.md | 25 ++++++++++ dist/index.d.ts | 7 ++- dist/index.js | 35 +++++++++----- dist/index.mjs | 35 +++++++++----- package.json | 2 +- src/model.ts | 1 + src/object-diff.ts | 36 ++++++++++----- src/utils.ts | 13 +++++- test/object-diff.test.ts | 98 ++++++++++++++++++++++++++++++++++++++++ 9 files changed, 213 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 92d9c59..45c3b0c 100644 --- a/README.md +++ b/README.md @@ -394,10 +394,35 @@ false; More examples are availble in the tests of the source code. +
+ +### OPTIONS + +`getObjectDiff()` and `isEqual()` accept a facultative `options` parameter: + +```ts +{ + discardArrayOrder?: boolean // false by default +} +``` + +If `discardArrayOrder` is set to `true`, `["hello", "world"]` and `["world", "hello"]` will be considered as `equal`, because the two arrays have the same value, just not in the same order. + ## CREDITS DoneDeal0 +## SUPPORT + +If you use Superdiff, please show your support by buying me coffee: +https://www.buymeacoffee.com/donedeal0 + +
+ + + +
+ ## CONTRIBUTING Pull requests are welcome! diff --git a/dist/index.d.ts b/dist/index.d.ts index 198eef1..ad1b53b 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts @@ -1,6 +1,9 @@ type DiffStatus = "added" | "equal" | "moved" | "deleted" | "updated"; type ObjectData = Record | undefined | null; type ListData = any; +type Options = { + discardArrayOrder?: boolean; +}; type ListDiff = { type: "list"; status: DiffStatus; @@ -31,11 +34,11 @@ type ObjectDiff = { }[]; }; -declare function getObjectDiff(prevData: ObjectData, nextData: ObjectData): ObjectDiff; +declare function getObjectDiff(prevData: ObjectData, nextData: ObjectData, options?: Options): ObjectDiff; declare const getListDiff: (prevList: ListData[] | undefined | null, nextList: ListData[] | undefined | null) => ListDiff; -declare function isEqual(a: any, b: any): boolean; +declare function isEqual(a: any, b: any, options?: Options): boolean; declare function isObject(value: any): value is Record; export { getListDiff, getObjectDiff, isEqual, isObject }; diff --git a/dist/index.js b/dist/index.js index bf46d63..ba9fe3a 100644 --- a/dist/index.js +++ b/dist/index.js @@ -10,13 +10,18 @@ var STATUS = { }; // src/utils.ts -function isEqual(a, b) { +function isEqual(a, b, options = { discardArrayOrder: false }) { if (typeof a !== typeof b) return false; if (Array.isArray(a)) { if (a.length !== b.length) { return false; } + if (options.discardArrayOrder) { + return a.every( + (v) => b.some((nextV) => JSON.stringify(nextV) === JSON.stringify(v)) + ); + } return a.every((v, i) => JSON.stringify(v) === JSON.stringify(b[i])); } if (typeof a === "object") { @@ -73,17 +78,17 @@ function formatSingleObjectDiff(data, status) { diff }; } -function getPreviousMatch(previousValue, nextSubProperty) { +function getPreviousMatch(previousValue, nextSubProperty, options) { if (!previousValue) { return void 0; } const previousMatch = Object.entries(previousValue).find( - ([subPreviousKey]) => isEqual(subPreviousKey, nextSubProperty) + ([subPreviousKey]) => isEqual(subPreviousKey, nextSubProperty, options) ); return previousMatch ? previousMatch[1] : void 0; } -function getValueStatus(previousValue, nextValue) { - if (isEqual(previousValue, nextValue)) { +function getValueStatus(previousValue, nextValue, options) { + if (isEqual(previousValue, nextValue, options)) { return STATUS.EQUAL; } return STATUS.UPDATED; @@ -105,7 +110,7 @@ function getDeletedProperties(previousValue, nextValue) { } return void 0; } -function getSubPropertiesDiff(previousValue, nextValue) { +function getSubPropertiesDiff(previousValue, nextValue, options) { const subPropertiesDiff = []; let subDiff; const deletedMainSubProperties = getDeletedProperties( @@ -123,7 +128,11 @@ function getSubPropertiesDiff(previousValue, nextValue) { }); } Object.entries(nextValue).forEach(([nextSubProperty, nextSubValue]) => { - const previousMatch = getPreviousMatch(previousValue, nextSubProperty); + const previousMatch = getPreviousMatch( + previousValue, + nextSubProperty, + options + ); if (!!!previousMatch && !!nextSubProperty) { return subPropertiesDiff.push({ name: nextSubProperty, @@ -135,7 +144,8 @@ function getSubPropertiesDiff(previousValue, nextValue) { if (isObject(nextSubValue)) { const data = getSubPropertiesDiff( previousMatch, - nextSubValue + nextSubValue, + options ); if (data && data.length > 0) { subDiff = data; @@ -146,14 +156,14 @@ function getSubPropertiesDiff(previousValue, nextValue) { name: nextSubProperty, previousValue: previousMatch, currentValue: nextSubValue, - status: getValueStatus(previousMatch, nextSubValue), + status: getValueStatus(previousMatch, nextSubValue, options), ...!!subDiff && { subDiff } }); } }); return subPropertiesDiff; } -function getObjectDiff(prevData, nextData) { +function getObjectDiff(prevData, nextData, options) { if (!prevData && !nextData) { return { type: "object", @@ -181,7 +191,8 @@ function getObjectDiff(prevData, nextData) { if (isObject(nextValue)) { const subPropertiesDiff = getSubPropertiesDiff( previousValue, - nextValue + nextValue, + options ); return diff.push({ property: nextProperty, @@ -195,7 +206,7 @@ function getObjectDiff(prevData, nextData) { property: nextProperty, previousValue, currentValue: nextValue, - status: getValueStatus(previousValue, nextValue) + status: getValueStatus(previousValue, nextValue, options) }); }); const deletedProperties = getDeletedProperties(prevData, nextData); diff --git a/dist/index.mjs b/dist/index.mjs index fdb89dd..68ef8e2 100644 --- a/dist/index.mjs +++ b/dist/index.mjs @@ -8,13 +8,18 @@ var STATUS = { }; // src/utils.ts -function isEqual(a, b) { +function isEqual(a, b, options = { discardArrayOrder: false }) { if (typeof a !== typeof b) return false; if (Array.isArray(a)) { if (a.length !== b.length) { return false; } + if (options.discardArrayOrder) { + return a.every( + (v) => b.some((nextV) => JSON.stringify(nextV) === JSON.stringify(v)) + ); + } return a.every((v, i) => JSON.stringify(v) === JSON.stringify(b[i])); } if (typeof a === "object") { @@ -71,17 +76,17 @@ function formatSingleObjectDiff(data, status) { diff }; } -function getPreviousMatch(previousValue, nextSubProperty) { +function getPreviousMatch(previousValue, nextSubProperty, options) { if (!previousValue) { return void 0; } const previousMatch = Object.entries(previousValue).find( - ([subPreviousKey]) => isEqual(subPreviousKey, nextSubProperty) + ([subPreviousKey]) => isEqual(subPreviousKey, nextSubProperty, options) ); return previousMatch ? previousMatch[1] : void 0; } -function getValueStatus(previousValue, nextValue) { - if (isEqual(previousValue, nextValue)) { +function getValueStatus(previousValue, nextValue, options) { + if (isEqual(previousValue, nextValue, options)) { return STATUS.EQUAL; } return STATUS.UPDATED; @@ -103,7 +108,7 @@ function getDeletedProperties(previousValue, nextValue) { } return void 0; } -function getSubPropertiesDiff(previousValue, nextValue) { +function getSubPropertiesDiff(previousValue, nextValue, options) { const subPropertiesDiff = []; let subDiff; const deletedMainSubProperties = getDeletedProperties( @@ -121,7 +126,11 @@ function getSubPropertiesDiff(previousValue, nextValue) { }); } Object.entries(nextValue).forEach(([nextSubProperty, nextSubValue]) => { - const previousMatch = getPreviousMatch(previousValue, nextSubProperty); + const previousMatch = getPreviousMatch( + previousValue, + nextSubProperty, + options + ); if (!!!previousMatch && !!nextSubProperty) { return subPropertiesDiff.push({ name: nextSubProperty, @@ -133,7 +142,8 @@ function getSubPropertiesDiff(previousValue, nextValue) { if (isObject(nextSubValue)) { const data = getSubPropertiesDiff( previousMatch, - nextSubValue + nextSubValue, + options ); if (data && data.length > 0) { subDiff = data; @@ -144,14 +154,14 @@ function getSubPropertiesDiff(previousValue, nextValue) { name: nextSubProperty, previousValue: previousMatch, currentValue: nextSubValue, - status: getValueStatus(previousMatch, nextSubValue), + status: getValueStatus(previousMatch, nextSubValue, options), ...!!subDiff && { subDiff } }); } }); return subPropertiesDiff; } -function getObjectDiff(prevData, nextData) { +function getObjectDiff(prevData, nextData, options) { if (!prevData && !nextData) { return { type: "object", @@ -179,7 +189,8 @@ function getObjectDiff(prevData, nextData) { if (isObject(nextValue)) { const subPropertiesDiff = getSubPropertiesDiff( previousValue, - nextValue + nextValue, + options ); return diff.push({ property: nextProperty, @@ -193,7 +204,7 @@ function getObjectDiff(prevData, nextData) { property: nextProperty, previousValue, currentValue: nextValue, - status: getValueStatus(previousValue, nextValue) + status: getValueStatus(previousValue, nextValue, options) }); }); const deletedProperties = getDeletedProperties(prevData, nextData); diff --git a/package.json b/package.json index 9060cfb..93d29e3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@donedeal0/superdiff", - "version": "1.0.4", + "version": "1.0.5", "description": "SuperDiff checks the changes between two objects or arrays. It returns a complete diff with relevant information for each property or piece of data", "main": "dist/index.js", "module": "dist/index.js", diff --git a/src/model.ts b/src/model.ts index 595d83b..71bd953 100644 --- a/src/model.ts +++ b/src/model.ts @@ -9,6 +9,7 @@ export const STATUS: Record = { export type DiffStatus = "added" | "equal" | "moved" | "deleted" | "updated"; export type ObjectData = Record | undefined | null; export type ListData = any; +export type Options = { discardArrayOrder?: boolean }; export type ListDiff = { type: "list"; diff --git a/src/object-diff.ts b/src/object-diff.ts index a64276a..cd2e83e 100644 --- a/src/object-diff.ts +++ b/src/object-diff.ts @@ -4,6 +4,7 @@ import { DiffStatus, STATUS, Subproperties, + Options, } from "./model"; import { isObject, isEqual } from "./utils"; @@ -60,19 +61,24 @@ function formatSingleObjectDiff( function getPreviousMatch( previousValue: any | undefined, - nextSubProperty: any + nextSubProperty: any, + options?: Options ): any | undefined { if (!previousValue) { return undefined; } const previousMatch = Object.entries(previousValue).find(([subPreviousKey]) => - isEqual(subPreviousKey, nextSubProperty) + isEqual(subPreviousKey, nextSubProperty, options) ); return previousMatch ? previousMatch[1] : undefined; } -function getValueStatus(previousValue: any, nextValue: any): DiffStatus { - if (isEqual(previousValue, nextValue)) { +function getValueStatus( + previousValue: any, + nextValue: any, + options?: Options +): DiffStatus { + if (isEqual(previousValue, nextValue, options)) { return STATUS.EQUAL; } return STATUS.UPDATED; @@ -103,7 +109,8 @@ function getDeletedProperties( function getSubPropertiesDiff( previousValue: Record | undefined, - nextValue: Record + nextValue: Record, + options?: Options ): Subproperties[] { const subPropertiesDiff: Subproperties[] = []; let subDiff: Subproperties[]; @@ -122,7 +129,11 @@ function getSubPropertiesDiff( }); } Object.entries(nextValue).forEach(([nextSubProperty, nextSubValue]) => { - const previousMatch = getPreviousMatch(previousValue, nextSubProperty); + const previousMatch = getPreviousMatch( + previousValue, + nextSubProperty, + options + ); if (!!!previousMatch && !!nextSubProperty) { return subPropertiesDiff.push({ name: nextSubProperty, @@ -134,7 +145,8 @@ function getSubPropertiesDiff( if (isObject(nextSubValue)) { const data: Subproperties[] = getSubPropertiesDiff( previousMatch, - nextSubValue + nextSubValue, + options ); if (data && data.length > 0) { subDiff = data; @@ -145,7 +157,7 @@ function getSubPropertiesDiff( name: nextSubProperty, previousValue: previousMatch, currentValue: nextSubValue, - status: getValueStatus(previousMatch, nextSubValue), + status: getValueStatus(previousMatch, nextSubValue, options), ...(!!subDiff && { subDiff }), }); } @@ -155,7 +167,8 @@ function getSubPropertiesDiff( export function getObjectDiff( prevData: ObjectData, - nextData: ObjectData + nextData: ObjectData, + options?: Options ): ObjectDiff { if (!prevData && !nextData) { return { @@ -184,7 +197,8 @@ export function getObjectDiff( if (isObject(nextValue)) { const subPropertiesDiff: Subproperties[] = getSubPropertiesDiff( previousValue, - nextValue + nextValue, + options ); return diff.push({ property: nextProperty, @@ -198,7 +212,7 @@ export function getObjectDiff( property: nextProperty, previousValue, currentValue: nextValue, - status: getValueStatus(previousValue, nextValue), + status: getValueStatus(previousValue, nextValue, options), }); }); const deletedProperties = getDeletedProperties(prevData, nextData); diff --git a/src/utils.ts b/src/utils.ts index c526893..be4d961 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,9 +1,20 @@ -export function isEqual(a: any, b: any): boolean { +import { Options } from "./model"; + +export function isEqual( + a: any, + b: any, + options: Options = { discardArrayOrder: false } +): boolean { if (typeof a !== typeof b) return false; if (Array.isArray(a)) { if (a.length !== b.length) { return false; } + if (options.discardArrayOrder) { + return a.every((v) => + b.some((nextV: any) => JSON.stringify(nextV) === JSON.stringify(v)) + ); + } return a.every((v, i) => JSON.stringify(v) === JSON.stringify(b[i])); } if (typeof a === "object") { diff --git a/test/object-diff.test.ts b/test/object-diff.test.ts index 00fa639..36cc1d7 100644 --- a/test/object-diff.test.ts +++ b/test/object-diff.test.ts @@ -301,4 +301,102 @@ describe("getObjectDiff", () => { ], }); }); + it("detects changed between two objects BUT doesn't care about array order as long as all values are preserved", () => { + expect( + getObjectDiff( + { + id: 54, + type: "sport", + user: { + name: "joe", + member: true, + hobbies: ["golf", "football"], + age: 66, + }, + }, + { + id: 54, + country: "us", + user: { + name: "joe", + member: false, + hobbies: ["football", "golf"], + nickname: "super joe", + }, + }, + { discardArrayOrder: true } + ) + ).toStrictEqual({ + type: "object", + status: "updated", + diff: [ + { + property: "id", + previousValue: 54, + currentValue: 54, + status: "equal", + }, + { + property: "country", + previousValue: undefined, + currentValue: "us", + status: "added", + }, + { + property: "user", + previousValue: { + name: "joe", + member: true, + hobbies: ["golf", "football"], + age: 66, + }, + currentValue: { + name: "joe", + member: false, + hobbies: ["football", "golf"], + nickname: "super joe", + }, + status: "updated", + subPropertiesDiff: [ + { + name: "age", + previousValue: 66, + currentValue: undefined, + status: "deleted", + }, + { + name: "name", + previousValue: "joe", + currentValue: "joe", + status: "equal", + }, + { + name: "member", + previousValue: true, + currentValue: false, + status: "updated", + }, + { + name: "hobbies", + previousValue: ["golf", "football"], + currentValue: ["football", "golf"], + status: "equal", + }, + { + name: "nickname", + previousValue: undefined, + currentValue: "super joe", + status: "added", + }, + ], + }, + { + property: "type", + previousValue: "sport", + currentValue: undefined, + status: "deleted", + }, + ], + }); + }); });