NAME

DateTime::Lite::TimeZone - Lightweight timezone support for DateTime::Lite

SYNOPSIS

use DateTime::Lite::TimeZone;

my $tz = DateTime::Lite::TimeZone->new( name => 'Asia/Tokyo' ) ||
    die( DateTime::Lite::TimeZone->error );

my $dt = DateTime::Lite->now( time_zone => $tz );

# Alias
my $tz2 = DateTime::Lite::TimeZone->new( name => 'US/Eastern' );

# Fixed offset
my $tz3 = DateTime::Lite::TimeZone->new( name => '+09:00' );

# Special zones
my $utc  = DateTime::Lite::TimeZone->new( name => 'UTC' );
my $flt  = DateTime::Lite::TimeZone->new( name => 'floating' );

# Single-argument shorthand
my $tz4 = DateTime::Lite::TimeZone->new( 'Europe/Paris' );

# Using latitude and longitude
my $tz = DateTime::Lite::TimeZone->new(
    latitude  => 35.658558,
    longitude => 139.745504,
) || die( "Could not find a timezone: ", DateTime::Lite::TimeZone->error );

# You can also use 'lat' and 'lon'
my $tz = DateTime::Lite::TimeZone->new(
    lat => 35.658558,
    lon => 139.745504,
) || die( "Could not find a timezone: ", DateTime::Lite::TimeZone->error );
say $tz->name;  # Asia/Tokyo

# Memory cache (three-layer: object + span + POSIX footer)
# Enable once at application start-up for best performance:
DateTime::Lite::TimeZone->enable_mem_cache;

# Or per-call:
my $tz5 = DateTime::Lite::TimeZone->new(
    name          => 'America/New_York',
    use_cache_mem => 1,
);

DateTime::Lite::TimeZone->disable_mem_cache;  # disables and clears
DateTime::Lite::TimeZone->clear_mem_cache;    # clears without disabling

# Offset and DST queries
use DateTime::Lite;
my $dt = DateTime::Lite->now( time_zone => $tz );

my $offset_secs = $tz->offset_for_datetime( $dt );        # e.g. -18000
my $local_off   = $tz->offset_for_local_datetime( $dt );  # from wall-clock time
my $is_dst      = $tz->is_dst_for_datetime( $dt );        # 1 or 0
my $abbr        = $tz->short_name_for_datetime( $dt );    # e.g. "EDT"

printf "%s", $tz->offset_as_string( $offset_secs );       # "-0500"
printf "%s", $tz->offset_as_string( $offset_secs, ':' );  # "-05:00"

# Parse an offset string to seconds:
my $secs = DateTime::Lite::TimeZone->offset_as_seconds( '-05:00' );  # -18000

# Zone metadata
$tz->name;             # canonical name, e.g. "America/New_York"
$tz->is_olson;         # 1 if an IANA named zone
$tz->is_utc;           # 1 if UTC
$tz->is_floating;      # 1 if floating
$tz->has_dst;          # 1 if zone ever observes DST
$tz->country_codes;    # arrayref of ISO 3166-1 alpha-2 codes, e.g. ['US']
$tz->countries;        # arrayref of hashrefs with full country data
$tz->coordinates;      # e.g. "+404251-0740023"
$tz->comment;          # free-text annotation from IANA data
$tz->latitude;
$tz->longitude;
$tz->tz_version;       # IANA release string, e.g. "2026a"
$tz->tzif_version;     # TZif binary format version (1, 2, 3, or 4)
$tz->footer_tz_string; # POSIX TZ string for recurring DST rules
$tz->transition_count;
$tz->type_count;
$tz->leap_count;

# Zone discovery
my $all      = DateTime::Lite::TimeZone->all_names;  # array reference
my @all      = DateTime::Lite::TimeZone->all_names;
my $cats     = DateTime::Lite::TimeZone->categories; # array reference
my @cats     = DateTime::Lite::TimeZone->categories;
my $in_cat   = DateTime::Lite::TimeZone->names_in_category('America');  # array reference
my @in_cat   = DateTime::Lite::TimeZone->names_in_category('America');
my $in_cc    = DateTime::Lite::TimeZone->names_in_country('JP');        # array reference
my @in_cc    = DateTime::Lite::TimeZone->names_in_country('JP');
my $is_valid = DateTime::Lite::TimeZone->is_valid_name('Asia/Tokyo');  # 1

my $aliases  = DateTime::Lite::TimeZone->aliases;   # hashref alias => canonical
my %aliases  = DateTime::Lite::TimeZone->aliases;   # hash    alias => canonical
my $links    = $tz->links;                          # arrayref of alias names

# Resolve a timezone abbreviation against the IANA types table
my $results = DateTime::Lite::TimeZone->resolve_abbreviation( 'JST' );
# $results = [
#     {
#         ambiguous        => 0,
#         extended         => 0,
#         first_trans_time => -2587712400,
#         is_active        => 1,
#         is_dst           => 0,
#         last_trans_time  => -577962000,
#         utc_offset       => 32400,
#         zone_name        => "Asia/Tokyo",
#     },
#     {
#         ambiguous        => 0,
#         extended         => 0,
#         first_trans_time => -1830414600,
#         is_active        => 0,
#         is_dst           => 0,
#         last_trans_time  => -1830414600,
#         utc_offset       => 32400,
#         zone_name        => "Asia/Pyongyang",
#     },
#     # etc...
# ]

# Narrow by co-parsed numeric offset
my $pst = DateTime::Lite::TimeZone->resolve_abbreviation( 'PST',
    utc_offset => -28800
);

# Period filter: zones that still used JST after 1950
my $modern = DateTime::Lite::TimeZone->resolve_abbreviation( 'JST',
    period => '>1950-01-01'
);

# Period filter with two ISO date bounds
my $wartime = DateTime::Lite::TimeZone->resolve_abbreviation( 'JST',
    period => ['>1941-01-01', '<1946-01-01']
);

