Perfect at Last
with Raku

by Arne Sommer

Perfect at Last with Raku

[194] Published 31. July 2022.

This is my response to The Weekly Challenge #175.

Challenge #175.1: Last Sunday

Write a script to list Last Sunday of every month in the given year.

For example, for year 2022, we should get the following:

2022-01-30
2022-02-27
2022-03-27
2022-04-24
2022-05-29
2022-06-26
2022-07-31
2022-08-28
2022-09-25
2022-10-30
2022-11-27
2022-12-25

This is almost the same as challenge #13.1 from June 2019 (Sunday instead of Friday this time). See Hofstadter, Friday and Raku for my take on that one.

File: last-sunday
#! /usr/bin/env raku

unit sub MAIN (Int $year = Date.today.year);    # [1]

for 1 .. 12 -> $month                           # [2]
{
  my $date = Date.new($year,                    # [3]
                      $month,
                      Date.new($year, $month, 1).days-in-month);

  $date.=pred while $date.day-of-week != 7;     # [4]

  say $date;                                    # [5]
}

[1] Default to the current year, which we get by getting the year from the current date (set up with Date.today).

See docs.raku.org/type/Date for more information about the Date class.

[2] For each month in the specified year,

[3] Get a Date object for the last day in the month, with another Date call and the days-in-month method to get there.

[4] Subtract one day at a time (from the object), as long as the day (day-of-week) is anything other than 7 (i.e. Sunday).

[5] Print the date. This stringifies to the required format.

Running it:

$ ./last-sunday
2022-01-30
2022-02-27
2022-03-27
2022-04-24
2022-05-29
2022-06-26
2022-07-31
2022-08-28
2022-09-25
2022-10-30
2022-11-27
2022-12-25

Looking good.

We can try another year:

$ ./last-sunday 2023
2023-01-29
2023-02-26
2023-03-26
2023-04-30
2023-05-28
2023-06-25
2023-07-30
2023-08-27
2023-09-24
2023-10-29
2023-11-26
2023-12-31

As a one-liner, shown here on multiple lines to make sort of it readable;

File: last-sunday-oneliner
#! /usr/bin/env raku

-> $year
{
  -> $month
  {
    -> $date
    {
      say $date.earlier(days => $date.day-of-week == 7 ?? ( 0 ) !! ( $date.day-of-week ) )
    }(Date.new($year, $month, Date.new($year, $month, 1).days-in-month))
  }($_) for 1 .. 12
}(@*ARGS[0] // Date.today.year);

It gives the expected output:

$ ./last-sunday-oneliner
2022-01-30
2022-02-27
2022-03-27
2022-04-24
2022-05-29
2022-06-26
2022-07-31
2022-08-28
2022-09-25
2022-10-30
2022-11-27
2022-12-25

Challenge #175.2: Perfect Totient Numbers

Write a script to generate first 20 Perfect Totient Numbers. Please checkout wikipedia page for more informations.

Output:
3, 9, 15, 27, 39, 81, 111, 183, 243, 255, 327, 363, 471, 729,
2187, 2199, 3063, 4359, 4375, 5571
File: ptm
#! /usr/bin/env raku

unit sub MAIN (Int $count where $count > 0 = 20, :v(:$verbose));  # [1]

my $ptm := (1 .. Inf).grep( *.&is-ptm );                          # [1]

say $ptm[^$count].join(", ");                                     # [1]

sub is-ptm ($number)                                              # [2]
{
  my @totients;                                                   # [3]
  my $c = $number;                                                # [4]

  while $c > 1                                                    # [5]
  {
    $c = totient($c);                                             # [6]
    @totients.push: $c;                                           # [7]
  }

  say ":: $number [@totients[]] sum:{ @totients.sum } \
    { $number == @totients.sum ?? "match" !! "" }" if $verbose;

  return @totients.sum == $number;                                # [8]
}

sub totient ($number)                                             # [9]
{
  my $count = 0;                                                  # [10]

  for 1 .. $number -1 -> $candidate                               # [11]
  {
    $count++ if $number gcd $candidate == 1;                      # [12]
  }

  return $count;                                                  # [13]
}

[1] These lines of code should be familiar by now. If not, see e.g. Disarmed Ranking with Raku for a detailed description.

[2] Is the number in question a Perfect Totient Number?

[3] We do this by collecting the totients (in this list).

[4] We need a writeable copy of the initial number, as we change it iteratively (see [5]) - and we need the initial value (see [8]).

[5] Stop when we reach 1.

[6] Get the Totient number of the current value.

[7] Add the current Totient value to the list.

[8] We have a Perfect Totient Number if the sum of the totients is equal to the number itself.

[9] Totient transformation, as described by Wikipedia.

[10] The count (initially none).

[11] For each value (integer) from 1 to one less than the number itself.

[12] Add one to the count if the greatest common denominator of the number and the loop value is 1.

See docs.raku.org/routine/gcd for more information about the Greatest Common Divisor operator gcd.

[13] Return the count.

Running it:

$ ./ptm 
3, 9, 15, 27, 39, 81, 111, 183, 243, 255, 327, 363, 471, 729, 2187, 2199, \
  3063, 4359, 4375, 5571

Looking good.

Let us have a go at verbose mode, combined with a low number of values (to avoid information overload).

$ ./ptm -v 4
:: 1 [] sum:0
:: 2 [1] sum:1
:: 3 [2 1] sum:3 match
:: 4 [2 1] sum:3
:: 5 [4 2 1] sum:7
:: 6 [2 1] sum:3
:: 7 [6 2 1] sum:9
:: 8 [4 2 1] sum:7
:: 9 [6 2 1] sum:9 match
:: 10 [4 2 1] sum:7
:: 11 [10 4 2 1] sum:17
:: 12 [4 2 1] sum:7
:: 13 [12 4 2 1] sum:19
:: 14 [6 2 1] sum:9
:: 15 [8 4 2 1] sum:15 match
:: 16 [8 4 2 1] sum:15
:: 17 [16 8 4 2 1] sum:31
:: 18 [6 2 1] sum:9
:: 19 [18 6 2 1] sum:27
:: 20 [8 4 2 1] sum:15
:: 21 [12 4 2 1] sum:19
:: 22 [10 4 2 1] sum:17
:: 23 [22 10 4 2 1] sum:39
:: 24 [8 4 2 1] sum:15
:: 25 [20 8 4 2 1] sum:35
:: 26 [12 4 2 1] sum:19
:: 27 [18 6 2 1] sum:27 match
3, 9, 15, 27

I have highlighted the Perfect Totient Numbers in green.

And that's it.