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> }
``````

`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 }
``````

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> }
``````

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> }
``````

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>* }
``````

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>* }
}
``````

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>;
}
}
``````

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;
``````

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    { '-'? }
}
``````

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

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

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:<÷>,
;
``````

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

``````token op { @(%OPS.keys) }
``````

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]>+ }
``````

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

``````rule func { <op> <number> }
``````

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{\$/};
}
``````

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 (\$/) {
}
``````

`\$<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 }();
``````

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]>+ }
}
``````

`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'),
);
``````

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) );
}
``````

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;
``````

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!