Skip to content

Commit 1f13c10

Browse files
committed
Initial commit
1 parent 84e469c commit 1f13c10

File tree

8 files changed

+528
-2
lines changed

8 files changed

+528
-2
lines changed

README.md

Lines changed: 89 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,89 @@
1-
# da-testdiff
2-
The "testDiff" deep diff/test function from Differentia.js, ported to TypeScript. Returns true if input 1 differs in any way from input 2. Performs deep object search by default, works OK with circular references.
1+
# js-testDiff
2+
3+
- [About]("#about")
4+
- [Building]("#building")
5+
- [Syntax]("#syntax")
6+
- [Usage Examples]("#usage-examples")
7+
8+
## About
9+
10+
Deep object diffing function for JavaScript; returns true if input 1 differs in any way from input 2. Original code was taken from Differentia.js and ported to TypeScript.
11+
12+
The testDiff function was originally created as a unit testing utility and it was primarily designed/used to test Differentia's search algorithm strategy system, setting a "gold standard" for that library's quality and algorithmic correctness.
13+
14+
The key difference with this version of testDiff is that I removed its "search index" functionality, as it introduced more complexity than it was worth.
15+
16+
Feel free to scavenge the original code as I have: https://github.com/Floofies/Differentia.js/blob/master/spec/testUtils.js
17+
18+
## Building
19+
20+
Run `npm run build` to compile and test module `dist/index.js`.
21+
22+
I have pre-compiled the most up-to-date files in `dist`. Enjoy.
23+
24+
`unitTest.js` can be safely ignored, as it's a development-only dependency for `test.mjs`.
25+
26+
## Syntax
27+
28+
```JavaScript
29+
import { testDiff } from "testDiff";
30+
```
31+
```JavaScript
32+
testDiff( input1:any, input2:any, [ deep:boolean = false ] );
33+
```
34+
35+
### Parameters:
36+
37+
#### `input1`, `input2`
38+
39+
Two values/objects to compare against each other.
40+
41+
#### `deep` (_Optional_) (_Default = `true`_)
42+
43+
TestDiff performs "deep" object/array traversal by default, comparing all reachable values; set this operand to `false` to disable traversal and nested comparisons.
44+
45+
### Return Value:
46+
47+
Returns `true` if `input1`'s structure, properties, or values differ in any way from `input2`, or `false` if otherwsie.
48+
49+
## Usage Examples
50+
51+
### Example 1: Arbitrary values.
52+
53+
Can handle any arbitrary values, as well as objects/arrays.
54+
55+
```JavaScript
56+
const myArray1 = "Hello World!";
57+
const myArray2 = "This is a test";
58+
const result = testDiff(myArray1, myArray2);
59+
// result = true
60+
```
61+
62+
### Example 2: Flat arrays/objects.
63+
64+
```JavaScript
65+
const myArray1 = [1,2,3];
66+
const myArray2 = [4,5,6];
67+
const result = testDiff(myArray1, myArray2);
68+
// result = true
69+
```
70+
71+
### Example 3: Nested arrays/objects.
72+
73+
```JavaScript
74+
const myArray1 = ["Hello",["World!"]];
75+
const myArray2 = ["Hello",["Developer!"]];
76+
const result = testDiff(myArray1, myArray2);
77+
// result = true
78+
```
79+
80+
### Example 4: Nested arrays. Traversal disabled.
81+
82+
In this example, the function returns `false` even though the arrays' contents differ; they are regarded as the same because there are no differences at the top, and disabling traversal prevents the algorithm from seeing deeper differences.
83+
84+
```JavaScript
85+
const myArray1 = ["Hello",["World!"]];
86+
const myArray2 = ["Hello",["Developer!"]];
87+
const result = testDiff(myArray1, myArray2, false);
88+
// result = false
89+
```

