Wherein we stay busy, busy, busy every day and weekends don’t count.
THE WEEKLY CHALLENGE – PERL & RAKU #178 Task 2
“My own business always bores me to death; I prefer other people’s.”
― Oscar Wilde
Business Date
Submitted by: Mohammad S Anwar
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.
ANALYSIS
There’s a lot of moving parts to this particular challenge. We need to start with inputting a timestamp string, and then add hours to it, carrying over into the next day as required until we land within the business hours of a single day. Weekends are excluded as well, so if we roll off the clock on Friday we resume Monday morning. And of course we’re only counting the hours elapsed within a limited frame of 9am to 6pm.
Fortunately the directionality of time limits our duration hours to positive values. So there is that. And seconds: no seconds, or nanoseconds, or timezones, as noted — but I can’t figure out how timezones would come into play anyway. Are we doing business on the Concorde?
To further stabilize the groundwork, I don’t think we’re going to worry about either lunch breaks or holidays either, and we’re going to hold fast to the given input timestamp format, without proper validation or anything. Life, and wrangling the pieces of this task, is complicated enough.
We’re also going to make one more simplifying assumption: that the timestamp input occurs within some business day. In a real program we’d surely want to verify this, and start the clock at the next business day, but if the ticket was submitted by the business personnel then the business would need to be open, no? Once we start second-guessing our human engineering, the whole thing degenerates into a thicket of hairy edge-cases. In real life, again, we’d want to do this, but it’d be quite distracting here.
METHOD
As the day of the week figures in as a very important part of the problem, and this in turn is subject to arcane leap-year rules in February, I think it prudent to armor ourselves under the protective aegis of a
module designed specifically for date and time manipulation. Using the objects in the DateTime
module we can poll the day-of-the-week value directly. However we can’t, I note, just add the specified hours to find a new date, as all hours are equal but some hours are more equal than others.
It seems a good strategy would be to break down the specified duration into 3 components. It also seems wise to convert our interval into minutes from the get-go, to avoid a lot of converting back and forth. One workday, then, is 9hours or 540 minutes.
The breakdown:
- the minutes remaining in the current day
- a series of 540-minute intervals comprising complete workdays
- a remaining portion of less than 540 minutes to place the endpoint timestamp
We will start by parsing the input timestamp into a new DateTime
object and commence deriving the three components from there. Which is fine, but DateTime
doesn’t actually do that. We’ll need something more, something else.
And what, pray tell, would that be?
As it work out, DateTime
does not parse dates because there are just way too many varieties to choose from when writing them down. So instead of trying to implement this functionality within the date module directly, an entire class of classes takes on this role instead: DateTime::Format
. This in turn is subclassed further with whatever scheme you might wish to accommodate.
We will choose DateTime::Format::Strptime
for our format, which basically implements the unix utilities strptime
and strftime
, allowing one to create a formatting template from a list of symbol options, including shortcuts for some of the more common layouts like year-month-day.
Once the timestamp is parsed we can then create another DateTime
object for the end of whatever business day we are in, and subtracting the earlier from the later create a duration until the end of the day.
If the end of the day is after the given span, we add the minutes to the current day and report. If we need to jump to the next day things get a little more complicated.
We need to first reset by removing the reamining mnutes for the current day from the count, ans start a new day object at 9am. This will be initialized at the next business day, skipping hte weekend if necessary. From there we subtract entire days in minutes, 540 each, until the span is less than 540, again skipping weekends if necessary. This locates the end date.
Once we have the end date we add the remaining minute count of the duration to obtain the end time within the day.
PERL 5 SOLUTION
The control flow is noted in the comments.
use warnings;
use strict;
use utf8;
use feature ":5.26";
use feature qw(signatures);
no warnings 'experimental::signatures';
use DateTime;
use DateTime::Format::Strptime ;
my ($timestamp, $delta) = @ARGV;
if (@ARGV == 0) { ($timestamp, $delta) = ( '2022-08-01 10:30', 400 )}
## convert to minutes
my $duration_minutes = $delta * 60;
## parse input and create a DateTime object for the timestamp
my $format = DateTime::Format::Strptime->new(
pattern => '%F %H:%M');
my $date = $format->parse_datetime($timestamp);
## calculate remaining minutes in current day
my $day_end = DateTime->new(
year => $date->year,
month => $date->month,
day => $date->day,
hour => 18,
minute => 0
);
my $date_remaining_duration = $day_end->subtract_datetime($date);
my $remaining_minutes_today = $date_remaining_duration->hours * 60 +
$date_remaining_duration->minutes;
## CASE 1: duration falls within current day
##
if ($duration_minutes <= $remaining_minutes_today) {
$date->add( minutes => $duration_minutes );
}
## CASE 2: we finish this day and locate the ending day
##
else {
## subtract remaining time within current day
$duration_minutes -= $remaining_minutes_today;
## start a new day to the next business day
$date->set_hour( 9 );
$date->set_minute( 0 );
$date->add( days => ($date->day_of_week == 5 ? 3 : 1));
## add any complete days, skipping weekends
while ($duration_minutes > 540) { ## 540 minutes in 9-hour day
$date->add( days => ($date->day_of_week == 5 ? 3 : 1));
$duration_minutes -= 540;
}
## add any remaining minutes forward from 9am on the end day
$date->add( minutes => $duration_minutes );
}
## output timestamp as per format
$date->set_formatter($format);
say $date->stringify;
The Perl Weekly Challenge, that idyllic glade wherein we stumble upon the holes for these sweet descents, is now known as
The Weekly Challenge – Perl and Raku
It is the creation of the lovely Mohammad Sajid Anwar and a veritable swarm of contributors from all over the world, who gather, as might be expected, weekly online to solve puzzles. Everyone is encouraged to visit, learn and contribute at