Imaginaries and the daily grind

Weekly challenge 178 — 15 August 2022

Week 178 - 15 Aug 2022

Task 2

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:

- divide
`$duration`

by 45 (working hours per week) to get a number of full weeks - divide the remainder by 9 to get the number of extra days (9 hrs per day)
- and the remainder is the time period after 9am on the date that results.

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

The content of this website which has been created by

Peter Campbell Smith is hereby placed in the public domain

Peter Campbell Smith is hereby placed in the public domain