This is my response to the Perl Weekly Challenge #40.
You are given two or more arrays. Write a script to display values of each list at a given index. For example:
We expect the following output:
|
The next challenge uses the term «list», and some of the values have more than one character. This challenge uses the term «array», and all the values are exactly one character. It is tempting to assume that the use of «array» is intentional, and assume the traditional C (the programming languange) view of a string; an array of chars. The conclusion is that we support single characters only.
The second issue is the length. Is the three arrays equally long? They are in the given example, but I'll let the program tackle different lengths.
The third issue is the sample arrays. The challenge says «two or more arrays», so I'll support user specified arrays. An unlimited amount of arrays.
Note that I use the expressions «array» and «list» interchangeably in this article. They are not synomyms in Raku (where an array is mutable, and a list is immutable), but that doesn't matter in this article.
File: array-vertical
multi MAIN () # [1]
{
MAIN('I L O V E Y O U', '2 4 0 3 2 0 1 9', '! ? £ $ % ^ & *'); # [1a]
}
multi MAIN (:$verbose, *@strings where @strings.elems) # [2]
{
my @arrays = @strings.map(*.words); # [3]
my $length = @arrays>>.elems.max; # [4]
############ # 4a ## # 4b ## 4c #
if $verbose # [5]
{
say ":A: { @arrays.perl }";
say ":L: $length";
}
for ^$length -> $index # [6]
{
print "{ $_[$index] // ' ' } " for @arrays; # [7]
say ""; # [7a]
}
}
[1] This «multi MAIN» candidate is used when we run the program without command line arguments. It passes the three default arrays (as strings) to the other candidate [1a].
[2]
This «multi MAIN» candidate is used when we run the program
with command line arguments. I have used the Slurpy Operator (*
)
collect all the arguments in an array. The default is to treat them as individual
scalar arguments. Note that the slurpy operator also accepts no values, so we
have to add a «where» clause to ensure at least one argument (so that the
program can choose the correct candidate when run without arguments).
[3]
The arrays are specified as strings with embedded spaces, and
we use .words
to get the individual characters as a list. We apply
it to a list (of strings), and uses map
to iterate over the
strings. The result is a list of list. (And we could have accessed the values
with a multi-dimesional lookup like e.g. @arrays[0][1]
which
gives «L».)
[4]
This gives us the maximum number of elements in the lists. We
start with the list of lists [4a], applies .elems
on all the lists
(as set up by >>.
instead of the normal .
invocation. We have three lists (in our default case), so we get three values
in return. And finally we apply .max
on that list to the highest
value.
[5] See the description of the verbose output below for details.
[6] Iterate over the elemensts, from 0 to the index of the last element (in the largest list). The default lists are equally long.
[7] •
Iterate over the lists (with
for @arrays
) and print
the value at the given position for each one. Note the use of the «Defined-or»
operator //
to avoid using undefined values (when we go past the
end of a short list). The more familiar «or» operator ||
would
have replaced the number 0 with a space, and that is wrong. End each row with
a newline [7a].
Running it:
$ raku array-vertical
I 2 !
L 4 ?
O 0 £
V 3 $
E 2 %
Y 0 ^
O 1 &
U 9 *
That is spot on.
We can specify the arrays, and enable verbose mode (which only works when we specify the arrays):
$ raku array-vertical --verbose \
"I L O V E Y O U" "2 4 0 3 2 0 1 9" "! ? £ $ % ^ & *"
:A: [("I", "L", "O", "V", "E", "Y", "O", "U").Seq,
("2", "4", "0", "3", "2", "0", "1", "9").Seq,
("!", "?", "£", "\$", "\%", "^", "\&", "*").Seq]
:L: 8
I 2 !
L 4 ?
O 0 £
V 3 $
E 2 %
Y 0 ^
O 1 &
U 9 *
Note that the lists are not lists at all, but Sequences. The distinction doesn't really matter now, but it will in the next section.
multi MAIN ()
{
MAIN('I L O V E Y O U', '2 4 0 3 2 0 1 9', '! ? £ $ % ^ & *');
}
multi sub MAIN (:$verbose, *@strings where @strings.elems)
{
my @arrays = @strings.map(*.words.List); # [3]
my $length = @arrays>>.elems.max;
my $width = @arrays>>.chars>>.max.max; # [1]
if $verbose
{
say ":A: { @arrays.perl }";
say ":L: $length";
say ":W: $width";
}
for ^$length -> $index
{
print "{ ($_[$index] // '').fmt("%-{ $width }s") } " for @arrays; say "";
} # [2]
}
[1] This one deserves an explanation. @arrays>>.chars>>
calculates the number of characters for each element, giving us a list of list with
the sizes. Applying >>.max
reduces that to a list with the highest
values in the inner lists. And finally applying .max
on that list gives
the longest string. We can show it in REPL:
> my @a = [("I", "L", "OV"), ("2", "412", "0"), ("!!!!", "?", "£")]
[(I L OV) (2 412 0) (!!!! ? £)]
> @a>>.chars
[(1 1 2) (1 3 1) (4 1 1)]
> @a>>.chars>>.max
(2 3 4)
> @a>>.chars>>.max.max
4
[2] Use the same length (the maximum) on all the strings, so that they come out nicely tabulated. «fmt» is the method form of «sprintf». We specify the width, and the minus sign indicates left justification (adding the spaces at the end).
[3] The program will not work if we keep the inner lists as sequences. A sequence can only be iterated over once (called consumed), and the code in [1] iterates so that [2] will fail. Coercing the sequences to lists fixes that problem.
Running it, with single quotes on the last one to prevent the shell from escaping the «!»s:
$ raku array-vertical2 --verbose "I L OV" "2 412 0" '!!!! ? £'
:A: [("I", "L", "OV"), ("2", "412", "0"), ("!!!!", "?", "£")]
:L: 3
:W: 4
I 2 !!!!
L 412 ?
OV 0 £
The tabulation is correct, but not that good. Let't try with more extreme data:
$ raku array-vertical2 --verbose "I L OV" "2 412 0" '!!!!!!!!!!!!! ? £'
:A: [("I", "L", "OV"), ("2", "412", "0"), ("!!!!!!!!!!!!!", "?", "£")]
:L: 3
:W: 13
I 2 !!!!!!!!!!!!!
L 412 ?
OV 0 £
The width of each column (or input array) should be computed separately. That is easy:
File: array-vertical3
multi MAIN ()
{
MAIN('I L O V E Y O U', '2 4 0 3 2 0 1 9', '! ? £ $ % ^ & *');
}
multi sub MAIN (:$verbose, *@strings where @strings.elems)
{
my @arrays = @strings.map(*.words.List);
my $length = @arrays>>.elems.max;
my @width = @arrays>>.chars>>.max; # [1]
if $verbose
{
say ":A: { @arrays.perl }";
say ":L: $length";
say ":W: { @width }"; # [1]
}
for ^$length -> $index
{
my $col = 0; # [2]
for @arrays
{
print "{ ($_[$index] // '').fmt("%-{ @width[$col] }s") } "; # [2]
$col++; # [2]
}
say "";
}
}
[1] We drop the final «.max» to keep the list of maximum values (for each list).
[2] use the column number to access the correct maximum element.
Running it:
$ raku array-vertical3 --verbose "I L OV" "2 412 0" '!!!!!!!!!!!!! ? £'
:A: [("I", "L", "OV"), ("2", "412", "0"), ("!!!!!!!!!!!!!", "?", "£")]
:L: 3
:W: 2 3 13
I 2 !!!!!!!!!!!!!
L 412 ?
OV 0 £
That looks much nicer, and still nicer if we drop verbose mode:
$ raku array-vertical3 "I L OV" "2 412 0" '!!!!!!!!!!!!! ? £'
I 2 !!!!!!!!!!!!!
L 412 ?
OV 0 £
The «multi MAIN» concept is nice, but we can manage quite well without it:
File: array-vertical-final
sub MAIN (:$verbose, *@strings) # [1]
{
@strings = ('I L O V E Y O U', '2 4 0 3 2 0 1 9', '! ? £ $ % ^ & *')
unless @strings.elems; # [2]
my @arrays = @strings.map(*.words.List);
my $length = @arrays>>.elems.max;
my @width = @arrays>>.chars>>.max;
if $verbose
{
say ":A: { @arrays.perl }";
say ":L: $length";
say ":W: { @width }";
}
for ^$length -> $index
{
my $col = 0;
for @arrays
{
print "{ ($_[$index] // '').fmt("%-{ @width[$col] }s") } ";
$col++;
}
say "";
}
}
[1] Just one «MAIN», and I have removed the «where» clause so that it works with, as well as without, arguments.
[2] Use the default values, if none were given on the command line.
You are given a list of numbers and set of indices belong to the list. Write a script to sort the values belongs to the indices. For example:
We would sort the values at indices 0, 2 and 5 i.e. 10,
1 and 3.
Final List would look like below:
|
This is easy, using array slices.
> my @array = 10, 4, 1, 8, 12, 3;
> my @indices = 0, 2, 5;
> my @values = @array[@indices]; # -> (10 1 3)
> my @sorted = @values.sort; # -> (1 3 10)
> @array[indices] = @sorted; # -> (1 3 10)
> say @array; # -> [1 4 3 8 12 10]
An illustration may help:
We can write the code more compact:
> my @array = 10, 4, 1, 8, 12, 3;
> @array[0,2,5].= sort;
> say @array;
[1 4 3 8 12 10]
The program should be easy to understand now. Note the named variables used to override the list of values and the list of indices, both as strings.
@array[0,2,5].= sort
is short for
@array[0,2,5] = @array[0,2,5].sort
. This construct can be used on most
operators so that they assign the new value back to the original variable. (E.g.
$a += 10
which is the same as $a = $a + 10
.)
sub MAIN (:$verbose, :$list = "10 4 1 8 12 3", :$sort = "0 2 5")
{
my @array = $list.words;
my @indices = $sort.words;
if $verbose
{
say ":A: @array[]";
say ":I: @indices[]";
say ":O: @array[@indices]";
say ":R: { @array[@indices].=sort }";
}
else
{
@array[@indices].=sort;
}
say @array;
}
Running it:
$ raku array-partialsort
[1 4 10 8 12 3]
That is not what we should expect. The challenge says «1 4 3 8 12 10».
We can use verbose mode to see what is going on:
$ raku array-partialsort --verbose
:A: 10 4 1 8 12 3
:I: 0 2 5
:O: 10 1 3
:R: 1 10 3
[1 4 10 8 12 3]
The «O» (Original) line is before sorting, and «R» (Replacement) is after. The sort order is wrong. Raku has decided to sort the values as strings (where «10» comes before «2»). The reason is that the values really are strings (as «.words» gives strings). The solution is to ensure numbers:
File: array-partialsort (changes only)
my @array = $list.words>>.Numeric;
Now we get the correct result:
$ raku array-partialsort
[1 4 3 8 12 10]
We can try with user specified values:
$ raku array-partialsort --verbose --list="1 2 3 4 5 6 7 8 9 10 11" --sort="9 1 5"
:A: 1 2 3 4 5 6 7 8 9 10 11
:I: 9 1 5
:O: 10 2 6
:R: 2 6 10
[1 6 3 4 5 10 7 8 9 2 11]
Looking good.
And that's it.