0

I am trying to loop through array of Objects and calculate the average of a nested Object containing several different keys. This is the start array:

[{
  course: "math",
  id: 4,
  values: {
    2017: 8,
    2018: 9
  }
}, {
  course: "math",
  id: 4,
  values: {
    2017: 5,
    2019: 7
  }
}]

This is my goal:

{2017:6.5,2018:9,2019:7}

Now it returns correct for 2017 but NaN for 2018 and 2019. If anyone have better way of solving this that doesn't require so much please provide to.

This is what I have tried so far. I have been searching a lot but not really found anything I can use.


const testObject = [{
    id: 4,
    course: "math",
    values: {
      2017: 8,
      2018: 9
    }
  },
  {
    id: 5,
    course: "English",
    values: {
      2017: 8,
      2018: 9
    }
  },
  {
    id: 4,
    course: "math",
    values: {
      2017: 5,
      2019: 7
    }
  },
  {
    id: 4,
    course: "english",
    values: {
      2017: 5,
      2019: 7
    }
  },
]
//First I filter out the id 4 and course Math
const mathid1 = testObject.filter((e) => e.id === 4 && e.course === "math");
//I than find all the different years
const ArrayOfAllYears = []
mathid1.map((element) => {
  ArrayOfAllYears.push(Object.keys(element.values));
})
//I here find all the different years
const withDuplicates = ArrayOfAllYears.reduce(function(arrayOne, arrayTwo) {
  return arrayOne.concat(arrayTwo);
}, []);

const withoutDuplicates = Array.from(new Set(withDuplicates));
//Here I just create the calculate average function
const Result = {}
const calculateAverage = (array) => {
  const sum = array.reduce((a, b) => a + b);
  return sum / array.length;
};
const newObj = {}
withoutDuplicates.map((year) => {
  let reformattedArray = mathid1.map(obj => {
    if (obj["values"][year]) {
      return obj["values"][year]
    }
  })
  newObj[year] = calculateAverage(reformattedArray)
})
console.log(newObj)
// I want to calculate the average of the mathid1 values and return it on a Object like {2017:..,2018..}

0

4 Answers 4

2

There are two simple steps to the problem.

First, you need to reduce the array to an object with years and values:

// this outputs
// { 2017: [8, 5], 2018: [9], 2019: [7] }
function byYear(array) {
    // take each item of an array
    return array.reduce((acc, data) => {
        // take the values of that item
        Object.entries(data.values).forEach(([year, value]) => {
            // and map all the values to years
            acc[year] = acc[year] || []
            acc[year].push(value)
        })
        return acc
    }, {})
}

The second step is just taking averages:

function average(object) {
    const averages = {}
    for (let key in object) {
        averages[key] = object[key].reduce((sum, value) => sum + value) / object[key].length 
    }
    return averages
}

And now you put them together:

average(byYear(input))

In here, the input is the filtered array. As a whole snippet:

function byYear(array) {
    return array.reduce((acc, data) => {
        Object.entries(data.values).forEach(([year, value]) => {
            acc[year] = acc[year] || []
            acc[year].push(value)
        })
        return acc
    }, {})
}

function average(object) {
    const averages = {}
    for (let key in object) {
        averages[key] = object[key].reduce((sum, value) => sum + value) / object[key].length 
    }
    return averages
}

const output = average(byYear([{
  course: "math",
  id: 4,
  values: {
    2017: 8,
    2018: 9
  }
}, {
  course: "math",
  id: 4,
  values: {
    2017: 5,
    2019: 7
  }
}]))

console.log(output)

Sign up to request clarification or add additional context in comments.

3 Comments

Is it possible to add filter to this so lets say 2017===0, than don't have it in calculation?
I could of course just add if(value!==0){acc[year].push(value)} but the problem with that is that whole code will crash if all of one year are 0.
If you want to add that filter, you need to add it even before the acc[year] = acc[year] || []. The problem is that if you create the array as empty but never add any values, it cannot calculate the average of empty array, if I understand correctly.
1

