DateTime-Lite
view release on metacpan or search on metacpan
lib/DateTime/Lite/TimeZone.pm view on Meta::CPAN
require File::Basename;
require File::Compare;
require File::Find;
my $size = -s( $file_to_match );
my $real_name;
local $@;
eval
{
local $SIG{__DIE__};
File::Find::find(
{
wanted => sub
{
if( !defined( $real_name ) &&
-f( $_ ) &&
!-l( $_ ) &&
$size == -s( $_ ) &&
File::Basename::basename( $_ ) ne 'posixrules' &&
File::Compare::compare( $_, $file_to_match ) == 0 )
{
$real_name = $_;
# Bail out of File::Find early using die with a sentinel
die({ found => 1 });
}
},
no_chdir => 1,
},
$zoneinfo_dir );
};
if( $@ )
{
# Re-raise anything that is not our own sentinel
unless( ref( $@ ) eq 'HASH' && $@->{found} )
{
return( $class->error( "Error while searching zoneinfo directory: $@" ) );
}
}
return( $real_name );
}
sub _nearest_zone
{
my( $class, $latitude, $longitude ) = @_;
return( $class->error( "Parameter 'latitude' must be a number." ) )
unless( defined( $latitude ) && Scalar::Util::looks_like_number( $latitude ) );
return( $class->error( "Parameter 'longitude' must be a number." ) )
unless( defined( $longitude ) && Scalar::Util::looks_like_number( $longitude ) );
return( $class->error( "Latitude must be between -90 and 90." ) )
unless( $latitude >= -90 && $latitude <= 90 );
return( $class->error( "Longitude must be between -180 and 180." ) )
unless( $longitude >= -180 && $longitude <= 180 );
my $sth;
unless( $sth = $class->_get_cached_statement( 'nearest_zone' ) )
{
my $dbh = $class->_dbh || return( $class->pass_error );
$class->_dbh_add_user_defined_functions( $dbh ) ||
return( $class->pass_error );
# Use the haversine formula entirely within SQLite to find the nearest zone.
# Only canonical zones with coordinates are considered.
# haversine(lat1, lon1, lat2, lon2):
# a = sin((lat2-lat1)/2)^2 + cos(lat1)*cos(lat2)*sin((lon2-lon1)/2)^2
# distance = 2 * asin(sqrt(a))
# in radians; no need for Earth radius since we are only ranking, not computing
# actual distance.
my $query = <<'SQL';
SELECT
name,
(
2.0 * asin( sqrt(
( sin( ( (latitude - ?) * 0.017453292519943 ) / 2.0 ) *
sin( ( (latitude - ?) * 0.017453292519943 ) / 2.0 ) )
+
cos( ? * 0.017453292519943 ) *
cos( latitude * 0.017453292519943 ) *
( sin( ( (longitude - ?) * 0.017453292519943 ) / 2.0 ) *
sin( ( (longitude - ?) * 0.017453292519943 ) / 2.0 ) )
) )
) AS distance
FROM zones
WHERE canonical = 1
AND latitude IS NOT NULL
AND longitude IS NOT NULL
ORDER BY distance ASC
LIMIT 1
SQL
local $@;
$sth = eval
{
$dbh->prepare( $query );
} || return( $class->error( "Cannot prepare nearest_zone: ", ( $@ || $dbh->errstr ), "\nQuery was: $query" ) );
$class->_set_cached_statement( nearest_zone => $sth );
}
my $rv = eval{ $sth->execute( $latitude, $latitude, $latitude, $longitude, $longitude ) };
if( $@ )
{
$sth->finish;
return( $class->error( "Error executing the query to get the nearest zone for latitude $latitude and longitude $longitude: $@", "\nSQL query was ", $sth->{Statement} ) );
}
elsif( !defined( $rv ) )
{
$sth->finish;
return( $class->error( "Error executing the query to get the nearest zone for latitude $latitude and longitude $longitude: ", $sth->errstr, "\nSQL query was ", $sth->{Statement} ) );
}
my $row = eval{ $sth->fetchrow_hashref };
if( $@ )
{
$sth->finish;
return( $class->error( "Error retrieving the nearest zone information for latitude $latitude and longitude $longitude: $@", "\nSQL query was ", $sth->{Statement} ) );
}
# We check for definedness, which means an error in DBI
elsif( !defined( $row ) && $sth->errstr )
{
$sth->finish;
return( $class->error( "Error retrieving the nearest zone information for latitude $latitude and longitude $longitude: ", $sth->errstr, "\nSQL query was ", $sth->{Statement} ) );
}
$sth->finish;
lib/DateTime/Lite/TimeZone.pm view on Meta::CPAN
=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 systeme 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;
( run in 0.676 second using v1.01-cache-2.11-cpan-df04353d9ac )