# Period filter with a raw epoch integer (post-1970 value, safe on all platforms)
my $epoch_2010 = 1262304000;  # 2010-01-01 00:00:00 UTC
my $recent = DateTime::Lite::TimeZone->resolve_abbreviation( 'EST',
    period => ">$epoch_2010"
);

# Period filter: only zones currently on this abbreviation
my $current = DateTime::Lite::TimeZone->resolve_abbreviation( 'JST',
    period => 'current'
);

# Extended mode: fall back to extended_aliases if not in IANA types
# (covers real-world abbreviations such as AFT, AMST, CEST, HAEC, ...)
my $aft = DateTime::Lite::TimeZone->resolve_abbreviation( 'AFT',
    extended => 1
);
# $aft = [
#     {
#         ambiguous  => 0,
#         extended   => 1,
#         is_dst     => undef,
#         is_primary => 1,
#         utc_offset => undef,
#         zone_name  => "Asia/Kabul",
#     },
# ]

# Database access (low-level)
my $path = DateTime::Lite::TimeZone->datafile;  # path to tz.sqlite3
# Raw SQLite queries via public view methods (return DBI statement handles):
#   $tz->zones, $tz->spans, $tz->transition, $tz->types,
#   $tz->aliases, $tz->countries, $tz->leap_second, $tz->metadata

# Error handling
my $bad = DateTime::Lite::TimeZone->new( name => 'Mars/Olympus' );
if( !defined( $bad ) )
{
    warn DateTime::Lite::TimeZone->error;  # "Unknown time zone 'Mars/Olympus'"
}
$tz->fatal(1);  # make errors die instead of warn+return undef

# Object context is detected even in errors, but allows the chain to unfold until the end to avoid the typical: "Can't call method "%s" on an undefined value"
# See https://perldoc.perl.org/perldiag#Can't-call-method-%22%25s%22-on-an-undefined-value
my $bad = DateTime::Lite::TimeZone->new( name => 'Mars/Olympus' )->name;

VERSION

v0.5.6

DESCRIPTION

DateTime::Lite::TimeZone is a drop-in replacement for DateTime::TimeZone designed to eliminate its heavy dependency and memory footprint.

DateTime::TimeZone loads 85 modules at startup, including the entire Specio, Params::ValidationCompiler, and Exception::Class stacks, simply to validate constructor arguments. DateTime::Lite::TimeZone replaces all of that with a single DBD::SQLite query against a compact bundled database (tz.sqlite3).

You may also be interested in the Unicode CLDR (Common Locale Data Repository) with the module Locale::Unicode::Data, which provides richer timezone information, such as metazones, regions, and historical timezone data.

For example:

my $cldr = Locale::Unicode::Data->new;
my $ref  = $cldr->timezone( timezone => 'Asia/Tokyo' );

This would return an hash reference with the following information:

{
   timezone_id => 281,
   timezone    => 'Asia/Tokyo',
   territory   => 'JP',
   region      => 'Asia',
   tzid        => 'japa',
   metazone    => 'Japan',
   tz_bcpid    => 'jptyo',
   is_golden   => 1,
   is_primary  => 0,
   is_preferred => 0,
   is_canonical => 0,
}

You can also returns all the timezones for a country code:

my $array_ref = $cldr->timezones( territory => 'US' );

Would return 55 results, such as:

{
    alias => [qw( America/Atka US/Aleutian )],
    is_canonical => 1,
    is_golden => 1,
    is_preferred => 0,
    is_primary => 0,
    metazone => "Hawaii_Aleutian",
    region => "America",
    territory => "US",
    timezone => "America/Adak",
    timezone_id => 55,
    tz_bcpid => "usadk",
    tzid => "haal",
}

You can also get the localised city name for a time zone:

my $ref = $cldr->timezone_city(
    locale   => 'de',
    timezone => 'Asia/Tokyo',
);

which would return:

{
   tz_city_id  => 7486,
   locale      => 'de',
   timezone    => 'Asia/Tokyo',
   city        => 'Tokio',
   alt         => undef,
}

And if you want to access historical information:

my $ref = $cldr->timezone_info(
    timezone    => 'Europe/Simferopol',
    start       => '1994-04-30T21:00:00',
);

which would return:

{
   tzinfo_id   => 594,
   timezone    => 'Europe/Simferopol',
   metazone    => 'Moscow',
   start       => '1994-04-30T21:00:00',
   until       => '1997-03-30T01:00:00',
}

or, maybe:

my $ref = $cldr->timezone_info(
    timezone    => 'Europe/Simferopol',
    start       => ['>1992-01-01', '<1995-01-01'],
);

This is handy if you do not know the exact date, and want to provide a range instead.

Database schema

The bundled tz.sqlite3 uses the following main tables:

aliases

Alias-to-zone_id FK mappings (such as US/Eastern to America/New_York)

metadata

Key/value pairs including the tzdata version

spans

Pre-computed time spans derived from transitions and types, indexed for fast range lookup

types

Local time type records from the TZif files

zones

Canonical IANA zone names with country codes and coordinates

Fallback mode

If DBD::SQLite is not available, or the bundled tz.sqlite3 cannot be found, DateTime::Lite::TimeZone falls back transparently to DateTime::TimeZone and emits a one-time warning, if warning is permitted.

If DateTime::TimeZone is not available, then it dies.

CONSTRUCTOR

new

my $zone = DateTime::Lite::TimeZone->new( 'Asia/Tokyo' );
my $zone = DateTime::Lite::TimeZone->new(
    name  => 'Asia/Tokyo',
    fatal => 1, # Makes all error fatal
);

# Using latitude and longitude
my $tz = DateTime::Lite::TimeZone->new(
    latitude  => 35.658558,
    longitude => 139.745504,
) || die( "Could not find a timezone: ", DateTime::Lite::TimeZone->error );

# You can also use 'lat' and 'lon'
my $tz = DateTime::Lite::TimeZone->new(
    lat => 35.658558,
    lon => 139.745504,
) || die( "Could not find a timezone: ", DateTime::Lite::TimeZone->error );
say $tz->name;  # Asia/Tokyo

