This article has been moved from «perl6.eu» and updated to reflect the language rename in 2019.
This is my response to the Perl Weekly Challenge #6.
Create a script which takes a list of numbers from command line and print the same in the compact form. For example, if you pass “1,2,3,4,9,10,14,15,16” then it should print the compact form like “1-4,9,10,14-16”. |
Getting the individual numbers from the command line is easy, as the
values are comma separated. Applying «.split(",")
» on the string gives a
list of the values:
> say "1,2,3,4,5,6,dd,wqqw,ao,1.1".split(",").raku;
("1", "2", "3", "4", "5", "6", "dd", "wqqw", "ao", "1.1").Seq
See
docs.raku.org/routine/split
for more information about «split
».
unit sub MAIN (Str $values);
my @values = $values.split(",");
Error checking on the values is a good thing. We'll
start by ensuring that the values are integers. The built-in «is-int
»
method doesn't work, as it is defined for Ranges only. But we can use the
«Int
» method:
die "Integers only!" unless all(@values>>.Int);
Note the hyper method call to «Int
», so that it is
done on each element in the array. The «all
» wrapper ensures that the
condition must be true for all of them, and «unless
» turns it upside
down; one single non-integer value causes the program to die.
Now, this dies if we give it a letter. But a number with a decimal point and a
fractional part isn't stopped, as the value can be coerced (converted) to
an integer (albeit by truncating the value). So I'll have another go with a
Regex; «\D
» matches non-digits, so it dies if we have any non-digit in
the list:
die "Integers only!" if @values.grep(/\D/);
We can also make sure that the integers come in correct order:
die "Wrong order!" unless [<] @values;
The Reduction Metaoperator []
applies
the operator (given in the brackets) between the values in the list after it. A more
familiar example is «[*] (1 .. 10)
» to calculate «10!» (10 faculty), but
any operator will do.
See docs.raku.org/language/operators#Reduction_metaoperators for more information about Reduction metaoperators.
File: int-erval
unit sub MAIN (Str $values);
my @values = $values.split(",");
die "Integers only!" if @values.grep(/\D/);
die "Wrong order!" unless [<] @values;
my @result;
my @current = @values.shift; # [1]
while @values # [2]
{
my $next = @values.shift; # [2]
if $next == @current[* -1] + 1
{
@current.push($next); # [3]
}
else
{
@result.push(fix-it(@current)); # [4]
@current = $next; # [4]
}
}
@result.push(fix-it(@current)) if @current.elems; # [5]
say @result.join(","); # [6]
sub fix-it (@list) # [7]
{
return @list[0] if @list.elems == 1; # [8]
return "@list[0],@list[1]" if @list.elems == 2; # [9]
return "@list[0]-@list[*-1]" if @list.elems > 2; # [10]
}
[1] We start with the first value (in @current).
[2] And continue as long as there are more values in the list (placed in $next).
[3] If the next value is one after the previuos one (the next integer value), we add it to @current.
[4] If not, the @current array is added to the @result list. And we reset the @current array.
[5] Finally (when we run out of new values), we add the last ones to the @result list.
[6] Add a comma between the value groups.
[7] This procedure fixes a list of (consecutive) integers;
[8] if one integer, print it.
[9] if two integers, print them with a comma in between.
[10] if more than two integers, print the first and last with a dash in between.
gather
/take
»
instead:
File: int-erval-gather
unit sub MAIN (Str $values);
my @values = $values.split(",");
die "Integers only!" if @values.grep(/\D/);
die "Wrong order!" unless [<] @values;
my $result := gather # [1]
{
my @current = @values.shift;
while @values
{
my $next = @values.shift;
if $next == @current[* -1] + 1
{
@current.push($next);
}
else
{
take fix-it(@current); # [2]
@current = $next;
}
}
take fix-it(@current) if @current.elems; # [3]
}
say $result.join(","); # [4]
multi sub fix-it (@list where @list.elems == 1) # [5]
{
return @list[0];
}
multi sub fix-it (@list where @list.elems == 2) # [5]
{
return "@list[0],@list[1]";
}
multi sub fix-it (@list where @list.elems > 2) # [5]
{
return "@list[0]-@list[*-1]";
}
[1] We use «gather
» (with binding; «:=
») to
set up the supply of values, as a lazy data structure (where the values are
only calculated when actually needed).
[2] We use «take
» to deliver a value to the consuming part of
the program.
[3] A final «take
», if we have any values left in the pipeline.
[4] Calling «$result» like this fetches all the values at the same time.
[5] I have chosen Multiple dispatch with «multi sub» and different signatures instead of the «if» conditions in the original version.
See my Raku Gather,
I Take article for more information about gather
/take
.
See docs.raku.org/syntax/multi for more information about «multi» and Multiple dispatch.
«int-erval-gather» is essentially the same program as «int-erval», but it scores higher on the show off index.
«Create a script to calculate Ramanujan’s constant with at least 32 digits of precision. Find out more about it here.» |
In my Raku P(i)ermutations article I showed that we got rounding errors when we have too many digits after a decimal point (in the «Fixing Pi» section, with indentation added to make it easier to spot the difference):
> 3.1415926535897932384626433832795028841971693993751
3.141592653589793221627946859855630841326670589198336
It turns out that we can use the «FatRat» type to avoid the rounding error. But this type must be requested explicitly:
> 3.1415926535897932384626433832795028841971693993751.FatRat
3.1415926535897932384626433832795028841971693993751
The «Rat» type represents a Rational Number, with 64 bits used for the two parts (the numerator and the denomirator). If we need larger numbers, the «FatRat» type is the answer. It has no limits on the size of the two parts, but is slower. It is not used by default, butc we must be explictly requested, as done above.
See docs.raku.org/type/Rat for more information about «Rat».
See docs.raku.org/type/FatRat for more information about «FatRat».
The obvious showoff value for «Rat» is «1/3», which is impossible to represent as a normal number without a rounding error:
> say 1/3 + 1/3 + 1/3; # -> 1
> say (1/3).WHAT; # -> (Rat)
> say (1/3).raku; # -> <1/3>
But it can also hold finite values, as done above.
> my $a = e ** (pi * sqrt(163)); # -> 2.625374126407677e+17
> $a.WHAT; # -> (Num)
See docs.raku.org/type/Num for more information about «Num».
Just coercing the result to «FatRat» doesn't work out, as the fractional part is missing:
> say (e ** (pi * sqrt(163))).FatRat; # -> 262537412640767712
Coercing a «Num» to a «FatRat» can actually make it loose precision, as shown here with «pi»:
> say pi; # -> 3.141592653589793
> say pi.FatRat; # -> 3.141593
> say pi.FatRat.raku; # -> FatRat.new(355, 113)
> say 355/113; # -> 3.141593
The «Rat::Precise» module «stringifies Rats to a configurable precision» and may be useful. Let us test it:
> use Rat::Precise;
> say pi.FatRat.precise; # -> 3.14159292035398230088495575221239
> say pi; # -> 3.141592653589793
# 3.141592653589793238462643383279502884197169...
The difference is quite significant! According to the experts (whose value is shown on the third line; at least partial as they present pi with a million digits), the unmodified «pi» is right. So the conclusion is that «Rat::Precise» isn't very precise.
The square root in the Ramanujan Constant is a victim of the same problem:
> say sqrt(163); # -> 12.767145334803704
> say sqrt(163).FatRat; # -> 12.767145
> say sqrt(163.FatRat); # -> 12.767145334803704
> say sqrt(163.FatRat).FatRat; # -> 12.767145
Conclusion: This isn't going to work out.
Which operators cause problems (by truncating the value) is above my pay grade, but we can use an operator that clearly isn't affected; assignment:
File: ramanujan (with a newline added to make it fit the screen)
my FatRat $ramanujan = 262537412640768743.9999999999992500725971981856888\
793538563373369908627075374103782106479101186073129511813461860645042.FatRat;
say $ramanujan;
The value has 119 characters (18 digits before the decimal point, the decimal point itself, and finally 100 digits after the decimal point). This clearly satisfies the demand for «at least 32 digits of precision». That is digits after the decimal point.
I got this 118 digit number from the Raku section of rosettacode.org/wiki/Ramanujan's_constant. And if you think that my solution is cheating, take a look at the (non-cheating) solutions presented by Rosetta Code.
my FatRat $pi = 3.14159265358979323 ... 9216420198938095.FatRat;
my FatRat $e = 2.718281828459045235 ... 889570350354.FatRat;
say $e ** ($pi * sqrt(163));
I got the value of «e» from www.miniwebtool.com/first-n-digits-of-e
Running it gives exactly the same result as when we used the built in
«pi
» and «e
», and the value is a «Num».
The problem is that the «sqrt
» call gives us a «Num», and that
leads to the result also being a «Num». Coercing it to «FatRat» doesn't help,
as it is done after the fact. And besides the square root is horribly truncated.
Andrew Shitow's FatRat vs Rat in Perl 6 article presents «Newton’s method of finding an approximate value of a square root», and gives us a «FatRat value». I wrapped it up as a procedure:
File: ramanujan2 (changes only)
sub FatRatRoot (Int $int where $int > 0, :$precision = 10)
{
my @x =
FatRat.new(1, 1),
-> $x { $x - ($x ** 2 - $int) / (2 * $x) } ... *;
return @x[$precision];
}
say $e ** ($pi * FatRatRoot(163));
And guess what? This gives the same annoying «Num» result as well:
$ raku ramanujan2
2.625374126407677e+17
The final problem is the exponentiation operator **
,
which returns a «Num», even if both operands are «FatRat»s. To quote
the documentation for «**»:
«If the right-hand side is a non-negative integer and the left-hand side is an
arbitrary precision type (Int, FatRat), then the calculation is carried out
without loss of precision.»
The right hand side is not an integer, so this doesn't apply. And the calculation is carried out with loss of precision.
So yes, I am ready to give up now.