Perl Weekly Challenge: Bus Route, Bus Goat, Under My Umbrellamaaaaaaaaaaaa

I’m sorry for the strange musical free association. but … 🎶 thinking of a sweet romance beginning in a queue… 🎶 there’s a reason this song is not very far away in my programming-addled brain.

So let’s take a ride down to Perl Weekly Challenge 274!

Task 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"

Approach

The approach for this is straightforward: split the string on whitespace, loop over the words, check whether the first character of the word is a vowel using a regex, if so, append "ma", otherwise move the first letter of the word to the end of the word and then append "ma". And then add an ever-lengthening number of "a"s to each word before slapping it back into a single string.

Raku

Of course, after realizing how to use .comb better last week, I have to use it this week to split the string into non-whitespace chunks. I needed to consult the documentation for the syntax to make the iteration variable in for writable, though.

my $startsWithVowel = rx/ :i ^<[aeiou]> /;

sub goatLatin($sentence) {
  my @words = $sentence.comb(/\S+/);
  my $a_count = 1;
  for @words <-> $word {
    if ($word ~~ $startsWithVowel) {
      $word ~= 'ma';
    }
    else {
      my $first = $word.substr(0,1);
      my $rest  = $word.substr(1);
      $word = $rest ~ $first ~ 'ma';
    }
    $word ~= 'a' x $a_count++;
  }
  return @words.join(' ');
}
$ raku/ch-1.raku
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"

Example 4:
Input: $sentence = "Bus stop Bus goes She stays Love grows Under my umbrella"
Output: "usBmaa topsmaaa usBmaaaa oesgmaaaaa heSmaaaaaa tayssmaaaaaaa oveLmaaaaaaaa rowsgmaaaaaaaaa Undermaaaaaaaaaa ymmaaaaaaaaaaa umbrellamaaaaaaaaaaaa"

View the entire Raku script for this task on GitHub.

Perl

As the example says, Perl and Raku are friends (sister languages, really), so the Perl solution is practically the same.

my $startsWithVowel = qr/ ^[aeiou] /ix;

sub goatLatin($sentence) {
  my @words = split /\s+/, $sentence;
  my $a_count = 1;
  foreach my $word ( @words ) {
    if ($word =~ /$startsWithVowel/) {
      $word .= 'ma';
    }
    else {
      my $first = substr($word, 0, 1);
      my $rest  = substr($word, 1);
      $word = $rest . $first . 'ma';
    }
    $word .= 'a' x $a_count++;
  }
  return join(' ', @words);
}

View the entire Perl script for this task on GitHub.

Python

Python added a wrinkle: there isn’t (that I know of) an easy way to modify the iteration variable in a for loop, so I just created a new array to contain the modified words as I looped through the source words. Of course, I just love the syntax of var[start:len] to access substrings…

import re

startsWithVowel = re.compile(r'^[aeiou]', re.I)

def goatLatin(sentence):
  words = sentence.split()
  new_words = []
  a_count = 1
  for word in words:
    if re.search(startsWithVowel, word):
      new_words.append(word + 'ma')
    else:
      first = word[0:1]
      rest  = word[1:]
      new_words.append(rest + first + 'ma')
    new_words[-1] += 'a' * a_count
    a_count += 1
  return ' '.join(new_words)

View the entire Python script for this task on GitHub.

Elixir

The goatLatin/1 function itself is pretty, because the pipe operator allows us to take the output of one function and send it to the next one. So the really ugly

Enum.join(addAs(Enum.map(String.split(sentence), &goatWord/1), [], 1), " ")

(Split sentence and feed that list into a map calling function goatWord which then produces a list fed into addAs) becomes the much easier to read

String.split(sentence)
    |> Enum.map(&goatWord/1)
    |> addAs([], 1)
    |> Enum.join(" ")

