Goat Bus
with Raku

by Arne Sommer

Goat Bus with Raku

[294] Published 23. June 2024.

This is my response to The Weekly Challenge #274.

Challenge #274.1: Goat Latin

You are given a sentence, $sentance.

Write a script to convert the given sentence to Goat Latin, a made up language similar to Pig Latin.

Rules for Goat Latin:
  1. If a word begins with a vowel ("a", "e", "i", "o", "u"), append "ma" to the end of the word
  2. If a word begins with consonant i.e. not a vowel, remove first letter and append it to the end then add "ma"
  3. Add letter "a" to the end of first word in the sentence, "aa" to the second word, etc etc
Example 1:
Input: $sentence = "I love Perl"
Output: "Imaa ovelmaaa erlPmaaaa"
Example 2:
Input: $sentence = "Perl and Raku are friends"
Output: "erlPmaa andmaaa akuRmaaaa aremaaaaa riendsfmaaaaaa"
Example 3:
Input: $sentence = "The Weekly Challenge"
Output: "heTmaa eeklyWmaaa hallengeCmaaaa"
File: goat-latin
#! /usr/bin/env raku

unit sub MAIN ($sentence where $sentence.chars > 0);   # [1]

say $sentence.words.map( *.&goatify-word ).join(" ");  # [2]

sub goatify-word ($word is copy)                       # [3]
{
  state $add = 2;                                      # [4]

  $word = $word.substr(1) ~ $word.substr(0,1)
    if $word !~~ /^<[aeiou]>/;                         # [5]

  $word ~= "m" ~ "a" x $add++;                         # [6]

  return $word;                                        # [7]
}

[1] Ensure at least one character. Still not a real sentence, but better than nothing.

[2] Split the sentence into words (with words), apply the procedure defined below with the special call-a-function-as-a-method syntax, and finally join the words together (separated by a space) and print the result.

[3] The proceure doing the conversion, so that the program (i.e. [2]) can look like a one liner. The is copy trait is there to make it possible to change the value of the variable, which is a local copy.

[4] Use a state variable to hold the number of «a»s to add to the end of the words. Note that this approach will not support beeing used multiple times - on new sentences - in the same program,

See docs.raku.org/syntax/state for more information about the variable declarator state.

[5] If the first character (substr(0,1)) of the word is not a consonant (!~~ ^<[aeiou]>/), move the first character to the end.

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

Note that I have used the vowel definition given in the challenge. There are a lot of additional Unicode vowels (e.g. the Norwegian Æ, Ø and Å), but they will be regarded as consonants by this program. As will digits and other non-letters.

[6] Add an «m» to the end of the word, and the number of «a»s according to the $add variable.

[7] Return the modified word.

Running it:

$ ./goat-latin "I love Perl"
Imaa ovelmaaa erlPmaaaa

 $ ./goat-latin "Perl and Raku are friends"
erlPmaa andmaaa akuRmaaaa aremaaaaa riendsfmaaaaaa

$ ./goat-latin "The Weekly Challenge"
heTmaa eeklyWmaaa hallengeCmaaaa

Looking good.

Challenge #274.2: Bus Route

Several bus routes start from a bus stop near my home, and go to the same stop in town. They each run to a set timetable, but they take different times to get into town.

Write a script to find the times - if any - I should let one bus leave and catch a strictly later one in order to get into town strictly sooner.

An input timetable consists of the service interval, the offset within the hour, and the duration of the trip.

Example 1:
Input: [ [12, 11, 41], [15, 5, 35] ]
Output: [36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47]

Route 1 leaves every 12 minutes, starting at 11 minutes past the hour
(so 11, 23, ...) and takes 41 minutes. Route 2 leaves every 15 minutes,
starting at 5 minutes past (5, 20, ...) and takes 35 minutes.

At 45 minutes past the hour I could take the route 1 bus at 47 past the
hour, arriving at 28 minutes past the following hour, but if I wait for
the route 2 bus at 50 past I will get to town sooner, at 25 minutes past
the next hour.
Example 2:
Input: [ [12, 3, 41], [15, 9, 35], [30, 5, 25] ]
Output: [ 0, 1, 2, 3, 25, 26, 27, 40, 41, 42, 43, 44, 45, 46, 47, 48,
          49, 50, 51, 55, 56, 57, 58, 59 ]
File: bus-route (partial)
#! /usr/bin/env raku

