Zero Lines with Raku & Perl

by Arne Sommer

Zero Lines with Raku & Perl

[86] Published 6. August 2020.

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

Challenge #072.1: Trailing Zeroes

You are given a positive integer $N (<= 10).

Write a script to print number of trailing zeroes in $N!.

Example 1
Input: $N = 10
Output: 2 as $N! = 3628800 has 2 trailing zeroes
Example 2
Input: $N = 7
Output: 1 as $N! = 5040 has 1 trailing zero
Peak: [ 47, 32, 39, 36 ]
Example 3
Input: $N = 4
Output: 0 as $N! = 24 has 0 trailing zero

! is the faculty operator (in a matemathical sense, as neither Raku nor Perl has it). N Faculty is defined as all the consecutive integers from 1 to N multiplied together.

So e.g. 5 Faculty is the same as 1 * 2 * 3 * 4 * 5 = 120.

We can use the Reduction Metaoperator [] to compute the value for us. In REPL:

> [*] 1 .. 5;  # -> 120
> [*] 1 .. 6;  # -> 720

See docs.raku.org/language/operators#Reduction_metaoperators for more information about Reduction Metaoperators.

Now we have the value, and we need to extract the number of trailing zeroes in it. We can do this with a regex:

> 120 ~~ /(0*)$/; say $0.chars;  # -> 1
> 121 ~~ /(0*)$/; say $0.chars;  # -> 0
> 200 ~~ /(0*)$/; say $0.chars;  # -> 2

Then we can wrap it up in a program:

File zero-faculty
#! /usr/bin/env raku

subset PosInt of Int where * >= 1;           # [1]

unit sub MAIN (PosInt $N, :v(:$verbose));    # [2]

my $faculty = [*] 1 .. $N;                   # [3]

say ": $N Faculty: $faculty" if $verbose;

$faculty ~~ /(0*)$/;                         # [4]

say $0.chars;

[1] We use a custom type (set up with subset) to ensure that $N is an integer, with the value 1 or higher.

[2] Note the way we can set up aliases. Khaled M. Elboray pointed this out for me after the previous challenge. This language keeps surprising me... See docs.raku.org/type/Signature#index-entry-argument_aliases for more information.

[3] Compute the value.

[4] Count the trailing zeroes.

See docs.raku.org/language/typesystem#index-entry-subset-subset for more information about subset.

Running it:

 ./zero-faculty -v 1
: 1 Faculty: 1
0

$ ./zero-faculty -v 2
: 2 Faculty: 2
0

$ ./zero-faculty -v 3
: 3 Faculty: 6
0

$ ./zero-faculty -v 4
: 4 Faculty: 24
0

$ ./zero-faculty -v 5
: 5 Faculty: 120
1

$ ./zero-faculty -v 6
: 6 Faculty: 720
1

./zero-faculty -v 14
: 14 Faculty: 87178291200
2

Looking good.

It is tedious to enter all those command lines. A shell script wrapper printing all the values from 1 to the specified limit could do the job, but it is better to extend the Raku program itself.

File: zero-faculty-upto
#! /usr/bin/env raku

subset PosInt of Int where * >= 1;

