Better use of transduction with objects list, instead of using 2 reducers

Viewed 95

Consider the following list of objects:

const blogs = [
    {
      title: "React patterns",
      author: "Michael Chan",
      url: "https://reactpatterns.com/",
      likes: 7,
    },
    {
      title: "Go To Statement Considered Harmful",
      author: "Edsger W. Dijkstra",
      likes: 5,
    },
    {
      title: "Canonical string reduction",
      author: "Edsger W. Dijkstra",
      likes: 12,
    },
    {
      title: "First class tests",
      author: "Robert C. Martin",
      likes: 10,
    },
    {
      title: "TDD harms architecture",
      author: "Robert C. Martin",
      likes: 0,
    },
    {
      title: "Type wars",
      author: "Robert C. Martin",
      likes: 2,
    }  
]

what I need to do, is write a function which returns the blog object, with the author that has the most blogs that he wrote, in the following format (which is also the expected result in this case):

{
    author: "Robert C. Martin",
    blogs: 3
}

This is the first solution I came up with, involving 2 reducers

const mostBlogs = blogs => {
    const formatted_blogs = blogs.reduce((acc, cur_blog)=>{
        if (!acc[cur_blog.author]){
            acc[cur_blog.author] = {
                author: cur_blog.author, blogs: 1
            }
        } else {
            acc[cur_blog.author].blogs++;
        }
        return acc;
    }, {});

    return Object.keys(formatted_blogs).reduce((acc, cur)=>{
        if (formatted_blogs[cur].blogs > (acc.blogs||0))
            return ( {...formatted_blogs[cur]} )
        return acc;
    }, {});
}

I was wondering how can I make it better, using only one reducer which would be more efficient and smarter. While I was typing out the question here, I actually came up with another solution using only 1 reducer. Would love to hear your opinions of it, could this be optimized? Is it smart/efficient enough? or can I do something different here which would be a better practise?

const mostBlogs = blogs => {
    const { most_blogs } = blogs.reduce((acc, cur_blog)=>{
        // if author doesn't exist in acc.all, add a new formatted obj
        if (!acc.all[cur_blog.author])
            acc.all[cur_blog.author] = { author: cur_blog.author, blogs: 1 };
        // if exists, simply increment the blogs prop count in acc.all
        else 
            acc.all[cur_blog.author].blogs++;

        // check if cur_blog blogs prop in acc.all is higher than
        // existing one in acc.most_blogs blogs prop, overwrite obj if so
        if (acc.all[cur_blog.author].blogs > (acc.most_blogs.blogs||0))
            acc.most_blogs = acc.all[cur_blog.author];

        return acc;
    }, {all: {}, most_blogs: {} });

    return most_blogs;
}
5 Answers

You could take a single loop with an array for top counts.

const
    blogs = [{ title: "React patterns", author: "Michael Chan", url: "https://reactpatterns.com/", likes: 7 }, { title: "Go To Statement Considered Harmful", author: "Edsger W. Dijkstra", likes: 5 }, { title: "Canonical string reduction", author: "Edsger W. Dijkstra", likes: 12 }, { title: "First class tests", author: "Robert C. Martin", likes: 10 }, { title: "TDD harms architecture", author: "Robert C. Martin", likes: 0 }, { title: "Type wars", author: "Robert C. Martin", likes: 2 }],
    mostBlogs = blogs => blogs
        .reduce((acc, { author })=> {
            var top = acc.top.length && acc.top[0].blogs;
            if (!acc.authors[author]) acc.authors[author] = { author, blogs: 0 };
            if (top < ++acc.authors[author].blogs) acc.top = [acc.authors[author]];
            if (top === acc.authors[author].blogs) acc.top.push(acc.authors[author]);
            return acc;
        }, { authors: {}, top: [] })
        .top;

console.log(mostBlogs(blogs));

This is what I would do.

const blogs = [{"title":"React patterns","author":"Michael Chan","url":"https://reactpatterns.com/","likes":7},{"title":"Go To Statement Considered Harmful","author":"Edsger W. Dijkstra","likes":5},{"title":"Canonical string reduction","author":"Edsger W. Dijkstra","likes":12},{"title":"First class tests","author":"Robert C. Martin","likes":10},{"title":"TDD harms architecture","author":"Robert C. Martin","likes":0},{"title":"Type wars","author":"Robert C. Martin","likes":2}];

const reducer = ({ mode, histogram }, { author }) => {
    const { [author]: frequency = 0 } = histogram;
    histogram[author] = frequency + 1;
    return { mode: histogram[author] > histogram[mode] ? author : mode, histogram };
};

const mostBlogs = blogs => {
    const initial = { mode: blogs[0].author, histogram: {} };
    const { mode, histogram: { [mode]: frequency } } = blogs.reduce(reducer, initial);
    return { author: mode, blogs: frequency };
};

