Perl Weekly Challenge: Week 216

Challenge 1:

Registration Number

You are given a list of words and a random registration number.

Write a script to find all the words in the given list that has every letter in the given registration number.

Example 1
Input: @words = ('abc', 'abcd', 'bcd'), $reg = 'AB1 2CD'
Output: ('abcd')

The only word that matches every alphabets in the given registration number is 'abcd'.
Example 2
Input: @words = ('job', 'james', 'bjorg'), $reg = '007 JB'
Output: ('job', 'bjorg')
Example 3
Input: @words = ('crack', 'road', 'rac'), $reg = 'C7 RA2'
Output: ('crack', 'rac')

The first thing we do in Raku is make an array of valid letters from $reg. We split it into characters with .comb(), filter out the non-alphabetic characters with .grep(), filter out any non-unique characters with .unique() and turn everything that remains lower-case with .map().

my @registration = $reg.comb.grep({$_ ~~ /<alpha>/}).unique.map({ $_.lc });

We will also need an array to store the results.

my @results;

Then for each word...

for @words -> $word {

...we do the same thing as we did for $reg.

    my @w = $word.comb.grep({$_ ~~ /<alpha>/}).unique.map({ $_.lc });

Then we compare @registration and @w using the subset or equal operator . If @registration is a subset of @w, the word is added to @results.

    if @registration ⊆ @w {
        @results.push($word);
    }
}

At the end we print the results in the format suggested by the spec.

say q{(}, @results.map({"'$_'"}).join(q{, }), q{)};

(Full code on Github.)

This is the Perl version. It uses some auxillary functions to make up for functionality in Raku that Perl doesn't have.

my @registration = combGrepUniqueLc($reg);
my @results;

for my $word (@words) {
    my @w = combGrepUniqueLc($word);
    if (isSubset(\@registration, \@w)) {
        push @results, $word;
    }
}

say q{(}, (join q{, }, map {"'$_'"} @results), q{)};

This function replaces the .comb.grep({$_ ~~ /<alpha>/}).unique.map({ $_.lc }) code in the Raku script.

sub combGrepUniqueLc {
    my ($str) = @_;

    my @chars = map { lc } grep { $_ =~ /[[:alpha:]]/ } split //, $str;

    my %unique;
    for my $c (@chars) {
        $unique{$c}++;
    }

    return keys %unique;
}

And this is my replacement for . I had done something like this before for a previous challenge and at first I tried to reuse that but it had the unfortunate characteristic of not working with lists containing duplicates which caused me no end of bother when I tried to use it in the second challenge this week. So I rewrote it like this and used it here as well as there.

sub isSubset {
    my @subset = @{$_[0]};

    my %set;
    for my $c (@{$_[1]}) {
        $set{$c}++;
    }

    for my $c (@subset) {
        if (!exists $set{$c} || $set{$c} == 0) {
            return undef;
        }
        $set{$c}--;
    }

    return 1;
}

With these two functions, writing a solution becomes easy.

my ($reg, @words) = @ARGV;

my @registration = combGrepUniqueLc($reg);
my @results;

for my $word (@words) {
    my @w = combGrepUniqueLc($word);
    if (isSubset(\@registration, \@w)) {
        push @results, $word;
    }
}

say q{(}, (join q{, }, map {"'$_'"} @results), q{)};

(Full code on Github.)

Challenge 2:

Word Stickers

You are given a list of word stickers and a target word.

Write a script to find out how many word stickers is needed to make up the given target word.

Example 1
Input: @stickers = ('perl','raku','python'), $word = 'peon'
Output: 2

We just need 2 stickers i.e. 'perl' and 'python'.
'pe' from 'perl' and
'on' from 'python' to get the target word.
Example 2
Input: @stickers = ('love','hate','angry'), $word = 'goat'
Output: 3

We need 3 stickers i.e. 'angry', 'love' and 'hate'.
'g' from 'angry'
'o' from 'love' and
'at' from 'hate' to get the target word.
Example 3
Input: @stickers = ('come','nation','delta'), $word = 'accommodation'
Output: 4

We just need 2 stickers of 'come' and one each of 'nation' & 'delta'.
'a' from 'delta'
'ccommo' from 2 stickers 'come'
'd' from the same sticker 'delta' and
'ation' from 'nation' to get the target word.
Example 4
Input: @stickers = ('come','country','delta'), $word = 'accommodation'
Output: 0

as there's no "i" in the inputs.

I'm not sure this is the best solution but it seems to work.

The first thing I checked was if all the letters in $word also occur in @stickers using the not subset or equal operator, . If the letters of $word aren't a subset, it is pointless to carry on. We can just print 0 and exit the script.

if $word.comb.unique ⊊ @stickers.join.comb.unique {
    say 0;
    exit;
}

Originally, I didn't have this line but it turns out you can't do example 3 which requires multiple stickers of the same type without it. The problem is how do you know in advance how many copies of which stickers do you need? It is safe to assume that at most one copy of each for every unique letter in $word will cover it. In most cases most of the extra copies will be wasted but I don't think you can do better.

This line uses the xx operator to add extra copies. .flat() is called at the end because otherwise Raku will insert array references.

my @stickerList = (@stickers xx $word.comb.unique.elems).flat;

This line creates a variable of type Bag. This is Rakus' equivalent of a C++ multiset i.e. a set that can hold multiple elements with the same value. In this case, the bag holds the individual characters in $word.

my $chars = Bag.new($word.comb);

We also need a place to store the result.

my $result = 0;

Then from 1 to the number of stickers in the sticker list...

LOOP: for 1 .. @stickerList.elems -> $i {

...We find the combinations of that length of words in @stickerList.

    for @stickerList.combinations($i) -> @combo {

We make another Bag consisting of the letters in that combination.

        my $sticker = Bag.new(@combo.join.comb);

If $chars is a subset of $sticker the result is the number of words in @combo. We are finished so we can break out of the outer loop.

        if $chars ⊆ $sticker {
            $result = @combo.elems;
            last LOOP;
        }
    }
}

Lastly, we print the result.

say $result;

(Full code on Github.)

As is usually the case, I had to write some additional code to translate my Raku solution to Perl. Luckily, some of it was already done. combinations() for instance is a function that I have used in many previous weeks challenges.

isSubset() was introduced above.

combUnique() is a stripped down version of combGrepUniqueLc() from above.

sub combUnique {
    my ($str) = @_;

    my @chars = split //, $str;

    my %unique;
    for my $c (@chars) {
        $unique{$c}++;
    }

    return keys %unique;
}

This is my version of xx.

sub xx {
    my ($array, $amount) = @_;
    my @result = @{$array};

    for (1 .. $amount - 1) {
        push @result, @{$array};
    }

    return @result;
}

Now the rest of the script is more or less the same as Raku.

my ($word, @stickers) = @ARGV;

unless (isSubset([combUnique($word)], [combUnique(join q{}, @stickers)])) {
    say 0;
    exit;
}

my @stickerList =  xx(\@stickers, scalar combUnique($word));
my @chars = split //, $word;
my $result = 0;

LOOP: for my $i (1 .. scalar @stickerList) {
    for my $combo (combinations(\@stickerList, $i)) {
        my @sticker = split //, (join q{}, @{$combo});
        if (isSubset(\@chars, \@sticker)) {
            $result = scalar @{$combo};
            last LOOP;
        }
    }
}

say $result;

(Full code on Github.)