1

I have a JS code bundle to handle basic internationalization.

var EN = {
    messages: {
        newButton: {
            text: "New"
        },
        userSettings: {
            language: {
                selectYourLanguage: "Choose your preferred language",
                fr: "French",
                en: "English"
            }
        }
    }
};


var FR = {
    messages: {
        newButton: {
            text: "Nouveau"
        },
        userSettings: {
            language: {
                selectYourLanguage: "Choissez votre langage préféré:",
                fr: "Français",
                en: "Anglais"
            }
        }
    }
};

I want to know if it is possible to compare the 2 objects by their "paths". I want to be able to ensure that there is no path of first object that is not also in second object.

I want to be sure that someone adding a translation for one language, never forget to add it to the other language too, to fail-fast.

I want to be able to add new languages in the future.

Any idea on an elegant and generic way to do achieve this?

I'm looking for something like this

haveSameKeys(objectList)

Where objectList contains objects (like FR / EN / ...). Order should not matter. That list will be keep a relatively small size.

I would tend to prefer pure solutions that return their result, not involving side effects like alerting or throwing errors.

I don't really care about runtime performances as it's to fail-fast in dev/integration and won't be run in production.

Sebastien Lorber
  • 89,644
  • 67
  • 288
  • 419
  • Iterate and compare? – Felix Kling Mar 02 '15 at 16:56
  • You might want to remove the "Any idea on a lib or custom code to handle that?" since asking for a library or code is a reason to close the question. – j08691 Mar 02 '15 at 16:58
  • see http://stackoverflow.com/questions/1200562/difference-in-json-objects-using-javascript-jquery http://jsfiddle.net/sbgoran/kySNu/ – Gaël Barbin Mar 02 '15 at 17:10
  • Do you really expect that someone give you complete solution of your problem while you didn't do anything to solve it youself? At least I don't see any evidence of your attempts. – hindmost Mar 02 '15 at 17:12
  • Check out my object-matcher library: https://github.com/bvaughn/jasmine-object-matchers. Does what you're looking for, but in the context of a Jasmine plugin. You should be able to port it. – bvaughn Mar 02 '15 at 17:13

4 Answers4

1

jsFiddle Demo

So you want to know if the structure is exactly the same as another object? Then each key would have to be present in the other object.

I want to be able to ensure that there is no path of first object that is not also in second object.

translation: all paths in object b must be in object a.

function compare(a,b){
 //all paths in b must be in a
 for(var key in b){
  if(a[key] == undefined) return false;
  if(toString.call(b[key]) == "[object Array]" || toString.call(b[key]) == "[object Object]"){
   if( !compare(a[key],b[key]) ) return false;    
  }
 }
 return true;
}

This function recurses through object b and makes sure that the key in the object is present in a at the top level, and at every nested level. It will return false if object b contains a key which is not present in object a at the same nesting level.

In order to leverage the function against a list of objects, you would use a nested for loop to compare them to each other

function haveSameKeys(objectList){
 for(var i = 0; i < objectList.length; i++)
 {
  for(var n = i+1; n < objectList.length; n++){
   if( !(compare(objectList[i],objectList[n]) && compare(objectList[n],objectList[i]) ) ) return false;
  }
 }
 return true;
}
Travis J
  • 81,153
  • 41
  • 202
  • 273
  • If var FR = {}, compare returns true while I am expecting false – Sebastien Lorber Mar 02 '15 at 18:06
  • @SebastienLorber - So you want them to be equal, and not just all of b to be in a. As `FR = {}` is empty and contains no keys, by definition all of FR's keys are in a. – Travis J Mar 02 '15 at 18:07
  • @SebastienLorber - In the case where you want both to be equal, it would require an if and only if which would basically be `if( compare(a,b) && compare(b,a) )`. This will give false for when `FR = {}` – Travis J Mar 02 '15 at 18:10
  • yes maybe it was a bit confusion, I added some informations to my question – Sebastien Lorber Mar 02 '15 at 18:12
  • @SebastienLorber - Your clarification makes a large difference. For a set of 10 objects, ensuring every single one is equal to every other one it will be 10!, meaning that without an optimized solution you are looking at O(n!). – Travis J Mar 02 '15 at 18:21
  • Travis I don't care that much as it's to fail fast. It won't run in production env – Sebastien Lorber Mar 02 '15 at 18:24
1

A simple solution is to "flatten" both objects using a function similar to this:

function flatObj(obj, key, res) {
  res = res || {};
    
  if(typeof obj != "object" || obj === null) {
    res[key] = obj;
    return res;
  }
  
  return Object.keys(obj).reduce(function(res, k) {
    return flatObj(obj[k], (key || "") + "." + k, res);
  }, res);
}
  



var EN = {
    messages: {
        newButton: {
            text: "New"
        },
        userSettings: {
            language: {
                selectYourLanguage: "Choose your preferred language",
                fr: "French",
                en: "English"
            }
        }
    }
};

flat = flatObj(EN)
document.write("<pre>"+JSON.stringify(flat,0,3))

and then calculate the set difference between Object.keys(flat(EN)) and Object.keys(flat(FR)).

georg
  • 211,518
  • 52
  • 313
  • 390
  • That seems the most elegant and reusable solution so far. Thanks – Sebastien Lorber Mar 02 '15 at 18:14
  • I accept this answer for giving me the inspiration of my final solution: http://stackoverflow.com/a/28829861/82609 – Sebastien Lorber Mar 03 '15 at 10:50
  • @SebastienLorber: thanks, but you should actually accept your own answer as it's more complete. Accepting isn't saying "thanks", it's an indication for future readers which answer worked for the OP. – georg Mar 03 '15 at 10:54
1

Here is what I finally used.

It requires underscore/lodash for the _.xor function call

function getPaths(obj, key, res) {
    res = res || {};
    if ( typeof obj != "object" || obj === null ) {
        res[key] = obj;
        return res;
    }
    return Object.keys(obj).reduce(function(res, k) {
        var prefix = key ? key + "." : ""
        return getPaths(obj[k], prefix + k, res);
    }, res);
}

function getPathsDifferences(objectArray) {
    var objectPaths = objectArray.map(function(o) {
        return Object.keys(getPaths(o));
    });
    return _.xor.apply(this,objectPaths);
}

var differences = getPathsDifferences([FR,EN]);
console.debug("differences",differences);
if ( differences.length > 0 ) {
    throw new Error("Localization bundles are not consistent on paths: \n" + differences.join("\n"));
}
Sebastien Lorber
  • 89,644
  • 67
  • 288
  • 419
0

Using lodash...

function compareObjects(firstObject, secondObject) {
  var keys = _.keys(firstObject);

  for (var i = 0, length = keys.length; i < length; i++) {
    var key = keys[i];
    if (!secondObject.hasOwnProperty(key)) {
      // This is what you don't want
    } else if (firstObject[key] instanceof Object) {
      compareObjects(firstObject[key], secondObject[key]);
    }
  }
}
bvaughn
  • 13,300
  • 45
  • 46