A new DateTime::Lite::TimeZone object can be instantiated by either passing the timezone as a single argument, or as an hash, such as name => 'Asia/Tokyo'

Recognised forms:

Named IANA timezones such as America/New_York, Europe/Paris.
Aliases such as US/Eastern, Japan.
Fixed-offset strings such as +09:00, -0500.
The special names UTC, floating, and local.

The local name instructs DateTime::Lite::TimeZone to determine the system's local timezone automatically, without requiring any external modules. The detection strategy is OS-specific, relying on $^O:

Linux, macOS (darwin), FreeBSD, OpenBSD, NetBSD, Solaris, AIX, HP-UX, OS/2, Cygwin

Tries, in order:

  • $ENV{TZ}

  • the /etc/localtime symlink target or a binary match against /usr/share/zoneinfo

  • /etc/timezone (Debian/Ubuntu)

  • /etc/TIMEZONE with a TZ= line (Solaris, HP-UX)

  • /etc/sysconfig/clock with a ZONE= or TIMEZONE= line (RedHat/CentOS)

  • /etc/default/init with a TZ= line (older Unix)

Windows (MSWin32, NetWare)

Tries $ENV{TZ} first, then reads the timezone name from the Windows Registry (SYSTEM/CurrentControlSet/Control/TimeZoneInformation) and maps it to an IANA name using the CLDR windowsZones.xml table. Requires Win32::TieRegistry (available on CPAN; not a hard dependency).

Android

Tries $ENV{TZ}, then getprop persist.sys.timezone, then falls back to UTC.

VMS

Checks the environment variables TZ, SYS$TIMEZONE_RULE, SYS$TIMEZONE_NAME, UCX$TZ, and TCPIP$TZ.

Symbian, EPOC, MS-DOS, Mac OS 9 and earlier

Checks $ENV{TZ} only.

If the local timezone cannot be determined, an error is set and undef is returned in scalar context, or an empty list in list context. In chaining (object context), it returns a dummy object (DateTime::Lite::Null) to avoid the typical Can't call method '%s' on an undefined value.

Coordinates via latitude and longitude arguments.

As an alternative to a name, you can pass decimal-degree coordinates to have DateTime::Lite::TimeZone resolve the nearest IANA timezone automatically:

my $tz = DateTime::Lite::TimeZone->new(
    latitude  => 35.658558,
    longitude => 139.745504,
);
say $tz->name;  # Asia/Tokyo

The resolution uses the reference coordinates stored in the IANA zone1970.tab file (one representative point per canonical zone) and finds the nearest zone by the haversine great-circle distance. This is an approximation: it is accurate for most locations, but may give incorrect results near timezone boundaries, in disputed territories, or for enclaves such as Kaliningrad. If you need boundary-precise resolution, consider Geo::Location::TimeZoneFinder instead.

latitude must be in the range -90 to 90; longitude in -180 to 180. An error object is set and undef is returned in scalar context, or an empty list in list context, if the values are out of range or if no zone with coordinates is found in the database.

The haversine formula is computed in SQLite when the database was compiled with -DSQLITE_ENABLE_MATH_FUNCTIONS (SQLite version >= 3.35.0, released on March 2021).

On older systems or builds where the math functions are absent, the required functions (sqrt, sin, cos, asin) are registered automatically as Perl UDFs (User Defined Functions) via "sqlite_create_function" in DBD::SQLite on first use, so coordinate resolution works transparently on all supported SQLite versions.

Detection is version-aware. Thus:

  • on SQLite with version >= 3.35.0, the special système table pragma_function_list is queried for sqrt before any UDF is registered, to ensure a native function is used in priority.

  • on SQLite with version < 3.35.0, where the math functions did not yet exist, UDFs are registered directly without querying pragma_function_list.

  • on SQLite version < 3.16.0, pragma_function_list is not available as a table-valued function, so UDFs are registered directly.

UDFs are available on all SQLite version >= 3.0.0.

On older systems that ships SQLite 3.31.1, the required functions (sqrt, sin, cos, asin) are registered automatically as Perl UDFs (User Defined Functions) via "sqlite_create_function" in DBD::SQLite on first use, so coordinate resolution works transparently on all supported SQLite versions.

A boolean option 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 "MEMORY CACHE" for details and for the class-level "enable_mem_cache" alternative.

# 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 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 JST, CET, or EST from an external source and wishes to resolve it to a canonical IANA zone without calling "resolve_abbreviation" explicitly.

When extended is set and the name is unknown as an IANA timezone, new calls resolve_abbreviation with the extended option set to true internally and, if a single unambiguous candidate is found, recurses with the resolved canonical name. If the abbreviation is ambiguous or not found even in the extended aliases table, the standard Unknown time zone error is returned.

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 exception object with error() and returns undef in scalar context, or an empty list in list context. In method-chaining (object) context, returns a DateTime::Lite::NullObject to avoid the error Can't call method '%s' on an undefined value. At the end of the chain, undef or an empty list will still be returned though.

MEMORY CACHE

By default, each call to "new" constructs a fresh object with a SQLite query. For applications that construct DateTime::Lite::TimeZone objects repeatedly with the same zone name, a three-layer cache is available.

Layer 1 - Object cache: When enabled, the second and subsequent calls for the same zone name return the original object directly from a hash, bypassing the database entirely.

Layer 2 - Span cache: Each cached TimeZone object stores the last matched UTC and local time span. Calls to offset_for_datetime and offset_for_local_datetime skip the SQLite query when the timestamp falls within the cached span's [utc_start, utc_end) or [local_start, local_end) range.

Layer 3 - POSIX footer cache: For zones where current dates are governed by a recurring DST rule (POSIX TZ footer string), the result of the footer calculation is cached by calendar day. DST transitions happen twice a year; on all other days the cached result is returned without re-evaluating the rule.

