DateTime-Lite

 view release on metacpan or  search on metacpan

lib/DateTime/Lite/TimeZone.pm  view on Meta::CPAN

            fatal       => $args{fatal},
        }, $class ) );
    }

    if( $name eq 'UTC' || $name eq 'Z' || $name eq '+0000' || $name eq '-0000' )
    {
        return( bless(
        {
            name        => 'UTC',
            is_floating => 0,
            is_utc      => 1,
            is_olson    => 0,
            has_dst     => 0,
            fatal       => $args{fatal},
        }, $class ) );
    }

    # Fixed-offset zones like "+09:00" or "-05:00" or "+0900"
    if( $name =~ /\A([+-])(\d{2}):?(\d{2})\z/ )
    {
        my $sign    = $1 eq '+' ? 1 : -1;
        my $offset  = $sign * ( $2 * 3600 + $3 * 60 );
        return( bless(
        {
            name         => $name,
            is_floating  => 0,
            is_utc       => ( $offset == 0 ? 1 : 0 ),
            is_olson     => 0,
            has_dst      => 0,
            fixed_offset => $offset,
            fatal        => $args{fatal},
        }, $class ) );
    }

    my $self = bless(
    {
        name        => $name,
        is_floating => 0,
        is_utc      => 0,
        is_olson    => 1,
        has_dst     => 0,
        _zone_id    => undef,
        fatal       => $args{fatal},
    }, $class );

    # Resolve aliases (such as "US/Eastern" -> "America/New_York")
    my $canonical = $self->_resolve_alias( $name ) ||
        return( $self->pass_error );

    $self->{_is_canonical} = ( $canonical eq $name ? 1 : 0 );
    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." ) );
            }
            return( $class->new( name => $candidates->[0]->{zone_name}, %args ) );
        }
    }
    return( $self->error( "Unknown time zone '$name'." ) ) unless( $ref );

    $self->{_zone_id}         = $ref->{zone_id};
    $self->{has_dst}          = $ref->{has_dst}   ? 1 : 0;
    $self->{is_olson}         = $ref->{canonical} ? 1 : 0;
    my @keys = qw( countries coordinates comment latitude longitude tzif_version footer_tz_string transition_count type_count leap_count isstd_count designation_charcount );
    @$self{ @keys } = @$ref{ @keys };

    # Store in process-level cache if requested.
    # Cache under both the input name (which may be an alias) and the
    # canonical name so that either form hits the cache on the next call.
    # Also initialise the per-object span cache so that _lookup_span
    # avoids SQLite queries for repeated timestamps in the same span.
    if( $use_cache )
    {
        $self->{_span_cache}             = {};
        $self->{_span_cache_local}       = {};
        $self->{_footer_cache_key}       = undef;
        $self->{_footer_cache_val}       = undef;
        $self->{_footer_local_cache_key} = undef;
        $self->{_footer_local_cache_val} = undef;
        $_CACHE->{ $name }               = $self;
        $_CACHE->{ $self->{name} }       = $self if( $self->{name} ne $name );
    }

    return( $self );
}

