1

Apologies if the title is not particularly clear. I have made a short reproducible example below of what I'm attempting to do:

var arrayOfPeopleStats = [
  person1 = { 
    this: { makes: 10, attempts: 15, pct: .666667 },
    that: { makes: 12, attempts: 16, pct: .75 },
    thos: { value: 10, rank: 24 },
    them: { value: 15, rank: 11 }
  },
  person2 = { 
    this: { makes: 5, attempts: 15, pct: .333333 },
    that: { makes: 10, attempts: 12, pct: .83333 },
    thos: { value: 4, rank: 40 },
    them: { value: 3, rank: 32 }
  },
  person3 = { 
    this: { makes: 14, attempts: 14, pct: 1 },
    that: { makes: 7, attempts: 8, pct: .875 },
    thos: { value: 12, rank: 12 },
    them: { value: 13, rank: 10 }
  }
]


var desiredOutput = [
  allPeople: {
    this: { makes: 29, attempts: 44, pct: .65909 },
    that: { makes: 29, attempts: 36, pct: .80555 },
    thos: { value: 26, rank: doesnt matter },
    them: { value: 31, rank: doesnt matter }
  }
]

arrayOfPeopleStats is an array with person objects. Each person objects has a couple of stats (this, that thos, them, in this example), and each of those stats is an object with either the trio of makes, attempts, steals, or value, rank.

I would like to sum the makes, attempts, and values over the stat objects for each person, and compute the new pct value for the summed-together data.

I will post shortly on the attempt I've done using ES6 reduce, although I'm not sure this is the best approach for the problem. Any help is greatly appreciated on this!

