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 ($/) {
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 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!
Top comments (0)