Together these three layers reduce the per-call cost of DateTime::Lite->new( time_zone => 'America/New_York' ) from ~430 µs to ~25 µs, putting it on par with DateTime.

Cache entries are keyed by the name passed to "new", plus the canonical name (after alias resolution). Both US/Eastern and America/New_York therefore map to the same cached object.

Cached objects are immutable in normal use. All public accessors are read-only, so sharing an object across callers is safe.

enable_mem_cache

Class method. Activates the memory cache for all subsequent "new" calls.

DateTime::Lite::TimeZone->enable_mem_cache;

# Every new() call now hits the cache after the first construction:
my $tz = DateTime::Lite::TimeZone->new( name => 'America/New_York' );
my $tz2 = DateTime::Lite::TimeZone->new( name => 'America/New_York' );
# $tz and $tz2 are the same object

Equivalent to passing use_cache_mem => 1 on every "new" call, but more convenient when you want the cache active for the lifetime of the process. Returns the class name to allow chaining.

disable_mem_cache

Class method. Disables the memory cache and clears all cached entries. Subsequent "new" calls will construct fresh objects.

DateTime::Lite::TimeZone->disable_mem_cache;

Returns the class name.

clear_mem_cache

Class method. Empties the cache without disabling it. The next "new" call for any zone name will re-query the database and re-populate the cache.

Useful if the tz.sqlite3 database has been replaced at runtime (an unusual operation):

DateTime::Lite::TimeZone->clear_mem_cache;

Returns the class name.

METHODS

aliases

# Checking for errors too
my $aliases  = DateTime::Lite::TimeZone->aliases ||
    die( DateTime::Lite::TimeZone->error );
my( %aliases ) = DateTime::Lite::TimeZone->aliases ||
    die( DateTime::Lite::TimeZone->error );
my $aliases    = $zone->aliases ||
    die( $zone->error );
my( %aliases ) = $zone->aliases ||
    die( $zone->error );

This can be called as an instance method, or as a class function.

This returns a hash of all the zones aliases (the old, deprecated names) to their corresponding canonical names.

For example:

Japan -> Asia/Tokyo

In scalar context, it returns an hash reference, and in list context, it returns an hash.

If an error occurred, this sets an exception object, and returns undef in scalar context, and an empty list in list context. The exception object can then be retrieved with "error"

all_names

# Checking for errors too
my $names    = DateTime::Lite::TimeZone->all_names ||
    die( DateTime::Lite::TimeZone->error );
my( @names ) = DateTime::Lite::TimeZone->all_names ||
    die( DateTime::Lite::TimeZone->error );
my $names    = $zone->all_names ||
    die( $zone->error );
my( @names ) = $zone->all_names ||
    die( $zone->error );

This can be called as an instance method, or as a class function.

This returns a list of all the time zone names sorted alphabetically. This list does not include zone alias (a.k.a. "links").

In scalar context, it returns an array reference, and in list context, it returns an array.

If an error occurred, this sets an exception object, and returns undef in scalar context, and an empty list in list context. The exception object can then be retrieved with "error"

categories

# Checking for errors too
my $categories    = DateTime::Lite::TimeZone->categories ||
    die( DateTime::Lite::TimeZone->error );
my( @categories ) = DateTime::Lite::TimeZone->categories ||
    die( DateTime::Lite::TimeZone->error );
my $categories    = $zone->categories ||
    die( $zone->error );
my( @categories ) = $zone->categories ||
    die( $zone->error );

This can be called as an instance method, or as a class function.

This returns a list of all time zone categories. A category is the part, if any, that precedes the forward slash of a zone name. For example, in Asia/Tokyo, the category would be Asia. However, with the special zone Factory, there would not be any category.

In scalar context, it returns an array reference, and in list context, it returns an array.

If an error occurred, this sets an exception object, and returns undef in scalar context, and an empty list in list context. The exception object can then be retrieved with "error"

category

my $zone = DateTime::Lite::TimeZone->new( name => "Asia/Tokyo" );
say $zone->category; # Asia
my $zone = DateTime::Lite::TimeZone->new( name => "UTC" );
say $zone->category; # undef

Returns the part of the time zone name before the first slash, such as Asia in Asia/Tokyo

comment

Returns the optional zone comment from zone1970.tab, such as "Mountain Time - south Idaho and east Oregon".

Returns undef in scalar context, or an empty list in list context if no comment is recorded.

coordinates

Returns the compact coordinate string from zone1970.tab, such as +3518+13942 for Tokyo. Returns undef in scalar context, or an empty list in list context when there are no coordinates, such as UTC, floating, and fixed-offset zones.

country_codes

Returns an arrayref of ISO 3166-1 alpha-2 country codes associated with this timezone, such as ["JP"] or ["US","CA"].

Returns undef in scalar context, or an empty list in list context when the timezone has no countries associated, such as UTC, floating, and fixed-offset zones.

countries

# Checking for errors too
my $countries    = DateTime::Lite::TimeZone->countries ||
    die( DateTime::Lite::TimeZone->error );
my( @countries ) = DateTime::Lite::TimeZone->countries ||
    die( DateTime::Lite::TimeZone->error );
my $countries    = $zone->countries ||
    die( $zone->error );
my( @countries ) = $zone->countries ||
    die( $zone->error );

This can be called as an instance method, or as a class function.

This returns a list of all the ISO 3166 2-letters country codes sorted alphabetically, and in lower-case. Those codes can be used to call "names_in_country".

In scalar context, it returns an array reference, and in list context, it returns an array.

If an error occurred, this sets an exception object, and returns undef in scalar context, and an empty list in list context. The exception object can then be retrieved with "error"

If you want to convert a country to its locale name, you can use the Unicode CLDR database designed specifically for this.

For example, using the locale en:

use Locale::Unicode::Data;
my $cldr = Locale::Unicode::Data->new;
my $ref  = $cldr->territory_l10n( locale => 'en', territory => 'JP', alt => undef );

# Returns an hash reference like this:

