Проблема FizzBuzz - это классическая задача, которая часто встречается на собеседованиях для программистов. Обычно она формулируется так:
Создайте программу, которая выводит числа от 1 до n.
- Если число делится на 3, выведите 'Fizz';
- если число делится на 5, выведите 'Buzz';
- если число делится и на 3 и на 5, выведите 'FizzBuzz'.
Это упражнение подтолкнуло меня к идее поработать над задачами с использованием Stream API, используя заранее определенные предикаты fizz
и buzz
. Наша цель – создать различные фильтры для потока чисел, используя эти предикаты.
Мы разберем решения четырех простых задач, а более сложная и интересная останется вам.
Сначала определим предикаты fizz
и buzz
:
IntPredicate fizz = i -> i % 3 == 0;
IntPredicate buzz = i -> i % 5 == 0;
Поток целых чисел от 1 до 20 создадим с помощью метода IntStream::rangeClosed
:
var numbers = IntStream.rangeClosed(1, 20);
Цель всех следующих задач – отфильтровать поток чисел, создав предикат fizzBuzz
на основе уже определённых предикатов.
Задача №1. Фильтрация чисел, кратных 3 и 5
Необходимо отфильтровать числа, которые делятся нацело на 3 и 5.
В этом случае есть два способа решить задачу. Например, мы можем использовать логический оператор &&
:
IntPredicate fizzBuzz = i -> fizz.test(i) && buzz.test(i);
Но более предпочтительный и удобный вариант – использовать метод and
, принадлежащий интерфейсу IntPredicate
:
IntPredicate fizzBuzz = fizz.and(buzz);
assertThat(numbers.filter(fizzBuzz))
.as("Numbers divisible by three and five")
.containsExactly(15);
Задача №2. Фильтрация чисел, кратных 3 или 5
Здесь решение аналогично предыдущей задаче, но мы используем метод or
вместо 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);
Задача №3. Фильтрация чисел, не кратных 3 и 5
Для того чтобы отфильтровать числа, которые не делятся ни на 3, ни на 5, можно использовать метод IntStream::negate
. Решение выглядит следующим образом:
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);
Задача №4. Фильтрация чисел, кратных 3 или 5, но не обоим
Следующая задача – отфильтровать числа, которые делятся на 3 или на 5, но не делятся нацело на оба числа одновременно. Здесь можно использовать все методы интерфейса IntStream
, однако лучший вариант – исключающее ИЛИ:
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);
Бонусная задача
В заключение предлагаю вам проверить ваш навык составления предикатов. Составьте предикат для чисел внутри последовательностей FizzBuzz исключая границы. Последовательность начинается числом, удовлетворяющим предикату fizz
, и заканчивается числом, удовлетворяющим предикату buzz
.
Например, для последовательности чисел от 1 до 20, результат должен выглядеть следующим образом:
1, 2, (3, 4, 5), (6, 7, 8, 9, 10), 11, (12, 13, 14, 15), 16, 17, (18, 19, 20)
Задача выбрать числа внутри скобок не включая границы.
Для удобства можете использовать следующий код для теста:
@Test
@DisplayName("Filter out numbers between integers divisible by three and by five.")
void numbers_between_integers_divisible_by_three_and_by_five() {
var numbers = IntStream.rangeClosed(1, 20);
// TODO: Define the predicate
IntPredicate fizzBuzz = i -> false;
assertThat(numbers.filter(fizzBuzz))
.as("Numbers between interegers divisible by three and by five")
.containsExactly(4, 7, 8, 9, 13, 14, 19);
}
Надеюсь, что последняя задачка более интересная чем предыдущие.
maxzh83
(6, 7, 8, 9, 10)
Тут и 6 и 9 удовлетворяют предикату fizz, при этом 6 не входит, а 9 входит. Почему так?
Rabestro Автор
Последовательность началась и теперь мы проверяем только "закрывающий" предикат.
В качестве альтернативы, вместо чисел можно использовать строки. Пример:
Rabestro Автор
.containsExactly("""
System.out.println("Hello, World!");""");
valery1707
В этом кейсе работает такой вариант:
Как сделать это без внешнего хранения флага нахождения внутри последовательности у меня пока нет идей.
В случае с последовательностью цифр можно как-то учитывать знание о том что было раньше и что будет потом, но в случае сырого текста у нас такого знания просто нет.
Rabestro Автор
Rabestro Автор
Для чисел аналогичный подход
maxzh83
Если это правильный ответ, то я разочарован.
Rabestro Автор
Можете предложить своё решение. С интересом посмотрим.
maxzh83
Дело не в решении, а в задаче скорее. Смотрите, вы всю статью показываете как, комбинируя предикаты, добиться результатов из примеров. В последней задаче ожидаешь какого-то аналогичного решения. Т.е. комбинации логических операторов. Начинаешь думать, понимая, что так не получится. Периодически отбрасываешь вариант с состоянием, как нечто чужеродное в данном контексте. А потом выясняешь, что так и надо было. Вот от этого разочарование.
Ну и в остальном, предикаты с состоянием это так себе идея в реальной жизни. Даже без учета того, что кто-то может засунуть их в parallelStream
valery1707
Действительно, состояние можно затащить внутрь и сэкономить на
AtomicBoolean
.valery1707
Последовательность начатая
6
-кой во время9
-ки ещё не завершилась - она завершится только на10
-ке и9
-ка окажется внутри последовательности[6, 10]
.