It will help you to start thinking about your data in terms of types.
a Person
is an object with four properties,
this
which is a Stat
object (which we'll define soon)
that
which is another Stat
object
thos
which is a Rank
object (which we'll define as well)
them
which is another Rank
object
To continue, we specify Stat
as an object with three properties,
makes
which is a number
attempts
which is another number
pct
which is another number
And lastly, Rank
is an object with two properties,
value
which is a number
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.