{
   terr_l10n_id    => 13385,
   locale          => 'en',
   territory       => 'JP',
   locale_name     => 'Japan',
   alt             => undef,
}

And, if you want to look up the ISO3166 code based on the locale country name, you could do something like this. Here we search for the country code matching アメリカ, which is America in Japanese:

use strict;
use warnings;
use utf8;
use open ':std' => ':utf8';
use Data::Pretty qw( dump );
use Locale::Unicode::Data;
my $cldr = Locale::Unicode::Data->new;
my $all = $cldr->territories_l10n( locale => 'ja' );
foreach my $ref ( @$all )
{
    if( $ref->{locale_name} =~ /アメリカ/ &&
        $ref->{territory} =~ /^[A-Z]{2}$/ ) # Because a territory, in Unicode CLDR, can also be a 3-digits code
    {
        say dump( $ref );
    }
}

which would produce something like this:

{
    alt => undef,
    locale => "ja",
    locale_name => "アメリカ合衆国",
    terr_l10n_id => 26334,
    territory => "US",
}

datafile

Returns the absolute path to the bundled tz.sqlite3 database file.

designation_charcount

Returns the total size of abbreviation string table (in bytes).

This is equivalent to TZif header field charcnt, including trailing NUL bytes.

See rfc9636, section 3.1

error

my $ex = $zone->error;

Returns the last exception object, if any.

fatal

Sets or gets the fatal property for this object.

When enabled, any error will trigger a fatal exception and call "die" in perlfunc

Returns the footer portion of the timezone.

See rfc9636, section 3.3

has_dst

This is an alias for "has_dst_changes"

has_dst_changes

Returns true if the timezone observes daylight saving time transitions.

is_canonical

my $zone = DateTime::Lite::TimeZone->new( 'Japan' );
say $zone->is_canonical; # false
my $zone = DateTime::Lite::TimeZone->new( 'Asia/Tokyo' );
say $zone->is_canonical; # true

Returns true if the timezone name provided is a canonical one, false otherwise.

is_dst_for_datetime( $dt )

Returns true if $dt falls within a DST period for this timezone.

is_floating

Returns true for the special floating timezone.

is_olson

Returns true for IANA/Olson-sourced timezones.

is_utc

Returns true for the UTC timezone and for fixed-offset +0000.

is_valid_name

say DateTime::Lite::TimeZone->is_valid_name( 'Singapore' );  # true
say $zone->is_valid_name( 'Singapore' );                     # true
say DateTime::Lite::TimeZone->is_valid_name( 'Paris' );      # false
say $zone->is_valid_name( 'Paris' );                         # false
say DateTime::Lite::TimeZone->is_valid_name( 'Asia/Seoul' ); # true
say $zone->is_valid_name( 'Asia/Seoul' );                    # true

This takes a canonical timezone or a timezone alias, and returns true if the value provided is valid, or false otherwise.

This sets an exception object, an returns an error only if no value was provided, so you may want to check if the value returned is defined.

Contrary to DateTime::TimeZone, passin a DateTime::TimeZone::Alias does not make that zone valid. This class, adhere strictly to the IANA time zones.

isstd_count

Returns the number of standard time (a.k.a "standard/wall") indicators.

This "must either be zero or equal to "typecnt".

See rfc9636, section 3.1

isut_count

Returns the number of UT/local time indicators.

This "must either be zero or equal to "typecnt".

See rfc9636, section 3.1

latitude

Returns the latitude for this zone, as a real number, if any.

This is an alias for "aliases"

longitude

Returns the longitude for this zone, as a real number, if any.

name

my $zone = DateTime::Lite::TimeZone->new( name => 'Japan' );
say $zone->name; # Asia/Tokyo

Returns the canonical timezone name, such as Asia/Tokyo.

This means that if you provide an alias upon instantiation, it will be resolved, and accessible with this method.

names_in_category

# Checking for errors too
my $names    = DateTime::Lite::TimeZone->names_in_category( 'Asia' ) ||
    die( DateTime::Lite::TimeZone->error );
my( @names ) = DateTime::Lite::TimeZone->names_in_category( 'Asia' ) ||
    die( DateTime::Lite::TimeZone->error );
my $names    = $zone->names_in_category( 'America' ) ||
    die( $zone->error );
my( @names ) = $zone->names_in_category( 'America' ) ||
    die( $zone->error );

This takes a category, which under this class means the left-hand side of the zone name, separated by a forward slash. So, with the example of Asia/Seoul, the category would be Asia.

With this category provided, this returns a list of the name on the left-hand side of the first forward slash.

For example:

For Asia/Taipei, the category would be Asia, and the list would return among the 74 results, the name Taipei.

For the category America, there would be 121 results, and of which Indiana/Vincennes whose full timezone is America/Indiana/Vincennes, would also be returned.

In scalar context, it returns an array reference, and in list context, it returns an array.

If an error occurred, this sets an exception object, and returns undef in scalar context, and an empty list in list context. The exception object can then be retrieved with "error"

names_in_country

# Checking for errors too
my $names    = DateTime::Lite::TimeZone->names_in_country( 'US' ) ||
    die( DateTime::Lite::TimeZone->error );
my( @names ) = DateTime::Lite::TimeZone->names_in_country( 'US' ) ||
    die( DateTime::Lite::TimeZone->error );
my $names    = $zone->names_in_country( 'US' ) ||
    die( $zone->error );
my( @names ) = $zone->names_in_country( 'US' ) ||
    die( $zone->error );

This takes a 2-letter ISO3166 country code, and returns a list of all the time zones associated with it.

This is case insensitive, so a country code provided, such as US or us would be treated equally.

In scalar context, it returns an array reference, and in list context, it returns an array.

If an error occurred, this sets an exception object, and returns undef in scalar context, and an empty list in list context. The exception object can then be retrieved with "error"

The order of the time zones returned is the same ones as set by IANA database.

offset_as_seconds

This takes an offset as a string, such as +09:00, and this returns the number of seconds represented by that offset either as a signed integer.

