Perl Weekly Challenge: Minute by minute, that’s how you win it!

Ok, it’s time for another Perl Weekly Challenge, but first a note about the blog title: when I first saw the name of the first task, my brain started singing the big number from Disney’s Newsies: The MusicalSeize The Day. Yes, I’m a Perl geek, but I’m also a theater geek.

So, onward with Perl Weekly Challenge 237!

Task 1: Seize The Day

Given a year, a month, a weekday of month, and a day of week (1 (Mon) .. 7 (Sun)), print the day.

Example 1

Input: Year = 2024, Month = 4, Weekday of month = 3, day of week = 2
Output: 16

The 3rd Tue of Apr 2024 is the 16th

Example 2

Input: Year = 2025, Month = 10, Weekday of month = 2, day of week = 4
Output: 9

The 2nd Thu of Oct 2025 is the 9th

Example 3

Input: Year = 2026, Month = 8, Weekday of month = 5, day of week = 3
Output: 0

There isn't a 5th Wed in Aug 2026

This reminds me a lot of challenge 227, which also involved manipulating dates: in that case, counting up the number of Friday the 13ths in a given year. For that challenge, I started at the first of the year, then found the first Friday, then rolled forward by a week and counted the number of times the day of the month was the 13th. This time, I’m starting at the first of the month that’s specified, finding the first occurrence of the specified day of the week, and then rolling forward by a week until I reach the desired weekday of the month or I roll off the end of the month.

Caveats:

  • The format of the examples is using a 0-Sunday-indexed day of the week (0 = Sun, 2 = Tue, 3 = Wed, 4 = Thu), but Time::Piece‘s wday method is 1-Sunday-indexed (1 = Sun, 3 = Tue, 4 = Wed, 5 = Thu)
  • If I want to print out the descriptive text, I probably want to use the ordinal method in Lingua::EN::Inflexion::Noun (I used the Lingua::EN::Inflexion module back in challenge 230).
# let's use the core modules for date manipulation
use Time::Piece;
use Time::Seconds qw( ONE_DAY );
use Lingua::EN::Inflexion qw( noun );

sub seizeTheDay {
  my %params = @_;
  # build the first day of the specified month
  my $start = $params{year} . '-' . $params{month} . '-01';
  # create an object for Jan 01 of the given year
  my $t = Time::Piece->strptime($start, "%Y-%m-%d")
                     ->truncate(to => 'day');
  # in Time::Piece->wday, 1 = Sun, 2 = Mon, 3 = Tue, but our
  # input is 0 = Sun, 1 = Mon, 2 = Tue, so adjust our input
  $params{day_of_week}++;

  # find the FIRST instance of the desired day of the week
  while ( $t->wday != $params{day_of_week} ) {
    $t += ONE_DAY; # add 1 day
  }

  # take note of some values that won't change
  # for our description
  my $year  = $t->year;
  my $month = $t->month;
  my $dow   = $t->wdayname;
  my $count = 1;

  my $ord_weekday_of_month = 
    noun($params{weekday_of_month})->ordinal(0);

  # now roll forward through the month until the desired
  # weekday of the month
  while (
    # we're still in the desired month
    $t->mon == $params{month}
    &&
    # we haven't reached the desired weekday of the month
    $count != $params{weekday_of_month}
  ) {
    # add a week to the date
    $t += ONE_DAY * 7;
    # add to the weekday of the month count
    $count++;
  }

  # if we rolled out of the month, return an error condition
  if ($t->mon != $params{month}) {
    return 0, "There isn't a $ord_weekday_of_month $dow "
            . "in $month $year";
  }
  else {
    # take note of what the day of the month is
    my $day = $t->day_of_month;
    my $ord_day_of_month = noun($day)->ordinal(0);
    return $day, "The $ord_weekday_of_month $dow "
               . "of $month $year is the $ord_day_of_month";
  }
}

View the entire Perl script for this task on GitHub.


In Raku, the built-in Date class is easier to deal with, and the output of the .day-of-week method matches the input we’re getting for the day of week, but…

  • There’s no built-in facility in Date to produce the English names for months and weekdays, so I loaded Tom Browder’s Date::Names module.
  • I needed a way to get ordinals from numbers, so I loaded Steve Schulze’s Lingua::EN::Numbers.
use Date::Names;
use Lingua::EN::Numbers; # for ordinal-digit()

sub seizeTheDay(
  Int :$year,
  Int :$month,
  Int :$weekday_of_month,
  Int :$day_of_week
) {
  # object for the first day of the specified month
  my $t = Date.new($year, $month, 1);

  # in Date.day-of-week, 0 = Sun, 1 = Mon, 2 = Tue,
  # which matches our input, so no adjustment is needed

  # find the FIRST instance of the desired day of the week
  while ( $t.day-of-week != $day_of_week ) {
    $t++; # add 1 day
  }

  # instantiate a Date::Names object
  my $dn = Date::Names.new;

  # take note of some values that won't change
  # for our description
  my $month_name = $dn.mon($t.month);
  my $dow        = $dn.dow($t.day-of-week);
  my $count      = 1;

  my $ord_weekday_of_month = ordinal-digit($weekday_of_month);

  # now roll forward through the month until the desired
  # weekday of the month
  while (
    # we're still in the desired month
    $t.month == $month
    &&
    # we haven't reached the desired weekday of the month
    $count < $weekday_of_month
  ) {
    # add a week to the date
    $t += 7;
    # add to the weekday of the month count
    $count++;
  }

  # if we rolled out of the month, return an error condition
  if ($t.month != $month) {
    return 0, "There isn't a $ord_weekday_of_month $dow "
            ~ "in $month $year";
  }
  else {
    # take note of what the day of the month is
    my $day = $t.day;
    my $ord_day_of_month = ordinal-digit($day);
    return $day, "The $ord_weekday_of_month $dow "
               ~ "of $month $year is the $ord_day_of_month";
  }
}

View the entire Raku script for this task in GitHub.


Python has a really robust datetime module, so of course I was going to use that, but I didn’t find anything in the standard library that generated ordinal numbers, so I went ahead and installed Savoir-faire Linux’s num2words module.

from datetime  import date, timedelta
from num2words import num2words

def seizeTheDay(year, month, weekday_of_month, day_of_week):
    """
    Function to determine, given a year, month, weekday of
    month and day of week, whether such a date exists and,
    if so, what day of the month it is.
    """
    # object for the first day of the specified month
    t = date(year, month, 1)

    # datetime.date.isoweekday returns 1 = Mon, 2 = Tue, etc.,
    # which matches our input, so no adjustment is needed

    # find the FIRST instance of the desired day of the week
    while ( t.isoweekday() != day_of_week ):
        t += timedelta(days = 1) # add 1 day

    # take note of some values that won't change
    # for our description
    month_name = t.strftime('%b')
    dow        = t.strftime('%a')
    count      = 1
  
    ord_weekday_of_month = num2words(
        weekday_of_month, to="ordinal_num"
    )

    # now roll forward through the month until the desired
    # weekday of the month
    while (
      # we're still in the desired month
      t.month == month
      and
      # we haven't reached the desired weekday of the month
      count < weekday_of_month
    ):
        # add a week to the date
        t += timedelta(days = 7)
        # add to the weekday of the month count
        count += 1

    # if we rolled out of the month, return an error condition
    if (t.month != month):
        return(
          0,
          f"There isn't a {ord_weekday_of_month} {dow} " +
          f"in {month_name} {year}"
        )
    else:
        # take note of what the day of the month is
        day = t.day
        ord_day_of_month = num2words(day, to="ordinal_num")
        return(
            day,
            f"The {ord_weekday_of_month} {dow} " +
            f"of {month_name} {year} is the {ord_day_of_month}"
        )

View the entire Python script in GitHub.


For the Java implementation, I decided to make seizeTheDay its own class, because I could then return an object that had attributes with the day and description in it. I had to search to learn how to do date manipulation, but once I found examples, it was pretty easy to grok. I couldn’t find a standard library to turn my numbers into ordinals, so I rolled my own.

I also learned that String.format() can accept C printf()-style format strings, not the weird positional ones I’d been using. That was a welcome discovery,

import java.util.Calendar;  
import java.text.SimpleDateFormat;

class seizeTheDay {
  public int    day;
  public String description;

  public seizeTheDay(
    int year,
    int month,
    int weekday_of_month,
    int day_of_week
  ) {
    // object for the first day of the specified month
    Calendar t = Calendar.getInstance();
    t.set(year, month - 1, 1);

    // find the FIRST instance of the desired day of the week
    while (t.get(Calendar.DAY_OF_WEEK) != day_of_week+1) {
      t.add(Calendar.DATE, 1);
    }

    String month_str =
      new SimpleDateFormat("MMM").format(t.getTime());
    String dow_str =
      new SimpleDateFormat("EEE").format(t.getTime());
    int count = 1;

    // now roll forward through the month until the desired
    // weekday of the month
    while (
      // we're still in the desired month
      t.get(Calendar.MONTH) == month - 1
      && 
      // we haven't reached the desired weekday of the month
      count < weekday_of_month
    ) {
      t.add(Calendar.DATE, 7);
      count++;
    }
    if (t.get(Calendar.MONTH) != month - 1) {
      this.day = 0;
      this.description = String.format(
        "There isn't a %s %s in %s %d",
        this.ord_suffix(weekday_of_month),
        dow_str,
        month_str,
        year
      );
    }
    else {
      this.day = t.get(Calendar.DATE);
      this.description = String.format(
        "The %s %s of %s %d is the %s",
        this.ord_suffix(weekday_of_month),
        dow_str,
        month_str,
        year,
        this.ord_suffix(this.day)
      );
    }
  }

  private String ord_suffix(int num) {
    // quick function to add an ordinal suffix
    // to a number
    if (num == 11 || num == 12 | num == 13) {
      return num + "th";
    }
    else {
      switch (num % 10) {
        case 1:  return num + "st";
        case 2:  return num + "nd";
        case 3:  return num + "rd";
        default: return num + "th";
      }
    }
  }
}

View the entire Java file in GitHub.


Task 2: Maximise Greatness

You are given an array of integers.

Write a script to permute the given array such that you get the maximum possible greatness.

To determine greatness, nums[i] < perm[i] where 0 <= i < nums.length

Example 1

Input: @nums = (1, 3, 5, 2, 1, 3, 1)
Output: 4

One possible permutation: (2, 5, 1, 3, 3, 1, 1) which returns 4 greatness as below:
nums[0] < perm[0]
nums[1] < perm[1]
nums[3] < perm[3]
nums[4] < perm[4]

Example 2

Input: @ints = (1, 2, 3, 4)
Output: 3

One possible permutation: (2, 3, 4, 1) which returns 3 greatness as below:
nums[0] < perm[0]
nums[1] < perm[1]
nums[2] < perm[2]

This problem looks like there are multiple ways to solve it. The simplest way would be to just make all the possible permutations of the number array and count up the “greatness” of each. But for the first example with an array of 7 items, that means generating 5040 (7!) permutations and testing each of them. There has to be a better way.

I think there is: count up how many of each number we have, and then build a permutation item by item where either the item is the smallest remaining number larger than the current item, or the smallest remaining number. In fact, when we look at the example permutations, that’s exactly how they’re structured…

In example 1, we have the list 1, 3, 5, 2, 1, 3, 1, which has three 1s, one 2, two 3s, and one 5. So when building the permutation, we pair up one of the 2s with the first 1, the 5 with the first 3, then because there’s nothing greater than 5, we use one of the 1s, then one of the 3s with the 2, the remaining 3 with the next 1, and then we have to use the remaining 1s for the rest of the list.

In example 2, we have the list 1, 2, 3, 4, which has one each of 1, 2, 3, 4, and as we build the greatest permutation, each time we pair up the smallest possible number that’s greater than the current item: 2, 3, 4, and then finally the 1, because nothing in the list was greater than 4.

So, now to implement this in Perl. My first crack looked like this:

sub greatness {
  # determine the "greatness" of a permutation
  # relative to the original array; accepts two
  # array references to do the comparison
  my ($nums, $perm) = @_;
  my $greatness = 0;
  foreach my $i ( 0 .. $#$nums ) {
    $greatness++ if $nums->[$i] < $perm->[$i];
  }
  return $greatness;
}

sub removeNumFromCount {
  # since we do this in two locations below,
  # make this a subroutine we can call;
  # accept a reference to the count hash and
  # the number being removed
  my ($num_count, $num) = @_;

  # decrement the count of that number available
  $num_count->{$num}--;

  # if there are no more of that number, remove it
  # from the %num_count hash
  delete $num_count->{$num}
    if $num_count->{$num} == 0;
}

sub greatestPermutation {
  my @nums = @_;
  # first, count up how many of each num we have
  my %num_count;
  foreach my $num ( @nums ) {
    $num_count{$num}++;
  }

  # now, build a permutation that maximizes "greatness"
  my @perm;
  NUM: foreach my $num ( @nums ) {
    my @available = sort keys %num_count;
    my $smallest_available = $available[0];
    foreach my $avail ( @available ) {
      if ( $avail > $num ) {
        # push the available number onto the permutation
        push @perm, $avail;

        removeNumFromCount(\%num_count, $avail);

        # go to the next input number
        next NUM;
      }
    }
    # we didn't find an available number larger than $num,
    # so let's put the smallest available number on @perm
    push @perm, $smallest_available;

    removeNumFromCount(\%num_count, $smallest_available);
  }

  return @perm;
}

But I was getting bothered by repeating sort keys %num_count the once per item in the input array. Sorting is a somewhat expensive operation, and really, if we’ve sorted the list of available numbers once, we don’t need to sort it again—we just need to remove numbers that aren’t available anymore, and that can be accomplished with a simple scan of the array. So I modified removeNumFromCount to accept a second reference, this time to the array I’m storing the result of sort keys %num_count in:

sub removeNumFromCount {
  # since we do this in two locations below,
  # make this a subroutine we can call;
  # accept references to the count hash and
  # the list of available numbers, and the
  # number being removed
  my ($num_count, $available, $num) = @_;

  # decrement the count of that number available
  $num_count->{$num}--;

  # if there are no more of that number, remove it
  # from the %num_count hash and the @available array
  if ( $num_count->{$num} == 0 ) {
    # remove key from the hash
    delete $num_count->{$num};
    # filter array to not include $num
    @$available = grep { $_ != $num } @$available;
  }
}

sub greatestPermutation {
  my @nums = @_;
  # first, count up how many of each num we have
  my %num_count;
  foreach my $num ( @nums ) {
    $num_count{$num}++;
  }

  # now, build a permutation that maximizes "greatness"
  my @perm;
  my @available = sort keys %num_count; # do the sort once
  NUM: foreach my $num ( @nums ) {
    my $smallest_available = $available[0];
    foreach my $avail ( @available ) {
      if ( $avail > $num ) {
        # push the available number onto the permutation
        push @perm, $avail;

        removeNumFromCount(
          \%num_count, \@available, $avail
        );

        # go to the next input number
        next NUM;
      }
    }
    # we didn't find an available number larger than $num,
    # so let's put the smallest available number on @perm
    push @perm, $smallest_available;

    removeNumFromCount(
      \%num_count, \@available, $smallest_available
    );
  }

  return @perm;
}

View the entire Perl script for this task on GitHub.


The Raku version is pretty much exactly the same, except for the syntactical differences between Perl and Raku:

sub greatness(@nums, @perm) {
  # determine the "greatness" of a permutation
  # relative to the original array; accepts two
  # arrays to do the comparison
  my $greatness = 0;
  for 0 .. @nums.elems - 1 -> $i {
    $greatness++ if @nums[$i] < @perm[$i];
  }
  return $greatness;
}

sub removeNumFromCount(%num_count, @available, $num) {
  # since we do this in two locations below,
  # make this a subroutine we can call; accept
  # the count hash and the list of available
  # numbers, and the number being removed

  # decrement the count of that number available
  %num_count{$num}--;

  # if there are no more of that number, remove it
  # from the %num_count hash and the @available array
  if ( %num_count{$num} == 0 ) {
    # remove key from the hash
    %num_count{$num}:delete;
    # filter array to not include $num
    @available = @available.grep( { $_ != $num } );
  }
}

