1

I have a list of natural numbers. How can I subgroup all the numbers divisible by 3, 5 and both, using Java Stream API?

For example:

ArrayList<Integer> list = new ArrayList<>();
list.add(24);
list.add(25);
list.add(45);
list.add(30);
list.add(3);
list.add(20);
list.add(5);

I want l3 = [3,24] , l5 = [5,20,25], l35 = [45,30]

Also, I don't want to call groupingBy() three times on the list as the list is really huge.

AmrDeveloper
  • 3,826
  • 1
  • 21
  • 30
kurious
  • 59
  • 8
  • I have looked into all those questions you are referring to, well before posting my question. None of them answers my question. – kurious Dec 29 '21 at 01:07
  • 2
    `list.collect(Collectors.partitioningBy(i -> i % 3 == 0, Collectors.partitioningBy(i -> i % 5 == 0)))` – shmosel Dec 29 '21 at 01:17
  • apologies -- I was wrong and @shmosel was right – Hovercraft Full Of Eels Dec 29 '21 at 02:04
  • Simplified solution with reference to @Shawn's answer Map> groups = intList.stream().collect(Collectors.groupingBy(n-> n%3 == 0 ? (n%5 == 0 ? "l35" : "l3") : n%5 == 0 ? "l5" : "NONE")); – Prasath Dec 29 '21 at 11:09

2 Answers2

4

I'd use a helper enum to describe the different possible classifications of the numbers, combined with Collectors.groupingBy():

import java.util.*;
import java.util.stream.*;

public class Demo {

    private enum Fizzbuzz {
        DIV_BY_THREE, DIV_BY_FIVE, DIV_BY_BOTH, DIV_BY_NEITHER;

        static public Fizzbuzz classify(int i) {
            if (i % 3 == 0) {
                return i % 5 == 0 ? DIV_BY_BOTH : DIV_BY_THREE;
            } else if (i % 5 == 0) {
                return DIV_BY_FIVE;
            } else {
                return DIV_BY_NEITHER;
            }
        }
    }

    public static void main(String[] args) {
        List<Integer> list = List.of(24, 25, 45, 30, 3, 20, 5);
        Map<Fizzbuzz, List<Integer>> groups =
            list.stream().collect(Collectors.groupingBy(Fizzbuzz::classify));
        System.out.println(groups);
    }
}

outputs

{DIV_BY_THREE=[24, 3], DIV_BY_FIVE=[25, 20, 5], DIV_BY_BOTH=[45, 30]}
Shawn
  • 47,241
  • 3
  • 26
  • 60
  • Good one. Then instead of creating enum we can go with this Function classifier = (n) -> (n % 3 == 0) ? ((n % 5 == 0) ? "l35" : "l3") : (n % 5 == 0) ? "l5" : "NONE"; which will return Map>. – Prasath Dec 29 '21 at 09:27
  • @Prasath That works too, but I like the stronger type safety of using an enum. – Shawn Dec 29 '21 at 14:26
  • The _type-safety_ and explicit _semantics_ encapsulating the classification inside an enum makes the code more robust and expressive. – hc_dev Jan 01 '22 at 13:31
1

Reduce to a map

Use streams reduction. Here we can use the reduce(identityFunc, accumulationFunc, combiningFunc) variant:

The initial map contains 4 String keys mapped to empty List<Integer>:

  1. "I" for all unmapped (not-fizz-buzz-able) integers
  2. "I3" for divisible by 3
  3. "I5" for divisible by 5
  4. "I35" for divisible by 3 and 5

See runnable demo on IDEone:

Map<String, List<Integer>> fizzBuzz = new HashMap<>(3);
fizzBuzz.put("I", new ArrayList());
fizzBuzz.put("I3", new ArrayList());
fizzBuzz.put("I5", new ArrayList());
fizzBuzz.put("I35", new ArrayList());

list.stream().reduce(
       fizzBuzz,
       (map, e) -> {
          String key = "I"; // default key
          if (e % 3 == 0) key = "I3";
          if (e % 5 == 0) key = "I5";
          if (e % (3*5) == 0) key = "I35";
          map.get(key).add(e);
          return map;
        },
        (m, m2) -> {
            m.putAll(m2);
            return m;
        }
    );
        
System.out.println(fizzBuzz);

Prints the expected:

{I=[], I3=[24, 3], I35=[45, 30], I5=[25, 20, 5]}

See also: Java stream reduce to map

hc_dev
  • 8,389
  • 1
  • 26
  • 38