An Imaginary Date
with Raku

by Arne Sommer

An Imaginary Date with Raku

[197] Published 21. August 2022.

This is my response to The Weekly Challenge #178.

Challenge #178.1: Quater-imaginary Base

Write a script to convert a given number (base 10) to quater-imaginary base number and vice-versa. For more informations, please checkout wiki page.

For Example:
$number_base_10 = 4
$number_quater_imaginary_base = 10300

The Quater to Decimal conversion is the easiest of the two, so I'll start with that (but add the scaffolding for the reverse operation):

File: quater-imaginary-base-stubbed
#! /usr/bin/env raku

unit sub MAIN (:b(:$base10), :q(:$quater));                             # [1]

die "Specify one of 'base10' or 'quater'" unless one($base10, $quater); # [2]

say decimal-to-quater($base10) if $base10;                              # [3]
say quater-to-decimal($quater) if $quater;                              # [7a]

sub quater-to-decimal ($number)                                         # [3]
{
  my $decimal = 0;                                                      # [4]
  my $length  = $number.chars;                                          # [4]
  
  for ^$length -> $d                                                    # [5]
  {
    $decimal += $number.substr($d, 1) * (2i ** --$length);              # [5a]
  }

  return $decimal ~~ /(.*)\+0i$/ ?? $decimal.Int !! $decimal;           # [6]
}

sub decimal-to-quater ($number)                                         # [7]
{
  !!! "Not implemented yet"                                             # [8]
}

[1] Specify a decimal (base10) value with «-b», or a quater value with «-q».

[2] Ensure we got exactly one of the values.

[3] The Quater to decimal conversion.

[4] Initial values.

[5] This is straight out of the Wikipedia article; the «Example» part in the «Converting from quater-imaginary» section

[6] We will get an imaginary value, as a result of using «2i» in [5a]. Get rid of the imaginary part, if it is zero.

[7] The decimal to Quater conversion..

[8] Stub code with !!! will fail when executed, with the given error message. If you do not need the message, use ... instead.

See docs.raku.org/type/X::StubCode for more information about the stub code ... and !!! operators.

Running it:

$ ./quater-imaginary-base-stubbed -q=10300
4

$ ./quater-imaginary-base-stubbed -q=1030003
-13

The program does not support non-integer input, though:

$ ./quater-imaginary-base-stubbed -q=10.2
Cannot convert string to number: radix point must be followed by one or more
  valid digits …

Trying to run stubbed code gives the expected error message:

$ ./quater-imaginary-base-stubbed -b=10300
Stub code executed
  in sub decimal-to-quater at ./quater-imaginary-base-stubbed line 24

Then we can fill in the blanks (un-stubbify «decimal-to-quater»). This is based on the javascript code found in the «To negaquaternary» section of en.wikipedia.org/wiki/Negative_base, which we get by following the link at the end of the «Converting into quater-imaginary» section in the Wikipedia article given in the challenge.

File: quater-imaginary-base (changes only)
sub decimal-to-quater ($number)
{
  die "Positive integers only, at this time" unless $number ~~ /^\d+$/;

  constant Schroeppel4 = 0xCCCCCCCC;

  return ( ($number + Schroeppel4 ) +^ Schroeppel4 ).base(4);  # [1]
}

[1] The bitwise XOS Operator has ben renamed to +^ in Raku.

See docs.raku.org/routine/+$CIRCUMFLEX_ACCENT for more information about the infix bitwise XOR (exclusive or) operator +^.

Running it gives the expected result on positive values:

$ ./quater-imaginary-base -b=4
130

$ ./quater-imaginary-base -b=-13
Positive integers only, at this time
  in sub decimal-to-quater at ./quater-imaginary-base line 25

Summary: The program support positive integer input only. That may be a problem, but I have run out of time.

Challenge #178.2: Business Date

You are given $timestamp (date with time) and $duration in hours.

Write a script to find the time that occurs $duration business hours after $timestamp. For the sake of this task, let us assume the working hours is 9am to 6pm, Monday to Friday. Please ignore timezone too.

For Example:
Suppose the given timestamp is 2022-08-01 10:30 and the duration is 4
hours. Then the next business date would be 2022-08-01 14:30.

Similar if the given timestamp is 2022-08-01 17:00 and the duration is
3.5 hours. Then the next business date would be 2022-08-02 11:30.

We had a go at dates in the first part of challenge 175. See Perfect at Last with Raku for my take.

Let us start by visualising this with a clock. The one on the left has the first 12 hours on the circumference, and the last 12 hours somewhat inside this. Thus we start the business day at 09:00 on the outer circle, reaches midday at 12:00 and swithes to the inner circle. We pass 15:00, and reaches the end of the business day at 18:00, both on the inner circle.

