Day 1 is normally a very gentle introduction to the Advent of Code problems, but this year’s opener came with one “gotcha!”. The problem, on the surface, was very simple: find the first and last single digit in a string, and concatenate them into a two-digit number. Do that for every line of the input and sum the results.

Just to make things a little bit more difficult I made a bet with a colleague that I could solve each part with a single line. In this case, the definition of “a single line” being one semi-colon (ignoring all the Java boilerplate; import statements, etc).

Part I posed no problem, even constrained to a single line. The solution ended up looking something like this:

return input.stream()
    .map(line -> Pattern.compile("([0-9])").matcher(line).results()
            .map(MatchResult::group)
            .collect(Collectors.toList()))
    .map(matches -> matches.getFirst() + matches.getLast())
    .mapToInt(Integer::parseInt)
    .sum();

I quickly moved on to part II: consider not only numeric digits, but also digits represented by words. This sounded straightforward enough and with a few tweaks to part I, I ended up with something along the lines of:

return input.stream()
    .map(line -> Pattern.compile("([0-9]|one|two|three|four|five|six|seven|eight|nine|zero)").matcher(line).results()
            .map(MatchResult::group)
            .map(mr -> mr.length() == 1 ? mr : List.of("zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine").indexOf(mr))
            .map(String::valueOf)
            .collect(Collectors.toList()))
    .map(matches -> matches.getFirst() + matches.getLast())
    .mapToInt(Integer::parseInt)
    .sum();

Not the most efficient way of doing it, but sticking within the “one line” challenge.

All-in-all it probably took about ten minutes and I had a passing test for part II… … at least, for the part II example.

Although my code was calculating the example correctly, it wasn’t working for the live data.

Enter the “gotcha”: some input strings contained overlapping digit words; e.g. twone contains both two and one. Although this pattern did show up in the example, it had never done so in a way that would affect the overall result. This wasn’t true for the actual input data.

Fundamentally, the fix is easy - but less when trying to stick within the one line limit. After trying a few different tricks with the Java Stream API, I finally settled on a fairly horrible solution using Stream#iterate:

return input.stream()
    .map(line -> Stream.iterate(
                Pattern.compile("([0-9]|zero|one|two|three|four|five|six|seven|eight|nine)").matcher(line), 
                m -> m.find(m.hasMatch() ? m.toMatchResult().start() + 1 : 0), 
                m -> m
            )
            .map(Matcher::toMatchResult)
            .distinct()
            .map(MatchResult::group)
            .map(str -> Map.of(
                    "one", "1",
                    "two", "2",
                    "three", "3",
                    "four", "4",
                    "five", "5",
                    "six", "6",
                    "seven", "7",
                    "eight", "8",
                    "nine", "9",
                    "zero", "0"
            ).getOrDefault(str, str))
            .collect(Collectors.toList()))
    .map(matches -> matches.getFirst() + matches.getLast())
    .mapToInt(Integer::parseInt)
    .sum();

Ugly? Yes. But functional? Yes.

I think this the first year I’ve ever had to actually debug Day 1!

Eventually, I bent the rules slightly and extracted the Pattern and Map to be constants.

Full code: Day1.java.