3 min read

Advent of Code 2020 Day 16

Streams kind of acts like MapReduce in some ways, but also more like serial calculations in other ways, and it seems you can support both from a greater perspective going forward.
Advent of Code 2020 Day 16

When I saw the setup and premise of today's puzzle, I'd be lying if I said I wasn't immediately excited for the premise of this problem, where you're given a bunch of ranges and have matching to those ranges. This is one of those things where I'm sure many people have dealt with this in a semi-custom way, but at work I've had to develop some custom range objects to support crazy things that may or may not make sense. Knowing this, I immediately went into the phase of developing the the data models around the data in the input data.  The first was <name>: <range1> or <range2>.  I wasn't sure if there were guaranteed to be 2 ranges or if there might be more, but I did a quick analysis of my input file to check, and all of them had 2 ranges.  I developed range parsing, and full row parsing (which also calls the range parsing) knowing that even through part one didn't need the name, I figured part two would need the name more likely than not (I was right here, so I fortunately didn't waste time with that early on).

In part one we processed only the "nearby tickets" to find all of the values that did not satisfy any ranges and add them together. This was done fairly simply in my code originally by an outer for loop, and an if statement by streaming through the rules, but I recently updated it to do it all in streams using flatMapToInt see below:

Before:

List<Integer> invalidValues = new ArrayList<Integer>();
for (String ticket : nearbyTicketStrs) {
  for (String value : ticket.split(",")) {
    int val = Integer.parseInt(value);
    if (rules.stream().filter(rule -> rule.satisfiesARange(val)).count() == 0) {
      invalidValues.add(val);
    }
  }
}
return invalidValues.stream().mapToLong(val -> (long) val).sum();

After:

List<Ticket> tickets =
    nearbyTicketStrs.stream().map(ticket -> new Ticket(ticket)).collect(Collectors.toList());
return tickets
    .stream()
    .flatMapToInt(ticket -> ticket.fields.stream().mapToInt(Integer::valueOf))
    .filter(val -> rules.stream().filter(rule -> rule.satisfiesARange(val)).count() == 0)
    .mapToLong(val -> (long) val)
    .sum();

As you can see, both are taking care of the logic for finding invalid values the same way, by doing a filter with the list of rules (collected before) but since my "Before" solution didn't use the Ticket class that I created in part two to ease some of the parsing. This parsing step really did lead me to the direction of using the additional external list for this work.  Seeing it actually parsed out now all in a single stream, what I'm doing in the "After" is far more realistic for a bigger processing system. The flatMapToInt gives me the ability to expand the SINGLE list into a stream of all of the lists in an easier way, and then every single value goes through the exact same processing system.  I could use parallel() to make this multi-threaded and I think that might be part of the goal of future work. Streams kind of acts like MapReduce in some ways, but also more like serial calculations in other ways, and it seems you can support both from a greater perspective going forward.

I feel like there are further optimizations that I can do in part two, and I think I might come back to my solution later on once I'm on PTO for the rest of the year, or once I have some time to actually look through some of this stuff in a January Advent of Code postmortem. Who knows at this point, but I really enjoyed the problem and super enjoyed the real processing involved here.  My solution is pretty quick, which I'm also really happy about, it's to the point, serial, and great in my humble opinion.  If you want to join my Advent of Code leaderboard, feel free to join with the code: 699615-aae0e8af.