Brazilian Means
with Raku

by Arne Sommer

Brazilian Means with Raku

[176] Published 24. March 2022. Updated 5. April 2022.

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

Challenge #157.1: Pythagorean Means

You are given a set of integers.

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.

Example 1:
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)

Example 2:
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
File: pythagorean-means
#! /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.

5. April Update

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.

Challenge #157.2: Brazilian Number

You are given a number $n > 3.

Write a script to find out if the given number is a 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.

File: brazilian-number
#! /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:

File: brazilian-number-poly
#! /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.