DEV Community

Daniel Mita
Daniel Mita

Posted on

Practicing Raku Grammars On Exercism

Grammars are a powerful tool in Raku for pattern matching and transformation. This post will cover several exercises from https://exercism.org/ which are great for experimenting with this functionality.

This post is a breakdown of my own use of grammars. If you would like to learn more about them, there is an introduction post from @jj which can be found here: https://dev.to/jj/introduction-to-grammars-with-perl6-75e

Phone Number

Check and return a valid phone number with non-digit characters stripped:
https://exercism.org/tracks/raku/exercises/phone-number

This exercise uses the North American Numbering Plan (NANP) as the phone number format, a ten-digit telephone number in the form of NPA-NXX-XXXX, where N represents a digit 2 through 9, and X represents a digit 0 through 9.

Let's start off with checks for N and X.

token X { <[0..9]> }
token N { <+X - [01]> <!before 11> }
Enter fullscreen mode Exit fullscreen mode

token X is the simplest here, merely being digits 0-9. token N is X with 0 and 1 removed, and an additional check has been added to ensure that this digit can't come before two sequential 1s (known as an N11 code e.g. 911).

The second and third portions of the number (NXX and XXXX) are known as exchange and station codes.

token exchange-code { <.N> <.X> ** 2 }
token station-code  { <.X> ** 4 }
Enter fullscreen mode Exit fullscreen mode

NPA (AKA the area code) has some additional rules in the real world. This is not relevant for this exercise so let's just copy exchange-code.

token area-code { <.exchange-code> }
Enter fullscreen mode Exit fullscreen mode

Now that all the needed parts of the number are defined, let's create a rule called TOP to bring it all together, with an extra part to check for a country code (a 1, with an optional leading plus sign).

rule TOP { ['+'? 1]? <area-code> <exchange-code> <station-code> }
Enter fullscreen mode Exit fullscreen mode

A rule and a token differ in how they handle whitespace. See more here: https://docs.raku.org/language/grammars#ws

And finally, let's alter what is considered to be whitespace. I'll be taking the lazy approach by matching anything that isn't 0-9.

token ws { <-X>* }
Enter fullscreen mode Exit fullscreen mode

All together this looks like:

grammar NANP {
    rule TOP { ['+'? 1]? <area-code> <exchange-code> <station-code> }

    token area-code     { <.exchange-code> }
    token exchange-code { <.N> <.X> ** 2 }
    token station-code  { <.X> ** 4 }

    token N { <+X - [01]> <!before 11> }
    token X { <[0..9]> }

    token ws { <-X>* }
}
Enter fullscreen mode Exit fullscreen mode

The important parts needed to complete this exercise will be named in a Match object. Let's now write a class which will be used to transform a Match into the desired format.

class Cleaner {
    method TOP ($/) {
        make [~] $<area-code exchange-code station-code>;
    }
}
Enter fullscreen mode Exit fullscreen mode

The TOP method (which will operate on the TOP match) will take a Match object, and concatenate the area-code, exchange-code, and station-code portions of that match into a string. The make routine will attach any given payload (the string in this case) to the Match object, which can be retrieved with the made routine.

The following example will return 9876543210:

NANP.parse('+1 (987) 654-3210', :actions(Cleaner)).made;
Enter fullscreen mode Exit fullscreen mode

My published solution to this exercise can be found here: https://exercism.org/tracks/raku/exercises/phone-number/solutions/m-dango

ISBN Verifier

Identify whether given data is a valid ISBN-10:
https://exercism.org/tracks/raku/exercises/isbn-verifier

This exercise uses a simpler grammar than the previous. A set of 9 digits, and a 10th digit or X, separated by dashes.

grammar ISBN {
    rule TOP    { <digit> ** 9 [ <digit> | X ] }
    token digit { <[0..9]> }
    token ws    { '-'? }
}
Enter fullscreen mode Exit fullscreen mode

The class being used for actions however is a bit more involved.

class Validator {
    method TOP ($/) {
        make ( (|$<digit>, 10) Z* (10...1) ).sum %% 11;
    }
}
Enter fullscreen mode Exit fullscreen mode

Here all the matched digits are multiplied using the zip metaoperator, i.e., the 1st digit is multiplied by 10, the 2nd multiplied by 9, etc. If there were 10 digits in the match, the subsequent 10 (which is there to substitute an X from the Match) is ignored. The result of this zip is then added up by the sum routine, and then that result is checked for divisibility by 11. The final payload will be a Bool for this check.

