The 13th Roman
with Raku

by Arne Sommer

The 13th Roman with Raku

[247] Published 26. July 2023.

This is my response to The Weekly Challenge #227.

Challenge #227.1: Friday 13th

You are given a year number in the range 1753 to 9999.

Write a script to find out how many dates in the year are Friday 13th, assume that the current Gregorian calendar applies.

Example:
Input: $year = 2023
Output: 2

Since there are only 2 Friday 13th in the given year 2023 i.e. 13th Jan
and 13th Oct.
File: friday-13th
#! /usr/bin/env raku

unit sub MAIN (Int $year where 1753 <= $year <= 9999 = Date.today.year, # [1]
               :v(:$verbose));

my $fridays = 0;                                                        # [2]

for 1 .. 12 -> $month                                                   # [3]
{
  my $date      = Date.new(year => $year, month => $month, day => 13);  # [4]
  my $is-friday = $date.day-of-week == 5;                               # [5]

  $fridays++ if $is-friday;                                             # [6]

  say ": $date { $is-friday ?? " Friday" !! ""}" if $verbose;
}

say $fridays;                                                           # [7]

[1] Ensure that the user specified year is within the limits, if given. If not, default to the current year (with Date.today.year).

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

[2] The result will end up here.

[3] Iterate over the months.

[4] Create a Date object for the 13th of each month.

[5] Check if it is indeed a Friday,

[6] and increase the counter if it is.

[7] Print the result (i.e. number of Friday 13ths in the given year).

Running it:

$ ./friday-13th 
2

Looking good.

With verbose mode:

$ ./friday-13th -v
: 2023-01-13  Friday
: 2023-02-13 
: 2023-03-13 
: 2023-04-13 
: 2023-05-13 
: 2023-06-13 
: 2023-07-13 
: 2023-08-13 
: 2023-09-13 
: 2023-10-13  Friday
: 2023-11-13 
: 2023-12-13 
2

We can make it shorter, with grep instead of the explicit for loop:

File: friday-13th-grep
#! /usr/bin/env raku

unit sub MAIN (Int $year where 1753 <= $year <= 9999 = Date.today.year);

say (1 .. 12).grep({Date.new(year => $year, month => $_,
                             day => 13).day-of-week == 5 }).elems;

Running it gives the expected result:

$ ./friday-13th-grep
2

Challenge #227.2: Roman Maths

Write a script to handle a 2-term arithmetic operation expressed in Roman numeral.

Example:
IV + V     => IX
M - I      => CMXCIX
X / II     => V
XI * VI    => LXVI
VII ** III => CCCXLIII
V - V      => nulla (they knew about zero but didn't have a symbol)
V / II     => non potest (they didn't do fractions)
MMM + M    => non potest (they only went up to 3999)
V - X      => non potest (they didn't do negative numbers)

This is very similar to the first part of Challenge 47: «Roman Calculator»; see Roman Gap with Raku for my take on it. The following code is a minor adaptation of code presented there, and I will only comment the changes.

We start off with a module with procedures converting to and from Roman numerals.

File: lib/Number/Roman.rakumod
unit module Number::Roman;

our sub to-roman (Numeric $number is copy) is export(:to)  # [1]
{
  return "nulla" if $number == 0;                          # [2]
  return "non potest" unless 0 < $number < 3999;           # [3]
  return "non potest" unless $number.Int == $number;       # [4]

  my $string = "";
  
  while $number >= 1000 { $string ~= "M";  $number -= 1000; }
  if $number >= 900     { $string ~= "CM"; $number -= 900; }
  if $number >= 500     { $string ~= "D";  $number -= 500; }
  if $number >= 400     { $string ~= "CD"; $number -= 400; }
  while $number >= 100  { $string ~= "C";  $number -= 100; }
  if $number >= 90      { $string ~= "XC"; $number -= 90; }
  if $number >= 50      { $string ~= "L";  $number -= 50; }
  if $number >= 40      { $string ~= "XL"; $number -= 40; }
  while $number >= 10   { $string ~= "X";  $number -= 10; }
  if $number >= 9       { $string ~= "IX"; $number -= 9; }
  if $number >= 5       { $string ~= "V";  $number -= 5; }
  if $number >= 4       { $string ~= "IV"; $number -= 4; }
  while $number >= 1    { $string ~= "I";  $number -= 1; }

  return $string;
}

my %value = (I => 1, V => 5, X => 10, L => 50, C => 100, D => 500, M => 1000);

my Set $valid-roman = %value.keys.Set;

my $current-value = Inf;

our sub from-roman (Str $roman) is export(:from)
{
  my @digits = $roman.comb;

  die "Non-Roman digit $_ detected." unless $valid-roman{$_} for @digits;

  my $number = 0;

  while @digits
  {
    my $current = @digits.shift;
        
    if @digits.elems
    {
      if %value{@digits[0]} > %value{$current}
      {
        $number += %value{@digits.shift} - %value{$current};
        next;
      }
    }
    $number += %value{$current};
  }

  return to-roman($number) eq $roman
    ?? $number
    !! die "Not a valid Roman Number: $roman";
}

[1] The type is now Numeric instead of Int. That makes it possible to pass on rational numbers (as e.g. 2.5 in the V / II example).

[2] Zeroes are illegal, return an error message.

[3] Negative vales, and values larger than 3999 are also illegal.

[4] Detect non-integer values.

The main program:

File: roman-maths
#! /usr/bin/env raku

use lib "lib";                           # [1]

use Number::Roman :to, :from;

unit sub MAIN (Str $first, Str $operator, Str $second);

my $f = from-roman($first);
my $s = from-roman($second);

given $operator
{
  when '+'  { say to-roman($f +  $s) };
  when '-'  { say to-roman($f -  $s) };
  when 'x'  { say to-roman($f *  $s) };  # [2]
  when '*'  { say to-roman($f *  $s) };
  when 'xx' { say to-roman($f ** $s) };  # [3]
  when '**' { say to-roman($f ** $s) };  # [3]
  when '/'  { say to-roman($f / $s) };
  default   { die "unknown operator"; }  # [4]
}

[1] The module is located in this directory.

[2] The * operation has this alias, so that we can drop the quotes (as the shell interferes with unquoted *).

[3] This operation in new in this version of the program.

[4] Error catching with default on the given/when structure was missing in the original program. My bad.

See docs.raku.org/language/control#index-entry-switch_(given) for more information about given/when.

See docs.raku.org/language/control#default_and_when for more information about default.

Running it:

$ ./roman-maths IV + V
IX

./roman-maths M - I
CMXCIX

$ ./roman-maths X / II
V

$ ./roman-maths XI x VI
LXVI

$ ./roman-maths VII xx III
CCCXLIII

$ ./roman-maths V - V
nulla

$ ./roman-maths V / II
non potest

$ ./roman-maths MMM + M
non potest

$ ./roman-maths V - X
non potest

Looking good.

And that's it.