Perl Weekly Challenge: Ticking away the moments that make up a challenge…

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}"
end

View 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 minutes

Perl

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 + ")
end

View 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

Leave a Reply