Как сравнить два 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-сервер, который можно запускать локально:

  1. Запустите DeltaJSON Rest Server (требуется установка Java и файл лицензии):
      java -jar deltajson-rest-1.1.0.jar
  1. В вашем 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>

Другие вопросы по тегам