unit sub MAIN (Str $routestr = "12 11 41 | 15 5 35", :v(:$verbose));   # [1]

my @routes = $routestr.split("|")>>.words>>.Numeric;                   # [2]

die "All the routes must have three integers each"
  unless all(@routes>>.elems) == 3;                                    # [3]

die "Non-negative integers only" unless all(@routes[*;*]) ~~ UInt;     # [4]

my @departures;                                                        # [5]

for @routes -> @route                                                  # [6]
{
  state $rid   = 0; $rid++;                                            # [7]
  my $interval = @route[0];                                            # [8]
  my $offset   = @route[1];                                            # [8a]
  my $duration = @route[2];                                            # [8b]

  say ": Route: $rid Interval: $interval Offset: $offset Duration: $duration"
    if $verbose;

  die "Route $rid: Interval does not comply with 60 min rule"
    unless 60 %% $interval;                                            # [9]

  die "Route $rid: Interval must be positive" unless $interval > 0;    # [10]

  my $min = $offset;                                                   # [11]

  while $min < 60                                                      # [12]
  {
    @departures.push: { dept    => $min,
                        route   => $rid,
                        arrival => $min + $duration
                      };                                               # [13]

    $min += $interval;                                                 # [14]
  }
}

my @sorted = @departures.sort({ $^a<dept>    <=> $^b<dept> ||         # [15]
                                $^a<arrival> <=> $^b<arrival> ||      # [15a]
                                $^a<route>   <=> $^b<route> });       # [15b]

if $verbose
{
  for @sorted -> %dep
  {
    say ": Departure at { %dep<dept>.fmt('%02d') }: route %dep<route> \
      arrive at %dep<arrival>";
  }
}

exit if $verbose && $verbose eq '2';	                               # [v]

[1] Note the default routes, given on my matrix-as-a-string format.

[2] Split the route(s) string into a matrix. The >>.Numeric part will get rid of the IntStr type for us (which would have made verbose output ugly), and will enforce that we only get numeric values.

[3] All the rows (each one is a route) must have three elements.

[4] Ensure that the matrix contains unsigned integers (the UInt type) only.

[5] The departures will end up here.

[6] Iterate over the routes (rows in the matrix).

[7] The route id, from 1 and counting. This is only used by verbose mode, and sorting (which is only relevent for verbose mode).

[8] The interval comes first, then the offset [8a] and finally the duration [8b].

[9] This will ensure that the second hour (and so on) will fit in just after the first one, with the same interval between all the departures - and the same departure times.

This 60 minute rule is not strictly necessary for us to answer the challenge, but it makes sense from a planning perspective. Let us say that we have an interval set to 50, with zero offset. The first departure is at 0:50, but is the second one at 1:50 (which would break the interval rule) or at 1:40 (which would make it impossible to say in advance when the bus will come without knowing which hour)?

[10] Ensure a positive interval between departures (i.e. not zero), thus avoiding an eternal loop in [12].

[11] The first departure is at the offset value.

[12] As long as we are within one hour.

[13] Add the departure to the list. I could have used a custom class instead of an hash here, but it is not worth the extra work in this case.

[14] Adjust the departure time, ready for the next iteration of [12].

[15] Now we have a list of departures, with route 1 first, followed by all ' the departures of route 2, and so on. We need to sort them, on the departure time. If there are more than one departure at the same time, sort them by arrival time (fastest bus first) [15a]. If we have several that are equally fast, sort them by route numbers [15b] to ensure the same verbose output each time.

[v] Let us run the program, up to this point and with verbose mode, to check that everything is in order:

$ ./bus-route -v=2
: Route: 1 Interval: 12 Offset: 11 Duration: 41
: Route: 2 Interval: 15 Offset: 5 Duration: 35
: Departure at 05: route 2 arrive at 40
: Departure at 11: route 1 arrive at 52
: Departure at 20: route 2 arrive at 55
: Departure at 23: route 1 arrive at 64
: Departure at 35: route 2 arrive at 70
: Departure at 35: route 1 arrive at 76
: Departure at 47: route 1 arrive at 88
: Departure at 50: route 2 arrive at 85
: Departure at 59: route 1 arrive at 100

