I have a durations
as such:
[{'duration': 3600}, {'duration': 3600}]
And I want the output to be {3600: 2}
, where 2 is the number of occurrences of 3600.
My first attempt is use a loop, as such:
var count = {};
for (var d of durations) {
var length = d.duration;
if (length in count) {
count[length] += 1;
}
else {
count[length] = 1;
}
}
console.log(count);
The second attempt uses reduce() in lodash:
function foo(result, value) {
var length = value.duration;
if (length in result) {
result[length] += 1;
}
else {
result[length] = 1;
}
return result;
}
var reduced = _.reduce(durations, foo, {});
console.log(reduced);
As can be seen, this second attempt is still as verbose as before.
Is there a way to write the iteratee function foo
more conform to functional programming?
3 Answers 3
First point
Don't add quotes around property names when defining JS objects.
[{'duration': 3600}, {'duration': 3600}]
// should be
[{duration: 3600}, {duration: 3600}]
You state
"And I want the output to be {3600: 2}, where 2 is the number of occurrences of 3600."
Which is not that clear. Going by your code I assume you want [{duration: 60}, {duration: 60}, , {duration: 10}]
converted to {60:2, 10:1}
I will also assume that all array items contain an object with the property named duration
Using these assumptions for the rest of the answer.
Rewrite
Taking your first snippet and turning it into a function.
- You don't need the
if (length in count) {
, you can just useif (count[d.duration]) {
- The object
count
andd
should be constants as you don't change them. - Using a ternary you can reduce the 6 lines of the if else to one line.
Code
function countDurations(durations) {
const counts = {};
for (const {duration: len} of durations) {
counts[len] = counts[len] ? counts[len] + 1 : 1;
}
return counts;
}
Or a little less verbose as a arrow function
const countDur = dur => dur.reduce((c, {duration: l}) => (c[l] = c[l]? c[l] + 1: 1, c), {});
or
const countDurs = durs =>
durs.reduce((counts, {duration: len}) =>
(counts[len] = counts[len] ? counts[len] + 1 : 1, counts)
, {}
);
Rewrite 2
The second snippet is algorithmicly the same, just uses a different iteration method.
- JavaScript has Array.reduce so you don't need to use a external library.
- Separating the iterator inner block into a function
count
Code
const count= (cnt, {duration: len}) => (cnt[len] = cnt[len] ? cnt[len] + 1 : 1, cnt);
const countDurations = durations => durations.reduce(count, {});
Or encapsulating the count
function via closure
const countDurations = (()=> {
const count = (cnt, {duration: len}) => (cnt[len] = cnt[len] ? cnt[len] + 1 : 1, cnt);
return durations => durations.reduce(count, {});
})();
Functional?
"Is there a way to write the iterate function foo more conform to functional programming?"
JavaScript is not designed to be a functional language (it is impossible to create pure functions) so It is best to use functional as a guide rather than a must.
Almost pure
The second rewrite is more along the functional approach, but functional purist would complain that the reducer function has side effects by modifying the counter. You can create a copy of the counts each call as follows..
const counter = (c, {duration: len}) => (c = {...c}, (c[len] = c[len] ? c[len] + 1 : 1), c);
const countDurations = durations => durations.reduce(counter, {});
However this add significant memory and processing overhead with zero benefit (apart from a functional pat on the back).
Note
- This answer uses destructuring property name alias so check for browser compatibility.
It's not really clear what you mean by "functional".
The use of reduce
is nifty, and correctly done, but it seem like cargo cult programming. The realization that a lot of loop-like actions can be represented in terms of reduce
(fold, aggregate, etc) is important to functional programing, but it should no more be your first choice than a while-loop.
The fact that it's verbose isn't necessarily a problem. It suggests that you might be failing at a general goal:
Write what you mean.
One of the things that makes functional programming good is that it helps us do that.
Javascript has limited tools for directly declaring lists. What you mean to do is declare a Dictionary from the Set of duration
values to the Count of the source array Filtered by the respective duration. Or even better you could use a grouping function.
You could bring in a library for the task, which may be a good idea, or you could compromise a little.
function getCounts(items, key){
let c = {};
for(let item of items){
c[key(item)] = items.filter(function(i){
return key(i) === key(item);
}).length;
}
return c;
};
counts = getCounts(durations, function(i){ return i['duration']; })'
You'll notice that that's quite inefficient. We could make it less wasteful, but it'd be less terse.
-
2\$\begingroup\$ The introduction of your answer is good, but the code is not since its complexity is \$\mathcal O(n^2)\$. \$\endgroup\$Roland Illig– Roland Illig2019年05月16日 02:29:05 +00:00Commented May 16, 2019 at 2:29
-
\$\begingroup\$ Yeah, don't actually use that code. \$\endgroup\$ShapeOfMatter– ShapeOfMatter2019年05月16日 14:33:59 +00:00Commented May 16, 2019 at 14:33
To reduce the verboseness of the code, you should extract the main idea of that code into a function:
function histogram(data, key) {
const count = {};
for (let item of data) {
const value = data[key];
count[value] = (count[value] || 0) + 1;
}
return count;
}
That way you can write your code as:
console.log(histogram(durations, 'duration'));
I first tried a simple count[value]++
, but that didn't work since undefined + 1
is still undefined
. Therefore I had to use the || 0
trick.
Explore related questions
See similar questions with these tags.