Functional Programming in Java, Second Edition: Functional Programming in Java, Second Edition: JUnit code improvements for Chapter 11, pages 185 ff “Refactoring Unbounded Loops”

The same remarks apply as for “Refactoring the Traditional for Loop”

  • No init()
  • Negative tests which apply for input less than 1900, resulting in exceptions.

but we cannot implement a common interface as the method parameters change. So we wrap the new class into a class that obeys the old interface, which agains llaows us to use the same test code to test both odl and new classes.

There is also a little tweak to the semantics of the case where 1900 is already rejected - instead of returning 0, we throw. Because there basically is no result in that case.

package chapter11;

import org.junit.jupiter.api.Test;

import java.time.Year;
import java.util.function.Predicate;
import java.util.stream.IntStream;

import static org.junit.jupiter.api.Assertions.*;

public class UnboundedLoopTest {

    interface Continue {

        boolean check(int year);

    }

    interface WithContinue {

        int countFrom1900(final Continue shouldContinue);

    }

    static class LeapYearsUnboundedBefore implements WithContinue {

        public int countFrom1900(final Continue shouldContinue) {
            if (!shouldContinue.check(1900)) {
                throw new IllegalArgumentException("Cannot 'continue' right at the start!'");
            }
            int count = 0;
            for (int year = 1900; ; year += 4) {
                if (!shouldContinue.check(year)) {
                    break;
                }
                if (Year.isLeap(year)) {
                    count++;
                }
            }
            return count;
        }
    }

    // Does NOT implement WithContinue!!

    static class LeapYearsUnboundedAfter {

        public int countFrom1900(final Predicate<Integer> shouldContinue) {
            if (!shouldContinue.test(1900)) {
                throw new IllegalArgumentException("Cannot 'continue' right at the start!'");
            }
            return (int) IntStream.iterate(1900, year -> year + 4)
                    .takeWhile(shouldContinue::test)
                    .filter(Year::isLeap)
                    .count();
        }
    }

    static class LeapYearsUnboundedAfterWrapped implements WithContinue {

        private final LeapYearsUnboundedAfter ly;

        public LeapYearsUnboundedAfterWrapped(LeapYearsUnboundedAfter ly) {
            this.ly = ly;
        }

        public int countFrom1900(final Continue shouldContinue) {
            return ly.countFrom1900(year -> shouldContinue.check(year));
        }

    }

    // One should maybe have a separate method to test the throws
    
    private static void commonLeapYearsTests(final WithContinue withContinue) {
        assertAll(
                () -> assertEquals(0, withContinue.countFrom1900(year -> year <= 1900)),
                () -> assertEquals(25, withContinue.countFrom1900(year -> year <= 2000)),
                () -> assertEquals(27, withContinue.countFrom1900(year -> year <= 2010)),
                () -> assertEquals(31, withContinue.countFrom1900(year -> year <= 2025)),
                () -> assertEquals(49, withContinue.countFrom1900(year -> year <= 2100)),
                () -> assertEquals(267, withContinue.countFrom1900(year -> year <= 3000)),
                () -> assertThrows(IllegalArgumentException.class, () ->
                        withContinue.countFrom1900(year -> year < 1800)
                ),
                () -> assertThrows(IllegalArgumentException.class, () ->
                        withContinue.countFrom1900(year -> year < 1900)
                )
        );
    }

    @Test
    void leapYearCountBefore() {
        commonLeapYearsTests(new LeapYearsUnboundedBefore());
    }

    @Test
    void leapYearCountAfter() {
        commonLeapYearsTests(new LeapYearsUnboundedAfterWrapped(new LeapYearsUnboundedAfter()));
    }

}