The Workdays are Numbered
with Raku (and Perl)

by Arne Sommer

The Workdays are Numbered with Raku (and Perl)

[154] Published 14. November 2021.

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

Challenge #138.1: Workdays

You are given a year, $year in 4-digits form.

Write a script to calculate the total number of workdays in the given year.

For the task, we consider, Monday - Friday as workdays.

Example 1:
Input: $year = 2021
Output: 261
Example 2:
Input: $year = 2020
Output: 262

The calender used is the Gregorian calendar, which replaced the Julian calendar from as early as 1582 - depending on country. Dates before the adoption are left as they are. This leads to a fine mess, which I have ignored. The program assumes a Gregorian calender all the way back to year 0 (or 0000, as you would have to specify it).

File: workdays
#! /usr/bin/env raku

unit sub MAIN (Int $year where $year.chars == 4);   # [1]

my $d         = Date.new("$year-01-01");            # [2]
my $first-day = $d.day-of-week;                     # [3]

my @workdaysinyear =                                # [4]
(
  0,   # dummy
  261, # Monday    - Monday
  261, # Tuesday   - Tuesday
  261, # Wednesday - wednesday
  261, # Thursday  - Thursday
  261, # Friday    - Friday
  260, # Saturday  - Saturday
  259, # Sunday    - Sunday
);

my @add-leap =                                      # [5]
(
  0, # Dummy
  1, # Mo -> Tu
  1, # Tu -> We
  1, # We -> Th
  1, # Th -> Fr
  0, # Fr -> Sa
  0, # Sa -> Su
  1, # Su -> Ma
);

my $count = @workdaysinyear[$first-day]             # [6]
 + ($d.is-leap-year ?? @add-leap[$first-day] !! 0);

say $count;

[1] Ensure a four digit number as input. Leading zeroes are permitted.

[2] Start with the first day of the specified year,

[3] and get which day of the week it is. (Monday is 1, up to Sunday which is 7.)

[4] The number of workdays in a given year, based on 365 days/year (i.e. ignoring leap years). 7 days * 52 weeks = 364 days. So we have to add on a single day to get to 365. The numer og working days is 5 * 52 = 260 + the extra day, if they are all workingdays. They are not. The table has the values. The first day in the comment is the first day of the year, and the second is the last.

[5] If the year is a leap year, does the extra day (number 365) land on a working day?

[6] Get the number of days + any leap year working day.

Running it:

$ ./workdays 2021
261

$ ./workdays 2020
262

A Perl Version

This is straight forward translation of the Raku version.

File: workdays-perl
#! /usr/bin/env perl

use strict;
use warnings;
use feature 'say';
use DateTime;

my $year = shift(@ARGV) // "";

die "Please specify a 4 digit year" unless $year =~ /^\d\d\d\d$/;

my $dt        = DateTime->new(year => $year, month => 1, day => 1);
my $first_day = $dt->day_of_week;

my @workdaysinyear =
(
  0,   # dummy 
  261, # Monday    - Monday
  261, # Tuesday   - Tuesday
  261, # Wednesday - wednesday
  261, # Thursday  - Thursday
  261, # Friday    - Friday
  260, # Saturday  - Saturday
  259, # Sunday    - Sunday
);

my @add_leap =
(
  0, # Dummy
  1, # Mo -> Tu
  1, # Tu -> We
  1, # We -> Th
  1, # Th -> Fr
  0, # Fr -> Sa
  0, # Sa -> Su
  1, # Su -> Ma
);

my $count = $workdaysinyear[$first_day]
  + ($dt->is_leap_year ? $add_leap[$first_day] : 0);

say $count;

Running it gives the same result as the Raku version:

$ ./workdays-perl 2021
261

$ ./workdays-perl 2020
262

Challenge #138.2: Split Number

You are given a perfect square.

Write a script to figure out if the square root the given number is same as sum of 2 or more splits of the given number.



Example 1:
Input: $n = 81
Output: 1

Since, sqrt(81) = 8 + 1
Example 2:
Input: $n = 9801
Output: 1

Since, sqrt(9801) = 98 + 0 + 1
Example 3:
Input: $n = 36
Output: 0

Since, sqrt(36) != 3 + 6

A perfect square is the result of squaring another integer.

File: split-number
#! /usr/bin/env raku

