Проблема 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, 789, 10), 11, (12, 1314, 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);
}

Надеюсь, что последняя задачка более интересная чем предыдущие.

Комментарии (11)


  1. maxzh83
    12.09.2023 08:47
    +1

    Последовательность начинается числом, удовлетворяющим предикату fizz

    (6, 789, 10)

    Тут и 6 и 9 удовлетворяют предикату fizz, при этом 6 не входит, а 9 входит. Почему так?


    1. Rabestro Автор
      12.09.2023 08:47

      Последовательность началась и теперь мы проверяем только "закрывающий" предикат.

      В качестве альтернативы, вместо чисел можно использовать строки. Пример:

      @Test
      @DisplayName("Filter out lines between [```java] and [```].")
      void extract_all_java_code_snippets_from_markdown_document() {
          var markdown = """
                  # Hello, World!
                  The following code snippet is written in Java:
                  ```java
                  System.out.println("Hello, World!");
                  ```
                  The following code snippet is written in Kotlin:
                  ```kotlin
                  println("Hello, World!")
                  ```
                  """;
          
          Predicate<String> fizz = "```java"::equals;
          Predicate<String> buzz = "```"::equals;
          
          // TODO: Define the predicate
          Predicate<String> fizzBuzz = i -> false;
      
          assertThat(markdown.lines().filter(fizzBuzz))
                  .as("Java code snippets")
                  .containsExactly("""
                          System.out.println("Hello, World!");
                          """);
      }


      1. Rabestro Автор
        12.09.2023 08:47

        .containsExactly("""
        System.out.println("Hello, World!");""");


      1. valery1707
        12.09.2023 08:47

        В этом кейсе работает такой вариант:

        var started = new AtomicBoolean();
        Predicate<String> fizzBuzz = i -> {
            if (started.get()) {
                if (buzz.test(i)) {
                    started.set(false);
                    return false;
                } else {
                    return true;
                }
            } else {
                started.set(fizz.test(i));
                return false;
            }
        };
        

        Как сделать это без внешнего хранения флага нахождения внутри последовательности у меня пока нет идей.
        В случае с последовательностью цифр можно как-то учитывать знание о том что было раньше и что будет потом, но в случае сырого текста у нас такого знания просто нет.


        1. Rabestro Автор
          12.09.2023 08:47

          var fizzBuzz = new Predicate<String> {
              boolean state;
            
              @Override 
              public boolean test() {
                ...
              }
          };


          1. Rabestro Автор
            12.09.2023 08:47

            var fizzBuzz = new IntPredicate {
                boolean state;
              
                @Override 
                public boolean test() {
                  ...
                }
            };

            Для чисел аналогичный подход


          1. maxzh83
            12.09.2023 08:47

            Если это правильный ответ, то я разочарован.


            1. Rabestro Автор
              12.09.2023 08:47

              Можете предложить своё решение. С интересом посмотрим.


              1. maxzh83
                12.09.2023 08:47

                Дело не в решении, а в задаче скорее. Смотрите, вы всю статью показываете как, комбинируя предикаты, добиться результатов из примеров. В последней задаче ожидаешь какого-то аналогичного решения. Т.е. комбинации логических операторов. Начинаешь думать, понимая, что так не получится. Периодически отбрасываешь вариант с состоянием, как нечто чужеродное в данном контексте. А потом выясняешь, что так и надо было. Вот от этого разочарование.

                Ну и в остальном, предикаты с состоянием это так себе идея в реальной жизни. Даже без учета того, что кто-то может засунуть их в parallelStream


          1. valery1707
            12.09.2023 08:47

            Действительно, состояние можно затащить внутрь и сэкономить на AtomicBoolean.


    1. valery1707
      12.09.2023 08:47
      +1

      Последовательность начинается числом, удовлетворяющим предикату fizz, и заканчивается числом, удовлетворяющим предикату buzz.

      Последовательность начатая 6-кой во время 9-ки ещё не завершилась - она завершится только на 10-ке и 9-ка окажется внутри последовательности [6, 10].