If no value was provided, or if that value is not comprised in the range -99:59:59 to +99:59:59, or, if the offset string provided does not match any of the following 2 patterns, then this sets an error object, and returns undef in scalar context or an empty list in list context.

The supported offset patterns are (sign defaults to + if absent):

Colon form: [+-]H:MM, [+-]HH:MM, [+-]HH:MM:SS

The regular expression is: \A([+-])?(\d{1,2}):(\d{2})(?::(\d{2}))?\z

Examples: +09:00, -02:00, 9:0:0

Compact form: [+-]HHMM, [+-]HHMMSS

The regular expression is: /\A([+-])?(\d{2})(\d{2})(\d{2})?\z/

Examples: +0900, -0200, 0900, +090000, 090000

The special string "0" (returns 0).

offset_as_string

say DateTime::Lite::TimeZone->offset_as_string(32400);       # +0900
say DateTime::Lite::TimeZone->offset_as_string(32400, ':' ); # +09:00
say $zone->offset_as_string(32400);                          # +0900
say $zone->offset_as_string(32400, ':');                     # +09:00

Class or instance method. This converts a numeric UTC offset in seconds to a formatted string such as +0900 (default) or +09:00 (with : as separator).

Drop-in compatible with "offset_as_string" in DateTime::TimeZone.

offset_for_datetime

my $offset = $zone->offset_for_datetime( $dt );

This takes a DateTime::Lite object, and returns the UTC offset in seconds applicable to that object.

Upon error, then this sets an error object, and returns undef in scalar context or an empty list in list context.

offset_for_local_datetime

my $offset = $zone->offset_for_local_datetime( $dt );

This takes a DateTime::Lite object, and returns the UTC offset in seconds given a local (wall-clock) time.

Used internally during timezone conversion.

Upon error, then this sets an error object, and returns undef in scalar context or an empty list in list context.

resolve_abbreviation

# Unambiguous: JST maps to a single UTC offset
# Results sorted by is_active DESC, first_trans_time ASC, last_trans_time DESC, name ASC.
my $results = DateTime::Lite::TimeZone->resolve_abbreviation( 'JST' );
# $results = [
#     {
#         ambiguous        => 0,
#         extended         => 0,
#         first_trans_time => -2587712400,
#         is_active        => 1,
#         is_dst           => 0,
#         last_trans_time  => -577962000,
#         utc_offset       => 32400,
#         zone_name        => "Asia/Tokyo",
#     },
#     {
#         ambiguous        => 0,
#         extended         => 0,
#         first_trans_time => -1830414600,
#         is_active        => 0,
#         is_dst           => 0,
#         last_trans_time  => -1830414600,
#         utc_offset       => 32400,
#         zone_name        => "Asia/Pyongyang",
#     },
#     # etc...
# ]

# Truly ambiguous: CST has different offsets in Asia and America
my $cst = DateTime::Lite::TimeZone->resolve_abbreviation( 'CST' );
# $cst->[0]{ambiguous} == 1

# Narrow by offset when already known (such as from a co-parsed %z token)
my $filtered = DateTime::Lite::TimeZone->resolve_abbreviation(
    'PST', utc_offset => -28800
);

# Period filter: only zones that used JST after 1950
my $modern = DateTime::Lite::TimeZone->resolve_abbreviation(
    'JST', period => '>1950-01-01'
);

# Period filter with two bounds: zones that used JST during WWII
my $wartime = DateTime::Lite::TimeZone->resolve_abbreviation(
    'JST', period => ['>1941-01-01', '<1946-01-01']
);

# Period filter: only zones currently on this abbreviation
my $current = DateTime::Lite::TimeZone->resolve_abbreviation(
    'JST', period => 'current'
);

# Extended mode: fall back to extended_aliases if not in IANA types
my $aft = DateTime::Lite::TimeZone->resolve_abbreviation(
    'AFT', extended => 1
);
# $aft = [
#   { zone_name => 'Asia/Kabul', utc_offset => undef, is_dst => undef,
#     ambiguous => 0, is_primary => 1, extended => 1 },
# ]

# extended => 1 is a no-op for abbreviations already in IANA types (such as IST, CST):
# the IANA result is returned and the extended_aliases table is not consulted.

Class or instance method. Resolves a timezone abbreviation such as JST or EST against the IANA data in the bundled tz.sqlite3 database, returning all canonical zones that have ever used that abbreviation.

IANA results are sorted by a four-level key:

2. first_trans_time ascending: among zones sharing the same is_active value, the one that adopted the abbreviation first appears first.
3. last_trans_time descending: among zones sharing the first two keys, the one that used it most recently appears first.
4. zone_name ascending: final deterministic tie-breaker.

In practice this means that for an abbreviation like CEST, the currently-active Central European zones come first (ordered by their date of adoption), followed by zones that have since migrated away from CEST (such as Europe/Kaliningrad, which abandoned CEST in 2014).

Extended alias results (when extended => 1 falls back to the extended_aliases table) use a different ordering: is_primary descending, then zone_name ascending. See "is_primary" below.

The single required argument is the abbreviation string. The following optional keyword arguments are accepted:

extended

Boolean. When true and the abbreviation is not found in the IANA types table, the method falls back to querying the extended_aliases table. This covers real-world abbreviations (such as AFT, AMST, or HAEC) that appear in date strings but are not stored as TZif type abbreviations in the IANA database.

When an extended result is returned, utc_offset and is_dst are undef since the extended alias table maps abbreviations to zone names only. If you need the offset, instantiate a DateTime::Lite::TimeZone object from the returned zone_name.

period

Restricts results to zones whose most recent matching transition (MAX(trans_time)) falls within a given time window. Accepts either a single string or an array reference of strings for multiple conditions.

Each value may be prefixed with a comparison operator:

> (default when no operator is given)

Greater than. The most common operator: zones whose last use of the abbreviation is more recent than the given date.

>=

Greater than or equal.

