1 package DateTime::TimeZone;
10 use DateTime::TimeZone::Catalog;
11 use DateTime::TimeZone::Floating;
12 use DateTime::TimeZone::Local;
13 use DateTime::TimeZone::OffsetOnly;
14 use DateTime::TimeZone::UTC;
15 use Params::Validate qw( validate validate_pos SCALAR ARRAYREF BOOLEAN );
17 use constant INFINITY => 100 ** 1000 ;
18 use constant NEG_INFINITY => -1 * (100 ** 1000);
20 # the offsets for each span element
21 use constant UTC_START => 0;
22 use constant UTC_END => 1;
23 use constant LOCAL_START => 2;
24 use constant LOCAL_END => 3;
25 use constant OFFSET => 4;
26 use constant IS_DST => 5;
27 use constant SHORT_NAME => 6;
29 my %SpecialName = map { $_ => 1 } qw( EST MST HST CET EET MET WET EST5EDT CST6CDT MST7MDT PST8PDT );
35 { name => { type => SCALAR } },
38 if ( exists $DateTime::TimeZone::Catalog::LINKS{ $p{name} } )
40 $p{name} = $DateTime::TimeZone::Catalog::LINKS{ $p{name} };
42 elsif ( exists $DateTime::TimeZone::Catalog::LINKS{ uc $p{name} } )
44 $p{name} = $DateTime::TimeZone::Catalog::LINKS{ uc $p{name} };
47 unless ( $p{name} =~ m,/,
48 || $SpecialName{ $p{name} }
51 if ( $p{name} eq 'floating' )
53 return DateTime::TimeZone::Floating->new;
56 if ( $p{name} eq 'local' )
58 return DateTime::TimeZone::Local->TimeZone();
61 if ( $p{name} eq 'UTC' || $p{name} eq 'Z' )
63 return DateTime::TimeZone::UTC->new;
66 return DateTime::TimeZone::OffsetOnly->new( offset => $p{name} );
69 my $subclass = $p{name};
71 $subclass =~ s{/}{::}g;
72 my $real_class = "DateTime::TimeZone::$subclass";
74 die "The timezone '$p{name}' in an invalid name.\n"
75 unless $real_class =~ /^\w+(::\w+)*$/;
77 unless ( $real_class->can('instance') )
79 my $e = do { local $@;
81 eval "require $real_class";
87 my $regex = join '.', split /::/, $real_class;
90 if ( $e =~ /^Can't locate $regex/i )
92 die "The timezone '$p{name}' could not be loaded, or is an invalid name.\n";
101 my $zone = $real_class->instance( name => $p{name}, is_olson => 1 );
103 if ( $zone->is_olson() )
106 $zone->can('olson_version')
107 ? $zone->olson_version()
109 my $catalog_version = DateTime::TimeZone::Catalog->OlsonVersion();
111 if ( $object_version ne $catalog_version )
113 warn "Loaded $real_class, which is from an older version ($object_version) of the Olson database than this installation of DateTime::TimeZone ($catalog_version).\n";
123 my %p = validate( @_,
124 { name => { type => SCALAR },
125 spans => { type => ARRAYREF },
126 is_olson => { type => BOOLEAN, default => 0 },
130 my $self = bless { name => $p{name},
132 is_olson => $p{is_olson},
135 foreach my $k ( qw( last_offset last_observance rules max_year ) )
138 $self->{$k} = $self->$m() if $self->can($m);
144 sub is_olson { $_[0]->{is_olson} }
146 sub is_dst_for_datetime
150 my $span = $self->_span_for_datetime( 'utc', $_[0] );
152 return $span->[IS_DST];
155 sub offset_for_datetime
159 my $span = $self->_span_for_datetime( 'utc', $_[0] );
161 return $span->[OFFSET];
164 sub offset_for_local_datetime
168 my $span = $self->_span_for_datetime( 'local', $_[0] );
170 return $span->[OFFSET];
173 sub short_name_for_datetime
177 my $span = $self->_span_for_datetime( 'utc', $_[0] );
179 return $span->[SHORT_NAME];
182 sub _span_for_datetime
188 my $method = $type . '_rd_as_seconds';
190 my $end = $type eq 'utc' ? UTC_END : LOCAL_END;
193 my $seconds = $dt->$method();
194 if ( $seconds < $self->max_span->[$end] )
196 $span = $self->_spans_binary_search( $type, $seconds );
200 my $until_year = $dt->utc_year + 1;
201 $span = $self->_generate_spans_until_match( $until_year, $seconds, $type );
204 # This means someone gave a local time that doesn't exist
205 # (like during a transition into savings time)
206 unless ( defined $span )
208 my $err = 'Invalid local time for date';
209 $err .= ' ' . $dt->iso8601 if $type eq 'utc';
210 $err .= " in time zone: " . $self->name;
219 sub _spans_binary_search
222 my ( $type, $seconds ) = @_;
224 my ( $start, $end ) = _keys_for_type($type);
227 my $max = scalar @{ $self->{spans} } + 1;
228 my $i = int( $max / 2 );
229 # special case for when there are only 2 spans
230 $i++ if $max % 2 && $max != 3;
232 $i = 0 if @{ $self->{spans} } == 1;
236 my $current = $self->{spans}[$i];
238 if ( $seconds < $current->[$start] )
241 my $c = int( ( $i - $min ) / 2 );
248 elsif ( $seconds >= $current->[$end] )
251 my $c = int( ( $max - $i ) / 2 );
256 return if $i >= $max;
260 # Special case for overlapping ranges because of DST and
261 # other weirdness (like Alaska's change when bought from
262 # Russia by the US). Always prefer latest span.
263 if ( $current->[IS_DST] && $type eq 'local' )
265 # Asia/Dhaka in 2009j goes into DST without any known
266 # end-of-DST date (wtf, Bangladesh).
267 return $current if $current->[UTC_END] == INFINITY;
269 my $next = $self->{spans}[$i + 1];
270 # Sometimes we will get here and the span we're
271 # looking at is the last that's been generated so far.
272 # We need to try to generate one more or else we run
274 $next ||= $self->_generate_next_span;
276 die "No next span in $self->{max_year}" unless defined $next;
278 if ( ( ! $next->[IS_DST] )
279 && $next->[$start] <= $seconds
280 && $seconds <= $next->[$end]
292 sub _generate_next_span
296 my $last_idx = $#{ $self->{spans} };
298 my $max_span = $self->max_span;
300 # Kind of a hack, but AFAIK there are no zones where it takes
301 # _more_ than a year for a _future_ time zone change to occur, so
302 # by looking two years out we can ensure that we will find at
303 # least one more span. Of course, I will no doubt be proved wrong
304 # and this will cause errors.
305 $self->_generate_spans_until_match
306 ( $self->{max_year} + 2, $max_span->[UTC_END] + ( 366 * 86400 ), 'utc' );
308 return $self->{spans}[ $last_idx + 1 ];
311 sub _generate_spans_until_match
314 my $generate_until_year = shift;
319 my @rules = @{ $self->_rules };
320 foreach my $year ( $self->{max_year} .. $generate_until_year )
322 for ( my $x = 0; $x < @rules; $x++ )
324 my $last_offset_from_std;
328 $last_offset_from_std =
329 $x ? $rules[0]->offset_from_std : $rules[1]->offset_from_std;
331 elsif ( @rules == 1 )
333 $last_offset_from_std = $rules[0]->offset_from_std;
337 my $count = scalar @rules;
338 die "Cannot generate future changes for zone with $count infinite rules\n";
341 my $rule = $rules[$x];
344 $rule->utc_start_datetime_for_year
345 ( $year, $self->{last_offset}, $last_offset_from_std );
347 # don't bother with changes we've seen already
348 next if $next->utc_rd_as_seconds < $self->max_span->[UTC_END];
351 DateTime::TimeZone::OlsonDB::Change->new
353 utc_start_datetime => $next,
354 local_start_datetime =>
356 DateTime::Duration->new
357 ( seconds => $self->{last_observance}->total_offset +
358 $rule->offset_from_std ),
360 sprintf( $self->{last_observance}->format, $rule->letter ),
361 observance => $self->{last_observance},
367 $self->{max_year} = $generate_until_year;
369 my @sorted = sort { $a->utc_start_datetime <=> $b->utc_start_datetime } @changes;
371 my ( $start, $end ) = _keys_for_type($type);
374 for ( my $x = 1; $x < @sorted; $x++ )
376 my $last_total_offset =
377 $x == 1 ? $self->max_span->[OFFSET] : $sorted[ $x - 2 ]->total_offset;
380 DateTime::TimeZone::OlsonDB::Change::two_changes_as_span
381 ( @sorted[ $x - 1, $x ], $last_total_offset );
383 $span = _span_as_array($span);
385 push @{ $self->{spans} }, $span;
388 if $seconds >= $span->[$start] && $seconds < $span->[$end];
394 sub max_span { $_[0]->{spans}[-1] }
398 $_[0] eq 'utc' ? ( UTC_START, UTC_END ) : ( LOCAL_START, LOCAL_END );
403 [ @{ $_[0] }{ qw( utc_start utc_end local_start local_end offset is_dst short_name ) } ];
406 sub is_floating { 0 }
410 sub has_dst_changes { 0 }
412 sub name { $_[0]->{name} }
413 sub category { (split /\//, $_[0]->{name}, 2)[0] }
421 $tz = eval { $_[0]->new( name => $_[1] ) };
424 return $tz && $tz->isa('DateTime::TimeZone') ? 1 : 0
438 my $serialized = shift;
440 my $class = ref $self || $self;
443 if ( $class->isa(__PACKAGE__) )
445 $obj = __PACKAGE__->new( name => $serialized );
449 $obj = $class->new( name => $serialized );
460 sub offset_as_seconds
465 shift if eval { $_[0]->isa('DateTime::TimeZone') };
470 return undef unless defined $offset;
472 return 0 if $offset eq '0';
474 my ( $sign, $hours, $minutes, $seconds );
475 if ( $offset =~ /^([\+\-])?(\d\d?):(\d\d)(?::(\d\d))?$/ )
477 ( $sign, $hours, $minutes, $seconds ) = ( $1, $2, $3, $4 );
479 elsif ( $offset =~ /^([\+\-])?(\d\d)(\d\d)(\d\d)?$/ )
481 ( $sign, $hours, $minutes, $seconds ) = ( $1, $2, $3, $4 );
488 $sign = '+' unless defined $sign;
489 return undef unless $hours >= 0 && $hours <= 99;
490 return undef unless $minutes >= 0 && $minutes <= 59;
491 return undef unless ! defined( $seconds ) || ( $seconds >= 0 && $seconds <= 59 );
493 my $total = $hours * 3600 + $minutes * 60;
494 $total += $seconds if $seconds;
495 $total *= -1 if $sign eq '-';
505 shift if eval { $_[0]->isa('DateTime::TimeZone') };
510 return undef unless defined $offset;
511 return undef unless $offset >= -359999 && $offset <= 359999;
513 my $sign = $offset < 0 ? '-' : '+';
515 $offset = abs($offset);
517 my $hours = int( $offset / 3600 );
519 my $mins = int( $offset / 60 );
521 my $secs = int( $offset );
524 sprintf( '%s%02d%02d%02d', $sign, $hours, $mins, $secs ) :
525 sprintf( '%s%02d%02d', $sign, $hours, $mins )
529 # These methods all operate on data contained in the DateTime/TimeZone/Catalog.pm file.
533 return wantarray ? @DateTime::TimeZone::Catalog::ALL : [@DateTime::TimeZone::Catalog::ALL];
539 ? @DateTime::TimeZone::Catalog::CATEGORY_NAMES
540 : [@DateTime::TimeZone::Catalog::CATEGORY_NAMES];
546 wantarray ? %DateTime::TimeZone::Catalog::LINKS : {%DateTime::TimeZone::Catalog::LINKS};
549 sub names_in_category
551 shift if $_[0]->isa('DateTime::TimeZone');
552 return unless exists $DateTime::TimeZone::Catalog::CATEGORIES{ $_[0] };
556 ? @{ $DateTime::TimeZone::Catalog::CATEGORIES{ $_[0] } }
557 : [ $DateTime::TimeZone::Catalog::CATEGORIES{ $_[0] } ];
563 ? ( sort keys %DateTime::TimeZone::Catalog::ZONES_BY_COUNTRY )
564 : [ sort keys %DateTime::TimeZone::Catalog::ZONES_BY_COUNTRY ];
569 shift if $_[0]->isa('DateTime::TimeZone');
571 return unless exists $DateTime::TimeZone::Catalog::ZONES_BY_COUNTRY{ lc $_[0] };
575 ? @{ $DateTime::TimeZone::Catalog::ZONES_BY_COUNTRY{ lc $_[0] } }
576 : $DateTime::TimeZone::Catalog::ZONES_BY_COUNTRY{ lc $_[0] };
586 DateTime::TimeZone - Time zone object base class and factory
591 use DateTime::TimeZone;
593 my $tz = DateTime::TimeZone->new( name => 'America/Chicago' );
595 my $dt = DateTime->now();
596 my $offset = $tz->offset_for_datetime($dt);
600 This class is the base class for all time zone objects. A time zone
601 is represented internally as a set of observances, each of which
602 describes the offset from GMT for a given time period.
604 Note that without the C<DateTime.pm> module, this module does not do
605 much. It's primary interface is through a C<DateTime> object, and
606 most users will not need to directly use C<DateTime::TimeZone>
611 This class has the following methods:
613 =head2 DateTime::TimeZone->new( name => $tz_name )
615 Given a valid time zone name, this method returns a new time zone
616 blessed into the appropriate subclass. Subclasses are named for the
617 given time zone, so that the time zone "America/Chicago" is the
618 DateTime::TimeZone::America::Chicago class.
620 If the name given is a "link" name in the Olson database, the object
621 created may have a different name. For example, there is a link from
622 the old "EST5EDT" name to "America/New_York".
624 When loading a time zone from the Olson database, the constructor
625 checks the version of the loaded class to make sure it matches the
626 version of the current DateTime::TimeZone installation. If they do not
627 match it will issue a warning. This is useful because time zone names
628 may fall out of use, but you may have an old module file installed for
631 There are also several special values that can be given as names.
633 If the "name" parameter is "floating", then a
634 C<DateTime::TimeZone::Floating> object is returned. A floating time
635 zone does have I<any> offset, and is always the same time. This is
636 useful for calendaring applications, which may need to specify that a
637 given event happens at the same I<local> time, regardless of where it
638 occurs. See RFC 2445 for more details.
640 If the "name" parameter is "UTC", then a C<DateTime::TimeZone::UTC>
643 If the "name" is an offset string, it is converted to a number, and a
644 C<DateTime::TimeZone::OffsetOnly> object is returned.
646 =head3 The "local" time zone
648 If the "name" parameter is "local", then the module attempts to
649 determine the local time zone for the system.
651 The method for finding the local zone varies by operating system. See
652 the appropriate module for details of how we check for the local time
657 =item * L<DateTime::TimeZone::Local::Unix>
659 =item * L<DateTime::TimeZone::Local::Win32>
661 =item * L<DateTime::TimeZone::Local::VMS>
665 If a local time zone is not found, then an exception will be thrown.
667 =head2 $tz->offset_for_datetime( $dt )
669 Given a C<DateTime> object, this method returns the offset in seconds
670 for the given datetime. This takes into account historical time zone
671 information, as well as Daylight Saving Time. The offset is
672 determined by looking at the object's UTC Rata Die days and seconds.
674 =head2 $tz->offset_for_local_datetime( $dt )
676 Given a C<DateTime> object, this method returns the offset in seconds
677 for the given datetime. Unlike the previous method, this method uses
678 the local time's Rata Die days and seconds. This should only be done
679 when the corresponding UTC time is not yet known, because local times
680 can be ambiguous due to Daylight Saving Time rules.
684 Returns the name of the time zone. If this value is passed to the
685 C<new()> method, it is guaranteed to create the same object.
687 =head2 $tz->short_name_for_datetime( $dt )
689 Given a C<DateTime> object, this method returns the "short name" for
690 the current observance and rule this datetime is in. These are names
691 like "EST", "GMT", etc.
693 It is B<strongly> recommended that you do not rely on these names for
694 anything other than display. These names are not official, and many
695 of them are simply the invention of the Olson database maintainers.
696 Moreover, these names are not unique. For example, there is an "EST"
697 at both -0500 and +1000/+1100.
699 =head2 $tz->is_floating
701 Returns a boolean indicating whether or not this object represents a
702 floating time zone, as defined by RFC 2445.
706 Indicates whether or not this object represents the UTC (GMT) time
709 =head2 $tz->has_dst_changes
711 Indicates whether or not this zone has I<ever> had a change to and
712 from DST, either in the past or future.
716 Returns true if the time zone is a named time zone from the Olson
721 Returns the part of the time zone name before the first slash. For
722 example, the "America/Chicago" time zone would return "America".
724 =head2 DateTime::TimeZone->is_valid_name($name)
726 Given a string, this method returns a boolean value indicating whether
727 or not the string is a valid time zone name. If you are using
728 C<DateTime::TimeZone::Alias>, any aliases you've created will be valid.
730 =head2 DateTime::TimeZone->all_names
732 This returns a pre-sorted list of all the time zone names. This list
733 does not include link names. In scalar context, it returns an array
734 reference, while in list context it returns an array.
736 =head2 DateTime::TimeZone->categories
738 This returns a list of all time zone categories. In scalar context,
739 it returns an array reference, while in list context it returns an
742 =head2 DateTime::TimeZone->links
744 This returns a hash of all time zone links, where the keys are the
745 old, deprecated names, and the values are the new names. In scalar
746 context, it returns a hash reference, while in list context it returns
749 =head2 DateTime::TimeZone->names_in_category( $category )
751 Given a valid category, this method returns a list of the names in
752 that category, without the category portion. So the list for the
753 "America" category would include the strings "Chicago",
754 "Kentucky/Monticello", and "New_York". In scalar context, it returns
755 an array reference, while in list context it returns an array.
757 The list is returned in order of population by zone, which should mean
758 that this order will be the best to use for most UIs.
760 =head2 DateTime::TimeZone->countries()
762 Returns a sorted list of all the valid country codes (in lower-case)
763 which can be passed to C<names_in_country()>. In scalar context, it
764 returns an array reference, while in list context it returns an array.
766 If you need to convert country codes to names or vice versa you can
767 use C<Locale::Country> to do so.
769 =head2 DateTime::TimeZone->names_in_country( $country_code )
771 Given a two-letter ISO3166 country code, this method returns a list of
772 time zones used in that country. The country code may be of any
773 case. In scalar context, it returns an array reference, while in list
774 context it returns an array.
776 =head2 DateTime::TimeZone->offset_as_seconds( $offset )
778 Given an offset as a string, this returns the number of seconds
779 represented by the offset as a positive or negative number. Returns
780 C<undef> if $offset is not in the range C<-99:59:59> to C<+99:59:59>.
782 The offset is expected to match either
783 C</^([\+\-])?(\d\d?):(\d\d)(?::(\d\d))?$/> or
784 C</^([\+\-])?(\d\d)(\d\d)(\d\d)?$/>. If it doesn't match either of
785 these, C<undef> will be returned.
787 This means that if you want to specify hours as a single digit, then
788 each element of the offset must be separated by a colon (:).
790 =head2 DateTime::TimeZone->offset_as_string( $offset )
792 Given an offset as a number, this returns the offset as a string.
793 Returns C<undef> if $offset is not in the range C<-359999> to C<359999>.
795 =head2 Storable Hooks
797 This module provides freeze and thaw hooks for C<Storable> so that the
798 huge data structures for Olson time zones are not actually stored in
799 the serialized structure.
801 If you subclass C<DateTime::TimeZone>, you will inherit its hooks,
802 which may not work for your module, so please test the interaction of
803 your module with Storable.
807 Support for this module is provided via the datetime@perl.org email
808 list. See http://datetime.perl.org/?MailingList for details.
810 Please submit bugs to the CPAN RT system at
811 http://rt.cpan.org/NoAuth/ReportBug.html?Queue=datetime%3A%3Atimezone
812 or via email at bug-datetime-timezone@rt.cpan.org.
816 If you'd like to thank me for the work I've done on this module,
817 please consider making a "donation" to me via PayPal. I spend a lot of
818 free time creating free software, and would appreciate any support
821 Please note that B<I am not suggesting that you must do this> in order
822 for me to continue working on this particular software. I will
823 continue to do so, inasmuch as I have in the past, for as long as it
826 Similarly, a donation made in this way will probably not make me work
827 on this software much more, unless I get so many donations that I can
828 consider working on free software full time, which seems unlikely at
831 To donate, log into PayPal and send money to autarch@urth.org or use
832 the button on this page:
833 L<http://www.urth.org/~autarch/fs-donation.html>
837 Dave Rolsky <autarch@urth.org>
841 This module was inspired by Jesse Vincent's work on
842 Date::ICal::Timezone, and written with much help from the
843 datetime@perl.org list.
847 Copyright (c) 2003-2008 David Rolsky. All rights reserved. This
848 program is free software; you can redistribute it and/or modify it
849 under the same terms as Perl itself.
851 The full text of the license can be found in the LICENSE file included
856 datetime@perl.org mailing list
858 http://datetime.perl.org/
860 The tools directory of the DateTime::TimeZone distribution includes
861 two scripts that may be of interest to some people. They are
862 parse_olson and tests_from_zdump. Please run them with the --help
863 flag to see what they can be used for.