Chris
Chris

Reputation: 1475

Group javascript array by one property and then summarise by another

I'm trying to group an array by one property (year) and then create a nested array inside each year, listing the weeks with their total Total Distance and Max Avg Speed.

i.e.

[
    {
        "year": 2015,
        "weeks": [
            {week: 45, totalDistance: nnn, maxAvgSpeed: nnn},
            {week: 46, totalDistance: nnn, maxAvgSpeed: nnn},
            {week: 47, totalDistance: nnn, maxAvgSpeed: nnn}
            ...

This is what I have so far, but it says "activitiesByYear.forEach is not a function" even though activitiesByYear is an array as expected.

const activities = [
    {
        "id": 1,
        "actvityYear": 2015,
        "actvityWeek": 45,
        "name": "One",
        "avgSpeed": 1200,
        "distance": 2

    },
    {
        "id": 2,
        "actvityYear": 2015,
        "actvityWeek": 45,
        "name": "Two",
        "avgSpeed": 1403,
        "distance": 6
    },
    {
        "id": 3,
        "actvityYear": 2015,
        "actvityWeek": 46,
        "name": "Three",
        "avgSpeed": 1700,
        "distance": 7
    },
    {
        "id": 4,
        "actvityYear": 2015,
        "actvityWeek": 47,
        "name": "Four",
        "avgSpeed": 600,
        "distance": 12
    },
    {
        "id": 5,
        "actvityYear": 2015,
        "actvityWeek": 47,
        "name": "Five",
        "avgSpeed": 300,
        "distance": 4
    },
    {
        "id": 6,
        "actvityYear": 2016,
        "actvityWeek": 2,
        "name": "Six",
        "avgSpeed": 1800,
        "distance": 15
    }
]

function groupBy(objectArray, property) {
    return objectArray.reduce((acc, obj) => {
    const key = obj[property];
    if (!acc[key]) {
        acc[key] = [];
    }
    // Add object to list for given key's value
    acc[key].push(obj);
    return acc;
    }, {});
}

const activitiesByYear = groupBy(activities, 'actvityYear');
let years = [];
activitiesByYear.forEach(year => {
    let weeks = [];
    year.reduce(function(result, value) {
        const week = moment(value.actvityDate).week();
        if (!result[week]) {
            result[week] = { week: week, totalDistance: 0, maxAvgSpeed: value.avgSpeed };
            weeks.push(result[week])
        }
        result[week].totalDistance += value.distance;
        if (value.average_speed > result[week].maxAvgSpeed) result[week].maxAvgSpeed = value.average_speed;
        return result;
    }, {});
    years.push({"year": year, "weeks": weeks})
})

console.log(years)

Upvotes: 1

Views: 59

Answers (2)

Majed Badawi
Majed Badawi

Reputation: 28434

Use Object#entries to iterate over the year-items pairs.

Solution after some enhancements:

  • In groupBy helper, we use Array#reduce to iterate over the object array to group its items by the property param which is actvityYear
  • Using Object#entries, get the list of grouped year-items list from the above
  • Using Array#reduce, iterate over this list of pairs while updating a list of resulting years
    • In each iteration, we use Array#reduce to iterate over the current year's items while updating an object where the week is the key and the grouped week-items array is the value.
    • Use Object#values to get the resulting week-items of this year and push a new item to the resulting years array

const activities = [
  { "id": 1, "actvityYear": 2015, "actvityWeek": 45, "name": "One", "avgSpeed": 1200, "distance": 2 },
  { "id": 2, "actvityYear": 2015, "actvityWeek": 45, "name": "Two", "avgSpeed": 1403, "distance": 6 },
  { "id": 3, "actvityYear": 2015, "actvityWeek": 46, "name": "Three", "avgSpeed": 1700, "distance": 7 },
  { "id": 4, "actvityYear": 2015, "actvityWeek": 47, "name": "Four", "avgSpeed": 600, "distance": 12 },
  { "id": 5, "actvityYear": 2015, "actvityWeek": 47, "name": "Five", "avgSpeed": 300, "distance": 4 },
  { "id": 6, "actvityYear": 2016, "actvityWeek": 2, "name": "Six", "avgSpeed": 1800, "distance": 15 }
];

function groupBy(objectArray, property) {
  return objectArray.reduce((acc, obj) => {
    const key = obj[property];
    if (!acc[key]) { acc[key] = []; }
    acc[key].push(obj);
    return acc;
  }, {});
}

const activitiesByYear = groupBy(activities, 'actvityYear');
const years = Object.entries(activitiesByYear).reduce((years, [year, items]) => {
  const weeks = Object.values(
    items.reduce((weeks, { actvityWeek: week, distance, avgSpeed }) => {
      if (!weeks[week]) {
        weeks[week] = { week, totalDistance: 0, maxAvgSpeed: avgSpeed };
      }
      weeks[week].totalDistance += distance;
      if (avgSpeed > weeks[week].maxAvgSpeed) {
        weeks[week].maxAvgSpeed = avgSpeed;
      }
      return weeks;
    }, {})
  );
  years.push({ year, weeks });
  return years;
}, []);

console.log(years);

Upvotes: 1

Franco Torres
Franco Torres

Reputation: 1106

You get TypeError: activitiesByYear.forEach is not a function because activitiesByYear is a object with the following keys: [ '2015', '2016' ]. Because the groupBy returns an object. You can get the keys of the object with Object.keys().

For example:

const keys = Object.keys(activitiesByYear)
// [ '2015', '2016' ]

Upvotes: 0

Related Questions