220 lines
6.9 KiB
Markdown
220 lines
6.9 KiB
Markdown
# [Year 2015 Day 5](https://adventofcode.com/2015/day/5)
|
|
|
|
This day provides a nice chance to introduce
|
|
[views](https://idris2.readthedocs.io/en/latest/tutorial/views.html),
|
|
specifically `String`'s
|
|
[`AsList`](https://www.idris-lang.org/docs/idris2/current/base_docs/docs/Data.String.html#Data.String.AsList)
|
|
view, which lets us treat `String`s as if they were lazy lists or iterators.
|
|
|
|
```idris hide
|
|
module Years.Y2015.Day5
|
|
|
|
import Control.Eff
|
|
|
|
import Runner
|
|
```
|
|
|
|
```idris
|
|
import Data.String
|
|
|
|
import Util
|
|
```
|
|
|
|
```idris hide
|
|
%default total
|
|
```
|
|
|
|
## Nice Strings
|
|
|
|
### Contains at least 3 vowels
|
|
|
|
First we must define what a vowel is
|
|
|
|
```idris
|
|
vowels : List Char
|
|
vowels = ['a', 'e', 'i', 'o', 'u']
|
|
```
|
|
|
|
Now define a function to check if a string contains a minimum number of vowels.
|
|
Here we make use of the `with` rule and the `AsList` view over strings.
|
|
|
|
The `with` rule provides dependently typed pattern matching, allowing us to make
|
|
use of the fact that the form of some of the arguments can be determined by the
|
|
values of the others.
|
|
|
|
The `AsList` view associates a `String` with a lazy list of the characters in
|
|
the string by providing incremental proofs that the `String` decomposes into the
|
|
lazy list. An `AsList` of value `Nil`/`[]` proves that the associated string is
|
|
empty, and an `AsList` of value `c :: xs` proves that the associated string can
|
|
be generated by `strCons`ing the character `c` onto the rest of the string as
|
|
captured by the `AsList` in `xs`.
|
|
|
|
We use the `asList` function, which generates the `AsList` view for the provided
|
|
string, as the argument to the `with` block. Functions like this that produce
|
|
views for a value are refereed to as "covering functions". In general, views are
|
|
dependently typed constructs whose value provides insight into the structure of
|
|
another value.
|
|
|
|
We implement this function by keeping a counter of the number of vowels we still
|
|
need to see to accept the string, returning true when that counter equals zero,
|
|
or false if we hit the end of the string (when the string argument is empty and
|
|
the associated `AsList` is `Nil`) while the counter is still non-zero.
|
|
|
|
If the counter is non-zero and the string is non-empty, we check the current `c`
|
|
character to see if it is a vowel, by checking to see if our `vowels` list
|
|
contains it, then recurse, decrementing the counter if it was a vowel, and
|
|
leaving the counter unchanged if it wasn't.
|
|
|
|
The use of `| xs` after the recursive call to `containsVowels` tells Idris to
|
|
recurse directly to the `with` block, allowing it to reuse the `AsList` value in
|
|
`xs` without having to generate the `AsList` view again.
|
|
|
|
```idris
|
|
containsVowels : (count : Nat) -> String -> Bool
|
|
containsVowels count str with (asList str)
|
|
containsVowels 0 str | _ = True
|
|
containsVowels (S k) "" | [] = False
|
|
containsVowels (S k) (strCons c str) | (c :: xs) =
|
|
if any (== c) vowels
|
|
then containsVowels k str | xs
|
|
else containsVowels (S k) str | xs
|
|
```
|
|
|
|
### At least one letter appears twice in a row
|
|
|
|
For this one, since we need to inspect two letters at a time, we break out a
|
|
nested `with` block, with the tail of the `AsList` as the argument of the block,
|
|
to also pattern match on the next character of the string.
|
|
|
|
Since the `AsList` view is lazy, we need to use `force` in the inner with block
|
|
to force the evaluation of the next component of the view, this is due to a
|
|
[bug in the compiler](https://github.com/idris-lang/Idris2/issues/3461).
|
|
|
|
The logic here is pretty simple, if we hit the empty string or get down to one
|
|
character, return `False`, if we have two characters available, compare them,
|
|
returning `True` if they match, and recursing to the outer with block, only
|
|
consuming one of the two characters, if they don't match.
|
|
|
|
```idris
|
|
doubleLetter : String -> Bool
|
|
doubleLetter str with (asList str)
|
|
doubleLetter "" | [] = False
|
|
doubleLetter (strCons c str) | (c :: x) with (force x)
|
|
doubleLetter (strCons c "") | (c :: x) | [] = False
|
|
doubleLetter (strCons c (strCons d str)) | (c :: x) | (d :: y) =
|
|
if c == d
|
|
then True
|
|
else doubleLetter (strCons d str) | x
|
|
```
|
|
|
|
### No Invalid Substrings
|
|
|
|
First we define the substrings that are disallowed
|
|
|
|
```idris
|
|
disallowedSubstrs : List String
|
|
disallowedSubstrs = ["ab", "cd", "pq", "xy"]
|
|
```
|
|
|
|
Idris's standard library does not currently define a function to check if a
|
|
string is a substring of another, so we instead use our own `isSubstr` function,
|
|
defined via use of the `AsList` view, from our [Util](../../Util.md#issubstr)
|
|
module.
|
|
|
|
```idris
|
|
noDisallowed : String -> Bool
|
|
noDisallowed str =
|
|
not $ any (`isSubstr` str) disallowedSubstrs
|
|
```
|
|
|
|
### isNice
|
|
|
|
Combine all three of our above functions together to check if a string is a
|
|
"nice" string
|
|
|
|
```idris
|
|
isNice : String -> Bool
|
|
isNice str = containsVowels 3 str && doubleLetter str && noDisallowed str
|
|
```
|
|
|
|
## New niceness
|
|
|
|
### Pair of letters that appears twice
|
|
|
|
Use a similar approach to `doubleLetter` above, but instead of comparing the two
|
|
chars for equality, pack them into a string and check if they are a substring of
|
|
the rest of the string.
|
|
|
|
```idris
|
|
pairTwice : String -> Bool
|
|
pairTwice str with (asList str)
|
|
pairTwice "" | [] = False
|
|
pairTwice (strCons c str) | (c :: x) with (force x)
|
|
pairTwice (strCons c "") | (c :: x) | [] = False
|
|
pairTwice (strCons c (strCons d str)) | (c :: x) | (d :: y) =
|
|
if pack [c, d] `isSubstr` str
|
|
then True
|
|
else pairTwice (strCons d str) | x
|
|
```
|
|
|
|
### Letter that repeats after one letter
|
|
|
|
More of the same, but this one gets extra fun because we are matching 3 letters
|
|
at a time. This would be a lot nicer if we could get away without nesting the
|
|
`with` blocks, but due to the aformentioned compiler bug, we need the nesting.
|
|
|
|
```idris
|
|
letterRepeatGap : String -> Bool
|
|
letterRepeatGap str with (asList str)
|
|
letterRepeatGap "" | [] = False
|
|
letterRepeatGap (strCons c str) | (c :: x) with (force x)
|
|
letterRepeatGap (strCons c "") | (c :: x) | [] = False
|
|
letterRepeatGap (strCons c (strCons d str)) | (c :: x) | (d :: y) with (force y)
|
|
letterRepeatGap (strCons c (strCons d "")) | (c :: x) | (d :: y) | [] = False
|
|
letterRepeatGap (strCons c (strCons d (strCons e str))) | (c :: x) | (d :: y) | (e :: z) =
|
|
if c == e
|
|
then True
|
|
else letterRepeatGap (strCons d (strCons e str)) | x
|
|
```
|
|
|
|
### New isNice
|
|
|
|
Glue our two new niceness functions together
|
|
|
|
```idris
|
|
isNice' : String -> Bool
|
|
isNice' str = pairTwice str && letterRepeatGap str
|
|
```
|
|
|
|
## Part Functions
|
|
|
|
### Part 1
|
|
|
|
Split our input up into a list of its component strings, and then count how many
|
|
satisfy the "nice" criteria.
|
|
|
|
```idris
|
|
part1 : Eff (PartEff String) (Nat, ())
|
|
part1 = do
|
|
strings <- map (lines . trim) $ askAt "input"
|
|
let x = count isNice strings
|
|
pure (x, ())
|
|
```
|
|
|
|
### Part 2
|
|
|
|
Same as above, but with the new niceness criteria
|
|
|
|
```idris
|
|
part2 : () -> Eff (PartEff String) Nat
|
|
part2 _ = do
|
|
strings <- map (lines . trim) $ askAt "input"
|
|
let x = count isNice' strings
|
|
pure x
|
|
```
|
|
|
|
```idris hide
|
|
public export
|
|
day5 : Day
|
|
day5 = Both 5 part1 part2
|
|
```
|