DateTime-Lite

 view release on metacpan or  search on metacpan

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

    warnings::register_categories( 'DateTime::Lite' );
    use vars qw(
        $VERSION $ERROR $DEBUG $FATAL_EXCEPTIONS
        $DB_FILE $DBH $STHS
        $FALLBACK_TO_DT_TZ
        $HAS_CONSTANTS
        $MISSING_AUTO_UTF8_DECODING
        $SQLITE_HAS_MATH_FUNCTIONS
        $USE_MEM_CACHE $_CACHE
    );
    use overload (
        '""'     => \&name,
        bool     => sub{1},
        fallback => 1,
    );
    use version ();
    use Config;
    use Cwd ();
    use File::Spec ();
    use Scalar::Util ();
    use Wanted;

    # Seconds between the Unix epoch (1970-01-01T00:00:00 UTC) and the Rata Die
    # epoch (0001-01-01T00:00:00) used internally by DateTime::Lite.
    #
    # The tz.sqlite3 database stores transition times as raw Unix seconds
    # (as they come from the TZif binary files). All lookups subtract this
    # constant from $dt->utc_rd_as_seconds before querying the database.
    #
    # Verified:
    # DateTime::Lite->new(
    #     year  => 1970,
    #     month => 1,
    #     day   => 1,
    #     time_zone => 'UTC',
    # )->utc_rd_as_seconds == 62_135_683_200
    use constant UNIX_TO_RD => 62_135_683_200;
    our $VERSION = 'v0.5.4';
    our $DEBUG   = 0;
    our $DBH     = {};
    # Cached prepared statements, keyed by db file path then by statement ID:
    # $STHS->{ $db_file }->{ $statement_id } = $sth
    our $STHS    = {};
    # Package-level memory cache: canonical_name -> blessed object.
    # Populated when use_cache_mem => 1 is passed to new(), or when
    # enable_mem_cache() has been called at the class level.
    # Keys are canonical zone names after alias resolution.
    our $_CACHE  = {};
    # $SQLITE_HAS_MATH_FUNCTIONS is not defined on purpose
    # Its definedness is checked in _dbh_add_user_defined_functions()

    # The bundled database lives next to this file
    {
        my( $vol, $parent, $file ) = File::Spec->splitpath( __FILE__ );
        $DB_FILE = File::Spec->catpath( $vol, $parent, 'tz.sqlite3' );
        $DB_FILE = File::Spec->rel2abs( $DB_FILE )
            unless( File::Spec->file_name_is_absolute( $DB_FILE ) );
    }

    # Detect whether DBD::SQLite is available. If not, we fall back to
    # DateTime::TimeZone transparently.
    $FALLBACK_TO_DT_TZ = 0;
    local $@;
    eval
    {
        require DBI;
        require DBD::SQLite;
    };
    if( $@ )
    {
        warn( "DateTime::Lite::TimeZone: DBD::SQLite not available, falling back to DateTime::TimeZone. Install DBD::SQLite for a lighter footprint." ) if( warnings::enabled( 'DateTime::Lite' ) );
        $FALLBACK_TO_DT_TZ = 1;
    }
    elsif( !-e( $DB_FILE ) )
    {
        warn( "DateTime::Lite::TimeZone: bundled database $DB_FILE not found, falling back to DateTime::TimeZone." ) if( warnings::enabled( 'DateTime::Lite' ) );
        $FALLBACK_TO_DT_TZ = 1;
    }

    if( !$FALLBACK_TO_DT_TZ && -l( $DB_FILE ) )
    {
        my $real_path = Cwd::realpath( $DB_FILE );
        if( !-e( $real_path ) )
        {
            warn( "DateTime::Lite::TimeZone: bundled database $DB_FILE is a symbolic link pointing to $real_path, but it could not be found, falling back to DateTime::TimeZone." ) if( warnings::enabled( 'DateTime::Lite' ) );
            $FALLBACK_TO_DT_TZ = 1;
        }
        else
        {
            $DB_FILE = $real_path;
        }
    }

    if( !$FALLBACK_TO_DT_TZ && -z( $DB_FILE ) )
    {
        warn( "DateTime::Lite::TimeZone: bundled database $DB_FILE is an empty file (zero byte)." ) if( warnings::enabled( 'DateTime::Lite' ) );
        $FALLBACK_TO_DT_TZ = 1;
    }

    if( $FALLBACK_TO_DT_TZ )
    {
        local $@;
        # Lazy loading of DateTime::TimeZone
        # We do not want to make it a dependency of our module.
        eval
        {
            require DateTime::TimeZone;
        };
        if( $@ )
        {
            die( "Neither SQLite nor DateTime::TimeZone are installed on your system. You need at least one of them to use this module." );
        }
    }
    else
    {
        # DBD::SQLite::Constants available since 1.48
        # Foreign key constraints since SQLite v3.6.19 (2009-10-14)
        # DBD::SQLite 1.27 (2009-11-23)
        # utf8 auto decoding from version 1.68 (2021-07-22)
        $HAS_CONSTANTS = ( version->parse( $DBD::SQLite::VERSION ) >= version->parse( '1.48' ) ) ? 1 : 0;
        # Native UTF-8 string mode available since 1.68

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

       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.

=head2 Database schema

The bundled C<tz.sqlite3> uses the following main tables:

=over 4

=item C<aliases>

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

=item C<metadata>

Key/value pairs including the tzdata version

=item C<spans>

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

=item C<types>

Local time type records from the TZif files

=item C<zones>

Canonical IANA zone names with country codes and coordinates

=back

=head2 Fallback mode

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

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

=head1 CONSTRUCTOR

=head2 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 C<DateTime::Lite::TimeZone> object can be instantiated by either passing the timezone as a single argument, or as an hash, such as C<< name => 'Asia/Tokyo' >>

Recognised forms:

=over 4

=item Named IANA timezones such as C<America/New_York>, C<Europe/Paris>.

=item Aliases such as C<US/Eastern>, C<Japan>.

=item Fixed-offset strings such as C<+09:00>, C<-0500>.

=item The special names C<UTC>, C<floating>, and C<local>.

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

=over 8

=item B<Linux, macOS (darwin), FreeBSD, OpenBSD, NetBSD, Solaris, AIX, HP-UX, OS/2, Cygwin>

Tries, in order:

=over 12

=item * C<$ENV{TZ}>

=item * the C</etc/localtime> symlink target or a binary match against C</usr/share/zoneinfo>

=item * C</etc/timezone> (Debian/Ubuntu)

=item * C</etc/TIMEZONE> with a C<TZ=> line (Solaris, HP-UX)

=item * C</etc/sysconfig/clock> with a C<ZONE=> or C<TIMEZONE=> line (RedHat/CentOS)

=item * C</etc/default/init> with a C<TZ=> line (older Unix)

=back

=item B<Windows (MSWin32, NetWare)>

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

=item B<Android>

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.



( run in 2.490 seconds using v1.01-cache-2.11-cpan-39bf76dae61 )