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;
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;
Challenge 2:
Split Array
You are given list of strings containing
0-9
anda-z
separated byspace
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{]};
}
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{]};
}