dist/index.js

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
"use strict";
2+
Object.defineProperty(exports, "__esModule", { value: true });
3+
exports.testDiff = void 0;
4+
function testValue(value1, value2) {
5+
if ((typeof value1) !== (typeof value2))
6+
return true;
7+
if (Number.isNaN(value1) !== Number.isNaN(value2))
8+
return true;
9+
if ((value1 !== value2))
10+
return true;
11+
}
12+
// Returns true if obj1 differs in any way from obj2.
13+
function testDiff(obj1, obj2, deep) {
14+
if (deep === void 0) { deep = true; }
15+
if (obj1 === null)
16+
return obj1 !== obj2;
17+
// Cheap comparisons first
18+
if (((typeof obj1) !== "object") && testValue(obj1, obj2))
19+
return true;
20+
var stack = [{ obj1: obj1, obj2: obj2 }];
21+
var seen = new Map();
22+
seen.set(obj1, stack[0]);
23+
var _loop_1 = function () {
24+
var objects = stack.pop();
25+
if (Array.isArray(objects.obj1) !== Array.isArray(objects.obj2))
26+
return { value: true };
27+
var props1 = Object.keys(objects.obj1);
28+
var props2 = Object.keys(objects.obj2);
29+
if (props1.length === 0 && props2.length === 0)
30+
return "continue";
31+
if (props1.length !== props2.length)
32+
return { value: true };
33+
if (!props1.every(function (value) { return props2.includes(value); }))
34+
return { value: true };
35+
if (!props2.every(function (value) { return props1.includes(value); }))
36+
return { value: true };
37+
_props: for (var loc = 0; loc < props1.length; loc++) {
38+
var prop = props1[loc];
39+
var value1 = objects.obj1[prop];
40+
var value2 = objects.obj2[prop];
41+
if ((typeof value1) !== (typeof value2))
42+
return { value: true };
43+
if (value1 === null || (typeof value1) !== "object") {
44+
if (testValue(value1, value2))
45+
return { value: true };
46+
continue _props;
47+
}
48+
if (value1 instanceof RegExp && value2 instanceof RegExp) {
49+
if (value1.source !== value2.source
50+
|| value1.ignoreCase !== value2.ignoreCase
51+
|| value1.global !== value2.global
52+
|| value1.multiline !== value2.multiline
53+
|| value1.sticky !== value2.sticky
54+
|| value1.unicode !== value2.unicode
55+
|| value1.flags !== value2.flags) {
56+
return { value: true };
57+
}
58+
continue _props;
59+
}
60+
if (seen.has(value1))
61+
continue _props;
62+
if (deep)
63+
stack.push({ obj1: value1, obj2: value2 });
64+
seen.set(value1, objects);
65+
}
66+
if (!deep)
67+
return { value: false };
68+
};
69+
_objects: while (stack.length !== 0) {
70+
var state_1 = _loop_1();
71+
if (typeof state_1 === "object")
72+
return state_1.value;
73+
}
74+
return false;
75+
}
76+
exports.testDiff = testDiff;

dist/lib/unitTest.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
"use strict";
2+
Object.defineProperty(exports, "__esModule", { value: true });
3+
exports.unitTest = void 0;
4+
// Low budget unit tests instead of Jasmine or Chai
5+
// Isolates between invocations. Safely contains everything that can go wrong.
6+
function unitTest(description, testFunction) {
7+
var testLog = ["Test run: ".concat(description, ":")];
8+
var expectQueue = [];
9+
var Expectation = /** @class */ (function () {
10+
function Expectation(anyValue) {
11+
this.actualValue = anyValue;
12+
return this;
13+
}
14+
Expectation.prototype.toBe = function (expectedValue) {
15+
var _this = this;
16+
expectQueue.push(function () {
17+
if (expectedValue !== _this.actualValue)
18+
throw new Error("Expected ".concat(expectedValue, " but received ").concat(_this.actualValue, " instead."));
19+
});
20+
};
21+
return Expectation;
22+
}());
23+
function expect(anyValue) {
24+
return new Expectation(anyValue);
25+
}
26+
var caughtError = false;
27+
try {
28+
testFunction(expect);
29+
if (expectQueue.length === 0) {
30+
testLog.push(" ❓ Test FAIL: No expectations were defined for this test.");
31+
process.exitCode = 1;
32+
caughtError = true;
33+
}
34+
}
35+
catch (error) {
36+
testLog.push("\t\u274C Test FAIL: ".concat(error.toString()));
37+
process.exitCode = 1;
38+
caughtError = true;
39+
}
40+
for (var _i = 0, expectQueue_1 = expectQueue; _i < expectQueue_1.length; _i++) {
41+
var exp = expectQueue_1[_i];
42+
try {
43+
exp();
44+
}
45+
catch (error) {
46+
testLog.push("\t\u274C Expectation FAIL: ".concat(error.toString()));
47+
process.exitCode = 1;
48+
caughtError = true;
49+
}
50+
}
51+
if (!caughtError)
52+
testLog.push(" 🟢 Test OK!");
53+
console.log(testLog.join("\n"));
54+
}
55+
exports.unitTest = unitTest;

