Skip to content

Commit 65a39d2

Browse files
committedFeb 21, 2020
ready to release
1 parent 84a37ff commit 65a39d2

8 files changed

+510
-303
lines changed
 

‎README.md

+124-2
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,124 @@
1-
# JSON Satisfy
2-
A simple library to compare 2 JSON value, which the 1st value of those JSON must be covered (or satisfied) by the 2nd JSON.
1+
# JS Identical
2+
A simple library to check that JS/JSON value is identical each other (Typescript support). The implementation will ignore the order of array or key index.
3+
4+
> This module is a sub implementation of the [js-deep-equals](https://github.com/joelwass/js-deep-equals). Arrays are compared by creating a [Merkle Tree](https://en.wikipedia.org/wiki/Merkle_tree) out of the input and comparing the top level hashes. hashing is done using [murmur v3](https://en.wikipedia.org/wiki/MurmurHash).
5+
6+
7+
## Usage
8+
9+
### Installation
10+
npm i js-identical
11+
12+
### Example
13+
```
14+
const isIdentical = require('js-identical')
15+
16+
const john1 = {name: 'John', age: 22, hobby: ['swimming', 'running', 'hiking']
17+
const john2 = {age: 22, name: 'John', hobby: ['running', 'swimming', 'hiking']
18+
19+
const people1 = [
20+
{
21+
id: 1,
22+
name: 'Marry',
23+
hobby: ['singing', 'drawing', 'playing'],
24+
favouriteFoods:[
25+
{
26+
name: 'Gado gado',
27+
from: 'Indonesia'
28+
},
29+
{
30+
name: 'Rendang',
31+
from: 'Indonesia'
32+
},
33+
{
34+
name: 'Kentucky Fried Chicken',
35+
from: 'USA',
36+
variant: ['original', 'crispy', 'spicy']
37+
}
38+
],
39+
age: 12
40+
},
41+
{
42+
id: 2,
43+
hobby: ['running', 'Dancing'],
44+
name: 'Rose',
45+
favouriteFoods:[
46+
{
47+
name: 'Rendang',
48+
from: 'Indonesia'
49+
}
50+
{
51+
name: 'Salad',
52+
from: 'USA'
53+
}
54+
],
55+
age: 14
56+
}
57+
]
58+
59+
const people2 = [
60+
{
61+
id: 1,
62+
name: 'Marry',
63+
hobby: ['drawing', 'singing', 'playing'],
64+
favouriteFoods:[
65+
{
66+
name: 'Rendang',
67+
from: 'Indonesia'
68+
},
69+
{
70+
name: 'Kentucky Fried Chicken',
71+
from: 'USA',
72+
variant: ['crispy', 'original', 'spicy']
73+
},
74+
{
75+
from: 'Indonesia',
76+
name: 'Gado gado'
77+
},
78+
],
79+
age: 12
80+
},
81+
{
82+
id: 2,
83+
name: 'Rose',
84+
favouriteFoods:[
85+
{
86+
from: 'USA'
87+
name: 'Salad',
88+
},
89+
{
90+
name: 'Rendang',
91+
from: 'Indonesia'
92+
}
93+
],
94+
age: 14,
95+
hobby: ['Dancing', 'running'],
96+
}
97+
]
98+
99+
console.log(isIdentical(john1, john2)) // true
100+
console.log(isIdentical(people1, people2)) // true
101+
```
102+
103+
## License
104+
MIT License
105+
106+
Copyright (c) 2020 Aditya Kresna
107+
108+
Permission is hereby granted, free of charge, to any person obtaining a copy
109+
of this software and associated documentation files (the "Software"), to deal
110+
in the Software without restriction, including without limitation the rights
111+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
112+
copies of the Software, and to permit persons to whom the Software is
113+
furnished to do so, subject to the following conditions:
114+
115+
The above copyright notice and this permission notice shall be included in all
116+
copies or substantial portions of the Software.
117+
118+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
119+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
120+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
121+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
122+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
123+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
124+
SOFTWARE.

‎build.sh

-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,5 @@
22

33
rm -Rf ./build
44
mkdir build
5-
cp -R ./src/pb ./build/pb
65
./node_modules/.bin/tsc --build tsconfig.prod.json
76
./node_modules/.bin/ef-tspm -c tsconfig.prod.json

‎package-lock.json

+27-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json

+21-11
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
2-
"name": "json-satisfy",
2+
"name": "js-identical",
33
"version": "1.0.0",
4-
"description": "Check if 1st json is satified by the 2nd json",
4+
"description": "Javascript library to check if 1st json is equal by the 2nd json",
55
"main": "build/index.js",
66
"scripts": {
77
"test": "mocha -r ts-node/register src/**/**.test.ts",
@@ -11,29 +11,29 @@
1111
},
1212
"repository": {
1313
"type": "git",
14-
"url": "git+ssh://git@github.com/slaveofcode/json-satisfy.git"
14+
"url": "git+ssh://git@github.com/slaveofcode/js-identical.git"
1515
},
1616
"keywords": [
17-
"json",
1817
"json",
1918
"compare",
20-
"json",
2119
"match",
22-
"json",
2320
"diff",
24-
"json",
25-
"patch"
21+
"identical",
22+
"equal",
23+
"deep-equal",
24+
"deep"
2625
],
2726
"author": "Aditya Kresna Permana",
2827
"license": "MIT",
2928
"bugs": {
30-
"url": "https://github.com/slaveofcode/json-satisfy/issues"
29+
"url": "https://github.com/slaveofcode/js-identical/issues"
3130
},
32-
"homepage": "https://github.com/slaveofcode/json-satisfy#readme",
31+
"homepage": "https://github.com/slaveofcode/js-identical#readme",
3332
"devDependencies": {
3433
"@ef-carbon/tspm": "^2.2.5",
3534
"@types/chai": "^4.2.9",
3635
"@types/mocha": "^7.0.1",
36+
"@types/node": "^13.7.4",
3737
"chai": "^4.2.0",
3838
"husky": "^4.2.3",
3939
"mocha": "^7.0.1",
@@ -46,5 +46,15 @@
4646
"files": [
4747
"build/**/*"
4848
],
49-
"dependencies": {}
49+
"dependencies": {
50+
"@types/murmurhash": "0.0.1",
51+
"dequal": "^1.0.0",
52+
"is-subset": "^0.1.1",
53+
"murmurhash": "0.0.2"
54+
},
55+
"husky": {
56+
"hooks": {
57+
"pre-commit": "npm run lint"
58+
}
59+
}
5060
}

‎src/index.test.ts

+279-204
Large diffs are not rendered by default.

‎src/index.ts

+57-82
Original file line numberDiff line numberDiff line change
@@ -1,101 +1,76 @@
1-
interface IOptions {
2-
strict: boolean;
3-
}
1+
import { v3 as hash } from 'murmurhash'
42

5-
const makeCopy = (jsonValue: any) => {
6-
let copyJson: any;
3+
const EMPTY_OBJECT = '__empty__obj'
4+
const EMPTY_ARRAY = '__empty__arr'
75

8-
if (Array.isArray(jsonValue)) {
9-
copyJson = [ ...jsonValue ]
10-
} else {
11-
copyJson = { ...jsonValue }
6+
class Node {
7+
public children: any
8+
public hash: number
9+
constructor() {
10+
this.children = []
11+
this.hash = 0
1212
}
13-
14-
return copyJson
1513
}
1614

17-
const atLeastSatisfySource = (objSource: any, objDest: any, strict: boolean = false) => {
18-
let satisfied: boolean = true
19-
20-
if (typeof objSource === 'object') {
21-
// handle json object
22-
for (const key of Object.keys(objSource)) {
23-
if (strict) {
24-
satisfied = satisfied && objSource[key] === objDest[key]
25-
} else {
26-
// tslint:disable-next-line: triple-equals
27-
satisfied = satisfied && objSource[key] == objDest[key]
28-
}
29-
}
30-
} else {
31-
// handle primitive type
32-
if (strict) {
33-
satisfied = satisfied && objSource === objDest
34-
} else {
35-
// tslint:disable-next-line: triple-equals
36-
satisfied = satisfied && objSource == objDest
37-
}
38-
}
39-
40-
return satisfied
15+
const hasher = (thing: any, prefix: string = '') => {
16+
const stringThing = prefix + (typeof thing) + '::' + thing
17+
return hash(stringThing)
4118
}
4219

43-
const compareObject = (objMaster: any, objToCompare: any, opt?: IOptions): boolean => {
44-
let satisfied: boolean = true;
45-
46-
if (Array.isArray(objMaster)) {
47-
satisfied = satisfied && compareArray(objMaster, objToCompare, opt);
48-
} else {
49-
satisfied = satisfied && atLeastSatisfySource(objMaster, objToCompare, opt?.strict)
50-
}
51-
52-
return satisfied;
20+
const combineHashes = (parentHash: any, child: any) => {
21+
/* tslint:disable:no-bitwise */
22+
return (parentHash + child.hash) & 0xFFFFFFFF
5323
}
5424

55-
const compareArray = (jsonArrMaster: any, jsonArrToCompare: any, opt?: IOptions): boolean => {
56-
const arrComparison: any = makeCopy(jsonArrToCompare)
57-
let satisfied: boolean = true;
58-
for (const key of Object.keys(jsonArrMaster)) {
59-
// check key is number
60-
const isKeyNumber = Number.NaN !== Number(key)
25+
const newNode = (thing: any, prefix: string) => {
26+
const node = new Node()
27+
node.hash = hasher(thing, prefix)
28+
return node
29+
}
6130

62-
if (isKeyNumber) {
63-
let foundSatisfy = false
64-
for (const keyOnComp of Object.keys(arrComparison)) {
65-
foundSatisfy = compareObject(jsonArrMaster[key], arrComparison[keyOnComp])
31+
const createTree = (currNode: Node, currentInput: any, prefix: string = '') => {
32+
const isObject = typeof currentInput === 'object'
33+
const isNil = currentInput === null || typeof currentInput === 'undefined'
34+
const isArray = Array.isArray(currentInput)
35+
const keys: any = !isNil ? Object.keys(currentInput) : []
6636

67-
if (foundSatisfy) {
68-
foundSatisfy = true
69-
delete arrComparison[keyOnComp]
70-
break
71-
}
72-
}
73-
satisfied = satisfied && foundSatisfy
74-
} else {
75-
satisfied = satisfied && compareObject(jsonArrMaster[key], arrComparison[key], opt);
76-
delete arrComparison[key]
77-
}
37+
// we're at a weird value
38+
if (currentInput instanceof Date || currentInput instanceof RegExp) {
39+
return newNode(currentInput, prefix)
7840
}
79-
return satisfied
80-
}
8141

82-
export const compare = (jsonMaster: any, jsonToCompare: any, opt?: IOptions): boolean => {
83-
if (!jsonMaster || !jsonToCompare) return false
42+
// if we're at a value
43+
if (!isObject && !isArray) {
44+
return newNode(currentInput, prefix)
45+
}
8446

85-
if (Array.isArray(jsonMaster) || Array.isArray(jsonToCompare)) {
86-
if (Array.isArray(jsonMaster) && !Array.isArray(jsonToCompare) ||
87-
!Array.isArray(jsonMaster) && Array.isArray(jsonToCompare)) {
88-
return false
47+
// if we're at an iterable
48+
if (!keys.length) {
49+
return isArray ? newNode(EMPTY_ARRAY, prefix) : newNode(EMPTY_OBJECT, prefix)
50+
}
51+
for (const key of keys) {
52+
let pfx
53+
if (!isArray && isObject) { // if we're dealing with an object prefix the key
54+
pfx = key
8955
}
56+
57+
const node = createTree(new Node(), currentInput[key], pfx)
58+
currNode.children.push(node)
9059
}
60+
// iterable's hash is combined hash of all children
61+
const combined = currNode.children.reduce(combineHashes, 0)
62+
currNode.hash = hasher(combined)
63+
return currNode
64+
}
9165

92-
let satisfied: boolean = true;
66+
const createFinalHash = (input: any) => {
67+
const tree = createTree(new Node(), input)
68+
return tree.hash
69+
}
9370

94-
if (Array.isArray(jsonMaster)) {
95-
satisfied = satisfied && compareArray(jsonMaster, jsonToCompare, opt);
96-
} else {
97-
satisfied = satisfied && compareObject(jsonMaster, jsonToCompare, opt);
71+
export const compare = (a: any, b: any) => {
72+
if (a && b && ((Array.isArray(a) && Array.isArray(b)) || (typeof a === 'object' && typeof b === 'object')) && (a.length === b.length)) {
73+
return createFinalHash(a) === createFinalHash(b)
9874
}
99-
100-
return satisfied;
101-
};
75+
return false
76+
}

‎tsconfig.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"compilerOptions": {
33
/* Basic Options */
44
// "incremental": true, /* Enable incremental compilation */
5-
"target": "ES5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
5+
"target": "ES3", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
66
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
77
// "lib": [], /* Specify library files to be included in the compilation. */
88
// "allowJs": true, /* Allow javascript files to be compiled. */

‎tsconfig.prod.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"sourceMap": false,
55
"baseUrl": "./src", /* Base directory to resolve non-absolute module names. */
66
"paths": {
7-
"@pb/*": [ "lib/*" ]
7+
"@lib/*": [ "lib/*" ]
88
}
99
},
1010
"exclude": [

0 commit comments

Comments
 (0)
Please sign in to comment.