$ ./bus-route -v=2 "12 3 41 | 15 9 35 | 30 5 25"
: Route: 1 Interval: 12 Offset: 3 Duration: 41
: Route: 2 Interval: 15 Offset: 9 Duration: 35
: Route: 3 Interval: 30 Offset: 5 Duration: 25
: Departure at 03: route 1 arrive at 44
: Departure at 05: route 3 arrive at 30
: Departure at 09: route 2 arrive at 44
: Departure at 15: route 1 arrive at 56
: Departure at 24: route 2 arrive at 59
: Departure at 27: route 1 arrive at 68
: Departure at 35: route 3 arrive at 60
: Departure at 39: route 2 arrive at 74
: Departure at 39: route 1 arrive at 80
: Departure at 51: route 1 arrive at 92
: Departure at 54: route 2 arrive at 89

Yes, everything seems to be in order.

Note the two departures at the same time (35 for the first example, and 39 for the second one). The best one (shortest) is shown first, in both cases.

Then the second half of the program:

File: bus-route (the rest)
my @skip;

for 0 .. 59 -> $min                                          # [16]
{
  my @todo = @sorted.grep( *<dept> >= $min );                # [17]

  unless @todo.elems                                         # [18]
  {
    if @skip && @skip[0] == 0                                # [19]
    {
      @skip.push: $min;                                      # [20]
      say ": Min:$min (no more departures this hour, and we skipped the \
        first one this hour) * skip" if $verbose;
    }
    elsif $verbose
    {
      say ": Min:$min (no more departures this hour, and we took the \
       first one this hour) - first";
    }
    next;                                                    # [21]
  }

  my %first   = @todo.shift;                                 # [22]
  my %wait_to = %first;                                      # [23]

  for @todo -> %dept                                         # [24]
  {
    %wait_to = %dept if %dept<arrival> < %wait_to<arrival>;  # [25]
  }

  my $ok = %first eqv %wait_to;                              # [26]

  @skip.push: $min if !$ok;                                  # [27]
  say ": Min:{ $min.fmt('%02d') }: { $min == %wait_to<dept> ?? "" !! \
    "wait to %wait_to<dept>" } take route %wait_to<route> and arrive at \
    %wait_to<arrival> { $ok ?? "- first" !! "* skip" }" if $verbose;
}

say "[{ @skip.join(", ") }]";                                # [28]

[16] Iterate over all the minutes in an hour.

[17] Only consider departures on or after that minute value.

[18] If there are no more departures this hour (as we e.g. have in the second example for minutes 55 to 59), we have to wait for the first departure of the next hour (which is the first departure of this hour, as we only consider a generic hour).

[19] Did we skip the 00 departure?

[20] If so, skip the current one as well (as we have to wait beyond the very first departure).

[21] We are done with this minute value.

[22] Get the first depature (which is a hash).

[23] We are waiting for this departure, initially set to the first departure as well.

[24] Iterate over the remaining departures.

[25] Wait for the iterated departure, if it arrives before the previously chosen one.

[26] Is it ok to take the first departure?

[27] Mark the minute as skipped if it is not ok.

[28] Print the result.

Running it:

$ ./bus-route 
[36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47]

$ ./bus-route "12 3 41 | 15 9 35 | 30 5 25"
[0, 1, 2, 3, 25, 26, 27, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51,
  55, 56, 57, 58, 59]

Looking good.

With verbose mode:

