Beefy Boxes and Bandwidth Generously Provided by pair Networks
Perl Monk, Perl Meditation
 
PerlMonks  

efficient determination of in/out of hours including Bank Holidays

by Random_Walk (Parson)
on Oct 17, 2013 at 10:10 UTC ( #1058615=CUFP: print w/ replies, xml ) Need Help??

Hello dear monks, fair and foul

I have a monitoring alert gateway. It must invoke different behaviours in working hours and out of hours. Sometimes it may be asked to forward 1000/sec. Rather than run a test for in/out of hours on each event, I decided to have a flag for ooh, and a record of the epoch time when the next change will happen. This way, most of the time I just check the flag and its validity. Only a couple of times a day, do I need to work out if its in hours, weekend, bank holiday, etc.

This script builds a closure which knows the bank hols and working hours. When it is invoked with an epoch time, it will return a flag for out of hours, and the epoch time until which this is valid.

There is a minor inefficiency when Monday is a holiday. At the end of Friday it will return Mondays start time as the limit of validity for the OOH flag. This is not a big problem, as when it is called then to find the next validity point, it will see it is in a bank holiday and act accordingly

All suggestions/comments/improvements/missed corner cases welcome

Update

soonix++ for spotting a problem. There were timezone issues with the bank holiday code (see following thread). This version should have fixed them, the original version is left below. I have also made working days a parameter, I am aware we don't all work mon..fri

Update 2

I have also added correction for when the valid until time falls across a daylight savings time boundary.

#!/usr/bin/perl use strict; use warnings; use POSIX qw(mktime); use Data::Dumper; use 5.014; sub ooh { my $start = shift; my $end = shift; my %wkwk = map {$_ => 1} @_; my @start = reverse split /:/, $start; # deliciously my @end = reverse split /:/, $end; # naughty my $i; my %mns=map{$_=>$i++}qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct No +v Dec); # Bank holidays # We assume they all begin at 00:00:00 and encompas the entire day # POSIX::mktime(15, 28, 06, 27, 02, 206);' my @bh; my $today = join ":", reverse +(localtime time)[3 .. 5]; while (<DATA>) { # read bank holiday file next unless /\d\d?\s+\w\w\w\s+\d{4}/; chomp; my ($day, $month, $year) = split; my $bh_start = join ":", $year-1900, $mns{$month}, $day; next if $bh_start lt $today; # history push @bh, $bh_start; } @bh = sort @bh; return sub { my $epoch = shift; my ($s, $m, $h, $date, $mnth, $yr, $day) = +(localtime $epoch) +[0 .. 6]; # Check if we are out of hours, and when next flip is due my ($valid, $ooh); if ("$yr:$mnth:$date" eq $bh[0]) { # hooray! bank holiday shift @bh; # rip page from calendar $ooh = 1; print "Bank Holiday "; } if (not $ooh and $wkwk{$day}) { # not a bank hol, is a working + day my $time = sprintf "%02d:%02d:%02d", $h,$m,$s; if ($time lt $start) { # not out of bed yet print "Early doors "; $valid = POSIX::mktime(@start, $date, $mnth, $yr); $ooh = 1; } elsif ($time lt $end) { # came in, dreaming of home print "Working hours "; $valid = POSIX::mktime(@end, $date, $mnth, $yr); $ooh = 0; } else { print "G'night "; } } else { print "Weekend " unless $ooh; } unless ($valid) { # we did not establish our validity limit ye +t # we are at end of day, weekend or hols. Find next working + day my $add = 1; ++$add until $wkwk{($day + $add)%7}; print "next working day is " .($day + $add)%7 . " "; $valid = POSIX::mktime(@start, $date, $mnth, $yr); $valid += $add * (24*60*60); # Daylight savings adjustment my $dst = $start[2] - +(localtime $valid)[2]; $valid += $dst * 60 * 60; $ooh = 1 } return $ooh, $valid; } } # get an ooh tester, work week mon..fri my $ooh_check = ooh('08:15:00', '17:45:00', 1..5); # and run some tests for (sort (time, 1382310123, 1359806999, 1360306452, 1381941000, 1360016452, 1382823512)) { my ($ooh, $valid) = $ooh_check->($_); say join " - ", scalar localtime $_, $ooh, scalar localtime $valid +; } __DATA__ 02 Feb 2013 18 Oct 2013 21 Oct 2013 23 Oct 2013

This is the original, broken version

use strict; use warnings; use POSIX qw(strftime mktime); use Data::Dumper; sub ooh { my $start = shift; my $end = shift; my %wkwk = map {$_ => 1} (1..5); # Mon .. Fri my $i; my %mns = map{$_=>$i++}qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct +Nov Dec); # Bank holidays # We assume they all begin at 00:00:00 and encompas the entire day # POSIX::mktime(15, 28, 06, 27, 02, 206);' my @bh; while (<DATA>) { # read bank holiday file next unless /\d\d?\s+\w\w\w\s+\d{4}/; chomp; my ($day, $month, $year) = split; my $mnth = $mns{$month}; my $bh_start = POSIX::mktime(00, 00, 00, $day, $mnth, $year - +1900); next if $bh_start < time; # history push @bh, $bh_start; } @bh = sort @bh; return sub { my $epoch = shift; my ($s, $m, $h, $date, $mnth, $yr, $day) = +(localtime $epoch) +[0 .. 6]; print $/; print scalar localtime $epoch; # Check if we are out of hours, and when next flip is due my $valid; my $ooh; if ($epoch > $bh[0]) { # its a new bank holiday shift @bh; # rip this page from the calendar $ooh = 1; print " Bank Holiday"; } if (not $ooh and $wkwk{$day}) { # not a bank hol, is a working + day my $time = sprintf "%02d:%02d:%02d", $h,$m,$s; print " given: $time "; if ($time lt $start) { # not out of bed yet print "early doors "; ($h, $m, $s) = split /:/, $start; $valid = POSIX::mktime($s, $m, $h, $date, $mnth, $yr); return 1, $valid; } elsif ($time lt $end) { # came in, dreaming of home print "within working hours "; ($h, $m, $s) = split /:/, $end; $valid = POSIX::mktime($s, $m, $h, $date, $mnth, $yr); return 0, $valid; } else { print "g'night"; } } else { print " weekend" unless $ooh; } # find next working day my $add = 1; ++$add until $wkwk{($day + $add)%7}; print " Next working day is " .($day + $add)%7 . " "; # end of day, weekend or hols ($h, $m, $s) = split /:/, $start; $valid = POSIX::mktime($s, $m, $h, $date, $mnth, $yr); $valid+= $add * (24*60*60); return 1, $valid; } } # get an ooh tester my $ooh_check = ooh('08:15:00', '17:45:00'); # and run some tests print join " -> ", 'OOH', $ooh_check->(1382310123); print join " -> ", 'OOH', $ooh_check->(1359806999); print join " -> ", 'OOH', $ooh_check->(1360306452); print join " -> ", 'OOH', $ooh_check->(1381941000); print join " -> ", 'OOH', $ooh_check->(1360016452); print join " -> ", 'OOH', $ooh_check->(time); __DATA__ 01 Jan 2012 02 Feb 2013 03 Feb 2014 04 Aug 2013 21 Oct 2013

Cheers,
R.

Pereant, qui ante nos nostra dixerunt!

Comment on efficient determination of in/out of hours including Bank Holidays
Select or Download Code
Re: efficient determination of in/out of hours
by soonix (Curate) on Oct 17, 2013 at 13:52 UTC
    next if $bh_start < time; # history
    If the program ist started on a holiday (even 1 second past midnight), the routine will think it is a workday... I wanted to propose
    ... use integer; my $today = time / 86400 * 86400; # chop hours, minutes, seconds while (<DATA>) { # read bank holiday file ... next if $bh_start < $today; # history ... } ...
    but this doesn't work correctly, either, because time is in seconds since some 1970-01-01 UTC...

      Nice catch, thanks. Perhaps something like this will fix it

      my $now = time; my $today = $now - ($now % 86400); while (<DATA>) { # read bank holiday file ... next if $bh_start < $today; # history ... } ...

      I gotta run home now, will test it really works later

      Update

      Sadly it does not work. It looks lie POSIX mktime is based on GMT, localtime, of course is not. I added the above change, plus a bit of printing and a bank holiday for today (localtime now: 2116, GMT 2016) running the code it does skip todays bank hol (17th Oct 2013)

      start of day (epoch based):Thu Oct 17 01:00:00 2013 Bank Hols Mon Oct 21 00:00:00 2013 Mon Feb 3 00:00:00 2014

      Is there a better way to get timezone offset than to mktime for epoch + 1 day and comapare to localtime for the same? Can I trust the environment, or is there a magic Perl var?

      Cheers,
      R.

      Pereant, qui ante nos nostra dixerunt!
        That's mathematically the same. (If 86400 were a power of 2, the most efficient way 'd be >> and <<.) However time and mktime refer to different timezones. Unless you happen to be in Iceland, there are some hours when this works wrong...
        Update: I posted this while you updated your post :-)
        I'm not at my computer, but perhaps something like mktime(gmtime) for determining today's midnight could work...

Log In?
Username:
Password:

What's my password?
Create A New User
Node Status?
node history
Node Type: CUFP [id://1058615]
Approved by moritz
Front-paged by Corion
help
Chatterbox?
and the web crawler heard nothing...

How do I use this? | Other CB clients
Other Users?
Others contemplating the Monastery: (5)
As of 2014-09-19 03:53 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    How do you remember the number of days in each month?











    Results (129 votes), past polls