diff --git a/.vscode/settings.json b/.vscode/settings.json index 4151e9d..3278f70 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,5 +2,6 @@ "editor.tabSize": 2, "editor.insertSpaces": true, "editor.defaultFormatter": "esbenp.prettier-vscode", - "editor.formatOnSave": true + "editor.formatOnSave": true, + "files.insertFinalNewline": true } diff --git a/package-lock.json b/package-lock.json index cfe6978..cc8cad9 100755 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,13 @@ { "name": "jsonpath-js", - "version": "0.0.1", + "version": "0.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "jsonpath-js", - "version": "0.0.1", + "version": "0.1.1", "license": "MIT", - "dependencies": { - "es-toolkit": "^1.27.0" - }, "devDependencies": { "@types/jest": "^29.5.8", "@types/node": "^18.7.3", @@ -32,7 +29,7 @@ "vitest": "^2.1.4" }, "engines": { - "node": ">=14" + "node": ">=18" } }, "node_modules/@babel/code-frame": { @@ -2274,15 +2271,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/es-toolkit": { - "version": "1.27.0", - "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.27.0.tgz", - "integrity": "sha512-ETSFA+ZJArcuSCpzD2TjAy6UHpx4E4uqFsoDg9F/nTLogrLmVVZQ+zNxco5h7cWnA1nNak07IXsLcaSMih+ZPQ==", - "workspaces": [ - "docs", - "benchmarks" - ] - }, "node_modules/esbuild": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", @@ -6826,10 +6814,11 @@ } }, "node_modules/vite": { - "version": "5.4.11", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz", - "integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==", + "version": "5.4.14", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.14.tgz", + "integrity": "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==", "dev": true, + "license": "MIT", "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", diff --git a/package.json b/package.json index fff8eb5..0601214 100755 --- a/package.json +++ b/package.json @@ -56,7 +56,5 @@ "typescript": "^5.5.3", "vitest": "^2.1.4" }, - "dependencies": { - "es-toolkit": "^1.27.0" - } -} \ No newline at end of file + "dependencies": {} +} diff --git a/src/comparator/ArrayComparator.ts b/src/comparator/ArrayComparator.ts index f62f467..06aa09b 100644 --- a/src/comparator/ArrayComparator.ts +++ b/src/comparator/ArrayComparator.ts @@ -1,6 +1,6 @@ import { JsonArray } from "../types/json"; +import { isEqual } from "../utils/isEqual"; import { ComparisonOperators } from "./ComparisonOperators"; -import { isEqual } from "es-toolkit"; // equal arrays, that is, arrays of the same length where each element of // the first array is equal to the corresponding element of the second array, or diff --git a/src/comparator/ObjectComparator.ts b/src/comparator/ObjectComparator.ts index 13ba4e2..468ef54 100644 --- a/src/comparator/ObjectComparator.ts +++ b/src/comparator/ObjectComparator.ts @@ -1,5 +1,5 @@ -import { isEqual } from "es-toolkit"; import { JsonObject } from "../types/json"; +import { isEqual } from "../utils/isEqual"; import { ComparisonOperators } from "./ComparisonOperators"; // equal objects with no duplicate names, that is, where: diff --git a/src/utils/isEqual.ts b/src/utils/isEqual.ts new file mode 100644 index 0000000..25428e3 --- /dev/null +++ b/src/utils/isEqual.ts @@ -0,0 +1,61 @@ +import { JsonValue } from "../types/json"; + +/** + * Check the deep equality of two JSON values. + * @param a - The first JSON value. + * @param b - The second JSON value. + * @returns `true` if the two JSON values are equal, `false` otherwise. + */ +export function isEqual(a: JsonValue, b: JsonValue): boolean { + return isEqualImpl(a, b); +} + +function isEqualImpl( + a: JsonValue, + b: JsonValue, + visited: WeakMap = new WeakMap(), +): boolean { + if (a === b) { + return true; + } + + if (typeof a !== typeof b) { + return false; + } + + if (a === null || b === null) { + return false; + } + + if (Array.isArray(a) && Array.isArray(b)) { + if (a.length !== b.length) { + return false; + } + + return a.every((value, index) => isEqualImpl(value, b[index], visited)); + } + + if (Array.isArray(a) || Array.isArray(b)) { + return false; + } + + if (typeof a === "object" && typeof b === "object") { + // Check circular references + if (visited.has(a)) { + return visited.get(a) === b; + } + + visited.set(a, b); + + const keysA = Object.keys(a); + const keysB = Object.keys(b); + + if (keysA.length !== keysB.length) { + return false; + } + + return keysA.every((key) => isEqualImpl(a[key], b[key], visited)); + } + + return false; +} diff --git a/tests/utils/isEqual.test.ts b/tests/utils/isEqual.test.ts new file mode 100644 index 0000000..6406e61 --- /dev/null +++ b/tests/utils/isEqual.test.ts @@ -0,0 +1,72 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { describe, expect, test } from "vitest"; +import { isEqual } from "../../src/utils/isEqual"; + +describe("isEqual", () => { + test("should return true for equal primitive values", () => { + expect(isEqual(1, 1)).toBe(true); + expect(isEqual(1.2, 1.2)).toBe(true); + expect(isEqual("a", "a")).toBe(true); + expect(isEqual(true, true)).toBe(true); + expect(isEqual(null, null)).toBe(true); + }); + + test("should return false for different primitive values", () => { + expect(isEqual(1, 2)).toBe(false); + expect(isEqual(1.2, 2.1)).toBe(false); + expect(isEqual("a", "b")).toBe(false); + expect(isEqual(true, false)).toBe(false); + expect(isEqual(null, 1)).toBe(false); + }); + + test("should return true for equal arrays", () => { + expect(isEqual([], [])).toBe(true); + expect(isEqual([1, 2, 3], [1, 2, 3])).toBe(true); + expect(isEqual([1, 2, [3, 4]], [1, 2, [3, 4]])).toBe(true); + }); + + test("should return false for different arrays", () => { + expect(isEqual([], [1])).toBe(false); + expect(isEqual([1, 2, 3], [1, 2, 4])).toBe(false); + expect(isEqual([1, 2, [3, 4]], [1, 2, [3, 5]])).toBe(false); + }); + + test("should return true for equal objects", () => { + expect(isEqual({}, {})).toBe(true); + expect(isEqual({ a: 1, b: 2 }, { a: 1, b: 2 })).toBe(true); + }); + + test("should return true for nested equal objects", () => { + expect(isEqual({ a: 1, b: { c: "2" } }, { a: 1, b: { c: "2" } })).toBe( + true, + ); + }); + + test("should return false for different objects", () => { + expect(isEqual({}, { a: 1 })).toBe(false); + expect(isEqual({ a: 1, b: 2 }, { a: 1, b: 3 })).toBe(false); + }); + + test("should return true for object that has circular dependency", () => { + const a: any = { foo: "bar" }; + const b: any = { foo: "bar" }; + + a.self = a; + b.self = b; + + expect(isEqual(a, b)).toBe(true); + }); + + test("should return false for different object that has circular dependency", () => { + const aChild: any = { baz: "qux" }; + const bChild: any = { baz: "quux" }; + + aChild.self = aChild; + bChild.self = bChild; + + const a = { foo: "bar", child: aChild }; + const b = { foo: "bar", child: bChild }; + + expect(isEqual(a, b)).toBe(false); + }); +});