by Arne Sommer

# Citizens Max with Raku

[252] Published 27. August 2023.

This is my response to The Weekly Challenge #231.

## Challenge #231.1: Min Max

You are given an array of distinct integers.

Write a script to find all elements that is neither minimum nor maximum. Return -1 if you can't.

Example 1: ```Input: @ints = (3, 2, 1, 4) Output: (3, 2) The minimum is 1 and maximum is 4 in the given array. So (3, 2) is neither min nor max. ``` Example 2: ```Input: @ints = (3, 1) Output: -1 ``` Example 3: ```Input: @ints = (2, 1, 3) Output: (2) The minimum is 1 and maximum is 3 in the given array. So 2 is neither min nor max. ```

File: min-max ```#! /usr/bin/env raku unit sub MAIN (*@ints where @ints.elems == @ints.unique.elems > 0 # [1] && all(@ints) ~~ Int); # [2] if @ints.elems < 3 # [3] { say "-1"; # [3] } else { my @min-max = @ints.grep( @ints.min < * < @ints.max ); # [4] say "({ @min-max.join(", ") })"; # [5] } ```

[1] Ensure that the values (of which there are at least 1) are distinct by comparing the number of elements of the original array with one without duplicates, courtesy of `unique`.

See docs.raku.org/routine/unique for more information about `unique`.

[2] All the values must be integers.

[3] 1 or 2 elements only? Print the «-1» error message.

[4] Get the values that are higher than the lowest value (`min`) and lower than the highest value (`max`).

See docs.raku.org/routine/min for more information about `min`.

See docs.raku.org/routine/max for more information about `max`.

[5] Print the result, comma separated and enclosed in parens.

Running it:

```\$ ./min-max 3 2 1 4 (3, 2) \$ ./min-max 3 1 -1 \$ ./min-max 2 1 3 (2) ```

Looking good.

## Challenge #231.2: Senior Citizens

You are given a list of passenger details in the form "9999999999A1122", where 9 denotes the phone number, A the sex, 1 the age and 2 the seat number.

Write a script to return the count of all senior citizens (age >= 60).

Example 1: ```Input: @list = ("7868190130M7522","5303914400F9211","9273338290F4010") Ouput: 2 The age of the passengers in the given list are 75, 92 and 40. So we have only 2 senior citizens. ``` Example 2: ```Input: @list = ("1313579440F2036","2921522980M5644") Ouput: 0 ```

File: senior-citizens-subset ```#! /usr/bin/env raku subset PassengerDetails where * ~~ /^ <[0..9]> ** 10 <[MF]> <[0..9]> ** 2 <[0..9]> ** 2 \$/; # [1] unit sub MAIN (*@list where @list.elems > 0 # [2] && all(@list) ~~ PassengerDetails, :v(:\$verbose)); my \$senior = 0; # [3] for @list -> \$passenger # [4] { my \$age = \$passenger.substr(11,2); # [5] if \$age >= 60 # [6] { say ": Passenger \$passenger (age \$age - senior)" if \$verbose; \$senior++; # [6a] } elsif \$verbose { say ": Passenger \$passenger (age \$age)"; } } say \$senior; # [7] ```

[1] A custom type (with `subset`) to ensure that we get legal passenger detail strings only.

See docs.raku.org/language/typesystem#index-entry-subset-subset for more information about `subset`.

[2] Use the custom type (from [1]), and make sure that we get at least one of them.

[3] The result (the count) will end up here.

[4] Iterate over the passengers.

[5] Get the age part. It is part of a longer string (also called a substring) at a fixed location, so `substr` does the trick.

See docs.raku.org/routine/substr for more information about `substr`.

[6] Increase the counter if we have a senior citizen.

[7] Print the result.

Running it:

```\$ ./senior-citizens-subset 7868190130M7522 5303914400F9211 9273338290F4010 2 \$ ./senior-citizens-subset 1313579440F2036 2921522980M5644 0 ```

Looking good.

With verbose mode:

```\$ ./senior-citizens-subset -v 7868190130M7522 5303914400F9211 9273338290F4010 : Passenger 7868190130M7522 (age 75 - senior) : Passenger 5303914400F9211 (age 92 - senior) : Passenger 9273338290F4010 (age 40) 2 \$ ./senior-citizens-subset -v 1313579440F2036 2921522980M5644 : Passenger 1313579440F2036 (age 20) : Passenger 2921522980M5644 (age 56) 0 ```