sub greatestPermutation(@nums) {
  # first, count up how many of each num we have
  my %num_count;
  for @nums -> $num {
    %num_count{$num}++;
  }

  # now, build a permutation that maximizes "greatness"
  my @perm;
  my @available = %num_count.keys().sort(); # do the sort once
  NUM: for @nums -> $num {
    my $smallest_available = @available[0];
    for @available -> $avail {
      if ( $avail > $num ) {
        # push the available number onto the permutation
        @perm.push($avail);

        removeNumFromCount(
          %num_count, @available, $avail
        );

        # go to the next input number
        next NUM;
      }
    }
    # we didn't find an available number larger than $num,
    # so let's put the smallest available number on @perm
    push @perm, $smallest_available;

    removeNumFromCount(
      %num_count, @available, $smallest_available
    );
  }

  return @perm;
}

View the entire Raku script for this task in GitHub.


When writing the Python version for this, it occurred to me that I don’t need to remove values from num_count when their count becomes 0 anymore: because I’m just removing values from available when their count drops to 0 instead of re-populating available by sorting the keys of num_count, I don’t care if there are keys in num_count with a 0 count anymore. I’m getting my next possible values for the permutation from available. I’ve gone back and made this change to the Perl and Raku versions I checked in, but I’m keeping how I originally wrote about them above.

def greatness(nums, perm):
    """
    Function to enumerate the greatness of
    the list perm relative to the list nums
    """
    greatness_num = 0
    for i in range(0, len(nums) - 1):
        if nums[i] < perm[i]:
            greatness_num += 1
    return greatness_num

def greatestPermutation(nums):
    """
    Function to generate a permutation of the list nums
    which has the largest relative "greatness" to nums
    """

    # first, count up how many of each num we have
    num_count = {}
    for num in nums:
        num_count[num] = num_count.get(num, 0) + 1

    # now, build a permutation that maximizes "greatness"
    perm = []
    available = sorted(num_count.keys()) # only sort once
    for num in nums:
        # default to the smallest available number
        num_to_add = available[0]

        # but now look for the smallest available number
        # that's GREATER than the current number
        for avail in available:
            if avail > num:
                num_to_add = avail
                break

        # add num_to_add to the permutation
        perm.append(num_to_add)

        # decrement its count in num_count
        num_count[num_to_add] -= 1

        # if there are no more, remove it from available
        if num_count[num_to_add] == 0:
            available = [
                x for x in available if x != num_to_add
            ]

    return perm

View the entire Python script in GitHub.


This Java solution was trickier because I wound up using a lot of three-term loops which I didn’t always get right.

  public static int greatness(int[] nums, int[] perm) {
    // determine the "greatness" of a permutation
    // relative to the original array; accepts two
    // arrays to do the comparison
    int greatness_num = 0;
    for (int i = 0; i < nums.length; i++) {
      if (nums[i] < perm[i]) {
        greatness_num++;
      }
    }
    return greatness_num;
  }

  public static int[] greatestPermutation(int[] nums) {
    // first, count up how many of each num we have
    HashMap<Integer, Integer> num_count =
      new HashMap<Integer, Integer>();
    for (int i = 0; i < nums.length; i++) {
      num_count.put(
        nums[i],
        num_count.getOrDefault(nums[i], 0) + 1
      );
    }

    // make a list of the available numbers
    // to put in a permutation
    List<Integer> available =
      new ArrayList<>(num_count.keySet());
    Collections.sort(available);

    // now, build a permutation that maximizes "greatness"
    List<Integer> perm = new ArrayList<>();
    for (Integer num : nums) {
      // default to the smallest available number
      int num_to_add = available.get(0);
      for (int i = 0; i < available.size(); i++) {
        int this_num = available.get(i);
        if (num < this_num) {
          num_to_add = this_num;
          break;
        }
      }
      perm.add(num_to_add);

      // decrement the count of that number available
      num_count.put(
        num_to_add,
        num_count.get(num_to_add) - 1
      );
  
      // if there are no more of that number, remove it
      // from available list
      if ( num_count.get(num_to_add) == 0 ) {
        // filter array to not include $num
        int size = available.size();
        for (int i = 1; i < size; i++) {
          int this_num = available.get(i);
          if (num_to_add == this_num) {
            available.remove(i);
            break;
          }
        }
      }
    }

    // because we built the permutations in a List,
    // convert the list to an int array for return
    int[] perm_return = new int[perm.size()];
    for (int i = 0; i < perm.size(); i++) {
      perm_return[i] = perm.get(i);
    }
    return perm_return;
  }

View the entire Java file in GitHub.


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

Perl Weekly Challenge: Exact Change and Array Loops

It’s time for the Perl Weekly Challenge 236!


Task 1: Exact Change

You are asked to sell juice each costs $5. You are given an array of bills. You can only sell ONE juice to each customer but make sure you return exact change back. You only have $5, $10 and $20 notes. You do not have any change in hand at first.

Write a script to find out if it is possible to sell to each customers with correct change.

Example 1

Input: @bills = (5, 5, 5, 10, 20)
Output: true

From the first 3 customers, we collect three $5 bills in order.
From the fourth customer, we collect a $10 bill and give back a $5.
From the fifth customer, we give a $10 bill and a $5 bill.
Since all customers got correct change, we output true.

Example 2

Input: @bills = (5, 5, 10, 10, 20)
Output: false

From the first two customers in order, we collect two $5 bills.
For the next two customers in order, we collect a $10 bill and give back a $5 bill.
For the last customer, we can not give the change of $15 back because we only have two $10 bills.
Since not every customer received the correct change, the answer is false.

Example 3

Input: @bills = (5, 5, 5, 20)
Output: true

Ok, how to attack this problem. I’m sure there’s a clever way to figure out whether or not the given assortment of bills can produce exact change, but as always, I’m going for straightforward and easy to understand over clever. A clever solution would be worth it if we needed performance.

So, the straightforward way is to keep a count of bills we take in and then return whether we can produce exact change. If at any point we don’t have the exact change on hand for a transaction, we can bail out of the function early and return false. If we get through all the transactions and are able to produce exact change for all of them, we return true. I’m going to use a hash because we’re tracking three bill amounts separated by a bit of space, and keeping those in a numerically indexed array would yield a bunch of empty elements I’d need to deal with.

sub isExactChangePossible {
  my @bills = @_;
  my %till; # we keep the bills in a "till"
  BILLS: foreach my $collected ( @bills ) {
    # put the bill we collected in the "till"
    $till{$collected}++;

    # calculate the required change
    my $change_required = $collected - 5;

    # if we don't need to make change,
    # skip to the next bill collected!
    next BILLS unless $change_required;

    # loop through the bills we have on hand
    # in descending size (try to make change
    # with the largest bills possible)
    foreach my $bill ( reverse sort { $a <=> $b } keys %till ) {

      # as long as we have more of this bill and
      # using it would not yield TOO MUCH change
      while ($till{$bill} > 0 && $change_required - $bill >= 0) {
        # deduct the amount from the required change
        $change_required -= $bill;

        # remove the bill from the till
        $till{$bill}--;
      }

      # move on if we managed to make exact change!
      next BILLS unless $change_required;
    }

    # if we weren't able to make change, fail
    return 0 if $change_required;
  }

  # we successfully made change for all transactions!
  return 1;
}

I’m just going to link to the full Perl script in GitHub.


The Raku version is almost identical:

sub isExactChangePossible(*@bills where ($_.all ~~ Int)) {
  my %till; # we keep the bills in a "till"
  BILLS: for @bills -> $collected {
    # put the bill we collected in the "till"
    %till{$collected}++;

    # calculate the required change
    my $change_required = $collected - 5;

    # if we don't need to make change,
    # skip to the next bill collected!
    next BILLS unless $change_required;

    # loop through the bills we have on hand
    for %till.keys().sort({ .Int }).reverse() -> $bill {
      # as long as we have more of this bill and
      # using it would not yield TOO MUCH change
      while (%till{$bill} > 0 && $change_required - $bill >= 0) {
        # deduct the amount from the required change
        $change_required -= $bill;

        # remove the bill from the till
        %till{$bill}--;
      }

      # move on if we managed to make exact change!
      next BILLS unless $change_required;
    }

    # if we weren't able to make change, fail
    return 0 if $change_required;
  }
  
  # we successfully made change for all transactions!
  return 1;
}

The one thing to note is that we can just say that the items being sorted are .Int and Raku will handle the comparison. Here’s the full Raku script in GitHub.


For Python, I had to tweak my logic a little to get around not being able to continue to the next iteration of the outer for bills loop from within the inner for till loop.

def isExactChangePossible(bills):
    till = {}; # we keep the bills in a "till"
    for collected in bills:
        # put the bill we collected in the "till"
        #
        # using .get(collected, 0) yields the value in the
        # dict for the key 'collected' if it exists, or the
        # specified default (in this case, 0) if it doesn't
        till[collected] = till.get(collected, 0) + 1

        # calculate the required change
        change_required = collected - 5

        # loop through the bills we have on hand
        for bill in sorted(till, reverse=True):
            # as long as we have more of this bill and
            # using it would not yield TOO MUCH change
            while till[bill] > 0 and change_required - bill >= 0:
                # deduct the amount from the required change
                change_required -= bill

                # remove the bill from the till
                till[bill] -= 1

        # if we weren't able to make change, fail
        if change_required:
            return 0
  
    # we successfully made change for all transactions!
    return 1

Here’s the full Python script in GitHub.


And now to the Java version. It’s slightly more annoying because Java Maps aren’t native to the language, but the approach works well:

  public static boolean isExactChangePossible(int[] bills) {
    // we keep the bills in a "till"
    HashMap<Integer, Integer> till =
      new HashMap<Integer, Integer>();

    for (int collected : bills) {
      // put the bill we collected in the "till"
      //
      // using .getOrDefault(collected, 0) yields the value
      // in the map for the key 'collected' if it exists, or
      // the specified default (in this case, 0) if it doesn't
      till.put(
        collected,
        till.getOrDefault(collected, 0) + 1
      );

      // calculate the required change
      int change_required = collected - 5;

      // loop through the bills we have on hand, making sure
      // we go from largest to smallest bill
      List<Integer> keys = new ArrayList<>(till.keySet());
      Collections.sort(keys, Collections.reverseOrder());
      for (Integer bill : keys) {
        // as long as we have more of this bill and
        // using it would not yield TOO MUCH change
        while (till.get(bill) > 0 &&
               change_required - bill >= 0) {
          // deduct the amount from the required change
          change_required -= bill;

          // remove the bill from the till
          till.put(bill, till.get(bill) - 1);
        }
      }
      // if we weren't able to make change, fail
      if (change_required > 0) {
          return false;
      }
    }
    return true;
  }

Here’s the full Java script in GitHub.


Task 2: Array Loops

You are given an array of unique integers.

Write a script to determine how many loops are in the given array.

To determine a loop: Start at an index and take the number at array[index] and then proceed to that index and continue this until you end up at the starting index.

Example 1

Input: @ints = (4,6,3,8,15,0,13,18,7,16,14,19,17,5,11,1,12,2,9,10)
Output: 3

To determine the 1st loop, start at index 0, the number at that index is 4, proceed to index 4, the number at that index is 15, proceed to index 15 and so on until you're back at index 0.

Loops are as below:
[4 15 1 6 13 5 0]
[3 8 7 18 9 16 12 17 2]
[14 11 19 10]

Example 2

Input: @ints = (0,1,13,7,6,8,10,11,2,14,16,4,12,9,17,5,3,18,15,19)
Output: 6

Loops are as below:
[0]
[1]
[13 9 14 17 18 15 5 8 2]
[7 11 4 6 10 16 3]
[12]
[19]

Example 3

Input: @ints = (9,8,3,11,5,7,13,19,12,4,14,10,18,2,16,1,0,15,6,17)
Output: 1

Loop is as below:
[9 4 5 7 19 17 15 1 8 12 18 6 13 2 3 11 10 14 16 0]

So to attack this we want to loop over each item in @ints and see if a loop starts at that element. Things I thought about:

  • I was worried about loops that don’t go back to the start, but one of the assumptions is that the list is unique integers, so we’re never going to have to worry about that.
  • Double counting loops: if we start at an element that’s in a loop we’ve already counted, we shouldn’t count it again.
  • When I wanted to pass multiple types of arguments to loopExistsAt(), I decided it would be a good opportunity to use the idea of having named parameters for a Per sub by passing it a hash.
sub loopExistsAt {
  my %params = @_;
  my $ints  = $params{ints};
  my $start = $params{start};
  my $seen  = $params{seen};

  # bail early if we're in a loop we've seen before
  return if exists $seen->{$start};

  my @loop;
  my $i = $start;
  for (;;) {
    # keep track of the values in the order we visit them
    push @loop, $ints->[$i];

    # track where we've already been
    # to avoid double-counting loops
    $seen->{$i} = 1;

    # get the next index
    $i = $ints->[$i];

    # make sure the index is in bounds
    last unless $i >= 0 && $i <= $#{$ints};

    # make sure we haven't seen the index before
    last if exists $seen->{$i};
  }

  # if the last element points back to
  # the start, it's a loop!
  if ($loop[-1] == $start) {
    return @loop;
  }
  # otherwise, return an empty list
  return;
}

sub identifyLoops {
  my @ints = @_;
  my @loops;
  my %seen; # keep track of indices we've seen
            # to avoid duplicating loops
  foreach my $start ( 0 .. $#ints ) {
    my @loop = loopExistsAt(
      start => $start,
      ints  => \@ints,
      seen  => \%seen
    );
    if (@loop) {
      push @loops, \@loop;
    }
  }
  return @loops;
}

Here’s the full Perl script in GitHub.


The Raku version wound up catching on bits of my Raku-newbie knowledge:

  • When I attempted to return nothing with just return;, what I wound up returning was an Any object. If I want to return an empty list, I need to return [];
  • I had to look up how to do named parameters in Raku.
sub loopExistsAt(:@ints, :$start, :%seen) {
  # bail early if we're in a loop we've seen before
  return [] if %seen{$start}:exists;

  my @loop;
  my $i = $start;
  loop (;;) {
    # keep track of the values in the order we visit them
    push @loop, @ints[$i];

    # track where we've already been
    # to avoid double-counting loops
    %seen{$i} = 1;

    # get the next index
    $i = @ints[$i];

    # make sure the index is in bounds
    last unless $i >= 0 && $i < @ints.elems;

    # make sure we haven't seen the index before
    last if %seen{$i}:exists;
  }

  # if the last element points back to
  # the start, it's a loop!
  if (@loop[*-1] == $start) {
    return @loop;
  }
  # otherwise, return an empty list
  return [];
}

sub identifyLoops {
  my @ints = @_;
  my @loops;
  my %seen; # keep track of indices we've seen
            # to avoid duplicating loops
  for 0 .. $@ints.elems - 1 -> $start {
    my @loop = loopExistsAt(
      start => $start,
      ints  => @ints,
      seen  => %seen
    );
    if (@loop) { 
      push @loops, @loop;
    }
  }
  return @loops;
}

Here’s the full Raku script in GitHub.


Python:

def loopExistsAt(ints=[], seen={}, start=0):
    # bail early if we're in a loop we've seen before
    if start in seen:
        return []

    loop = [] # initialize an empty list to start
    i = start # initialize i to starting point
    while True:
        # keep track of the values in the order we visit them
        loop.append(ints[i])

        # track where we've already been
        # to avoid double-counting loops
        seen[i] = 1

        # get the next index
        i = ints[i]

        # make sure the index is in bounds
        if i < 0 or i >= len(ints):
            break

        # make sure we haven't seen the index before
        if i in seen:
            break

    # if the last element points back to
    # the start, it's a loop!
    if loop[-1] == start:
        return loop

    # otherwise, return an empty list
    return []

def identifyLoops(ints):
    loops = []
    seen = {}; # keep track of indices we've seen
               # to avoid duplicating loops
    for start in range(0, len(ints)):
        loop = loopExistsAt(
          start = start,
          ints  = ints,
          seen  = seen
        )
        if loop:
            loops.append(loop)
    return loops

Here’s the full Python script in GitHub.


Java:

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.stream.Collectors;

