This is my response to the Perl Weekly Challenge #157.
Write a script to compute all three Pythagorean Means
i.e Arithmetic Mean,
Geometric Mean and Harmonic Mean of the given set of integers. Please refer to wikipedia
page for more informations.
Input: @n = (1,3,5,6,9)
Output: AM = 4.8, GM = 3.8, HM = 2.8
CORRECTION [2022-03-21 16:35] GM = 3.9 (before)
Input: @n = (2,4,6,8,10)
Output: AM = 6.0, GM = 5.2, HM = 4.4
Example 3:
Input: @n = (1,2,3,4,5)
Output: AM = 3.0, GM = 2.6, HM = 2.2
#! /usr/bin/env raku
unit sub MAIN (*@n where @n.elems > 0 && all(@n) ~~ Int); # [1]
my $n = @n.elems; # [2]
my $am = (@n.sum / $n); # [3]
my $gm = ([*] @n).abs.roots($n)[0].Real; # [4]
my $hm = $n / @n.map( { 1/$_ } ).sum; # [5]
say "AM = { $am.round(0.1).fmt('%.1f') }, ", # [6]
"GM = { $gm.round(0.1).fmt('%.1f') }, ", # [6]
"HM = { $hm.round(0.1).fmt('%.1f') }"; # [6]
[1]
A slurpy array (*@n
) takes any number of arguments, including
zero. So we slap on a lower size limit of 1 (@n.elems > 0
) and ensure that
they are all integers with an all
Junction and smartmatching it against the
integer type (all(@n) ~~ Int
).
See
docs.raku.org/routine/all
for more information about the all
Junction.
[2] Get the number of elements (as we need it twice).
See
https://docs.raku.org/routine/elems
for more information about elems
.
[3] The first one is easy; the sum of all the values divided by the number of elements.
See
docs.raku.org/routine/sum
for more information about sum
.
[4]
The second one is trickier. We start by multiplying all
the values together, using the Reduction Metaoperator []
to - in this
case - multiply the values together. Then we remove the sign, if any, with
abs
(absolute value). Raku has a function to compute the nth
root, roots
, so we use that. Unfortunately for us it returns complex
numbers. The first return value is the one we want, so we apply an index to get it.
This is the imaginary part of a complex number, so we get rid of the "i" postfix
by coercing the value to Real
.
See
docs.raku.org/language/operators#Reduction_metaoperators for more
information about the Reduction Metaoperator []
.
See
docs.raku.org/routine/abs
for more information about abs
.
See
docs.raku.org/routine/roots
for more information about roots
.
See
docs.raku.org/routine/real
for more information about the real
method.
[5] Here we use map
to get 1 divided by each number.
Then we add them together with sum
and divide the number of values by
that sum.
See
docs.raku.org/routine/map
for more information about map
.
[6]
The challenge wanted the values with one digit after the comma, so
we round them off with round(0.1)
. We want that digit after the comma
to be shown even if it is zero, so use fmt('%.1f')
- which is the method
form of sprintf
to do so.
See
docs.raku.org/routine/fmt for
more information about fmt
.
See
docs.raku.org/routine/round for
more information about round
.
Running it:
$ ./pythagorean-means 1 3 5 6 9
AM = 4.8, GM = 3.8, HM = 2.8
$ ./pythagorean-means 2 4 6 8 10
AM = 6, GM = 5.2, HM = 4.4
$ ./pythagorean-means 1 2 3 4 5
AM = 3, GM = 2.6, HM = 2.2
Looking good.
As gfldex pointed out in Reddit, we can use a hyper operator on HM. The amended program looks like this:
File: pythagorean-means-hyper
#! /usr/bin/env raku
unit sub MAIN (*@n where @n.elems > 0 && all(@n) ~~ Int);
my $n = @n.elems;
my $am = (@n.sum / $n);
my $gm = ([*] @n).abs.roots($n)[0].Real;
my $hm = $n / ( [+] 1 «/« @n );
say "AM = { $am.round(0.1).fmt('%.1f') }, ",
"GM = { $gm.round(0.1).fmt('%.1f') }, ",
"HM = { $hm.round(0.1).fmt('%.1f') }";
Running it gives the same result - but it does not speed up the program:
$ time ./pythagorean-means 1 3 5 6 9
AM = 4.8, GM = 3.8, HM = 2.8
real 0m0,290s
user 0m0,374s
sys 0m0,037s
$ time ./pythagorean-means-hyper 1 3 5 6 9
AM = 4.8, GM = 3.8, HM = 2.8
real 0m0,290s
user 0m0,378s
sys 0m0,033s
Both programs uses concurrency, as shown by the sum of «user» and «sys» beeing larger than «real».
So the hyper operator does not help us in this case, and the program is harder to understand.
$n > 3
.
Brazilian Number
.
A positive integer number N has at least one natural number B where 1 < B < N-1 where the representation of N in base B has same digits.Example 1:
Input: $n = 7
Output: 1
Since 7 in base 2 is 111.
Example 2:
Input: $n = 6
Output: 0
Since 6 in base 2 is 110,
6 in base 3 is 20 and
6 in base 4 is 12.
Example 3:
Input: $n = 8
Output: 1
Since 8 in base 3 is 22.
Raku has the base
method that can convert a (decimal) number
to any base from 2 to 36 (both included). The upper limit is the result of adding the 26
letters to the 10 digits. This means that the following program will not support higher
bases than 36.
See
docs.raku.org/routine/base
for more information about base
.
#! /usr/bin/env raku
unit sub MAIN (Int $n where 3 < $n <= 38, :v(:$verbose)); # [1]
for 2 .. $n -2 -> $base # [2]
{
my $digits = $n.base($base); # [3]
say ":N:$n in base $base -> $digits" if $verbose;
if [eq] $digits.comb { say 1; exit; } # [4]
}
say 0; # [5]
[1] The lower limit is 4 (as given in the challenge). The upper limit is 38, as that leads to base 36 (the limit minus 2 in [2]).
[2] Iterate over the possible bases (from 2 to $n -2
, as given by
< N-1
).
[3] Get the number in the specified base.
[4] Split the number into a string of single digits (in the current base, so can be digits
as well as letter). Then we compare them all with eachother with the Reduction Metaoperator
[eq]
. If they are all the same, we print 1 and exit.
[5] No match. We have failed. Print 0.
Running it:
$ ./brazilian-number -v 7
:N:7 in base 2 -> 111
1
$ ./brazilian-number -v 6
:N:6 in base 2 -> 110
:N:6 in base 3 -> 20
:N:6 in base 4 -> 12
0
$ ./brazilian-number -v 8
:N:8 in base 2 -> 1000
:N:8 in base 3 -> 22
1
Spot on.
Let us try a high number:
$ ./brazilian-number -v 38
:N:38 in base 2 -> 100110
:N:38 in base 3 -> 1102
:N:38 in base 4 -> 212
:N:38 in base 5 -> 123
:N:38 in base 6 -> 102
:N:38 in base 7 -> 53
:N:38 in base 8 -> 46
:N:38 in base 9 -> 42
:N:38 in base 10 -> 38
:N:38 in base 11 -> 35
:N:38 in base 12 -> 32
:N:38 in base 13 -> 2C
:N:38 in base 14 -> 2A
:N:38 in base 15 -> 28
:N:38 in base 16 -> 26
:N:38 in base 17 -> 24
:N:38 in base 18 -> 22
:N:38 in base 19 -> 20
:N:38 in base 20 -> 1I
:N:38 in base 21 -> 1H
:N:38 in base 22 -> 1G
:N:38 in base 23 -> 1F
:N:38 in base 24 -> 1E
:N:38 in base 25 -> 1D
:N:38 in base 26 -> 1C
:N:38 in base 27 -> 1B
:N:38 in base 28 -> 1A
:N:38 in base 29 -> 19
:N:38 in base 30 -> 18
:N:38 in base 31 -> 17
:N:38 in base 32 -> 16
:N:38 in base 33 -> 15
:N:38 in base 34 -> 14
:N:38 in base 35 -> 13
:N:38 in base 36 -> 12
0
What about bases > 36? The first problem is which symbols to use to represent the values. 0-9 and A-Z have been used so far, according to standard practice. But there are no consensus about where to go after that.
But the challenge does not actually care about the representation, only the result
of the comparison. So we can use the values in decimal form (where they have one
or more digits), and compare them numerically. The raku function polymod
is ideal here:
#! /usr/bin/env raku
unit sub MAIN (Int $n where $n > 3, :v(:$verbose)); # [1]
for 2 .. $n -2 -> $base
{
my @digits = $n.polymod($base, $base ... *); # [2]
say ":N:$n in base $base -> { @digits.join(",") }" if $verbose;
if [==] @digits { say 1; exit; } # [3]
}
say 0;
[1] No upper limit this time.
[2] We use polymod
to get the digit values (in decimal).
The first digit is the result of integer divison by the first value in the argument list
(which is $base
). The remainder is divided by the second (which also is
$base
). Then we use a Sequence to generate as many more of $base
as required.
See
docs.raku.org/routine/polymod for more information about polymod
.
[3] We have numbers this time, so numeric comparison (with ==
) is the thing.
Running it:
$ ./brazilian-number-poly -v 7
:N:7 in base 2 -> 1,1,1
1
$ ./brazilian-number-poly -v 6
:N:6 in base 2 -> 0,1,1
:N:6 in base 3 -> 0,2
:N:6 in base 4 -> 2,1
0
$ ./brazilian-number-poly -v 8
:N:8 in base 2 -> 0,0,0,1
:N:8 in base 3 -> 2,2
1
Let us show off the support for higher bases:
$ ./brazilian-number-poly -v 99
:N:99 in base 2 -> 1,1,0,0,0,1,1
:N:99 in base 3 -> 0,0,2,0,1
:N:99 in base 4 -> 3,0,2,1
:N:99 in base 5 -> 4,4,3
:N:99 in base 6 -> 3,4,2
:N:99 in base 7 -> 1,0,2
:N:99 in base 8 -> 3,4,1
:N:99 in base 9 -> 0,2,1
:N:99 in base 10 -> 9,9
1
$ ./brazilian-number-poly -v 100
:N:100 in base 2 -> 0,0,1,0,0,1,1
:N:100 in base 3 -> 1,0,2,0,1
:N:100 in base 4 -> 0,1,2,1
:N:100 in base 5 -> 0,0,4
:N:100 in base 6 -> 4,4,2
:N:100 in base 7 -> 2,0,2
:N:100 in base 8 -> 4,4,1
:N:100 in base 9 -> 1,2,1
:N:100 in base 10 -> 0,0,1
:N:100 in base 11 -> 1,9
:N:100 in base 12 -> 4,8
:N:100 in base 13 -> 9,7
:N:100 in base 14 -> 2,7
:N:100 in base 15 -> 10,6
:N:100 in base 16 -> 4,6
:N:100 in base 17 -> 15,5
:N:100 in base 18 -> 10,5
:N:100 in base 19 -> 5,5
Note that the resulting array from
polymod
is reversed (as compared to the positional values the digits
represent), but that does not matter as the order is irrelevenat in a comparison.
Verbose mode will show it:
$ ./brazilian-number -v 8
:N:8 in base 2 -> 1000
:N:8 in base 3 -> 22
1
$ ./brazilian-number-poly -v 8
:N:8 in base 2 -> 0,0,0,1
:N:8 in base 3 -> 2,2
1
And finally, let us do the Sequence:
File: brazilian-number-seq
#! /usr/bin/env raku
unit sub MAIN (Int $count where $count > 0);
(4..Inf).grep( *.&is-brazilian ).head($count).join(", ").say;
sub is-brazilian ($number)
{
for 2 .. $number -2 -> $base
{
my @digits = $number.polymod($base, $base ... *);
return True if [==] @digits;
}
return False;
}
It should not require any addition explanation. Note that the return True
statement is easier on the eye than the block in the previous programs.
Running it:
$ ./brazilian-number-seq 10
7, 8, 10, 12, 13, 14, 15, 16, 18, 20
$ ./brazilian-number-seq 100
7, 8, 10, 12, 13, 14, 15, 16, 18, 20, 21, 22, 24, 26, 27, 28, 30, 31, 32, 33,\
34, 35, 36, 38, 39, 40, 42, 43, 44, 45, 46, 48, 50, 51, 52, 54, 55, 56, 57,\
58, 60, 62, 63, 64, 65, 66, 68, 69, 70, 72, 73, 74, 75, 76, 77, 78, 80, 81,\
82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 96, 98, 99, 100, 102, 104,\
105, 106, 108, 110, 111, 112, 114, 115, 116, 117, 118, 119, 120, 121, 122,\
123, 124, 125, 126, 127, 128, 129, 130, 132
And that's it.