Perl Weekly Challenge: Week 329

Challenge 1:

Counter Integers

You are given a string containing only lower case English letters and digits.

Write a script to replace every non-digit character with a space and then return all the distinct integers left.

Example 1
Input: $str = "the1weekly2challenge2"
Output: 1, 2

2 is appeared twice, so we count it one only.
Example 2
Input: $str = "go21od1lu5c7k"
Output: 21, 1, 5, 7
Example 3
Input: $str = "4p3e2r1l"
Output: 4, 3, 2, 1

In Raku, this is a one-liner. First we substitute non-digits in the first command-line parameter. Because @*ARGS is immutable, we have to use the S version of the substitution parameter. Then we remove whitespace at the beginning and end of the string with .trim(). I initially omitted this step but that resulted in an unsightly extra space in the output. Now we have a string with numbers and varying amounts of whitespace bewtween them; .split() extracts just the numbers. .unique() removes any duplicate numbers. .join() makes the output look a little nicer and .say() prints it out.

(S:g/\D/ / given @*ARGS[0]).trim.split(/\s+/).unique.join(q{, }).say

(Full code on Github.)

Alas, we cannot do a one-liner in Perl because we need to provide a replacement for unique(). I had one from previous challenges but it did not preserve the order of elements as the output in the spec does. So I wrote a different function I call orderedUnique.

It takes a list as its only parameter.

sub orderedUnique(@list) {

A list holds the result.

    my @ordered;

A Hash is used to keep track of elements seen so we can find duplicates.

    my %seen;

For each element in the input...

    for my $elem (@list) {

...if the element has not been seen before it is added to the seen elements and the element is added to the list of ordered results.

        unless (exists $seen{$elem}) {
            $seen{$elem} = true;
            push @ordered, $elem;
        }

If the element has been seen, it is a duplicate. It is ignored and we move on to the next element.

    }

Finally, the ordered unique elements are returned.

    return @ordered;
}

On to the main code, $str is taken from the first command-line argument.

my $str = shift;

The non-digit characters in it are replaced with spaces.

$str =~ s/\D/ /g;

Whitespace is trimmed from the beginning and of the string.

$str =~ s/^\s+ || \s+$//g;

The numbers are extracted with split(), duplicates are removed with orderedUnique() and the result is output with join() and say().

say join q{, }, orderedUnique(split /\s+/, $str);

(Full code on Github.)

Challenge 2:

Nice String

You are given a string made up of lower and upper case English letters only.

Write a script to return the longest substring of the give string which is nice. A string is nice if, for every letter of the alphabet that the string contains, it appears both in uppercase and lowercase.

Raku

Example 1
Input: $str = "YaaAho"
Output: "aaA"
Example 2
Input: $str = "cC"
Output: "cC"
Example 3
Input: $str = "A"
Output: ""

No nice string found.

This one was more complicated than I thought it would be at first glance.

First we Generate all possible substrings of $str and stores them in @substrings.

my @substrings;

for 0 .. $str.chars - 1 -> $i {
    for $i .. $str.chars -> $j {
        @substrings.push($str.substr($i, $j));
    }
}

Now we Iterate over each .unique() substring. A little optimization I did here was to .sort() the substrings from longest to shortest. This way we will find the longest nice string a little faster.

for @substrings.unique.sort({ $^b.chars <=> $^a.chars }) -> $substring {

For each substring, we split it into individual characters with .comb() and classify each character into lowercase and uppercase groups. This is done with the .classify() method which creates a Hash called %case and adds each character to keys called lower and upper respectively.

    $substring.comb.classify({$_ eq $_.lc ?? 'lower' !! 'upper'}, :into(my %case));

The .values() of the two keys are de-duplicated with .unique(), .sort()ed and .join()ed back into strings which are then compared (The upper string is converted to lowercase so the two strings can be compared case-insensitively.)

    if %case<lower>.values.unique.sort.join eq %case<upper>.values.unique.sort.join.lc {

If they match, it is a nice string. The longest one because of the sorting we did earlier. The substring is printed out and we caese processing.

        say $substring;
        last;
    }
}

(Full code on Github.)

This is the Perl version.

We can avoid Perls' lack of unique() by storing the substrings in a hash rather than a list.

my %substrings;

for my $i (0 .. length($str) - 1) {
    for my $j ($i .. length($str)) {
        $substrings{substr($str, $i, $j)} = true;
    }
}

Thus keys %substrings will give the unique substrings.

for my $substring (sort { length $b <=> length $a } keys %substrings) {

We don't have classify() either so we just loop through the characters and manually assign each one to hashes called %lower and %upper. Once again, by use of hashes, we can avoid the need for a unique() function. Also we can store upper-case characters directly as lower case avoiding the need for calling lc() later.

    my %lower;
    my %upper;

    for my $c (split //, $substring) {
        if ($c eq lc $c) {
            $lower{$c}++;
        } else {
            $upper{lc $c}++;
        }
    }

    if ((join q{}, sort keys %lower) eq (join q{}, sort keys %upper)) {
        say $substring;
        last;
    }
}

(Full code on Github.)