The FizzBuzz problem is a classic coding challenge often used in programming interviews. It is usually presented as follows:
Create a program that prints numbers from 1 to n. - If the number is divisible by 3, print ‘Fizz’; - If the number is divisible by 5, print ‘Buzz’; - If the number is divisible by both 3 and 5, print ‘FizzBuzz’.
This problem inspired me to work with Stream API using predefined fizz
and buzz
predicates. We aim to create various filters for a stream of integers using these predicates.
We will cover solutions for several basic tasks, while a more interesting and challenging one will be left for you to solve.
First, let’s define the fizz
and buzz
predicates:
IntPredicate fizz = i -> i % 3 == 0;
IntPredicate buzz = i -> i % 5 == 0;
We can create a stream of integers from 1 to 20 using the IntStream::rangeClosed
method:
var numbers = IntStream.rangeClosed(1, 20);
The goal for all the following tasks is to filter the integer stream by creating a fizzBuzz
predicate based on the defined fizz
and buzz
predicates.
We need to filter numbers that are exactly divisible by both 3 and 5. There are two ways to accomplish this. For instance, we can use the logical operator &&
:
IntPredicate fizzBuzz = i -> fizz.test(i) && buzz.test(i);
However, a more convenient and preferable approach would be to use the and
method from the IntPredicate
interface:
IntPredicate fizzBuzz = fizz.and(buzz);
assertThat(numbers.filter(fizzBuzz))
.as("Numbers divisible by three and five")
.containsExactly(15);
This solution is similar to the previous one but uses the or
method instead of and
:
IntPredicate fizzBuzz = fizz.or(buzz);
assertThat(numbers.filter(fizzBuzz))
.as("Numbers divisible by three or five")
.containsExactly(3, 5, 6, 9, 10, 12, 15, 18, 20);
In order to filter numbers that are not divisible by 3 or 5, we can use the IntStream::negate
method. The solution looks like this:
IntPredicate fizzBuzz = fizz.or(buzz).negate();
assertThat(numbers.filter(fizzBuzz))
.as("Numbers not divisible by three or five")
.containsExactly(1, 2, 4, 7, 8, 11, 13, 14, 16, 17, 19);
The next task is to filter numbers that are divisible by either 3 or 5, but not both simultaneously. While it is possible to use all the methods from the IntStream
interface, the optimal choice would be the exclusive OR (XOR):
IntPredicate fizzBuzz = i -> fizz.test(i) ^ buzz.test(i);
assertThat(numbers.filter(fizzBuzz))
.as("Numbers divisible by either three or five")
.containsExactly(3, 5, 6, 9, 10, 12, 18, 20);
This problem is very similar to the first one. However, our task is now to filter the numbers down to a divisible number by 3 and 5. We should stop processing numbers as soon as we come across such a number.
We define the predicate fizzBuzz precisely the same way as in the first problem. However, we use the takeWhile
method instead of filter
.
var fizzBuzz = fizz.and(buzz);
var result = numbers.takeWhile(fizzBuzz.negate());
assertThat(result)
.as("Numbers down to a number divisible by three and five")
.containsExactly(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14);
This is the opposite task to the previous one. We must throw out all the numbers up to the divisible number by three and five. All we need to solve this problem is to replace the takeWhile
method with the dropWhile
method.
var fizzBuzz = fizz.and(buzz);
var result = numbers.dropWhile(fizzBuzz.negate());
assertThat(result)
.as("Numbers after a number divisible by three and five")
.containsExactly(15, 16, 17, 18, 19, 20);
Programming languages like Ruby or Raku have a flip-flop operator, which is not available in Java by default. However, we can create it from scratch.
As a final task, I invite you to test your predicate-building skills. Create a method that takes two predicates, fizz
and buzz
as input and returns a flip-flop predicate. This predicate must filter down a series of numbers, starting with a number satisfying the predicate fizz
and ending with a number satisfying the predicate buzz
. The method signature looks like this:
IntPredicate flipFlop(IntPredicate fizz, IntPredicate buzz) {
// TODO: Define the predicate
}
To define the flip-flop predicate, we call the method as follows.
var fizzBuzz = flipFlop(fizz, buzz);
assertThat(numbers.filter(fizzBuzz))
.as("Numbers between numbers divisible by three and by five")
.containsExactly(3, 4, 5, 6, 7, 8, 9, 10, 12, 13, 14, 15, 18, 19, 20);
To demonstrate the operation of this predicate, I enclosed the number sequences in square brackets.
1, 2, [3, 4, 5], [6, 7, 8, 9, 10], 11, [12, 13, 14, 15], 16, 17, [18, 19, 20]
We get a completely different sequence if we swap the predicates fizz
and buzz
.
IntPredicate buzzFizz = flipFlop(buzz, fizz);
assertThat(numbers.filter(buzzFizz))
.as("Numbers between numbers divisible by five and by three")
.containsExactly(5, 6, 10, 11, 12, 15, 20);
Please note that in this case, the number 15 begins and ends the sequence, and the number 20 starts the series, but we don’t have its completion. At the same time, we ignore the number 3, which should end the sequence since our sequence has not yet begun.
1, 2, 3, 4, [5, 6], 7, 8, 9, [10, 11, 12], 13, 14, [15], 16, 17, 18, 19, [20
I hope you're willing to give the final problem a shot.
The topic of predicates is extensive, and covering all the issues in a tiny article is impossible. Nevertheless, I wanted to show how many possibilities we have, even limited to integer predicates and elementary initial data.