public class Ch2 {
  public static ArrayList<Integer> loopExistsAt(
    int start, int[] ints, HashMap<Integer, Integer> seen
  ) {
    // bail early if we're in a loop we've seen before
    if (seen.get(start) != null) {
      // return an empty ArrayList
      return new ArrayList<Integer>();
    }

    // initialize an empty list to start
    ArrayList<Integer> loop = new ArrayList<Integer>();
    // initialize i to starting point
    int i = start;
    while (true) {
      // keep track of the values in the order we visit them
      loop.add(ints[i]);

      // track where we've already been
      // to avoid double-counting loops
      seen.put(i, 1);

      // get the next index
      i = ints[i];

      // make sure the index is in bounds
      if (i < 0 || i >= ints.length) {
        break;
      }

      // make sure we haven't seen the index before
      if (seen.get(i) != null) {
        break;
      }
    }

    // if the last element points back to
    // the start, it's a loop!
    if (loop.get(loop.size() - 1) == start) {
        return loop;
    }

    // otherwise, return an empty ArrayList
    return new ArrayList<Integer>();
  }

  public static ArrayList<ArrayList<Integer>> identifyLoops(int[] ints) {
    ArrayList<ArrayList<Integer>> loops =
      new ArrayList<ArrayList<Integer>>();
    HashMap<Integer, Integer> seen = 
      new HashMap<Integer, Integer>();

    for (int i = 0; i < ints.length; i++) {
      ArrayList<Integer> loop = loopExistsAt(i, ints, seen);
      if (loop.size() > 0) {
        loops.add(loop);
      }
    }
    return loops;
  }

  public static String comma_joined(int[] ints) {
    // we're using it more than once, make it a method
    return Arrays.stream(ints)
                 .mapToObj(String::valueOf)
                 .collect(Collectors.joining(","));
  }

  public static void solution(int[] ints) {
    System.out.println("Input: @ints = (" + comma_joined(ints) +
                       ")");
    ArrayList<ArrayList<Integer>> loops = identifyLoops(ints);
    int count = loops.size();
    System.out.println(String.format("Output: %1$d", count));
    if (count > 0) {
      String loop_noun = (count == 1) ? "Loop" : "Loops";
      String are_verb  = (count == 1) ? "is"   : "are";
      System.out.println("\n" + loop_noun + " " + are_verb +
                         " as below:");

      for (ArrayList<Integer> loop : loops) {
        String as_list = loop.stream()
                             .map(String::valueOf)
                             .collect(Collectors.joining(" "));
        System.out.println("[" + as_list + "]");
      }
    }
  }

  public static void main(String[] args) {
    System.out.println("Example 1:");
    solution(new int[] {4,6,3,8,15,0,13,18,7,16,14,
                        19,17,5,11,1,12,2,9,10});

    System.out.println("\nExample 2:");
    solution(new int[] {0,1,13,7,6,8,10,11,2,14,16,
                        4,12,9,17,5,3,18,15,19});

    System.out.println("\nExample 3:");
    solution(new int[] {9,8,3,11,5,7,13,19,12,4,14,
                        10,18,2,16,1,0,15,6,17});
  }
}

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

Perl Weekly Challenge: Remove and Duplicate, Challenge Edition

It’s time for Perl Weekly Challenge 235!


Task 1: Remove One

You are given an array of integers.

Write a script to find out if removing ONLY one integer makes it strictly increasing order.


Example 1

Input: @ints = (0, 2, 9, 4, 6)
Output: true

Removing ONLY 9 in the given array makes it strictly increasing order.

Example 2

Input: @ints = (5, 1, 3, 2)
Output: false

Example 3

Input: @ints = (2, 2, 3)
Output: true

Ok, let’s look at this challenge. We’re looking to return true or false depending on whether removing ONLY one integer from a list makes it strictly increasing order.

So what we need to do is go through the list and keep track of how many times a value is less than or equal to the value before it.

EDITED: As James Curtis-Smith on Facebook pointed out with a counter-example, this logic won’t work. I need to see if removing the value yields a strictly increasing list. So I added a function to test if the list was strictly increasing, and leveraged that:

#!/usr/bin/env perl
 
use v5.38;

sub isStrictlyIncreasing {
  my @ints = @_;
  # get the first integer from the list
  my $last_int = shift @ints;
  # test to make sure each subsequent int is greater
  foreach my $this_int ( @ints ) {
    return 0 if $this_int <= $last_int;
    $last_int = $this_int;
  }
  return 1;
}

sub isStrictlyIncreasingExceptOne {
  my @ints = @_;

  # the list cannot be strictly increasing unless
  # there are at least two items in it
  return 0 if @ints <= 1;

  # if it's strictly increasing without removing
  # an item, it fails the test
  return 0 if isStrictlyIncreasing(@ints);

  # loop over the list by index
  for (my $i = 1; $i <= $#ints; $i++) {
    if ($ints[$i] <= $ints[$i - 1]) {
      # remove the bigger item from the list
      splice(@ints, $i-1, 1);
      # after removing the element, is 
      # the list strictly increasing?
      return isStrictlyIncreasing(@ints);
    }
  }
}

sub solution {
  my @ints = @_;
  say 'Input: @ints = (' . join(', ', @ints) . ')';
  my $output = isStrictlyIncreasingExceptOne(@ints);
  say 'Output: ' . ($output ? 'true' : 'false');
}
 
say "Example 1:";
solution(0, 2, 9, 4, 6);
 
say "\nExample 2:";
solution(5, 1, 3, 2);
 
say "\nExample 3:";
solution(2, 2, 3);

say "\nExample 4 from James Curtis-Smith:";
solution(1,2,3,4,1,2,3);

For those interested, Curtis-Smith does have a very tight, very Perlish solution posted on Facebook. Personally, I’d want to comment the heck out of it so it was clear what was happening, but it does demonstrate using array slices to solve the problem. I will admit that I borrowed the idea of doing it in two functions (one to check whether it’s in order, one to check if only one removal is needed), but I wanted my solution to be a bit easier to read.

EDITED A SECOND TIME! @SpaceLifeForm@infosec.exchange challenged me to do it without removal or recursion, and I managed to do it, but I do have to backtrack in the loop a bit:

sub isStrictlyIncreasingExceptOne {
  my @ints = @_;
  my $count = 0;

  # the index of the first int we're comparing against
  my $last_int = 0;
  LOOP: for (my $this_int = 1;
             $this_int <= $#ints;
             $this_int++) {
    unless ( $ints[$last_int] < $ints[$this_int] ) {
      return 0 if ++$count > 1;

      # if we're comparing something after the first integer,
      # move the comparison back to the previous good integer
      # and then retry the comparison
      if ($last_int > 0) {
        $last_int--;
        redo LOOP;
      }
      # if we were comparing the first two integers, $last_int
      # will become 1 and we'll compare that against $this_int
      # as 2 the next time through the loop
    }
    $last_int = $this_int;
  }
  return $count == 1;
}

I’ve already submitted the first edit, so I’m not going to redo all of the other solutions to fit this second edit. I’m just posting it to show it can be done.


The Raku version is almost identical; really the only changes were to how the list of integers was passed to functions, and the syntax of the for loop and the infix ternary operator.

#!/usr/bin/env raku
 
use v6;

sub isStrictlyIncreasing(*@ints where ($_.all ~~ Int)) {
  # get the first integer from the list
  my $last_int = shift @ints;
  for @ints -> $this_int {
    return 0 if $this_int <= $last_int;
    $last_int = $this_int;
  }
  return 1;
}

sub isStrictlyIncreasingExceptOne(*@ints where ($_.all ~~ Int)) {
  # the list cannot be strictly increasing unless
  # there are at least two items in it
  return 0 if @ints <= 1;

  # if it's strictly increasing without removing
  # an item, it fails the test
  return 0 if isStrictlyIncreasing(@ints);

  # loop over the list by index
  loop (my $i = 1; $i < @ints.elems; $i++) {
    if (@ints[$i] <= @ints[$i - 1]) {
      # remove the bigger item from the list
      @ints.splice($i-1, 1);
      # after removing the element, is 
      # the list strictly increasing?
      return isStrictlyIncreasing(@ints);
    }
  }
}

sub solution(*@ints where ($_.all ~~ Int)) {
  say 'Input: @ints = (' ~ @ints.join(', ') ~ ')';
  my $output = isStrictlyIncreasingExceptOne(@ints);
  say 'Output: ' ~ ($output ?? 'true' !! 'false');
}
 
say "Example 1:";
solution(0, 2, 9, 4, 6);
 
say "\nExample 2:";
solution(5, 1, 3, 2);
 
say "\nExample 3:";
solution(2, 2, 3);

say "\nExample 4 from James Curtis-Smith:";
solution(1,2,3,4,1,2,3);

The Python version varies a little because the list of integers is passed into my isStrictlyIncreasing function by reference, so I can’t modify the list while I’m testing it.

#!/usr/bin/env python

def isStrictlyIncreasing(ints):
    intlist = ", ".join([ str(i) for i in ints ])
    # get the first integer from the list
    last_int = ints[0]

    for this_int in ints[1:]:
        if this_int <= last_int:
            return False
        last_int = this_int
    return True

def isStrictlyIncreasingExceptOne(ints):
    # the list cannot be strictly increasing unless
    # there are at least two items in it
    if len(ints) <= 1:
        return False

    # if it's strictly increasing without removing
    # an item, it fails the test
    if isStrictlyIncreasing(ints):
        return False

    length = len(ints)
    for i in range(1, length-1):
        if ints[i] <= ints[i - 1]:
            print(f'removed {ints[i - 1]} at {i-1}')
            ints.pop(i - 1)
            return isStrictlyIncreasing(ints)

def solution(ints):
    intlist = ", ".join([ str(i) for i in ints ])
    print(f'Input: @ints = ({ intlist })')
    output = isStrictlyIncreasingExceptOne(ints)
    print(f'Output: {output}')
 
print("Example 1:")
solution([0, 2, 9, 4, 6])
 
print("\nExample 2:")
solution([5, 1, 3, 2])

print("\nExample 3:")
solution([2, 2, 3])

print("\nExample 4 from James Curtis-Smith:")
solution([1,2,3,4,1,2,3])

Now to the language I’m learning for this challenge, Java. It may seem odd that I’m teaching myself a non-Perl language for the Perl Weekly Challenge, but there’s a method to my madness: there’s a lot of Java code out there, and I want to be able to credibly work with it.

One of the things that’s different about Java is that it’s all statically typed. Not only do variables require a type, but if you’re declaring a native array of a particular type, you need to pre-define the size of the array. However, don’t worry: the language does provide for dynamically sized arrays—they’re just a special class, not a native data type.

Things I had to take note of:

  • Joining arrays of non-string values takes a little bit of work. Fortunately, the Collectors class handles this beautifully, and the third example down in the documentation perfectly demonstrates converting elements into strings and then concatenating them separated by commas.
  • Java allows you to concatenate a boolean to a string; the stringification is handled automatically.
import java.util.Arrays;
import java.util.stream.Collectors;

public class Ch1 {
  public static String joined(int[] ints) {
    // we're using it more than once, make it a method
    return Arrays.stream(ints)
                 .mapToObj(String::valueOf)
                 .collect(Collectors.joining(", "));
  }

  public static boolean isStrictlyIncreasing(int[] ints) {
    // get the first integer from the list
    int last_int = ints[0];

    // note that we start with element 1, because
    // we've already put the value of the 0th
    // element into last_int
    for (int i = 1; i < ints.length; i++) {
      if (ints[i] <= last_int) {
        return false;
      }
      last_int = ints[i];
    }
    return true;
  }

  public static boolean isStrictlyIncreasingExceptOne(int[] ints) {
    // the list cannot be strictly increasing unless
    // there are at least two items in it
    if (ints.length <= 1) {
      return false;
    }

    // if it's strictly increasing without removing
    // an item, it fails the test
    if (isStrictlyIncreasing(ints)) {
      return false;
    }

    for (int i = 1; i < ints.length; i++) {
      if (ints[i] <= ints[i-1]) {
        // make a new list to hold the list
        // with one value removed
        int[] newlist = new int[ints.length - 1];
        // copy over all but the (i-1)th element
        for (int j = 0; j < ints.length; j++) {
          if (j == i - 1) {
            continue;
          }
          if (j < i - 1) {
            newlist[j] = ints[j];
          }
          else {
            newlist[j-1] = ints[j];
          }
        }
        // now test this new list to see
        // if it's strictly increasing
        return isStrictlyIncreasing(newlist);
      }
    }
    return false;
  }


  public static void solution(int[] ints) {
    System.out.println("Input: @ints = (" + joined(ints) + ")");
    boolean output = isStrictlyIncreasingExceptOne(ints);
    System.out.println("Output: " + output);
  }

  public static void main(String[] args) {
    System.out.println("Example 1:");
    solution(new int[] {0, 2, 9, 4, 6});

    System.out.println("\nExample 2:");
    solution(new int[] {5, 1, 3, 2});

    System.out.println("\nExample 3:");
    solution(new int[] {2, 2, 3});

    System.out.println("\nExample 4 from James Curtis-Smith:");
    solution(new int[] {1,2,3,4,1,2,3});
  }
}

Task 2: Duplicate Zeros

You are given an array of integers.

Write a script to duplicate each occurrence of ZERO in the given array and shift the remaining to the right but make sure the size of array remain the same.

Example 1

Input: @ints = (1, 0, 2, 3, 0, 4, 5, 0)
Ouput: (1, 0, 0, 2, 3, 0, 0, 4)

Example 2

Input: @ints = (1, 2, 3)
Ouput: (1, 2, 3)

Example 3

Input: @ints = (0, 3, 0, 4, 5)
Ouput: (0, 0, 3, 0, 0)

This seems like a perfect solution to use Perl’s splice command. Whenever we want to duplicate a 0 at position $i, we execute splice(@ints, $i+1, 0, 0) followed by splice(@ints, -1) (which is essentially a pop(@ints)).

#!/usr/bin/env perl
 
use v5.38;

sub duplicateZeros {
  my @ints = @_;
  for (my $i = 0; $i < scalar(@ints); $i++) {
    if ($ints[$i] == 0) {
      splice(@ints, $i+1, 0, 0); # insert a 0 at $i+1
      splice(@ints, -1);         # pop the last element off @ints
      $i++; # skip over the 0 we added!
    }
  }
  return @ints;
}

sub solution {
  my @ints = @_;
  say 'Input: @ints = (' . join(', ', @ints) . ')';
  @ints = duplicateZeros(@ints);
  say 'Output: (' . join(', ', @ints) . ')';
}
 
say "Example 1:";
solution(1, 0, 2, 3, 0, 4, 5, 0);
 
say "\nExample 2:";
solution(1, 2, 3);
 
say "\nExample 3:";
solution(0, 3, 0, 4, 5);

In Raku, the C-style three statement loop is loop, not for. We need to use this form of loop if we want to modify the loop counter after we’ve inserted a 0 so we don’t processes it a second time.

#!/usr/bin/env raku
 
use v6;
 
sub duplicateZeros(*@ints where ($_.all ~~ Int)) {
  loop (my $i = 0; $i < @ints.elems; $i++) {
    if (@ints[$i] == 0) {
      @ints.splice($i+1, 0, 0); # insert a 0 at $i+1
      @ints.splice(*-1);        # pop the last element off @ints
      $i++; # skip over the 0 we added!
    }
  }
  return @ints;
}

sub solution(*@ints where ($_.all ~~ Int)) {
  say 'Input: @ints = (' ~ @ints.join(', ') ~ ')';
  @ints = duplicateZeros(@ints);
  say 'Output: (' ~ @ints.join(', ') ~ ')';
}
 
say "Example 1:";
solution(1, 0, 2, 3, 0, 4, 5, 0);
 
say "\nExample 2:";
solution(1, 2, 3);
 
say "\nExample 3:";
solution(0, 3, 0, 4, 5);

In Python, however, there isn’t a three statement loop. Also, from experience, I know if we explicitly use the array in the control of the loop, Python will complain when we modify the array. So let’s capture the length of the array first, and use a variable to track which iterations through the loop we’re skipping to pass over the 0 we added…

#!/usr/bin/env python

def duplicateZeros(ints):
    length = len(ints)
    skip_me = -1
    for i in range(0, length):
        if skip_me == i:
            continue
        if ints[i] == 0:
            ints.insert(i+1, 0) # insert a 0 at i+1
            ints.pop(-1)        # pop the last element off ints
            skip_me = i+1       # skip over the 0 we added!
    return ints

