view release on metacpan or search on metacpan
v0.6.4 2026-04-23T16:06:35+0900
[Maintenance]
- SQLite database updated with the newly released IANA data version 2026b.
[New Features]
- DateTime::Lite::TimeZone->new() now accepts an C<extended> boolean option.
When set to a true value and the supplied name is not recognised as a valid
IANA timezone name, new() calls resolve_abbreviation() with extended => 1
internally and, if a single unambiguous candidate is found, recurses with the
resolved canonical name. This allows timezone abbreviations such as JST, CET,
or EST to be passed directly to new() without requiring the caller to call
resolve_abbreviation() explicitly. See synopsis for examples.
- DateTime::Lite->new() and set_time_zone() now accept a hash reference for the
time_zone parameter. The hash reference is passed directly to
DateTime::Lite::TimeZone->new(), allowing options such as extended => 1 or
coordinate-based resolution to be specified inline.
[Bug Fixes]
- DateTime::Lite->_new() now validates that a reference passed as time_zone
is a blessed DateTime::Lite::TimeZone object, returning a clear error
lib/DateTime/Lite.pm view on Meta::CPAN
=item * C<time_zone>
The time zone for the datetime. Accepts:
=over 8
=item * A zone name string, such as C<Asia/Tokyo>, a fixed-offset string such as C<+09:00>, C<UTC>, C<floating>, or C<local>.
=item * A L<DateTime::Lite::TimeZone> object.
=item * A hash reference whose keys are passed directly to L<DateTime::Lite::TimeZone/new>. This allows passing options that are not available on the string form, such as C<< extended => 1 >> (to resolve timezone abbreviations such as C<JST> or C<CET...
time_zone => { name => 'JST', extended => 1 }
time_zone => { latitude => 35.658558, longitude => 139.745504 }
=back
If omitted, and the C<locale> argument carries a BCP47 C<-u-tz-> extension, such as C<he-IL-u-ca-hebrew-tz-jeruslm>, the corresponding IANA canonical timezone is resolved automatically. If neither is provided, the default floating timezone is used (o...
=item * C<locale>
lib/DateTime/Lite/TimeZone.pm view on Meta::CPAN
if( !$self->{_is_canonical} )
{
$self->{name} = $canonical;
$self->{_alias_of} = $name;
}
my $ref = $self->_get_zone_info( $self->{name} );
return( $self->pass_error ) if( !defined( $ref ) && $self->error );
# If the name is not a known IANA timezone, and extended mode was requested,
# attempt to resolve it as a timezone abbreviation (such as JST or CET) via the
# extended_aliases table. Use the first unambiguous result.
if( !$ref && $extended )
{
my $candidates = $self->resolve_abbreviation( $name, extended => 1 );
if( defined( $candidates ) && @$candidates )
{
if( $candidates->[0]->{ambiguous} )
{
return( $self->error( "Timezone abbreviation '$name' is ambiguous (maps to multiple UTC offsets). Use resolve_abbreviation() with a utc_offset filter to disambiguate." ) );
}
lib/DateTime/Lite/TimeZone.pm view on Meta::CPAN
# Determine whether all candidates share the same UTC offset.
# If they do, the abbreviation is unambiguous in terms of wall-clock meaning,
# even if multiple zone names match (such as JST covering several Asian zones
# all at +09:00). Genuinely ambiguous abbreviations such as IST or CST map to
# different offsets and get ambiguous => 1.
my %offsets = map{ $_->{utc_offset} => 1 } @$all;
my $ambiguous = scalar( keys( %offsets ) ) > 1 ? 1 : 0;
# Compute is_active from footer_tz_string via word-bounded regex.
# Alphabetic abbreviations: must not be adjacent to another alpha char
# (so CEST's footer "CET-1CEST" does not match a search for "CST").
# Numeric/sign-prefixed abbreviations (such as '-03' or '+0430'): must
# appear wrapped in <...> per POSIX TZ spec. The regex is compiled
# once per call; the resulting ~35 microseconds per call overhead
# has been measured on the full 178-abbreviation test set.
my $rx;
if( $abbr =~ /^[A-Za-z]+\z/ )
{
$rx = qr/(?:^|[^A-Za-z])\Q$abbr\E(?:[^A-Za-z]|\z)/;
}
else
lib/DateTime/Lite/TimeZone.pm view on Meta::CPAN
=back
A boolean option C<use_cache_mem> set to a true value activates the process-level memory cache for this call. When set, subsequent calls with the same zone name (or its alias) return the cached object without a database query. See L</MEMORY CACHE> fo...
# Each of these hits the cache after the first construction:
my $tz = DateTime::Lite::TimeZone->new(
name => 'America/New_York',
use_cache_mem => 1,
);
A boolean option C<extended> set to a true value enables abbreviation resolution as a fallback when the name is not recognised as a valid IANA timezone name. This is useful when the caller receives a timezone abbreviation such as C<JST>, C<CET>, or C...
When C<extended> is set and the name is unknown as an IANA timezone, C<new> calls C<resolve_abbreviation> with the C<extended> option set to true internally and, if a single unambiguous candidate is found, recurses with the resolved canonical name. I...
my $tz = DateTime::Lite::TimeZone->new( name => 'JST', extended => 1 );
say $tz->name; # Asia/Tokyo
Returns the new object on success. On error, sets the L<exception object|DateTime::Lite::Exception> with C<error()> and returns C<undef> in scalar context, or an empty list in list context. In method-chaining (object) context, returns a C<DateTime::L...
=head1 MEMORY CACHE
lib/DateTime/Lite/TimeZone.pm view on Meta::CPAN
For extended alias results: C<1> if there are multiple candidates and none or more than one is marked C<is_primary>; C<0> if exactly one candidate has C<is_primary = 1>.
=item C<extended>
C<1> if this result came from the C<extended_aliases> table; C<0> if it came from the IANA types table.
=item C<is_active>
Only present in IANA results (C<extended =E<gt> 0>). C<1> if the zone's POSIX C<TZ> footer string still references this abbreviation (meaning the zone continues to cycle through this abbreviation under its current daylight-saving rules, or uses it as...
For example, C<Europe/Berlin>'s footer is C<CET-1CEST,M3.5.0,M10.5.0/3>, so both C<CET> and C<CEST> yield C<is_active =E<gt> 1>. In contrast, C<Europe/Kaliningrad>'s footer is C<EET-2>, so C<resolve_abbreviation('CEST')> still lists Kaliningrad (it u...
This field is used as the primary sort key, so zones with C<is_active =E<gt> 1> appear before zones with C<is_active =E<gt> 0>. Callers that only want currently-active zones can filter with C<grep { $_-E<gt>{is_active} } @$results>.
The detection is a word-boundary regex against the footer string: alphabetic abbreviations must be adjacent to non-alphabetic characters (so C<CST> does not match C<CEST>), and numeric/sign-prefixed abbreviations (such as C<-03>, C<+0430>) must appea...
Absent from extended alias results, where the ordering signal is the editorial C<is_primary> marker instead.
=item C<is_primary>
Only present in extended alias results (C<extended =E<gt> 1>). When multiple candidates match, C<1> marks the editorially-chosen canonical zone for this abbreviation (such as C<America/Sao_Paulo> for C<BRT>), and C<0> marks the others. Absent from IA...
scripts/build_tz_database.pl view on Meta::CPAN
BST => { comment => "British Summer Time (British Standard Time from Feb 1968 to Oct 1971)", timezones => ['Europe/London', 'Pacific/Pago_Pago'] },
BTT => { comment => "Bhutan Time", timezones => ['Asia/Thimphu'] },
# NOTE: C
C => { comment => "Charlie Military Time Zone", timezones => ['Etc/GMT-3'] },
CAST => { comment => "Casey Time Zone", timezones => ['Antarctica/Casey'] },
CAT => { comment => "Central Africa Time", timezones => ['Africa/Harare', 'Africa/Maputo', 'Africa/Lusaka', 'Africa/Blantyre', 'Africa/Bujumbura', 'Africa/Gaborone', 'Africa/Kigali', 'Africa/Lubumbashi'] },
CCT => { comment => "Cocos Islands Time", timezones => ['Indian/Cocos'] },
CDT => { comment => "Cuba Daylight Time", timezones => ['America/Chicago', 'America/Winnipeg', 'America/Havana'] },
CEST => { comment => "Central European Summer Time", timezones => ['Europe/Paris', 'Europe/Berlin', 'Europe/Rome', 'Europe/Madrid', 'Europe/Warsaw', 'Europe/Amsterdam', 'Europe/Brussels', 'Europe/Copenhagen', 'Europe/Oslo', 'Europe/Stockholm...
CETDST=> { comment => "Central Europe Summer Time", timezones => ['Europe/Paris', 'Europe/Berlin'] },
CET => { comment => "Central European Time", timezones => ['Europe/Paris', 'Europe/Berlin', 'Europe/Rome', 'Europe/Madrid', 'Europe/Warsaw', 'Europe/Amsterdam'] },
CHADT => { comment => "Chatham Daylight Time", timezones => ['Pacific/Chatham'] },
CHAST => { comment => "Chatham Standard Time", timezones => ['Pacific/Chatham'] },
CHOST => { comment => "Choibalsan Summer Time", timezones => ['Asia/Choibalsan'] },
CHOT => { comment => "Choibalsan Standard Time", timezones => ['Asia/Choibalsan'] },
CHST => { comment => "Chamorro Standard Time", timezones => ['Pacific/Guam', 'Pacific/Saipan'] },
CHUT => { comment => "Chuuk Time", timezones => ['Pacific/Chuuk'] },
CIST => { comment => "Clipperton Island Standard Time", timezones => ['Pacific/Pitcairn'] },
CKT => { comment => "Cook Island Time", timezones => ['Pacific/Rarotonga'] },
CLST => { comment => "Chile Summer Time", timezones => ['America/Santiago'] },
CLT => { comment => "Chile Standard Time", timezones => ['America/Santiago'] },
t/10.timezone.t view on Meta::CPAN
is( $ny->is_dst_for_datetime( $fake->( timegm(0,0,15,4,6,126) + $U ) ),
1, 'NY 2026-07-04: is_dst=1 (footer)' );
is( $ny->short_name_for_datetime( $fake->( timegm(0,0,12,15,0,126) + $U ) ),
'EST', 'NY 2026-01-15: abbr=EST (footer)' );
is( $ny->short_name_for_datetime( $fake->( timegm(0,0,15,4,6,126) + $U ) ),
'EDT', 'NY 2026-07-04: abbr=EDT (footer)' );
is( $paris->offset_for_datetime( $fake->( timegm(0,0,12,15,0,126) + $U ) ),
3600, 'Paris 2026-01-15: winter CET (footer)' );
is( $paris->offset_for_datetime( $fake->( timegm(0,0,15,4,6,126) + $U ) ),
7200, 'Paris 2026-07-04: summer CEST (footer)' );
is( $tok->offset_for_datetime( $fake->( timegm(0,0,12,15,0,126) + $U ) ),
32400, 'Tokyo 2026: JST +9h, no DST (footer)' );
is( $tok->is_dst_for_datetime( $fake->( timegm(0,0,12,15,0,126) + $U ) ),
0, 'Tokyo 2026: is_dst=0 (footer)' );
t/12.tz_database.t view on Meta::CPAN
{
my @tests = (
# zone unix_ts exp_off exp_dst exp_abbr label
[ 'Asia/Tokyo', timegm(0,0,15,1,3,1951), 32400, 0, 'JST', 'Tokyo 1951 (historical)' ],
[ 'Asia/Tokyo', timegm(0,0,15,1,3,1980), 32400, 0, 'JST', 'Tokyo 1980 (historical)' ],
[ 'America/New_York', timegm(0,0,15,14,6,2000), -14400, 1, 'EDT', 'NY 2000-07 summer (historical)' ],
[ 'America/New_York', timegm(0,0,15,14,11,2000), -18000, 0, 'EST', 'NY 2000-12 winter (historical)' ],
[ 'America/New_York', timegm(0,0,15,14,6,2006), -14400, 1, 'EDT', 'NY 2006-07 summer (historical)' ],
[ 'America/New_York', timegm(0,0,15,14,11,2006), -18000, 0, 'EST', 'NY 2006-12 winter (historical)' ],
[ 'Europe/Paris', timegm(0,0,15,14,5,1995), 7200, 1, 'CEST', 'Paris 1995 summer (historical)' ],
[ 'Europe/Paris', timegm(0,0,15,14,11,1995), 3600, 0, 'CET', 'Paris 1995 winter (historical)' ],
[ 'Etc/UTC', timegm(0,0,12,1,0,2026), 0, 0, 'UTC', 'UTC any date' ],
);
foreach my $t ( @tests )
{
my( $zone, $ts, $exp_off, $exp_dst, $exp_abbr, $label ) = @$t;
my $r = $dbh->selectrow_hashref(
q{SELECT s.offset, s.is_dst, s.short_name
FROM spans s
JOIN zones z ON z.zone_id = s.zone_id
t/18.resolve_abbreviation_ordering.t view on Meta::CPAN
is( $results->[0]->{is_active}, 1,
'Asia/Beirut is_active=1 for EEST' );
};
};
# NOTE: is_active regex correctness - the word-boundary must not trigger
# false positives. EST must NOT match because of the EEST substring,
# and CST must NOT match because of the CEST substring.
subtest 'is_active regex avoids substring false positives' => sub
{
# A zone that has CET and CEST but not CST in its footer
# (e.g. Europe/Paris: "CET-1CEST,M3.5.0,M10.5.0/3")
# should have is_active=1 for CEST but is_active=0 for any substring
# that isn't a proper token of the footer.
#
# We verify indirectly: for CEST we expect most Central European zones
# to be is_active=1; for a nonsense abbreviation like "ZZZ" we expect
# no results at all (method returns undef).
my $cest = DateTime::Lite::TimeZone->resolve_abbreviation( 'CEST' );
ok( defined( $cest ), 'CEST resolved' );
my @active = grep{ $_->{is_active} } @$cest;
ok( scalar( @active ) >= 10,
t/19.threads.t view on Meta::CPAN
foreach my $thr ( @threads )
{
$success &&= $thr->join();
}
ok( $success, 'All $NUM_THREADS threads constructed TimeZone objects without error' );
};
# NOTE: Thread safety: resolve_abbreviation concurrent calls
subtest 'resolve_abbreviation concurrent calls' => sub
{
my @abbrs = qw( JST CET EST PST GMT UTC );
my @threads = map
{
my $abbr = $abbrs[ $_ % scalar( @abbrs ) ];
threads->create(sub
{
my $candidates = DateTime::Lite::TimeZone->resolve_abbreviation(
$abbr,
extended => 1,
);
t/19.threads.t view on Meta::CPAN
foreach my $thr ( @threads )
{
$success &&= $thr->join();
}
ok( $success, 'All $NUM_THREADS threads called resolve_abbreviation without error' );
};
# NOTE: Thread safety: new() with extended => 1
subtest 'TimeZone->new with extended => 1 concurrent' => sub
{
my @abbrs = qw( JST CET WET EET HKT );
my @threads = map
{
my $abbr = $abbrs[ $_ % scalar( @abbrs ) ];
threads->create(sub
{
my $tz = DateTime::Lite::TimeZone->new( name => $abbr, extended => 1 );
diag( "DateTime::Lite::TimeZone returned '", ( $tz // 'undef' ), "' for '$abbr'." ) if( $DEBUG );
return(0) unless( defined( $tz ) );
# Result must be a proper IANA canonical name, not the abbreviation
t/19.threads.t view on Meta::CPAN
}
ok( $success, 'All $NUM_THREADS threads constructed DateTime::Lite objects without error' );
};
# NOTE: Thread safety: DateTime::Lite->new with time_zone as hashref
subtest 'DateTime::Lite->new with time_zone hashref concurrent' => sub
{
my @specs = (
{ name => 'JST', extended => 1 },
{ name => 'Asia/Tokyo' },
{ name => 'CET', extended => 1 },
{ name => 'UTC' },
{ name => 'EST', extended => 1 },
);
my @threads = map
{
my $spec = $specs[ $_ % scalar( @specs ) ];
threads->create(sub
{
my $dt = DateTime::Lite->new(
t/20.dst_abolition.t view on Meta::CPAN
my $tz = DateTime::Lite::TimeZone->new( name => 'Europe/Amsterdam' );
ok( defined( $tz ), 'TimeZone object created' );
my $offset_winter = $tz->offset_for_datetime(
DateTime::Lite->new( year => 2021, month => 2, day => 1, time_zone => $tz )
);
my $offset_summer = $tz->offset_for_datetime(
DateTime::Lite->new( year => 2021, month => 5, day => 1, time_zone => $tz )
);
isnt( $offset_winter, $offset_summer, '2021: winter and summer offsets differ (DST active)' );
is( $offset_winter, 3600, '2021-02: CET = +0100 = 3600s' );
is( $offset_summer, 7200, '2021-05: CEST = +0200 = 7200s' );
my $offset_future_winter = $tz->offset_for_datetime(
DateTime::Lite->new( year => 2026, month => 2, day => 1, time_zone => $tz )
);
my $offset_future_summer = $tz->offset_for_datetime(
DateTime::Lite->new( year => 2026, month => 5, day => 1, time_zone => $tz )
);
isnt( $offset_future_winter, $offset_future_summer, '2026: winter and summer offsets differ (DST still active)' );
is( $offset_future_winter, 3600, '2026-02: CET = +0100 = 3600s' );
is( $offset_future_summer, 7200, '2026-05: CEST = +0200 = 7200s' );
};
# NOTE: Tehran: DST abolished September 2022
subtest 'Asia/Tehran: DST abolished 2022' => sub
{
my $tz = DateTime::Lite::TimeZone->new( name => 'Asia/Tehran' );
ok( defined( $tz ), 'TimeZone object created' );
# Before DST season 2021 (DST starts ~21 March)