console.log(mostBlogs(blogs));

It is possible to use reduce method to count title and then just sort them by titles:

const grouped = Object.values(blogs.reduce((a, {author, title}) => {
    a[author] = a[author] || {author, blogs: 0};
    a[author].blogs += 1;
    return a;
}, {}));


grouped.sort((a, b) => b.blogs - a.blogs)[0];

I was inspired by Nina Scholz answer(Thanks for showing such great way of using accumulator in reduce method). In my view, Nina's answer is the best and should be marked as an answer. Maybe it is a little bit refactored version of Nina's answer, however, in my view, the reply should be published. So this version has O(1) complexity:

const grouped = blogs.reduce((a, { author }) => {
    a[author] = a[author] || {author, blogs: 0};
    a[author].blogs += 1;
    if (a.topAuthor.hasOwnProperty('blogs') && a[author].blogs >  a.topAuthor.blogs)
        a.topAuthor = a[author];
    else if (Object.keys(a.topAuthor) == 0)
        a.topAuthor = a[author];
    return a;
}, {author: {}, topAuthor: {}});

console.log(grouped.topAuthor);

An example:

const blogs = [
    {
      title: "React patterns", author: "Michael Chan",
      url: "https://reactpatterns.com/", likes: 7,
    },
    {
      title: "Go To Statement Considered Harmful", author: "Edsger W. Dijkstra", 
      likes: 5,
    },
    {
      title: "Canonical string reduction", author: "Edsger W. Dijkstra",
      likes: 12,
    },
    {
      title: "First class tests", author: "Robert C. Martin",
      likes: 10,
    },
    {
      title: "TDD harms architecture", author: "Robert C. Martin", likes: 0,
    },
    {
      title: "Type wars", author: "Robert C. Martin", likes: 2,
    }
];

const grouped = blogs.reduce((a, { author }) => {
    a[author] = a[author] || {author, blogs: 0};
    a[author].blogs += 1;
    if (a.topAuthor.hasOwnProperty('blogs') && a[author].blogs >  a.topAuthor.blogs)
        a.topAuthor = a[author];
    else if (Object.keys(a.topAuthor) == 0)
        a.topAuthor = a[author];
    return a;
}, {author: {}, topAuthor: {}});

console.log(grouped.topAuthor);

If you're prepared to take on an extra 8KB library (disclaimer... it's mine), you could use blinq. It makes this sort of thing easy and legible:

import { blinq } from "blinq";
//...
const v = blinq(blogs)
  .groupBy(blog => blog.author)
  .select(grp => ({ author: grp.key, blogs: grp.count() }))
  .maxBy(o => o.blogs) //returns a sequence... there might be several maximums
  .first();

const {
  blinq
} = window.blinq
const blogs = [{
    title: "React patterns",
    author: "Michael Chan",
    url: "https://reactpatterns.com/",
    likes: 7
  },
  {
    title: "Go To Statement Considered Harmful",
    author: "Edsger W. Dijkstra",
    likes: 5
  },
  {
    title: "Canonical string reduction",
    author: "Edsger W. Dijkstra",
    likes: 12
  },
  {
    title: "First class tests",
    author: "Robert C. Martin",
    likes: 10
  },
  {
    title: "TDD harms architecture",
    author: "Robert C. Martin",
    likes: 0
  },
  {
    title: "Type wars",
    author: "Robert C. Martin",
    likes: 2
  }
];

const v = blinq(blogs)
  .groupBy(blog => blog.author)
  .select(grp => ({
    author: grp.key,
    blogs: grp.count()
  }))
  .maxBy(o => o.blogs) //returns a sequence... there might be several maximums
  .first();

console.log(v);
<script src="https://cdn.jsdelivr.net/npm/blinq"></script>

Here's a simple solution using vanilla JS.

const input = [{
    title: "React patterns",
    author: "Michael Chan",
    url: "https://reactpatterns.com/",
    likes: 7,
  },
  {
    title: "Go To Statement Considered Harmful",
    author: "Edsger W. Dijkstra",
    likes: 5,
  },
  {
    title: "Canonical string reduction",
    author: "Edsger W. Dijkstra",
    likes: 12,
  },
  {
    title: "First class tests",
    author: "Robert C. Martin",
    likes: 10,
  },
  {
    title: "TDD harms architecture",
    author: "Robert C. Martin",
    likes: 0,
  },
  {
    title: "Type wars",
    author: "Robert C. Martin",
    likes: 2,
  }
]

const topBloggers = input.reduce ((o, { author }) => 
   ({ ...o, [author]: o[author] ? o[author] + 1 : 1 }), {})

const [[author, blogs]] = Object.entries (topBloggers)
                         .sort (([,countA], [,countB]) => countB - countA)

const topBlogger = { author, blogs }

console.log (topBlogger)

Related