DateTime FAQ

"FAQ Administrivia"

"Basic DateTime usage"

"Time Zones"

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

"Sample Date Calculations"

"Miscellaneous"

"Build Problems"

"Credits"

"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. If you see TODO that means that we know there is work to be done in the FAQ there. QUESTION means that there are questions or open issues that need to be straightened out.

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 <perlfunc/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 language 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.

QUESTION Is it worthwhile to add an optional resolution argument to compare to allow easier usage?

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 or DateTime::Format::Strptime to easily build a parser.

QUESTION What is the overlap between DateTime::Format::Builder and DateTime::Format::Strptime? Which is preferred?

QUESTION Typo in DateTime::Format::Strptime "This method is synonymous with DateTime's strptime method."

2.7: 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"); 
  
  # QUESTION How do you print non 0 or space padded years and months?

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. 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" in perlfunc 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...

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?

Yoy 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 better America/New_York.

In general you want to use the long time zone specifications since they are more accurate and handle historical changes better. The short ones should only be used for display purposes.

QUESTION What happened to the module that was going to handle guessing?

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

4.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 "TODO Leap seconds, short / long hours across DST changes").

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

4.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 ajust the result manually:

QUESTION: How do you make the last_day_of_month from an existing date? Perhaps if called as a method with no args then it should use the given object for the arguments (obviously skipping day)?

4.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 "TODO Leap seconds, short / 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;

4.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);

QUESTION Do Sets constructed from explicit dates always get sorted?

QUESTION Do we need a DateTime::Set clone method (ditto span, etc.)?

QUESTION Did we ever work out how to attatch information to a date from a set?

4.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.

QUESTION Are we going to return an object for +/-inf or will it always be a string?

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

One of the most imporatant 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:

$set3 = $set1-union($set2)> $set3 will contain all items from $set1 and $set2.

$set3 = $set1-complement($set2)> $set3 will contain only the items from $set1 that are not in $set2.

$set3 = $set1-intersection($set2)> $set3 will contain only the items from $set1 that are in $set2.

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

QUESTION Is that true about the SpanSet? If so the docs are wrong.

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

The following modules create some useful common recurrences.

DateTime::Event::Recurrence Creates DateTime::Sets for each unit of time (day, hour, etc.) and allows you to customize them.

DateTime::Event::Cron Creates DateTime::Sets using the crontab syntax.

DateTime::Event::ICal Creates DateTime::Sets (and other objects) using the powerful but complex iCal syntax.

4.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::Durations they have fixed start points and ending points.

TODO More

4.9: What are DateTime::SpanSet objects?

A DateTime::SpanSet represents a set of DateTime::Spans. 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 );

QUESTION How do I set a time zone on sets generated from DateTime::Event::Recurrence?

5: Sample Date Calculations

5.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?"

5.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);

5.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);

5.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;
  }

QUESTION: Is there a better way to do this using DateTime::Sets?

5.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_occurence {
    my $dt  = shift;
    return int( $dt->day_of_month_0() / 7 + 1 );
  }

QUESTION: Is there a better way to do this using DateTime::Sets?

5.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);
  }

QUESTION: Is there a better way to do this using DateTime::Sets?

5.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

QUESTION: Is there a better way to do this using DateTime::Sets?

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

QUESTION: Do we do anything with business logic?

5.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

QUESTION: Is there a better way to do this using DateTime::Sets?

5.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++;
  }

5.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 ));

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

QUESTION: Is the best way to do this simply to make a DateTime::Set and loop over it?

5.13: How do I find yesterday's Date?

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

6: Miscellaneous

6.1: QUESTION How do I do business week calculations?

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

6.2: Something about the language / locale

TODO - Still waiting for this to settle out

6.3: TODO Leap seconds, short / long hours across DST changes

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

6.4: Stringificiation

TODO Explain how to stringify

TODO Other Modules that are useful

7: Build Problems

7.1: make fails on some platforms with "line too long" for DateTime::TimeZone

Ron Hill has seen this on HPUX and there was a similar problem reported by Jean Forget on cygwin (but Jean reported a workaround, see the mailing list archive).

The problem is that there are some massive lines generated in the Makefile for installing all of the time zones. Installing GNU make (gmake) fixed the problem.

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.