zerosleeps

Since 2010

Advent of Code 2020 day 4

My solution for Advent of Code 2020 day 4, this time in Ruby. I have a working Python solution as well, but it’s ugly: Ruby shines with chained methods and blocks.

I have a faint feeling that we’ll be revisiting this passport thing in future challenges…

class Passport
  def initialize(attributes)
    @attributes = attributes
  end

  def self.parse_raw_input(raw_input)
    raw_input
      .strip
      .split($/ + $/) # Passports are separated by two newlines in batch file
      .map do |passport|
        passport
          .gsub($/, ' ') # Each passport may be split into multiple lines
      end
      .map do |passport|
        passport
          .split # Conveniently, String.split also strips white-space
          .to_h do |element|
            [
              element.split(':').first.to_sym,
              element.split(':').last
            ]
          end
      end
  end

  def all_fields_present?
    [:byr, :iyr, :eyr, :hgt, :hcl, :ecl, :pid].all? do |f|
      @attributes.has_key?(f)
    end
  end

  def all_fields_valid?
    return false unless @attributes[:byr].to_i.between?(1920, 2002)
    return false unless @attributes[:iyr].to_i.between?(2010, 2020)
    return false unless @attributes[:eyr].to_i.between?(2020, 2030)
    return false unless (
      @attributes.has_key?(:hgt) && (
        @attributes[:hgt][-2..] == 'cm' && @attributes[:hgt][0..-3].to_i.between?(150, 193) ||
        @attributes[:hgt][-2..] == 'in' && @attributes[:hgt][0..-3].to_i.between?(59, 76)
      )
    )
    return false unless @attributes[:hcl] =~ /^#[\da-f]{6}$/
    return false unless ['amb', 'blu', 'brn', 'gry', 'grn', 'hzl', 'oth'].count(@attributes[:ecl]) == 1
    return false unless @attributes[:pid] =~ /^\d{9}$/
    return true
  end

end

def part_one(raw_input)
  passports = Passport.parse_raw_input(raw_input).map do |parsed_input|
    Passport.new(parsed_input)
  end
  passports.filter { |p| p.all_fields_present? }.length
end

def part_two(raw_input)
  passports = Passport.parse_raw_input(raw_input).map do |parsed_input|
    Passport.new(parsed_input)
  end
  passports.filter { |p| p.all_fields_valid? }.length
end

puts "Part one: #{part_one(IO.read(File.join(__dir__, '../day_04_input.txt')))}"
puts "Part two: #{part_two(IO.read(File.join(__dir__, '../day_04_input.txt')))}"