The 2004 Perl Advent Calendar
[about] | [archives] | [contact] | [home]

On the 1st day of Advent my True Language brought to me..
DateTime

DateTime has over the last few years become the module for dealing with dates and times. It was designed as a common object that could be used by a wide range of modules to represent a date and time, allowing many different date manipulation libraries to work together.

DateTime has matured to have a wonderfully straight forward logical interface that allows you do manipulate the DateTime object with simple method calls rather than forcing you do complex maths. Using it is so simple that it's a real pain to go back

With this interface has come a fanatical zeal for correctness. As an example, with DateTime it's possible to easily work out the time a day before, even in the edge cases like when your day doesn't have twenty-four hours in it (as happens twice a year during daylight saving changes) without having to worry about these little details. All this is handled pretty much automatically, squashing numerous bugs that might otherwise sat in your code for a year waiting to rear their ugly head at a non-existent 1.30am in the morning.

Easy to use, correct, and very expandable. Just what a module should be.

DateTime objects can be created in a number of simple ways. Perhaps the simplest way is to specify all the values by hand. Unspecified values (here I've not bothered to list nanoseconds) are assumed to be zero (or one, in the case of things like months that don't start at zero)

  my $happy_time = DateTime->new(
    year   => 2004,
    month  => 12,
    day    => 1,
    hour   => 8,
    minute => 50,
    second => 0,
    time_zone => "Europe/London",
  );

Alternatively, if the time period can be represented on your system as seconds from midnight on the 1st of Jan 1970 GMT, you can create a time from an epoch:

  my $dt = DateTime->from_epoch( epoch => 1101852490 );

Or simply, if we want to create a DateTime that represents the current time:

  my $dt = DateTime->now;

Many, many modules exist on the CPAN for parsing (and creating) strings in the common formats that times are recorded in that create or work from DateTime objects. For example, to parse a time-string from a HTTP header we can use the DateTime::Format::HTTP module.

  # standard HTTP format
  my $string = "Wed, 09 Feb 1994 22:23:32 GMT";
  my $dt = DateTime::Format::HTTP
               ->parse_datetime($string);

This is the true strength of DateTime; So many other modules exist on The CPAN that extend it's functionality that can be used seamlessly with it.

Sane Accessor Methods

One of the things you've got to love about DateTime is the sane accessor methods. For example, if I want to know the year a date time object is in, I call year on it and it simply returns it:

  # what year are we in?
  my $year = DateTime->now->year;

Alternatively, when we try this with epoch seconds and Perl's built in functions, our lives are made unessesarily complex:

  # get a time in epoch seconds
  my $time = time;
  # break it into years, months, days, hours, etc
  # remembering to use either 'gmtime' or 'localtime'
  # depending on what we want since epochs don't record
  # what timezone they're in
  my @values = localtime($time);
  # now get the year out of that array, looking up with
  # "perldoc -f gmtime" each time to work out what index
  # the year is at.
  my $year = $values[5]
  # now for no good reason add 1900 onto the year
  $year += 1900;

Jeepers! That's a lot of work. I'm glad we've got sane accessor methods. All of the standard accessor methods (year, day, month, hour, minute, second, nanosecond) are present and are sane (for example, months start at "1" not "0".) You can also use accessors to set new values without having to do any complex maths:

  my $dt = DateTime->now();
  $dt->set( hour => 0, minute => 0,
            second => 0, nanosecond => 0 );
  print $dt->epoch;

Getting Common Values Out

It's trivial to get ISO standard dates and times out of an object. The ymd method can give us the standard unambiguous dates format that can be string sorted correctly:

  # 4th March 1978
  my $birthday = DateTime->new(
    day   => 4,
    month => 3,
    year  => 1978,
  );
  print $birthday->ymd;

Prints out:

  1978-03-04

It's also possible to get UK or US style dates out, using whatever separator we want:

  print $birthday->dmy('/'), "\n";  # UK style dates
  print $birthday->mdy('/'), "\n";  # US style dates

Which prints what we expect:

  04/03/1978
  03/04/1978

We can get time out in the same way:

  # in this case print "00:00:00" - not very interesting
  print $birthday->hms;

And these can be combined to give us a ISO standard date and time:

  print "My birthday starts at ", $birthday->iso8601, "\n";

Which prints:

  My birthday starts at 1978-03-04T00:00:00

Since DateTime overloads stringification so that whenever you treat a DateTime object as a string it renders into a ISO datetime, that can be more simply written as:

  print "My birthday starts at $birthday\n";

TimeZones

