Perl Weekly Challenge: Week 178
Challenge 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
This is the hardest challenge I've tried in a long time. It involves complex numbers and the like and for me a complex number is any that can't be counted on fingers and toes. Mathematicians have a slightly different definition and they use complex numbers for all kinds of interesting purposes.
Take the subject of this challenge for instance. Expressing a number in this format means its digits will be from 0 - 3 only which means it can be encoded in e.g. computer memory in only 2 bits instead of the standard 4 for base 10 or 16. While storage space is not at such a premium as it was at the time that the great Donald Knuth came up with the quater-imaginary base idea, I can see how it might still be useful today.
This offered little comfort in trying to implement especially when the referenced wikipedia page is a wall of forbidding maths. Raku does have built in support for complex numbers and base conversion so I hoped it might be as simple as:
4.base(2i).say;
Unfortunately .base()
only supports bases from 2 - 36. So I decided to punt and use a Module instead
which is something I usually try to avoid. Base::Any supports
imaginary bases so I was able to make this a one-liner after all.
raku -MBase::Any -e 'to-base(@*ARGS[0].Int, 2i).say;'
Unfortunately, it was not quite that simple in Perl. There are plenty of modules for converting between bases on CPAN but none of them support imaginary bases. Math::GSL::Complex looked like it might have done the trick but I was unable to get it to work.
It was then after reading the wiki page yet again I noticed that it said that you could also convert a base 10 number to quater-imaginary by converting it to base -4. My joy was short lived as none of the CPAN modules seem to support negative bases either. I searched elsewhere and found some code for negative bases on RosettaCode This is my adapted version:
It works by converting each base 10 digit to base -4 and adding it to an array. This array will be backwards so as a last step
it is reverse()
d. Also a 0 is placed between each digit when the array is join()
ed back into a string. Surprisingly, this
is all it takes to get a correct answer.
sub quaterImaginaryBase {
my($n) = @_;
my @result;
my $r = 0;
while ($n) {
$r = $n % -4;
$n = floor($n / -4);
if ($r < 0) {
$n++;
$r += 4;
}
push @result, todigits($r, 4) || 0;
}
return join '0', reverse @result;
}
EDIT: after I submitted this, I realized the spec says "...and vice-versa". My code only converts to quater-imaginary base not from. Oh well.
Challenge 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.
While the second challenge required more code, I was on firmer ground so it actually took me a lot less time. I will start with the Perl version first this time.
I used the DateTime module (and indirectly DateTime::Duration. There are just too many corner cases in calendar code to risk rolling your own.
The first step is to parse the timestamp into its individual components. There is a module for that, namely DateTime::Format::Strptime but using a regex is simple enough.
my ($year, $month, $day, $hour, $minute);
if ($timestamp =~ / ^ (\d{4}) [-] (\d{2}) \- (\d{2}) [ ] (\d{2}) [:] (\d{2}) $/msx) {
($year, $month, $day, $hour, $minute) = @{^CAPTURE};
} else {
die "Bad timestamp format\n";
}
If the timestamp is ok, it is used to populate a DateTime
object.
my $start = DateTime->new(
year => $year,
month => $month,
day => $day,
hour => $hour,
minute => $minute
);
Another DateTime
is created to represent the end of a working day.
my $endOfDay = $start->clone->set(hour => 18, minute => 0);
Yet another won represents the end of the duration i.e. $timestamp
+ $duration
.
my $endOfDuration = $start->clone->add(hours => $duration);
If the duration ends before the end of the day all we need to do is print that time. The .strftime()
method
formats it appropriately.
if ($endOfDuration <= $endOfDay) {
say $endOfDuration->strftime('%F %H:%M');
} else {
If it goes over time, we need to find out how much over and apply that difference to the next business day.
my $difference = $endOfDuration - $endOfDay;
say nextBusinessDay($start)->add($difference)->strftime('%F %H:%M');
}
How do we know the next business day? The aptly named nextBusinessDay()
function takes care of that.
sub nextBusinessDay {
It takes the current day as input.
my ($dt) = @_;
A DateTime
object is created which initally has the same value as the input.
my $next = $dt->clone;
If it is a Friday...
if ($dt->day_of_week == 5) {
$next->add(days => 3);
...or Saturday...
} elsif ($dt->day_of_week == 6) {
$next->add(days => 2);
...the appropriate amount of days are added to make the next day Monday. For any other day of the week, one day is added.
} else {
$next->add(days => 1);
}
Finally the time on the next day is set to 9AM. (We are never late for work!) and the new object is returned.
return $next->set(hour => 9, minute => 0)
}
Here's the Raku version. It mostly works the same but Rakus' DateTime
class does things a little differently.
Instead of custom output formatting being included in the class, you have to provide your own as a function which will be passed to the the DateTime
objects .formatter()
method.
sub format($self) {
sprintf "%04d-%02d-%02d %02d:%02d", .year, .month, .day, .hour, .minute
given $self;
}
sub nextBusinessDay(DateTime $dt) {
my $next = DateTime.new(
date => $dt.Date,
hour => 9,
minute => 0,
formatter => &format
);
Instead of .add()
(and .subtract()
), Raku uses .later()
(and .earlier()
) which makes more sense for time related
objects in my opinion.
if ($dt.day-of-week == 5) {
$next = $next.later(days => 3);
} elsif ($dt.day-of-week == 6) {
$next = $next.later(days => 2);
} else {
$next = $next.later(days => 1);
}
return $next;
}
sub MAIN(
Str $timestamp, #= a datetime string in the format YYYY-MM-DD HH:MM
Real $duration #= a duration as a decimal number of hours
) {
my ($year, $month, $day, $hour, $minute);
if $timestamp.match(/ ^ (\d ** 4) '-' (\d ** 2) '-' (\d ** 2) ' ' (\d ** 2) ':' (\d ** 2) $/) {
($year, $month, $day, $hour, $minute) = $/.List;
} else {
die "Bad timestamp format";
}
my $start = DateTime.new(
year => $year,
month => $month,
day => $day,
hour => $hour,
minute => $minute,
formatter => &format
);
my $endOfDay = DateTime.new(date => $start.Date, hour => 18, minute => 0);
For some reason, I was unable to successfully change times in existing objects in any other units than seconds. I didn't stop to investigate why though.
my $endOfDuration = $start.clone.later(seconds => 3_600 * $duration);
if $endOfDuration <= $endOfDay {
say $endOfDuration;
} else {
my $difference = $endOfDuration - $endOfDay;
say nextBusinessDay($start).later(seconds => $difference);
}
}