225 lines
4.0 KiB
Go
225 lines
4.0 KiB
Go
package four
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"os"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
type passport struct {
|
|
byr, iyr, eyr, hgt, hcl, ecl, pid, cid string
|
|
}
|
|
|
|
func (p *passport) validate(strict bool, optional ...string) bool {
|
|
valid := true
|
|
requiredFields := []string{"byr", "iyr", "eyr", "hgt", "hcl", "ecl", "pid", "cid"}
|
|
filtered := []string{}
|
|
// remove optional fields
|
|
for _, f := range requiredFields {
|
|
required := true
|
|
for _, o := range optional {
|
|
if f == o {
|
|
required = false
|
|
break
|
|
}
|
|
}
|
|
if required {
|
|
filtered = append(filtered, f)
|
|
}
|
|
}
|
|
|
|
for _, f := range filtered {
|
|
if strict {
|
|
if !p.validateField(f) {
|
|
valid = false
|
|
}
|
|
} else {
|
|
if p.getField(f) == "" {
|
|
valid = false
|
|
}
|
|
}
|
|
}
|
|
|
|
return valid
|
|
}
|
|
|
|
func (p *passport) validateField(code string) bool {
|
|
switch code {
|
|
case "byr":
|
|
year, err := strconv.Atoi(p.byr)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return year >= 1920 && year <= 2002
|
|
case "iyr":
|
|
year, err := strconv.Atoi(p.iyr)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return year >= 2010 && year <= 2020
|
|
case "eyr":
|
|
year, err := strconv.Atoi(p.eyr)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return year >= 2020 && year <= 2030
|
|
case "hgt":
|
|
var height int
|
|
var unit string
|
|
count, err := fmt.Sscanf(p.hgt, "%d%s", &height, &unit)
|
|
if count != 2 || err != nil {
|
|
return false
|
|
}
|
|
if unit == "in" {
|
|
return height >= 59 && height <= 76
|
|
}
|
|
return height >= 150 && height <= 193
|
|
case "hcl":
|
|
if len(p.hcl) != 7 {
|
|
return false
|
|
}
|
|
var validColour = regexp.MustCompile(`^#[a-z0-9]+$`)
|
|
return validColour.MatchString(p.hcl)
|
|
case "ecl":
|
|
switch p.ecl {
|
|
case "amb", "blu", "brn", "gry", "grn", "hzl", "oth":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
case "pid":
|
|
if len(p.pid) != 9 {
|
|
return false
|
|
}
|
|
var validColour = regexp.MustCompile(`^[0-9]+$`)
|
|
return validColour.MatchString(p.pid)
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func (p *passport) getField(code string) string {
|
|
switch code {
|
|
case "byr":
|
|
return p.byr
|
|
case "iyr":
|
|
return p.iyr
|
|
case "eyr":
|
|
return p.eyr
|
|
case "hgt":
|
|
return p.hgt
|
|
case "hcl":
|
|
return p.hcl
|
|
case "ecl":
|
|
return p.ecl
|
|
case "pid":
|
|
return p.pid
|
|
case "cid":
|
|
return p.cid
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func (p *passport) setField(code, value string) {
|
|
switch code {
|
|
case "byr":
|
|
p.byr = value
|
|
case "iyr":
|
|
p.iyr = value
|
|
case "eyr":
|
|
p.eyr = value
|
|
case "hgt":
|
|
p.hgt = value
|
|
case "hcl":
|
|
p.hcl = value
|
|
case "ecl":
|
|
p.ecl = value
|
|
case "pid":
|
|
p.pid = value
|
|
case "cid":
|
|
p.cid = value
|
|
default:
|
|
return
|
|
}
|
|
}
|
|
|
|
type customs struct {
|
|
passports []passport
|
|
}
|
|
|
|
func (c *customs) load(filename string) error {
|
|
file, err := os.Open(filename)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer file.Close()
|
|
|
|
scanner := bufio.NewScanner(file)
|
|
var p *passport
|
|
for scanner.Scan() {
|
|
if scanner.Text() == "" {
|
|
// Blank line means we have moved on to the next passport
|
|
if p != nil {
|
|
c.passports = append(c.passports, *p)
|
|
p = nil
|
|
}
|
|
continue
|
|
}
|
|
|
|
if p == nil {
|
|
p = &passport{}
|
|
}
|
|
|
|
fields := strings.Split(scanner.Text(), " ")
|
|
for _, f := range fields {
|
|
var key, value string
|
|
count, err := fmt.Sscanf(f, "%3s:%s", &key, &value)
|
|
if count != 2 || err != nil {
|
|
return fmt.Errorf("Unable to parse field %s: %w", f, err)
|
|
}
|
|
|
|
p.setField(key, value)
|
|
}
|
|
}
|
|
// commit the last passport if there isn't a blank line
|
|
if p != nil {
|
|
c.passports = append(c.passports, *p)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *customs) check(strict bool, optional ...string) int {
|
|
valid := 0
|
|
|
|
for _, p := range c.passports {
|
|
if p.validate(strict, optional...) {
|
|
valid++
|
|
}
|
|
}
|
|
|
|
return valid
|
|
}
|
|
|
|
// PartOne find all the valid passports
|
|
func PartOne() string {
|
|
customs := customs{}
|
|
customs.load("four/passports.txt")
|
|
validPassports := customs.check(false, "cid")
|
|
|
|
return fmt.Sprintf("Found %d valid passports", validPassports)
|
|
}
|
|
|
|
// PartTwo Strictly find all the valid passports
|
|
func PartTwo() string {
|
|
customs := customs{}
|
|
customs.load("four/passports.txt")
|
|
validPassports := customs.check(true, "cid")
|
|
|
|
return fmt.Sprintf("Found %d strictly valid passports", validPassports)
|
|
}
|