package-lock.json

Lines changed: 22 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"name": "js-testdiff",
3+
"version": "1.0.0",
4+
"description": "The \"testDiff\" deep diff/test function from Differentia.js, ported to TypeScript. Returns true if input 1 differs in any way from input 2. Performs deep object search by default, works OK with circular references.",
5+
"main": "dist/index.js",
6+
"scripts": {
7+
"test": "npm run build",
8+
"build": "tsc src/index.ts src/lib/unitTest.ts --lib es2016 --outDir dist/ && node test.mjs"
9+
},
10+
"repository": {
11+
"type": "git",
12+
"url": "git+https://github.com/Floofies/da-testdiff.git"
13+
},
14+
"keywords": [
15+
"test",
16+
"diff",
17+
"object",
18+
"comparison",
19+
"compare",
20+
"deep",
21+
"circular"
22+
],
23+
"author": "Dani Glore",
24+
"license": "MIT",
25+
"bugs": {
26+
"url": "https://github.com/Floofies/da-testdiff/issues"
27+
},
28+
"homepage": "https://github.com/Floofies/da-testdiff#readme",
29+
"devDependencies": {
30+
"@types/node": "^18.15.13"
31+
}
32+
}

src/index.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
type searchObj = any|object|Array<any>;
2+
type objPair = {obj1:searchObj, obj2: searchObj};
3+
function testValue(value1, value2) {
4+
if ((typeof value1) !== (typeof value2))
5+
return true;
6+
if(Number.isNaN(value1) !== Number.isNaN(value2))
7+
return true;
8+
if((value1 !== value2))
9+
return true;
10+
}
11+
// Returns true if obj1 differs in any way from obj2.
12+
export function testDiff(obj1:searchObj, obj2:searchObj, deep:boolean = true):boolean {
13+
if (obj1 === null)
14+
return obj1 !== obj2;
15+
// Cheap comparisons first
16+
if(((typeof obj1) !== "object") && testValue(obj1, obj2))
17+
return true;
18+
const stack:Array<objPair> = [{ obj1: obj1, obj2: obj2 }];
19+
const seen:Map<searchObj, objPair> = new Map();
20+
seen.set(obj1, stack[0]);
21+
_objects: while (stack.length !== 0) {
22+
const objects:objPair = <objPair> stack.pop();
23+
if (Array.isArray(objects.obj1) !== Array.isArray(objects.obj2))
24+
return true;
25+
const props1 = Object.keys(objects.obj1);
26+
const props2 = Object.keys(objects.obj2);
27+
if (props1.length === 0 && props2.length === 0)
28+
continue;
29+
if (props1.length !== props2.length)
30+
return true;
31+
if (!props1.every(value => props2.includes(value)))
32+
return true;
33+
if (!props2.every(value => props1.includes(value)))
34+
return true;
35+
_props: for (var loc = 0; loc < props1.length; loc++) {
36+
const prop:string = props1[loc];
37+
const value1:any = objects.obj1[prop];
38+
const value2:any = objects.obj2[prop];
39+
if ((typeof value1) !== (typeof value2))
40+
return true;
41+
if (value1 === null || (typeof value1) !== "object") {
42+
if(testValue(value1, value2))
43+
return true;
44+
continue _props;
45+
}
46+
if (value1 instanceof RegExp && value2 instanceof RegExp) {
47+
if (
48+
value1.source !== value2.source
49+
|| value1.ignoreCase !== value2.ignoreCase
50+
|| value1.global !== value2.global
51+
|| value1.multiline !== value2.multiline
52+
|| value1.sticky !== value2.sticky
53+
|| value1.unicode !== value2.unicode
54+
|| value1.flags !== value2.flags
55+
) {
56+
return true;
57+
}
58+
continue _props;
59+
}
60+
if (seen.has(value1))
61+
continue _props;
62+
if(deep)
63+
stack.push({ obj1: value1, obj2: value2 });
64+
seen.set(value1, objects);
65+
}
66+
if(!deep)
67+
return false;
68+
}
69+
return false;
70+
}

0 commit comments

Comments
 (0)