DEV Community

loading...

Sorting an array into groups with reduce

Jacob Paris
Sales funnels and B2B SaaS for the mortgage industry, almond latte fanatic, LA @eggheadio , formerly ToolStache
・2 min read

I help run the discord server Devcord and spend much of my time helping novice programmers with technical difficulties. Starting now, I'll be formalizing any help I give there into short blog posts here so the information isn't buried under a sea of conversations.

Here I had a user with a list of email addresses to be sorted by domain.

const emails = [
  "alice@gmail.com"
  "bob@gmail.com"
  "carol@yahoo.ca"
  "doug@hotmail.com"
  "ellie@protonmail.com"
];
Enter fullscreen mode Exit fullscreen mode

While there are many ways to tackle this problem, my preferred is the functional model using Array.prototype.reduce

const sortedEmails = emails.reduce((groups, email) => {
  const splitString = email.split('@');
  const account = splitString[0];
  const domain = splitString[1];

  if(!groups[domain]) groups[domain] = [];

  groups[domain].push(account);
  return groups;
}, {});
Enter fullscreen mode Exit fullscreen mode

The reduce function iterates each element of the array and passes the return object to the next iteration. The accumulator (named groups here) is set initially as a {} empty object.

For each email, we break it into variables for each the account and the domain. If our current domain isn't already one of our groups, initialize it as an empty array.

Then add the new account name to the group and return groups to pass it into the next iteration.

{
  "gmail.com": ["alice", "bob"],
  "yahoo.ca": ["carol"],
  "hotmail.com": ["doug"],
  "protonmail.com": ["ellie"]
}
Enter fullscreen mode Exit fullscreen mode

It should be noted that this code isn't completely suitable for production environments. While almost every email address has only one @ symbol and this will work for all of those, there are valid email addresses that have multiple. Parsing the entire specification of valid email addresses is outside the scope of this article.

Discussion (10)

Collapse
d521bb85 profile image
Vladimir Ivanenko

Hi! Thank you for the article. Please, let me share my point.
I'm not feeling very confident with functional-like practices, because I started applying them not a long ago. But if I got it right, a callback function we pass to .reduce method should be pure and mutating of input arguments it's not a good practice.

I would love to suggest the following implementation:

function groupEmails(addresses) {
  return addresses.reduce(
    (result, addr) => {
      const [name, host] = addr.split('@');
      const value = host in result ? result[host].concat(name) : [name];

      return {
        ...result,
        [host]: value
      };
    },
    {}
  );
}

Thank you. Sorry if I got you wrong.

Collapse
jacobmparis profile image
Jacob Paris Author

The accumulator used in a reduce function is a temporary variable created by the reduce function. The only scope that has access to it is the current iteration of reduce, so creating a new variable each time is unnecessary.

If we were to mutate the addresses argument, we would run into the problems you're hinting at so that indeed is bad practice, but none of those issues exist with the reduce-accumulator.

Collapse
nancysavchenko profile image
nancysavchenko

how do you combine [name] with [host] to become email?

Collapse
cubiclebuddha profile image
Cubicle Buddha

Great article. It’s a great example of how to utilize .reduce but it also reminds me why I’d rather see a helper library like lodash or immutableJS since it’s shorter, more readable, and has already been tested.

But seriously awesome job of proving the value of reduce (especially since it’s basically the cornerstone of functional programming) :)

Collapse
jacobmparis profile image
Jacob Paris Author • Edited

I'm really not sure how lodash or immutable are shorter or more readable. Immutable just has its own wrapper method for its own classes, and Lodash's implementation is almost identical to JS.

// summing with javascript
array.reduce((acc, item) => acc + item, 0);

// summing with lodash
_.reduce(array, (acc, item) => acc + item, 0);

It's better to use the native javascript method because the performance of that will improve over time with improvements to the runtime engines and development of javascript as a whole. Lodash's performance depends on the version of the library you are using at the time.

Collapse
cubiclebuddha profile image
Cubicle Buddha • Edited

Forgive me for not being more clear in my first comment. I didn't mean that you should replace native .reduce with lodash's .reduce. I meant that .groupBy is a drop in replacement for your custom reduce.

const sortedEmails = _.groupBy(emails, anEmail => {
    const emailProvider = anEmail.split('@')[1];
    return emailProvider;
})

That's 3 lines instead of 8 and (to me) it's more scan-able/readable since I see "groupBy" and I immediately know that we're categorizing by email provider.

Btw, I often use native .reduce. But when I can reach out to a method name that more clearly communicates my goals, then I do that since it's more in line with self-documenting code. Again, those are just my opinions. What are your thoughts?

Thread Thread
jacobmparis profile image
Jacob Paris Author

Oh yes -- I understand now.

I'm a strong believer in refactoring code for reusability, so if I needed to group more arrays by one of their fields I would absolutely adopt a specialized and well-named function that does that. In this case, Lodash is a perfectly acceptable alternative.

Collapse
wolverineks profile image
Kevin Sullivan

This looks like a good use case for array destructuring.

const [account, domain] = email.split('@')

Nice article.

Collapse
jacobmparis profile image
Jacob Paris Author

Yes you're right — that's a better way to write that