And I’m using my usual trick of the recursive function that returns whatever result it produces once it’s exhausted the input list. Another thing to point out is the String.slice/2 syntax, which uses the first..last//step syntax for specifying which characters to select, in this case, starting with the character at position 1 and continuing until the last character (-1).

  @startsWithVowel Regex.compile!("^[aeiou]", "i")
  
  def addAs([], list, _), do: list

  def addAs([first | rest], list, a_count) do
    list = list ++ [ first <> String.duplicate("a", a_count) ]
    addAs(rest, list, a_count + 1)
  end

  def goatWord(word) do
    if Regex.run(@startsWithVowel, word) do
      word <> "ma"
    else
      first = String.first(word)
      rest  = String.slice(word, 1 .. -1//1)
      rest <> first <> "ma"
    end
  end

  def goatLatin(sentence) do
    String.split(sentence)
    |> Enum.map(&goatWord/1)
    |> addAs([], 1)
    |> Enum.join(" ")
  end

View the entire Elixir script for this task on GitHub.


Task 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 ]

Approach

Ok, this is not a trivial problem. To attack it, I divided it up into four chunks:

  1. Determine the arrival times (in minutes after the hour of departure) for each departure for each route. So if a departure occurs at 23 minutes after the hour and the trip takes 41 minutes, the arrival time will be 64 minutes after the hour. This will make it easy to compare arrival times.
  2. Merge the arrival times into a large hashmap of departure time => arrival time; if two routes depart at the same time, keep whichever route has the earlier arrival (because the problem is concerned with catching a “strictly later” bus).
  3. Loop through the departures and find which ones have later departures that provide earlier arrivals, add these to a list of bad departure times.
  4. Loop through the minutes in the hour to determine which ones are between the last good departure time and a bad departure time.

Raku

To be honest, I worked out the approach while writing the Raku code. I knew I could just loop through the returned hash from routeTimes() using .kv, so I didn’t need to store that return hash anywhere. And the last part (looping through the minutes) came after I had my list of “bad departure times” and I needed to present the output the way the problem statement asked for. In fact, it was when I looked at my output that I realized that, in order to be able to get times up to 59 minutes after the hour in my output, I needed to generate routeTimes() for one more departure into the next hour, so line 7 went from while ($start < 60) to while ($start < 60 + $interval).

sub routeTimes(@route) {
  my ($interval, $start, $duration) = @route;
  my %times;
  while ($start < 60 + $interval) {
    %times{$start} = $start + $duration;
    $start += $interval;
  }
  return %times;
}

sub faster(@routes) {
  # find the arrival times for each route's departures
  my %times;
  for @routes -> @route {
    for routeTimes(@route).kv -> $start, $arrive {
      if (%times{$start}:exists && %times{$start} > $arrive) {
        # this departure for this route will arrive sooner than
        # the departure for the earlier route, keep this one!
        %times{$start} = $arrive;
      }
      elsif (%times{$start}:!exists) {
        # no route we've seen has a departure at this time
        %times{$start} = $arrive;
      }
    }
  }

  # now look at all the departures and see if there are
  # later departures with earlier arrivals
  my @bad_starts;
  my @starts = %times.keys.sort: *.Int;
  for @starts.kv -> $pos, $start {
    for @starts[$pos+1 .. *] -> $i {
      if (%times{$i} < %times{$start} ) {
        # we found a later departure with an earlier arrival!
        @bad_starts.push($start);
      }
    }
  }
  @bad_starts = @bad_starts.unique;

  # now determine which minutes of the hour we should say
  # "skip the next bus and take the later one"
  my @skip;
  for 0 .. 59 -> $min {
    if (@starts && @bad_starts && @starts[0] == @bad_starts[0]) {
      # we're in a bad window!
      @skip.push($min);
    }
    if (@starts && @starts[0] == $min) {
      @starts.shift; # remove the start time
    }
    if (@bad_starts && @bad_starts[0] == $min) {
      @bad_starts.shift; # remove the bad start time
    }
  }
  return @skip;
}
$ raku/ch-2.raku
Example 1:
Input: [ [12, 11, 41], [15, 5, 35] ]
Output: [36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47]

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]

View the entire Raku script for this task on GitHub.

Perl

As before, being sister languages makes translating Raku to Perl very easy. In fact, it’s even easier that it used to be. With the release of Perl 5.40, iterating over multiple values at a time with for os no longer experimental. So, instead of having to render

for routeTimes(@route).kv -> $start, $arrive {
  ...
}

as

my %routeTimes = routeTimes($route);
foreach my $start ( keys %routeTimes ) {
  my $arrive = $routeTimes{$start};
  ...
}

