Perl Weekly Challenge: Week 33

Challenge 1:

Count Letters (A..Z)

Create a script that accepts one or more files specified on the command-line and count the number of times letters appeared in the files.

So with the following input file sample.txt

  The quick brown fox jumps over the lazy dog.

the script would display something like:

   a: 1
   b: 1
   c: 1
   d: 1
   e: 3
   f: 1
   g: 1
   h: 2
   i: 1
   j: 1
   k: 1
   l: 1
   m: 1
   n: 1
   o: 4
   p: 1
   q: 1
   r: 2
   s: 1
   t: 2
   u: 2
   v: 1
   w: 1
   x: 1
   y: 1
   z: 1

This is conceptually similar to challenge 1 from last week except we are counting letters not words and there is no requirement for CSV output.

my @contents;
if (scalar @ARGV) {
    for my $file (@ARGV) {
        open my $fh, '<', $file or die "$OS_ERROR\n";
        local $INPUT_RECORD_SEPARATOR = undef;
        push @contents, split //, <$fh>;
        close $fh;
    }
} else {
    local $INPUT_RECORD_SEPARATOR = undef;
    push @contents, split //, <STDIN>;
}

my %totals;
for my $item (@contents) {
    $totals{lc $item}++;
}

for my $total (sort grep / \p{XPosixLower} /msx, keys %totals) {
    say "$total: $totals{$total}";
}

(Full code on Github.)

So basically, I just repurposed last weeks' code. The spec does not say whether it is neccessary to be able to read from standard input in the absence of command-line arguments but as I implemented this last week anyway and it is a good feature to have, I kept it.

After counting the total numbers of each character in %totals, we have to remove some results because the spec says we only want letters. So before printing the results I grepped for those. Actually I only need to search for lower-case letters because when inserting keys into the %totals hash I converted them to lower case. Note instead of grepping for [a-z] I used the class PosixLower. This way we can also get lower-case letters from non-English languages.

The raku version is also recycled from last week.

sub MAIN(
    *@files
) {
    my %totals;

    if @files.elems {
        for @files -> $file {
            $file.IO.comb.map({ %totals{$_.lc}++; });
        }
    } else {
        $*IN.comb.map({ %totals{$_.lc}++; });
    }

    %totals.keys.grep({ / <lower> / }).sort.map({
        say "$_: %totals{$_}";
    });
}

(Full code on Github.)

The IO class has .lines and .words methods but not .chars. Instead you have to use .comb which is a bit non-intuitive IMO.

Challenge 2:

Formatted Multiplication Table

Write a script to print 11x11 multiplication table, only the top half triangle.

    x|   1   2   3   4   5   6   7   8   9  10  11
  ---+--------------------------------------------
    1|   1   2   3   4   5   6   7   8   9  10  11
    2|       4   6   8  10  12  14  16  18  20  22
    3|           9  12  15  18  21  24  27  30  33
    4|              16  20  24  28  32  36  40  44
    5|                  25  30  35  40  45  50  55
    6|                      36  42  48  54  60  66
    7|                          49  56  63  70  77
    8|                              64  72  80  88
    9|                                  81  90  99
   10|                                     100 110
   11|                                         121

The hard part here is to offset each row so the bottom half of the triangle is left blank. It's not actually that hard when you reallize that there will be one less blank column than the number of the row you are on i.e. 0 on row 1, 1 on row 2 etc. up to 10 on row 11.

print '  x|', (join q{}, map { sprintf('% 4s', $_); } 1 .. 11), "\n",
    '---+', '----' x 11, "\n";

for my $row (1 .. 11) {
    printf("% 3s|%s\n", $row,
        (join q{},
        map { sprintf('% 4s', ($_ < $row) ? q{ } x 4 : $row * $_); } 1 .. 11)
    );
}

(Full code on Github.)

The first iteration of my Raku solution was only a translation of the Perl code above but then I decided to see if I could use more uniquely Raku features.

constant $N = 11;

In Raku you can easily declare a variable is a constant.

say '  x|', (1 .. $N).fmt('% 4s', q{}), "\n", '---+', ('----' x $N);

To format a list, you can use the join() map() sprintf() sequence same as Perl but Raku lists have a nice shortcut—the .fmt() method. A small gotcha was that you must explicitly specify the second parameter (which represents the separator between list elements) as q{} because it defaults to q{ }.

my @table = (1 .. $N X 1 .. $N).grep({ $_[1] >= $_[0]}).map({ $_[0] * $_[1] });

I was reminded of the existence of the cross-product or X operator by reading Andrew Shitov's new book "Using Raku" which you can download as a PDF for free here. What (1 .. $N X 1 .. $N) does is create a list of 121 two-element lists from (1,1) to (11,11). I then used .grep() to filter that to the 66 (N!) two-element lists in the top half-triangle of the multiplication table and then .map() to multiply the values of each of those lists and reduce them to a single 66 element list.

for (1 .. $N) {
    printf "% 3s|%s%s\n",
        $_,
        q{ } x 4 * ($_ - 1), 
        @table.splice(0, $N - $_ + 1).fmt('% 4s', q{});
};

(Full code on Github.)

When printing the @tables list out, I first print the row label and blank space for each row and then use .splice() to carve out a chunk of the list, 11 elements for the first row, 10 for the second and so on upto 1 on the eleventh row. .fmt() once again is used to prettify the chunk.