Citizens Max
with Raku

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.