def solution(ints):
    intlist = ", ".join([ str(i) for i in ints ])
    print(f'Input: @ints = ({ intlist })')
    ints = duplicateZeros(ints)
    intlist = ", ".join([ str(i) for i in ints ])
    print(f'Output: ({ intlist })');
 
print("Example 1:")
solution([1, 0, 2, 3, 0, 4, 5, 0])
 
print("\nExample 2:")
solution([1, 2, 3])
 
print("\nExample 3:")
solution([0, 3, 0, 4, 5])

Which, finally, brings us to the Java implementation. We could have used variable-length array objects, but because the whole point of this task is that the array length stays the same, I should lean into that and just move the array elements manually and not rely on some splice method.

import java.util.Arrays;
import java.util.stream.Collectors;

public class Ch2 {
  public static String joined(int[] ints) {
    // we're using it more than once, make it a method
    return Arrays.stream(ints)
                 .mapToObj(String::valueOf)
                 .collect(Collectors.joining(", "));
  }

  public static int[] duplicateZeros(int[] ints) {
    for (int i = 0; i < ints.length; i++) {
      if (ints[i] == 0) {
        // shift all the values in the array to the right by one 
        for (int j = ints.length - 1; j > i; j--) {
          ints[j] = ints[j - 1];
        }
        ints[i + 1] = 0; // insert a new 0
        i++; // skip over the 0 we added!
      }
    }
    return ints;
  }

  public static void solution(int[] ints) {
    System.out.println("Input: @ints = (" + joined(ints) + ")");
    ints = duplicateZeros(ints);
    System.out.println("Output: (" + joined(ints) + ")");
  }

  public static void main(String[] args) {
    System.out.println("Example 1:");
    solution(new int[] {1, 0, 2, 3, 0, 4, 5, 0});

    System.out.println("\nExample 2:");
    solution(new int[] {1, 2, 3});

    System.out.println("\nExample 3:");
    solution(new int[] {0, 3, 0, 4, 5});
  }
}

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

Perl Weekly Challenge: Common, but Unequal, Triplet Characters

Ok, trying to be creative with my title for this week’s Perl Weekly Challenge 234 blog title is probably a miss. But it’s the effort that counts!


Task 1: Common Characters

You are given an array of words made up of alphabetic characters only.
Write a script to return all alphabetic characters that show up in all words including duplicates.

Example 1

Input: @words = ("java", "javascript", "julia")
Output: ("j", "a")

Example 2

Input: @words = ("bella", "label", "roller")
Output: ("e", "l", "l")

Example 3

Input: @words = ("cool", "lock", "cook")
Output: ("c", "o")

So, looking at this problem, I see that in addition to preserving duplicated characters, the sample output (I’m glad Mohammed fixed the typo “Ouput” that’s been persistent in the past few weeks) is preserving the order of the characters based on the first word in the input list.

Since I’m looking for a new coding gig, I’ve been taking some coding tests, and one of the strategies the preparations for coding tests encourages is to look for patterns in the data once you’ve done an initial pass over it. One of the things I notice in this task is that it feels like a combination of the two tasks from last the challenge. We’re splitting the words into characters, and we’re counting the frequency of those characters. Any character that has a frequency of 1 or greater in each word occurs once, any character that has a frequency of 2 or greater in each word occurs twice, and so on.

This will get us the frequencies of the characters for each word:

sub charFrequency {
  my $word = shift;
  my %freq;
  foreach my $c ( split //, $word ) {
    $freq{$c}++;
  }
  return \%freq; # return a hash REFERENCE
}

Then we need to find the common characters:

sub commonCharacters {
  my @words = @_;
  my @freq = map { charFrequency($_) } @words;
  # grab the character frequency map for the first word
  my $first = shift @freq;
  # now check the characters in the first word against
  # the characters in all the subsequent words
  foreach my $subsequent ( @freq ) {
    foreach my $c ( keys %$first ) {
      if (! exists $subsequent->{$c}) {
        # this character isn't in subsequent words,
        # so let's remove it from the frequency map
        # of the first word
        delete $first->{$c};
      }
      else {
        # the character IS in subsequent words,
        # so let's set the frequency count to be
        # the minimum count found in those words
        $first->{$c} = min($first->{$c}, $subsequent->{$c});
      }
    }
  }
}

But that’s not enough to satisfy the problem: we need to output the characters in the order they appear in the first word. So let’s add to this function:

sub commonCharacters {
  ...
  # now we generate a list of characters in the order they
  # appear in the first word
  my @output;
  # once again, loop over the characters in the first word
  foreach my $c ( split //, $words[0] ) {
    next unless exists $first->{$c};
    if ($first->{$c} > 1) {
      # there's more than one occurence, so let's decrement
      # the count for the next time through the loop
      $first->{$c}--;
    }
    else {
      # there is only one occurence left, so remove the
      # character
      delete $first->{$c};
    }
    push @output, $c;
  }
  return @output;
}

Which, as an entire script, looks like this:

#!/usr/bin/env perl

use v5.38;

use List::Util qw( min );

sub charFrequency {
  my $word = shift;
  my %freq;
  foreach my $c ( split //, $word ) {
    $freq{$c}++;
  }
  return \%freq; # return a hash REFERENCE
}

sub commonCharacters {
  my @words = @_;
  my @freq = map { charFrequency($_) } @words;
  # grab the character frequency map for the first word
  my $first = shift @freq;
  # now check the characters in the first word against
  # the characters in all the subsequent words
  foreach my $subsequent ( @freq ) {
    foreach my $c ( keys %$first ) {
      if (! exists $subsequent->{$c}) {
        # this character isn't in subsequent words,
        # so let's remove it from the frequency map
        # of the first word
        delete $first->{$c};
      }
      else {
        # the character IS in subsequent words,
        # so let's set the frequency count to be
        # the minimum count found in those words
        $first->{$c} = min($first->{$c}, $subsequent->{$c});
      }
    }
  }

  # now we generate a list of characters in the order they
  # appear in the first word
  my @output;
  # once again, loop over the characters in the first word
  foreach my $c ( split //, $words[0] ) {
    next unless exists $first->{$c};
    if ($first->{$c} > 1) {
      # there's more than one occurence, so let's decrement
      # the count for the next time through the loop
      $first->{$c}--;
    }
    else {
      # there is only one occurence left, so remove the
      # character
      delete $first->{$c};
    }
    push @output, $c;
  }
  return @output;
}

sub solution {
  my @words = @_;
  say 'Input: @words = ("' . join('", "', @words) . '")';
  my @output = commonCharacters(@words);
  say 'Output: ("' . join('", "', @output) . '")';
}

say "Example 1:";
solution("java", "javascript", "julia");

say "\nExample 2:";
solution("bella", "label", "roller");

say "\nExample 3:";
solution("cool", "lock", "cook");


Things to note in the Raku solution:

  • When splitting a string into its component characters, make sure you add the :skip-empty parameter, otherwise you’ll get leading and trailing empty character entries.
  • Deleting elements from a hash isn’t a method call, it’s a Subscript Adverb, :delete.
  • Similarly, testing for the existence of an element is the Subscript Adverb :exists.
  • If you try to use the construction ! $hash{$key}:exists, you get the error Precedence issue with ! and :exists, perhaps you meant :!exists?
#!/usr/bin/env raku

use v6;

sub charFrequency(Str $word) {
  my %freq;
  for $word.split('', :skip-empty) -> $c {
    %freq{$c}++;
  }
  return %freq;
}

sub commonCharacters(*@words where ($_.all ~~ Str)) {
  my @freq = @words.map({ charFrequency($_) });
  # grab the character frequency map for the first word
  my $first = shift @freq;
  # now check the characters in the first word against
  # the characters in all the subsequent words
  for @freq -> $subsequent {
    for $first.keys() -> $c {
      if ($subsequent{$c}:!exists) {
        # this character isn't in subsequent words,
        # so let's remove it from the frequency map
        # of the first word
        $first{$c}:delete;
      }
      else {
        # the character IS in subsequent words,
        # so let's set the frequency count to be
        # the minimum count found in those words
        $first{$c} = min($first{$c}, $subsequent{$c});
      }
    }
  }

  # now we generate a list of characters in the order they
  # appear in the first word
  my @output;
  # once again, loop over the characters in the first word
  for @words[0].split('', :skip-empty) -> $c  {
    next unless $first{$c}:exists;
    if ($first{$c} > 1) {
      # there's more than one occurence, so let's decrement
      # the count for the next time through the loop
      $first{$c}--;
    }
    else {
      # there is only one occurence left, so remove the
      # character
      $first{$c}:delete;
    }
    push @output, $c;
  }
  return @output;
}

sub solution {
  my @words = @_;
  say 'Input: @words = ("' ~ @words.join('", "') ~ '")';
  my @output = commonCharacters(@words);
  say 'Output: ("' ~ @output.join('", "') ~ '")';
}

say "Example 1:";
solution("java", "javascript", "julia");

say "\nExample 2:";
solution("bella", "label", "roller");

say "\nExample 3:";
solution("cool", "lock", "cook");

Things to note in the Python solution:

  • You don’t shift elements off the beginning of an array, you pop the 0th element.
  • You don’t push elements onto the end of an array, you append them
  • There’s a Counter type in the collections module that lets you essentially autovivify elements in a dictionary by adding to them
  • In both Perl and Raku, the keys function/method for a hash returned a list that we were then able to iterate over, so we could remove elements from the hash while we were looping over it. Not so in Python: RuntimeError: dictionary changed size during iteration. This is easily handled by making a copy of the dictionary and looping over that.
#!/usr/bin/env python

from collections import Counter

def charFrequency(word):
    # https://docs.python.org/3/library/collections.html#counter-objects
    freq = Counter()
    for c in word:
        freq[c] += 1
    return freq

def commonCharacters(words):
    # get the character freqencies for each word
    freq = list(map(charFrequency, words))

    # grab the character frequency map for the first word
    first = freq.pop(0)

    # make a copy of the dictionary since we'll
    # be modifying it in the loop
    first_orig = dict(first)

    # now check the characters in the first word against
    # the characters in all the subsequent words
    for subsequent in freq:
        for c in first_orig:
            if c not in subsequent:
                # this character isn't in subsequent words,
                # so let's remove it from the frequency map
                # of the first word
                first.pop(c)
            else:
                # the character IS in subsequent words,
                # so let's set the frequency count to be
                # the minimum count found in those words
                first[c] = min(first[c], subsequent[c])

    # now we generate a list of characters in the order they
    # appear in the first word
    output = []
    # once again, loop over the characters in the first word
    for c in words[0]:
        if c not in first:
            continue
        if first[c] > 1:
            first[c] -= 1
        else:
            first.pop(c)
        output.append(c)
    return output

def solution(words):
    quoted = '"' + '", "'.join(words) + '"'
    print(f'Input: @words = ({quoted})')
    output = commonCharacters(words)
    quoted = '"' + '", "'.join(output) + '"'
    print(f'Output: ({quoted})')

print("Example 1:")
solution(["java", "javascript", "julia"])

print("\nExample 2:")
solution(["bella", "label", "roller"])

print("\nExample 3:")
solution(["cool", "lock", "cook"])

But this does go towards demonstrating something I’ve been saying for years: Python isn’t all that different than Perl. It just makes some different decisions and tries to cut down on TMTOWTDI as much as possible.


Task 2: Unequal Triplets

You are given an array of positive integers.

Write a script to find the number of triplets (i, j, k) that satisfies num[i] != num[j], num[j] != num[k] and num[k] != num[i].

Example 1

Input: @ints = (4, 4, 2, 4, 3)
Ouput: 3

(0, 2, 4) because 4 != 2 != 3
(1, 2, 4) because 4 != 2 != 3
(2, 3, 4) because 2 != 4 != 3

Example 2

Input: @ints = (1, 1, 1, 1, 1)
Ouput: 0

Example 3

Input: @ints = (4, 7, 1, 10, 7, 4, 1, 1)
Output: 28

triplets of 1, 4, 7  = 3x2×2 = 12 combinations
triplets of 1, 4, 10 = 3×2×1 = 6  combinations
triplets of 4, 7, 10 = 2×2×1 = 4  combinations
triplets of 1, 7, 10 = 3x2x1 = 6 combinations

I think this time I’m going to break from my tradition of spitting out the exact explanatory text and just generate something that looks like the first example.

The meat of this solution is just a triple-nested loop:

sub findTriplets {
  my @ints = @_;
  my @solutions;
  foreach my $i ( 0 .. $#ints - 2 ) {
    foreach my $j ( $i+1 .. $#ints - 1 ) {
      foreach my $k ( $j+1 .. $#ints ) {
        if ($ints[$i] != $ints[$j] &&
            $ints[$j] != $ints[$k] &&
            $ints[$i] != $ints[$k]) {
          push @solutions, [$i, $j, $k];
        }
      }
    }
  }
  return @solutions;
}

The rest of the code is just formatting the results:

#!/usr/bin/env perl

use v5.38;

sub findTriplets {
  my @ints = @_;
  my @solutions;
  foreach my $i ( 0 .. $#ints - 2 ) {
    foreach my $j ( $i+1 .. $#ints - 1 ) {
      foreach my $k ( $j+1 .. $#ints ) {
        if ($ints[$i] != $ints[$j] &&
            $ints[$j] != $ints[$k] &&
            $ints[$i] != $ints[$k]) {
          push @solutions, [$i, $j, $k];
        }
      }
    }
  }
  return @solutions;
}

sub solution {
  my @ints = @_;
  say 'Input: @ints = (' . join(', ', @ints) . ')';
  my @solutions = findTriplets(@ints);
  say 'Output: ' . scalar(@solutions);
  say "" if @solutions;
  foreach my $triplet ( @solutions ) {
    my($i, $j, $k) = @$triplet;
    say "($i, $j, $k) because "
      . "$ints[$i] != $ints[$j] != $ints[$k]";
  }
}

say "Example 1:";
solution(4, 4, 2, 4, 3);

say "\nExample 2:";
solution(1, 1, 1, 1, 1);

say "\nExample 3:";
solution(4, 7, 1, 10, 7, 4, 1, 1);

And the output from the third example looks like this:

Example 3:
Input: @ints = (4, 7, 1, 10, 7, 4, 1, 1)
Output: 28

(0, 1, 2) because 4 != 7 != 1
(0, 1, 3) because 4 != 7 != 10
(0, 1, 6) because 4 != 7 != 1
(0, 1, 7) because 4 != 7 != 1
(0, 2, 3) because 4 != 1 != 10
(0, 2, 4) because 4 != 1 != 7
(0, 3, 4) because 4 != 10 != 7
(0, 3, 6) because 4 != 10 != 1
(0, 3, 7) because 4 != 10 != 1
(0, 4, 6) because 4 != 7 != 1
(0, 4, 7) because 4 != 7 != 1
(1, 2, 3) because 7 != 1 != 10
(1, 2, 5) because 7 != 1 != 4
(1, 3, 5) because 7 != 10 != 4
(1, 3, 6) because 7 != 10 != 1
(1, 3, 7) because 7 != 10 != 1
(1, 5, 6) because 7 != 4 != 1
(1, 5, 7) because 7 != 4 != 1
(2, 3, 4) because 1 != 10 != 7
(2, 3, 5) because 1 != 10 != 4
(2, 4, 5) because 1 != 7 != 4
(3, 4, 5) because 10 != 7 != 4
(3, 4, 6) because 10 != 7 != 1
(3, 4, 7) because 10 != 7 != 1
(3, 5, 6) because 10 != 4 != 1
(3, 5, 7) because 10 != 4 != 1
(4, 5, 6) because 7 != 4 != 1
(4, 5, 7) because 7 != 4 != 1

Things to note in the Raku solution:

  • Because .elems returns the number of elements in the array, we need to subtract an additional 1 to get the index of the last value.
#!/usr/bin/env raku

use v6;

sub findTriplets(@ints where ($_.all ~~ Int)) {
  my @solutions;
  for 0 .. @ints.elems - 3 -> $i {
    for $i + 1 .. @ints.elems - 2 -> $j {
      for $j + 1 .. @ints.elems - 1 -> $k {
        if (@ints[$i] != @ints[$j] &&
            @ints[$j] != @ints[$k] &&
            @ints[$i] != @ints[$k]) {
          push @solutions, [$i, $j, $k];
        }
      }
    }
  }
  return @solutions;
}