My published solution to this exercise can be found here: https://exercism.org/tracks/raku/exercises/isbn-verifier/solutions/m-dango

Wordy

Parse and solve a written mathematical problem:
https://exercism.org/tracks/raku/exercises/wordy

I had a lot of fun with this one! A mathematical problem is given in the form of a question in English, and a numeric solution is expected as a result. The operations are expected to be resolved from left to right.

What is 3 plus 2 multiplied by 3? = 15

First, let's take the expected operations, and associate them with the appropriate functions.

constant %OPS = 
    'plus'          => &infix:<+>,
    'minus'         => &infix:<->,
    'multiplied by' => &infix:<ร—>,
    'divided by'    => &infix:<รท>,
;
Enter fullscreen mode Exit fullscreen mode

Then, in the grammar, let's use the keys from this hash inside a token.

token op { @(%OPS.keys) }
Enter fullscreen mode Exit fullscreen mode

Every number in the string will be a positive or negative integer, so let's use something simple to match those.

token number { '-'? <[0..9]>+ }
Enter fullscreen mode Exit fullscreen mode

And now let's pair these up to create a function with them later.

rule func { <op> <number> }
Enter fullscreen mode Exit fullscreen mode

In the corresponding action, let's now create some methods to build functions.

method func ($/) {
    make -> $x { $<op>.made.($x, $<number>) };
}

method op ($/) {
    make %OPS{$/};
}
Enter fullscreen mode Exit fullscreen mode

If plus 2 were part of the given text, what would happen is the op method would fetch the &infix:<+> routine from the %OPS hash, and the func method would create a new function: -> $x { &infix:<+>($x, 2) }, or more simply -> $x { $x + 2 }.

Let's now put together the final TOP matcher and method.

rule TOP { What is <number> <func>* '?' }

method TOP ($/) {
    make $<number>.&( [Rโˆ˜] $<func>.map(*.made) );
}
Enter fullscreen mode Exit fullscreen mode

$<func> will be an array which can contain 0 or more matches. The map will retrieve each function created by the func method, and these functions are then reduced using the function composition operator, ultimately creating a single function to call with the first number from the match. The function composition operator usually has the left function called with the result of the right function, so the R metaoperator is used to reverse this.

As an example, the phrase What is 1 plus 2 multiplied by 3? is transformed into the equivalent of:

1
==> -> $x { $x + 2 }()
==> -> $x { $x * 3 }();
Enter fullscreen mode Exit fullscreen mode

My published solution to this exercise can be found here: https://exercism.org/tracks/raku/exercises/wordy/solutions/m-dango

Meetup

Determine a date from a written description:
https://exercism.org/tracks/raku/exercises/meetup

This grammar is also a straightforward one, intended to match a description such as Second Friday of December 2013. Month, Weekday and Week are all set up as enums. 1 to 12 for Jan to Dec, 1 to 7 for Mon to Sun, and Week being the first possible day of each week expected from the description.

enum Week (
    |(<First Second Third Fourth> Z=> (1, 8 ... *)),
    Teenth => 13,
);

grammar Description {
    rule TOP { <week> <weekday> of <month> <year> }

    token week    { @(Week.keys) | Last }
    token weekday { @(Weekday.keys) }
    token month   { @(Month.keys) }
    token year    { <[0..9]>+ }
}
Enter fullscreen mode Exit fullscreen mode

Last is a special case so a specific value has not been assigned to it.

The TOP method in the action class then has a few steps.

First, a date object is created for the wanted week.

my Date $week.=new(
    year  => $<year>,
    month => ::($<month>),
    |(day => ::($<week>) if $<week> ne 'Last'),
);
Enter fullscreen mode Exit fullscreen mode

A day is not specified if the last week is wanted. Instead, a condition is used to adjust this date to the beginning of the final week.

if $<week> eq 'Last' {
    $week.=later( (months => 1, weeks => -1) );
}
Enter fullscreen mode Exit fullscreen mode

And with the date now being at the start of the given week, let's adjust it to match the wanted day of the week.

make .later(days => (::($<weekday>) - .day-of-week) % Weekday.keys) given $week;
Enter fullscreen mode Exit fullscreen mode

If the start of the week is a Friday (5) and the desired day is a Tuesday (2), the date will be advanced by (2 - 5) % 7 = 4 days.

My published solution to this exercise can be found here: https://exercism.org/tracks/raku/exercises/meetup/solutions/m-dango

I hope you've enjoyed these examples of grammars, and I hope to see and experiment with more applications of them in future!

Top comments (0)