The problem you have there is in some sense similar to what JavaScripts's Array.reduce function is designed to tackle (other languages provide alike facilities).
It generalizes various data accumulation/summarizing procedures, so you could use that for inspiration.
Here's a quick overview: The function iterates over an array, and you pass in a lambda that takes in an "accumulator" object
and the next element in the iteration sequence. Inside the lambda, you update the accumulator and return it.
Optionally, you can pass in the initial accumulator value (otherwise, the first element will be used).
const initialVal = 0;
let sum = [1, 2, 3].reduce((acc, next) => acc + next, initialVal);
// (0, 1) => 0 + 1 acc input is 'initialVal', returns the new acc value for the next iteration
// (1, 2) => 1 + 2 post execution, acc is 3
// (3, 3) => 3 + 3 post execution, acc is 6
// sum == 6 final result
You can do sums, products, and all kinds of other things. It's not hard to roll your own:
function reduce(collection, reducer, initialValue) {
let acc = initialValue;
for(const item of collection)
acc = reducer(acc, item);
return acc;
}
You can adapt this design to serve your purposes. I'm going to assume that it makes sense to pre-fetch
all the data beforehand, and store it in an in-memory array for later processing.
Your array might look something like this:
[
{ toy_count: 10, inspected_count: 5, cleaned_count: 4 },
{ toy_count: 20, inspected_count: 12, cleaned_count: 6 },
{ toy_count: 15, inspected_count: 15, cleaned_count: 10 },
...
]
Now, the problem you have is that related changes are spread-out; this makes it easy
to forget to make all the necessary edits. Let's bring all the related stuff together.
First, create a "config" object that keeps all the information about collecting data in one place.
Here's one way to do it:
const config = {
metrics: [
{
initial_value: 0,
prop: "collectors_count",
reducer: (acc, _) => acc + 1,
},
{
initial_value: 0,
prop: "toys_count",
reducer: (acc, details) => acc + details.toy_count,
},
{
initial_value: 0,
prop: "toys_inspected",
reducer: (acc, details) => acc + details.inspected_count,
},
{
initial_value: 0,
prop: "toys_cleaned",
reducer: (acc, details) => acc + details.cleaned_count,
},
],
derived_metrics: [
(storeData) => storeData.inspection_percent = (storeData.toys_inspected / storeData.toys_count) * 100,
(storeData) => storeData.cleaning_percent = (storeData.toys_cleaned / storeData.toys_count) * 100,
]
}
Now create a reduce function that can work with this config object. Compared to the vanilla reduce function from before, the parameter list is a bit different, and there are now two loops, but it's the same general idea:
function reduce(detailsList, storeData, config) {
for (const details of detailsList) {
for (const metricSpec of config.metrics) {
let acc = storeData[metricSpec.prop] || metricSpec.initial_value;
acc = metricSpec.reducer(acc, details);
storeData[metricSpec.prop] = acc;
}
}
}
And another function for the derived metrics:
function deriveMetrics(storeData, config) {
for (const metricCalculator of config.derived_metrics) {
metricCalculator(storeData);
}
}
Finally, you can use them like this:
let storeData = {};
reduce(detailsList, storeData, config);
deriveMetrics(storeData, config);
// at this point storeData will be filled in
Note that now you don't have to initialize any properties in storeData,
everything is driven by the config object. You also won't have to touch reduce or deriveMetrics, assuming your changes don't require you to fundamentally alter the accumulation logic.
If you for some reason cannot pre-fetch the data, and need reduce to be async and work on the fly, you can do that too:
async function reduce(collectors, storeData, config) {
for (const collector of collectors) {
const details = await db.getCollectorDetails(collector.id);
if (!details) continue;
for (const metricSpec of config.metrics) {
let acc = storeData[metricSpec.prop] || metricSpec.initial_value;
acc = metricSpec.reducer(acc, details);
storeData[metricSpec.prop] = acc;
}
}
}
// And then elsewhere
let storeData = {};
await reduce(detailsList, storeData, config);
deriveMetrics(storeData, config);