Perl Weekly Challenge: Week 274

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

I was hoping to be able to do this one as one big regexp but after a few tries I gave up and did it in stages.

This variable is for implementing rule 3 as we shall see.

my $repeat = 1;

First the sentance is split into words.

$sentance
    .words

Then, for each word that doesn't begin with a vowel the first part of rule 2 is implemented. Otherwise the word is left as is.

    .map({
        /^ (<-[AaEeIiOoUu]>) (\S+)/ ?? "$1$0" !! $_;
    })

'ma' is appended to each word (rule 1 and the second part of rule 2) and then an incrementally increasing number of 'a's for each consecutive word.

    .map({
        $_ ~ 'ma' ~ ('a' x $repeat++);
    })

Finally all the words are joined back into one sentence and printed out. If there were differing amounts of spaces between words in the original sentance, they won't be preserved. Unfortunately, I couldn't think of a simple way to do it.

    .join(q{ })
    .say;

(Full code on Github.)

This is the Perl version. It is a straightforward translation of Raku.

my $repeat = 1;

say 
    join q{ },
    map {
        $_ . 'ma' . ('a' x $repeat++);
    }
    map {
        if (/^([^AaEeIiOoUu])(\S+)/) {
            s/(\S)(\S+)/$2$1/;
        }
        $_
    }

With the one exception that we dont have .words() so split() is used instead.

    split /\s+/, $sentance;

(nFull code on Github.)

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

Most weeks I find the challenges fairly easy to comprehend, some a little easier and some a little harder, but occasionally Mohammad sends us a real head-scratcher. It took me a long time to figure out exactly what this one was asking and how to go about solving it. So much time that I missed the submission deadline. But I think I've got it now.

First I created a hash to represent the commings and goings of the buses. The keys are departure times and the values are the arrival times. Both are expressed as minutes past the hour.

my %timetable;

%timetable is populated from command-line arguments. Each one represents one bus route as a string that consists of three integers separated by spaces.

So for each of these routes, we have to split it up (with .words()) into the frequency that the bus departs each hour, the first start time in the hour the bus departs and how much time it takes to arrive at its destination.

for @routes -> $route {
    my ($freq, $start, $time) = $route.words;

A bus leaves several times an hour (every $freq minutes to be precise) so we have to add multiple entries to %timetable for each route.

    my $departure = $start;
    while $departure < 60 {
        %timetable{$departure} = $departure + $time;
        $departure += $freq;
    }
}

A sorted list of bus departure times will be needed for the next bit.

my @starts = %timetable.keys.sort({ $^a <=> $^b });

And a list to hold the results.

my @output;

Now for each minute in an hour...

for 0 .. 59 -> $minute {

We find when the next bus is by looking through @starts and finding the next departure time. $next stores the index of that departure time in @starts.

    my $next = 0;
    for @starts.keys -> $s {
        if @starts[$s] >= $minute {
            $next = $s;
            last;
        } 
    }

We also need to find the arrival time of next bus after that which is stored (the time not the index) in $later. Because we have stored an index in $next this is straigtforard. It is usually just the arrival time of the next consecutive index in @starts. There is one special case though; if $next was the last index, we have to wrap around and make $later the arrival time of the first index. Because this is in the next hour we have to add 60 to the time. This is why $later has to be a time value not an index.

    my $later =  $next == @starts.end 
        ?? %timetable{@starts[0]} + 60
        !! %timetable{@starts[$next + 1]};

If the arrival time of the $next bus is greater than $later we are better of skipping it and taking the next bus after that (sorry all these uses of next are a bit confusing I know). The minute we are on is added to @output.

    if %timetable{@starts[$next]} > $later {
        @output.push($minute);
    }
}

Once all the minutes are processed we can print @output in the style of the examples.

say q{[}, @output.join(q{, }), q{]};

(Full code on Github.)

Once again, the Perl version is a direct translation from Raku except the use of split() instead of .words().

my %timetable;

for my $route (@ARGV) {
    my ($freq, $start, $time) = split /\s+/, $route;
    my $departure = $start;
    while ($departure < 60) {
        $timetable{$departure} = $departure + $time;
        $departure += $freq;
    }
}

my @starts = sort { $a <=> $b } keys %timetable;
my @output;

for my $minute (0 .. 59) {
    my $next = 0;
    for my $s (keys @starts) {
        if ($starts[$s] >= $minute) {
            $next = $s;
            last;
        } 
    }
    my $later =  $next == (scalar @starts - 1) 
        ? $timetable{$starts[0]} + 60
        : $timetable{$starts[$next + 1]};

    if ($timetable{$starts[$next]} > $later) {
        push @output, $minute;
    }
}

say q{[}, (join q{, }, @output), q{]};

(Full code on Github.)