4: Converting To and From Other Date and Time Formats
5: Groups and ranges of dates: Durations, Sets, Spans and SpanSets
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.
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
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:
DateTime::Calendar
family modules (including the base DateTime module).strptime
(DateTime::Format::Builder).
Disadvantages:
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).
# This is the bit you really want use DateTime;
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.
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.
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
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?
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.
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
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");
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"
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"
TODO Finish this section
TODO Talk about eval if needed...
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?.
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
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
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.
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";
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
.
See How Do I Convert between Epoch Times and DateTime Objects? for the recipe.
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.
See How Do I Convert between Epoch Times and DateTime Objects? for the recipe.
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
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()");
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.
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
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;
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;
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);
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.
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:
$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.
The following modules create some useful common recurrences.
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
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 );
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?
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);
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);
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; }
# Takes as arguments: # - The date sub get_day_occurrence { my $dt = shift; return int( $dt->day_of_month_0() / 7 + 1 ); }
# 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); }
# 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
TODO
# 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
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++; }
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 ));
TODO
my $dt = DateTime->now()->subtract( days => 1 ); print $dt->ymd;
TODO
e.g. 3 business days from now...
TODO - needs to be written
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.
TODO Explain why some days have 23 or 25 hours, and so on.
TODO Explain how to stringify
TODO Other Modules that are useful
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.
Copyright (C) 2003, Benjamin Bennett. All rights reserved.
Released under the same terms as Perl.
<%method title> DateTime FAQ %method>