Advent of Code Day 4 - validating regex

Todays task (esp part 2) was the task the most resembles my daily work...

Part 1

For parsing the input I decided to also go the string/regex way. I concatenate the whole input into on long string, replace an empty line (i.e. two newlines) with a record seperator (|, pipe), replace all remaning spaces and newlines (\s) with a field seperator (;), and then split the long string into an array on the record seperator.

my $in = join('',<STDIN>);
$in =~ s/\n\n/SEP/gm;
$in =~ s/\s/;/g;
my @d = split(/SEP/,$in);

my $valid;
for my $l (@d) {
    my %p = map { split /:/ } split( /;/, $l);
    $valid++ if (keys %p  8 || (keys %p  7 && !$p{cid}));
}
say $valid;

Now we can convert each line (eyr:2039;hcl:#cfa07d;hgt:171cm) into an hash, by first splitting on the field seperator and then on the key/value seperator (:).

Then we only need to check if the hash has either 8 keys, or 7 keys, none of which shall be cid;

Now that I'm writing this I realized that I could choose : as the field seperator, and thus do not need the two splits when converting a line to the hash, again using one of the slightly confusing features of Perl that is actually very powerful. You can assign a list to either an array or an hash, and if you do the latter, Perl will use one value for the key and the other for the value:

my %hash = ( "a", "apple", "b", "banana" );  # { a => 'apple', b => 'banana' }

So we don't need to first split into fields and then into key/value, we can do this in one go by replacing this

my %p = map { split /:/ } split( /;/, $l);

with

my %p = split( /:/, $l);

Which leads to bit more light golfing and:

Part 1 - No spaces

$_=join('',<STDIN>);s/\n\n/|/gm;s/\s/:/g;for(split/\|/){my%p=split/:/;$a++if(keys%p==8||(keys%p==7&&!$p{cid}))}say$a

Not as good as APL, but we're getting there :-)

Part 2

Now we just have to implement a few more validators, which translate very straight forward from the spec to some regex and value checks:

my $in = join('',<STDIN>);
$in =~ s/\n\n/SEP/gm;
$in =~ s/\s/;/g;
my @d = split(/SEP/,$in);

my $valid = 0;
for my $l (@d) {
    my %p = map { split /:/ } split( /;/, $l);
    next unless (keys %p  8 || (keys %p  7 && !$p{cid}));
    next unless $p{byr} =~ /^\d{4}$/ && $p{byr} >= 1920 && $p{byr} <= 2002;
    next unless $p{iyr} =~ /^\d{4}$/ && $p{iyr} >= 2010 && $p{iyr} <= 2020;
    next unless $p{eyr} =~ /^\d{4}$/ && $p{eyr} >= 2020 && $p{eyr} <= 2030;
    next unless $p{hcl} =~ /^#[0-9a-f]{6}$/;
    next unless $p{ecl} =~ /^(amb|blu|brn|gry|grn|hzl|oth)$/;
    next unless $p{pid} =~ /^\d{9}$/;
    if ($p{hgt} =~ /^(\d\d\d)cm$/) {
        next unless $1 >= 150 && $1 <= 193;
    }
    elsif ($p{hgt} =~ /^(\d\d)in$/) {
        next unless $1 >= 59 && $1 <= 76;
    }
    else {
        next;
    }

    $valid++;
}
say $valid;

One small "trick" was to test for height last, so we don't have to check for other invalid heights. I lost a few minutes here because I wrote two if-blocks (for centimeter and inches), which discard all valid passports. After using the valid example data and some debugging (see this commit) I found my error and submitted a valid result.

Stats & Links