1: FAQ Administrivia

2: Basic DateTime usage

3: Time Zones

4: Converting To and From Other Date and Time Formats

5: Groups and ranges of dates: Durations, Sets, Spans and SpanSets

6: Sample Date Calculations

7: Miscellaneous

8: Credits

9: Copyright

1: FAQ Administrivia

1.1: Introduction

This FAQ covers the Perl DateTime modules (see http://datetime.perl.org/ for more information). The goal of this FAQ is not to cover date and time issues in general, but just the usage of the DateTime family of modules.

This FAQ is still a work in progress, so there are various items listed as TODO.

If you have questions not covered by the FAQ, please mail them to the mailing list.

1.2: Searching the DateTime archives

You can use http://www.google.com/ to search the mailing list archives (see http://datetime.perl.org/mailing_list.html). However you need to limit the search to the http://archive.develooper.com/datetime@perl.org/ site. Simply paste the URL below into your web browser and replace QUERY with your query: http://www.google.com/search?$q=site%3Aarchive.develooper.com +inurl%3Adatetime%40perl.org%3AQUERY

1.3: Why use DateTime?

The DateTime family of modules present a unified way to handle dates and times in Perl. For a good overview of the previous state of Perl's date and time modules see: http://www.perl.com/pub/a/2003/03/13/datetime.html.

The short version is that there are several different packages with overlapping functionality, but it was hard to take dates in one package and convert them to another which you would often have to do to get a function from a different package.

Advantages:

Disadvantages:

1.4: Where is calendar X?

If you have looked at http://datetime.perl.org/modules.html and nothing appears relevant try the mailing list. We are still missing some important calendars (Islamic, Hebrew and Chinese just off the top of my head). Please help by writing one (but check the mailing list first to make sure that no one else has started).

2: Basic DateTime usage

2.1: How do I use DateTime?

  # This is the bit you really want
  use DateTime;

2.2: How do I make a DateTime object?

There are a variety of ways to create a DateTime object. You can either specify each item in the date. All parameters are optional except for year. The defaults are shown for the other parameters:

  my $dt1 = 
    DateTime->new( year       => 2003,
                   month      => 1,
                   day        => 1,
                   hour       => 0,
                   minute     => 0,
                   second     => 0,
                   nanosecond => 0,
                   time_zone  => "floating",
                  );

Create a time from the Unix epoch (e.g. what perl's time() function returns):

  # Create a DateTime object for the current time
  my $dt2 = DateTime->from_epoch( epoch => time() );

  # And to get the time back as an offset from the epoch
  my $time = $dt2->epoch();

See also DateTime::Format::Epoch if you need finer control over the epoch conversion.

Since you often want to represent the current time, there is a more simple syntax:

  my $dt3 = DateTime->now();   # With date and time
  my $dt4 = DateTime->today(); # Truncated to date

All of the above take optional time_zone and locale parameters, for information about the floating time zone see What is the floating time zone?. For other more sophisticated constructors, see the DateTime documentation.

2.3: Why do I need to truncate dates?

Since DateTime objects represent exact instants in time (down to the nanosecond resolution), when comparing two dates you need to decide what units you want to use. e.g:

  my $some_date = ...;
  my $now = DateTime->now();

  # Naive comparison
  if ($some_date == $now) { # WRONG!
  }

  my $some_date2 = $some_date->clone()->truncate(to => 'days');
  my $now = DateTime->today();  # Same as now() truncated to days
  if ($some_date == $now) { # Right!
  }

If you hadn't changed both to days then they are unlikely to match. Of course if you are trying to work out if an hour long meeting is going on now then you should truncate to hours... but for that kind of thing you probably want a DateTime::Span.

2.4: When do I need to clone dates?

You need to clone a date if you plan on changing it and have copied the variable. Since the dates are hash references internally, just copying the variable holding the date doesn't actually make a new date object, so changing one will change the other.

  my $dt1 = DateTime->new( year => 2000 );

  # "copy" the date and change the "copy"
  $dt2 = $dt1;
  $dt2->set( year => 2003 );

  print $dt1->year();  # Surprise, it is 2003

The right way to do it is to clone when you make the copy:

  my $dt1 = DateTime->new( year => 2000 );

  # copy the date and change the copy
  $dt2 = $dt1->clone();
  $dt2->set( year => 2003 );

  print $dt1->year();  # Prints: 2000

2.5: How do I compare two dates?

There are a few ways to do this. You can do it explicitly:

  $dt1 = DateTime->new( year => 1999 );
  $dt2 = DateTime->new( year => 2000 );

  my $cmp = DateTime->compare($dt1, $dt2);
  # $cmp is -1, 0, or 1 if $dt1 is <, ==, or > than $dt2 respectively

Or using the normal perl syntax:

  if ($dt1 > $dt2)   { $foo = 3; }
  if ($dt1 == $dt2)  { $foo = 4; }
  if ($dt1 < $dt2)   { $foo = 5; }
  my @sorted_dates = sort ($dt2, $dt1);

There are some issues doing date comparisons when one of the objects has a floating time zone. See What is the floating time zone?

2.6: How do I convert a date from format FOO into a DateTime object (and vice versa)?

First you should check to see if there is an appropriate DateTime::Format module, these usually have both input and output filters so you can read and write dates to external sources.

If there is no appropriate module already you can use DateTime::Format::Builder to easily build a parser.

2.7: I printed a date with strftime, how do I parse it again?

Use DateTime::Format::Strptime. This module implements the POSIX strptime function that is the inverse of strftime. The difference between this module and DateTime::Format::Builder which has the capability to create a parser from strptime formats is that DateTime::Format::Strptime is oriented to parsing a string in the program a few times, whereas DateTime::Format::Builder is really for building a new DateTime::Format module and can chain together string definitions that will be tried in succession until one matches the source or all definitions have been exhausted.

  # TODO When I can get it installed  

2.8: How can I get a string representing a date?

  my $dt = DateTime->new(year => 1998, month  => 4,  day => 7,
                         hour => 13,   minute => 55);

  # Some standard ones
  my $s1 = $dt->date();     # 1998-04-07
  my $s2 = $dt->mdy('|');   # 04|07|1998
  my $s3 = $dt->datetime(); # 1998-04-07T13:55:00
  my $s4 = $dt->time();     # 13:55:00
  my $s5 = $dt->hms('x');   # 13x55x00

  # Then you can get fancy with custom strftime formats (see the
  # DateTime perldoc for the full format details

  # 1998-04-07 01:55:00 PM
  my $s6 = $dt->strftime("%F %r");

  # Tue, 07 Apr 1998 13:55:00 +0000 (RFC 2925) 
  my $s7 = $dt->strftime("%a, %d %b %Y %H:%M:%S %z"); 
  

2.9: How small an increment of time can I represent?

DateTime can represent nanoseconds. You can create obects with that resolution using the nanosecond parameter to new or set and there is a corresponding nanosecond accessor. For these you give an integer count of the nanoseconds.

A millisecond is a thousandth of a second (10^-3 or 0.001). The abbreviation is ms. A microsecond is a millionth of a second (10^-6 or 0.000001). The abbreviation is us (or more properly µs). A nanosecond is a billionth (US) of a second (10^-9 or 0.000000001). The abbreviation is ns.

  # The ns part is 0.000000230 below
  my $dt_ns = DateTime->new(year => 2003, month => 3,   day => 1,
                            hour => 6,    minute => 55, second => 23,
                            nanosecond => 230);
  print "ns: ", $dt_ns->nanosecond, "\n";  # Prints: "ns: 230\n"

  # Assuming we got milliseconds as an argument
  my $ms = 42;
  my $dt_ms = DateTime->new(year => 2003, month => 3,   day => 1,
                            hour => 6,    minute => 55, second => 23,
                            nanosecond => $ms * 1_000_000);
  print "ns: ", $dt_ms->nanosecond, "\n";  # Prints: "ns: 42000000\n"

2.10: How do I parse multiple formats at once?

Provided by Iain Truskett (spoon at cpan dot org)

If your date string could be one of several existing formats then you can use DateTime::Format::Builder to make a single parser that tries several in sequence (and you can add your own additional rules if needed).

  package DateTime::Format::Fall;
  use DateTime::Format::HTTP;
  use DateTime::Format::Mail;
  use DateTime::Format::IBeat;

  use DateTime::Format::Builder (
    parsers => { parse_datetime => [
      sub { eval { DateTime::Format::HTTP->parse_datetime( $_[1] ) } },
      sub { eval { DateTime::Format::Mail->parse_datetime( $_[1] ) } },
      sub { eval { DateTime::Format::IBeat->parse_datetime( $_[1] ) } },
    ] }
  );

Then in in another package:

  package main;
  for ( '20030719T155345', 'Sat, 19 Jul 2003 15:53:45 -0500', '@d19.07.03 @704' ) {
    print DateTime::Format::Fall->parse_datetime($_)->datetime(), "\n";
  }
  # Prints: "2003-07-19T15:53:45\n2003-07-19T15:53:45\n2003-07-19T15:53:45\n"

2.11: How do I make a DateTime from a string?

TODO Finish this section

TODO Talk about eval if needed...

3: Time Zones

3.1: What is the floating time zone?

The floating time zone is used when there is no known time zone that can be used, or when you simply do not care about time zones for what you are doing. If you compare a floating time with a time with a time zone using either the compare method or one of the overloaded comparisons (==, etc.) then the floating time is converted to the other zone for comparison:

 my $dt1 = DateTime->new(year => 2002, month  => 4,  day => 7,
                         hour => 13,   minute => 55,                         
                         time_zone => 'America/New_York');
 my $dt2 = DateTime->new(year => 2002, month  => 4,  day => 7,
                         hour => 13,   minute => 55,                         
                         time_zone => 'America/Los_Angeles');
 my $dt_float = 
           DateTime->new(year => 2002, month  => 4,  day => 7,
                         hour => 13,   minute => 55,                         
                         time_zone => 'floating');

 print "fixed date 1 == floating date\n" if $dt1 == $dt_float;
 print "fixed date 2 == floating date\n" if $dt2 == $dt_float;
 print "fixed date 1 != fixed date 2\n"  if $dt1 != $dt2;

If you want to treat the floating items as if they were in the UTC time zone (i.e. an offset of 0) then use the compare_ignore_floating class method.

However, since the result of the comparison will change if you compare fixed with dates in different time zones that will really mess up the sort() function. In this case you either need to convert every floating time zone to a fixed one, or use the compare_ignore_floating class method in a custom sort comparator to treat floating time zones as UTC.

Unless you really know what you are doing then you shouldn't mix floating time zones with fixed ones. Always convert the floating time zones to the appropriate fixed time zone (you will have to decide if local, UTC or something else is correct):

  # Convert all floating dates to the given $time_zone
  # Args:
  #  $dates is an arrayref of the source dates
  #  $time_zone is either a string or DateTime::TimeZone object
  #  $clone governs whether or not to clone the list items
  # Returns: an arrayref containing the cleaned dates (note that the
  #   source array will be changed unless $clone is true)
  sub unfloat_dates {
      my ($dates, $time_zone, $clone) = @_;
      $time_zone = "UTC" unless $time_zone;

      my @clean_dates = ();
      foreach my $d (@$dates) {
          $d = $d->clone() if $clone;
          $d->set_time_zone($time_zone)
              if $d->time_zone()->is_floating();
          push @clean_dates, $d;
      }
      
      return \@clean_dates;
  }

  my %time = (year => 2003, month  => 3,  day => 1,
              hour => 1,    minute => 32);
  my @dates = 
      (DateTime->new(%time, time_zone => "America/New_York"),
       DateTime->new(%time, time_zone => "floating"),
       DateTime->new(%time, time_zone => "UTC"),
       );
       
  if ($dates[0] == $dates[1] and $dates[2] == $dates[1]) {
      # This will be true
      print "Floating time equals the other two\n";
  }

  unfloat_dates(\@dates, "UTC", 0);

  if ($dates[0] != $dates[1] and $dates[2] == $dates[1]) {
      # This will be true
      print "Floating time is now fixed to UTC\n";
  }

For example MySQL does not store time zone information along with the dates so DateTime::Format::MySQL returns all of the dates it generates from MySQL style dates with a floating time zone. It is up to the user to know what time zone the dates are stored in. Hopefully the developers of the system using MySQL have thought about that and are writing them all in in a consistent timezone.

Also note that if an object's time zone is the floating time zone, then it ignores leap seconds when doing date math, because without knowing the time zone, it is impossible to know when to apply leap seconds.

You also need to use the floating time zone as an intermediate step if you want to convert a time from one zone to the other but want to keep the local time the same. See How do I change the time zone without changing the local time?.

3.2: If I know my local time, how do I determine the local time in another time zone?

  my $source = DateTime->new(year => 1998, month  => 4,  day => 7,
                             hour => 13,   minute => 55,
                             time_zone => 'America/New_York');
  my $result = $source->clone()
                      ->set_time_zone( 'America/Los_Angeles' );
  print $source->strftime("%F %r %Z"), " became ", 
        $result->strftime("%F %r %Z");
  # Prints: 1998-04-07 01:55:00 PM EDT became 1998-04-07 10:55:00 AM PDT

3.3: How do I change the time zone without changing the local time?

You have to first switch to the floating time zone (see What is the floating time zone?) otherwise the displayed time will be adjusted (keeping the internal time the same) rather than keeping the local clock time the same.

  my $source = DateTime->new(year => 1998, month  => 4,  day => 7,
                             hour => 13,   minute => 55,
                             time_zone => 'America/New_York');
  my $result = $source->clone()
                      ->set_time_zone( 'floating' )
                      ->set_time_zone( 'America/Los_Angeles'    );
  print $source->strftime("%F %r %Z"), " became ", 
        $result->strftime("%F %r %Z");
  # Prints: 1998-04-07 01:55:00 PM EDT became 1998-04-07 01:55:00 PM PDT

3.4: Why doesn't DateTime accept the EST time zone?

Well there are several EST timezones... one in the United States and the other in Australia. If you want to use the US one then use EST5EDT, or preferably America/New_York.

The short names for time zones are not unique, and so any attempt to determine the actual time zone from such a name involves guessing. Use the long names instead.

4: Converting To and From Other Date and Time Formats

4.1: How Do I Convert between Epoch Times and DateTime Objects?

DateTime provides a constructor from_epoch(...) that takes an epoch argument with a value giving the count since the epoch. The exact definition of the epoch varies depending on the system, but if you use time() and friends to get the value then this will behave correctly. (Note that the constructor also takes time_zone and locale parameters).

See also DateTime::Format::Epoch for more control over the exact definition of the epoch you are using. You might need to use this if you are getting epoch times from another system. e.g. on Unix the epoch is defined as Jan 1st, 1970 at 00:00:00 and the epoch times are given as a count of seconds since then (disregarding leap seconds).

To get the epoch corresponding to a given DateTime use the epoch() method. If you want a high resolution time that includes nanoseconds use hires_epoch().

When you create a time from an epoch, the time zone is automatically set to 'UTC', unless you supply a time_zone parameter to override this.

  my $time = 1057632876;
  my $dt = DateTime->from_epoch(epoch => $time);
  print $dt->datetime(), "\n";

  my $epoch = $dt->epoch();
  print $dt->epoch(), "\n";

4.2: How Do I Convert between Times from the Perl Builtin Functionality and DateTime Objects?

See How Do I Convert between Epoch Times and DateTime Objects? for the recipe. Use the time() function to get epoch times or Time::Local if you have the time pieces from gmtime or localtime.

4.3: How Do I Convert between POSIX Times and DateTime Objects?

See How Do I Convert between Epoch Times and DateTime Objects? for the recipe.

4.4: How Do I Convert between Time::HiRes and DateTime Objects?

See How Do I Convert between Epoch Times and DateTime Objects? for the recipe. Note that DateTimefrom_epoch(...) will preserve the nanoseconds when doing the conversion, but you have to use hires_epoch() to get the correct time back.

4.5: How Do I Convert between Times from the TimeDate Family and DateTime Objects?

See How Do I Convert between Epoch Times and DateTime Objects? for the recipe.

4.6: How Do I Convert between Time::Piece Times and DateTime Objects?

See How Do I Convert between Epoch Times and DateTime Objects? for the basic recipe. However you need to call Time::Piece's epoch method to get the epoch from the object. To create a Time::Piece object you will need to call gmtime or localtime (which is overriden by Time::Piece) with the value from DateTime's epoch().

  use Time::Piece;

  my $lt1 = localtime(1057632876);
  my $dt = DateTime->from_epoch(epoch => $lt1->epoch());
  print $dt->datetime(), "\n"; # Prints: 2003-07-08T02:54:36

  my $lt2 = gmtime($dt->epoch());
  print $lt2, "\n";  # Prints: Tue Jul  8 02:54:36 2003

4.7: How Do I Convert between Date::Manip and DateTime Objects?

Use the DateTime::Format::DateManip module. Note that you can also use the same module to convert Date::Manip durations.

  use DateTime::Format::DateManip;
  use Date::Manip;

  my $dt = DateTime::Format::DateManip->parse_datetime
               ("Jan 1st, 2001 12:30 AM GMT");
  my $dm = DateTime::Format::DateManip->format_datetime($dt);
  # Similarly we have to use the epoch to do comparisons because
  # Date::Manip stores the times in the local timezone
  my $ep = UnixDate($dm, "%s");
  is($ep,
     '978309000',
     "DateTime::Format::Manip->format_datetime()");

4.8: How Do I Convert between Date::Calc Times and DateTime Objects?

This is a little tricky because Date::Calc allows you to have GMT based times and local times, but you have to track which is which manually. The best way to handle this is to use the Date_To_Time(...) or Mktime(...) functions to convert a GMT or Local time to an epoch, then use the recipe in How Do I Convert between Epoch Times and DateTime Objects?.

To convert from a DateTime into the Date::Calc form, use the Gmtime() or Localtime() functions with the DateTime's epoch() depending on what kind of time you want returned.

To work with a Date::Calc::Object the methods are similar, just the calling style changes.

5: Groups and ranges of dates: Durations, Sets, Spans and SpanSets

5.1: What is a DateTime::Duration?

A DateTime::Duration represents a period of time. You get DateTime::Duration objects when you subtract one DateTime object from another and you can add a DateTime::Duration to an existing DateTime object to create a new DateTime object.

A DateTime::Duration is broken down into the constituent parts, since adding 31 days may not be the same as adding 1 month, or 60 seconds may not be the same as 1 minute if there are leap seconds (see Leap seconds, short and long hours across DST changes).

  use DateTime::Duration;
  
  # TODO Think up a good example, we already do age above

5.2: What are the three end_of_month modes in DateTime::Duration?

The three modes govern how date overflows are handled when dealing with month or year durations. So if you have the following:

  use DateTime::Duration();
  
  sub test_duration_mode {
      my ($dt, $mode) = @_;

      my $dur = DateTime::Duration->new
                  (years => 1, end_of_month => $mode);
      my $res = $dt + $dur;

      print $res->ymd(), "\n";
  }

  my $dt1 = DateTime->new(year => 2000, month => 2, day => 29);
  my $dt2 = DateTime->new(year => 2003, month => 2, day => 28);

  # wrap rolls any extra over to the next month
  test_duration_mode($dt1, "wrap");     # Prints: "2001-03-01\n"
  
  # limit prevents a rollover
  test_duration_mode($dt1, "limit");    # Prints: "2001-02-28\n"

  # but will lose the end of monthness after 3 years:
  test_duration_mode($dt2, "limit");    # Prints: "2004-02-28\n"

  # preserve keeps the value at the end of the month
  test_duration_mode($dt1, "preserve"); # Prints: "2001-02-28\n"
  
  # even if it would have fallen slightly short:
  test_duration_mode($dt2, "preserve"); # Prints: "2004-02-29\n"

If you need to use to use an offset from the end of the month for days other than the last of the month you will have adjust the result manually:

TODO Finish this.

One option is:

  # From Flavio Glock
  $set = DateTime::Event::Recurrence->monthly( days => -2 );
  print "Next occurrence ", $set->next( $dt )->datetime;

5.3: How do I compare DateTime::Duration objects?

You can not directly compare DateTime::Duration objects because the number of days in a month varies, the number of hours in a day and the number of seconds in a minute (see Leap seconds, short and long hours across DST changes).

So if you have a DateTime::Duration that represents 1 month and another that represents 29 days, you can't say whether the 29 days is 1 month until you know what dates you are dealing with to know if that covers February or not (and it is not a leap year).

To actually compare the durations you need to fix them to a starting time:

  use DateTime::Duration;

  # To compare the durations we need a date
  sub compare_durations {
    my ($dur1, $dur2, $date) = @_;

    my $dt1 = $date + $dur1;
    my $dt2 = $date + $dur2;

    return $dt1 <=> $dt2;
  }

  # So:
  my $dur1 = DateTime::Duration->new( months => 1);
  my $dur2 = DateTime::Duration->new( days   => 28);

  my $dt1  = DateTime->new(year => 2003, month => 2, day => 1);
  my $dt2  = DateTime->new(year => 2004, month => 2, day => 1);
  
  print "Month1 is 29 days\n"
    if compare_durations($dur1, $dur2, $dt1) == 0;
  print "Month2 is not 29 days\n"
    if compare_durations($dur1, $dur2, $dt2) != 0;

5.4: What are DateTime::Set objects?

A DateTime::Set is an efficient representation of a number of DateTime objects. You can either create them from a list of existing DateTime objects:

  use DateTime::Set;

  my $dt1  = DateTime->new(year => 2003, month => 6, day => 1);
  my $dt2  = DateTime->new(year => 2003, month => 3, day => 1);
  my $dt3  = DateTime->new(year => 2003, month => 3, day => 2);
  
  my $set1 = DateTime::Set->from_datetimes( dates => [ $dt1, $dt2 ] );
  $set1 = $set1->union($dt3);  # Add in another date

  print "Min of the set is the lowest date\n"  if $dt2 == $set1->min();
  print "Max of the set is the highest date\n" if $dt1 == $set1->max();

  my $it = $set1->iterator();
  while ( my $dt = $it->next() ) {
    print $dt->ymd(), "\n";
  }
  # Prints: "2003-03-01\n2003-03-02\n2003-06-01\n"

Or DateTime::Set can handle sets that do not fully exist. For instance you could make a set that represents the first of every month:

  my $set = DateTime::Set->from_recurrence(
              recurrence => sub {
                  $_[0]->truncate( to => 'month' )->add( months => 1 )
              });
  my $dt1 = DateTime->new(year => 2003, month => 3, day => 1);
  my $dt2 = DateTime->new(year => 2003, month => 2, day => 11);
  print "2003-03-01 is the first of the month\n"
      if $set->contains($dt1);
  print "2003-03-01 is not the first of the month\n"
      unless $set->contains($dt2);

5.5: How do I get at the dates inside a DateTime::Set?

You can use contains() to see if a given date is in the set as shown in What are DateTime::Set objects? or you can use an iterator to loop over all values in the set.

To iterate over a set you need to make sure that the start date of the set is defined (and if you want the iterator to ever finish you need to make sure that there is an end date. If your set does not have one yet, you can either create a new DateTime::Set or a DateTime::Span and take the intersection of the set. As a convenience, the iterator() method takes the same arguments as DateTime::Span and will use them to limit the iteration as if the corresponding span were used.

In the following example we use DateTime::Event::Recurrence to more easily define a monthly recurrence that is equivalent to the one we defined manually in What are DateTime::Set objects?.

  use DateTime::Event::Recurrence;

  my $set = DateTime::Event::Recurrence->monthly();
  my $dt1 = DateTime->new(year => 2003, month => 3, day => 2);
  my $dt2 = DateTime->new(year => 2003, month => 6, day => 1);

  # Unlimited iterator on an unbounded set
  my $it1 = $set->iterator();
  print $it1->next(), "\n";  # Prints: "-inf\n"

  # Limited iterator on an unbounded set
  my $it2 = $set->iterator(start => $dt1, end => $dt2);
  while ( $dt = $it2->previous() ) {
    print $dt->ymd(), "\n";
  }
  # Prints: "2003-06-01\n2003-05-01\n2003-04-01\n"

In the previous example we used the method previous() to iterate over a set from the highest date to the lowest.

Or you can turn a DateTime::Set into a simple list of DateTime objects using the as_list method. If possible you should avoid doing this because the DateTime::Set representation is far more efficient.

5.6: What are the DateTime::Set set operations?

One of the most important features of DateTime::Set is that you can perform set operations. For instance you can take a set representing the first day in each month and intersect it with a set representing Mondays and the resultant set would give you the dates where Monday is the first day of the month:

  use DateTime::Event::Recurrence;

  # First of the month
  my $fom = DateTime::Event::Recurrence->monthly();

  # Every Monday (first day of the week)
  my $mon = DateTime::Event::Recurrence->weekly( days => 1 );

  # Every Monday that is the start of a month
  my $set = $fom->intersection($mon);
  
  my $it = $set->iterator
             (start  =>
              DateTime->new(year => 2003, month => 1, day => 1),
              before =>
              DateTime->new(year => 2004, month => 1, day => 1));

  while ( my $dt = $it->previous() ) {
    print $dt->ymd(), "\n";
  }
  # Prints: "2003-12-01\n2003-09-01\n"
  

The complete list of set operations are:

The last operator, unary complement $set3 = $set1-complement()> returns all of the items that do not exist in $set1 as a DateTime::SpanSet.

5.7: Is there an easier way to create sets than writing subroutines?

The following modules create some useful common recurrences.

5.8: What are DateTime::Span objects?

A DateTime::Span represents an event that occurs over a range of time rather than a DateTime which really is a point event (although a DateTime can be used to represent a span if you truncate the objects to the same resolution, see L"Why do I need to truncate dates?>). Unlike DateTime::Duration objects they have fixed start points and ending points.

TODO More

5.9: What are DateTime::SpanSet objects?

A DateTime::SpanSet represents a set of DateTime::Span objects. For example you could represent the stylized working week of 9-5, M-F with 12-1 as lunch break (ignoring holidays) as follows:

  use DateTime::Event::Recurrence;
  use DateTime::SpanSet;

  # Make the set representing the work start times: M-F 9AM and 1PM
  my $start = DateTime::Event::Recurrence->weekly
               ( days => [1 .. 5], hours => [8, 13] );
  # Make the set representing the work end times: M-F 12PM and 5PM
  my $end   = DateTime::Event::Recurrence->weekly
               ( days => [1 .. 5], hours => [12, 17] );

  # Build a spanset from the set of starting points and ending points
  my $spanset = DateTime::SpanSet->from_sets
                  ( start_set => $start,
                    end_set   => $end );

  # Iterate from Thursday the 3rd to Monday the 6th 
  my $it = $spanset->iterator
             (start  =>
              DateTime->new(year => 2003, month => 1, day => 3),
              before =>
              DateTime->new(year => 2003, month => 1, day => 7));

  while (my $span = $it->next) {
      my ($st, $end) = ($span->start(), $span->end());
      print $st->day_abbr, " ", $st->hour, " to ", $end->hour, "\n";
  }
  # Prints: "Fri 8 to 12\nFri 13 to 17\nMon 8 to 12\nMon 13 to 17\n"

  # Now see if a given DateTime falls within working hours
  my $dt = DateTime->new(year => 2003, month => 2, day => 11, hour => 11);
  print $dt->datetime, " is a work time\n"
      if $spanset->contains( $dt );

6: Sample Date Calculations

6.1: How do I check whether a given date lies within a certain range of days?

  my $dt1  = DateTime->new(year => 2002, month => 3, day => 1);
  my $dt2  = DateTime->new(year => 2002, month => 2, day => 11);
  my $date = DateTime->new(year => 2002, month => 2, day => 23);

  # Make sure $dt1 is less than $dt2
  ($dt1, $dt2) = ($dt2, $dt1) if $dt1 > $dt2;

  # Truncate all dates to day resolution (skip this if you want
  # to compare exact times)
  $dt1->truncate(  to => 'day' );
  $dt1->truncate(  to => 'day' );
  $date->truncate( to => 'day' );

  # Now do the comparison
  if ($dt1 <= $date and $date <= $dt2) {
    print '$date is between the given dates';
  }

Or you can do it using DateTime::Span:

  use DateTime::Span;

  my $dt1  = DateTime->new(year => 2002, month => 3, day => 1);
  my $dt2  = DateTime->new(year => 2002, month => 2, day => 11);
  my $date = DateTime->new(year => 2002, month => 2, day => 23);

  # Make sure $dt1 is less than $dt2
  ($dt1, $dt2) = ($dt2, $dt1) if $dt1 > $dt2;

  # Make the span (use after and before if you want > and < rather
  # than the >= and <= that start and end give)
  my $span = DateTime::Span->from_datetimes(start => $dt1,
                                            end   => $dt2);
  if ($span->contains(date)) {
    print '$date is between the given dates';
  }

TODO enable testing when this actually works =for example_testing is($_STDOUT_, '$date is between the given dates', 'Date in range (span)');

See also Why do I need to truncate dates?

6.2: How do I check whether two dates and times lie more or less than a given time interval apart?

  use DateTime::Duration;

  my $dt1  = DateTime->new(year => 2002, month => 3, day => 1);
  my $dt2  = DateTime->new(year => 2002, month => 2, day => 11);

  # Make a duration object to represent the interval
  $interval =
    DateTime::Duration->new( days => 19, hours => 3, minutes => 12);
  
  sub within_interval {
      my ($dt1, $dt2, $interval) = @_;

      # Make sure $dt1 is less than $dt2
      ($dt1, $dt2) = ($dt2, $dt1) if $dt1 > $dt2;

      # If the older date is more recent than the newer date once we
      # subtract the interval then the dates are closer than the
      # interval
      if ($dt2 - $interval < $dt1) {
          return 1;
      } else {
          return 0;
      }
  }

  print 'closer than $interval'
     if within_interval($dt1, $dt2, $interval);

6.3: How do I verify whether someone has a certain age?

This is just an application of the How do I check whether two dates and times lie more or less than a given time interval apart?

Note that simply subtracting the dates and looking at the year component will not work. See How do I compare DateTime::Duration objects?

  # Build a date representing their birthday
  my $birthday = DateTime->new(year => 1974, month  => 2, day => 11,
                               hour => 6,    minute => 14);

  # Make sure we are comparing apples to apples by truncating to days
  # since you don't have to be 18 exactly by the minute, just to the day
  $birthday->truncate( to => 'day' );
  my $today = DateTime->today();

  # Represent the range we care about
  my $age_18 = DateTime::Duration->new( years => 18 );

  print "You may be able to drink or vote..."
     unless within_interval($birthday, $today, $age_18);

6.4: How do I calculate the number of the week of month the given date lies in?

For example:

            April 1998
    Mon Tue Wed Thu Fri Sat Sun
              1   2   3   4   5  =  week #1
      6   7   8   9  10  11  12  =  week #2
     13  14  15  16  17  18  19  =  week #3
     20  21  22  23  24  25  26  =  week #4
     27  28  29  30              =  week #5
  # Takes as arguments:
  #  - The date
  #  - The day that we want to call the start of the week (1 is Monday, 7
  #    Sunday) (optional)
  sub get_week_num {
    my $dt            = shift;
    my $start_of_week = shift || 1;

    # Work out what day the first of the month falls on
    my $first = $dt->clone();
    $first->set(day => 1);
    my $wday  = $first->day_of_week();

    # And adjust the day to the start of the week
    $wday = ($wday - $start_of_week + 7) % 7;

    # Then do the calculation to work out the week
    my $mday  = $dt->day_of_month_0();

    return int ( ($mday + $wday) / 7 ) + 1;
  }

6.5: How do I calculate whether a given date is the 1st, 2nd, 3rd, 4th or 5th of that day of week in the given month?

  # Takes as arguments:
  #  - The date
  sub get_day_occurrence {
    my $dt  = shift;
    return int( $dt->day_of_month_0() / 7 + 1 );
  }

6.6: How do I calculate the date of the Wednesday of the same week as the current date?

  # Takes as arguments:
  #  - The date
  #  - The target day (1 is Monday, 7 Sunday)
  #  - The day that we want to call the start of the week (1 is Monday, 7
  #    Sunday) (optional)
  # NOTE: This may end up in a different month...
  sub get_day_in_same_week {
    my $dt            = shift;
    my $target        = shift;
    my $start_of_week = shift || 1;

    # Work out what day the date is within the (corrected) week
    my $wday = ($dt->day_of_week() - $start_of_week + 7) % 7;

    # Correct the argument day to our week
    $target = ($target - $start_of_week + 7) % 7;

    # Then adjust the current day
    return $dt->clone()->add(days => $target - $wday);
  }

6.7: How do I calculate the last and the next Saturday for any given date?

  # The date and target (1 is Monday, 7 Sunday)
  my $dt = DateTime->new(year => 1998, month => 4, day => 3); # Friday
  my $target = 6; # Saturday

  # Get the day of the week for the given date
  my $dow = $dt->day_of_week();
  
  # Apply the corrections
  my ($prev, $next) = ($dt->clone(), $dt->clone());

  if ($dow == $target) {
      $prev->add( days => -7 );
      $next->add( days =>  7 );
  } else {
      my $correction = ( $target - $dow + 7 ) % 7;
      $prev->add( days => $correction - 7 );
      $next->add( days => $correction );
  }

  # $prev is 1998-03-28, $next is 1998-04-04

6.8: How can I calculate the last business day (payday!) of a month?

TODO

6.9: How can I find what day the third Friday of a month is on?

  # Define the meeting time and a date in the current month
  my $meeting_day  = 5; # (1 is Monday, 7 is Sunday)
  my $meeting_week = 3;
  my $dt = DateTime->new(year => 1998, month => 4, day => 4);

  # Get the first of the month we care about
  my $result = $dt->clone()->set( day => 1 );

  # Adjust the result to the correct day of the week and adjust the
  # weeks
  my $dow = $result->day_of_week();
  $result->add( days => ( $meeting_day - $dow + 7 ) % 7,
                weeks => $meeting_week - 1 );

  # See if we went to the next month
  die "There is no matching date in the month"
     if $dt->month() != $result->month();

  # $result is now 1998-4-17

6.10: How can I iterate through a range of dates?

The following recipe assumes that you have 2 dates and want to loop over them. An alternate way would be to create a DateTime::Set and iterate over it.

  my $start_dt = DateTime->new(year => 1998, month  => 4,  day => 7);
  my $end_dt   = DateTime->new(year => 1998, month  => 7,  day => 7);

  my $weeks = 0;
  for (my $dt = $start_dt->clone();
       $dt <= $end_dt;
       $dt->add(weeks => 1) )
  {
    $weeks++;
  }

6.11: How can I create a list of dates in a certain range?

There are a few ways to do this, you can create a list of DateTime objects, create a DateTime::Set object that represents the list, or simply use the iterator from question How can I iterate through a range of dates?.

Of the three choices, the simple iteration is probably fastest, but you can not easily pass the list around. If you need to pass a list of dates around then DateTime::Set is the way to go since it doesn't generate the dates until they are needed and you can easily augment or filter the list. See What are DateTime::Set objects?.

  # As a Perl list
  my $start_dt = DateTime->new(year => 1998, month  => 4,  day => 7);
  my $end_dt   = DateTime->new(year => 1998, month  => 7,  day => 7);

  my @list = ();
  for (my $dt = $start_dt->clone();
       $dt <= $end_dt;
       $dt->add(weeks => 1) )
  {
    push @list, $dt->clone();
  }
  
  # As a DateTime::Set.  We use DateTime::Event::Recurrence to easily
  # create the sets (see also DateTime::Event::ICal for more
  # complicated sets)
  use DateTime::Event::Recurrence;
  use DateTime::Span;
  my $set = DateTime::Event::Recurrence->daily(start    => $start_dt,
                                               interval => 7);
  $set = $set->intersection(DateTime::Span->from_datetimes
                                (start => $start_dt, end => $end_dt ));

6.12: How can I calculate the difference in days between dates, but without counting Saturdays and Sundays?

TODO

6.13: How do I find yesterday's Date?

     my $dt = DateTime->now()->subtract( days => 1 );
     print $dt->ymd;

7: Miscellaneous

7.1: How do I do business week calculations?

TODO

e.g. 3 business days from now...

7.2: Something about the locales

TODO - needs to be written

7.3: What are with GMT, TAI, UTC, and UT1?

Provided by Flavio Glock (fglock at pucrs dot br), from discussions with Peter J. Acklam, Flavio Glock, John Peacock, Eugene Van Der Pijll and others

Before 1972, the "international time" reference was GMT. In GMT, all days have the same number of seconds. A day starts at "midnight" and has 86400 seconds. However, the length of a second would vary since it was based on astronomical obervations.

TAI is another time measuring system, in which seconds depend on "atomic time" only, instead of the Sun-Earth position. TAI days have 86400 seconds, and its origin is in 1958 January 1.

Parallel with those, there exists UT1, which is the "astronomical time". UT1 depends only on Sun-Earth position. UT1 - TAI is some fractional seconds.

In 1972 UTC was introduced, in order to approximate "international time" to "astronomical time". Now, whenever the difference between UTC and UT1 is big enough, a leap second is introduced. UTC is synchronized to TAI, which means that UTC - TAI is an integer number of seconds. UTC - UT1 is a fraction of a second.

The DateTime module keeps time in UTC.

7.4: Leap seconds, short and long hours across DST changes

TODO Explain why some days have 23 or 25 hours, and so on.

7.5: Stringificiation

TODO Explain how to stringify

TODO Other Modules that are useful

8: Credits

Major thanks to Dave Rolsky for being sufficiently insane to write the DateTime module to start with and to shepherd the rest of the asylum into making something cool.

Equally major thanks to the rest of the asylum (Flavio Glock, Rick Measham, Iain Truskett, Eugene van der Pijll, Claus Färber, Kellan Elliot-McCrea, Daniel Yacob, Jean Forget, Joshua Hoblitt, Matt Sisk, Ron Hill, and many others) for working on this wonderful project and bearing with my silly questions.

Thanks to Steffen Beyer from whose POD in the Date::Calc module I nicked most of the initial questions (and to Ron Hill for suggesting that.

9: Copyright

Copyright (C) 2003, Benjamin Bennett. All rights reserved.

Released under the same terms as Perl.

<%method title> DateTime FAQ