The problem with your current code lies in how you build the reformattedArray variable. First, notice that your map function implicitly returns undefined whenever that year is missing from the current object:

let reformattedArray = mathid1.map(obj => {
  if (obj["values"][year]) {
    return obj["values"][year]
  }

  // There is an implicit return undefined, right here...
})

When you use the array .map method, every item of the array will be replaced by the return value of the map function. In the case that the year is not present, it will not go into the if block, and so it implicitly returns undefined upon reaching the end of the function.

So, ultimately all you have to do is remove the undefined entries from this array, and your code will work as-is.


One way to do that is to just use .filter(Boolean) on the array, which removes any falsey entries (which undefined is). Eg:

let reformattedArray = mathid1.map(obj => {
  /* code here */
}).filter(Boolean); // Note the filter here...

Here is your snippet with that modification:

const testObject = [{
    id: 4,
    course: "math",
    values: {
      2017: 8,
      2018: 9
    }
  },
  {
    id: 5,
    course: "English",
    values: {
      2017: 8,
      2018: 9
    }
  },
  {
    id: 4,
    course: "math",
    values: {
      2017: 5,
      2019: 7
    }
  },
  {
    id: 4,
    course: "english",
    values: {
      2017: 5,
      2019: 7
    }
  },
]
//First I filter out the id 4 and course Math
const mathid1 = testObject.filter((e) => e.id === 4 && e.course === "math");
//I than find all the different years
const ArrayOfAllYears = []
mathid1.map((element) => {
  ArrayOfAllYears.push(Object.keys(element.values));
})
//I here find all the different years
const withDuplicates = ArrayOfAllYears.reduce(function(arrayOne, arrayTwo) {
  return arrayOne.concat(arrayTwo);
}, []);

const withoutDuplicates = Array.from(new Set(withDuplicates));
//Here I just create the calculate average function
const Result = {}
const calculateAverage = (array) => {
  const sum = array.reduce((a, b) => a + b);
  return sum / array.length;
};
const newObj = {}
withoutDuplicates.map((year) => {
  let reformattedArray = mathid1.map(obj => {
    if (obj["values"][year]) {
      return obj["values"][year]
    }
  }).filter(Boolean)
  newObj[year] = calculateAverage(reformattedArray)
})
console.log(newObj)
// I want to calculate the average of the mathid1 values and return it on a Object like {2017:..,2018..}

Comments

0
  1. Group items by year.
  2. Calculate average.
const items=[{
  course: "math",
  id: 4,
  values: {
    2017: 8,
    2018: 9
  }
}, {
  course: "math",
  id: 4,
  values: {
    2017: 5,
    2019: 7
  }
}]

const groupedValues=items.reduce((groupedValues,item)=>{
  Object.entries(item.values).forEach(([year,value])=>{
      if(groupedValues[year]){
        groupedValues[year]={value:groupedValues[year].value+value,items:groupedValues[year].items+1};
      } else {
        groupedValues[year]={value,items:1};
      }
  });
  return groupedValues;
},{})

console.log(groupedValues);

const result = Object.entries(groupedValues).reduce((result,item)=>{
  result[item[0]]=item[1].value/item[1].items;
  return result;
},{})

console.log(result);

Comments

0

I would recommend extracting the years information into a map:

/** @type {Map<string, number[]} */
const years = new Map();

testObject.forEach((obj) => {
  Object.keys(obj.values).forEach((key) => {
    if (!years.has(key)) years.set(key, []);
    years.set(key, [...years.get(key), obj.values[key]]);
  });
});

Then you can simply loop over the map and create the resulting object:

const result = {};
years.forEach((values, key) => {
  Object.defineProperty(result, key, {
    value: values.reduce((acc, val) => acc + val) / values.length,
    enumerable: true,
  });
});

console.log(result);

It should output:

{ '2017': 6.5, '2018': 9, '2019': 7 }

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.