最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

Comparing 2 JSON objects structure in JavaScript - Stack Overflow

programmeradmin1浏览0评论

I am using angular-translate for a big application. Having several people mitting code + translations, many times the translation objects are not in sync.

I am building a Grunt plugin to look at both files' structure and pare it (just the keys and overall structure, not values).

The main goals are:

  • Look into each file, and check if the structure of the whole object (or file, in this case) is the exact same as the translated ones;
  • On error, return the key that doesn't match.

It turns out it was a bit more plicated than I anticipated. So i figured I could do something like:

  1. Sort the object;
  2. Check the type of data the value contains (since they are translations, it will only have strings, or objects for the nestings) and store it in another object, making the key equal to the original key and the value would be a string 'String', or an object in case it's an object. That object contains the children elements;
  3. Recursively repeat steps 1-2 until the whole object is mapped and sorted;
  4. Do the same for all the files
  5. Stringify and pare everything.

A tiny example would be the following object:

{
  key1: 'cool',
  key2: 'cooler',
  keyWhatever: {
    anotherObject: {
      key1: 'better',
      keyX: 'awesome'
    },
    aObject: 'actually, it\'s a string'
  },
  aKey: 'more awesomeness'
}

would map to:

{
  aKey: 'String',
  key1: 'String',
  key2: 'String',
  keyWhatever: {
    aObject: 'String',
    anotherObject: {
      key1: 'String',
      keyX: 'String'
    }
  }
}

After this, I would stringify all the objects and proceed with a strict parison.

My question is, is there a better way to perform this? Both in terms of simplicity and performance, since there are many translation files and they are fairly big.

I tried to look for libraries that would already do this, but I couldn't find any.

Thank you

EDIT: Thank you Jared for pointing out objects can't be sorted. I am ashamed for saying something like that :D Another solution could be iterating each of the properties on the main translation file, and in case they are strings, pare the key with the other files. In case they are objects, "enter" them, and do the same. Maybe it is even simpler than my first guess. What should be done?

I am using angular-translate for a big application. Having several people mitting code + translations, many times the translation objects are not in sync.

I am building a Grunt plugin to look at both files' structure and pare it (just the keys and overall structure, not values).

The main goals are:

  • Look into each file, and check if the structure of the whole object (or file, in this case) is the exact same as the translated ones;
  • On error, return the key that doesn't match.

It turns out it was a bit more plicated than I anticipated. So i figured I could do something like:

  1. Sort the object;
  2. Check the type of data the value contains (since they are translations, it will only have strings, or objects for the nestings) and store it in another object, making the key equal to the original key and the value would be a string 'String', or an object in case it's an object. That object contains the children elements;
  3. Recursively repeat steps 1-2 until the whole object is mapped and sorted;
  4. Do the same for all the files
  5. Stringify and pare everything.

A tiny example would be the following object:

{
  key1: 'cool',
  key2: 'cooler',
  keyWhatever: {
    anotherObject: {
      key1: 'better',
      keyX: 'awesome'
    },
    aObject: 'actually, it\'s a string'
  },
  aKey: 'more awesomeness'
}

would map to:

{
  aKey: 'String',
  key1: 'String',
  key2: 'String',
  keyWhatever: {
    aObject: 'String',
    anotherObject: {
      key1: 'String',
      keyX: 'String'
    }
  }
}

After this, I would stringify all the objects and proceed with a strict parison.

My question is, is there a better way to perform this? Both in terms of simplicity and performance, since there are many translation files and they are fairly big.

I tried to look for libraries that would already do this, but I couldn't find any.

Thank you

EDIT: Thank you Jared for pointing out objects can't be sorted. I am ashamed for saying something like that :D Another solution could be iterating each of the properties on the main translation file, and in case they are strings, pare the key with the other files. In case they are objects, "enter" them, and do the same. Maybe it is even simpler than my first guess. What should be done?

Share Improve this question edited Oct 16, 2015 at 17:23 fgarci03 asked Oct 16, 2015 at 17:04 fgarci03fgarci03 4308 silver badges15 bronze badges 5
  • 1 Although many implementations maintain insertion order, per the spec javascript objects are unordered key/value pairs. You will have to use Object.keys and use arrays, you can't 'sort' objects. – Jared Smith Commented Oct 16, 2015 at 17:07
  • Thank you Jared. I corrected my question details! – fgarci03 Commented Oct 16, 2015 at 17:24
  • It's a big issue so I don't claim to have solutions, but one thing you may not have thought of: How will you treat this? root = {myself: root } (maybe you'll throw an error, but you can't crash the page recursing forever) – Katana314 Commented Oct 16, 2015 at 17:27
  • @Katana314 keep in mind that JSON cannot represent circular structures. That won't be a problem for OP – skeggse Commented Oct 16, 2015 at 18:24
  • @skeggse No, it can't, but even just detecting the fact that it's trying to handle one is something you need to keep in mind. Otherwise, it's very possible for your code to enter a recursive infinite loop, and leave anyone using the function confused, rather than realizing they fed it a bad object. – Katana314 Commented Oct 17, 2015 at 21:20