One of the most confusing things with computers is working out the timezone changes between places.

  # create a time in UTC (GMT)
  my $dt = DateTime->now;
  print $dt->hms,"\n";
  # adjust the time zone to NYC
  $dt->set_time_zone("America/New_York");
  print $dt->hms, "\n";

This prints out what we might expect...first the time in GMT, then the time in NYC

  22:37:44
  17:37:44

There - as easy as can be. The only complication is where you create a time and don't specify the time_zone:

  my $social = DateTime->new(
    year   => 2004,
    month  => 12,
    day    => 25,
    hour   => 7,
    minute => 0,
  );

The trouble is that DateTime doesn't know what time zone to put this in. Which 7pm do I mean? The one in London? The one in New York? The one in Zanzibar? DateTime puts this in the "floating" timezone. This means the first time I call set_time_zone on it no changes to the hour or day are made - since none can be computed.

Maths

There's lots of clever maths that you can do with DateTime objects. For example, you can easily work out the same time the previous day:

  my $same_time_tomorrow = $dt->clone;
  $same_time_tomorrow->add( days => 1 );
  my $same_time_yesterday = $dt->clone;
  $same_time_yesterday->subtract( days => 1 );

Note that this doesn't mean I'm taking away twenty-four hours, it means that I'm taking away a day. For example:

  my $dt = DateTime->new(
    year      => 2004,
    month     => 10,
    day       => 31,
    hour      => 9,
    time_zone => "Europe/London",
  );
  print "$dt\n";  # prints '2004-10-31T09:00:00'
  $dt->subtract( days => 1 );
  print "$dt\n";  # prints '2004-10-30T09:00:00'
  $dt->add( hours => 24 );
  print "$dt\n";  # prints '2004-10-31T08:00:00'

Because of daylight saving, the effect of adding days and hours is different, since the 31st October this year in London was twenty five hours long (we gained an hour.) This means we can't easily switch between days and hours. For a similar reason, we can't switch between minutes and seconds, since every so often we have a leap second that makes some minutes 61 seconds long.

This makes doing mathematics between DateTime objects hard, but it's not something that we can ignore. To make our lives easier, maths operations are handily overridden. This means we can do things like this:

  my $dt = DateTime->now->add( seconds => 5);
  sleep(1) while DateTime->now < $dt;

Subtraction and addition work too:

  my $dt = DateTime->now;
  my $future = $dt->clone->add( days => 1 );
  my $duration = $future - $dt;

Duration is a DateTime::Duration object. We can scale it, add it onto times, etc:

  # make dt two days later
  $dt += 2 * $duration;

DateTime::Duration objects can be interigated to find out how long they are in any particular units of time:

  my $duration = DateTime::Duration->new( years => 1 );
  print $duration->in_units('months'), "\n";  # prints 12

However, when we do this we have to remember we can't ask it to convert units it can't compare, like days into hours (since it doesn't necessarly know how long days are):

  my $duration = DateTime::Duration->new( days => 1 );
  print $duration->in_units('hours'), "\n";   # prints 0

One thing we can do if we want to use "idealised" days of twenty-four hours and minutes of sixty seconds is to use the absolute methods when comparing datetimes:

  my $duration = $dt->subtract_datetime_absolute( $dt2 );

This gives us a DateTime::Duration where the whole duration is represented by seconds and nanoseconds, allowing us to do what manipulation on it we want. Then we can take the duration and use the the DateTime::Format::Duration module from CPAN to render it.

  my $formatter =  DateTime::Format::Duration->new(
     pattern => '%e days, %H hours, %M minutes, %S seconds'
  );
  $formatter->set_normalising(1);
  print $formatter->format_duration($duration);

This creates a string where none of the units are larger than their normal values (so if the duration contains more than fifty nine seconds then the extra time will be converted into minutes, and minutes to hours, and so on.)

DateTime::Format::Duration is also capable of doing conversion between abstract units and actual duration if we give it a base to work from. For example:

  my $formatter =  DateTime::Format::Duration->new(
     pattern => '%e days, %H hours, %M minutes, %S seconds'
  );
  $formatter->set_normalising(1);
  $formatter->set_base( DateTime->new(
    year      => 2004,
    month     => 10,
    day       => 30,
    hour      => 9,
    time_zone => "Europe/London",
  ));
  # one day
  my $duration = DateTime::Duration->new( hours => 25 );
 
  # prints "1 days, 00 hours, 00 minutes, 00 seconds"
  # due to daylight saving
  print $formatter->format_duration($duration);

  • datetime.perl.org
  • DateTime::Duration
  • DateTime::Format::Duration
  • DateTime::Format::HTTP