unit sub MAIN (PosInt $N, :v(:$verbose), :$u($upto);

$upto
  ?? (1 .. $N).map({ say faculty-of($_) })  # [1]
  !! say faculty-of($N);                    # [2]

sub faculty-of ($value)                     # [3]
{
  my $faculty = [*] 1 .. $value;
  say ": $value Faculty: $faculty" if $verbose;
  $faculty ~~ /(0*)$/;
  return $0.chars;
}

[1] In «upto» mode, print all the values up to the limit. Note the use of map instead of a loop.

[2] One value only.

[3] Compute the actual value.

Running it:

./zero-faculty-upto --upto  10
0
0
0
0
1
1
1
1
1
2

$ ./zero-faculty-upto -u -v 10
: 1 Faculty: 1
0
: 2 Faculty: 2
0
: 3 Faculty: 6
0
: 4 Faculty: 24
0
: 5 Faculty: 120
1
: 6 Faculty: 720
1
: 7 Faculty: 5040
1
: 8 Faculty: 40320
1
: 9 Faculty: 362880
1
: 10 Faculty: 3628800
2

But why bother computing the faculty each time, as we already have the previous value (printed on the line above it in the output)?

file: zero-faculty-upto-loop
#! /usr/bin/env raku

subset PosInt of Int where * >= 1;

unit sub MAIN (PosInt $N, :v(:$verbose), :u(:$upto));

my $faculty = 1;

for 1 .. $N -> $value
{
  $faculty *= $value;
  
  if $upto || $value == $N   # [1]
  {
    say ": $value Faculty: $faculty" if $verbose;
    $faculty ~~ /(0*)$/;
    say $0.chars;
  }
}

[1] Print the last value, and all the others if we have requested «upto» mode.

A Perl Version

This is pretty much a straight forward translation from the last Raku version:

File: zero-faculty-perl
#! /usr/bin/env perl

use strict;
use feature 'say';

my $N = shift(@ARGV) // die 'Please specify $N';
my $verbose;
my $upto;

while ($N eq "--verbose" || $N eq "-v" || $N eq "--upto" || $N eq "-u")
{
  $verbose++ if $N eq "--verbose" || $N eq "-v";
  $upto++    if $N eq "--upto"    || $N eq "-u";

  $N = shift(@ARGV) // die 'Please specify $N';
}

die '$N must be an integer >= 1' unless int($N) == $N && $N >= 1;

my $faculty = 1;

for my $value (1 .. $N)
{
  $faculty *= $value;
  
  if ($upto || $value == $N)
  {
    say ": $value Faculty: $faculty" if $verbose;
    $faculty =~ /(0*)$/;
    say length $1;
  }
}

Raku! Bonus!

I said earlier that Raku did not have a Faculty operator. That is true, but it is very easy to add one. The reason this one is missing is perhaps just to make it easy for writers such as myself to show off the extensibility of Raku?

File: faculty
#! /usr/bin/env raku

sub postfix:<!>($n)
{
  return [*] 1 .. $n;
}

for 1 .. 20 -> $int
{
  say "$int! = { $int! }";
}

Running it:

$ ./faculty
1! = 1
2! = 2
3! = 6
4! = 24
5! = 120
6! = 720
7! = 5040
8! = 40320
9! = 362880
10! = 3628800
11! = 39916800
12! = 479001600
13! = 6227020800
14! = 87178291200
15! = 1307674368000
16! = 20922789888000
17! = 355687428096000
18! = 6402373705728000
19! = 121645100408832000
20! = 2432902008176640000

Note that defining custom operators does not work in REPL.

Challenge #072.2: Lines Range

You are given a text file name $file and range $A - $B where $A <= $B.

Write a script to display lines range $A and $B in the given file.

Example

Input:
$ cat input.txt
L1
L2
L3
L4
...
...
...
...
L100
$A = 4 and $B = 12

Output:
L4
L5
L6
L7
L8
L9
L10
L11
L12

This is trivial in Raku:

File: line-range
#! /usr/bin/env raku

subset PosInt of Int where * >= 1;

unit sub MAIN (Str $filename, PosInt $A, PosInt $B where $B >= $A,  # [1]
               :v(:$verbose));

die "No such file $filename" unless $filename.IO.e;                 # [2]
die "Not a file $filename" unless $filename.IO.f;                   # [2a]
die "Cannot read from $filename" unless $filename.IO.r;             # [2b]

my $count = 0;                                                      # [3]

for $filename.IO.lines -> $line
{
  $count++;

  say ": Considering line $count: $line" if $verbose;

  next if $count < $A;          # [4]

  say $line;
  
  last if $count == $B;         # [5]
}

[1] The constraints on the two variables $A and $B are easy to set up.

[2] Complain if the file does not exist (IO.e), if it is not a file (IO.f), and finally if it is not readable by the program (IO.r).

[3] Keep track of the line number.

[4] Before the requested start, skip it.

[5] After the requested end, exit.

See docs.raku.org/routine/e, docs.raku.org/routine/f and docs.raku.org/routine/r for more information about those file tests.

We can simplity this considerably, with an array slice:

File: line-range-slice
#! /usr/bin/env raku

subset PosInt of Int where * >= 1;

unit sub MAIN (Str $filename, PosInt $A, PosInt $B where $B >= $A,
               :v(:$verbose));

my @lines = $filename.IO.lines;          # [1]
my $lines = @lines.elems;                # [3]
my $start = min($lines -1, $A -1);       # [4]
my $stop  = min($lines -1, $B -1);       # [5]

say ": $start .. $stop" if $verbose;

say @lines[$start .. $stop].join("\n");  # [2]

[1] Reading the whole file like this is generally not a problem, as it does so lazily. It will only read the lines from the file when actually needed.

[2] The array slice, simplicity itself... But we have to ensure that the indices exist, or we'll get an error.

[3] We cannot read more lines than actually exist, and elems gives us the number of lines in the file. The problem is that is has to read the whole file to be able to figure this out.

[4] Ensure that the first line to print actually exist. The -1 part is caused by the array indices starting with zero, and the line numbers at 1.

[5] Ditto for the last one.

Running it:

$ ./line-range-slice line-range-slice 999 999
say @lines[$start .. $stop].join("\n");

$ ./line-range-slice -v line-range-slice 999 999
: 13 .. 13
say @lines[$start .. $stop].join("\n");

The program chooses to print the last line in the file, if both indices are out of bound. That is not good.

This program is unsuitable when run with very large files, as it reads the entire file before orinting anything. So we should discard it.

We can just go ahead with indices, and get rid of undefined values (missing rows) with grep:

File: line-range-grep:
#! /usr/bin/env raku

subset PosInt of Int where * >= 1;

unit sub MAIN (Str $filename, PosInt $A, PosInt $B where $B >= $A);

say $filename.IO.lines[$A -1 .. $B -1].grep( *.defined ).join("\n");

We can put the say statement at the end of the last line, to make it even easier to read:

$filename.IO.lines[$A -1 .. $B -1].grep( *.defined ).join("\n").say;

But you are probably used to seeing output statements at the start of the line, so this change may actually reduce readability.

Verbose mode has gone as well, as there is nothing left to be verbose about.

Running it:

$ ./line-range-grep line-range-grep 1 1
#! /usr/bin/env raku
$ ./line-range-grep line-range-grep 999 999

Yes, we got an empty line. That is caused by the say statement.

Note that we get a large array with undefined values if we run it with a high $B value on a file with a small number of lines. This is not very efficient.

So we can discard this one as well, and stick with «line-range».

A Perl Version

This is a straight forward translation of the «line-range» Raku program.

File: line-range-perl
#! /usr/bin/env perl

use strict;
use feature 'say';

my $verbose;

if ($ARGV[0] eq "--verbose" || $ARGV[0] eq "-v")
{
  $verbose++;
  shift(@ARGV);
}

my $filename = shift(@ARGV) // die 'Please specify a file';

my $A = shift(@ARGV) // die 'Please specify $A';
my $B = shift(@ARGV) // die 'Please specify $B';

die '$A must be an integer >= 1'  unless int($A) == $A && $A >= 1;
die '$B must be an integer >= $A' unless int($B) == $B && $B >= $B;

my $count = 0;

open my $in, $filename or die "$filename: $!";

while (my $line = <$in>)
{
  $count++;

  say ": Considering line $count: $line" if $verbose;

  next if $count < $A;

  print $line;
  
  last if $count == $B;
}

close $in;

This one handles zero lines correct, as does the Raku version it is based on:

$ ./line-range-perl line-range-perl 999 999

And that's it.