<

Less than. Returns zones whose last use is older than the given date.

<=

Less than or equal.

The operators = and != are accepted but map to SQL IS and IS NOT. They have no practical use for timestamp comparisons and are not recommended.

Value types: ISO date strings such as 1950-01-01 are converted to Unix epoch via SQLite strftime('%s', ...). Plain integers are treated as epoch seconds and passed as CAST(? AS INTEGER) to ensure correct numeric comparison regardless of how the Perl scalar is internally represented. For portability, use post-1970 epoch values when passing raw integers; pre-1970 negatives may behave unexpectedly on some platforms.

The special value current returns only zones whose most recent use of the abbreviation is in the past and whose next scheduled transition has not yet occurred, which means zones that are on this abbreviation right now.

period => '>1950-01-01'                   # last used after 1950 (ISO date)
period => ['>1941-01-01', '<1946-01-01']  # last used within WWII window
period => '>1262304000'                   # last used after 2010-01-01 (epoch int)
period => 'current'                       # currently active only

Period filtering does not apply to extended alias results.

utc_offset

Integer seconds east of UTC. Narrows the results to candidates with a matching offset, which is useful when the numeric offset has already been parsed from the same string (such as from a co-parsed %z token). Only applies to the IANA types lookup; not used for the extended aliases fallback.

Returns an array reference of hashrefs on success, each with the following keys:

zone_name

The canonical IANA zone name, such as Asia/Tokyo.

utc_offset

The UTC offset in seconds east of UTC for this abbreviation in this zone. undef for extended alias results.

is_dst

1 if this abbreviation represents a DST period, 0 otherwise. undef for extended alias results.

ambiguous

For IANA results: 1 if the abbreviation maps to multiple distinct UTC offsets (a genuine ambiguity such as IST or CST); 0 if all candidates share the same UTC offset.

For extended alias results: 1 if there are multiple candidates and none or more than one is marked is_primary; 0 if exactly one candidate has is_primary = 1.

extended

1 if this result came from the extended_aliases table; 0 if it came from the IANA types table.

is_active

Only present in IANA results (extended => 0). 1 if the zone's POSIX 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 its permanent abbreviation); 0 if the footer no longer mentions the abbreviation, which typically means the zone has transitioned away from it.

For example, Europe/Berlin's footer is CET-1CEST,M3.5.0,M10.5.0/3, so both CET and CEST yield is_active => 1. In contrast, Europe/Kaliningrad's footer is EET-2, so resolve_abbreviation('CEST') still lists Kaliningrad (it used CEST until 2014), but with is_active => 0.

This field is used as the primary sort key, so zones with is_active => 1 appear before zones with is_active => 0. Callers that only want currently-active zones can filter with grep { $_->{is_active} } @$results.

The detection is a word-boundary regex against the footer string: alphabetic abbreviations must be adjacent to non-alphabetic characters (so CST does not match CEST), and numeric/sign-prefixed abbreviations (such as -03, +0430) must appear inside <...> per POSIX TZ syntax. The regex was validated against all 1449 (zone, abbreviation) pairs in the bundled database with zero false negatives against an empirical ground truth; see the internal RESOLVE_ABBREVIATION_DESIGN_NOTES.md if curious about the details.

Absent from extended alias results, where the ordering signal is the editorial is_primary marker instead.

is_primary

Only present in extended alias results (extended => 1). When multiple candidates match, 1 marks the editorially-chosen canonical zone for this abbreviation (such as America/Sao_Paulo for BRT), and 0 marks the others. Absent from IANA results, because no single zone is canonically designated among IANA candidates of an abbreviation; the is_active marker plus the sort order (first-used first, still-active first) serve the analogous role for IANA results.

If you need a canonical or preferred zone designation in the CLDR sense, use Locale::Unicode::Data which exposes CLDR's is_golden, is_primary, and is_preferred flags on a per-timezone basis. See "USING Locale::Unicode::Data FOR CANONICAL DESIGNATION" below for an example.

first_trans_time

Unix epoch of the earliest transition in this zone using the abbreviation. Absent from extended alias results. Used as a sort key (ascending) after is_active, so among zones with the same is_active value, the one that adopted the abbreviation first appears first.

last_trans_time

Unix epoch of the most recent transition in this zone using the abbreviation. Absent from extended alias results. Used as a secondary sort key (descending): among zones with the same is_active and same first_trans_time, the one that has used the abbreviation most recently appears first.

If an error occurred, this sets an exception object, and returns undef in scalar context, and an empty list in list context. The exception object can then be retrieved with "error".

Note that many abbreviations such as EST or PST match multiple zone names that all share the same UTC offset. These are not genuinely ambiguous for the purpose of parsing a datetime string; the ambiguous flag will be 0 in those cases. Genuinely ambiguous abbreviations such as IST (Irish Summer Time, Indian Standard Time, or Israel Standard Time) will have ambiguous => 1.

short_name_for_datetime

say $zone->short_name_for_datetime( $dt );

This takes a DateTime::Lite object, and returns the abbreviated timezone name applicable, such as JST or EDT.

transition_count

Returns the number of transitions record for this timezone.

Equivalent to TZif header field timecnt

See rfc9636, section 3.1

type_count

Returns the number of types for this timezone.

See rfc9636, section 3.1

tz_version

Returns the IANA tzdata version string from the database metadata table, such as 2026a.

tzif_version

Returns the timezone version string from the timezone data.

The possible values are 1, 2, 3 or 4

See rfc9636, section 3.1

USING Locale::Unicode::Data FOR CANONICAL DESIGNATION

When "resolve_abbreviation" returns several IANA candidates for an abbreviation (such as the 30 zones that match CEST), the result set contains no is_primary marker. DateTime::Lite::TimeZone deliberately does not try to pick a canonical zone among IANA candidates, because no single heuristic gives a satisfying answer across all abbreviations.

