Как сравнить два jsons, игнорируя порядок элементов в свойствах массива?
Мне нужно сравнить две строки, которые представляют объекты JSON. В целях тестирования мне нужен способ сравнения этих строк, игнорирующий не только порядок дочерних элементов (что является довольно распространенным), но и порядок элементов в свойствах массива jsons. То есть:
group: {
id: 123,
users: [
{id: 234, name: John},
{id: 345, name: Mike}
]
}
должно быть равно:
group: {
id: 123,
users: [
{id: 345, name: Mike},
{id: 234, name: John}
]
}
В идеале мне нужна библиотека JavaScript, но приветствуются и другие подходы.
8 ответов
Используйте JSONAssert
У них есть свободное утверждение.
Сыпучие:
JSONAssert.assertEquals(exp, act, false);
Строгое:
JSONAssert.assertEquals(exp, act, true);
Я не знаю, существует ли такая вещь, но вы можете реализовать ее самостоятельно.
var group1 = {
id: 123,
users: [
{id: 234, name: "John"},
{id: 345, name: "Mike"}
]
};
var group2 = {
id: 123,
users: [
{id: 345, name: "Mike"},
{id: 234, name: "John"}
]
};
function equal(a, b) {
if (typeof a !== typeof b) return false;
if (a.constructor !== b.constructor) return false;
if (a instanceof Array)
{
return arrayEqual(a, b);
}
if(typeof a === "object")
{
return objectEqual(a, b);
}
return a === b;
}
function objectEqual(a, b) {
for (var x in a)
{
if (a.hasOwnProperty(x))
{
if (!b.hasOwnProperty(x))
{
return false;
}
if (!equal(a[x], b[x]))
{
return false;
}
}
}
for (var x in b)
{
if (b.hasOwnProperty(x) && !a.hasOwnProperty(x))
{
return false;
}
}
return true;
}
function arrayEqual(a, b) {
if (a.length !== b.length)
{
return false;
}
var i = a.length;
while (i--)
{
var j = b.length;
var found = false;
while (!found && j--)
{
if (equal(a[i], b[j])) found = true;
}
if (!found)
{
return false;
}
}
return true;
}
alert(equal(group1, group2))
Вы можете нарезать массивы, отсортировать их по Id, затем преобразовать их в формат JSON и сравнить строки. Для многих участников это должно работать довольно быстро. Если вы продублируете идентификаторы, произойдет сбой, поскольку сортировка не изменит порядок.
Используйте DeltaJSON.
Это сложно сделать правильно - довольно легко написать код для конкретного случая, но сложнее написать общий код и еще сложнее тот, который масштабируется для больших массивов. Для большого количества данных JSON это беспорядочное сравнение массивов действительно важно, и оно должно работать не только для точного равенства, но и для поиска «наилучшего совпадения». DeltaJSON - это сервис SaaS, который делает это, и он бесплатный для небольших файлов, поэтому может быть вам полезен.
Я использую объектный хеш
Но я не уверен, что он эффективен для производственного кода.
// import * as hash from 'object-hash'
const hash = require('object-hash')
const objA = {
id: 123,
users: [
{id: 234, name: "John"},
{id: 345, name: "Mike"}
]
}
const objB = {
id: 123,
users: [
{id: 345, name: "Mike"},
{id: 234, name: "John"}
]
}
const options = {unorderedArrays: true}
hash(objA, options) == hash(objB, options) //true
В этом ответе описывается решение проблемы с использованием DeltaJSON REST API. DeltaJSON - это коммерческий продукт, который предоставляет API как услугу (SaaS) или через REST-сервер, который можно запускать локально:
- Запустите DeltaJSON Rest Server (требуется установка Java и файл лицензии):
java -jar deltajson-rest-1.1.0.jar
- В вашем JavaScript вызовите DeltaJSON REST API с
arrayAlignment
свойство установлено наorderless
.
В приведенном ниже примере кода показано, как вызвать API с этим параметром свойства:
async function runTest() {
const group1 = {
id: 123,
users: [
{ id: 234, name: "John" },
{ id: 345, name: "Mike" }
]
};
const group2 = {
id: 123,
users: [
{ id: 345, name: "Mikey" },
{ id: 234, name: "John" }
]
};
// call wrapper function that makes the REST API call:
const isEqual = await compare(group1, group2);
// log the comparison result: true
console.log("isEqual", isEqual);
}
async function compare(aData, bData) {
const aString = JSON.stringify(aData);
const bString = JSON.stringify(bData);
const blobOptions = { type: "application/json" };
var formdata = new FormData();
formdata.append("a", new Blob([aString], blobOptions));
formdata.append("b", new Blob([bString], blobOptions));
formdata.append("arrayAlignment", "orderless");
const myHeaders = new Headers();
myHeaders.append("Accept", "application/json");
var requestOptions = {
method: "POST",
headers: myHeaders,
body: formdata,
redirect: "follow"
};
try {
const response = await fetch(
"http://localhost:8080/api/json/v1/compare",
requestOptions
);
const jsonObj = await response.json();
console.log(jsonObj);
const dataSets = jsonObj.dx_deltaJSON.dx_data_sets;
const isEqual = dataSets === "A=B";
return isEqual;
} catch (e) {
console.error(e);
}
}
// run the test:
runTest(); // true
Объяснение:
Ответ DeltaJSON Rest API - это аннотированная форма входных данных JSON. Дополнительный
dx_
префиксные свойства добавляются для описания изменений. Свойство метаданных также включено в JSON.
Ценность
dx_deltaJSON
свойство - это объект, имеющий
dx_data_sets
свойство, которое мы можем проверить, чтобы увидеть (при двустороннем сравнении), что значение равно
A=B
.
Вот результат, где входные данные немного отличаются от того, что в вопросе. Здесь, а также порядок изменения элементов массива, «Майк» был изменен на «Майки»:
{
"dx_deltaJSON": {
"dx_data_sets": "A!=B",
"dx_deltaJSON_type": "diff",
"dx_deltaJSON_metadata": {
"operation": {
"type": "compare",
"input-format": "multi_part",
"output-format": "JSON"
},
"parameters": {
"dxConfig": [],
"arrayAlignment": "orderless",
"wordByWord": false
}
},
"dx_deltaJSON_delta": {
"id": 123,
"users": [
{
"id": 345,
"name": {
"dx_delta": {
"A": "Mike",
"B": "Mikey"
}
}
},
{
"id": 234,
"name": "John"
}
]
}
}
}
Мне нравится решение Фрэнсиса, и оно очень хорошо работает.
Просто добавьте следующую нулевую проверку в начале equal
функция для предотвращения ошибок с пустыми или неопределенными входами.
if (a == null && b == null) {
return true;
}
if (a == null || b == null) {
return false;
}
Таким образом, все решение будет выглядеть примерно так:
function equal(a, b) {
if (a == null && b == null) {
return true;
}
if (a == null || b == null) {
return false;
}
if (typeof a !== typeof b) return false;
if (a.constructor !== b.constructor) return false;
if (a instanceof Array)
{
return arrayEqual(a, b);
}
if(typeof a === "object")
{
return objectEqual(a, b);
}
return a === b;
}
function objectEqual(a, b) {
for (var x in a)
{
if (a.hasOwnProperty(x))
{
if (!b.hasOwnProperty(x))
{
return false;
}
if (!equal(a[x], b[x]))
{
return false;
}
}
}
for (var x in b)
{
if (b.hasOwnProperty(x) && !a.hasOwnProperty(x))
{
return false;
}
}
return true;
}
function arrayEqual(a, b) {
if (a.length !== b.length)
{
return false;
}
var i = a.length;
while (i--)
{
var j = b.length;
var found = false;
while (!found && j--)
{
if (equal(a[i], b[j])) found = true;
}
if (!found)
{
return false;
}
}
return true;
}
Вот моя попытка пользовательской реализации:
var equal = (function(){
function isObject(o){
return o !== null && typeof o === 'object';
}
return function(o1, o2){
if(!isObject(o1) || !isObject(o2)) return o1 === o2;
var key, allKeys = {};
for(key in o1)
if(o1.hasOwnProperty(key))
allKeys[key] = key;
for(key in o2)
if(o2.hasOwnProperty(key))
allKeys[key] = key;
for(key in allKeys){
if(!equal(o1[key], o2[key])) return false;
}
return true;
}
})();
Пример этого с тестовыми примерами:
var p1 = {
tags: ['one', 'two', 'three'],
name: 'Frank',
age: 24,
address: {
street: '111 E 222 W',
city: 'Provo',
state: 'Utah',
zip: '84604'
}
}
var p2 = {
name: 'Frank',
age: 24,
tags: ['one', 'two', 'three'],
address: {
street: '111 E 222 W',
city: 'Provo',
state: 'Utah',
zip: '84604'
}
}
var p3 = {
name: 'Amy',
age: 24,
tags: ['one', 'two', 'three'],
address: {
street: '111 E 222 W',
city: 'Provo',
state: 'Utah',
zip: '84604'
}
}
var p4 = {
name: 'Frank',
age: 24,
tags: ['one', 'two', 'three'],
address: {
street: '111 E 222 W',
city: 'Payson',
state: 'Utah',
zip: '84604'
}
}
var p5 = {
name: 'Frank',
age: 24,
tags: ['one', 'two'],
address: {
street: '111 E 222 W',
city: 'Provo',
state: 'Utah',
zip: '84604'
}
}
var equal = (function(){
function isObject(o){
return o !== null && typeof o === 'object';
}
return function(o1, o2){
if(!isObject(o1) || !isObject(o2)) return o1 === o2;
var key, allKeys = {};
for(key in o1)
if(o1.hasOwnProperty(key))
allKeys[key] = key;
for(key in o2)
if(o2.hasOwnProperty(key))
allKeys[key] = key;
for(key in allKeys){
if(!equal(o1[key], o2[key])) return false;
}
return true;
}
})();
var cases = [
{name: 'Compare with self', a: p1, b: p1, expected: true},
{name: 'Compare with identical', a: p1, b: p2, expected: true},
{name: 'Compare with different', a: p1, b: p3, expected: false},
{name: 'Compare with different (nested)', a: p1, b: p4, expected: false},
{name: 'Compare with different (nested array)', a: p1, b: p5, expected: false}
];
function runTests(tests){
var outEl = document.getElementById('out');
for(var i=0; i < tests.length; i++){
var actual = equal(tests[i].a, tests[i].b),
result = tests[i].expected == actual
? 'PASS'
: 'FAIL';
outEl.innerHTML +=
'<div class="test ' + result + '">' +
result + ' ' +
tests[i].name +
'</div>';
}
}
runTests(cases);
body{
font-family:monospace;
}
.test{
margin:5px;
padding:5px;
}
.PASS{
background:#EFE;
border:solid 1px #32E132;
}
.FAIL{
background:#FEE;
border:solid 1px #FF3232;
}
<div id=out></div>