Imaginaries and the daily grind
Weekly challenge 178 — 15 August 2022
Week 178: 15 Aug 2022
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.
Example 1: 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. Example 2: 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.
This is a task which I have often had to address as a project manager: Fred has a workpackage estimated to take 200 hours. If he starts now, when should he finish? Of course in real life there are complications - bank holidays, annual leave, sick leave, distractions, overtime and so on. However, again in real life, precision is not required, not least because the 200 hour estimate is probably far from exactly correct.
But today's task demands, I think, precision. After a few false starts my submitted algorithm looks like this.
We start with $timestamp
and $duration
. Let's assume that $timestamp
falls within the working week (so not 2am on a Saturday, for example). A little thought suggests that it would be much easier if $timestamp
were 9am on a Monday. So here goes:
Move $timestamp
back to 9am on the preceding Monday, and increase $duration
by the number of working hours we've moved it back. That will be 9 hours for every full day plus the time span between 9am and $timestamp
. So for example, if $timestamp
is 11:00 on Wednesday, we are increasing duration by 18 hours for Monday and Tuesday plus 2 hours on Wednesday.
We could then:
$duration
by 45 (working hours per week) to get a number of full weeks
All that date arithmetic can easily be done using the epoch date (the seconds from 01.01.1970 used by Unix), remembering that a day is 24 * 60 * 60 seconds.
But it doesn't quite work.
It doesn't quite work because of daylight saving time. If the $duration
takes us over the date when the clocks go back or forward, we'll be out by an hour. Can we rely on 'Please ignore timezone too' to ignore that?
Being of a cautious frame of mind, I adapted my algorithm slightly. Instead of just working with the epoch date, I treated the date and the time-of-day separately. I have a function date10_add()
which takes a date (eg 2022-12-25) and adds an integer number of calendar days, and I used that to move forward by the full weeks and extra days from my algorithm above. As the working day is always 9 hours, even on the days the clocks change, we don't need to worry about that.
There is a slight anomaly to my algorithm. If the $duration
ends at 6pm, the algorithm will give 9am on the next working day as the answer. This is of course strictly correct, but perhaps not what is expected.
#!/usr/bin/perl # Peter Campbell Smith - 2022-08-15 # PWC 178 task 2 use v5.26; use utf8; use warnings; use Time::Local; my (@tests, $test, $start, $duration, $d, $m, $y, $h, $i, $s, $date, $day_of_week, $whole_weeks, $whole_days, $end, @t); @tests = ( ['2022-08-17 15:00:00', 93], ['2022-08-17 15:00:00', 45 * 52 + 9], # a year ['2023-01-02 09:00:00', 999999], ['2022-08-22 18:00:00', 0], ['2022-08-17 12:00:00', 1.56]); for $test (@tests) { $start = $test->[0]; $duration = $test->[1]; # hours $start =~ m|(....).(..).(..).(..).(..).(..)|; ($y, $m, $d, $h, $i, $s) = ($1, $2, $3, $4, $5, $6); # start must be in working hours @t = localtime(timelocal($s, $i, $h, $d, $m - 1, $y - 1900)); if ($t[6] == 0 or $t[6] == 6 or $h < 9 or ($h >= 18 and $h.$i.$s ne '180000')) { say qq[\ntimestamp $start not within working hours]; next; } # move back to 9am on the starting day $date = substr($start, 0, 10); $duration += $h - 9 - $i / 60 - $s / 3600 ; # revised duration # move back to preceding Monday 9am @t = localtime(timelocal($s, $i, $h, $d, $m - 1, $y - 1900)); $day_of_week = $t[6]; # 0 = Sunday $date = date10_add($start, 1 - $day_of_week); $duration += ($day_of_week - 1) * 9; # move forward complete weeks $whole_weeks = int($duration / 45); $date = date10_add($date, 7 * $whole_weeks); $duration -= $whole_weeks * 45; # and whole days $whole_days = int($duration / 9); $date = date10_add($date, $whole_days); $duration -= $whole_days * 9; # and the time of day $h = 9 + int($duration); $duration -= int($duration); $duration *= 3600; # seconds $i = int($duration / 60); $s = $duration - $i * 60; $end = sprintf('%s %02d:%02d:%02d', $date, $h, $i, $s); say qq[\nInput: \$timestamp = $start, \$duration = $test->[1]]; say qq[Output: $end]; } sub date10_add { # (date10a, days) -- returns date10 which is days after date10a my (@t); if ($_[0] =~ m|^(....)-(..)-(..)|) { @t = localtime(timelocal(0, 0, 12, $3, $2 - 1, $1 - 1900) + $_[1] * 86400); return sprintf('%04d-%02d-%02d', $t[5] + 1900, $t[4] + 1, $t[3]); } else { return 0; } }
Input: $timestamp = 2022-08-17 15:00:00, $duration = 93 Output: 2022-09-01 09:00:00 Input: $timestamp = 2022-08-17 15:00:00, $duration = 2349 Output: 2023-08-17 15:00:00 Input: $timestamp = 2023-01-02 09:00:00, $duration = 999999 Output: 2448-11-24 09:00:00 Input: $timestamp = 2022-08-22 18:00:00, $duration = 0 Output: 2022-08-23 09:00:00 Input: $timestamp = 2022-08-17 12:00:00, $duration = 1.56 Output: 2022-08-17 13:33:36
Any content of this website which has been created by Peter Campbell Smith is in the public domain