77
\$\begingroup\$

The use case is to convert an array of objects into a hash map where one property is the key and the other property is the value. Common case of using this is converting a "link" object in a hypermedia response into a hash map of links.

jsfiddle

function toHashMap(data, name, value) {
    return _.zipObject(_.pluck(data, name),
                       _.pluck(data, value));
}

function toMap(data, name, value) {
    return _.reduce(data, function(acc, item) {
        acc[item[name]] = item[value];
        return acc;
    }, {});
}

var data = [
    { rel: 'link1', href: 'url1'},
    { rel: 'link2', href: 'url2'},
    { rel: 'link3', href: 'url3'},
    { rel: 'link4', href: 'url4'},
];

console.log(toHashMap(data, 'rel', 'href'));

console.log(toMap(data, 'rel', 'href'));

toHashMap appears much more readable, but less efficient. toMap seems to have the best efficiency at the expense of readability.

Is there a more elegant solution - i.e. both readable and efficient? I suspect that Lodash might have a function to do this already, but I haven't found anything like this after looking at the API documentation.

\$\endgroup\$
91
\$\begingroup\$

I think you are looking for _.keyBy (or _.indexBy in older versions)

_.keyBy(data, 'rel');
\$\endgroup\$
  • 3
    \$\begingroup\$ I don't understand why is this the accepted answer since it doesn't give the same result as the exposed functions \$\endgroup\$ – Pau Fracés Feb 17 '15 at 15:53
  • 1
    \$\begingroup\$ @PauFracés See below for revised solution. \$\endgroup\$ – Pete Mar 13 '15 at 21:14
  • 4
    \$\begingroup\$ Note: Now known as _.keyBy(lodash.com/docs#keyBy). \$\endgroup\$ – jtheletter Feb 18 '16 at 2:07
40
\$\begingroup\$

Revised Solution

As per Pau Fracés comment above, here is the complete solution. The solution given by John Anderson would index all objects by the key. However, this would not create a key-value pair map.

To complete the solution of generating a full hash map, the values must be mapped to the key. Using the mapValues function, the values can be extracted from the objects and mapped back to the key or in this case rel.

Pseudo Code

  1. Index all objects by the chosen key.
  2. Map all values to the key.

Code

Below is the complete code with logging enabled. For a non-logging version, remove all lines with the tap function.

var data = [{ rel: 'link1', href: 'url1' }, 
            { rel: 'link2', href: 'url2' }, 
            { rel: 'link3', href: 'url3' },
            { rel: 'link4', href: 'url4' }];

function log(value) {
  document.getElementById("output").innerHTML += JSON.stringify(value, null, 2) + "\n"
}

var hashmap = _.chain(data)
  .keyBy('rel')

  .tap(log) // Line used just for logging

  .mapValues('href')

  .tap(log)
  
  .value();
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.6.1/lodash.min.js"></script>
<pre id="output"></pre>

\$\endgroup\$
  • 1
    \$\begingroup\$ Nice solution! Personally I would go for mapValues instead of transform. With transform you're updating state outside the closure (it is a side-effecting function), whereas with mapValues you are not. \$\endgroup\$ – Martijn Jul 7 '15 at 9:14
  • 1
    \$\begingroup\$ @Martijn Oh great suggestion! Makes for a much cleaner solution IMHO. I've updated the solution with your suggestion. Thanks! \$\endgroup\$ – Pete Jul 10 '15 at 20:21
  • 1
    \$\begingroup\$ @Pete Awesome! Just realized that lodash v4 renamed indexBy to keyBy \$\endgroup\$ – Pau Fracés Feb 11 '16 at 20:22
  • \$\begingroup\$ Updated to use keyBy \$\endgroup\$ – Pete Mar 8 '16 at 13:18
  • 1
    \$\begingroup\$ with lodash-fp this can also be done as a one-liner flow(fpKeyBy('rel'), fpMapValues('href'))(data) (fp-prefixed functions are fp-lodash versions) \$\endgroup\$ – Spain Train May 7 '16 at 0:05
18
\$\begingroup\$

In ES7 it's now as simple as:

data.reduce((acc, { rel, href }) => ({ ...acc, [rel]: href }), {});

(without even using Lodash).

\$\endgroup\$
  • \$\begingroup\$ would be great if it worked. but it doesn't seem to work! (node 5.0.0) \$\endgroup\$ – Rene Wooller Sep 8 '16 at 0:55
  • \$\begingroup\$ This is ES7, not ES2015 \$\endgroup\$ – Rene Wooller Sep 8 '16 at 1:01
  • \$\begingroup\$ @ReneWooller Right, sorry. Hard to keep track of what's what when using Babel :) \$\endgroup\$ – hon2a Sep 8 '16 at 9:15
  • \$\begingroup\$ That works, and it's nice to not have to have to have the lodash dependency, but damn if the lodash way isn't a million times more readable. \$\endgroup\$ – Thor84no Jan 29 '18 at 10:31
  • \$\begingroup\$ @Thor84no Full Lodash solution is e.g. _.mapValues(_.keyBy(data, 'rel'), 'href') (the accepted answer doesn't produce what the OP asked for). Readability depends on your familiarity with Lodash though. \$\endgroup\$ – hon2a Jan 29 '18 at 13:56
9
\$\begingroup\$

A simpler way would be to use "reduce".

var data = [
    { rel: 'link1', href: 'url1'},
    { rel: 'link2', href: 'url2'},
    { rel: 'link3', href: 'url3'},
    { rel: 'link4', href: 'url4'},
];

var hashmap = _.reduce(data, function(hash, value) {
  var key = value['rel'];
  hash[key] = value['href'];
  return hash;
}, {});

JSFiddle: https://jsfiddle.net/6txzzxq2/

With reduce you can iterate over the source array with an "accumulator" (in this case, a new object). Above we key the hash by the rel attribute with href as the value.

\$\endgroup\$
  • \$\begingroup\$ You could also use .transform to reduce the amount of code... but that's a matter of personal preference. \$\endgroup\$ – Pete Mar 14 '16 at 15:22
  • \$\begingroup\$ As stated by Martijn in a previous comment, both .reduce and .transform update state outside the closure whereas mapValues does not. So keep this in mind when using .reduce. \$\endgroup\$ – Pete Mar 16 '16 at 21:37
  • \$\begingroup\$ @Pete Updating outside state is not an inherent property of .reduce, but rather of the reducer itself. You can just as well use .reduce without side-effects (see my short answer). \$\endgroup\$ – hon2a Mar 24 '16 at 10:42
  • \$\begingroup\$ @hon2a True... I one thing that bugs me about this particular reduce solution, though, is that 'rel' and 'href' are hardcoded into the callback. It makes it less reusable. I tried to avoid that in the original solution. The maintenance cost of a one-liner to a five-liner is much more appealing to me. As for readability, reduce is less fluent compared keyBy and mapValues to a non-programmer. Granted, all this is at the cost of performance and so it's not the end all of solutions. Thanks for updating to ES2015. \$\endgroup\$ – Pete Mar 24 '16 at 13:40
  • \$\begingroup\$ @Pete I'd argue that a simple use of reduce is actually better readable than keyBy + mapValues(propNameShortcut), as reduce is a native (thus ubiquitous) method that doesn't require you to know the details of a specific (Lodash) API. \$\endgroup\$ – hon2a Apr 11 '16 at 10:39
7
\$\begingroup\$

Since lodash 4, you can use _.fromPairs:

var data = [
    { rel: 'link1', href: 'url1'},
    { rel: 'link2', href: 'url2'},
    { rel: 'link3', href: 'url3'},
    { rel: 'link4', href: 'url4'},
];

var hashmap = _.fromPairs(data.map(function(item) {
   return [item.rel, item.href];
}));
\$\endgroup\$
  • 1
    \$\begingroup\$ Is there a way to parameterize 'rel' and 'href' outside of the map method to make the solution more generic? \$\endgroup\$ – Pete May 25 '16 at 14:21
  • 1
    \$\begingroup\$ function makePair(keyProperty, valueProperty) { return function(item) { return [item[keyProperty], item[valueProperty]]; }; } var hashmap = _.fromPairs(data.map(makePair('rel', 'href'))); \$\endgroup\$ – Ricardo Stuven Aug 28 '17 at 13:20
2
\$\begingroup\$

With Lodash 4:

Using keyBy and mapValues

_.mapValues(_.keyBy(data, 'rel'), v => v.href);

Or:

_.chain(data).keyBy('rel').mapValues(v => v.href).value();
\$\endgroup\$
0
\$\begingroup\$

I see a lot of good answers already. I would just like to add one more answer to the list using Ramda's indexBy.

const data = [{
    id: 1,
    foo: 'bar'
  },
  {
    id: 2,
    baz: 'lorem'
  }
];

const dataById = R.indexBy(R.prop('id'), data);
console.log(dataById)
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.25.0/ramda.min.js"></script>

I hope it helps someone else :)

\$\endgroup\$
  • \$\begingroup\$ This isn't really a review of the code provided, is it? \$\endgroup\$ – Mast Jan 15 '18 at 20:20
  • \$\begingroup\$ Sorry misunderstood the use of this website. More familiar with StackOverflow :) \$\endgroup\$ – Abhishek Ghosh Jan 16 '18 at 0:33

protected by Jamal Jan 21 at 18:46

Thank you for your interest in this question. Because it has attracted low-quality or spam answers that had to be removed, posting an answer now requires 10 reputation on this site (the association bonus does not count).

Would you like to answer one of these unanswered questions instead?

Not the answer you're looking for? Browse other questions tagged or ask your own question.