93
\$\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\$
1

7 Answers 7

110
\$\begingroup\$

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

_.keyBy(data, 'rel');
\$\endgroup\$
3
  • 4
    \$\begingroup\$ I don't understand why is this the accepted answer since it doesn't give the same result as the exposed functions \$\endgroup\$ Commented Feb 17, 2015 at 15:53
  • 1
    \$\begingroup\$ @PauFracés See below for revised solution. \$\endgroup\$
    – Pete
    Commented Mar 13, 2015 at 21:14
  • 4
    \$\begingroup\$ Note: Now known as _.keyBy(lodash.com/docs#keyBy). \$\endgroup\$
    – 2540625
    Commented Feb 18, 2016 at 2:07
43
\$\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\$
5
  • 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
    Commented Jul 7, 2015 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
    Commented Jul 10, 2015 at 20:21
  • 1
    \$\begingroup\$ @Pete Awesome! Just realized that lodash v4 renamed indexBy to keyBy \$\endgroup\$ Commented Feb 11, 2016 at 20:22
  • \$\begingroup\$ Updated to use keyBy \$\endgroup\$
    – Pete
    Commented Mar 8, 2016 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\$ Commented May 7, 2016 at 0:05
22
\$\begingroup\$

In ES7 it's now as simple as:

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

(without even using Lodash).

\$\endgroup\$
9
  • \$\begingroup\$ would be great if it worked. but it doesn't seem to work! (node 5.0.0) \$\endgroup\$ Commented Sep 8, 2016 at 0:55
  • \$\begingroup\$ This is ES7, not ES2015 \$\endgroup\$ Commented Sep 8, 2016 at 1:01
  • \$\begingroup\$ @ReneWooller Right, sorry. Hard to keep track of what's what when using Babel :) \$\endgroup\$
    – hon2a
    Commented Sep 8, 2016 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\$
    – Vala
    Commented Jan 29, 2018 at 10:31
  • 1
    \$\begingroup\$ @digitai My point was mostly that you can easily just inline it in plain JS now. Obviously you could create an abstraction for it, but then you'd likely want to make it more efficient as well (e.g. avoid creating a new object on every pass). If key and value prop names would be external variables, you'd destructure as { [keyProp]: key, [valueProp]: value }. \$\endgroup\$
    – hon2a
    Commented Apr 1, 2020 at 8:36
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\$
8
  • \$\begingroup\$ You could also use .transform to reduce the amount of code... but that's a matter of personal preference. \$\endgroup\$
    – Pete
    Commented Mar 14, 2016 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
    Commented Mar 16, 2016 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
    Commented Mar 24, 2016 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
    Commented Mar 24, 2016 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
    Commented Apr 11, 2016 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\$
2
  • 1
    \$\begingroup\$ Is there a way to parameterize 'rel' and 'href' outside of the map method to make the solution more generic? \$\endgroup\$
    – Pete
    Commented May 25, 2016 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\$ Commented Aug 28, 2017 at 13:20
3
\$\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\$
2
  • \$\begingroup\$ This isn't really a review of the code provided, is it? \$\endgroup\$
    – Mast
    Commented Jan 15, 2018 at 20:20
  • \$\begingroup\$ Sorry misunderstood the use of this website. More familiar with StackOverflow :) \$\endgroup\$ Commented Jan 16, 2018 at 0:33

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