Perl Weekly Challenge: Week 253

Challenge 1:

Split Strings

You are given an array of strings and a character separator.

Write a script to return all words separated by the given character excluding empty string.

Example 1
Input: @words = ("one.two.three","four.five","six")
    $separator = "."
Output: "one","two","three","four","five","six"
Example 2
Input: @words = ("$perl$$", "$$raku$")
    $separator = "$"
Output: "perl","raku"

I'm embarrased to say this challenge took me twice as long to solve as it should have due to a silly oversight. In my script I take the first command-line argument as the separator and the rest as the words. So e.g. for example 2, they would be "$" "$perl$$" "$$raku". Note the use of double quotes? I'm using Linuq and quoting arguments is a good idea so special characters don't get interpreted by the Linux shell. But double quotes have the unfortunate side effect of being interpolated by the shell just as double-quoted strings get interpolated in Raku/Perl. $$ is a variable in shell that represents the process id of the current process. So my code failed in many fun ways. Replacing with single quotes like this: '$' '$perl$$' '$$raku' made everything work properly again.

Apart from boilerplate to bring in the command-line arguments, the code is simple and can be expressed in one line. We take the list of words @words and split each one into strings based on the $separator using .map() and .split(). | has to be used to "flatten" each call to split into one array instead of an array of array references. If two separators are next to each other as in example 2, we will get an empty string so .grep() is called to filter only non-empty strings. Then .map() is called again to put double quotes arround each string as in the example output and .join() joins them together separated by commas, again, as in the example output. Finally, the output is printed with .say().

@words.map({| $_.split($separator) }).grep({ $_ }).map({ "\"$_\"" }).join(q{,}).say;

(Full code on Github.)

The Perl version is similarly succint. One difference is that split() in Perl does not take string literals, only regular expressions. both . from example 1 and $ from example 2 have special significance in regexs and you cannot "turn off" that behavior. So what I did was make the separator into a character class by wrapping it in [] so it would be treated as a normal chararacter.

say join q{,}, map { "\"$_\"" } grep { $_ } map { split /[$separator]/ } @ARGV;

(Full code on Github.)

Challenge 2:

Weakest Row

You are given an m x n binary matrix i.e. only 0 and 1 where 1 always appear before 0.

A row i is weaker than a row j if one of the following is true:

a) The number of 1s in row i is less than the number of 1s in row j.
b) Both rows have the same number of 1 and i < j.

Write a script to return the order of rows from weakest to strongest.

Example 1
Input: $matrix = [
                [1, 1, 0, 0, 0],
                [1, 1, 1, 1, 0],
                [1, 0, 0, 0, 0],
                [1, 1, 0, 0, 0],
                [1, 1, 1, 1, 1]
                ]
Output: (2, 0, 3, 1, 4)

The number of 1s in each row is:
- Row 0: 2
- Row 1: 4
- Row 2: 1
- Row 3: 2
- Row 4: 5
Example 2
Input: $matrix = [
                [1, 0, 0, 0],
                [1, 1, 1, 1],
                [1, 0, 0, 0],
                [1, 0, 0, 0]
                ]
Output: (0, 2, 3, 1)

The number of 1s in each row is:
- Row 0: 1
- Row 1: 4
- Row 2: 1
- Row 3: 1

The matrix is created from the command-line arguments with each argument becoming one row with columns separated by spaces. E.g. for example 1, "1 1 0 0 0" "1 1 1 1 0" "1 0 0 0 0" "1 1 0 0 0" "1 1 1 1 1".

my @matrix = @args.map({ [ $_.words.map({ .Int }) ] });

This line and the last can be ignored as they are only for wrapping the output in parentheses as in the examples.

say q{(},

The actual work is done in one long chain of methods. I have split it across several lines for clarity.

Using .map() to iterate over the rows of the matrix (which is in the aptly named array @matrix), we count the number of 1's with .grep() and .elems(). This is turned into a sequence of key-value pairs with .kv() where the key is the index of the row in the matrix and the value is the count of 1's we just calculated. Wrapping this in %(...) casts the sequence to a hash.

    %(@matrix.map({ @$_.grep({ $_ == 1}).elems }).kv)

This hash is sorted in ascending numeric order according to its values. Some entries in the hash may have the same values; we break ties by sorting keys in ascending numeric order.

    .sort({ $^a.value <=> $^b.value || $^a.key <=> $^b.key })

We are only interested in the order of rows so this line transforms the hash into an array of its' keys. You would think .keys() would have been a better way to do this but I was unable to get it to work and I didn't have much time to investigate.

    .map({ $_.key })

This line joins the results into a comma-separeted string so the output looks like it does in the examples.

    .join(q{, }),
q{)};

(Full code on Github.)

As usual, the Perl version is a bit more verbose than the Raku but not too much so.

my @matrix = map { [ map { 0 + $_ } split /\s+/ ] } @ARGV;

I had to explicitly create a hash as it not possible in Perl to chain methods together as nicely as in Raku.

my %ones;

I use the "trick" of getting the indices of an array with .keys() and similar methods in Raku but I did not know you could do the same in Perl. Well, apparently yes you can. In the line below each() returns the index of an array element as well as the element itself.

while (my ($key, $row) = each @matrix) {
    $ones{$key} = scalar grep { $_ == 1 } @{$row};
}

say q{(},
    (join q{, }, sort { $ones{$a} <=> $ones{$b} || $a <=> $b } keys %ones),
    q{)};

(Full code on Github.)