This week in the Perl Weekly Challenge, I’m free associating. I want to use a Bag for task 2, and my wife has a bag from her production of Ragtime two and a half years ago that whenever she uses it, I sing 🎶 The people called it Bag-time… 🎶
So let’s be the Perl on the swing for Perl Weekly Challenge 319.
Task 1: Word Count
You are given a list of words containing alphabetic characters only.
Write a script to return the count of words either starting with a vowel or ending with a vowel.
Example 1
Input: @list = ("unicode", "xml", "raku", "perl")
Output: 2
The words are "unicode" and "raku".
Example 2
Input: @list = ("the", "weekly", "challenge")
Output: 2
Example 3
Input: @list = ("perl", "python", "postgres")
Output: 0
Approach
This problem really calls for regular expressions.
Raku
I always want to challenge myself when using Raku Regexes; I’ve been writing Perl-style regexes for more than 2/5th of my life, so it’s easy to just fall into the old habits. But part of what I want to do with PWC is learn all the neat features in Raku, so I decided to use the named regex definition syntax.
One of the things I had to suss out was how to interpolate the vowels
regex into the starts_or_ends_with_vowel
regex. My first attempt, { ^ &vowels || &vowels $ }
, failed miserably. It only matched words starting with vowels. So I read up on Regex interpolation, and discovered that I wanted to wrap those named regexes in <{ }>
.
But in going through the documentation I also discovered that I could anchor my regex in places other than the beginning or end of the string (or lines). Perl has a word boundary match, \b
, but Raku lets you match at the left and right word boundary as well!
my regex vowels { <[aeiou]> };
my regex starts_or_ends_with_vowel {
« <{&vowels}> # left word boundry followed by a vowel
|| # or
<{&vowels}> » # a vowel followed by the right word boundry
};
sub wordCount(@list) {
my @matched;
for @list -> $word {
@matched.push($word) if $word ~~ &starts_or_ends_with_vowel;
}
return @matched.elems;
}
View the entire Raku script for this task on GitHub.
$ raku/ch-1.raku
Example 1:
Input: @list = ("unicode", "xml", "raku", "perl")
Output: 2
Example 2:
Input: @list = ("the", "weekly", "challenge")
Output: 2
Example 3:
Input: @list = ("perl", "python", "postgres")
Output: 0
Perl
In converting this to Perl, I kept the structure I established in the Raku version, but went with the familiar Perl-ish ways to do things: instead of named regexes, I assigned qr//
compiled regexes to variables, and since Perl doesn’t support left or right word word boundaries, I just used a non-specific word boundary (\b
).
my $vowels = qr{[aeiou]};
my $starts_or_ends_with_vowel = qr{
\b $vowels # word boundry followed by a vowel
| # or
$vowels \b # a vowel followed by a word boundry
}x;
sub wordCount(@list) {
my @matched;
foreach my $word (@list) {
push @matched, $word
if $word =~ /$starts_or_ends_with_vowel/;
}
return scalar(@matched);
}
View the entire Perl script for this task on GitHub.
Python
In Python, of course, we need to import the re
module to provide regular expressions. If we build the regular expression as a string, we can then pass it as the first argument to re.search()
to scan the string for matches. We don’t want to use re.match
, because that would only match at the beginning of the string, an the only string we’d match would be unicode
.
import re
vowels = '[aeiou]'
starts_or_ends_with_vowel = rf'\b{vowels}|{vowels}\b'
def wordCount(word_list):
matched = []
for word in word_list:
if re.search(starts_or_ends_with_vowel, word):
matched.append(word)
return len(matched)
View the entire Python script for this task on GitHub.
Elixir
One of the things I wanted to do this time around with Elixir is use something I keep seeing at work, Module attributes. Basically, this is a way to define constants in the module, and it would work perfectly with the way I was using vowels
and starts_or_ends_with_vowel
. Because I was interpolating a value into starts_or_ends_with_vowel
, I needed to build it as a string (and specify \b
as \\b
because otherwise it would become \x08
) and then pass it through Regex.compile!/2
.
@vowels "[aeiou]"
@starts_or_ends_with_vowel Regex.compile!(
"\\b#{@vowels}|#{@vowels}\\b"
)
def wordCount(words) do
{_, matched} = Enum.map_reduce(words, [], fn word, list ->
list = if Regex.match?(@starts_or_ends_with_vowel, word) do
[ word | list ]
else
list
end
# always have to return the both
# the value and the accumulator
{word, list}
end)
length(matched)
end
View the entire Elixir script for this task on GitHub.
Task 2: Minimum Common
You are given two arrays of integers.
Write a script to return the minimum integer common to both arrays. If none found return -1.
Example 1
Input: @array_1 = (1, 2, 3, 4)
@array_2 = (3, 4, 5, 6)
Output: 3
The common integer in both arrays: 3, 4
The minimum is 3.
Example 2
Input: @array_1 = (1, 2, 3)
@array_2 = (2, 4)
Output: 2
Example 3
Input: @array_1 = (1, 2, 3, 4)
@array_2 = (5, 6, 7, 8)
Output: -1
Approach
As I said at the start of this post, I saw this problem and thought “Bag!” Take the first array, make it into a Bag, then loop through the second array and find elements that are in the first array though the bag. Only keep the minimum element.
Raku
But when I actually started working on the problem in Raku, I remembered that Raku has a lot of cool Set operations. I could turn both arrays into a Set, find the common elements using the intersection (∩) operator, and then find the minimum from that set.
sub minCommon(@arr1, @arr2) {
my $set1 = Set.new(@arr1);
my $set2 = Set.new(@arr2);
my $common = $set1 ∩ $set2;
if ($common) {
return min($common.keys);
}
return -1;
}
View the entire Raku script for this task on GitHub.
$ raku/ch-2.raku
Example 1:
Input: @array_1 = (1, 2, 3, 4)
@array_2 = (3, 4, 5, 6)
Output: 3
Example 2:
Input: @array_1 = (1, 2, 3)
@array_2 = (2, 4)
Output: 2
Example 3:
Input: @array_1 = (1, 2, 3, 4)
@array_2 = (5, 6, 7, 8)
Output: -1
Perl
Perl, as I discovered back in PWC #285, doesn’t have a built-in set type, so I’m using the Set::Scalar module again.
use Set::Scalar;
use List::AllUtils qw( min );
sub minCommon($arr1, $arr2) {
my $set1 = Set::Scalar->new(@$arr1);
my $set2 = Set::Scalar->new(@$arr2);
my $common = $set1 * $set2;
if ($common) {
return min($common->elements);
}
return -1;
}
View the entire Perl script for this task on GitHub.
Python
Again, in PWC #285 I mentioned that sets are built in to Python, and they’re pretty easy to use.
def minCommon(arr1, arr2):
set1 = set(arr1)
set2 = set(arr2)
common = set1.intersection(set2)
if common:
return min(list(common))
return -1
View the entire Python script for this task on GitHub.
Elixir
MapSet
is the module for manipulating sets in Elixir.
def minCommon(arr1, arr2) do
set1 = MapSet.new(arr1)
set2 = MapSet.new(arr2)
common = MapSet.intersection(set1, set2)
cond do
MapSet.size(common) == 0 -> -1
true -> common |> MapSet.to_list |> Enum.min
end
end
View the entire Elixir script for this task on GitHub.
Here’s all my solutions in GItHub: https://github.com/packy/perlweeklychallenge-club/tree/master/challenge-319/packy-anderson