sub solution {
  my @ints = @_;
  say 'Input: @ints = (' ~ @ints.join(', ') ~ ')';
  my @solutions = findTriplets(@ints);
  say 'Output: ' ~ @solutions.elems;
  say "" if @solutions;
  for @solutions -> @triplet {
    my ($i, $j, $k) = @triplet;
    say "($i, $j, $k) because "
      ~ "@ints[$i] != @ints[$j] != @ints[$k]";
  }
}

say "Example 1:";
solution(4, 4, 2, 4, 3);

say "\nExample 2:";
solution(1, 1, 1, 1, 1);

say "\nExample 3:";
solution(4, 7, 1, 10, 7, 4, 1, 1);

Things to note in the Python solution:

  • The Python equivalent of x .. y is range(x, y)
  • You can’t just .join() a list of integers. You need to call .join() on the string you want to join them with, and convert each of the integers into strings:
    ", ".join([ str(i) for i in ints ])
    (though last week, I did it like this; ', '.join(map(lambda i: str(i), ints)))
  • Interpolating values in strings got a lot easier in Python 3.6 with the addition of f-strings.
#!/usr/bin/env python

def findTriplets(ints):
    solutions = []
    for i in range(0, len(ints) - 3 ):
        for j in range(i + 1, len(ints) - 2):
            for k in range(j + 1, len(ints) - 1):
                if (ints[i] != ints[j] and
                    ints[j] != ints[k] and
                    ints[i] != ints[k]):
                    solutions.append([i, j, k])
    return solutions

def solution(ints):
    intlist = ", ".join([ str(i) for i in ints ])
    print(f'Input: @ints = ({intlist})')
    solutions = findTriplets(ints)
    print(f'Output: {len(solutions)}')
    if solutions:
        print("")
        for triplet in solutions:
            i, j, k = triplet
            print(
                f"({i}, {j}, {k}) because " +
                f"{ints[i]} != {ints[j]} != {ints[k]}"
            )

print("Example 1:")
solution([4, 4, 2, 4, 3])

print("\nExample 2:")
solution([1, 1, 1, 1, 1])

print("\nExample 3:")
solution([4, 7, 1, 10, 7, 4, 1, 1])

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

Perl Weekly Challenge: What’s the Frequency, Kenneth?

There was no Perl Weekly Challenge last week so we jump from 231 to 233!

Both tasks this week deal with accepting lists of items and then manipulating those lists.


Task 1: Similar Words

You are given an array of words made up of alphabets only.

Write a script to find the number of pairs of similar words. Two words are similar if they consist of the same characters.

Example 1

Input: @words = ("aba", "aabb", "abcd", "bac", "aabc")
Output: 2

Pair 1: similar words ("aba", "aabb")
Pair 2: similar words ("bac", "aabc")

Example 2

Input: @words = ("aabb", "ab", "ba")
Output: 3

Pair 1: similar words ("aabb", "ab")
Pair 2: similar words ("aabb", "ba")
Pair 3: similar words ("ab", "ba")

Example 3

Input: @words = ("nba", "cba", "dba")
Output: 0

So what we’re looking for is a way to break down words into a sorted set of the characters that occur in that word so we can use that list to compare whether the words use only the same characters:

sub charsInWord {
    my $word = shift;
    # split the word into characters, then map those characters
    # to a hash
    my %charset = map { $_ => 1 } split //, $word;
    # return the set of characters as a string, sorted
    return join q{}, sort keys %charset;
}

We can then use those character set strings as the keys to a hash. By storing each word in an array referenced in a hash by the character set for that word:

my %similar;
foreach my $word ( @words ) {
  my $charset = charsInWord($word);
  push @{ $similar{$charset} }, $word;
}

Now, you might wonder why I’m not checking to see if $similar{$charset} exists already or has an array reference before just pushing a value there. It’s all through the magic of a feature of Perl called autovivification. When we attempt to access the hash %similar using the key $charset, if that key doesn’t exist, Perl will automatically create it and make it undefined. Similarly, when we try to push a value onto an array reference in a variable that is currently undefined, Perl creates an array reference and populates the variable with it. So when %similar is empty and I say
push @{ $similar{$charset} }, $word; the entry in %similar for $charset winds up containing a reference to an array with one entry: $word.

Autovivification can cause problems in your code, because undefined values in a hash can get autovivified just by referencing their keys, so if I’m ever testing to see if something is defined, I always check to see if the key exists in the array using exists rather than just testing the value of $hash{$key}. However, in this case, I’m putting values into a hash or into an array reference, so if they don’t already exist, I want to create them. If Perl didn’t have autovivification, I’d have to do this:

# if this is the first time we've seen this charset,
# create an empty arrayref to store the word in
$similar{$charset} = [] if ! exists $similar{$charset};

# append the word to the list for this charset
push @{ $similar{$charset} }, $word;

The next bit of the problem that I noticed on carefully reading the examples is it doesn’t just want a list of words using the same character set: it wants pairs of words using the same character set. So we need to take our list of words using the same character set and present it as pairs.

A little thought produced the algorithm for this. Given the list of words A, B, C, D, the list can be broken down into pairs by taking the first word off the list (A), pairing it with each of the remaining words (AB, AC, AD), then repeating the process with the shortened list (B, C, D) until we ran out of words (BC, BD, CD). This is easily done in Perl with a pair of loops:

my @pairs;
while ( scalar(@list) >= 2 ) {
  my $first = shift @list; # remove the first element
  foreach my $second ( @list ) {
    push @pairs, [ $first, $second ];
  }
}

And that pretty much does all the heavy lifting for this problem. The rest is presentation:

#!/usr/bin/env perl
  
use v5.38;

sub charsInWord {
  my $word = shift;
  # split the word into characters, then map those
  # characters to a hash so we only have unique ones
  my %charset = map { $_ => 1 } split //, $word;
  # return the set of characters as a string, sorted
  return join q{}, sort keys %charset;
}

sub findSimilarWordPairs {
  my @words = @_;

  # get the set of characters in each word,
  # store each word in an array reference under
  # the hash key for its character set
  my %similar;
  foreach my $word ( @words ) {
    my $charset = charsInWord($word);
    # if $similar{$charset} is undefined when we
    # try to use it as an array reference to store
    # a value, Perl will "autovivify" a reference
    # to an empty array
    push @{ $similar{$charset} }, $word;
  }

  # filter out character sets that only have one word
  my @multiples = grep {
    # only allow letter sets
    # that have more than one word
    scalar( @{ $similar{$_} } ) > 1
  } keys %similar;

  # make pairs by looping over the list
  # of letter sets that had multiple entries
  my @pairs;
  foreach my $charset ( @multiples ) {
    my @list = @{ $similar{$charset} };

    while ( scalar(@list) >= 2 ) {
      # remove the first word from the list of words
      my $first = shift @list;
      # pair it with each of the remaining words
      foreach my $second ( @list ) {
        push @pairs, [ $first, $second ];
      }
    }
  }
  return @pairs;
}

sub solution {
  my @words = @_;
  say 'Input: @words = ("' . join('", "', @words) . '")';

  my @pairs = findSimilarWordPairs(@words);

  say 'Output: ' . scalar(@pairs);
  my $count = 0;
  foreach my $pair ( @pairs ) {
    say "" if $count == 0;
    say 'Pair ' . ++$count . ': similar words ("'
      . join('", "', @$pair) . '")';
  }
}

say "Example 1:";
solution("aba", "aabb", "abcd", "bac", "aabc");

say "";
say "Example 2:";
solution("aabb", "ab", "ba");

say "";
say "Example 3:";
solution("nba", "cba", "dba");

In the Raku version, some of the language features allowed me to make some different choices:

sub charsInWord(Str $word) {
  # split the word into characters, then use the Raku
  # array method unique to have each character appear once.
  return $word.split('').unique.sort.join;
}

Raku having a .unique method on the array class (really, the Any class) meant I didn’t need to use a hash to get only the unique characters. Autovivification works much the same, however:

my %similar;
for @words -> $word {
  my $charset = charsInWord($word);
  %similar{$charset}.push($word);
}

But then I ran into a problem when I was trying to make the pairs. I wanted to make a copy of the list of similar words so I could modify it, but when I had the assignment my @list = %similar{$charset}, what I got wasn’t what I expected: instead of the elements of the list pointed to by %similar{$charset} being assigned to @list, I got the list itself assigned as the first element of @list. I needed a way to say “return the elements in this list” instead of “return this list”. Unfortunately, the method that feels right for this, .elems, just returns the count of elements, not the elements themselves. I wound up using the .splice method to return a list of all the elements in the array.

So here’s the Raku version:

#!/usr/bin/env raku
  
use v6;

sub charsInWord(Str $word) {
  # split the word into characters, then use the Raku
  # array method unique to have each character appear once.
  return $word.split('').unique.sort.join;
}

sub findSimilarWordPairs(*@words where ($_.all ~~ Str)) {
  my %similar;
  for @words -> $word {
    my $charset = charsInWord($word);
    %similar{$charset}.push($word);
  }

  # filter out character sets that only have one word
  my @multiples = %similar.keys.grep: {
    %similar{$_}.elems > 1
  };

  # make pairs by looping over the list
  # of letter sets that had multiple entries
  my @pairs;
  for @multiples -> $charset {
    # if we assign @list = %similar{$charset}, we get
    # an array with a single element, an array object.
    # By using .splice, I can get all the elements in 
    # the array object assigned to @list
    my @list = %similar{$charset}.splice(0, *);

    while ( @list.elems >= 2 ) {
      # remove the first word from the list of words
      my $first = @list.shift;
      # pair it with each of the remaining words
      for @list -> $second {
        @pairs.push([ $first, $second ]);
      }
    }
  }
  return @pairs;
}

sub solution {
  my @words = @_;
  say 'Input: @words = ("' ~ @words.join('", "') ~ '")';

  my @pairs = findSimilarWordPairs(@words);

  say 'Output: ' ~ @pairs.elems;
  my $count = 0;
  for @pairs -> $pair {
    say "" if $count == 0;
    say 'Pair ' ~ ++$count ~ ': similar words ("'
      ~ $pair.join('", "') ~ '")';
  }
}

say "Example 1:";
solution("aba", "aabb", "abcd", "bac", "aabc");

say "";
say "Example 2:";
solution("aabb", "ab", "ba");

say "";
say "Example 3:";
solution("nba", "cba", "dba");

Task 2: Frequency Sort

You are given an array of integers.

Write a script to sort the given array in increasing order based on the frequency of the values. If multiple values have the same frequency then sort them in decreasing order.

Example 1

Input: @ints = (1,1,2,2,2,3)
Ouput: (3,1,1,2,2,2)

'3' has a frequency of 1
'1' has a frequency of 2
'2' has a frequency of 3

Example 2

Input: @ints = (2,3,1,3,2)
Ouput: (1,3,3,2,2)

'2' and '3' both have a frequency of 2, so they are sorted in decreasing order.

Example 3

Input: @ints = (-1,1,-6,4,5,-6,1,4,1)
Ouput: (5,-1,4,4,-6,-6,1,1,1)

Ok, the lack of explanatory English text at the end of Example 3 and the lack '1' has a frequency of 1 of at the end of Example 2 makes me believe that Mohammad isn’t expecting that text to be part of the output. I’m including it anyway. 😉

This is the simpler of the two problems. First, we just need to count up how often we see each integer:

my %counts;
foreach my $int ( @ints ) {
  $counts{$int}++;
}

Easy-peasy. Then, much like with the last task, we use a hash of arrays to group together integers that have the same frequency:

my %frequency;
foreach my $int ( keys %counts ) {
  push @{ $frequency{ $counts{$int} } }, $int;
}

Then, putting those integers back into an output array in the proper order:

my @output;
foreach my $freq ( sort keys %frequency ) {
  # get each integer for this frequency in descending order
  foreach my $int ( reverse sort @{ $frequency{$freq} } ) {
    # we need to put the integer on the list $freq times
    foreach ( 1 .. $freq ) {
      push @output, $int;
    }
  }
}

Once I add in all the stuff to print the English output after the required array output, and the boilerplate to echo the input, we get this:

#!/usr/bin/env perl
  
use v5.38;

use Lingua::EN::Inflexion qw( wordlist );

sub solution {
  my @ints = @_;
  say 'Input: @ints = (' . join(', ', @ints) . ')';

  # count how often each integer occurs
  my %counts;
  foreach my $int ( @ints ) {
    $counts{$int}++;
  }

  # now create a hash of arrays listing which integers
  # occur at what frequencies
  my %frequency;
  foreach my $int ( keys %counts ) {
    push @{ $frequency{ $counts{$int} } }, $int;
  }

  my @output;
  my $text;
  foreach my $freq ( sort keys %frequency ) {
    my @list = @{ $frequency{$freq} };
    # get each integer for this frequency in descending order
    foreach my $int ( reverse sort @list ) {
      # we need to put the integer on the list $freq times
      foreach ( 1 .. $freq ) {
        push @output, $int;
      }
    }
    # now let's do the English description of the output.
    # have the integers in ascending order in the text,
    # and wrap them in quotes
    @list = map { "'$_'" } sort @list;
    if (@list == 1) {
      $text .= $list[0] . " has a frequency of $freq\n";
    }
    else {
      $text .= wordlist(@list);
      if (@list == 2) {
        $text .= ' both';
      }
      $text .= " have a frequency of $freq, "
            .  "so they are sorted in decreasing order\n";
    }
  }

  say "Output: (" . join(', ', @output) . ")";
  say "\n$text";
}

say "Example 1:";
solution(1,1,2,2,2,3);

say "";
say "Example 2:";
solution(2,3,1,3,2);

say "";
say "Example 3:";
solution(-1,1,-6,4,5,-6,1,4,1);

Producing the output

$ perl/ch-2.pl
Example 1:
Input: @ints = (1, 1, 2, 2, 2, 3)
Output: (3, 1, 1, 2, 2, 2)

'3' has a frequency of 1
'1' has a frequency of 2
'2' has a frequency of 3


Example 2:
Input: @ints = (2, 3, 1, 3, 2)
Output: (1, 3, 3, 2, 2)

'1' has a frequency of 1
'2' and '3' both have a frequency of 2, so they are sorted in decreasing order


Example 3:
Input: @ints = (-1, 1, -6, 4, 5, -6, 1, 4, 1)
Output: (5, -1, 4, 4, -6, -6, 1, 1, 1)

'-1' and '5' both have a frequency of 1, so they are sorted in decreasing order
'-6' and '4' both have a frequency of 2, so they are sorted in decreasing order
'1' has a frequency of 3

The Raku version didn’t change very much:

#!/usr/bin/env raku
  
use v6;

use Lingua::Conjunction;

sub solution (*@ints where {$_.all ~~ Int}) {
  say 'Input: @ints = (' ~ @ints.join(', ') ~ ')';

  # count how often each integer occurs
  my %counts;
  for @ints -> $int {
    %counts{$int}++;
  }

  # now create a hash of arrays listing which integers
  # occur at what frequencies
  my %frequency;
  for %counts.keys -> $int {
    %frequency{ %counts{$int} }.push($int);
  }

  my @output;
  my $text;
  for %frequency.keys.sort -> $freq {
    my @list = %frequency{$freq}.splice(0, *);
    # get each integer for this frequency in descending order
    for @list.sort.reverse -> $int {
      # we need to put the integer on the list $freq times
      @output.append($int xx $freq);
    }
    # now let's do the English description of the output.
    # have the integers in ascending order in the text,
    # and wrap them in quotes
    @list = @list.sort.map: { "'$_'" };
    if (@list.elems == 1) {
      $text ~= @list[0] ~ " has a frequency of $freq\n";
    }
    else {
      $text ~= conjunction @list;
      if (@list.elems == 2) {
        $text ~= ' both';
      }
      $text ~= " have a frequency of $freq, "
            ~  "so they are sorted in decreasing order\n";
    }
  }

  say "Output: (" ~ @output.join(', ') ~ ")";
  say "\n$text";
}

say "Example 1:";
solution(1,1,2,2,2,3);

