12

I want to reverse the mapping of an object (which might have duplicate values). Example:

const city2country = {
    'Amsterdam': 'Netherlands',
    'Rotterdam': 'Netherlands',
    'Paris': 'France'
};

reverseMapping(city2country) Should output:

{
    'Netherlands': ['Amsterdam', 'Rotterdam'],
    'France': ['Paris']
}

I've come up with the following, naive solution:

const reverseMapping = (obj) => {
    const reversed = {};
    Object.keys(obj).forEach((key) => {
        reversed[obj[key]] = reversed[obj[key]] || [];
        reversed[obj[key]].push(key);
    });
    return reversed;
};

But I'm pretty sure there is a neater, shorter way, preferably prototyped so I could simply do:

const country2cities = city2country.reverse();
8
  • 7
    AFAIK there's nothing built in that does this. Your code looks fine. Commented Aug 17, 2017 at 6:44
  • 4
    preferably prototyped - you don't want to add to Object.prototype Commented Aug 17, 2017 at 6:45
  • 2
    @SagarV OP is looking for a one-liner. The only way to get that is with a library, since there's nothing built-in that does it. Commented Aug 17, 2017 at 7:30
  • 1
    I'd prefer readability of code over a "neater, shorter way", so your code is OK. You could even make it a bit more readable with var objKey=obj[key]; inside the forEach. You have a working code, don't waste your time trying to golf it (unless that is your actual goal). Commented Aug 17, 2017 at 7:32
  • 1
    Popular opinion. Search SO. It's been discussed ad nauseam Commented Aug 17, 2017 at 14:49

7 Answers 7

12

There is no such built-in function in JavaScript. Your code looks fine, but given that there are so many edge cases here that could wrong, I'd suggesting using invertBy from lodash, which does exactly what you describe.

Example

var object = { 'a': 1, 'b': 2, 'c': 1 };

_.invertBy(object);
// => { '1': ['a', 'c'], '2': ['b'] }
Sign up to request clarification or add additional context in comments.

4 Comments

Did someone really downvote this answer because it requires a 3rd-party library? It's the only answer that really satisfies what the OP is looking for.
Thanks, we currently don't use lodash and it seems a bit of an overkill to add for such a small method. Don't understand the downvote either.
@Daniel lodash is modular, which means you can only install this single function and use it. Not much different in terms of size than copy/paste a function from here, but much better for maintenance. Just my two cents.
@LazarLjubenović actually looks like lodash code is pretty similar to mine except for hasOwnProperty defense, thanks anyway!
11

You could use Object.assign, while respecting the given array of the inserted values.

const city2country = { Amsterdam: 'Netherlands', Rotterdam: 'Netherlands', Paris: 'France' };
const reverseMapping = o => Object.keys(o).reduce((r, k) =>
        Object.assign(r, { [o[k]]: (r[o[k]] || []).concat(k) }), {})

console.log(reverseMapping(city2country));

Comments

1

You could use reduce to save the declaration line reduce.

Abusing && to check if the map[object[key]] is defined first before using Array.concat.

It's shorter, but is it simpler? Probably not, but a bit of fun ;)

const reverseMapping = (object) => 
    Object.keys(object).reduce((map, key) => {
        map[object[key]] = map[object[key]] && map[object[key]].concat(key) || [key]
     return map;
    }, {});

2 Comments

Why do you need to prevent duplicates? Object keys are guaranteed to be unique.
Hmm, turns out, I was mistaken. I must have used concat(x, y) rather than concat(x). Updating my code, as I would prefer not to use Set.
0

You can use something like this to get raid of duplicates first :

function removeDuplicates(arr, key) {
   if (!(arr instanceof Array) || key && typeof key !== 'string') {
    return false;
   }

  if (key && typeof key === 'string') {
    return arr.filter((obj, index, arr) => {
        return arr.map(mapObj => mapObj[key]).indexOf(obj[key]) === index;
    });

  } else {
    return arr.filter(function(item, index, arr) {
        return arr.indexOf(item) == index;
    });
  }
} 

and then use this to make it reverse :

function reverseMapping(obj){
  var ret = {};
  for(var key in obj){
  ret[obj[key]] = key;
  }
    return ret;
  }

1 Comment

reverseMapping(city2country) just maps the country to one city, not an array of cities. How do you use your answer to get his desired result?
0

You could try getting an array of values and an array of keys from the current object, and setup a new object to hold the result. Then, as you loop through the array of values -

  • if the object already has this value as the key, like Netherlands, you create a new array, fetch the already existing value (ex: Rotterdam), and add this and the new value (Amsterdam) to the array, and set up this array as the new value for the Netherlands key.
  • if the current value doesn't exist in the object, set it up as a new string, ex: France is the key and Paris is the value.

Code -

const city2country = {
    'Amsterdam': 'Netherlands',
    'Rotterdam': 'Netherlands',
    'Paris': 'France',
};

function reverseMapping(obj) {
  let values = Object.values(obj);
  let keys = Object.keys(obj);
  let result = {}
  values.forEach((value, index) => {
    if(!result.hasOwnProperty(value)) {
      // create new entry
      result[value] = keys[index];
    }
    else {
      // duplicate property, create array
      let temp = [];
      // get first value
      temp.push(result[value]);
      // add second value
      temp.push(keys[index]);
      // set value 
      result[value] = temp;
    }
  });
  
  console.log(result);
  return result;
}

reverseMapping(city2country)

The benefit here is - it adjusts to the structure of your current object - Netherlands being the repeated values, gets an array as it's value in the new object, while France gets a string value Paris as it's property. Of course, it should be very easy to change this.

Note - Object.values() might not be supported across older browsers.

1 Comment

He's looking for something simpler than his existing code. Your function is 3x longer.
0

@Nina Scholz answer works well for this exact question. :thumbsup:

But if you don't need to keep both values for the Netherlands key ("Netherlands": ["Amsterdam", "Rotterdam"]), then this is a little bit shorter and simpler to read:

const city2country = { Amsterdam: 'Netherlands', Rotterdam: 'Netherlands', Paris: 'France' };

console.log(
  Object.entries(city2country).reduce((obj, item) => (obj[item[1]] = item[0]) && obj, {})
);
// outputs `{Netherlands: "Rotterdam", France: "Paris"}`

2 Comments

Actually you broke her code. You lost the value "Amsterdam".... check the SO's question again; and Nina's answer code output.
You're right, I didn't pay attention to that part! I will just keep the answer here but give more details to specify the case when it's a correct usage. It could help someone in a slightly different case. :)
0

Inspired by some of the other answers, here is a short reusable function:

/**
 * Take an object with key->value mapping and return
 * an object with the inverse value->keys[] mapping.
 */
function invertObj(obj) {
  return Object.entries(obj)
    .reduce((result, [key, value]) => {
      result[value] = (result[value] || []).concat(key);
      return result;
    }, {});
}

Usage with the example from the question:

invertObj({
    'Amsterdam': 'Netherlands',
    'Rotterdam': 'Netherlands',
    'Paris': 'France'
});
// => {
//   "Netherlands": [ "Amsterdam", "Rotterdam" ],
//   "France": [ "Paris" ],
// }

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.