There are some programs/scripts that need to be run at specific times in a timezone different from the system timezone.
A la crontab in Perl, but one that honors a timezone and DST rules in a region different from that in which the system is configured.
Here is the use case : I will create an excel sheet with the time in PT in column B and the corresponding program/Perl script to run in column C.
Nothing specific about this information being in a Excel sheet - could be plain text file/"crontab" entry too.
A Perl script will read in the data from the excel sheet and run/spawn those scripts at the correct time.
The thing to keep at mind is that the Perl script should run correctly regardless of what timezone the system that it is running on is.
Regardless of whether the script is running on a Box in NY or IL or CA, it should spawn the scripts at the time mentioned in the file entries as per the Pacific Standard Time with DST at mind.
It is very important, as I said before, of it being aware, "automagically" ( without me doing any explicit programmming ) of the latest DST rules for the PT region.
What would you suggest?
Maybe I can visit some website that shows current time in that region and scan the time value from it, and run the scripts when it's the correct time?
Any such Perl screen scraper friendly site?
Or maybe I can use some smart Perl module, like Schedule::Cron
For the record, a large number of good suggestions came by at http://www.perlmonks.org/index.pl?node_id=772934, however, they, in typical at/cron fashion, work as per the system configured timezone.
In general, if you care about timezones, represent times internally in some universal format and convert times for display purposes only.
Applying this to your problem, write a crontab whose times are expressed in GMT. On each worker machine, convert to local time and install the crontab.
Front matter:
#! /usr/bin/perl
use warnings;
use strict;
use feature qw/ switch /;
use Time::Local qw/ timegm /;
For the conversions this program supports, use today's date and substitute the time from the current cronjob. Return the adjusted hour and day-of-week offset:
sub gmtoday {
my($gmmin,$gmhr,$gmmday,$gmmon,$gmwday) = #_;
my #gmtime = gmtime $^T;
my(undef,undef,$hour,$mday,$mon,$year,$wday) = #gmtime;
my #args = (
0, # sec
$gmmin eq "*" ? "0" : $gmmin,
$gmhr,
$mday,
$mon,
$year,
);
my($lhour,$lwday) = (localtime timegm #args)[2,6];
($lhour, $lwday - $wday);
}
Take the five-field time specification from the current cronjob and convert it from GMT to local time. Note that a fully general implementation would support 32 (i.e., 2 ** 5) cases.
sub localcron {
my($gmmin,$gmhr,$gmmday,$gmmon,$gmwday) = #_;
given ("$gmmin,$gmhr,$gmmday,$gmmon,$gmwday") {
# trivial case: no adjustment necessary
when (/^\d+,\*,\*,\*,\*$/) {
return ($gmmin,$gmhr,$gmmday,$gmmon,$gmwday);
}
# hour and maybe minute
when (/^(\d+|\*),\d+,\*,\*,\*$/) {
my($lhour) = gmtoday #_;
return ($gmmin,$lhour,$gmmday,$gmmon,$gmwday);
}
# day of week, hour, and maybe minute
when (/^(\d+|\*),\d+,\*,\*,\d+$/) {
my($lhour,$wdoff) = gmtoday #_;
return ($gmmin,$lhour,$gmmday,$gmmon,$gmwday+$wdoff);
}
default {
warn "$0: unhandled case: $gmmin $gmhr $gmmday $gmmon $gmwday";
return;
}
}
}
Finally, the main loop reads each line from the input and generates the appropriate output. Note that we do not destroy unhandled times: they instead appear in the output as comments.
while (<>) {
if (/^\s*(?:#.*)?$/) {
print;
next;
}
chomp;
my #gmcron = split " ", $_, 6;
my $cmd = pop #gmcron;
my #localcron = localcron #gmcron;
if (#localcron) {
print join(" " => #localcron), "\t", $cmd, "\n"
}
else {
print "# ", $_, "\n";
}
}
For this sorta-crontab
33 * * * * minute only
0 0 * * * minute and hour
0 10 * * 1 minute, hour, and wday (same day)
0 2 * * 1 minute, hour, and wday (cross day)
the output is the following when run in the US Central timezone:
33 * * * * minute only
0 18 * * * minute and hour
0 4 * * 1 minute, hour, and wday (same day)
0 20 * * 0 minute, hour, and wday (cross day)
In the schedule, store the number of seconds from the epoch when each run should occur rather than a date/time string.
Expanding a little:
#!/usr/bin/perl
use strict; use warnings;
use DateTime;
my $dt = DateTime->new(
year => 2010,
month => 3,
day => 14,
hour => 2,
minute => 0,
second => 0,
time_zone => 'America/Chicago',
);
print $dt->epoch, "\n";
gives me
Invalid local time for date in time zone: America/Chicago
because 2:00 am on March 14, 2010 is when the switch occurs. On the other hand, using hour => 3, I get: 1268553600. Now, in New York, I use:
C:\Temp> perl -e "print scalar localtime 1268553600"
Sun Mar 14 04:00:00 2010
So, the solution seems to be to avoid scheduling these events during non-existent times in your local time zone. This does not require elaborate logic: Just wrap the DateTime constructor call in an eval and deal with the exceptional time.
While I certainly think that there are likely "cleaner" solutions, would the following work?
set the cron to run the scripts several hours ahead of the possible range of times you actually want the script to run
handle the timezone detection in the script and have it sleep for the appropriate amount of time
Again, I know this is kinda kludgey but I thought I would put it out there.
Use the DateTime module to calculate times.
So if your setup says to run a script at 2:30 am every day, you will need logic to:
Try to create a DateTime object for 2:30am in timezone America\Los_Angeles.
If no object add 5 minutes to the time and try again. Give up after 2 hours offset.
Once you have a DateTime object, you can do comparisons with DateTime->now or extract an epoch time from your object and compare that with the results of time.
Note that I chose 2:30 am, since that time won't exist at least 1 day a year. That's why you need to have a loop that adds an offset.
Related
My script calculates the difference in days between two dates. However, all the time I encounter errors. The solution must work for all OS. It is advisable to do it in UNIX epoch time, but if it is impossible then there may be another solution.
I tried:
Time::ParseDate - does not work on MS Windows
Time::Local - does not work on dates from the 31st of the month
Sample code:
#!/usr/bin/perl -w
use strict;
use Time::Local;
use POSIX;
sub toepoch {
my #a = split /[- :]/, $_[0];
$a[0] =~ s/^.{2}//;
if (! defined $a[5]) {
$a[5] = 00
}
my $b = timelocal($a[5], $a[4], $a[3], $a[2], $a[1], $a[0]);
return $b;
}
my $days = sprintf("%d",(&toepoch('2018-03-31 11:00') - &toepoch('2018-04-02 11:00') / 86400));
print $days;
Output: Day '31' out of range 1..30 at epoch.pl line 12.
What module should I check in next? I remind you that the solution must work on UNIX and MS Windows systems.
From the documentation for Time::Local:
It is worth drawing particular attention to the expected ranges for
the values provided. The value for the day of the month is the actual
day (ie 1..31), while the month is the number of months since January
(0..11). This is consistent with the values returned from
"localtime()" and "gmtime()".
So by supplying timelocal the array (0, 00, 11, 31, 03, 18) you're trying to use day 31 of month 4, which doesn't work since April only ever has 30 days. If only the error message included the month it's assuming!
When doing the conversion, you need to mind to keep month values within 0..11 and adjust the year accordingly.
(Alternately you can use timelocal_nocheck() to be allowed to input month -1 and have the function do the conversion to the previous year. Although if you did use that function, you'd have had a bug that was a lot harder to track down, since it would have automatically converted 31st of April to 1st of May and you'd have no idea why your time difference is only 1 day.)
Secondly, you have a misplaced parenthesis on the calculation line, so you divide only the latter time by 86400.
My edited code:
use strict;
use warnings;
use Time::Local;
use POSIX;
sub toepoch {
my #a = split /[- :]/, $_[0];
$a[0] =~ s/^.{2}//;
if (! defined $a[5]) {
$a[5] = 00
}
--$a[1];
if ($a[1] < 0) {
--$a[0];
$a[1] += 12;
}
my $b = timelocal($a[5], $a[4], $a[3], $a[2], $a[1], $a[0]);
return $b;
}
my $days = sprintf("%d",(&toepoch('2018-03-31 11:00') - &toepoch('2018-04-02 11:00')) / 86400);
print $days;
Output:
-2
EDIT:
I assume you know what you're doing when using format %d for the value - it truncates the value down to the next whole number, meaning if you had dates
2018-03-31 11:00
2018-04-02 10:59
that is, just 1 minute short of 2 days, your program would report the time difference as "-1".
To round to nearest whole number, use the format %.0f instead.
#!/usr/bin/perl
sub parkingcharge {
sub exittime
{
($sec, $min, $hour) = localtime();
print "exit time:$hour:$min:$sec\n";
}
my $exit = exittime();
my $entry = "9:10:8";
print "\nvehicle entry time is :$entry\n";
print "\nvehicle exit time is :$exit\n";
my $parkingCharge = ($entry - $exit);
print "\ntotal parking charge is : $parkingCharge\n";
}
parkingcharge();
The output appears like this
exit time:5:46:57
vehicle entry time is :9:10:8
vehicle exit time is :1
total parking charge is : 8
I want to find the parking charge in a Perl vehicle management program. The rate is $2 per hour, so I want to find the difference between entry time and exit time in hours and multiply by 2. The code I have written produces the wrong result.
How to take the difference between times in hours?
You can use Time::Piece, which is included with Perl. It gives you a convenient way to parse dates into Time::Piece objects, which are essentially epoch timestamps with syntactic sugar. The nice thing about them is that you can use them in math and you'll get seconds.
Because you only have times, we need to have the same date for both the entry and the exit time. One way to do that would be to check today's date and use it in both variables. But it's easier to just leave it out. Time::Piece will assume it's 1970-01-01, which is fine, because we don't care. It's only important that both timestamps have the same date as long as you cannot park over night.
use strict;
use warnings;
use Time::Piece;
my $entry = Time::Piece->strptime( '9:10:8', '%H:%M:%S' );
We use the strptime method to parse the entry time. The second argument is a pattern of placeholders. %H is hours in 24 hour notation, %M is minutes and %S is seconds. This also works without the leading zeroes.
We now have an entry date of 1970-01-01 09:10:08, or Thu Jan 1 09:10:08 1970 if you just print $entry.
Next we need to get the exit time.
my ( $sec, $min, $hour ) = localtime;
my $exit = Time::Piece->strptime( "$hour:$min:$sec", '%H:%M:%S' );
Because just using localtime in scalar context would give us today's date, we have to do an extra step. Your code already got the seconds, minutes and hours of this moment. We just use that as a string in the right format and feed it into strptime the same way we did for $entry. Now we have the exit timestamp, which is Thu Jan 1 14:46:56 1970 while I write this.
Getting the duration is a simple matter of subtraction. Converting it to hours is just a division by 60 for minutes and by 60 for hours.
my $duration = $exit - $entry;
my $duration_in_hours = $duration / 60 / 60;
The $duration_in_hours is 5.61333333333333 for me right now. If you want people to pay for every started hour, you'd have to round up.
my $fee_started_hours = int( $duration_in_hours + 1 ) * $hourly_fee;
I prefer to only pay for full hours of parking, so I'd like rounding down more.
my $fee_full_hours = int( $duration_in_hours ) * $hourly_fee;
I need to write a perl script that executes a command at a specified time.
use net::ssh::expect to login to a router
read the time from the router's clock ("show clock" command displays the time.)
At the 17:30:00 execute a command.
I tried writing script for it but it doesn't work. Any suggestions please ?
use strict;
use warnings;
use autodie;
use feature qw/say/;
use Net::SSH::Expect;
my $Time;
my $ssh = Net::SSH::Expect->new(
host => "ip",
password => 'pwd',
user => 'user name',
raw_pty => 1,
);
my $login_output = $ssh->login();
while(1) {
$Time = localtime();
if( $Time == 17:30:00 ) {
my $cmd = $ssh->exec("cmd");
print($cmd);
} else {
print" Failed to execute the cmd \n";
}
}
Several things here:
First, use Time::Piece. It's now included in Perl.
use Time::Piece;
for (;;) { # I prefer using "for" for infinite loops
my $time = localtime; # localtime creates a Time::Piece object
# I could also simply look at $time
if ( $time->hms eq "17:30:00" ) {
my $cmd $ssh->exec("cmd");
print "$cmd\n";
}
else {
print "Didn't execute command\n";
}
}
Second, you shouldn't use a loop like this because you're going to be tying up a process just looping over and over again. You can try sleeping until the correct time:
use strict;
use warnings;
use feature qw(say);
use Time::Piece;
my $time_zone = "-0500"; # Or whatever your offset from GMT
my $current_time = local time;
my $run_time = Time::Piece(
$current_time->mdy . " 17:30:00 $time_zone", # Time you want to run including M/D/Y
"%m-%d-%Y %H:%M:%S %z"); # Format of timestamp
sleep $run_time - $current_time;
$ssh->("cmd");
...
What I did here was calculate the difference between the time you want to run your command and the time you want to execute the command. Only issue if I run this script after 5:30pm local time. In that case, I may have to check for the next day.
Or, even better, if you're on Unix, look up the crontab and use that. The crontab will allow you to specify exactly when a particular command should be executed, and you don't have to worry about calculating it in your program. Simply create an entry in the crontab table:
30 17 * * * my_script.pl
The 30 and 17 say you want to run your script everyday at 5:30pm. The other asterisks are for day of the month, the month, and the day of the week. For example, you only want to run your program on weekdays:
30 17 * * 1-5 my_script.pl # Sunday is 0, Mon is 1...
Windows has a similar method called the Schedule Control Panel where you can setup jobs that run at particular times. You might have to use perl my_scipt.pl, so Windows knows to use the Perl interpreter for executing your program.
I highly recommend using the crontab route. It's efficient, guaranteed to work, allows you to concentrate on your program an not finagling when to execute your program. Plus, it's flexible, everyone knows about it, and no one will kill your task while it sits there and waits for 5:30pm.
localtime converts a Unix timestamp (seconds since epoch, which is about 1.4 billion now) to a list of values. The time function conveniently provides that timestamp. From perldoc -f localtime:
Converts a time as returned by the time function to a 9-element
list with the time analyzed for the local time zone. Typically
used as follows:
# 0 1 2 3 4 5 6 7 8
($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) =
localtime(time);
For your time comparison you could do:
$Time = join ':', (localtime(time))[2, 1, 0];
if ($Time eq '17:30:00') {
...
}
Since Perl allows the postcircumfix [...] operator to index into lists just like it does with arrays, we can use it to remove the slice of the (localtime(time)) list that contains hours, minutes, and seconds, join them with colons, and assign the resulting string to $Time.
Note that because $Time now holds a string, you should compare it to '17:30:00' rather than the bareword 17:30:00, which isn't a valid numeric form and should result in a compilation error. And since we're comparing strings instead of numbers, we use the eq operator. == forces numeric context on its operands, and since 17:30:00 isn't a valid number, Perl will treat it as 0 and warn you with
Argument "foo" isn't numeric in numeric eq (==) at ....
I have this Perl script where I need to monitor the execution time of DBI calls.
In Europe (France), I have no problem: 2 seconds execution time is reported 2 seconds.
This same script running on a computer in Singapore reports 30 minutes and 2 seconds.
Why ?
use strict;
use Time::Format qw(%time);
use Time::HiRes qw(gettimeofday);
my $time_start = gettimeofday();
sleep 2; # some action goes here
my $stat_perf = gettimeofday() - $time_start;
print STDOUT $time{'mm:ss.mmm', $stat_perf} . " \n";
The output in France is
00:02.000
The same script running in Singapore yields:
30:02.001
Why ?
According to this documentation, the gettimeofday function returns seconds or microseconds since the unix epoch, which is 1/1/1970 UTC. Because it is in UTC, it is not affected by time zones at all.
Also, in your original code you are just using gettimeofday, which is going to be returning timestamps from now, not from 1970. But in your suggested answer, for some reason, you have hard-set the timestamp, which won't help you do much.
Yes, there is history to just about every time zone, including Singapore. You can see it in the TZDB here. But you are incorrect about it being +8:30 at the epoch. It was actually +7:30. You can verify also on this site. But it doesn't matter anyway because like I said, gettimeofday works strictly in UTC.
I think the problem is in how you are interpreting the results. You have as your last line:
print STDOUT $time{'mm:ss.mmm', $stat_perf} . " \n";
But $stat_perf is the elapsed duration of time, not a value that you can treat as a timestamp. You probably shouldn't be passing it to $time, since that will use the local time zone and be expecting a full timestamp.
Also, you may want to use tv_interval, as shown in the examples.
Update
I searched through the CPAN archives and I'm sure somewhere there is a module for formatting an elapsed duration of time, but I can't seem to find it. Anyway, it's not too hard to write this on your own. Here, this should work:
my $min = $stat_perf / 60;
my $sec = ($stat_perf * 1000 % 60000) / 1000;
my $elapsed = sprintf("%02u:%06.3f", $min, $sec);
print STDOUT $elapsed . "\n";
The anser is ...
Singapore is now 08h00 offset from UTC. In 1970, it was offset by 08h30. Asking for the conversion of a few seconds into a string will get us to 1970, not today's date, and timezone.
By requesting
print STDOUT $time{'mm:ss.mmm', 2} . " \n";
the system adjusts to 1970 (epoch) timezone offset.
In order to get a correct result in Singapore, we must shift to after 1982, when Singapore made its last timezone change.
print STDOUT $time{'mm:ss.mmm', 2 + 1356994800} . " \n";
as
UNIX_TIMESTAMP('2013-01-01 00:00:00') = 1356994800
We are only concerned by the time of day portion of the date, so this does it.
Check with
zdump -v Asia/Singapore
This is the trick.
Here is a script that emulates $time{} in converting a real number into a string representing the mm:ss sexagesimal conversion of its integer part, concatenated with the decimal remainder formatted as microseconds.
As this is going to be part of a library, there are protections set to avoid invoking it with bad arguments.
I hope I didn't miss something.
use strict;
use Time::Format qw(%time);
# ----------------------------------------------------------
# A substitute to $time{} as we have issues with TZ offsets at epoch days in some part of the World
# A real to sexagesimal converter
# Format will be set to match $time{'mm:ss.mmm', $stat_perf};
sub microTime {
return '' unless (my ($intertime) = #_);
return '' unless (ref ($intertime) eq '');
return '' unless (sprintf("%s", $intertime) =~ m/^(?:[\d]+)(?:\.(?:[\d]+))?$/);
my $intNum = int($intertime);
"a" =~ /a/; # Resets regex buffers
sprintf ("%.03f", $intertime - $intNum) =~ m,\.([\d]+),;
my $intDec = $1; # It's always defined
my $intUnder = $intNum % 3600;
my $intMin = int($intUnder / 60);
my $intSec = $intUnder % 60;
return sprintf ("%02d:%02d.%03d", $intMin, $intSec, $intDec);
}
my $stat_perf;
$stat_perf = 345.987;
$stat_perf = 345;
$stat_perf = 3945.987;
$stat_perf = 0;
$stat_perf = 3945.918733;
print STDOUT sprintf (" >> %s\n", µTime ($stat_perf));
print STDOUT sprintf (" == %s\n", $time{'mm:ss.mmm', $stat_perf});
I must be doing something wrong, but I can't figure it out. When I give seconds as an argument to localtime(), I get seconds and 16 hours back.
my $startTime = time;
(process)
my $endTime = time;
my $diffTime = ( $endTime - $startTime );
($sec,$min,$hour) = localtime( $diffTime );
print STDERR "diffTime = $diffTime\n";
print STDERR "hour = $hour\n";
print STDERR "min= $min\n";
print STDERR "sec = $sec\n";
print( sprintf( "Elapsed time : %02d:%02d:%02d\n", $hour, $min, $sec ) );
...always prints:
diffTime = 4
hour = 16
min= 0
sec = 4
Elapsed time : 16:00:04
OKAY. Figured out how to add comments - NoScript settings were too tight.
Thanks...
I can't seem to add comments to this thread, so I'll just thank everyone here.
NOT using gmtime was the problem. It may not be the most efficient solution, but it works for what I need, which is just a simple bit of info for the user to evaluate how long he/she might wait for the routine to complete and make decisions about how large an input dataset he/she is comfortable with.
You're mixing durations and timestamps, that's asking for trouble...
You might get away with using gmtime, like Mansoor suggests, but still, you're using an apple as an orange.
The following works, and uses the core Time::Piece and Time::Seconds modules. For $diffTime->pretty, you need a newer version (1.20) of Time::Piece than is bundled with current versions of Perl, though.
use strict;
use warnings;
use 5.010;
use Time::Piece 1.20;
my $startTime = localtime();
# (process)
my $endTime = localtime();
my $diffTime = ( $endTime - $startTime );
say STDERR "diffTime = ", $diffTime;
say STDERR "hours = ", $diffTime->hours; # not the same thing as above
say STDERR "minutes = ", $diffTime->minutes; # not the same thing as above
say STDERR "seconds = ", $diffTime->seconds; # not the same thing as above
say "Elapsed time: ", $diffTime->pretty;
This might print something like:
150
0.0416666666666667
2.5
150
2 minutes, 30 seconds
$difftime is the difference between two "epoch times" (seconds since midnight GMT 1/1/1970), but that doesn't make it an epoch time. So it's not really a suitable argument for localtime(). You're better off computing the h/m/s breakdown yourself:
($sec,$min,$hour) = ($difftime % 60, int($difftime/60) % 60, int($difftime/3600));
You want to use the gmtime function instead of the localtime function.
Since the epoch date is Jan. 1, 1970 00:00:00 GMT/UTC, calling localtime(0) will give you the epoch date in your time zone.
EDIT: Thanks for the follow-ups. As has been mentioned, this only works if you're measuring an interval that is less than 24 hours. Both the localtime and gmtime functions actually return the values of a date: seconds, minutes, hours, day of the month, month, year, day of the year, and whether the time falls into a daylight savings period. Of course, everything beyond seconds, minutes, and hours don't make sense outside of the context of a date.
This is the same problem as saying that since ℉ → ℃ requires a conversion of C = (F - 32) * 5/9, that if it got warmer by 5 ℉ that that must be the same as getting warmer by −15 ℃, since that’s the number you get when you just plug 5 ℉ into the obvious-but-wrong formula.
See why that makes no sense? Getting warmer by 5 ℉ is not at all the same as getting colder by 15 ℃!
You’ve made much the same error with your times. When you subtract quantities each measuring seconds elapsed since the epoch, you do not get a result that also measures seconds elapsed since the epoch!!
You can only call localtime or gmtime on epoch seconds, meaning on seconds elapsed since the epoch, not on seconds in general. You have to do your own dividing otherwise, or use a module, to find out how many larger units there are in the result of that subtraction.