The custom type (`subset`) is only used to validate the input, but we can use it (or rather a grammarified version of it) to retrieve the values (or rather, the age):

File: senior-citizens-grammar ```#! /usr/bin/env raku unit sub MAIN (*@list where @list.elems > 0, :v(:\$verbose)); grammar PassengerDetails # [1] { token TOP { ^ <phone> <sex> <age> <seat> \$ } # [2] token phone { <[0..9]> ** 10 } token sex { <[MF]> } token age { <[0..9]> ** 2 } token seat { <[0..9]> ** 2 } } my \$senior = 0; for @list -> \$passenger { my \$p = PassengerDetails.parse(\$passenger) # [3] || die "Illegal argument \$passenger"; my \$age = \$p<age>; # [4] if \$age >= 60 { say ": Passenger \$passenger (age \$age - senior)" if \$verbose; \$senior++; } elsif \$verbose { say ": Passenger \$passenger (age \$age)"; } } say \$senior; ```

[1] A `grammar` set up with the fields that the strings must contain.

[2] The «TOP» token is the entry point. The `token` keyword is one of three we can use in Grammars; the others are `regex` and `rule`.

[3] Parse the passenger details, with the grammer from [1]. Terminate the program if the input did not match.

[4] Retrieve the «age» part.

Running it gives the expected result, but input with errors are not catched by the `where` clause, as we got rid of the custom type.

```\$ ./senior-citizens-grammar -v 7868190130M7522 5303914400F9211 9273338290F4010 : Passenger 7868190130M7522 (age 75 - senior) : Passenger 5303914400F9211 (age 92 - senior) : Passenger 9273338290F4010 (age 40) 2 \$ ./senior-citizens-grammar -v 7868190130M7522 5303914400F9211 9273338290F4010x : Passenger 7868190130M7522 (age 75 - senior) : Passenger 5303914400F9211 (age 92 - senior) Illegal argument 9273338290F4010x in sub MAIN at ./senior-citizens-grammar line 20 in block <unit> at ./senior-citizens-grammar line 1 ```

The result (and downside) is that the error message comes after a lot of verbose output. That is not very nice.

We can fix that, by using the grammar in the `where` clause, but it requires a lot of extra hoops:

• We are going to use the grammar in the `where` clause, so we have to define said grammar before the `MAIN` thingy
• We cannot use `unit sub` after we have started writing code, so we have to remove the `unit` part and explicitly add the formerly implied block
• We cannot smartmatch against a grammer (with the handy `all` junction), but a `map` and the `parse` method on each one will do the job. Almost. The result of the `map` part is coerced to a Boolean value (by the `&&` operator), and that will blow up the program (with the very unhelpful «This type cannot unbox to a native integer: P6opaque, PassengerDetails» error message. The remedy: coerce the parse objects to a Boolean value (with `so`)

See docs.raku.org/routine/so for more for more information about the Boolean Context Operator `so`.

File: senior-citizens ```#! /usr/bin/env raku grammar PassengerDetails { token TOP { ^ <phone> <sex> <age> <seat> \$ } token phone { <[0..9]> ** 10 } token sex { <[MF]> } token age { <[0..9]> ** 2 } token seat { <[0..9]> ** 2 } } sub MAIN (*@list where @list.elems > 0 && @list.map( so PassengerDetails.parse(*) ), :v(:\$verbose)) { my \$senior = 0; for @list -> \$passenger { my \$p = PassengerDetails.parse(\$passenger); my \$age = \$p<age>; if \$age >= 60 { say ": Passenger \$passenger (age \$age - senior)" if \$verbose; \$senior++; } elsif \$verbose { say ": Passenger \$passenger (age \$age)"; } } say \$senior; } ```

Running it gives the expected result, with legal as well as illegal input:

```\$ ./senior-citizens -v 7868190130M7520 5303914400F9211 9273338290F4010 : Passenger 7868190130M7520 (age 75 - senior) : Passenger 5303914400F9211 (age 92 - senior) : Passenger 9273338290F4010 (age 40) 2 \$ ./senior-citizens -v 7868190130M7520 5303914400F9211 9273338290F4010xc Usage: ./senior-citizens [-v|--verbose[=Any]] [<list> ...] ```

And that's it.