Search code examples
javascriptreport

Functional Javascript map/reduce in preparing report data


I am struggling to understand how to properly employ Array.reduce() to assemble summary/report data (i.e., grouped counts). I typically do this manually by looping over the data with forEach and building up the object, but I feel like chaining map and reduce has got to be a more elegant, functional way of doing it; I'm just failing to figure out how to do this correctly.

Here is where my experimentation has gotten me:

const articleData = [
    {newsSource: "AP", title: "Some article.", category: "World"},
    {newsSource: "BBC", title: "Some article.", category: "Politics"},
    {newsSource: "CBS", title: "Some article.", category: "Sports"},
    {newsSource: "CNN", title: "Some article.", category: "Finance"},
    {newsSource: "CNN", title: "Another article.", category: "Politics"},
    {newsSource: "NBC", title: "Some article.", category: "Politics"},
    {newsSource: "NBC", title: "Another article.", category: "Finance"},
    {newsSource: "Reuters", title: "Some article.", category: "World"},
    {newsSource: "Reuters", title: "Another article.", category: "Politics"},
    {newsSource: "Reuters", title: "Yet another article.", category: "Finance"}
];

// I want to set reportData to be the result of the chained map/reduce
// ...not declare it here and assemble it within the functions below
let reportData = {};

articleData.map((item, index) => {
    if (item.newsSource) return item.newsSource;
})
.reduce((acc, newsSource) => {

    if (!reportData[newsSource]) {
        reportData[newsSource] = 1;
    } else {
        reportData[newsSource] = reportData[newsSource] + 1;
    }

    return (acc, reportData);

});

console.log(reportData);
// Expected output:
// { AP: 1, BBC: 1, CBS: 1, CNN: 2, NBC: 2, Reuters: 3 }

There are a number of problems here. (Not the least of which is that it skips the first data element! I think I understand why, but I don't know how to fix it.) But most importantly, I want to understand how to construct my reduce function so I'm not altering reportData within it, rather I'm returning the properly structured data shown under "Expected output".

Reducing a flat array is clear to me, but I get confused once I'm dealing with a structure of any depth beyond that.


Solution

  • You can just use Array.reduce directly on articleData:

    reportData = articleData.reduce(function(c, a) {
      c[a.newsSource] = (c[a.newsSource] || 0) + 1;
      return c;
    }, {});
    
    console.log(reportData);
    

    Note that as @CertainPerformance pointed out in their comment, you need to pass an initial value to reduce otherwise the first value in the array is used as the initial value and not included in the iteration.

    const articleData = [{
        newsSource: "AP",
        title: "Some article.",
        category: "World"
      },
      {
        newsSource: "BBC",
        title: "Some article.",
        category: "Politics"
      },
      {
        newsSource: "CBS",
        title: "Some article.",
        category: "Sports"
      },
      {
        newsSource: "CNN",
        title: "Some article.",
        category: "Finance"
      },
      {
        newsSource: "CNN",
        title: "Another article.",
        category: "Politics"
      },
      {
        newsSource: "NBC",
        title: "Some article.",
        category: "Politics"
      },
      {
        newsSource: "NBC",
        title: "Another article.",
        category: "Finance"
      },
      {
        newsSource: "Reuters",
        title: "Some article.",
        category: "World"
      },
      {
        newsSource: "Reuters",
        title: "Another article.",
        category: "Politics"
      },
      {
        newsSource: "Reuters",
        title: "Yet another article.",
        category: "Finance"
      }
    ];
    
    reportData = articleData.reduce(function(c, a) {
      c[a.newsSource] = (c[a.newsSource] || 0) + 1;
      return c;
    }, {});
    
    console.log(reportData);