5 min read

Advent of Code 2020 Day 2

David discusses Java 8 Streams and learning how to actually leverage them for the second day of the Advent of Code.
Advent of Code 2020 Day 2

Today's puzzle was an interesting one mostly because I've seen these kinds of problems before, and the way I've approached them in the past is to use plain-old logic rather than trying to optimize for the language that I'm using.  One of the things I'm trying to do this year with Advent of Code is use more language-specific functionality.  Since I'm using Java (to help myself better understand Java 8 at work) this REALLY means that I'm focusing time on learning Java Streams. Java streams have a lot of the same kind of concepts that lambdas in other language have, where you can do quick and easy work on single items/elements in order to simplify code and logic.  This is something I see my co-workers who are more familiar with Java 8 using frequently, and I struggle to read the code most of the time, I want to use this year's Advent of Code to clear that up.  Before I go through today's puzzle solution, I want to give some examples of why streams are so helpful.


Say you are given an array of numbers, and you want to convert that array of numbers into an array of the square of those numbers, or count how many of those numbers are even, or provide the subset of those numbers that fall into some criteria to another function for other use. Typically, you'd see something like below for the first example (squaring the values)

List<Integer> values = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> squares = new ArrayList<Integer>();
for(Integer value : values){
    squares.add(value*value);
}
System.out.println(squares);

Output:

[1, 4, 9, 16, 25]

This is made fairly simple by leveraging Java Lists, but for the most part, this would be a reasonable way for someone to provide this type of an operation. If this were to be done with arrays, it might look a bit different, but you'd likely be going through the same process; Create the output list/array, go through the initial list/array, perform the square action, store the result in the output list/array. With the Java 8 streams, you have a bit more of a capability to do this a bit more fancy like below

List<Integer> values = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> squares = values.stream()
                              .map(m -> m * m)
                              .collect(Collectors.toList());
System.out.println(squares);

Output:

[1, 4, 9, 16, 25]

As you can see, the second solution is pretty easy to understand AFTER seeing the first one, but there are ways to make streams act way more like the initial version.  The reality though, is that the m -> m * m piece of the map function in the above code can be anything, including calls to other methods.  Another example of this is with the filter function, which allows you to use boolean logic to keep some items going down the stream and others not.  An example looking to determine the values that are even in a List of Integers.

List<Integer> values = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> evens = values.stream()
                            .filter(value -> value % 2 == 0)
                            .collect(Collectors.toList());
System.out.println(evens);

Output:

[2, 4]

In here, you can see that much like python or other lambdas, you can specify your internal variable by whatever name you want (here, I chose value rather than m from before) and do the operation as desired at that point.

Alright enough about streams, now onto how I solved the day 2 puzzle.  In today's puzzle, you had to take the input file which had each line having a specific format for what it represents (different part 1 vs. part 2, but all of the data needed to be parsed regardless) and then the validity of the row was determined based on the criteria from the specific part.  The lines in the file had the following format:

1-3 a: abcde
1-3 b: cdefg
2-9 c: ccccccccc

For part one, the first line parsed to minimum count: 1, maximum count: 3, letter to count: a, password: abcde; the second line parsed to minimum count: 1, maximum count: 3, letter to count: b, password: cdefg; the third line parsed to minimum count: 2, maximum count: 9, letter to count: c, password: ccccccccc. Knowing from previous Advent of Code years that you typically need to re-use some or all of the logic, I created a separate class (PasswordEntry) to store this data for each line.  For part one, I had called the two numbers minCount, maxCount but since part two had different meanings for those values, I later changed their name to num1, num2 (creative, I know). The logic to parse these values out of the input data was put solely on the constructor and that constructor was called using streams in the file reading method which also used the Java 8 streams.

BufferedReader reader = new BufferedReader(new InputStreamReader(fileName));
return reader.lines()
             .map(m -> new PasswordEntry(m))
             .collect(Collectors.toList());

The alternative to this would be something like the following:

BufferedReader reader = new BufferedReader(new InputStreamReader(fileName));
List<PasswordEntry> passwords = new ArrayList<PasswordEntry>();
String line = reader.readLine();
while(line != null){
    passwords.add(new PasswordEntry(line);
    line = reader.readLine();
}
return passwords;

As you can see, in simply the File I/O portion, streams really clean up the code and reduces unnecessary "boilerplate" logic and code that is trivial and annoying to have to include. As you learn more about streams, it all becomes more readable, and as it becomes more readable, it becomes more abstract (in a way).  This is kind of the problem with languages allowing multiple ways to approach the same things, those who are familiar with the "old school" approach feel lost until they're able to use the "new school" approach, but once you learn the concepts, it's all pretty easy to understand (seeing synonymous code helps me, so hopefully it helps others).

Filters and maps can also call methods, similar to what I had done for both part one and part two in their actual processing methods to simply call the PasswordEntry validation method on each of those values.  If that returns true, we then count the number of items in the list using the count() stream method and return that count (after casting it to an integer, because picking types can be hard).

All-in-all, I felt this day of puzzles has really helped me to better understand streams and how useful they are and how to now properly dissect code that has streams as part of it for reading. In my mind, I still convert the streams into the "old school" logic, just like I would do with a foreign language, but perhaps in the future, it will be my primary interchange language.

As far as this puzzle goes, I started later in the day and submitted a bit later, so my "stats" aren't as great, but I ranked 31,595 in part 1, 29,537 in part 2.  Yesterday was 24,348 and 22,102 respectively. I do know that as the Advent goes on, fewer people will be participating, so we'll see how that goes going into the future. If you want to join my leaderboard the code is 699615-aae0e8af.