Advent of Code 2023: Day 1
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.