say "";
say "Example 2:";
solution(2,3,1,3,2);

say "";
say "Example 3:";
solution(-1,1,-6,4,5,-6,1,4,1);

It does, however use the really cool xx operator that does sort of what x does, except for arrays instead of strings. If you execute say 'a' x 5; in Raku (or in Perl), you’ll get the output aaaaa. But if you execute say 'a' xx 5; in Raku, you’ll get (a a a a a).

Also, if I use .push() to put the elements into @output, I’d wind up pushing the arrays themselves into @output and get output like this:
Output: (3, 1 1, 2 2 2)

By using .append(), I was able to append the individual integers to @output and wind up with output like this:
Output: (3, 1, 1, 2, 2, 2)


I’ve also decided that I’m going to start adding more solutions in what the challenge calls “Guest Languages”… namely, anything that isn’t Perl or Raku. This week, I’m adding solutions in another language I know: Python. I want to pick up more languages so I’m more employable, and as I learn them I’ll be adding them to this exercise.


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

Perl Weekly Challenge: Min/Maxing your Senior Citizens

Another week, another Perl Weekly Challenge!

Task 1: Min Max

Submitted by: Mohammad S Anwar

You are given an array of distinct integers.

Write a script to find all elements that is neither minimum nor maximum. Return -1 if you can’t.

Example 1

Input: @ints = (3, 2, 1, 4)
Output: (3, 2)

The minimum is 1 and maximum is 4 in the given array. So (3, 2) is neither min nor max.

Example 2

Input: @ints = (3, 1)
Output: -1

Example 3

Input: @ints = (2, 1, 3)
Output: (2)

The minimum is 1 and maximum is 3 in the given array. So 2 is neither min nor max.

I’m taking some liberties with this one; it says to return -1 if there aren’t elements in the input array that are neither the minimum or the maximum. I’d prefer to return an empty list, but OK, I’ll do as it asks. But because the requested output cites both the minimum and maximum value, I figure the function I write should return those as well.

Which means returning multiple values from the function and unpacking them into individual variables, which Perl does very well. I whipped up this function to do the work being requested:

sub neither_min_nor_max {
  my $min = min @_; # find the minimum value
  my $max = max @_; # find the maximum value

  # grep preserves the order of the array, it just
  # passes along the elements that meet the criteria
  my @neither = grep { $_ != $min && $_ != $max } @_;
  if (@neither) {
    # if we have elements, pass them back in an array reference
    return ($min, $max, \@neither);
  }
  else {
    # otherwise, pass back the requested -1
    return ($min, $max, -1);
  }
}

And calling that function and getting back three different values is easy-peasy:

my ($min, $max, $neither) = neither_min_nor_max(@ints);

Really, the rest of the code is presentation: making the output look like Mohammad asked for in stating the problem. However, I decided I didn’t want to present a single value by itself like he did in example 3: I wanted it to still have parenthesis so you could see that it was a value in an array.

#!/usr/bin/env perl
 
use v5.38;

use Lingua::EN::Inflexion qw( verb );
use List::Util qw( min max );

sub array_join {
  return '(' . join(', ', @_) . ')';
}

sub neither_min_nor_max {
  my $min = min @_; # find the minimum value
  my $max = max @_; # find the maximum value

  # grep preserves the order of the array, it just
  # passes along the elements that meet the criteria
  my @neither = grep { $_ != $min && $_ != $max } @_;
  if (@neither) {
    # if we have elements, pass them back in an array reference
    return ($min, $max, \@neither);
  }
  else {
    # otherwise, pass back the requested -1
    return ($min, $max, -1);
  }
}

sub solution {
  my @ints = @_;
  say 'Input: @ints = ' . array_join(@ints);

  my ($min, $max, $neither) = neither_min_nor_max(@ints);

  print 'Output: ';
  if ( ref($neither) eq 'ARRAY' ) {
    # if we were passed back an array reference,
    # print it like an array
    say array_join(@$neither);

    # inflect the verb!
    my $is = @$neither == 1
           ? verb('is')->singular(3)
           : verb('is')->plural(3);

    print "The minimum is $min and maximum is $max "
        . "in the given array. ";
    say "So " . array_join(@$neither)
      . " $is neither min nor max.";
  }
  else {
    # otherwise, print the value unadorned
    say $neither;
  }
}

say "Example 1:";
solution(3, 2, 1, 4);

say "";

say "Example 2:";
solution(3, 1);

say "";

say "Example 3:";
solution(2, 1, 3);

The Raku version:

#!/usr/bin/env raku
 
use v6;

sub neither_min_nor_max (*@ints where {$_.all ~~ Int}) {
  my $min = @ints.min; # find the minimum value
  my $max = @ints.max; # find the maximum value

  # grep preserves the order of the array, it just
  # passes along the elements that meet the criteria
  my @neither = @ints.grep({ $_ != $min && $_ != $max });
  if (@neither) {
    # if we have elements, pass them back in an array reference
    return $min, $max, @neither;
  }
  else {
    # otherwise, pass back the requested -1
    return $min, $max, -1;
  }
}

sub solution (*@ints where {$_.all ~~ Int}) {
  say 'Input: @ints = (' ~ @ints.join(', ') ~ ')';

  my ($min, $max, $neither) = neither_min_nor_max(@ints);

  print 'Output: ';
  if ( $neither ~~ Array ) {
    # if we were passed back an array reference,
    # print it like an array
    say '(' ~ $neither.join(', ') ~ ')';

    # inflect the verb!
    my $is = @$neither == 1 ?? 'is' !! 'are';

    print "The minimum is $min and maximum is $max "
        ~ "in the given array. ";
    say "So (" ~ $neither.join(', ')
      ~ ") $is neither min nor max.";
  }
  else {
    # otherwise, print the value unadorned
    say $neither;
  }
}

say "Example 1:";
solution(3, 2, 1, 4);

say "";

say "Example 2:";
solution(3, 1);

say "";

say "Example 3:";
solution(2, 1, 3);

Task 2: Senior Citizens

Submitted by: Mohammad S Anwar

You are given a list of passenger details in the form “9999999999A1122”, where 9 denotes the phone number, A the sex, 1 the age and 2 the seat number.

Write a script to return the count of all senior citizens (age >= 60).

Example 1

Input: @list = ("7868190130M7522","5303914400F9211","9273338290F4010")
Output: 2

The age of the passengers in the given list are 75, 92 and 40.
So we have only 2 senior citizens.

Example 2

Input: @list = ("1313579440F2036","2921522980M5644")
Output: 0

Now, I believe based on the first example, the second example should print the following after the 0 count: “The age of the passengers in the given list are 20 and 56. So we have 0 senior citizens.”

Looking at the meaty part of the problem (parsing the passenger details into their respective fields), there’s a few ways to do this. You could use the substr function:

sub passenger_details {
  my $data = shift;
  my $phone = substr($data, 0, 10);
  my $sex   = substr($data, 10, 1);
  my $age   = substr($data, 11, 2);
  my $seat  = substr($data, 13, 2);
  return ($phone, $sex, $age, $seat);
}

Or you could extract the data using regular expressions:

sub passenger_details {
  my $data = shift;
  my($phone, $sex, $age, $seat) = $data
    =~ /\A(\d{10})(\w)(\d{2})(\d{2})\z/;
  return ($phone, $sex, $age, $seat);
}

But my mind went back to a really old function I used to use a lot back in the mid-aughts when my job was processing a lot of fixed-format text data: unpack.

sub passenger_details {
  my $data = shift;
  my($phone, $sex, $age, $seat) = unpack "A10A1A2A2", $data;
  return ($phone, $sex, $age, $seat);
}

Because the data is all ASCII data, we just need the A format specifier followed by the length of the data we’re extracting. It’s a really great way to extract fixed-width data. In fact, looking at perlpacktut, it looks like the other options of substr and regular expressions are covered as well.

Combining this with the Lingua::EN::Inflexion tricks I discovered last week, we get:

#!/usr/bin/env perl
 
use v5.38;

use Lingua::EN::Inflexion qw( inflect wordlist );

sub quoted_array {
  return '("' . join('", "', @_) . '")';
}

sub passenger_details {
  my $data = shift;
  my($phone, $sex, $age, $seat) = unpack "A10A1A2A2", $data;
  return ($phone, $sex, $age, $seat);
}

sub ages_only {
  return map {
    my($phone, $sex, $age, $seat) = passenger_details($_);
    $age
  } @_;
}

sub count_senior_citizens {
  my @seniors = grep { $_ >= 60} @_;
  return scalar(@seniors);
}

sub solution {
  my @list = @_;
  say 'Input: ' . quoted_array(@list);
  my @ages  = ages_only(@list);
  my $count = scalar(@ages);
  my $senior_count = count_senior_citizens(@ages);

  say "Output: $senior_count";
  say "";
  my $wordlist = wordlist(@ages);
  say inflect "<#d:$count>The <N:age> of the <N:passenger> "
            . "in the given list <V:is> $wordlist.";
  say inflect "So we have <#n:$senior_count> "
            . "senior <N:citizen>.";
}

say "Example 1:";
solution("7868190130M7522","5303914400F9211","9273338290F4010");

say "";

say "Example 2:";
solution("1313579440F2036","2921522980M5644");

say "";

say "Example 3:";
solution("5188675309F6002");

Yes, I added a third example to show what we get when there’s only one senior citizen.

Unfortunately, unpack in Raku is only available as an experimental method on the Blob (binary large object) role, so we’ll need to use another way to extract the fields from the fixed-width data. Let’s use regular expressions with named captures:

sub passenger_details (Str $data) {
  $data ~~ /^ $<phone>=(\d ** 10) $<sex>=(\w ** 1) 
              $<age>=(\d ** 2) $<seat>=(\d ** 2) $/;
  return (~$<phone>, ~$<sex>, ~$<age>, ~$<seat>);
}

I love how in Raku, all the regular expressions allow whitespace without having to specify extra qualifiers (like /x in Perl).

Anyway, this yields the following script in Raku:

#!/usr/bin/env raku
 
use v6;

use Lingua::Conjunction;
use Lingua::EN::Numbers;

sub quoted_list ( *@list ) {
  # given a list, quote the elements and join them with commas
  my @quoted = @list.map: { qq{"$_"} };
  return @quoted.join(q{, });
}

sub passenger_details (Str $data) {
  $data ~~ /^ $<phone>=(\d ** 10) $<sex>=(\w ** 1) 
              $<age>=(\d ** 2) $<seat>=(\d ** 2) $/;
  return (~$<phone>, ~$<sex>, ~$<age>, ~$<seat>);
}

sub ages_only (*@list where {$_.all ~~ Str}) {
  return @list.map: {
    my ($phone, $sex, $age, $seat) = passenger_details($_);    
    $age.Int;
  }
}

sub count_senior_citizens (*@list where {$_.all ~~ Int}) {
  my @seniors = @list.grep: { $_ >= 60};
  return @seniors.elems;
}

sub solution (*@list where {$_.all ~~ Str}) {
  say 'Input: @list = (' ~ quoted_list(@list) ~ ')';
  my @ages  = ages_only(@list);
  my $count = @ages.elems;
  my $senior_count = count_senior_citizens(@ages);

  say "Output: $senior_count";
  say "";

  my $str = "The age[|s] of the passenger[|s] "
          ~ "in the given list [is|are] |list|.";
  say conjunction @ages, :$str;
  my $no       = $senior_count == 0 ?? 'no' !! $senior_count.Str;
  my $citizens = $senior_count == 1 ?? 'citizen' !! 'citizens';
  say "So we have $no senior $citizens.";
}

say "Example 1:";
solution("7868190130M7522","5303914400F9211","9273338290F4010");

say "";

say "Example 2:";
solution("1313579440F2036","2921522980M5644");

say "";

say "Example 3:";
solution("5188675309F6002");

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

Perl Weekly Challenge: Not the Stated Problem

Another week, another Perl Weekly Challenge!

This week, I was looking at the challenges and the second one jumped out at me, and not for the challenge it was purporting to state… for the way it asked to present the results.

Task 2: Count Words

You are given an array of words made up of alphabetic characters and a prefix.

Write a script to return the count of words that starts with the given prefix.

Example 1

Input: @words  = ("pay", "attention", "practice", "attend")
       $prefix = "at"
Ouput: 2

Two words "attention" and "attend" starts with the given prefix "at".

Example 2

Input: @words  = ("janet", "julia", "java", "javascript")
       $prefix = "ja"
Ouput: 3

Three words "janet", "java" and "javascripr" starts with the given prefix "ja".

I was looking at the challenge, and the task itself is nothing special: does a given word start withe a given string. That’s easy: $word =~ /^$string/. What caught my eye was that the output asked to then present the count of words in English. This seemed to be a great opportunity to showcase Lingua::EN::Numbers and Lingua::En::Inflect!

The first one is simple: the module provides a function num2en, which converts a number (such as 123) into English text (“one hundred and twenty-three”). The second one is more fun: it provides plural inflections, “a”/”an” selection for English words, and manipulation of numbers as words. One thing that jumped out at me in the example was that the word “start” was improperly inflected: a single word starts, multiple words start. But that’s the kind of thing this module handles for you.

Then I took a look at the documentation for Lingua::En::Inflect to remind myself how it worked and I discovered something: the author, Damian Conway had put the module in “maintenance mode” and suggested people use Lingua::EN::Inflexion instead. That module not only had a cleaner way to inflect verbs and nouns, but it also had a function for rendering numbers as English text. Bonus! One module for all my needs. It also had a function to do something I’d written myself in the past: taking a list of items and sticking “and” between the last two items.

So here’s the script I wound up with:

#!/usr/bin/env perl

use v5.38;

use Lingua::EN::Inflexion qw( inflect wordlist );

sub quoted_list {
  # given a list, quote the elements and join them with commas
  my @quoted = map { qq{"$_"} } @_;
  return join q{, }, @quoted;
}