Add a ment  | 

3 Answers 3

Reset to default 4

Lets say you have two JSON objects, jsonA and jsonB.

function pareValues(a, b) {

    //if a and b aren't the same type, they can't be equal
    if (typeof a !== typeof b) {
        return false;
    }
 
    // Need the truthy guard because
    // typeof null === 'object'
    if (a && typeof a === 'object') {
        var keysA = Object.keys(a).sort(),
            keysB = Object.keys(b).sort();

        //if a and b are objects with different no of keys, unequal
        if (keysA.length !== keysB.length) {
            return false;
        }

        //if keys aren't all the same, unequal
        if (!keysA.every(function(k, i) { return k === keysB[i];})) {
            return false;
        }

        //recurse on the values for each key
        return keysA.every(function(key) {
            //if we made it here, they have identical keys
            return pareValues(a[key], b[key]);
        });

    //for primitives just use a straight up check
    } else {
        return a === b;
    }
}

//true if their structure, values, and keys are identical    
var passed = pareValues(jsonA, jsonB); 

Note that this can overflow the stack for deeply nested JSON objects. Note also that this will work for JSON but not necessarily regular JS objects as special handling is needed for Date Objects, Regexes, etc.

Actually you do need to sort the keys, as they are not required to be spit out in any particular order. Write a function,

function getComparableForObject(obj) {
    var keys = Object.keys(obj);
    keys.sort(a, b => a > b ? 1 : -1);

    var parable = keys.map(
            key => key + ":" + getValueRepresentation(obj[key])
        ).join(",");
    return "{" + parable + "}";
}

Where getValueRepresentation is a function that either returns "String" or calls getComparableForObject. If you are worried about circular references, add a Symbol to the outer scope, repr, assign obj[repr] = parable in the function above, and in getValueRepresentation check every object for a defined obj[repr] and return it instead of processing it recursively.

Sorting an array of the keys from the object works. However, sorting has an average time plexity of O(n⋅log(n)). We can do better. A fast general algorithm for ensuring two sets A and B are equivalent is as follows:

for item in B
  if item in A
    remove item from A
  else
    sets are not equivalent
sets are equivalent iff A is empty

To address @Katana31, we can detect circular references as we go by maintaining a set of visited objects and ensuring that all descendents of that object are not already in the list:

# poorly written pseudo-code
fn detectCycles(A, found = {})
  if A in found
    there is a cycle
  else
    found = clone(found)
    add A to found
    for child in A
      detectCycles(child, found)

Here's a plete implementation (you can find a simplified version that assumes JSON/non-circular input here):

var hasOwn = Object.prototype.hasOwnProperty;
var indexOf = Array.prototype.indexOf;

function isObjectEmpty(obj) {
  for (var key in obj) {
    return false;
  }

  return true;
}

function copyKeys(obj) {
  var newObj = {};

  for (var key in obj) {
    newObj[key] = undefined;
  }

  return newObj;
}

// pares the structure of arbitrary values
function pareObjectStructure(a, b) {
  return function innerCompare(a, b, pathA, pathB) {
    if (typeof a !== typeof b) {
      return false;
    }

    if (typeof a === 'object') {
      // both or neither, but not mismatched
      if (Array.isArray(a) !== Array.isArray(b)) {
        return false;
      }

      if (indexOf.call(pathA, a) !== -1 || indexOf.call(pathB, b) !== -1) {
        return false;
      }

      pathA = pathA.slice();
      pathA.push(a);

      pathB = pathB.slice();
      pathB.push(b);

      if (Array.isArray(a)) {
        // can't pare structure in array if we don't have items in both
        if (!a.length || !b.length) {
          return true;
        }

        for (var i = 1; i < a.length; i++) {
          if (!innerCompare(a[0], a[i], pathA, pathA)) {
            return false;
          }
        }

        for (var i = 0; i < b.length; i++) {
          if (!innerCompare(a[0], b[i], pathA, pathB)) {
            return false;
          }
        }

        return true;
      }

      var map = copyKeys(a), keys = Object.keys(b);

      for (var i = 0; i < keys.length; i++) {
        var key = keys[i];

        if (!hasOwn.call(map, key) || !innerCompare(a[key], b[key], pathA,
            pathB)) {
          return false;
        }

        delete map[key];
      }

      // we should've found all the keys in the map
      return isObjectEmpty(map);
    }

    return true;
  }(a, b, [], []);
}

Note that this implementation directly pares two objects for structural equivalency, but doesn't reduce the objects to a directly parable value (like a string). I haven't done any performance testing, but I suspect that it won't add significant value, though it will remove the need to repeatedly ensure objects are non-cyclic. For that reason, you could easily split pareObjectStructure into two functions - one to pare the structure and one to check for cycles.

发布评论

评论列表(0)

  1. 暂无评论