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.
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.
-
\$\begingroup\$ I've compiled the most common ways to turn an object into an array in this lodash feature request (which currently needs upvotes!) \$\endgroup\$BlueRaja - Danny Pflughoeft– BlueRaja - Danny Pflughoeft2016年11月17日 23:10:58 +00:00Commented Nov 17, 2016 at 23:10
7 Answers 7
I think you are looking for _.keyBy
(or _.indexBy
in older versions)
_.keyBy(data, 'rel');
-
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\$Pau Fracés– Pau Fracés2015年02月17日 15:53:44 +00:00Commented Feb 17, 2015 at 15:53
-
1\$\begingroup\$ @PauFracés See below for revised solution. \$\endgroup\$Pete– Pete2015年03月13日 21:14:07 +00:00Commented Mar 13, 2015 at 21:14
-
4\$\begingroup\$ Note: Now known as
_.keyBy
(lodash.com/docs#keyBy). \$\endgroup\$5260452– 52604522016年02月18日 02:07:41 +00:00Commented Feb 18, 2016 at 2:07
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
- Index all objects by the chosen key.
- 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>
-
1\$\begingroup\$ Nice solution! Personally I would go for
mapValues
instead oftransform
. Withtransform
you're updating state outside the closure (it is a side-effecting function), whereas withmapValues
you are not. \$\endgroup\$Martijn– Martijn2015年07月07日 09:14:44 +00:00Commented 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– Pete2015年07月10日 20:21:18 +00:00Commented Jul 10, 2015 at 20:21
-
1\$\begingroup\$ @Pete Awesome! Just realized that lodash v4 renamed
indexBy
tokeyBy
\$\endgroup\$Pau Fracés– Pau Fracés2016年02月11日 20:22:15 +00:00Commented Feb 11, 2016 at 20:22 -
\$\begingroup\$ Updated to use
keyBy
\$\endgroup\$Pete– Pete2016年03月08日 13:18:30 +00:00Commented 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\$Spain Train– Spain Train2016年05月07日 00:05:26 +00:00Commented May 7, 2016 at 0:05
In ES7 it's now as simple as:
data.reduce((acc, { rel, href }) => ({ ...acc, [rel]: href }), {});
(without even using Lodash).
-
\$\begingroup\$ would be great if it worked. but it doesn't seem to work! (node 5.0.0) \$\endgroup\$Rene Wooller– Rene Wooller2016年09月08日 00:55:05 +00:00Commented Sep 8, 2016 at 0:55
-
\$\begingroup\$ This is ES7, not ES2015 \$\endgroup\$Rene Wooller– Rene Wooller2016年09月08日 01:01:12 +00:00Commented Sep 8, 2016 at 1:01
-
\$\begingroup\$ @ReneWooller Right, sorry. Hard to keep track of what's what when using Babel :) \$\endgroup\$hon2a– hon2a2016年09月08日 09:15:18 +00:00Commented 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– Vala2018年01月29日 10:31:00 +00:00Commented 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– hon2a2020年04月01日 08:36:40 +00:00Commented Apr 1, 2020 at 8:36
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.
-
\$\begingroup\$ You could also use
.transform
to reduce the amount of code... but that's a matter of personal preference. \$\endgroup\$Pete– Pete2016年03月14日 15:22:18 +00:00Commented Mar 14, 2016 at 15:22 -
\$\begingroup\$ As stated by Martijn in a previous comment, both
.reduce
and.transform
update state outside the closure whereasmapValues
does not. So keep this in mind when using.reduce
. \$\endgroup\$Pete– Pete2016年03月16日 21:37:16 +00:00Commented 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– hon2a2016年03月24日 10:42:19 +00:00Commented 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 comparedkeyBy
andmapValues
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– Pete2016年03月24日 13:40:18 +00:00Commented Mar 24, 2016 at 13:40 -
\$\begingroup\$ @Pete I'd argue that a simple use of
reduce
is actually better readable thankeyBy
+mapValues(propNameShortcut)
, asreduce
is a native (thus ubiquitous) method that doesn't require you to know the details of a specific (Lodash) API. \$\endgroup\$hon2a– hon2a2016年04月11日 10:39:13 +00:00Commented Apr 11, 2016 at 10:39
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];
}));
-
1\$\begingroup\$ Is there a way to parameterize 'rel' and 'href' outside of the
map
method to make the solution more generic? \$\endgroup\$Pete– Pete2016年05月25日 14:21:34 +00:00Commented 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\$Ricardo Stuven– Ricardo Stuven2017年08月28日 13:20:57 +00:00Commented Aug 28, 2017 at 13:20
With Lodash 4:
Using keyBy
and mapValues
_.mapValues(_.keyBy(data, 'rel'), v => v.href);
Or:
_.chain(data).keyBy('rel').mapValues(v => v.href).value();
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 :)
-
\$\begingroup\$ This isn't really a review of the code provided, is it? \$\endgroup\$2018年01月15日 20:20:17 +00:00Commented Jan 15, 2018 at 20:20
-
\$\begingroup\$ Sorry misunderstood the use of this website. More familiar with StackOverflow :) \$\endgroup\$Abhishek Ghosh– Abhishek Ghosh2018年01月16日 00:33:55 +00:00Commented Jan 16, 2018 at 0:33