Reputation: 29967
I have an Object such as
{
a: {
x: 1,
y: 2
},
b: {
x: 1,
y: 2
},
c: {
x: 100,
y: 2
},
}
I would like to count the number of elements which fulfill the condition x: 1
. Is there a straightforward way to do this?
I could go the simple way but I would like to learn the JavaScriptonic way (if there is one):
let data = {
a: {
x: 1,
y: 2
},
b: {
x: 1,
y: 2
},
c: {
x: 100,
y: 2
},
}
let counter = 0
for (k in data) {
if (data[k].x === 1) {
counter += 1
}
}
console.log(counter)
// 2
Upvotes: 1
Views: 1177
Reputation: 336
using forEach
let data = {
a: {
x: 1,
y: 2
},
b: {
x: 1,
y: 2
},
c: {
x: 100,
y: 2
},
}
let counter = 0
Object.values(data).forEach(item=>{
item.x===1?counter++:0;
});
console.log(counter)
Upvotes: 0
Reputation: 50787
As Aplet123 pointed out, probably the shortest answer is
Object .values (data) .filter (v => v.x == 1) .length
While this is fine, it does some unnecessary object creation, creating a new array of objects that match, only to throw it away after retrieving its count. Unless you're doing this very often or with huge data sets, this is not likely a problem, but it's something to keep in mind.
The other alternatives, either some variant of your original loop or a reduce
-based solution work fine, although they might be a bit verbose.
I would suggest, though, that you might want to think of this in terms of reusable parts. Even a question as simple as this can be broken down further. Let's imagine we are trying to turn this exercise into a reusable function.
Q: What does this proposed function do?
A: It counts all the object properties of our input which have an
x
value of1
.Q: Well, we've already done that with
(data) => Object .values (data) .filter (v => v.x == 1) .length
Is this enough?
A: I don't see why not. It does what we need.
Q: So you think you'll never need to find all those with
x
value of2
?A: Well, maybe, but if I do, I can just write a new version like
(data) => Object .values (data) .filter (v => v.x == 2) .length.
There's nothing wrong with that, is there?
Q: And then if you need
x
value of3
?A: Then I can just do it again, right? This is easy
Q: (a dirty look)
A: Ok, I get it! We need to parameterize that value. How about writing
(xVal, data) => Object .values (data) .filter (v => v.x == xVal) .length
Q: Does that feel like progress?
A: Perhaps, but I feel like you're going to spring something now...
Q: Me? Never!
A: I think I can guess what's next. You're going to ask what happens if I need to count the objects with a
y
value of1
.Q: You are learning, young Padawan! That's exactly right. So, how would you handle that?
A: I think we can just do
(name, val, data) => Object .values (data) .filter (v => v[name] == val) .length
(...thinks for a short while...) Yes, that should work.
Q: So now if you need to count those where both
x
equals1
andy
equals3
. Is it easy to adapt this for that problem?A: It's not trivial. But I can see how I might pass to our function something like
{x: 1, y: 3}
and check each of the properties of that object. That is certainly doable. Is that how you think I should write it?Q: Now, don't get impatient! We'll get there. That might be a useful function. But what happens if I want to count those where property
x
is greater than property 'y', and both are greater than42
?A: Time out! This is straying really far from the original requirement. Why should I be concerned with a case like that?
Q: What if I were to tell you that you with about the same number of lines of code as in your original approach, we can solve all these problems at once? Would that intrigue you?
A: I suppose, but you make it sound as though we could count the matching properties for any condition at all. That sounds crazy.
Q: That's exactly what I'm suggesting. How could we go about that?
A: I don't know. It doesn't make much sense to me. You'd have to pass a whole program into our function. How do you do that?
Q: Well, perhaps not a program. How about a function?
A: I suppose that would do it too. But we can't pass functions around willy-nilly, can we?
Q: We did it above.
A: What do you mean?
Q: What did we pass to the
filter
method ofArray.prototype
in our answers so far?A: An arrow function. Yes, I get it, but that's for some magic built-in part of the language. Just because we can pass a function to
Array.prototype.filter
, it doesn't meant that we can pass one to our own function. How would that work?Q: Let's try it and see. If we're going to have a parameter, we have to name it. What do you think we should call it?
A: I know. How about
magicScrewyThingThatTeacherThinksWillWorkButIStillDoubt
?Q: Sure, but you get to type it every time!
A: Seriously, though, I don't know. Do you have a suggestion?
Q: I often call such functions
pred
.A: Short for "predator"? "Predilection"? "Prednisone"?...
Q: "Predicate". A function that returns
true
orfalse
.A: Ok, and then we count the
true
s, right?Q: Exactly, so how would we write this?
A: Well, we would start with
(data, pred) => Object .values (data) .filter (v =>
... But then what?Q: Well, then we want to call our function, right? How do we call a function?
A: We just use the function name then the arguments inside parentheses. But we don't have the true function name here. All we have is that
pred
.Q: But inside our call, that is the function name. So we can do the same thing. What would that look like?
A: I suppose it would be just
(data, pred) => Object .values (data) .filter (v => pred (v)) .length
Q: And how would we apply that to our original problem?
A: Can I pass an arrow function like we did to
filter
?Q: Absolutely.
A: Then... something like this?
const countMatchingProps = (data, pred) => Object.values (data) .filter (v => pred (v)) .length countMatchingProps (data, v => v.x == 1) //=> 2
Q: Does that work?
A: Yes! Yes it does. So we've reached a solution?
Q: Well no, not quite yet. First off, what have we been learning about currying?
A: That again? I know how to do it. I still don't really understand why. We didn't do that in our Java classes.
Q: Ok, but here we write 99% of our functions in a curried form. How would this function change?
A: So we put the parameter least likely to change first, then the one next most likely to change, and so on. For our two parameters, this means the predicate goes first, then the data. So how about this?
const countMatchingProps = (pred) => (data) => Object.values (data) .filter (v => pred (v)) .length
Does that look good?
Q: 𝅘𝅥𝅘𝅥𝅮 You've got to admit it's getting better... Getting so much better all the time. 𝅘𝅥𝅮𝅘𝅥𝅘𝅥𝅮
A: Umm...
Q: Never mind (muttering "Youth these days!"). That's much nicer.
A: "...but"?
Q: There's one minor thing. What does
v => pred(v)
do?A: It's a function. It accepts a value, and returns
true
orfalse
.Q: Exactly. And what does
pred
do?A: It's a function too. It accepts a value, and returns
true
orfalse
. Wait! What does that mean?Q: It means one more simplification.
A: Can I really replace
v => pred(v)
with justpred
? It really feels like cheating somehow.Q: Let's try it and see.
A:
const countMatchingProps = (pred) => (data) => Object.values (data) .filter (pred) .length countMatchingProps (v => v.x == 1) (data) //=> 2
Yes, that works fine. But I'm not sure I want to include that arrow function...
Q: remember that...
A: I know, I know, "remember that we prefer to call arrow functions lambdas." I still don't know why. Anyway, I'm not sure I want to include that lambda every time I need to count those with
x
property of1
. Can I make a specific function from this for my use-case?Q: That's what the lessons about currying were supposed to be for. Don't you recall how we do this?
A: That's ... partial application? Of the curried function?
Q: (nods)
A: So, I can just write
const countX1s = countMatchingProps (v => v.x == 1) // ... later countX1s (data) //=> 2
Oh, that makes sense. Why didn't you just say this when we were discussing currying?
Q: (exasperated sigh)
A: Ok, so we've finally arrived at our answer!
Q: Well...
A: Oh no! More?
Q: Just one more step. What if we wanted to do something similar for arrays? Count all those which match some condition?
A: Well after that, it's easy:
const countMatches = (pred) => (arr) => arr .filter (pred) .length
Q: Do you see similarities between
countMatches
andcountMatchingProps
?A: Well, yeah, it's as though
countMatches
is embedded incountMatchingProps
.Q: Exactly. So what do we do in cases like that?
A: We refactor.
Q: Exactly. How would that look?
A: I think it's easy enough:
const countMatches = (pred) => (arr) => arr .filter (pred) .length const countMatchingProps = (pred) => (arr) => countMatches (pred) (Object .values (arr)) const countX1s = countMatchingProps (v => v.x == 1) // ... later countX1s (data) //=> 2
Q: And how do you feel about this code?
A: Well, we started from a simple requirement and what we wrote is much, much more powerful. That's got to be good.
Q: And has the code gotten much more complex?
A: Well, compared to my initial
for
-loop version, it's probably less complex. But it's still more complex than the version from Aplet123.Q: Yes, abstraction always has it's cost. But how do you like this version of the code?
A: I'm still absorbing it. But I think I really like it. Is this how you would write it?
Q: No, but that's a lesson for another day.
A: Come on, after you subjected me to your rendition of the Beatles? I think you owe me.
Q: Oh, so you did recognize the song? Perhaps there's still hope for today's youth... Ok. My version might look like this:
const count = compose (length, filter) const countProps = compose2 (count, identity, values) const countX1s = countProps (propEq ('x', 1))
A: Huh?
Q: As I said, a lesson for another day.
Upvotes: 0
Reputation: 11622
You can use .reduce()
function combined with Object.values()
, here is a working snippet:
let data = {
a: {
x: 1,
y: 2
},
b: {
x: 1,
y: 2
},
c: {
x: 100,
y: 2
},
}
let counter = Object.values(data).reduce((acc,item) => {
(item.x === 1) ? acc++: 0;
return acc
}, 0);
console.log(counter)
Upvotes: 1
Reputation: 35482
The cleanest way I can come up with is using Object.values
:
console.log(Object.values(data).filter(v => v.x === 1).length); // 2
Upvotes: 7