Advent of Code - Year 2015, Day 05

Problem statement:

Part A

Like the other problems so far, the requirements are fairly simple and straight-forward. For part A, we simply have to evaluate each string for three conditions and count how many match all three conditions. None of the conditions is particularly difficult to understand, though implementation will reveal some cool tricks.

Part B

The problem doesn't change for part B, only the conditions. The second condition (repeat letter) is not complicated, but the first condition has a catch that is easily missed: the string must have the same pair twice, but the pairs must not overlap. There are several ways to evaluate this, but I found that the easiest way is keep an index with each letter pair and ensure that if there is more than one pair, the index for each pair is at least 2 positions apart.


Structurally, I could have written it in a single function, but separating all of the disparate conditions into different functions improves the readability significantly.

HasThreeVowels() should be fairly obvious. The .Take(3) is unusual; this is a "performance" improvement (as if performance is an issue when this script takes 7 ms to run): once we have found three vowels, we don't need to process the rest of the string. This would be useful if we were dealing with a large number of long strings. It isn't necessary here; we could take out the .Take(3) and check that the count is >= 3 instead.

For HasPair(), we need to evaluate each letter with the letter after it to find any pairs. Obviously one way to do it would be to for-loop over the string and compare by index. Using .Zip() is interesting though, because it provides the pairs in a set-based form which improves usability and readability. The way this works is by zipping the base string with the string skipped one, we'll get an enumeration of each character and the character that follows it. For example, for the list [0, 1, 2, 3, 4], l.Zip(l.Skip(1), ...) will call the map function with {0, 1}, {1, 2}, {2, 3}, {3, 4}. This makes it easy to check to see if there are any pairs at all.

HasRepeatLetter() repeats this technique, to evaluate pairs that are offset by two positions instead of one position.

HasDuplicatePair() is the most complicated. What we need to know here is that there exists a pair that is separated by 2 or more positions (aaaa would work because aa is at positions 0 and 2; position 1 being irrelevant; aaaj would not work because aa is only at positions 0 and 1, so it is not a duplicate pair. The way this is accomplished is by getting a substring of two letters at each position, grouping them by the string itself, find the ones that are in more than one position (.Count() > 1), and checking that the minimum index and the maximum index are more than one position separated.


Once we start using LINQ to write the code for the C# version, the F# looks fairly similar. The only unique thing to note on the F# side is Seq.windowed. This does what the .Zip(.Skip()) technique does on the C# side: it provides an array of windowed enumeration values for each position in the array.