p3-moritz

Download the raw code.

use v6;

# Solution to problem 3 of
# http://strangelyconsistent.org/blog/masaks-perl-6-coding-contest
# by Moritz Lenz, 2010-12-10

class SignedRange {
    has ($.start, $.end, $.including);

    method ACCEPTS($other) {
        $.start <= $other <= $.end;
    }
}

grammar Ranges::Grammar {
    token TOP   { ^ :s  [<range> <.ws>]* $               }
    rule  range { <sign>\[ <start=int> '..' <end=int> \] }
    token int   { <.sign>? \d+                           }
    token sign  { <[+\-]>                                }
}

class Ranges::Actions {
    method TOP($/) { make $<range>ยป.ast };
    method range($/) {
        make SignedRange.new(
            start       => $<start>.Int,
            end         => $<end>.Int,
            including   => $<sign> eq '+',
        );
    }
}

sub parse-and-range-check(Cool $range-spec, Cool $tester) {
    my $r = Ranges::Grammar.parse($range-spec, :actions(Ranges::Actions));
    return "invalid input" unless $r && $tester ~~ m:s/^ \-? \d+ $/;
    return 'no' unless $r.ast && $r.ast.[0];

    given $r.ast.reverse.first({ $tester.Int ~~ $^x }) {
        return $_ && .including ?? 'yes' !! 'no';
    }
}

# the rest of the code is "just" tests. To run them, 
# call this script with the 'test' argument:
# $ perl6 code test

multi MAIN() {
    say parse-and-range-check get, get;
}

multi MAIN('test') {
    use Test;
    plan *;

    ok my $r = Ranges::Grammar.parse('+[-12..34]', :actions(Ranges::Actions)),
            "can parse string";
    isa_ok $r.ast.[0], SignedRange, 'got the right object';
    is $r.ast.[0].start, -12, 'right start';
    is $r.ast.[0].end,    34, 'right end';
    ok $r.ast.including,      'and it is including';

    for '',
        ' ',
        '+[-12..34]-[1..-3]',
        ' +[ -12 .. 34 ] -[ 1 .. -3 ]'
        -> $x {
        ok Ranges::Grammar.parse($x), "Can parse string '$x'";
    }

    for '+[]', '+[3..]', '+-[3..5]', '+[3.4..5]' -> $x {
        nok Ranges::Grammar.parse($x), "Correctly failed to parse '$x'";
    }

    is parse-and-range-check('', ''), 'invalid input', 'needs a number';
    is parse-and-range-check('', '4'), 'no', 'No ranges => "no"';
    is parse-and-range-check('+[1..5]', '4'), 'yes', '4 in +[1..5]';

    is parse-and-range-check('+[1..5] -[2..4]', '1'), 'yes', '+ and -';
    is parse-and-range-check('+[1..5] -[2..4]', '2'), 'no',  '+ and -';

    is parse-and-range-check('-[2..4] +[1..5]', '1'), 'yes', '- and +';
    is parse-and-range-check('-[2..4] +[1..5]', '2'), 'yes', '- and +';
    is parse-and-range-check('-[2..4] +[1..5]', '-2'), 'no', '- and +';

    done_testing;
}

# vim: set ft=perl6

Readability

This was the first submission I got, a couple of hours after the contest was announced. I still think it's the nicest one (across all tasks) in terms of readability.

Consistency

Very minor nit: I would have preferred the adjective $.inclusive to the participle $.including.

Clarity of intent

I think the comment # the rest of the code is "just" tests. on line 43 is misplaced.

Algorithmic efficiency

This algorithm is linear, but it one-ups all the other submissions, including those that got the algorithm wrong, by being twice as fast on average. It all happens on line 38.

Idiomatic use of Perl 6

Giving your type an ACCEPTS method and then smartmatching on line 38? Nice!

Brevity

Like someone setting up a chain of dominoes, this code defines a class, a grammar, one more class, a sub, and then just does this:

multi MAIN() {
    say parse-and-range-check get, get;
}

By that point, we're on line 49, not even breaking a sweat trying to keep things short. This is why I love Perl 6.