Perl Weekly Challenge: Week 184

Challenge 1:

Sequence Number

You are given list of strings in the format aa9999 i.e. first 2 characters can be anything 'a-z' followed by 4 digits '0-9'.

Write a script to replace the first two characters with sequence starting with '00', '01', '02' etc.

Example 1
Input: @list = ( 'ab1234', 'cd5678', 'ef1342')
Output: ('001234', '015678', '021342')
Example 2
Input: @list = ( 'pq1122', 'rs3334')
Output: ('001122', '013334')

One of the things I love about Perl is the "magic" it has built in to various operations. For instance, most programming languages have an increment operator, usually spelled ++, but among the languages I know only Perl (well, and Raku as we shall see but that is because of Perl) lets you increment strings. A feature that makes solving this challenge incredibly easy.

my $seq = '00';

$seq++ means that $seq will become 01, 02, 03 and so on which is just what we want to substitute into our strings.

my @output = map { s/^../$seq++/emsx; $_; } @list;

This third line could actually be combined with the second but I kept them separate for clarity.

say join q{ }, @output;

(Full code on Github.)

Some may say these magic features make Perl unnecessarily baroque and hard to learn which affects maintainability in the long run but it is extremely handy for most day to day tasks where you need to whip up a small script to get things done. I guess your point of view will boil down to whether you think the P in Perl (assuming it is an acronym which it actually isn't) stands for "Practical" or "Pathetic(ally Eclectic)"

This is the equally succint Raku version.

my $seq = '00';
my @output = @list.map({ $_.subst(/^../, $seq++)});
@output.join(q{ }).say;

(Full code on Github.)

Challenge 2:

Split Array

You are given list of strings containing 0-9 and a-z separated by space only.

Write a script to split the data into two arrays, one for integers and one for alphabets only.

Example 1
Input: @list = ( 'a 1 2 b 0', '3 c 4 d')
Output: [[1,2,0], [3,4]] and [['a','b'], ['c','d']]
Example 2
Input: @list = ( '1 2', 'p q r', 's 3', '4 5 t')
Output: [[1,2], [3], [4,5]] and [['p','q','r'], ['s'], ['t']]

We begin our Perl solution by defining two arrays, one for integers...

my @allInts;

...and one for alphabets.

my @allAlphas;

For each string in our input list, we provide two arrays which will hold the integers and alphabets found in that string.

for my $string (@list) {
    my @ints;
    my @alphas;

We split the string into characters then for each character,

    for my $char (split q{ }, $string) {

if the character is between 0 to 9 it is added to the @ints array whereas if it is between a to z it is add to @alphas. I thought there was a way to do this via smartmatch on a range but I was not able to get it to work so I just used regular expressions to match. One other thing to note is that the output shown in the spec has alphabetic characters quoted so I surround them with single quotes before pushing them to @alphas.

        if ($char =~ /[0-9]/) {
            push @ints, $char;
        } elsif ($char =~ /[a-z]/) {
            push @alphas, "'$char'";
        }
    }

After the processing of a string is complete, we add its' @ints and @alphas arrays to the master @allInts and @allAlphas arrays. One problem I found which exhibits itself in e.g. example 2, is that @ints or @alphas can be empty if the string did not contain any of that type of character. These should not be displayed in the output according to the spec so I added tests to make sure only arrays that had elements would be added.

    if (@ints) {
        push @allInts, \@ints;
    }
    if (@alphas) {
        push @allAlphas, \@alphas;
    }
}

I wanted the output to look like it is shown in the spec so I wrote the printArray() function to get all the square brackets, commas and spaces in the right places.

say printArray(\@allInts), ' and ', printArray(\@allAlphas);

It look like this:

sub printArray {
    my ($array) = @_;

Using join() everywhere means we don't have to deal with dangling commas and the like.

    my @output = map { q{[} . (join q{,}, @{$_}) . q{]} } @{$array};

    return q{[} . (join q{, }, @output) . q{]};
}

(Full code on Github.)

This is the Raku version:

my @allInts;
my @allAlphas;

for @list -> $string {

Raku lists have a nice method called .classify() that as the documentation says, "transforms a list of values into a hash representing the classification of those values." So all the work we had to do in Perl is formalized into one standard operation. Ultimately it does not save many lines of code but does make the script conceptually clearer in my opinion.

    $string.split(q{ }).classify({

The main part of .classify() is a subroutine you provide that classifies the values in the list. The subroutine returns strings which will be used as keys in the output hash. I noticed with satisfaction that smartmatch on a range works properly in Raku.

        if $_ ~~ 0..9 {
                'integer'
            } elsif $_ ~~ 'a'..'z' {
                'alpha';
            }
        },

We can also transform a value before classifying it using the :as parameter. In this case I'm adding single quotes around alphabetic characters.

        :as({ $_ ~~ 'a'..'z' ?? "'$_'" !! $_; }),

I mentioned the output is a hash. Where is that defined? Well we can assign the return value as we usually do for a function, something like my %type = classify(), or we can use the :into parameter. I must confess I don't see the value this has over the usual way but there it is.

        :into( my %type )
    );

After .classify() is finished we have all the integers in %type{'integer'} and all the alphabets in %type{'alpha'}. We can go on and add them to @allInts and @allAlphas checking that they are not empty just as we did in Perl.

    if %type{'integer'} {
        @allInts.push(%type{'integer'});
    }
    if %type{'alpha'} {
        @allAlphas.push(%type{'alpha'});
    }
}

And finally, print the results.

say printArray(@allInts), ' and ', printArray(@allAlphas);

Here is the Raku version of printArray():

sub printArray(@array) {

    my @output = @array.map({ q{[} ~ $_.join(q{,}) ~ q{]} });

    return q{[} ~ @output.join(q{, }) ~ q{]};
}

(Full code on Github.)