Day 4: Passport Processing
Puzzle Description
Today I originally used CoffeeScript but I decided to not rewrite CoffeeScript code as it's just a bleh language for me, and Scala has a really elegant solution for today.
Solution Summary
- Parse passports
- Count number of valid passports in list
Part 1
Part 1 just is looking for the existance - the hard part of this is actually parsing.
First let's define our Passport
:
case class Passport
(byr: Option[Int],
iyr: Option[Int],
eyr: Option[Int],
hgt: Option[String],
hcl: Option[String],
ecl: Option[String],
pid: Option[String])
As you can see it's just a bunch of Option
-al fields.
Because the test is so simple for seeing if a passport is valid, let's do that now.
Using the monadic properties of Option
with the monadic for
makes this really easy:
case class Passport(...):
def isValid: Boolean =
(for
_ <- byr
_ <- iyr
_ <- eyr
_ <- hgt
_ <- hcl
_ <- ecl
_ <- pid
yield ()).isDefined
Now let's do the actual parsing:
def parse(str: String): List[Passport] =
str.split("\n\n").map: block =>
var byr = Option.empty[Int]
var iyr = Option.empty[Int]
var eyr = Option.empty[Int]
var hgt = Option.empty[String]
var hcl = Option.empty[String]
var ecl = Option.empty[String]
var pid = Option.empty[String]
block.split(raw"\s+").foreach:
// throw if not valid int
case s"byr:$b" => byr = Some(b.toInt)
case s"iyr:$i" => iyr = Some(i.toInt)
case s"eyr:$e" => eyr = Some(e.toInt)
case s"hgt:$h" => hgt = Some(h)
case s"hcl:$h" => hcl = Some(h)
case s"ecl:$e" => ecl = Some(e)
case s"pid:$p" => pid = Some(p)
case _ => ()
Passport(byr, iyr, eyr, hgt, hcl, ecl, pid)
.toList
We don't bother testing for country id as it never matters for us - each block's new lines don't matter either and
each component doesn't have spaces, so we can split on flat out whitespace. As a note, the split
function takes
a regex string as a parameter, so the regex here is just "1 or more whitespace characters". We can then match on each
passport segment, and set the correct variable if it matches.
Our input will never have invalid years, so we can convert those here, but the others need to be tested later.
Now let's do part 1:
def part1(input: List[Passport]): Int = input.count(_.isValid)
Part 2
Part 2 is harder but we can still use the monadic for
to our advantage. Namely, the if
in a for
will return None
if the expression is false.
So we can test with just a for
:
case class Passport(...):
def isValidP2: Boolean =
(for
byr <- this.byr
if byr >= 1920 && byr <= 2002
iyr <- this.iyr
if iyr >= 2010 && iyr <= 2020
eyr <- this.eyr
if eyr >= 2020 && eyr <= 2030
hgt <- this.hgt
(heightNumStr, heightUnit) = hgt.span(_.isDigit)
heightNum = heightNumStr.toInt
if (heightUnit == "cm" && heightNum >= 150 && heightNum <= 193)
|| (heightUnit == "in" && heightNum >= 59 && heightNum <= 76)
hcl <- this.hcl
if hcl.head == '#' && hcl.tail.length == 6
&& hcl.tail.forall(it => it.isDigit || (it >= 'a' && it <= 'f'))
ecl <- this.ecl
if ecl == "amb" || ecl == "blu" || ecl == "brn"
|| ecl == "gry" || ecl == "grn" || ecl == "hzl" || ecl == "oth"
pid <- this.pid
if pid.length == 9
yield ()).isDefined
After getting the value of each component, we test to see if it's valid. If it's not, the for loop will immediately short circuit for us.
(if you're curious what if
in a for
desugars to, it's the filter
function.)
Then part2
is just part1
but using our new valid function:
def part2(input: List[Passport]) = input.count(_.isValidP2)
Final Code
case class Passport
(byr: Option[Int],
iyr: Option[Int],
eyr: Option[Int],
hgt: Option[String],
hcl: Option[String],
ecl: Option[String],
pid: Option[String]):
def isValid: Boolean =
(for
_ <- byr
_ <- iyr
_ <- eyr
_ <- hgt
_ <- hcl
_ <- ecl
_ <- pid
yield ()).isDefined
def isValidP2: Boolean =
(for
byr <- this.byr
if byr >= 1920 && byr <= 2002
iyr <- this.iyr
if iyr >= 2010 && iyr <= 2020
eyr <- this.eyr
if eyr >= 2020 && eyr <= 2030
hgt <- this.hgt
(heightNumStr, heightUnit) = hgt.span(_.isDigit)
heightNum = heightNumStr.toInt
if (heightUnit == "cm" && heightNum >= 150 && heightNum <= 193)
|| (heightUnit == "in" && heightNum >= 59 && heightNum <= 76)
hcl <- this.hcl
if hcl.head == '#' && hcl.tail.length == 6
&& hcl.tail.forall(it => it.isDigit || (it >= 'a' && it <= 'f'))
ecl <- this.ecl
if ecl == "amb" || ecl == "blu" || ecl == "brn"
|| ecl == "gry" || ecl == "grn" || ecl == "hzl" || ecl == "oth"
pid <- this.pid
if pid.length == 9
yield ()).isDefined
def parse(str: String): List[Passport] =
str.split("\n\n").map: block =>
var byr = Option.empty[Int]
var iyr = Option.empty[Int]
var eyr = Option.empty[Int]
var hgt = Option.empty[String]
var hcl = Option.empty[String]
var ecl = Option.empty[String]
var pid = Option.empty[String]
block.split(raw"\s+").foreach:
// throw if not valid int
case s"byr:$b" => byr = Some(b.toInt)
case s"iyr:$i" => iyr = Some(i.toInt)
case s"eyr:$e" => eyr = Some(e.toInt)
case s"hgt:$h" => hgt = Some(h)
case s"hcl:$h" => hcl = Some(h)
case s"ecl:$e" => ecl = Some(e)
case s"pid:$p" => pid = Some(p)
case _ => ()
Passport(byr, iyr, eyr, hgt, hcl, ecl, pid)
.toList
def part1(input: List[Passport]): Int = input.count(_.isValid)
def part2(input: List[Passport]): Int = input.count(_.isValidP2)
Benchmark
Part 1
Mean |
Error |
|
---|---|---|
JVM |
10.613 ms |
+/- 2.532 ms |
JS |
21.586 ms |
+/- 3.374 ms |
Native |
9.611 ms |
+/- 0.372 ms |
Part 2
Mean |
Error |
|
---|---|---|
JVM |
10.131 ms |
+/- 2.511 ms |
JS |
26.568 ms |
+/- 4.220 ms |
Native |
9.206 ms |
+/- 0.103 ms |