Easy Date and Time stuff in perl

(March 17 2006)

perl has a couple of simplistic date and time operators built in, but they're not particularly easy to use …

Dave Rolsky's article on perl.com presents a nice overview of the options, which I'm reducing to just perl's internal functions (including the Time::Local module which is pretty much always installed)

Here's a quick run through of what you can do with basic perl :-

  # Show the three basic functions ...
  #
  # time() returns a number of seconds, so we can do basic maths on it.
  my $rightnow = time();
  my $twolater = $rightnow + 2;
  print "now: $rightnow, it will be $twolater soon\n";
  sleep 2;
  print "2 seconds later it is ". time() ."\n";

The above block of code starts off by grabbing the output from time(), which is “the number of non-leap seconds since January 1, 1970, UTC” – otherwise known as epoch seconds. It's a simple number, so we can do ordinary operations on it – like predict what the time will be in two seconds. We test that by using the sleep function to wait for 2 seconds, then call time() again to make sure.

Of course, that doesn't take into account how long the various operations in your program take to execute, so sometimes you'll come out of sleep later than you thought, and see a three second difference – or worse if your machine's performance is really hammered. Don't worry about it.

Now we can look at the localtime() function (and its friend, gmtime).

  # localtime and gmtime return a human-readable string if used as a scalar,
  # and they default to "time()" if you don't provide a value
  print "localtime: ". localtime() ."\n";
  print "two seconds ago it was ". localtime($rightnow) ."\n";
  print "gmtime: ". gmtime() ."\n";

If you just call localtime, it'll default to “now”. Well, that's the same as localtime(time()) actually! If we provide a number of epoch seconds, it'll translate that for you – luckily we'd saved the $rightnow value from the beginning of the program, before the sleep.

  # localtime will break out each element separately if you ask it ...
  # Note that year is a Y2K nightmare! And most things count from zero.
  my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime();
  print "hour $hour, year $year\n";

  # If you only wanted one of the output vars from localtime(), you can
  # address the array results directly ...
  print "month ". ((localtime)[4] +1) ."\n";

localtime() will return an array if you ask it nicely (i.e. don't evaluate it in a scalar context), so you can break out some or all of the individual date fields, for later work. Note that months and wdays start from zero, not one … (which makes it easier to look up directly into an array of names) … and the year is counted from 1900,

The reverse of localtime is timelocal, which takes an array of values and returns a number of seconds. However, this isn't available by default within perl – but it is usually installed by default on your OS. Try the command “=perldoc Tile::Local=” to get more info.

  use Time::Local;
  # This provides opposites to localtime and gmtime, called
  # timelocal and timegm :-)

  # Let's take the results we had earlier ...
  my $back1year=$year-1;
  my $lastyear = timelocal($sec,$min,$hour,$mday,$mon,$back1year,$wday,$yday,$isdst);
  print "lastyear: $lastyear\n";
  print "nicely .. ". localtime($lastyear) ."\n";

This example loads the Time::Local module, and uses timelocal() to construct last year's date, using all the values we grabbed from localtime() in the earlier code section.

It then outputs the epoch seconds value, and passes it back into localtime in order to be rendered as a nice human-readable value.

The last example here deals with log file date parsing. Here we take a typical line from a syslog file, which is in the format "(Short Month Name) (Day of Month) (Hour:Minute:Seconds) (some interesting text)". We want to parse this, and construct an epoch second value.

The first step is breaking out each individual field from the entry, and we're using regular expressions to do that :-

  my $syslogentry = "Mar 11 03:31:02 Something happened";
  my ($s_monname, $s_day, $s_hour, $s_min, $s_sec) = \
     ($syslogentry =~ m/^(\w+)\s+(\d+)\s+(\d\d):(\d\d):(\d\d)/);

We're taking advantage of the return value from being an array, and collecting them into the s_monname, s_day, etc. variables. Constructing good regexps is a difficult art … :-)

  # We have to know how to turn month names into numbers, too ...
  my %monthnum = qw( Jan 0 Feb 1 Mar 2 Apr 3 May 4 Jun 5 Jul 6 Aug 7 Sep 8 Oct 9 Nov 10 Dec 11 );
  print "s_monname is $s_monname, number is $monthnum{$s_monname}\n";

Then we have to figure out how to change the string “Mar” into a month number (number 2 in fact – because the month value should start at 0 for January). A simple (but not very elegant) way is to look these up in a hash, which provides a mapping from words to numbers … Proper date libraries will do all this for you, and localise the strings. But this is about just what basic perl can do for you …

  my $syslogdate = timelocal($s_sec,$s_min,$s_hour,$s_day,$monthnum{$s_monname},$year);
  print "log entry line is \"$syslogentry\" -  date is $syslogdate ... ". localtime($syslogdate) ."\n";

Finally, we pump our new values back into a timelocal call to get epoch seconds, and then re-present it nicely with localtime. Job done!

For more powerful/automated work, take a look at the Date::Calc module …