$ ./bus-route -v
: Route: 1 Interval: 12 Offset: 11 Duration: 41
: Route: 2 Interval: 15 Offset: 5 Duration: 35
: Departure at 05: route 2 arrive at 40
: Departure at 11: route 1 arrive at 52
: Departure at 20: route 2 arrive at 55
: Departure at 23: route 1 arrive at 64
: Departure at 35: route 2 arrive at 70
: Departure at 35: route 1 arrive at 76
: Departure at 47: route 1 arrive at 88
: Departure at 50: route 2 arrive at 85
: Departure at 59: route 1 arrive at 100
: Min:00: wait to 5 take route 2 and arrive at 40 - first
: Min:01: wait to 5 take route 2 and arrive at 40 - first
: Min:02: wait to 5 take route 2 and arrive at 40 - first
: Min:03: wait to 5 take route 2 and arrive at 40 - first
: Min:04: wait to 5 take route 2 and arrive at 40 - first
: Min:05:  take route 2 and arrive at 40 - first
: Min:06: wait to 11 take route 1 and arrive at 52 - first
: Min:07: wait to 11 take route 1 and arrive at 52 - first
: Min:08: wait to 11 take route 1 and arrive at 52 - first
: Min:09: wait to 11 take route 1 and arrive at 52 - first
: Min:10: wait to 11 take route 1 and arrive at 52 - first
: Min:11:  take route 1 and arrive at 52 - first
: Min:12: wait to 20 take route 2 and arrive at 55 - first
: Min:13: wait to 20 take route 2 and arrive at 55 - first
: Min:14: wait to 20 take route 2 and arrive at 55 - first
: Min:15: wait to 20 take route 2 and arrive at 55 - first
: Min:16: wait to 20 take route 2 and arrive at 55 - first
: Min:17: wait to 20 take route 2 and arrive at 55 - first
: Min:18: wait to 20 take route 2 and arrive at 55 - first
: Min:19: wait to 20 take route 2 and arrive at 55 - first
: Min:20:  take route 2 and arrive at 55 - first
: Min:21: wait to 23 take route 1 and arrive at 64 - first
: Min:22: wait to 23 take route 1 and arrive at 64 - first
: Min:23:  take route 1 and arrive at 64 - first
: Min:24: wait to 35 take route 2 and arrive at 70 - first
: Min:25: wait to 35 take route 2 and arrive at 70 - first
: Min:26: wait to 35 take route 2 and arrive at 70 - first
: Min:27: wait to 35 take route 2 and arrive at 70 - first
: Min:28: wait to 35 take route 2 and arrive at 70 - first
: Min:29: wait to 35 take route 2 and arrive at 70 - first
: Min:30: wait to 35 take route 2 and arrive at 70 - first
: Min:31: wait to 35 take route 2 and arrive at 70 - first
: Min:32: wait to 35 take route 2 and arrive at 70 - first
: Min:33: wait to 35 take route 2 and arrive at 70 - first
: Min:34: wait to 35 take route 2 and arrive at 70 - first
: Min:35:  take route 2 and arrive at 70 - first
: Min:36: wait to 50 take route 2 and arrive at 85 * skip
: Min:37: wait to 50 take route 2 and arrive at 85 * skip
: Min:38: wait to 50 take route 2 and arrive at 85 * skip
: Min:39: wait to 50 take route 2 and arrive at 85 * skip
: Min:40: wait to 50 take route 2 and arrive at 85 * skip
: Min:41: wait to 50 take route 2 and arrive at 85 * skip
: Min:42: wait to 50 take route 2 and arrive at 85 * skip
: Min:43: wait to 50 take route 2 and arrive at 85 * skip
: Min:44: wait to 50 take route 2 and arrive at 85 * skip
: Min:45: wait to 50 take route 2 and arrive at 85 * skip
: Min:46: wait to 50 take route 2 and arrive at 85 * skip
: Min:47: wait to 50 take route 2 and arrive at 85 * skip
: Min:48: wait to 50 take route 2 and arrive at 85 - first
: Min:49: wait to 50 take route 2 and arrive at 85 - first
: Min:50:  take route 2 and arrive at 85 - first
: Min:51: wait to 59 take route 1 and arrive at 100 - first
: Min:52: wait to 59 take route 1 and arrive at 100 - first
: Min:53: wait to 59 take route 1 and arrive at 100 - first
: Min:54: wait to 59 take route 1 and arrive at 100 - first
: Min:55: wait to 59 take route 1 and arrive at 100 - first
: Min:56: wait to 59 take route 1 and arrive at 100 - first
: Min:57: wait to 59 take route 1 and arrive at 100 - first
: Min:58: wait to 59 take route 1 and arrive at 100 - first
: Min:59:  take route 1 and arrive at 100 - first
[36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47]

