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