I can now just do

foreach my($start, $arrive) ( routeTimes($route) ) {
  ...
}

I also realized that I could use List::Util’s mesh to simulate Raku’s .kv on an array:

foreach my($pos, $start) ( mesh [0 .. $#starts], \@starts ) {
  ...
}
use List::Util qw( mesh uniq );

sub routeTimes($route) {
  my ($interval, $start, $duration) = @$route;
  my %times;
  while ($start < 60 + $interval) {
    $times{$start} = $start + $duration;
    $start += $interval;
  }
  return %times;
}

sub faster(@routes) {
  # find the arrival times for each route's departures
  my %times;
  foreach my $route ( @routes ) {
    foreach my($start, $arrive) ( routeTimes($route) ) {
      if (exists $times{$start} && $times{$start} > $arrive) {
        # this departure for this route will arrive sooner than
        # the departure for the earlier route, keep this one!
        $times{$start} = $arrive;
      }
      elsif (! exists $times{$start}) {
        # no route we've seen has a departure at this time
        $times{$start} = $arrive;
      }
    }
  }

  # now look at all the departures and see if there are
  # later departures with earlier arrivals
  my @bad_starts;
  my @starts = sort { $a <=> $b } keys %times;
  foreach my($pos, $start) ( mesh [0 .. $#starts], \@starts ) {
    foreach my $i (@starts[$pos+1 .. $#starts]) {
      if ($times{$i} < $times{$start} ) {
        # we found a later departure with an earlier arrival!
        push @bad_starts, $start;
      }
    }
  }
  @bad_starts = uniq @bad_starts;

  # now determine which minutes of the hour we should say
  # "skip the next bus and take the later one"
  my @skip;
  foreach my $min ( 0 .. 59 ) {
    if (@starts && @bad_starts && $starts[0] == $bad_starts[0]) {
      # we're in a bad window!
      push @skip, $min;
    }
    if (@starts && $starts[0] == $min) {
      shift @starts; # remove the start time
    }
    if (@bad_starts && $bad_starts[0] == $min) {
      shift @bad_starts; # remove the bad start time
    }
  }
  return @skip;
}

View the entire Perl script for this task on GitHub.

Python

Things went smoothly going from Raku to Python. .items() for the dictionaries and enumerate() for the lists allowed me to do the double-value loops that Raku’s .kv allows. At first, I ran into a snag because my usual method of making a list in Python unique, list(set(listVar)), wound up un-sorting the list. At first I sorted the list, but then I realized that if I just checked to see if a start time was already in the list of bad_starts before appending it, I could avoid having duplicates in the first place.

def routeTimes(route):
  (interval, start, duration) = route
  times = {}
  while (start < 60 + interval):
    times[start] = start + duration
    start += interval
  return times

def faster(routes):
  # find the arrival times for each route's departures
  times = {}
  for route in routes:
    for start, arrive in routeTimes(route).items():
      if (start in times) and (times[start] > arrive):
        # this departure for this route will arrive sooner than
        # the departure for the earlier route, keep this one!
        times[start] = arrive
      elif (start not in times):
        # no route we've seen has a departure at this time
        times[start] = arrive

  # now look at all the departures and see if there are
  # later departures with earlier arrivals
  bad_starts = []
  starts = sorted(times.keys(), key=int)
  for pos, start in enumerate(starts):
    for i in starts[pos+1:]:
      if (times[i] < times[start]):
        # we found a later departure with an earlier arrival!
        if start not in bad_starts:
          bad_starts.append(start)

  # now determine which minutes of the hour we should say
  # "skip the next bus and take the later one"
  skip = []
  for min in range(60):
    if (starts and bad_starts and starts[0] == bad_starts[0]):
      # we're in a bad window!
      skip.append(min)
    if (starts and starts[0] == min):
      starts.pop(0) # remove the start time
    if (bad_starts and bad_starts[0] == min):
      bad_starts.pop(0) # remove the bad start time

  return skip

View the entire Python script for this task on GitHub.

Elixir

I’m holding off on my Elixir implementation, mostly because I don’t have it working yet!


Here’s all my solutions in GItHub: https://github.com/packy/perlweeklychallenge-club/tree/master/challenge-274/packy-anderson

Leave a Reply