$ ./bus-route -v "12 3 41 | 15 9 35 | 30 5 25"
: Route: 1 Interval: 12 Offset: 3 Duration: 41
: Route: 2 Interval: 15 Offset: 9 Duration: 35
: Route: 3 Interval: 30 Offset: 5 Duration: 25
: Departure at 03: route 1 arrive at 44
: Departure at 05: route 3 arrive at 30
: Departure at 09: route 2 arrive at 44
: Departure at 15: route 1 arrive at 56
: Departure at 24: route 2 arrive at 59
: Departure at 27: route 1 arrive at 68
: Departure at 35: route 3 arrive at 60
: Departure at 39: route 2 arrive at 74
: Departure at 39: route 1 arrive at 80
: Departure at 51: route 1 arrive at 92
: Departure at 54: route 2 arrive at 89
: Min:00: wait to 5 take route 3 and arrive at 30 * skip
: Min:01: wait to 5 take route 3 and arrive at 30 * skip
: Min:02: wait to 5 take route 3 and arrive at 30 * skip
: Min:03: wait to 5 take route 3 and arrive at 30 * skip
: Min:04: wait to 5 take route 3 and arrive at 30 - first
: Min:05:  take route 3 and arrive at 30 - first
: Min:06: wait to 9 take route 2 and arrive at 44 - first
: Min:07: wait to 9 take route 2 and arrive at 44 - first
: Min:08: wait to 9 take route 2 and arrive at 44 - first
: Min:09:  take route 2 and arrive at 44 - first
: Min:10: wait to 15 take route 1 and arrive at 56 - first
: Min:11: wait to 15 take route 1 and arrive at 56 - first
: Min:12: wait to 15 take route 1 and arrive at 56 - first
: Min:13: wait to 15 take route 1 and arrive at 56 - first
: Min:14: wait to 15 take route 1 and arrive at 56 - first
: Min:15:  take route 1 and arrive at 56 - first
: Min:16: wait to 24 take route 2 and arrive at 59 - first
: Min:17: wait to 24 take route 2 and arrive at 59 - first
: Min:18: wait to 24 take route 2 and arrive at 59 - first
: Min:19: wait to 24 take route 2 and arrive at 59 - first
: Min:20: wait to 24 take route 2 and arrive at 59 - first
: Min:21: wait to 24 take route 2 and arrive at 59 - first
: Min:22: wait to 24 take route 2 and arrive at 59 - first
: Min:23: wait to 24 take route 2 and arrive at 59 - first
: Min:24:  take route 2 and arrive at 59 - first
: Min:25: wait to 35 take route 3 and arrive at 60 * skip
: Min:26: wait to 35 take route 3 and arrive at 60 * skip
: Min:27: wait to 35 take route 3 and arrive at 60 * skip
: Min:28: wait to 35 take route 3 and arrive at 60 - first
: Min:29: wait to 35 take route 3 and arrive at 60 - first
: Min:30: wait to 35 take route 3 and arrive at 60 - first
: Min:31: wait to 35 take route 3 and arrive at 60 - first
: Min:32: wait to 35 take route 3 and arrive at 60 - first
: Min:33: wait to 35 take route 3 and arrive at 60 - first
: Min:34: wait to 35 take route 3 and arrive at 60 - first
: Min:35:  take route 3 and arrive at 60 - first
: Min:36: wait to 39 take route 2 and arrive at 74 - first
: Min:37: wait to 39 take route 2 and arrive at 74 - first
: Min:38: wait to 39 take route 2 and arrive at 74 - first
: Min:39:  take route 2 and arrive at 74 - first
: Min:40: wait to 54 take route 2 and arrive at 89 * skip
: Min:41: wait to 54 take route 2 and arrive at 89 * skip
: Min:42: wait to 54 take route 2 and arrive at 89 * skip
: Min:43: wait to 54 take route 2 and arrive at 89 * skip
: Min:44: wait to 54 take route 2 and arrive at 89 * skip
: Min:45: wait to 54 take route 2 and arrive at 89 * skip
: Min:46: wait to 54 take route 2 and arrive at 89 * skip
: Min:47: wait to 54 take route 2 and arrive at 89 * skip
: Min:48: wait to 54 take route 2 and arrive at 89 * skip
: Min:49: wait to 54 take route 2 and arrive at 89 * skip
: Min:50: wait to 54 take route 2 and arrive at 89 * skip
: Min:51: wait to 54 take route 2 and arrive at 89 * skip
: Min:52: wait to 54 take route 2 and arrive at 89 - first
: Min:53: wait to 54 take route 2 and arrive at 89 - first
: Min:54:  take route 2 and arrive at 89 - first
: Min:55 (no more departures this hour, and we skipped the first one this hour) * skip
: Min:56 (no more departures this hour, and we skipped the first one this hour) * skip
: Min:57 (no more departures this hour, and we skipped the first one this hour) * skip
: Min:58 (no more departures this hour, and we skipped the first one this hour) * skip
: Min:59 (no more departures this hour, and we skipped the first one this hour) * skip
[0, 1, 2, 3, 25, 26, 27, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 55, 56, 57,
  58, 59]

And that's it.