This is my response to The Weekly Challenge #227.
Input: $year = 2023
Output: 2
Since there are only 2 Friday 13th in the given year 2023 i.e. 13th Jan
and 13th Oct.
#! /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:
#! /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
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.