简体   繁体   中英

How to conditionally count the number of elements in an Object?

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

The cleanest way I can come up with is using Object.values :

console.log(Object.values(data).filter(v => v.x === 1).length); // 2

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)

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 of 1 .

Q: Well, we've already done that with

(data) => Object.values (data).filter (v => vx == 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 of 2 ?

A: Well, maybe, but if I do, I can just write a new version like

(data) => Object.values (data).filter (v => vx == 2).length.

There's nothing wrong with that, is there?

Q: And then if you need x value of 3 ?

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 => vx == 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 of 1 .

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 equals 1 and y equals 3 . 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 than 42 ?

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 of Array.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 or false .

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 => vx == 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 or false .

Q: Exactly. And what does pred do?

A: It's a function too. It accepts a value, and returns true or false . Wait? What does that mean?

Q: It means one more simplification.

A: Can I really replace v => pred(v) with just pred ? 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 => vx == 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 of 1 . 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 => vx == 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 and countMatchingProps ?

A: Well, yeah, it's as though countMatches is embedded in countMatchingProps .

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 => vx == 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.

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)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM