I mean, with Perl Weekly Challenge 348‘s tasks being “String Alike” and “Convert Time”, how could I pick anything else besides Time?
Though I was amused by Mohammad Sajid Anwar initially mistyping the second task as “COVERT Time”, and I thought it would be a spy task…
Task 1: String Alike
You are given a string of even length.
Write a script to find out whether the given string can be split into two halves of equal lengths, each with the same non-zero number of vowels.
Example 1
Input: $str = "textbook"
Output: false
1st half: "text" (1 vowel)
2nd half: "book" (2 vowels)
Example 2
Input: $str = "book"
Output: true
1st half: "bo" (1 vowel)
2nd half: "ok" (1 vowel)
Example 3
Input: $str = "AbCdEfGh"
Output: true
1st half: "AbCd" (1 vowel)
2nd half: "EfGh" (1 vowel)
Example 4
Input: $str = "rhythmmyth"
Output: false
1st half: "rhyth" (0 vowel)
2nd half: "mmyth" (0 vowel)
Example 5
Input: $str = "UmpireeAudio"
Output: false
1st half: "Umpire" (3 vowels)
2nd half: "eAudio" (5 vowels)
Approach
Finding the answer is a two step process: first, dividing the string into two equal halves, and second, counting the vowels in each half.
Raku
I decided I wanted to encapsulate the vowel counting & word inflection because it’s otherwise repeated code. Because I love Raku’s Str.comb routine, I decided to put the vowels in an array rather than put them into a new string.
sub countVowels($str) {
my @vowels = $str.lc.comb(/<[aeiou]>/);
my $count = @vowels.elems;
my $vowel = 'vowel' ~ ($count == 1 ?? '' !! 's');
return ($count, $vowel);
}
sub stringAlike($str) {
my $explain;
# split the string
my $halfLen = $str.chars / 2;
my $first = $str.substr(0..($halfLen-1));
my $second = $str.substr($halfLen);
# count the vowels
my ($count1, $vowel1) = countVowels($first);
$explain = qq{1st half: "$first" ($count1 $vowel1)\n};
my ($count2, $vowel2) = countVowels($second);
$explain ~= qq{2nd half: "$second" ($count2 $vowel2)};
# return the result
my $bool = $count1 == $count2 && $count1 > 0;
return "$bool\n\n$explain";
}View the entire Raku script for this task on GitHub.
$ raku/ch-1.raku
Example 1:
Input: $str = "textbook"
Output: False
1st half: "text" (1 vowel)
2nd half: "book" (2 vowels)
Example 2:
Input: $str = "book"
Output: True
1st half: "bo" (1 vowel)
2nd half: "ok" (1 vowel)
Example 3:
Input: $str = "AbCdEfGh"
Output: True
1st half: "AbCd" (1 vowel)
2nd half: "EfGh" (1 vowel)
Example 4:
Input: $str = "rhythmmyth"
Output: False
1st half: "rhyth" (0 vowels)
2nd half: "mmyth" (0 vowels)
Example 5:
Input: $str = "UmpireeAudio"
Output: False
1st half: "Umpire" (3 vowels)
2nd half: "eAudio" (5 vowels)Perl
The Perl solution is just like the Raku solution, except I kept the vowels in a string instead of splitting them into an array. I also got to flex a little by assigning $str to $vowels and then stripping all the non-vowels from $vowels without touching $str.
sub countVowels($str) {
(my $vowels = $str) =~ s/[^aeiou]//ig;
my $count = length($vowels);
my $vowel = 'vowel' . ($count == 1 ? '' : 's');
return ($count, $vowel);
}
sub stringAlike($str) {
my $explain;
# split the string
my $halfLen = length($str) / 2;
my $first = substr($str, 0, $halfLen);
my $second = substr($str, $halfLen);
# count the vowels
my ($count1, $vowel1) = countVowels($first);
$explain = qq{1st half: "$first" ($count1 $vowel1)\n};
my ($count2, $vowel2) = countVowels($second);
$explain .= qq{2nd half: "$second" ($count2 $vowel2)};
# return the result
my $bool = $count1 == $count2 && $count1 > 0 ? 'True' : 'False';
return "$bool\n\n$explain";
}View the entire Perl script for this task on GitHub.
Python
By the time I wrote the Python solution, I decided I didn’t need to make a copy of the string I was stripping non-vowels from because… well, it’s already a copy. I could just modify it in place.
import re
def countVowels(text):
text = re.sub(r'[^aeiou]', '', text, flags=re.IGNORECASE)
count = len(text)
vowel = "vowel" if count == 1 else "vowels"
return count, vowel
def string_alike(text):
# split the string
half_len = int(len(text) / 2)
first = text[0:half_len]
second = text[half_len:]
# count the vowels
count1, vowel1 = countVowels(first)
explain = f'1st half: "{first}" ({count1} {vowel1})\n'
count2, vowel2 = countVowels(second)
explain += f'2nd half: "{second}" ({count2} {vowel2})'
# return the result
bool = "True" if count1 == count2 and count1 > 0 else "False"
return f'{bool}\n\n{explain}'View the entire Python script for this task on GitHub.
Elixir
Elixir let me use the same i flag for case insensitivity Perl did, and Elixir’s ternary-like construct var = if condition, do: True, else: False looks very much like Python’s.
def count_vowels(str) do
str = str |> String.replace(~r/[^aeiou]/i, "")
count = String.length(str)
vowels = if count == 1, do: "vowel", else: "vowels"
{ count, vowels }
end
def string_alike(str) do
# split the string
half_len = Integer.floor_div(String.length(str), 2)
first = String.slice(str, 0, half_len)
second = String.slice(str, half_len, half_len)
# count the vowels
{count1, vowel1} = count_vowels(first)
explain = "1st half: \"#{first}\" (#{count1} #{vowel1})\n"
{count2, vowel2} = count_vowels(second)
explain = explain
<> "2nd half: \"#{second}\" (#{count2} #{vowel2})"
# return the result
bool = if count1 == count2 and count1 > 0, do: "True",
else: "False"
"#{bool}\n\n#{explain}"
endView the entire Elixir script for this task on GitHub.
Task 2: Convert Time
You are given two strings, $source and $target, containing time in 24-hour time form.
Write a script to convert the source into target by performing one of the following operations:
1. Add 1 minute
2. Add 5 minutes
3. Add 15 minutes
4. Add 60 minutes
Find the total operations needed to get to the target.
Example 1
Input: $source = "02:30"
$target = "02:45"
Output: 1
Just one operation i.e. "Add 15 minutes".
Example 2
Input: $source = "11:55"
$target = "12:15"
Output: 2
Two operations i.e. "Add 15 minutes" followed by "Add 5 minutes".
Example 3
Input: $source = "09:00"
$target = "13:00"
Output: 4
Four operations of "Add 60 minutes".
Example 4
Input: $source = "23:45"
$target = "00:30"
Output: 3
Three operations of "Add 15 minutes".
Example 5
Input: $source = "14:20"
$target = "15:25"
Output: 2
Two operations, one "Add 60 minutes" and one "Add 5 minutes".
Approach
There are two parts to this: first, finding the number of minutes between $source and $target, and then breaking that into chunks of 60, 15, 5, or 1 minutes. There is one wrinkle, though, and example 4 shows it: the value for $source appears to be earlier than the value for $target. Now, it’s pretty easy to guess that’s because $target is a timestamp for the next day, but we’ll need to take that into account.
Raku
In Raku, I decided to let Raku’s DateTime class do the “how many minutes between” calculations for me. Because I needed a particular date for these times, I picked Jan 1, 2000, since that would work as well as any other date. Dater creating DateTime objects for the source and target times, I test to see if the target is less than the source; if it is, the time must be on the next day, so I add a day to it. After that, subtracting the source from the target gets me the number of seconds between them, and dividing by 60 gets me the number of minutes.
Then I need to find how many operations are required. Starting with the longest duration, I loop through the operations and compute the difference div the operation; if it isn’t 0, I push that many of the operation onto a list of operations, then reduce the difference by that many of the operation. If there’s still a difference left, keep looping through the operators.
As with the first task, I’m inflecting the words “minute” and “operation” in the output, which I tweaked to no longer have word numbers, instead just listing the operations.
sub convertTime($source, $target) {
# find difference between $source and $target
my $s = DateTime.new("2000-01-01T$source:00");
my $t = DateTime.new("2000-01-01T$target:00");
if ($t < $s) { # it must be a time in the next day
$t = $t.later(days => 1);
}
my $diff = ($t - $s) / 60; # (t-s) is in seconds
# find how many operations needed
my @ops;
for [60, 15, 5, 1] -> $op {
if (my $n = $diff div $op) {
my $min = $op == 1 ?? "minute" !! "minutes";
@ops.push("Add $op $min") for 1..$n;
$diff -= $n * $op;
last unless $diff > 0;
}
}
my $count = @ops.elems;
my $operations = $count == 1 ?? "Operation" !! "Operations";
return "$count\n\n$operations:\n + {@ops.join(qq/\n + /)}";
}View the entire Raku script for this task on GitHub.
$ raku/ch-2.raku
Example 1:
Input: $source = "02:30"
$target = "02:45"
Output: 1
Operation:
+ Add 15 minutes
Example 2:
Input: $source = "11:55"
$target = "12:15"
Output: 2
Operations:
+ Add 15 minutes
+ Add 5 minutes
Example 3:
Input: $source = "09:00"
$target = "13:00"
Output: 4
Operations:
+ Add 60 minutes
+ Add 60 minutes
+ Add 60 minutes
+ Add 60 minutes
Example 4:
Input: $source = "23:45"
$target = "00:30"
Output: 3
Operations:
+ Add 15 minutes
+ Add 15 minutes
+ Add 15 minutes
Example 5:
Input: $source = "14:20"
$target = "15:25"
Output: 2
Operations:
+ Add 60 minutes
+ Add 5 minutesPerl
In Perl, I’m doing the same thing using the core module Time::Piece. However, because this blog doesn’t display long lines well, I spun the actual call to Time::Piece->strptime into a mytime() function so the lines stay short. The rest of it is pretty much the same.
use Time::Piece;
use Time::Seconds qw( ONE_DAY );
sub mytime($str) {
Time::Piece->strptime("2000-01-01T$str", "%Y-%m-%dT%H:%M");
}
sub convertTime($source, $target) {
# find difference between $source and $target
my $s = mytime($source);
my $t = mytime($target);
if ($t < $s) { # it must be a time in the next day
$t += ONE_DAY;
}
my $diff = ($t - $s) / 60; # (t-s) is in seconds
# find how many operations needed
my @ops;
foreach my $op (60, 15, 5, 1) {
if (my $n = int($diff / $op)) {
my $min = $op == 1 ? "minute" : "minutes";
push @ops, "Add $op $min" for 1..$n;
$diff -= $n * $op;
last unless $diff > 0;
}
}
my $count = scalar(@ops);
my $operations = $count == 1 ? "Operation" : "Operations";
return "$count\n\n$operations:\n + @{[join(qq/\n + /, @ops)]}";
}View the entire Perl script for this task on GitHub.
Python
In Python, we’re using the datetime and timedelta objects from the datetime module. Again, the code to instantiate a datetime object is long enough I put it in its own function. Also, when you subtract a datetime object from anothe datetime object, you get back a timedelta object, so on line 13 I’m taking that object and calling the .seconds method on it to get back the number of seconds. I would have loved to have had a .minutes method, but my choices were .seconds, .days, and .microseconds.
from datetime import datetime, timedelta
def mytime(t):
return datetime.strptime(f'2000-01-01T{t}', '%Y-%m-%dT%H:%M')
def convert_time(source, target):
s = mytime(source)
t = mytime(target)
if t < s: # it must be a time in the next day
t += timedelta(days = 1) # add 1 day
diff = int((t - s).seconds / 60)
ops = []
for op in [60, 15, 5, 1]:
n = diff // op
if n:
minutes = "minute" if op == 1 else "minutes"
for i in range(n): ops.append(f'Add {op} {minutes}')
diff -= n * op
if diff == 0: break
count = len(ops)
operations = "Operation" if count == 1 else "Operations"
return f'{count}\n\n{operations}:\n + {"\n + ".join(ops)}'View the entire Python script for this task on GitHub.
Elixir
The Elixir solution was fun because I’d never played with time before, only dates. I found out that I could instantiate DateTime objects, use add/4 to add days to them, and use diff/3 to get the difference in minutes!
As usual, when I want to do a loop that I can bail out of early, I’m using recursion. Note we have two different stopping cases on lines 8 & 9: when we exhaust the list of operations, and when the difference is 0.
def mytime(t) do
{_, dt, _} = DateTime.from_iso8601("2000-01-01T#{t}:00Z")
dt
end
def convert_time([], _, ops), do: ops
def convert_time(_, diff, ops) when diff == 0, do: ops
def convert_time([op | rest], diff, ops) do
n = Integer.floor_div(diff, op)
{diff, ops} = cond do
n > 0 ->
minutes = if op == 1, do: "minute", else: "minutes"
{
diff - n * op,
ops ++ Enum.map(
Range.to_list(1..n),
fn _ -> "Add #{op} #{minutes}" end
)
}
true -> {diff, ops}
end
convert_time(rest, diff, ops)
end
def convert_time(source, target) do
s = mytime(source)
t = mytime(target)
t = if t < s, # it must be a time in the next day
do: DateTime.add(t, 1, :day), else: t
diff = DateTime.diff(t, s, :minute)
ops = convert_time([60, 15, 5, 1], diff, [])
count = length(ops)
operations = if count == 1, do: "Operation",
else: "Operations"
"#{count}\n\n#{operations}:\n + " <> Enum.join(ops, "\n + ")
endView the entire Elixir script for this task on GitHub.
Here’s all my solutions in GItHub: https://github.com/packy/perlweeklychallenge-club/tree/master/challenge-348/packy-anderson