Practicing Raku Grammars On Exercism
Tags: raku, grammar, exercism, regex
Published: 2024-02-29 23:20:25

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.

```raku
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.

```raku
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`.

```raku
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).

```raku
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.

```raku
token ws { <-X>* }
```

All together this looks like:

```raku
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.

```raku
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`:
```raku
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.

```raku
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.

```raku
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.

```raku
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.

```raku
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.

```raku
token number { '-'? <[0..9]>+ }
```

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

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

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

```raku
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.

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

method TOP ($/) {
    make $<number>.&( [R∘] $<func>.map(*.made) );
}
```

`$<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](https://docs.raku.org/type/List#routine_reduce) using the [function composition](https://docs.raku.org/language/operators#infix_o,_infix_%E2%88%98) 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:

```raku
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.

```raku
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.

```raku
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.

```raku
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.

```raku
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!