sub aliases
{
    # Can be called as class method, instance method, or plain function.
    my $class_or_self = shift( @_ );
    local $@;
    my $sth;
    unless( $sth = $class_or_self->_get_cached_statement( 'all_aliases' ) )
    {
        my $dbh = $class_or_self->_dbh || return( $class_or_self->pass_error );
        my $query = q{SELECT alias_name, zone_name FROM v_zone_aliases ORDER BY alias_name};
        $sth = eval
        {
            $dbh->prepare( $query );
        } || return( $class_or_self->error( "Error preparing the query to get all timezone aliases: ", ( $@ || $dbh->errstr ), "\nSQL query was $query" ) );
        $class_or_self->_set_cached_statement( all_aliases => $sth );
    }

    my $rv = eval{ $sth->execute };
    if( $@ )

lib/DateTime/Lite/TimeZone.pm  view on Meta::CPAN

JOIN transition tr ON tr.zone_id = t.zone_id AND tr.type_id = t.type_id
WHERE z.canonical = 1
  AND t.abbreviation = ?
GROUP BY z.name, t.utc_offset, t.is_dst, z.footer_tz_string${having_sql}
ORDER BY first_trans_time ASC, last_trans_time DESC, z.name ASC
SQL
    # Use the SQL string itself as the cache key - provably collision-free
    # regardless of any subtlety in the period_key_parts logic.
    my $cache_id = 'resolve_abbreviation_sql_' . $query;
    unless( $sth = $self->_get_cached_statement( $cache_id ) )
    {
        my $dbh  = $self->_dbh || return( $self->pass_error );
        $sth = eval
        {
            $dbh->prepare( $query );
        } || return( $self->error( "Error preparing the abbreviation resolution query: ", ( $@ || $dbh->errstr ), "\nSQL query was: $query" ) );
        $self->_set_cached_statement( $cache_id => $sth );
    }

    my $rv = eval
    {
        $sth->execute( $abbr, @having_values );
    };
    if( $@ )
    {
        $sth->finish;
        return( $self->error( "Error executing the abbreviation resolution query for '$abbr': $@\nSQL query was: ", $sth->{Statement} ) );
    }
    elsif( !defined( $rv ) )
    {
        $sth->finish;
        return( $self->error( "Error executing the abbreviation resolution query for '$abbr': ", $sth->errstr, "\nSQL query was: ", $sth->{Statement} ) );
    }

    my $all = eval{ $sth->fetchall_arrayref( {} ) };
    if( $@ )
    {
        $sth->finish;
        return( $self->error( "Error retrieving abbreviation resolution results for '$abbr': $@\nSQL query was: ", $sth->{Statement} ) );
    }
    elsif( !defined( $all ) && $sth->errstr )
    {
        $sth->finish;
        return( $self->error( "Error retrieving abbreviation resolution results for '$abbr': ", $sth->errstr, "\nSQL query was: ", $sth->{Statement} ) );
    }
    $sth->finish;

    # IANA types table returned results: use them directly.
    if( @$all )
    {
        # 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
        {
            $rx = qr/<\Q$abbr\E>/;
        }

        foreach my $row ( @$all )
        {
            my $footer = $row->{footer_tz_string};
            $row->{is_active} =
                ( defined( $footer ) && length( $footer ) && $footer =~ $rx )
                    ? 1 : 0;
        }

        # Re-sort with is_active as primary key. The SQL already gave us
        # (first_trans_time ASC, last_trans_time DESC, name ASC), so a
        # stable sort on is_active preserves that ordering within each group.
        # Perl's sort is stable since 5.8, so this is safe.
        my @sorted = sort { $b->{is_active} <=> $a->{is_active} } @$all;

        return([
            map
            {
                {
                    zone_name        => $_->{zone_name},
                    utc_offset       => $_->{utc_offset},
                    is_dst           => $_->{is_dst} ? 1 : 0,
                    ambiguous        => $ambiguous,
                    is_active        => $_->{is_active},
                    extended         => 0,
                    first_trans_time => $_->{first_trans_time},
                    last_trans_time  => $_->{last_trans_time},
                }
            }
            @sorted
        ]);
    }

    # No results in the IANA types table.
    # If extended mode is not requested, fail with the standard message.
    unless( $extended )
    {
        return( $self->error( "No timezone found for abbreviation '$abbr'." ) );
    }

    # Extended mode: fall back to the extended_aliases table.
    # Covers real-world abbreviations (such as BDT, CEST, JST) that are not stored as
    # type abbreviations in the IANA TZif data but map to known zones.
    # Note: extended aliases carry no utc_offset or is_dst data; those fields are
    # undef in the result. The caller must resolve offset from the zone itself if
    # needed.
    # Period filtering does not apply to extended aliases (no trans_time data).

lib/DateTime/Lite/TimeZone.pm  view on Meta::CPAN


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

=item B<VMS>

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

=item B<Symbian, EPOC, MS-DOS, Mac OS 9 and earlier>

Checks C<$ENV{TZ}> only.

=back

If the local timezone cannot be determined, an error is set and C<undef> is returned in scalar context, or an empty list in list context. In chaining (object context), it returns a dummy object (C<DateTime::Lite::Null>) to avoid the typical C<Can't c...

=item Coordinates via C<latitude> and C<longitude> arguments.

As an alternative to a C<name>, you can pass decimal-degree coordinates to have C<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 C<zone1970.tab> file (one representative point per canonical zone) and finds the nearest zone by the L<haversine great-circle distance|https://en.wikipedia.org/wiki/Haversine_formula>. ...

C<latitude> must be in the range C<-90> to C<90>; C<longitude> in C<-180> to C<180>. An L<error object|DateTime::Lite::Exception> is set and C<undef> is returned in scalar context, or an empty list in list context, if the values are out of range or i...

The haversine formula is computed in SQLite when the database was compiled with C<-DSQLITE_ENABLE_MATH_FUNCTIONS> (SQLite version E<gt>= 3.35.0, L<released on March 2021|https://sqlite.org/changes.html>).

On older systems or builds where the math functions are absent, the required functions (C<sqrt>, C<sin>, C<cos>, C<asin>) are registered automatically as Perl UDFs (User Defined Functions) via L<DBD::SQLite/sqlite_create_function> on first use, so co...

Detection is version-aware. Thus:

=over 8

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

=item * on SQLite with version E<lt> 3.35.0, where the math functions did not yet exist, UDFs are registered directly without querying C<pragma_function_list>.

=item * on SQLite version E<lt> 3.16.0, C<pragma_function_list> is not available as a table-valued function, so UDFs are registered directly.

=back

UDFs are available on all SQLite version E<gt>= 3.0.0.


On older systems that ships SQLite 3.31.1, the required functions (C<sqrt>, C<sin>, C<cos>, C<asin>) are registered automatically as Perl UDFs (User Defined Functions) via L<DBD::SQLite/sqlite_create_function> on first use, so coordinate resolution w...

=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

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

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

B<Layer 2 - Span cache>: Each cached TimeZone object stores the last matched UTC and local time span. Calls to C<offset_for_datetime> and C<offset_for_local_datetime> skip the SQLite query when the timestamp falls within the cached span's C<[utc_star...

B<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 c...

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

Cache entries are keyed by the name passed to L</new>, plus the canonical name (after alias resolution). Both C<US/Eastern> and C<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.

=head2 enable_mem_cache

Class method. Activates the memory cache for all subsequent L</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 C<< use_cache_mem => 1 >> on every L</new> call, but more convenient when you want the cache active for the lifetime of the process. Returns the class name to allow chaining.

=head2 disable_mem_cache

Class method. Disables the memory cache and clears all cached entries. Subsequent L</new> calls will construct fresh objects.

    DateTime::Lite::TimeZone->disable_mem_cache;

Returns the class name.

=head2 clear_mem_cache

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

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

    DateTime::Lite::TimeZone->clear_mem_cache;

Returns the class name.

=head1 METHODS

=head2 aliases

    # Checking for errors too

lib/DateTime/Lite/TimeZone.pm  view on Meta::CPAN


=item C<< < >>

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

=item C<< <= >>

Less than or equal.

=back

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

B<Value types>: ISO date strings such as C<1950-01-01> are converted to Unix epoch via SQLite C<strftime('%s', ...)>. Plain integers are treated as epoch seconds and passed as C<CAST(? AS INTEGER)> to ensure correct numeric comparison regardless of h...

The special value C<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.

=item C<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 C<%z> token). Only applies to the IANA types lookup;...

=back

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

=over 4

=item C<zone_name>

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

=item C<utc_offset>

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

=item C<is_dst>

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

=item C<ambiguous>

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

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

If you need a canonical or preferred zone designation in the CLDR sense, use L<Locale::Unicode::Data> which exposes CLDR's C<is_golden>, C<is_primary>, and C<is_preferred> flags on a per-timezone basis. See L</"USING Locale::Unicode::Data FOR CANONIC...

=item C<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 C<is_active>, so among zones with the same C<is_active> value, the one that adopted the abbreviation f...

=item C<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 C<is_active> and same C<first_trans_time>, the one that has used th...

=back

If an error occurred, this sets an L<exception object|DateTime::Lite::Exception>, and returns C<undef> in scalar context, and an empty list in list context. The exception object can then be retrieved with L</error>.

Note that many abbreviations such as C<EST> or C<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 C<ambiguous> flag will be C<0> in those cases. Gen...

=head2 short_name_for_datetime

    say $zone->short_name_for_datetime( $dt );

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

=head2 transition_count

Returns the number of transitions record for this timezone.

Equivalent to TZif header field C<timecnt>

See L<rfc9636, section 3.1|https://www.rfc-editor.org/rfc/rfc9636.html#name-tzif-header>

=head2 type_count

Returns the number of types for this timezone.

See L<rfc9636, section 3.1|https://www.rfc-editor.org/rfc/rfc9636.html#name-tzif-header>

=head2 tz_version

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

=head2 tzif_version

Returns the timezone version string from the timezone data.

The possible values are C<1>, C<2>, C<3> or C<4>

See L<rfc9636, section 3.1|https://www.rfc-editor.org/rfc/rfc9636.html#name-tzif-header>

=head1 USING Locale::Unicode::Data FOR CANONICAL DESIGNATION



( run in 0.555 second using v1.01-cache-2.11-cpan-39bf76dae61 )