2

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;
}
Nina Scholz
  • 376,160
  • 25
  • 347
  • 392
spider monkey
  • 304
  • 1
  • 4
  • 10

5 Answers5

2

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));
Nina Scholz
  • 376,160
  • 25
  • 347
  • 392
  • Nice, looks kinda similar to my 2nd solution, but probably cleaner. Is there any advantage of using an array to store the author with highest blogs (top) over an object? – spider monkey Feb 25 '20 at 15:10
  • it keeps the authors with the same top amount, instead of only one (depending on the implementation the first or last, but not all). – Nina Scholz Feb 25 '20 at 15:12
1

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));
Aadit M Shah
  • 72,912
  • 30
  • 168
  • 299
0

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);
StepUp
  • 36,391
  • 15
  • 88
  • 148
  • This returns an array of titles for each author instead of blogs property with count, but no problem as this is easily changeable. Regarding calling sort, this basically means the list is traversed twice right? once with the reduce, and one with sort? If so, would this not affect performance? I did like the reducer, looks very clean – spider monkey Feb 25 '20 at 15:13
  • yeah, you are right! The complexity of this solution is `O(n) + O(n log n)`. `reduce` has `O(n)`, `sort` can use various different sorting algorithms so regardless of which algorithm is used, it is probably better to think `O(n log n)`. So Nina Scholz's soution is better. – StepUp Feb 25 '20 at 15:31
0

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>
spender
  • 117,338
  • 33
  • 229
  • 351
  • Looks good, I liked it. I'll take a look at that.. Thanks for sharing – spider monkey Feb 25 '20 at 15:11
  • this violates presumably the request of op to limit the count of reducing ... btw there a different implementations of LINQ in javascript. a custom of for example an SQL like approach is simple: https://stackoverflow.com/questions/56901732/js-filter-array-of-objects-by-max-value-per-category/56902694#56902694 – Nina Scholz Feb 25 '20 at 15:16
0

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)
Matías Fidemraizer
  • 63,804
  • 18
  • 124
  • 206