24:00 vs 00:00

The 24 hour day (sans seconds) starts at 00:00 and ends at 24:00, seemingly. This clearly is the same moment, so which day does it belong to? Let us see what Raku has to say:

> say DateTime.new('2022-08-20T23:59:00').later(minutes => 1)
2022-08-21T00:00:00Z

The next day. So 24:00 does not exist. Good to know.

We should insist that the start time is within the business hours. Simply because, what should we do if it was not? (Jump forward to the nearest start time, or backwards (thus keeping the day) - and should we keep the minutes, if any?)

The next step is pretending that only business hours exist, so that we have a day that is 9 hour long. We can integer divide the $duration with 9, and get the number of days to add. Then we add the remaining hours (from the integer division). Finally we must ensure that we are still inside the business hours, and I will come back to this part in [6] and [7] below.

File: business-date-int
#! /usr/bin/env raku

unit sub MAIN (Str $timestamp, UInt $duration);                      # [1]

my ($ymd, $hm) = $timestamp.split(" ");                              # [2]

my $dt = DateTime.new($ymd ~ "T" ~ $hm ~ ":00");                     # [3]

die "Only 09:00-18:00 allowed"
  if $dt.hour < 9 || $dt.hour == 18 && $dt.minute || $dt.hour > 18;  # [4]

$dt.=later([days => $duration div 9, hours => $duration % 9]);       # [5]

if $dt.hour < 9                                                      # [6]
{
  $dt.=later(hours => 9);
}
elsif $dt.hour == 18 && $dt.minute || $dt.hour > 18                  # [7]
{
  $dt.=later(hours => 15);
}

say $dt.yyyy-mm-dd ~ " " ~ $dt.hh-mm-ss.substr(0,5);                 # [8]

[1] The timestamp does not map to the DateTime type (see [3]), so we use a string on it. The duration should be a postive number, and UInt ensures a non-negative integer.

See docs.raku.org/type/DateTime for more information about «DateTime».

See docs.raku.org/type/UInt for more information about the Uint type.

[2] Split the timestamp in two; the date and the time/hour parts.

[3] Create a DateTime object given the input. Note the "T" where we had a space in the inoput, and the postfixed zero seconds.

[4] Ensure that the specified timestamp is within the business hours.

[5] Add the number of days (the hours divided by 9, with integer division), and the remaining hours (after we have done the days).

[6] We were inside the business hours before adding the spare hours (in [5]). If we are outside it, but before the start, we simply add 9 hours - as the 9 hours between midnight and 09:00 do not exist in our business hour domain.

[7] If we are outside of the business hours, but after it, we must add 15 hours - which is the number of hours between the end of one business day and the start of the next one.

[8] Print it. (Note that we could have specified a custom formatter, with the formatter argument when we created the object, so that simply printing it would have given this answer. But doing it manually is easier, in this case at least.)

Running it:

$ ./business-date-int "2022-08-01 10:30" 4
2022-08-01 14:30

$ ./business-date-int "2022-08-01 17:00" 3.5
Usage:
  ./business-date-int <timestamp> <duration>

Opps. Fractional hours are not supported, obviously beacuse of UInt. That is fixable:

File: business-date
#! /usr/bin/env raku

unit sub MAIN (Str $timestamp, Str $duration);

my ($duration-hour, $duration-min) = $duration.split(".")>>.Int;  # [1]

$duration-min = $duration-min ?? $duration-min * 6 !! 0;          # [2]

my ($ymd, $hm) = $timestamp.split(" ");

my $dt = DateTime.new($ymd ~ "T" ~ $hm ~ ":00");

die "Only 09:00-18:00 allowed"
  if $dt.hour < 9 || $dt.hour == 18 && $dt.minute || $dt.hour > 18;

$dt.=later([days  => $duration-hour div 9,
            hours => $duration-hour % 9,
            minutes => $duration-min]);                            # [3]

if $dt.hour < 9
{
  $dt.=later(hours => 9);
}
elsif $dt.hour == 18 && $dt.minute || $dt.hour > 18
{
  $dt.=later(hours => 15);
}

say $dt.yyyy-mm-dd ~ " " ~ $dt.hh-mm-ss.substr(0,5);

[1] Split the duration into an hour and a fractional part.

[2] The minutes part. If we started with "3.5" in [1] we get "5" as the fractional part, so we must divide by ten to get the fraction of an hour (i.e. "0.5"). Then we multiply with 60 to get the number of minutes. I have merged these operations.

[3] Add the minutes as well.

Running it:

$ ./business-date "2022-08-01 17:00" 3.5
2022-08-02 11:30

$ ./business-date "2022-08-01 17:00" 3.5
2022-08-02 11:30

Looking good.

And that's it.