sub solution {
  my $prefix = shift;
  my @words  = @_;
  say qq{Input: \@words  = (} . quoted_list(@words) . q{)};
  say qq{       \$prefix = "$prefix"};

  my @matched;
  foreach my $word ( @words ) {
    # "next unless" is a perl idiom
    next unless $word =~ /^$prefix/;
    push @matched, $word;
  }
  my $count = scalar(@matched);
  say "Ouput: $count";
  say "";

  # put the list of words into an English list using "and"
  my $wordlist = wordlist( map { qq{"$_"} } @matched );

  # let's inflect the words 'word' and 'start'
  say ucfirst inflect qq{<#w:$count> <N:word> $wordlist "
    . "<V:start> with the given prefix "$prefix".};
}

say "Example 1:";
solution("at", "pay", "attention", "practice", "attend");

say "";

say "Example 2:";
solution("ja", "janet", "julia", "java", "javascript");

And my output looked like this;

$ perl/ch-2.pl
Example 1:
Input: @words  = ("pay", "attention", "practice", "attend")
       $prefix = "at"
Output: 2

Two words "attention" and "attend" start with the given prefix "at".

Example 2:
Input: @words  = ("janet", "julia", "java", "javascript")
       $prefix = "ja"
Output: 3

Three words "janet", "java", and "javascript" start with the given prefix "ja".

The Raku version wound up, as always, mostly the same:

#!/usr/bin/env raku

use v6;

use Lingua::Conjunction;
use Lingua::EN::Numbers;

sub quoted_list ( *@list ) {
  # given a list, quote the elements and join them with commas
  my @quoted = @list.map: { qq{"$_"} };
  return @quoted.join(q{, });
}

sub solution (Str $prefix, *@words where {$_.all ~~ Str}) {
  say qq{Input: \@words  = (} ~ quoted_list(@words) ~ q{)};
  say qq{       \$prefix = "$prefix"};

  my @matched;
  for @words -> $word {
    # "next unless" is a raku idiom, too
    next unless $word ~~ /^$prefix/;
    push @matched, $word;
  }
  my $count = @matched.elems;
  say "Output: $count";
  say "";

  # the examples show the word count in English as well, so
  # let's use the Lingua::EN::Numbers module
  my $count_en = tclc cardinal($count);

  # also, let's inflect the words 'word' and 'start'
  #
  # The documentation for Lingua::Conjunction says "You can use 
  # special sequence [|] (e.g. octop[us|i]) where string to the
  # left of the | will be used when the list contains just one
  # item and the string to the right will be used otherwise."
  # but there's a bug where it uses the left when there is one
  # OR TWO items.
  #
  # I've fixed it and created a pull request
  # https://github.com/raku-community-modules/Lingua-Conjunction/pull/2
  my $str = qq{$count_en word[|s] |list| start[s|] }
          ~ qq{with the given prefix "$prefix".};
  my @quoted = @matched.map: { qq{"$_"} };
  say conjunction @quoted, :$str;
}

say "Example 1:";
solution("at", "pay", "attention", "practice", "attend");

say "";

say "Example 2:";
solution("ja", "janet", "julia", "java", "javascript");

I’ve started putting types into the parameter signatures on my functions, and there wasn’t a module to do noun/verb inflection automatically, but there was a module that made providing those inflections easier, and happily enough, it was a module to render the list with “and”. Getting to fix a bug in that module was just a bonus!


Task 1: Separate Digits

You are given an array of positive integers.

Write a script to separate the given array into single digits.

Example 1

Input: @ints = (1, 34, 5, 6)
Output: (1, 3, 4, 5, 6)

Example 2

Input: @ints = (1, 24, 51, 60)
Output: (1, 2, 4, 5, 1, 6, 0)

This one was easy: getting the digits from an integer just means a little modulo division. $int % 10 gets you the ones place digit, and int( $int / 10 ) shifts every digit down a place. Loop over those and you get your digits.

What I wound up with for each integer was an array of digits. I wanted each of those digits in a master array of digits separately. I could have looped over the digits and pushed them onto my master array individually:

foreach my $digit ( @digits_of_int ) {
  push @digits_in_array, $digit;
}

But I knew there was a way to do it in a single command, and looking around, I figured out splice was my friend here:

splice @digits_in_array, scalar(@digits_in_array), 0, @digits_of_int;

The first parameter is the array we’re putting things into, the second parameter is the position of the array we’re putting them, the third is how many elements we’re replacing in the target array, and the last is the array of elements being spliced into the array. The tricky bit is the starting position: that’s going to be the length of the target array. On the first pass, the length will be zero, so we’ll insert elements into at the 0 position. Every other time, the length will point to the position in the array right after the last element (remember, Perl arrays start at 0).

So here’s the final script:

#!/usr/bin/env perl

use v5.38;

sub display_array {
  return "(" . join(q{, }, @_) . ")";
}

sub solution {
  my @ints = @_;
  say "Input: \@ints = " . display_array(@ints);
  # the description says that the array is positive integers,
  # so let's treat them as integers and divide them
  my @digits_in_array;
  foreach my $int ( @ints ) {
    my @digits_of_int;
    while ( $int > 0 ) {
      # first get the ones place digit
      my $ones_place = $int % 10;
      # push it onto the BEGINNING of @digits_of_int
      unshift @digits_of_int, $ones_place;
      # divide the number by 10, discarding the fraction
      $int = int( $int / 10 );
    }
    # push the elements from @digits_of_int onto the end
    # of @digits_in_array
    splice @digits_in_array, scalar(@digits_in_array), 0, @digits_of_int;
  }
  say "Output: " . display_array(@digits_in_array);
}

say "Example 1:";
solution(1, 34, 5, 6);

say "";

say "Example 2:";
solution(1, 24, 51, 60);

Translating this into Raku had a hiccup, however: When I started with this

sub solution (*@ints where {$_.all ~~ Int}) {
  say "Input: \@ints = " ~ display_array(@ints);
  # the description says that the array is positive integers,
  # so let's treat them as integers and divide them
  my @digits_in_array;
  for @ints -> $int {
    my @digits_of_int;
    while ( $int > 0 ) {
      # first get the ones place digit
      my $ones_place = $int % 10;
      # push it onto the BEGINNING of @digits_of_int
      unshift @digits_of_int, $ones_place;
      # divide the number by 10, discarding the fraction
      $int = ($int / 10).truncate;
    }
    # append the elements from @digits_of_int onto the end
    # of @digits_in_array
    @digits_in_array.append: @digits_of_int;
  }
  say "Output: " ~ display_array(@digits_in_array);
}

I got the following:

$ raku/ch-1.raku
Example 1:
Input: @ints = (1, 34, 5, 6)
Parameter '$int' expects a writable container (variable) as an
argument, but got '1' (Int) as a value without a container.
  in sub solution at raku/ch-1.raku line 14
  in block <unit> at raku/ch-1.raku line 32

Ahhh! Everything in Raku is an object, and I was passing immutable numbers into my array. That was an easy enough fix:

  for @ints -> $value {
    my $int = Int.new($value);

I just took the immutable value and used it to create a mutable Int object. And voilá, we’re done.

#!/usr/bin/env raku

use v6;

sub display_array (@array) {
  return "(" ~ @array.join(q{, }) ~ ")";
}

sub solution (*@ints where {$_.all ~~ Int}) {
  say "Input: \@ints = " ~ display_array(@ints);
  # the description says that the array is positive integers,
  # so let's treat them as integers and divide them
  my @digits_in_array;
  for @ints -> $value {
    my $int = Int.new($value);
    my @digits_of_int;
    while ( $int > 0 ) {
      # first get the ones place digit
      my $ones_place = $int % 10;
      # push it onto the BEGINNING of @digits_of_int
      unshift @digits_of_int, $ones_place;
      # divide the number by 10, discarding the fraction
      $int = ($int / 10).truncate;
    }
    # append the elements from @digits_of_int onto the end
    # of @digits_in_array
    @digits_in_array.append: @digits_of_int;
  }
  say "Output: " ~ display_array(@digits_in_array);
}

say "Example 1:";
solution(1, 34, 5, 6);

say "";

say "Example 2:";
solution(1, 24, 51, 60);

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

Perl Weekly Challenge: Two out of Three Ain’t Lexicographically Bad

Another week, time for another Weekly Perl Challenge!

Task 1: Lexicographic Order

You are given an array of strings.

Write a script to delete element which is not lexicographically sorted (forwards or backwards) and return the count of deletions.

Example 1

Input: @str = ("abc", "bce", "cae")
Output: 1

In the given array "cae" is the only element which is not lexicographically sorted.

Example 2

Input: @str = ("yxz", "cba", "mon")
Output: 2

In the given array "yxz" and "mon" are not lexicographically sorted.

I had to look up when “Lexicographic Order” was on the off chance that it wasn’t what I thought it was. Essentially, it means it’s sorted alphabetically. That was pretty obvious from the examples, so I just dove in:

#!/usr/bin/env perl

use v5.38;

sub quoted_list {
  # given a list, quote the elements and join them with commas
  my @quoted = map { qq{"$_"} } @_;
  return join q{, }, @quoted;
}

sub quoted_english_list {
  # given a list, quote the elements and join them 
  # in a way that makes sense to english speakers
  my @quoted = map { qq{"$_"} } @_;
  my $last = pop @quoted; # last element in array
  if (@quoted == 0) {
    # using an array in a scalar context returns
    # the number of elements in the array

    # there was only one element in the list
    return $last;
  }
  my $joined = join q{, }, @quoted;
  if (@quoted > 1) {
    # if there's more than element, add an Oxford comma
    $joined .= q{,};
  }
  return "$joined and $last";
}

sub is_lexically_sorted {
  my $input = shift @_;

  # get the characters in the input string
  my @characters = split //, $input;

  # generate a string of the characters sorted ascending
  # (with case folding)
  my $forwards  = join q{}, sort {
    fc($a) cmp fc($b)
  } @characters;

  # generate a string of the characters sorted descending
  # (with case folding)
  my $backwards = join q{}, sort {
    fc($b) cmp fc($a)
  } @characters;

  # if the input string is matches either sorted string,
  # then return true
  return( $input eq $forwards || $input eq $backwards );
}

sub solution {
  my @str = @_;
  say "Input: \@str = (" . quoted_list(@str) . ")";

  my @not_lexically_sorted = grep {
    ! is_lexically_sorted($_)
  } @str;

  say "Output: " . scalar(@not_lexically_sorted);
  say "";

  if (@not_lexically_sorted == 0) {
    say "In the given array all elements are"
      . " lexicographically sorted.";
  }
  elsif (@not_lexically_sorted == 1) {
    say "In the given array "
      . quoted_list(@not_lexically_sorted)
      . " is the only element which is not"
      . " lexicographically sorted.";
  }
  else {
    say "In the given array "
      . quoted_english_list(@not_lexically_sorted)
      . " are not lexicographically sorted.";
  }
}

say "Example 1:";
solution("abc", "bce", "cae");

say "";

say "Example 2:";
solution("yxz", "cba", "mon");

I added a bunch of extra subroutines to make the code more readable: quoted_list and quoted_english_list let me just say how I want to display the list, rather than repeating the code every time I want to display it. And the is_lexically_sorted function make the grep that I’m using to determine which array elements aren’t lexicographically sorted more readable as well. Whether it’s Perl or not, sometimes it’s just good coding practice to pull out pieces of your code that represent a concept and make them their own function, even if they’re only being used in only one place, because it just makes the code conceptually easier to understand.

The Raku version

#!/usr/bin/env raku

use v6;

sub quoted_list ( *@list ) {
  # given a list, quote the elements and join them with commas
  my @quoted = @list.map: { qq{"$_"} };
  return @quoted.join(q{, });
}

sub quoted_english_list ( *@list ) {
  # given a list, quote the elements and join them 
  # in a way that makes sense to english speakers
  my @quoted = @list.map: { qq{"$_"} };
  my $last = @quoted.pop(); # last element in array
  if (@quoted == 0) {
    # using an array in a scalar context returns
    # the number of elements in the array

    # there was only one element in the list
    return $last;
  }
  my $joined = join q{, }, @quoted;
  if (@quoted > 1) {
    # if there's more than element, add an Oxford comma
    $joined ~= q{,};
  }
  return "$joined and $last";
}

sub is_lexically_sorted ($input) {
  # get the characters in the input string
  # putting $input in quotes casts it as a Str
  my @characters = "$input".split("", :skip-empty);

  # sort the characters ascending
  my @forwards  = @characters.sort: { $^a.fc cmp $^b.fc };

  # sort the characters descending
  my @backwards = @characters.sort: { $^b.fc cmp $^a.fc };

  # if the input string is matches either sorted string,
  # then return true
  return( $input eq @forwards.join("")
          ||
          $input eq @backwards.join("") );
}

sub solution (*@str) {
  say "Input: \@str = (" ~ quoted_list(@str) ~ ")";

  my @not_lexically_sorted = @str.grep({
    !is_lexically_sorted($_)
  });

  say "Output: " ~ @not_lexically_sorted.elems;
  say "";

  if (@not_lexically_sorted.elems == 0) {
    say "In the given array all elements are"
      ~ " lexicographically sorted.";
  }
  elsif (@not_lexically_sorted.elems == 1) {
    say "In the given array "
      ~ quoted_list(@not_lexically_sorted)
      ~ " is the only element which is not"
      ~ " lexicographically sorted.";
  }
  else {
    say "In the given array "
      ~ quoted_english_list(@not_lexically_sorted)
      ~ " are not lexicographically sorted.";
  }
}

say "Example 1:";
solution("abc", "bce", "cae");

say "";

say "Example 2:";
solution("yxz", "cba", "mon");

This is mostly like the Perl solution above, but I decided to play around a little with slurpy parameters in my function signatures.

Task 2: Two out of Three

You are given three array of integers.

Write a script to return all the elements that are present in at least 2 out of 3 given arrays.

Example 1

Input: @array1 = (1, 1, 2, 4)
       @array2 = (2, 4)
       @array3 = (4)
Ouput: (2, 4)

Example 2

Input: @array1 = (4, 1)
       @array2 = (2, 4)
       @array3 = (1, 2)
Ouput: (1, 2, 4)

Perl version:

#!/usr/bin/env perl

use v5.38;

# function to return unique elements in array
use List::Util qw( uniq );

sub display_array {
  return "(" . join(q{, }, @_) . ")";
}

sub solution {
  my @arrays = @_;
  say "Input: \@array1 = " . display_array( @{ $arrays[0] } );
  say "       \@array2 = " . display_array( @{ $arrays[1] } );
  say "       \@array3 = " . display_array( @{ $arrays[2] } );

  # Return all the elements that are present in at least 2 out
  # of 3 given arrays.  In the sample input, there are arrays 
  # where there elements appear multiple times in a given
  # array, so we want to examine only UNIQUE elements
  my @unique;
  foreach my $arrayref ( @arrays ) {
    push @unique, [ uniq @$arrayref ];
  }

  # now that we have arrays of only unique elements, let's find
  # elements that occur in more than one array using a hash
  my %occurrences;
  foreach my $arrayref ( @unique ) {
    foreach my $element ( @$arrayref ) {
      $occurrences{$element}++;
    }
  }

  say "Output: " . display_array(
    sort # sort the resulting array of elements numerically
    grep {
      # only include elements that were counted more than once
      $occurrences{$_} > 1;
    } keys %occurrences
  );
}

say "Example 1:";
solution(
  [1, 1, 2, 4],
  [2, 4],
  [4]
);

say "";

say "Example 2:";
solution(
  [4, 1],
  [2, 4],
  [1, 2]
);

Raku version

#!/usr/bin/env raku

use v6;

sub display_array (@array) {
  return "(" ~ @array.join(q{, }) ~ ")";
}

sub solution (@array1, @array2, @array3) {
  say "Input: \@array1 = " ~ display_array(@array1);
  say "       \@array2 = " ~ display_array(@array2);
  say "       \@array3 = " ~ display_array(@array3);

  # Return all the elements that are present in at least 2 out
  # of 3 given arrays.  In the sample input, there are arrays
  # where there elements appear multiple times in a given
  # array, so we want to examine only UNIQUE elements, then
  # find elements that occur in more than one array using
  # a hash
  my %occurrences;
  for ( @array1.unique,
        @array2.unique,
        @array3.unique ).flat -> $element {
    %occurrences{$element}++;
  }

  say "Output: " ~ display_array(
    # only include elements that were counted more than once
    %occurrences.keys().grep: { %occurrences{$_} > 1 } 
  ).sort; # sort the resulting array of elements numerically
}

say "Example 1:";
solution(
  (1, 1, 2, 4),
  (2, 4),
  (4,)
);

say "";

say "Example 2:";
solution(
  (4, 1),
  (2, 4),
  (1, 2)
);

I want to point out my discovering the .flat method for Arrays.

Note that in the Perl version, I’m passing around array references to keep the three lists, separate, but in Raku, I’m able to make the three different parameters full-on arrays. Also, in Perl I had to pull in a function from a core module to get a list of unique elements in an array, but in Raku, the .unique method is provide on the base class Any.


Here’s my solutions in GitHub: https://github.com/manwar/perlweeklychallenge-club/tree/master/challenge-229/packy-anderson

Perl Weekly Challenge: Unique Sums and Empty Arrays

Another week, time for another Perl Weekly Challenge!

Task 1: Unique Sum

You are given an array of integers.

Write a script to find out the sum of unique elements in the given array.

Example 1

Input: @int = (2, 1, 3, 2)
Output: 4

In the given array we have 2 unique elements (1, 3).

Example 2

Input: @int = (1, 1, 1, 1)
Output: 0

In the given array no unique element found.

Example 3

Input: @int = (2, 1, 3, 4)
Output: 10

In the given array every element is unique.


The examples make what this challenge is looking for pretty clear. We find the unique elements in the array, and sum those up. I immediately thought of using a hash to accomplish the task:

# find the unique elements
my %unique;
foreach my $int ( @ints ) {
  $unique{$int}++;
}

# make a list of ONLY the unique ints
my @unique_ints = grep { $unique{$_} == 1 } @ints;

It’s a common use-case in Perl to use a hash to count how many times something occurs, whether it’s to only do something once or to actually count up occurrences.

I guess I could have populated the %unique hash via a map, but I wanted to keep what the code was doing obvious, and sometimes I think using a map just to execute code in the code block and not to return an array/hash can be confusing.

map { $unique{$_}++ } @ints;

The other thing I knew I wanted to do was show off some List::Util functions

use List::Util qw( sum );

# sum the unique elements
my $sum = sum(@unique_ints) // 0;

Sure, it would be easy enough to say

my $sum = 0;
foreach my $int ( @unique_ints ) {
  $sum += $int;
}

But sum makes it is a lot shorter. So here’s the entire script…

#!/usr/bin/env perl

use v5.38;

use List::Util qw( sum );

# just accept the list of integers on the command line
my @ints = @ARGV;

# find the unique elements
my %unique;
foreach my $int ( @ints ) {
  $unique{$int}++;
}

# make a list of ONLY the unique ints
my @unique_ints = grep { $unique{$_} == 1 } @ints;

# sum the unique elements
my $sum = sum(@unique_ints) // 0;

# produce the output
say "Input: \@int = (" . join(', ', @ints) . ")";
say "Output: $sum";
say "";

print "In the given array ";
if ( scalar(@unique_ints) == scalar(@ints) ) {
  say "every element is unique.";
}
elsif ( scalar(@unique_ints) == 0 ) {
  say "no unique element found.";
}
else {
  say "we have " . scalar(@unique_ints) . " unique elements ("
    . join(', ', @unique_ints) . ").";
}

As always, I started with my Perl script and made changes to make it valid Raku:

#!/usr/bin/env raku

use v6;

# just accept the list of integers on the command line
my @ints = @*ARGS;

# find the unique elements
my %unique;
for @ints -> $int {
  %unique{$int}++;
}

# make a list of ONLY the unique ints
my @unique_ints = grep { %unique{$_} == 1 }, @ints;

# sum the unique elements
my $sum = [+] @unique_ints;

# produce the output
say "Input: \@int = (" ~ @ints.join(', ') ~ ")";
say "Output: $sum";
say "";

print "In the given array ";
if ( @unique_ints.elems == @ints.elems ) {
  say "every element is unique.";
}
elsif ( @unique_ints.elems == 0 ) {
  say "no unique element found.";
}
else {
  say "we have " ~ @unique_ints.elems ~ " unique elements ("
    ~ @unique_ints.join(', ') ~ ").";
}

Now, the big decision I had to make was how to do the sum. I picked showing off Raku’s Reduction Metaoperator: [ ]. When you put an operator between square brackets and put that in front of a Raku Positional (like an Array), it turns the Positional into a single value by applying the operator to the first two elements, and then applying the operator to the result and the next element, and so on until the Positional has run out of elements. You can multiply all the elements of a Positional using [*], you can concatenate all the elements of a Positional using [~], There’s even a max infix operator that given two operands will return the larger of the two, and this can be applied to a Positional to find the largest value using [max].

But I could have used the .sum routine provided by Raku’s List class (which Arrays are a subclass of):

my $sum = @unique_ints.sum;

Task 2: Empty Array

You are given an array of integers in which all elements are unique.

Write a script to perform the following operations until the array is empty and return the total count of operations.

If the first element is the smallest then remove it otherwise move it to the end.

Example 1

Input: @int = (3, 4, 2)
Output: 5

Operation 1: move 3 to the end: (4, 2, 3)
Operation 2: move 4 to the end: (2, 3, 4)
Operation 3: remove element 2: (3, 4)
Operation 4: remove element 3: (4)
Operation 5: remove element 4: ()

Example 2

Input: @int = (1, 2, 3)
Output: 3

Operation 1: remove element 1: (2, 3)
Operation 2: remove element 2: (3)
Operation 3: remove element 3: ()

This time, the List::Util function I wanted to use was min:

#!/usr/bin/env perl

use v5.38;

use List::Util qw( min );

# just accept the list of integers on the command line
my @ints = @ARGV;

my @operations;
my $count = 1;
while ( scalar(@ints) > 0 ) {
  my $min = min @ints;

  # in either case, we're removing the first element from the list
  my $first = shift @ints;

  if ($min == $first) {
    # the first element is the minimum, discard it
    push @operations, "Operation $count: "
                    . "remove element $min: ("
                    . join(',', @ints) . ")";
  }
  else {
    # the first element is NOT the minimum, add it to the end
    push @ints, $first;
    push @operations, "Operation $count: "
                    . "move $first to the end: ("
                    . join(',', @ints) . ")";
  }
  $count++;
}

# produce the output
# let's use @ARGV again, since we modify @ints as we go along
say "Input: \@int = (" . join(', ', @ARGV) . ")";
say "Output: " . scalar(@operations);
say "";
say join "\n", @operations;

This also does an excellent job of showing off array operations: shift to remove the first element of an array, and push to append an element to the end of an array (though, I will admit I really like the way PHP allows you to append to the end of an array: $ints[] = $first).

At first, I was using $ints[0] to examine the first element in the array and then using shift to remove it and discard the value if the first element was the minimum value, and if it wasn’t, using shift to remove the first value and save itm like this:

if ($min == $ints[0]) {
  shift @ints;
  push @operations, ...;
}
else {
  my $first = shift @ints;
  push @operations, ...;
}

But then I realized that I was shift-ing the value off @ints in either case, and it would just be cleaner to do it before the comparison so I could use $first instead of $ints[0].

The Raku version is nothing fancy this time:

#!/usr/bin/env raku

use v6;

# just accept the list of integers on the command line
my @ints = @*ARGS;

my @operations;
my $count = 1;
while ( @ints.elems > 0 ) {
  my $min = @ints.min;

  # in either case, we're removing the first element
  # from the list
  my $first = @ints.shift;

  if ($min == $first) {
    # the first element is the minimum, discard it
    push @operations, "Operation $count: "
                    ~ "remove element $min: ("
                    ~ @ints.join(', ') ~ ")";
  }
  else {
    # the first element is NOT the minimum, add it to the end
    push @ints, $first;
    push @operations, "Operation $count: "
                    ~ "move $first to the end: ("
                    ~ @ints.join(', ') ~ ")";
  }
  $count++;
}

# produce the output
# let's use @ARGV again, since we modofy @ints as we go along
say "Input: \@int = (" ~ @*ARGS.join(', ') ~ ")";
say "Output: " ~ @operations.elems;
say "";
say join "\n", @operations;

Perl Weekly Challenge #227

This week’s challenge brought two new tasks: Friday the 13th & Roman Maths.

Task 1: Friday 13th

You are given a year number in the range 1753 to 9999.

Write a script to find out how many dates in the year are Friday 13th, assume that the current Gregorian calendar applies.

Example

Input: $year = 2023
Output: 2

Since there are only 2 Friday 13th in the given year 2023 i.e. 13th Jan and 13th Oct.

This was going to be easy, because I knew there were date manipulation modules in the core distribution: Time::Piece and Time::Seconds. I figured I wanted to start at the first of the year, add one day in a loop until I found the first Friday, and then skip from one Friday to the next by adding seven days, using the wday property of a Time::Piece object to check whether or not the date was the 13th. Yes, instantiating the first day of the year would have been easier using the DateTime module, but I wanted to use only core modules if they did what I needed (and, really how much more complex is Time::Piece->strptime("$year-01-01", "%Y-%m-%d")->truncate(to => 'day'); versus DateTime->new(year => $year, month => 1, day => 1)->truncate(to => 'day');).

#!/usr/bin/env perl
use v5.38;

# let's use the core modules for date manipulation
use Time::Piece;
use Time::Seconds qw( ONE_DAY );

# get the year from the command line
my $year = shift @ARGV
  or die "usage: $0 year\n";

# do bounds checking as specified in the problem
if ($year < 1753 || $year > 9999) {
  die "Only years between 1753 to 9999 are allowed ($year is out of range)\n";
}

# create an object for Jan 01 of the given year
my $t = Time::Piece->strptime("$year-01-01", "%Y-%m-%d")
                   ->truncate(to => 'day');

# find the first friday
# in Time::Piece->wday, 1 = Sunday, 6 = Friday
while ( $t->wday != 6) {
  $t += ONE_DAY; # add 1 day
}

# now keep adding 7 days to the date until the year changes,
# noting how many times the day of the month is 13
my $thirteen_count = 0;
while ( $t->year == $year ) {
  $thirteen_count++ if $t->mday == 13;
  $t += ONE_DAY * 7;
}

say "Input: \$year = $year";
say "Output: $thirteen_count";

Doing this problem in Raku wound up being even easier, because in Raku, Date objects are a native part of the language, and incrementing a Date object increases the value by one day. Even instantiating a Date object was easier, because I didn’t need to parse a date format or specify an array with 0-indexed months or years with 1900 subtracted from them, I was able to specify a year, month, day in my new() call:

#!/usr/bin/env raku

sub MAIN($year) {
  # do bounds checking as specified in the problem
  if ($year < 1753 || $year > 9999) {
    say "Only years between 1753 to 9999 are allowed ($year is out of range)";
    exit 1;
  }

  # create an object for Jan 01 of the given year
  my $t = Date.new($year, 1, 1);

  # find the first friday
  # in Date.day-of-week, 0 = Sunday, 5 = Friday
  while ( $t.day-of-week != 5) {
    $t++; # add 1 day
  }

  # now keep adding 7 days to the date until the year changes,
  # noting how many times the day of the month is 13
  my $thirteen_count = 0;
  while ( $t.year == $year ) {
    $thirteen_count++ if $t.day == 13;
    $t += 7;
  }

  say "Input: \$year = $year";
  say "Output: $thirteen_count";
}

Task 2: Roman Maths

Write a script to handle a 2-term arithmetic operation expressed in Roman numeral.

Example

IV + V     => IX
M - I      => CMXCIX
X / II     => V
XI * VI    => LXVI
VII ** III => CCCXLIII
V - V      => nulla (they knew about zero but didn't have a symbol)
V / II     => non potest (they didn't do fractions)
MMM + M    => non potest (they only went up to 3999)
V - X      => non potest (they didn't do negative numbers)

Now, I’m not going to get into how Roman numerals did have ways of expressing fractions or numbers larger that 3,999, because that’s not part of the challenge. Remember, I want to showcase how easy it is to solve problems in Perl & Raku. And I knew just the module to use: Roman. Unfortunately, none of the modules for manipulating Roman numerals are in the core Perl distribution, so I had to use cpanm to install it: $ cpanm Roman (I could have used $ cpan install Roman instead, but I like the cpanm tool).

#!/usr/bin/env perl
use v5.38;

use Roman; # there's a module for handling Roman Numerals!

sub do_arithmetic {
  my $line = shift;
  # split the inout line into the three parts:
  # the two operands and the infix operator
  my($operand1r, $operator, $operand2r) = split /\s+/, $line;
  unless (defined $operand1r &&
          defined $operator  &&
          defined $operand2r) {
    say q{Lines must be of the form "operand1 operator operand2"};
    say q{where both operands are valid roman numerals and the};
    say q{operator is one of the following:  +  -  *  /  **};
    return;
  }

  my($operand1a, $operand2a);

  # check that the first operand is a roman numeral
  if (isroman($operand1r)) {
    # it is a roman numeral, convert it
    $operand1a = arabic($operand1r);
  }
  else {
    say "'$operand1r' is not a roman numberal!";
    return;
  }

  # check that the second operand is a roman numeral
  if (isroman($operand2r)) {
    # it is a roman numeral, convert it
    $operand2a = arabic($operand2r);
  }
  else {
    say "'$operand2r' is not a roman numberal!";
    return;
  }

  # calculate the results
  my $result;
  if ($operator eq '+') {
    $result = $operand1a + $operand2a;
  }
  elsif ($operator eq '-') {
    $result = $operand1a - $operand2a;
  }
  elsif ($operator eq '*') {
    $result = $operand1a * $operand2a;
  }
  elsif ($operator eq '/') {
    $result = $operand1a / $operand2a;
  }
  elsif ($operator eq '**') {
    $result = $operand1a ** $operand2a;
  }
  else {
    die "Unknown operator '$operator'; valid operators are + - * / **\n";
  }

  # handle all the special output cases
  if ($result == 0) {
    say "$operand1r $operator $operand2r => nulla "
      . "(they knew about zero but didn't have a symbol)";
  }
  elsif (int($result) != $result) {
    say "$operand1r $operator $operand2r => non potest "
      . "(they didn't do fractions)";
  }
  elsif ($result > 3999) {
    say "$operand1r $operator $operand2r => non potest "
      . "(they only went up to 3999)";
  }
  elsif ($result < 0) {
    say "$operand1r $operator $operand2r => non potest "
      . "(they didn't do negative numbers)";
  }
  else {
    say "$operand1r $operator $operand2r => " . uc roman($result);
  }
}

# while we have input on STDIN, process the calculations
while (my $line = <>) {
  chomp $line;
  do_arithmetic($line);
}

At first, I whipped it up as a command-line tool that accepted the two operands and the operator on the command line, but I realized it wouldn’t be easy to produce output as close to the sample as possible doing things this way, so I modified it to read the operations from STDIN. This also allowed me to add a file that could be used by both my Perl and Raku solutions to make the input standardized.

I also wanted to do some extra checking: not just the stuff between lines 53-72 to handle the special cases called out in the example; I wanted to check for invalid Roman numerals and for input that didn’t have two operands separated by an operator. Lines 22-40 do the check using the isroman() function provided by the Roman module, and lines 49-51 make sure that we generate an error if we’re not passed one of the five operators specified in the requirements.

The Raku version of this proved slightly more challenging, because the Math::Roman module available for Raku didn’t have a function corresponding to Perl’s Roman module’s isroman() function. So I had to make one:

#!/usr/bin/env raku
use Math::Roman; # it's v0.0.1, but usable

sub isroman ( $var ) {
  # Math::Roman doesn't have a test to see if a string is
  # a Roman numeral, but it does throw an exception if it
  # cannot convert it
  my $result;
  try {
    CATCH {
      default {
        return False;
      }
    }
    $result = Math::Roman.new: $var;
  }
  # Math::Roman also doesn't respect the maximum of 3999
  if ($result.as-arabic > 3999) {
    return False;
  }

  return True;
}

sub do_arithmetic (Str $line) {
  # split the inout line into the three parts:
  # the two operands and the infix operator
  my ($operand1, $operator, $operand2) = $line.split(/\s+/);

  unless (defined $operand1 &&
          defined $operator  &&
          defined $operand2) {
    say q{Lines must be of the form "operand1 operator operand2"};
    say q{where both operands are valid roman numerals and the};
    say q{operator is one of the following:  +  -  *  /  **};
    return;
  }

  # check that the first operand is a roman numeral
  if (isroman($operand1)) {
    # it is a roman numeral, convert it
    $operand1 = Math::Roman.new: $operand1;
  }
  else {
    say "'$operand1' is not a roman numberal!";
    return;
  }

  # check that the second operand is a roman numeral
  if (isroman($operand2)) {
    # it is a roman numeral, convert it
    $operand2 = Math::Roman.new: $operand2;
  }
  else {
    say "'$operand2' is not a roman numberal!";
    return;
  }

  # # calculate the results
  my $result;
  if ($operator eq '+')     {
    $result = $operand1.as-arabic + $operand2.as-arabic;
  }
  elsif ($operator eq '-')  {
    $result = $operand1.as-arabic - $operand2.as-arabic;
  }
  elsif ($operator eq '*')  {
    $result = $operand1.as-arabic * $operand2.as-arabic;
  }
  elsif ($operator eq '/')  {
    $result = $operand1.as-arabic / $operand2.as-arabic;
  }
  elsif ($operator eq '**') {
    $result = $operand1.as-arabic ** $operand2.as-arabic;
  }
  else {
    die "Unknown operator '$operator'; valid operators are + - * / **\n";
  }

  # handle all the special output cases
  if ($result == 0) {
    say "$operand1 $operator $operand2 => nulla "
      ~ "(they knew about zero but didn't have a symbol)";
  }
  elsif ($result.truncate != $result) {
    say "$operand1 $operator $operand2 => non potest "
      ~ "(they didn't do fractions)";
  }
  elsif ($result > 3999) {
    say "$operand1 $operator $operand2 => non potest "
      ~ "(they only went up to 3999)";
  }
  elsif ($result < 0) {
    say "$operand1 $operator $operand2 => non potest "
      ~ "(they didn't do negative numbers)";
  }
  else {
    $result = Math::Roman.new: value => $result.Int;
    say "$operand1 $operator $operand2 => $result";
  }
}

# while we have input on STDIN, process the calculations
for $*IN.lines -> $line {
  do_arithmetic($line);
}