unit sub MAIN (Int $psq where $psq.sqrt.Int == $psq.sqrt && $psq > 9,  # [1]
  :v(:$verbose));

my $sqrt  = $psq.sqrt;                                                 # [2]
my $size  = $psq.chars;                                                # [3]

say ": Square root of $psq = $sqrt" if $verbose;

my $found = False;                                                     # [4]

my $splits := gather                                                   # [5]
{
  recurse( $psq, () );                                                 # [5a]
}

for $splits -> $candidate                                              # [6]
{
  my @list = @$candidate;                                              # [6a]

  if @list.elems == 1                                                  # [7]
  {
     say ": Candidate list: { @list.join(", ") } (ignored size)" if $verbose;
     next;
  }

  my $sum = @list.sum;                                                 # [8]

  if $sqrt == $sum                                                     # [8a]
  {
    say ": Candidate list: { @list.join(", ") } - with correct sum $sqrt"
      if $verbose;

    say 1 unless $found;                                               # [8b]
    
    $verbose
      ?? ( $found = True )
      !! exit;                                                         # [8c]
  }
  else
  {
    say ": Candidate list: { @list.join(", ") } (wrong sum $sum vs \
      expected $sqrt)" if $verbose;
  }
}

say 0 unless $found;                                                   # [9]

sub recurse ($remainder, @done)                                        # [10]
{
  for 1 .. $remainder.chars -> $count                                  # [11]
  {
    my $partial = $remainder.substr(0, $count);                        # [12]
    my $new     = $remainder.substr($count);                           # [13]
    my @so-far  = @done.clone;                                         # [14]
    
    @so-far.push: $partial;                                            # [15]

    $new eq ""                                                         # [16]
      ?? take @so-far                                                  # [16a]
      !! recurse($new, @so-far);                                       # [16b]
  }
}

[1] Ensure that the given integer is a perfect square. Also, that it has at least to digits (because of «2 or more splits of the given number», which requires at least 2 digits).

[2] The quare root.

[3] The number of digits (length of the string).

[4] In case of verbose mode, where we go through all the candidates even after finding a match.

[5] Setting up the sequence with gather/take is ideal here, with a recurisve procedure. The actual take is hidden away in the procedure (see [16a]).

See my Raku Gather, I Take article or docs.raku.org/syntax/gather take for more information about gather/take.

[6] For each candidate, which is a list of values (substrings).

[7] Skip lists with 1 element (i.e. no split at all).

[8] Get the sum. Do we have a match [8a]? If so, print «1» [8b] and exit [8c].

[9] No match? Then say so.

[10] The recursive procedure. The first argument is the remainder of the original number that has not been processes yet. The second is the list of splits so far in this iteration.

[11] We can have from 1 to the entire string characters in each split. (We do not need the whole string, but it felt right to make a general procedure).

[12] Get the new partial,

[13] and the new remainder.

[14] The list of splits, which we clone so that we do not clobber up the values for the other iterations.

See docs.raku.org/routine/clone for more information about the clone method.

[15] Add the new partial to the list.

[16] No remainder? If so return the value (with take).

Running it:

$ ./split-number 81
1

$ ./split-number 9801
1

$ ./split-number 36
0

Looking good.

With verbose mode:

$ ./split-number -v 81
: Square root of 81 = 9
: Candidate list: 8, 1 - with correct sum 9
1
: Candidate list: 81 (ignored size)

$ ./split-number -v 9801
: Square root of 9801 = 99
: Candidate list: 9, 8, 0, 1 (wrong sum 18 vs expected 99)
: Candidate list: 9, 8, 01 (wrong sum 18 vs expected 99)
: Candidate list: 9, 80, 1 (wrong sum 90 vs expected 99)
: Candidate list: 9, 801 (wrong sum 810 vs expected 99)
: Candidate list: 98, 0, 1 - with correct sum 99
1
: Candidate list: 98, 01 - with correct sum 99
: Candidate list: 980, 1 (wrong sum 981 vs expected 99)
: Candidate list: 9801 (ignored size)

$ ./split-number -v 36
: Square root of 36 = 6
: Candidate list: 3, 6 (wrong sum 9 vs expected 6)
: Candidate list: 36 (ignored size)
0

Perl

No perl version of this program this week.

And that's it.