Locale::Unicode::Data exposes CLDR's own canonical-designation flags (is_golden, is_primary, is_preferred, is_canonical) on a per-timezone basis, and is the recommended source of truth when a single representative zone is needed. A typical pattern is to resolve the abbreviation here, then ask Locale::Unicode::Data which candidate is the CLDR golden zone:

use DateTime::Lite::TimeZone;
use Locale::Unicode::Data;

my $candidates = DateTime::Lite::TimeZone->resolve_abbreviation( 'CEST' );
my $cldr       = Locale::Unicode::Data->new;

my $golden;
foreach my $c ( @$candidates )
{
    my $info = $cldr->timezone( timezone => $c->{zone_name} ) || next;
    if( $info->{is_golden} )
    {
        $golden = $c->{zone_name};
        last;
    }
}
# $golden is now 'Europe/Paris' (the CLDR golden zone for the
# 'Europe_Central' metazone)

Alternatively, if the abbreviation is known to map to a specific metazone, you can query the golden zone directly:

my $zones  = $cldr->timezones( metazone => 'Europe_Central', is_golden => 1 );
my $golden = $zones->[0]->{timezone};  # 'Europe/Paris'

For abbreviations that are not in the IANA types table (such as BRT, HAEC, AFT, and many others), use "resolve_abbreviation" with the extended flag instead: the extended_aliases table carries its own editorial is_primary marker that identifies the preferred zone among the candidates.

ERROR HANDLING

Upon error, this class methods sets an exception object, and return undef in scalar context, and an empty list in list context. The exception is accessible via:

my $err = DateTime::Lite::TimeZone->error;   # class method
my $err = $tz->error;                        # instance method

The exception stringifies to a human-readable message including the source file and line number.

If the instance option fatal has been enabled, then any error triggered will be fatal.

EPOCH CONVENTION

The tz.sqlite3 database stores span boundaries as Unix seconds (seconds since 1970-01-01T00:00:00 UTC), matching the raw values from the TZif binary files. DateTime::Lite uses Rata Die seconds (seconds since 0001-01-01T00:00:00).

The conversion constant is:

UNIX_TO_RD = 62_135_683_200

All lookup methods subtract UNIX_TO_RD from $dt->utc_rd_as_seconds before querying the database. NULL span boundaries represent ±infinity (before the first recorded transition, and after the last).

BUILDING THE DATABASE

The bundled tz.sqlite3 is generated by running:

perl scripts/build_tz_database.pl [--verbose, --debug 3]

This fetches the latest tzcode and tzdata release from IANA, verifies the GPG signature, compiles it with zic(1), and populates the database. Run this script once per tzdata release, then commit the updated lib/DateTime/Lite/tz.sqlite3.

SQL SCHEMA

The SQLite SQL schema is available in the file scripts/cldr-schema.sql

The data are populated into the SQLite database using the script located in scripts/build_tz_database.pl and the data accessible from https://ftp.iana.org/tz/releases

The SQL schema used to create the SQLite database is available in the scripts directory of this distribution in the file tz_schema.sql

The tables used are as follows, in alphabetical order:

aliases

  • alias

    A string field, case insensitive.

  • zone_id

    An integer field.

countries

  • code

    A string field, case insensitive.

  • name

    A string field, case insensitive.

extended_aliases

  • abbr_id

    An integer field.

  • abbreviation

    A string field, case insensitive.

  • zone_id

    An integer field.

  • is_primary

    A boolean field.

    Defaults to false

  • comment

    A string field.

leap_second

  • leap_sec_id

    An integer field.

  • zone_id

    An integer field.

  • leap_index

    An integer field.

  • occurrence_time

    An integer field.

  • correction

    An integer field.

  • is_expiration

    A boolean field.

    Defaults to false

metadata

  • key

    A string field.

  • value

    A string field.

spans

  • span_id

    An integer field.

  • zone_id

    An integer field.

  • type_id

    An integer field.

  • span_index

    An integer field.

  • utc_start

    An integer field.

  • utc_end

    An integer field.

  • local_start

    An integer field.

  • local_end

    An integer field.

  • offset

    An integer field.

  • is_dst

    A boolean field.

    Defaults to false

  • short_name

    A string field, case insensitive.

transition

  • trans_id

    An integer field.

  • zone_id

    An integer field.

  • trans_index

    An integer field.

  • trans_time

    An integer field.

  • type_id

    An integer field.

types

  • type_id

    An integer field.

  • zone_id

    An integer field.

  • type_index

    An integer field.

  • utc_offset

    An integer field.

  • is_dst

    A boolean field.

  • abbreviation

    A string field, case insensitive.

  • designation_index

    An integer field.

  • is_standard_time

    A boolean field.

  • is_ut_time

    A boolean field.

  • is_placeholder

    A boolean field.

    Defaults to false

zones

  • zone_id

    An integer field.

  • name

    A string field, case insensitive.

  • canonical

    A boolean field.

    Defaults to true

  • has_dst

    A boolean field.

    Defaults to false

  • countries

    A string array field.

  • coordinates

    A string field.

  • latitude

    A real field.

  • longitude

    A real field.

  • comment

    A string field.

  • tzif_version

    An integer field.

  • footer_tz_string

    A string field.

  • transition_count

    An integer field.

  • type_count

    An integer field.

  • leap_count

    An integer field.

  • isstd_count

    An integer field.

  • isut_count

    An integer field.

  • designation_charcount

    An integer field.

  • category

    A string field, case insensitive.

  • subregion

    A string field, case insensitive.

  • location

    A string field, case insensitive.

SEE ALSO

DateTime::Lite, DateTime::TimeZone, Locale::Unicode::Data

RFC 9636 (The Time Zone Information Format (TZif)) https://www.rfc-editor.org/rfc/rfc9636

Locale::Unicode::Data for historical data of time zones, metazones, and BCP47 time zones data.

AUTHOR

Jacques Deguest <jack@deguest.jp>

COPYRIGHT & LICENSE

Copyright(c) 2026 DEGUEST Pte. Ltd.

All rights reserved

This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.