Perl Weekly Challenge 72: One-Liners for Trailing Zeros and Line Ranges
These are some answers to the Week 72 of the Perl Weekly Challenge organized by Mohammad S. Anwar.
Spoiler Alert: This weekly challenge deadline is due in a few hours. This blog post offers some solutions to this challenge, please don’t read on if you intend to complete the challenge on your own.
Since both tasks in this week challenge are quite simple, I decided to use only one-liners to solve each task, both in Raku and in Perl.
Task 1: Trailing Zeros
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
Example 3:
Input: $N = 4
Output: 0 as $N! = 24 has 0 trailing zero
Trailing Zeroes in Raku
We start with actually computing $N!
and counting the number of trailing zeros:
$ raku -e 'my $f = [*] 1.. @*ARGS[0]; say $f ~~ /(0+$)/ ?? $0.Str.chars !! 0;' 10
2
$ raku -e 'my $f = [*] 1.. @*ARGS[0]; say $f ~~ /(0+$)/ ?? $0.Str.chars !! 0;' 7
1
$ raku -e 'my $f = [*] 1.. @*ARGS[0]; say $f ~~ /(0+$)/ ?? $0.Str.chars !! 0;' 4
0
But it is a bit silly to compute $N!
: to find the number of trailing zeros, we only need to find the number of fives in the prime factors of the list of numbers that get multiplied in the factorial product. And since we know that the input integer is less than or equal to 10, we only need to take the integer part of the division of the input number by 5 (that works well until 24 and breaks at 25 because of the two fives in the prime factors of 25):
$ raku -e 'say (@*ARGS[0] / 5).Int;' 10
2
$ raku -e 'say (@*ARGS[0] / 5).Int;' 7
1
$ raku -e 'say (@*ARGS[0] / 5).Int;' 4
0
We could also use the integer division operator. But that doesn’t work as I would hope:
$ raku -e 'say @*ARGS[0] div 5;' 10
Cannot resolve caller infix:<div>(Str:D, Int:D); none of these signatures match:
(Int:D \a, Int:D \b --> Int:D)
(int $a, int $b --> int)
in block <unit> at -e line 1
Unfortunately, Raku doesn’t recognize the argument (10) as an integer, but “thinks” it is a string. To me, this is a bug: in my view, the program should be able to recognize an integer. It is of course quite easy to solve the problem by coercing the argument to an integer::
$ raku -e 'say @*ARGS[0].Int div 5;' 10
2
Trailing Zeroes in Perl
We’ve seen in the Raku section above that, up to 24, we can just divide the argument by 5 (and the task specification says that the input argument should be less or equal to 10). So porting the second Raku one-liner to Perl is quite simple:
$ perl -E 'say int shift()/5;' 10
2
$ perl -E 'say int shift()/5;' 7
1
$ perl -E 'say int shift()/5;' 4
0
Task2: Line Ranges
You are given a text file name 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
Since this blog post is about Raku and Perl one-liners, let’s start with populating the input file with a one-liner. This can be done as follows in Raku:
$ raku -e 'say "L$_" for 1..100' > input.txt
Or with exactly the same one-liner in Perl:
$ perl -E 'say "L$_" for 1..100' > input.txt
Both produce the desired input file:
$ cat input.txt
L1
L2
L3
(Lines omitted for brevity)
L98
L99
L100
Line Ranges in Raku
We just read the file and print the lines when the line number is within the range. Note that using IO.lines.kv
, line numbers start at 0; therefore, we need to shift the value by 1 to get the proper result.
$ raku -e 'sub MAIN (Int $a, Int $b where * > $a) {for "input.txt".IO.lines.kv -> $i, $j {$j.say if $b > $i >= $a-1}}' 7 10
L7
L8
L9
L10
We can make the one-liner script slightly shorter by removing the pointy block and using self-declared variables instead:
$ raku -e 'sub MAIN (Int $a, Int $b where * > $a) {for "input.txt".IO.lines.kv { $^d.say if $b > $^c >= $a-1}}' 7 10
L7
L8
L9
L10
I was hoping to use the ff
flip-flop operator, but that did not lead to simpler code (contrary to Perl, as shown below).
Line Ranges in Perl
There ought to be a few things for which Perl is better than Raku. This seems to be the case for this task, where the ..
flip-flop operator does all the work for us:
$ perl -ne 'print if 7..10' input.txt
L7
L8
L9
L10
Wrapping up
The next week Perl Weekly Challenge will start soon. If you want to participate in this challenge, please check https://perlweeklychallenge.org/ and make sure you answer the challenge before 23:59 BST (British summer time) on Sunday, August 16, 2020. And, please, also spread the word about the Perl Weekly Challenge if you can.
Task2: Line Ranges
Here is a shorter version:
raku -e 'say "input.txt".IO.lines[Range.new(|@*ARGS>>.Int) - 1]' 7 10
If we ignore the argument parsing we have thi:
"input.txt".IO.lines[Range.new(|[7, 10]) - 1]
Note the slip operator
|
and that adding/subtracting from an entire range is legal.Thank you for your comment.
Yeah, if I had really wanted to make it short, I surely would not have used a MAIN subroutine with a detailed signature representing almost half of the code, but would have used @*ARGS, as I did in the first task. In fact, in real life, when typing a one-line at the *nix prompt, I would also avoid passing arguments and hard-code them in the one-liner. Having said that, I'm also sometimes using Perl one-liners in the context of a shell script (often to avoid creating an additional software component). In that case, my "one-liner" may actually have two or three lines and may take arguments in the form of shell environment variables. Obviously, in such cases, I don't try to golf, but rather try to be clear and explicit. With all that said, I agree that the [Range.new(|@*ARGS>>.Int) - 1] (or [Range.new(|[7, 10]) - 1])) syntax is a nice and useful shortcut. And I did not know that adding/subtracting from an entire range was legal. That's quite useful. Thank you for that.