Canovice
  • 9,012
  • 22
  • 93
  • 211
  • 1
    _"Apologies if the title is not particularly clear"_ it could not actually be clearer – Phil Mar 14 '19 at 04:05
  • Your `person1 = `, `person2 = `, etc statements are unnecessary (unless you're actually using those variables elsewhere) – Phil Mar 14 '19 at 04:07
  • According to me, you have to use map function to convert the array to a different array. In map, you could do something like this `arr.map((person) => { for key of person, perform reduce` and inside reduce, you could accumulate values corresponding to the `key` property and return the mapped element for the new array – Abhishek Mehandiratta Mar 14 '19 at 04:11
  • This is very, very simple with a `forEach()` statement. Is there an absolute requirement to us map/reduce? – Randy Casburn Mar 14 '19 at 04:19

3 Answers3

3

It will help you to start thinking about your data in terms of types.

a Person is an object with four properties,

  1. this which is a Stat object (which we'll define soon)
  2. that which is another Stat object
  3. thos which is a Rank object (which we'll define as well)
  4. them which is another Rank object

To continue, we specify Stat as an object with three properties,

  1. makes which is a number
  2. attempts which is another number
  3. pct which is another number

And lastly, Rank is an object with two properties,

  1. value which is a number
  2. rank which is another number

We can now regard arrayOfPeopleStats as an array of items where each item is of type Person. To combine like types, we define a way to create values of our type and a way to concat (add) them.

I'll start by defining the smaller types first, Stat -

const Stat =
  { create: (makes, attempts) =>
      ({ makes, attempts, pct: makes / attempts })

  , concat: (s1, s2) =>
      Stat .create
        ( s1.makes + s2.makes
        , s1.attempts + s2.attempts
        )
  }

The Rank type is similar. You said it "doesn't matter" what the combined value for rank is, but this shows you that a choice must be made. You can pick one or the other, add the two values together, or some other option entirely. In this implementation, we choose the highest rank value -

const Rank =
  { create: (value, rank) =>
      ({ value, rank })

  , concat: (r1, r2) =>
      Rank .create
        ( r1.value + r2.value
        , Math .max (r1.rank, r2.rank)
        ) 
  }

Lastly, we implement Person. I renamed this, that, thos, them to a, b, c, d because this is a special variable in JavaScript and I think it makes the example a little easier to follow; the data in your question looks mocked anyway. What's important to note here is that the Person.concat function relies on Stat.concat and Rank.concat, keeping the combination logic for each type nicely separated -

const Person =
  { create: (a, b, c, d) =>
      ({ a, b, c, d })

  , concat: (p1, p2) =>
      Person .create
        ( Stat .concat (p1.a, p2.a)
        , Stat .concat (p1.b, p2.b)
        , Rank .concat (p1.c, p2.c)
        , Rank .concat (p1.d, p2.d)
        )
  }

Now to combine all person items in your array, simply reduce using the Person.concat operation -

arrayOfPeopleStat .reduce (Person.concat)

// { a: { makes: 29, attempts: 44, pct: 0.6590909090909091 }
// , b: { makes: 29, attempts: 36, pct: 0.8055555555555556 }
// , c: { value: 26, rank: 40 }
// , d: { value: 31, rank: 32 }
// }

Note, when you have nicely defined data constructors, writing the data out by hand using { ... } is cumbersome and error prone. Instead use the create function of your type -

var arrayOfPeopleStat =
  [ Person .create
      ( Stat .create (10, 15)
      , Stat .create (12, 16)
      , Rank .create (10, 24)
      , Rank .create (15, 11)
      )
  , Person .create
      ( Stat .create (5, 15)
      , Stat .create (10, 12)
      , Rank .create (4, 40)
      , Rank .create (3, 32)
      )
  , Person .create
      ( Stat .create (14, 14)
      , Stat .create (7, 8)
      , Rank .create (12, 12)
      , Rank .create (13, 10)
      )
  ]

An added advantage of using create functions over writing out data by-hand using { ... } is the constructors can do favors for you. Note how pct is automatically computed for Stat. This prevents someone from writing an invalid stat like { makes: 1, attempts: 2, pct: 3 } where the pct property is not equal to makes/attempts. If the receiver of this data doesn't check, we have no way of ensuring the pct property's integrity. Using our constructor on the other hand, Stat.create only accepts makes and attempts and the pct field is automatically computed, guaranteeing a correct pct value.

The constructor could also prevent someone from creating an stat which would result in an error, such as Stat .create (10, 0) which would attempt to compute 10/0 (division by zero error) for the pct property -

const Stat =
  { create: (makes = 0, attempts = 0) =>
      attempts === 0
        ? { makes, attempts, pct: 0 }                // instead of throwing error
        : { makes, attempts, pct: makes / attempts } // safe to divide

  , concat: ...
  }

Ultimately these choices are up to you. Another programmer might favor an error in this situation. Such as -

Stat .create (10, 0)
// Error: invalid Stat. makes (10) cannot be greater than attempts (0). attempts cannot be zero.

The result of the reduce is the same, of course -

arrayOfPeopleStat .reduce (Person.concat)

// { a: { makes: 29, attempts: 44, pct: 0.6590909090909091 }
// , b: { makes: 29, attempts: 36, pct: 0.8055555555555556 }
// , c: { value: 26, rank: 40 }
// , d: { value: 31, rank: 32 }
// }

Expand the snippet below to verify the results in your own browser -

const Stat =
  { create: (makes, attempts) =>
      ({ makes, attempts, pct: makes / attempts })

  , concat: (s1, s2) =>
      Stat .create
        ( s1.makes + s2.makes
        , s1.attempts + s2.attempts
        )
  }

const Rank =
  { create: (value, rank) =>
      ({ value, rank })
  
  , concat: (r1, r2) =>
      Rank .create
        ( r1.value + r2.value
        , Math .max (r1.rank, r2.rank)
        ) 
  }

const Person =
  { create: (a, b, c, d) =>
      ({ a, b, c, d })

  , concat: (p1, p2) =>
      Person .create
        ( Stat .concat (p1.a, p2.a)
        , Stat .concat (p1.b, p2.b)
        , Rank .concat (p1.c, p2.c)
        , Rank .concat (p1.d, p2.d)
        )
  }

var data =
  [ Person .create
      ( Stat .create (10, 15)
      , Stat .create (12, 16)
      , Rank .create (10, 24)
      , Rank .create (15, 11)
      )
  , Person .create
      ( Stat .create (5, 15)
      , Stat .create (10, 12)
      , Rank .create (4, 40)
      , Rank .create (3, 32)
      )
  , Person .create
      ( Stat .create (14, 14)
      , Stat .create (7, 8)
      , Rank .create (12, 12)
      , Rank .create (13, 10)
      )
  ]

console .log (data .reduce (Person.concat))

// { a: { makes: 29, attempts: 44, pct: 0.6590909090909091 }
// , b: { makes: 29, attempts: 36, pct: 0.8055555555555556 }
// , c: { value: 26, rank: 40 }
// , d: { value: 31, rank: 32 }
// }

But what happens if data is an empty array?

[] .reduce (Person.concat)
// Uncaught TypeError: Reduce of empty array with no initial value

If a piece of our code works for some arrays but breaks for other arrays, that's bad. An array is used to represent zero or more values, so we want to cover all of our bases, including the case of zero stats. A function that is defined for all possible values of its input is a total function and we strive to write our programs this way whenever possible.

The error message provides the wisdom for the fix; we must provide the initial value. Such as 0 in -

const add = (x, y) =>
  x + y

console .log
  ( [] .reduce (add, 0)           // 0
  , [ 1, 2, 3 ] .reduce (add, 0)  // 6
  )

Using the initial value, we always received a proper result, even when the array was empty. For adding numbers, we use the initial value of zero, because it is the identity element. You can think of this as a sort of empty value.

If we're trying to reduce an array of Person types, what is the initial value for Person?

arrayOfPeopleStat .reduce (Person.concat, ???)

We define the an empty value for each of our types, if possible. We'll start with Stat -

const Stat = 
  { empty:
      { makes: 0
      , attempts: 0
      , pct: 0
      }
  , create: ...
  , concat: ...
  }

Next we'll do Rank -

const Rank =
  { empty:
      { value: 0
      , rank: 0
      }
  , create: ...
  , concat: ...
  }

Again, we're seeing the Person as compound data, ie data constructed of other data. We rely on Stat.empty and Rank.empty to create Person.empty -

const Person =
  { empty:
      { a: Stat.empty
      , b: Stat.empty
      , c: Rank.empty
      , d: Rank.empty
      }
  , create: ...
  , concat: ...
  }

Now we can specify Person.empty as the initial value and prevent aggravating errors from popping up -

arrayOfPeopleStat .reduce (Person.concat, Person.empty)
// { a: { makes: 29, attempts: 44, pct: 0.6590909090909091 }
// , b: { makes: 29, attempts: 36, pct: 0.8055555555555556 }
// , c: { value: 26, rank: 40 }
// , d: { value: 31, rank: 32 }
// }

[] .reduce (Person.concat, Person.empty)
// { a: { makes: 0, attempts: 0, pct: 0 }
// , b: { makes: 0, attempts: 0, pct: 0 }
// , c: { value: 0, rank: 0 }
// , d: { value: 0, rank: 0 }
// }

As a bonus, you now have a basic understanding of monoids.

Mulan
  • 129,518
  • 31
  • 228
  • 259
  • Damn, this is an entire approach to coding that never crossed my mind, really appreciate the long and thorough solution. – Canovice Mar 15 '19 at 22:22
  • Thanks for the comment, I'm happy you enjoyed it. I made a [revision](https://stackoverflow.com/posts/55155663/revisions) that includes an extra remark about the added usefulness of the `create` functions. – Mulan Mar 15 '19 at 22:41
1

When deciding between map and reduce you can think that map will return a new array with same length of original one. It isn't what you want, so you must not use it. You actually want to reduce your array to an object. So just use reduce:

About a function to reduce it, here is a possible one:

var arrayOfPeopleStats=[person1={this:{makes:10,attempts:15,pct:.666667},that:{makes:12,attempts:16,pct:.75},thos:{value:10,rank:24},them:{value:15,rank:11}},person2={this:{makes:5,attempts:15,pct:.333333},that:{makes:10,attempts:12,pct:.83333},thos:{value:4,rank:40},them:{value:3,rank:32}},person3={this:{makes:14,attempts:14,pct:1},that:{makes:7,attempts:8,pct:.875},thos:{value:12,rank:12},them:{value:13,rank:10}}];

const resp = Object.values(arrayOfPeopleStats).reduce((obj, { this: localThis, that, thos, them }) => {
  obj.this = { makes: obj.this.makes + localThis.makes, attempts: obj.this.attempts + localThis.attempts };
  obj.this.pct = obj.this.makes / obj.this.attempts;
  obj.that = { makes: obj.that.makes + that.makes, attempts: obj.that.attempts + that.attempts };
  obj.that.pct = obj.that.makes / obj.that.attempts;
  obj.thos = { value: obj.thos.value + thos.value, rank: obj.thos.rank + thos.rank };
  obj.them = { value: obj.them.value + them.value, rank: obj.thos.rank + them.rank };
  return obj;
})

const formattedResp = [{ allPeople: resp }]

console.log(formattedResp)

And I don't suggest you using this as a property name once it can conflict with this keyword.

guijob
  • 4,413
  • 3
  • 20
  • 39
  • 1
    The reason this feels suffocating is because there is no separation of concerns. The reader cannot glance at the `reduce` lambda and understand its intention. See my answer for another approach that allows your program to grow in a more manageable way. – Mulan Mar 15 '19 at 22:44
-1
arrayOfPeopleStats.reduce((a, v) => {
  a.allPeople.this = {
    makes: a.allPeople.this.makes + v.this,
    attempts: a.allPeople.this.attempts + v.this.attempts,
  };
  a.allPeople.that = {
    makes: a.allPeople.that.makes + v.that.makes,
    attempts: a.allPeople.that.attempts + v.that.attempts,
  };
  a.allPeople.thos = {
    value: a.allPeople.thos.value + v.thos.value,
  };
  a.allPeople.them = {
    value: a.allPeople.them.value + v.them.value,
  };
  a.allPeople.this.pct = a.allPeople.this.makes / a.allPeople.this.attempts
  a.allPeople.that.pct = a.allPeople.that.makes / a.allPeople.that.attempts
  return a;
}, {allPeople: {
    this: { makes: 0, attempts: 0, pct: 0 },
    that: { makes: 0, attempts: 0, pct: 0 },
    thos: { value: 0, rank: 0 },
    them: { value: 0, rank: 0 }
  }
}});

I have ignored rank as you have said it does not matter, it will be 0 always with the above solution. I have also chosen to not wrap the resulting object in an array as you have done in your desired result. See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce for an explanation of this API.

MaxPRafferty
  • 4,819
  • 4
  • 32
  • 39