Addition and Multiplication with Raku

by Arne Sommer

Addition and Multiplication with Raku

Published 6. November 2019

This is my response to the Perl Weekly Challenge #33.

Challenge 33.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 quite easy, especially if we ignore the fine print.

File: lettercount-wrong
my %result = $*ARGFILES.comb.Bag;  # [1]
############ # [1a] ### [1b] [1c]

for %result.keys.sort -> $key      # [2]
{
  say "$key: %result{$key}";       # [3]
}

[1] Take the contents of all the files specified on the command line (1a), turn them into a list if single characters (1b), and that list into a Bag where the keys are the characters and the value is the count (1c).

[2] Iterate over the keys (the characters) in sorted order.

[3] Print the character and the count.

See docs.perl6.org/type/Bag for more information about the Bag type.

Running it shows that we are not quite there:

$ raku lettercount-wrong sample.txt 
: 1
 : 8
.: 1
T: 1
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: 1
u: 2
v: 1
w: 1
x: 1
y: 1
z: 1

I have highlighted two problems. The first (in red) that I'll look into is that uppercase letters should be counted as lowercase, and we can fix that by turning everything into lowercase:

File: lettercount-wrongish
my %result = $*ARGFILES.comb>>.lc.Bag; # [1]

for %result.keys.sort -> $key
{
  say "$key: %result{$key}";
}

[1] «lc» works on a single value, but «comb» gives us a list. The solution is the «>>.» syntax which applies the function (to the right) to all the elements in parallel.

This version of the program renoves the «T» line, and gives the correct value for «t».

The second problem (in blue) is that we let non-letters (as space and a period) through. The challenge specifies A..Z only, but I am inclined to allow any unicode letter:

File: lettercount-unicode
my %result = $*ARGFILES.comb>>.lc.grep(* ~~ /<:L>/).Bag;

for %result.keys.sort -> $key
{
  say "$key: %result{$key}";
}

[1] If the character is a letter (the Unicode Letter class), we allow it. Everything else is removed.

Running it on a file with German text:

File: sample-unicode.txt
Es war ein super-schwüler Tag. Das Fußballspiel. Übersetzung. 
$ raku lettercount-unicode sample-unicode.txt
a: 4
b: 2
c: 1
d: 1
e: 7
f: 1
g: 2
h: 1
i: 2
l: 4
n: 2
p: 2
r: 4
s: 6
t: 2
u: 3
w: 2
z: 1
ß: 1
ü: 2

Note that «lc» translates «Ü» to «ü», as it should.

We should have a go a specifying more than one file:

$ raku lettercount-unicode sample-unicode.txt sample-unicode.txt 
a: 8
b: 4
c: 2
d: 2
e: 14
f: 2
g: 4
h: 2
i: 4
l: 8
n: 4
p: 4
r: 8
s: 12
t: 4
u: 6
w: 4
z: 2
ß: 2
ü: 4

The values have been doubled, so it works.

Non-Unicode

The challenge did ask for A..Z, so perhaps we should do just that:

File: lettercount-ascii
my %result = $*ARGFILES.comb>>.lc.grep(* ~~ /<[a .. z]>/).Bag; #[1]

for %result.keys.sort -> $key
{
  say "$key: %result{$key}";
}

[1] An explicit character class (range), doing what it looks like.

Note that «grep» is a «for» loop in disguise, iterating over the elements in the list and selecting only those that matches the condition. The «>>.lc» call could have been done with an old school «map» instead. (And yes, «map» is also a «for» loop in disguise.)

Running it on my German file shows that the Unicode letters («ß» and «ü») are gone:

$ raku lettercount-ascii sample-unicode.txt
a: 4
b: 2
c: 1
d: 1
e: 7
f: 1
g: 2
h: 1
i: 2
l: 4
n: 2
p: 2
r: 4
s: 6
t: 2
u: 3
w: 2
z: 1

Challenge 33.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

Here it is:

File: mul11
say "  x|   1   2   3   4   5   6   7   8   9  10  11";  # [1]
say "---+--------------------------------------------";  # [1]

for 1 .. 11 -> $row                  # [2]
{
  print $row.fmt('%3d') ~ "|";       # [3]
  print "    " x $row - 1;           # [4]

  for $row .. 11 -> $col             # [5]
  {
    print ($row * $col).fmt('%4d');  # [6]
  }
  print "\n";                        # [7]
}

[1] It have chosen to hard code these lines.

[2] Iterate over the 11 rows.

[3] Print the line number. Note the indentation; we use 3 characters (see the dash-line above for confirmation).

[4] Indentation (the missing bottom half triangle): 4 spaces multiplied with one less than the line number (as the first line should not have any indentation).

[5] Iterate over the columns, starting with the row number (to skip the bottom half triangle) and up to 11.

[6] Multiply the values, and print the result. Note the size of the values. The largest numbers (e.g. 121) have three digits, so we use 4 (to get at least one leading space).

[7] Finish the row.

See docs.perl6.org/routine/x for more information about the «x» operator.

Running it:

$ raku mul11
  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

Why 11?

What about a version of the program where we can set the from and to values?

File: mulX (with the changes highlighted)
unit sub MAIN (:$from = 1, :$to = 11);        # [1]

my $size-label = $to.chars + 1;               # [2]
my $size-val   = ($to * $to).chars + 1;       # [2]

print "x".fmt("%{$size-label}s") ~ "|";       # [3]
print .fmt("%{$size-val}d") for $from .. $to; # [3]
print "\n";                                   # [3]

print "-" x $size-label;                      # [4]
print "+";                                    # [4]
say   "-" x ($size-val * ($to - $from + 1));  # [4]

for $from .. $to -> $row
{
  print $row.fmt("%{$size-label}d") ~ "|";
  print (" " x ($row - $from) x $size-val;

  for $row ..  $to -> $col
  {
    print ($row * $col).fmt("%{$size-val}d"));
  }
  print "\n";
}

[1] The program defaults to 1 as the start and 11 as the end values, unless specified.

[2] The length of the lables (to the left of the «|») and the values. We get the largest value by multiplying it with itself. Note the «+1» to get at least one space before each number.

[3] This block prints the first line.

[4} And this block prints the line of dashes.

Running it:

$ raku mulX
  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

$ raku mulX --from=1 --to=4
 x|  1  2  3  4
--+------------
 1|  1  2  3  4
 2|     4  6  8
 3|        9 12
 4|          16

$ raku mulX --from=999 --to=1003
    x|     999    1000    1001    1002    1003
-----+----------------------------------------
  999|  998001  999000  999999 1000998 1001997
 1000|         1000000 1001000 1002000 1003000
 1001|                 1002001 1003002 1004003
 1002|                         1004004 1005006
 1003|                                 1006009

And that's it.