Revision history for Perl distribution DateTime-Lite

v0.6.6 2026-05-03T21:09:36+0900
    [Bug Fixes]
    - Corrected fallback handler. XSLoader fallback to pure-Perl was not triggered on
      platforms with old compilers (such as FreeBSD 9.2 with GCC 4.2) that emit an
      "Undefined symbol" error for __builtin_unreachable. The error filter regex has
      been tuned to cover this case.
    - Added, or corrected the following missing or broken methods to the pure-Perl
      fallback (PERL_DATETIME_LITE_PP=1):
        - clone(): fallback was added in DateTime::Lite.
        - _compare(): in DateTime::Lite, moved is_infinite() check before the
          floating-zone clone block to prevent incorrect behaviour when comparing
          Infinite objects in pure-Perl.
        - _posix_tz_lookup(): in DateTime::Lite::TimeZone, corrected to add pure-Perl
          fallback.
        - DateTime::Lite::PP: removed dependency on DateTime::LeapSecond (part of the
          DateTime distribution). The leap second table is now inlined directly from
          leap_seconds.h, making the pure-Perl fallback fully self-contained.
        - DateTime::Lite::PP: _normalize_tai_seconds() and _normalize_leap_seconds()
          did not short-circuit on non-finite values (Inf/-Inf), causing
          DateTime::Lite::Infinite::Future and DateTime::Lite::Infinite::Past objects
          to be constructed with corrupted utc_rd_days (-2 instead of Inf).
          Both functions now return immediately when the seconds argument is not a
          finite number, mirroring the dtl_isfinite() guard in the XS implementation.

    [CI]
    - Added a new job in the CI pipeline: perl-5.38-pure-perl to verify the full test
      suite passes on every release with PERL_DATETIME_LITE_PP=1.

v0.6.5 2026-04-24T15:17:55+0900
    [Bug Fixes]
    - Added %O in the supported patterns tokens for strftime(). It returns now the
      IANA timezone name via $dt->time_zone->name (such as Asia/Tokyo or Europe/London).
      This makes %O usable symmetrically for both parsing and formatting in conjunction
      with DateTime::Format::Lite.
    - Added %E (abbreviated weekday name, alias for %a) and %h (abbreviated month
      name, alias for %b) that were missing from the strftime supported patterns tokens.
    - Corrected %{n}N patterns (such as %3N for milliseconds and %6N for microseconds)
      that were returning 9 digits regardless of the specified precision.
      The N entry in the strftime dispatch table was ignoring its second argument
      (the digit count captured by the %(\d+)N regex branch).

    [New Feature]
    - Added %:z to strftime(), returning the UTC offset with a colon separator
      (such as +01:00 or -05:00). This is a GNU extension also supported by Ruby and
      Python, useful for producing W3C/RFC 3339 formatted timestamps. The strftime
      regex has been extended to handle this multi-character specifier.

    [Improvement]
    - Improved the POD for DateTime::Lite with a detailed section on supported strftime
      tokens.

v0.6.4 2026-04-23T16:06:35+0900
    [Maintenance]
    - SQLite database updated with the newly released IANA data version 2026b.

    [New Features]
    - DateTime::Lite::TimeZone->new() now accepts an C<extended> boolean option.
      When set to a true value and the supplied name is not recognised as a valid
      IANA timezone name, new() calls resolve_abbreviation() with extended => 1
      internally and, if a single unambiguous candidate is found, recurses with the
      resolved canonical name. This allows timezone abbreviations such as JST, CET,
      or EST to be passed directly to new() without requiring the caller to call 
      resolve_abbreviation() explicitly. See synopsis for examples.
    - DateTime::Lite->new() and set_time_zone() now accept a hash reference for the
      time_zone parameter. The hash reference is passed directly to
      DateTime::Lite::TimeZone->new(), allowing options such as extended => 1 or
      coordinate-based resolution to be specified inline.

    [Bug Fixes]
    - DateTime::Lite->_new() now validates that a reference passed as time_zone
      is a blessed DateTime::Lite::TimeZone object, returning a clear error
      instead of silently misbehaving with an unexpected reference type.
    - Fixed: offset_for_datetime() and offset_for_local_datetime() returned an
      incorrect offset for timezones that abolished DST after having used it, such as
      Asia/Tehran (DST abolished September 2022).
      The POSIX footer string (<+0330>-3:30 for Tehran) was applied unconditionally
      whenever a footer was present, overwriting historically correct bounded DST
      spans with the post-abolition rule.
      The footer is now only applied when the matched database span is open-ended
      (utc_end IS NULL / local_end IS NULL), which means that the timestamp is beyond
      all stored transitions.
      For timestamps within stored transitions, the database span is returned as-is.
      Also, both span lookup queries now use:
      ORDER BY (utc_start IS NULL) ASC, utc_start DESC (resp. local_start)
      to guarantee deterministic results when multiple spans satisfy the WHERE clause,
      because without ORDER BY, LIMIT 1 is non-deterministic across SQLite versions.
      The IS NULL expression is used instead of the NULLS LAST syntax, because it is
      only available from SQLite 3.30.0 (2019-10-04) onward and would fail on older
      installations.
      Reported by Andrew Grechkin (@andrew-grechkin), GitLab issue #2.

    [Thread Safety]
    - DateTime::Lite::TimeZone: the package-level database handle cache ($DBH)
      and prepared statement cache ($STHS) are now keyed by thread ID (or PID
      when not running under threads). This prevents a DBD::SQLite error, such as
      "handle 2 is owned by thread aaaae23f22a0 not current thread"
      when multiple threads call timezone methods concurrently.
      The thread ID is obtained via threads->tid() when $Config{useithreads} is true,
      threads.pm is loaded, and threads->can('tid') is available; otherwise $$ is used.

v0.6.3 2026-04-20T21:57:34+0900
    [Improvement]
    - In resolve_abbreviation(), reworked the default sort order for IANA
      (non-extended) results returned by DateTime::Lite::TimeZone->resolve_abbreviation().
      The previous order (ORDER BY MAX(tr.trans_time) DESC) produced counter-intuitive
      top results such as CEST -> Africa/Tripoli first.
    - The new ordering is a four-level key:
                                   abbreviation come first)
      For CEST, this puts Europe/Berlin first. For PST, America/Los_Angeles.
      For JST, Asia/Tokyo. For WET, Atlantic/Faroe. The ordering now matches
      user intuition as closely as the data allows.
        1. is_active DESC         (zones whose POSIX footer still references the
        2. first_trans_time ASC   (earliest adopter of the abbreviation wins)
        3. last_trans_time DESC   (tie-break by persistence)
        4. zone_name ASC          (deterministic final tie-breaker)
    - Added two new fields to IANA results:
      - 'is_active': 1 if the zone's POSIX footer_tz_string still references the
        abbreviation; 0 otherwise. This is computed post-query via regex.
      - 'first_trans_time': Unix epoch of the earliest transition in this zone
        using the abbreviation (complements the existing last_trans_time).
    - Extended alias results (when extended is set to true, falls back to
      extended_aliases table) keep their previous shape: 'is_primary' instead of
      'is_active', no 'first_trans_time'. The two branches intentionally expose
      different ordering signals because their sources of authority differ
      (curated editorial for extended, transition data for IANA).

    [Documentation]
    - Rewrote the POD for resolve_abbreviation() for is_primary and added a new
      section "USING Locale::Unicode::Data FOR CANONICAL DESIGNATION" that explains
      why no single zone is marked as "primary" among IANA candidates of an
      abbreviation, and demonstrates how to delegate canonical designation to
      Locale::Unicode::Data (which exposes CLDR's 'is_golden', 'is_primary',
      'is_preferred' flags via its metazones mapping).
    - Updated the POD examples for resolve_abbreviation() to show the new result
      shape ('is_active', 'first_trans_time') and documented the four-level sort order
      in prose.

    [Tests]
    - Updated t/17.resolve_abbreviation_period.t to assert the new sort order, and
      added two subtests checking that IANA results carry 'is_active', and that
      extended results carry 'is_primary' (but not 'is_active').
    - Added new test suite t/18.resolve_abbreviation_ordering.t

    [Performance]
    - Measured the cost of the new sort: roughly +35 microseconds per call
      on the full 178-abbreviation test set (around +17% over the v0.6.2
      baseline). Memory overhead is two additional hash keys per result row
      (approximately 50 bytes per row).

v0.6.2 2026-04-19T14:15:33+0900
    [Critical Bug Fixes in DateTime::Lite::TimeZone]
    - Moved 'use strict' and 'use warnings' out of the BEGIN block and into
      package-level scope. This hid some latent bugs fixed below.
    - Fixed nine class-callable methods that referenced an undeclared $self
      variable throughout their bodies, where only $class_or_self was actually assigned
      from the first shifted argument.
      Without strict in effect, these references silently evaluated to undef or crashed
      at runtime when called. The affected methods are:
      - aliases()
      - all_names()
      - categories()
      - countries()
      - is_valid_name()
      - names_in_category()
      - names_in_country()
      - offset_as_seconds()
      - and offset_as_string().
      All nine now work correctly when invoked as class methods, as intended and
      documented.
    - Fixed _get_zone_info() which ignored its $name parameter and used object-based
      properties even when called as a class function, which would have triggered a
      perl error.
    - Modified fatal() to use the global variable $FATAL_EXCEPTIONS when called as a 
      class function, or the 'fatal' instance property.
    - Fixed names_in_country(): the prepared statement cache key was incorrectly shared
      with names_in_category() (both using 'names_in_category'), so whichever method was
      called second would retrieve the other method's prepared statement and produce
      silently wrong results.
      The cache key is now 'names_in_country'. The method now also correctly passes the
      $code argument to execute() (it was  previously called with no arguments).
    - Fixed names_in_category() which called execute() without passing the $cat argument,
      so the SQL placeholder bound to NULL and the method returned zero matches for every
      input. Now passes $cat to execute().
    - Fixed categories() query which returned an empty-string entry from the Factory zone
      (which has a NULL category in IANA data). Query now filters with 'WHERE category
      IS NOT NULL'.
    - Fixed bad variable name $row used in aliases(), all_names(),
      categories(), countries(), names_in_category(), and names_in_country().

    [Defensive Guards]
    - Added instance guards to eight class-callable methods that require a blessed
      instance: category(), country_codes(), has_dst_changes(), is_dst_for_datetime(),
      offset_for_datetime(), offset_for_local_datetime(), short_name_for_datetime(),
      and tzif_version(). These return a clean error via $self->error() rather than
      dereferencing a class name string under strict refs.
    - Added die() guards in _set_get_prop() and _lookup_span() to catch internal design
      errors where a class name is passed instead of an instance. These are private
      methods, so a die() (rather than a clean return) is appropriate: reaching them in
      class context indicates a caller bug that should surface immediately.

    [API Additions]
    - Introduced new package variable $FATAL_EXCEPTIONS for DateTime::Lite::TimeZone,
      similar to the one used in DateTime::Lite.
      When set to a true value, newly constructed DateTime::Lite::TimeZone objects
      default to fatal set to true. Per-object fatal flags remain individually settable
      via the fatal() accessor/mutator.

v0.6.1 2026-04-19T08:22:20+0900
    - Fixed _dbh_add_user_defined_functions() in DateTime::Lite::TimeZone to
      correctly handle SQLite installations where pragma_function_list is
      unavailable.
      The previous implementation queried pragma_function_list unconditionally to
      detect whether SQLite's built-in math functions (sqrt, sin, cos, asin) were
      compiled in (with the macro 'SQLITE_ENABLE_MATH_FUNCTIONS'). That table-valued
      pragma is only available since SQLite 3.16.0 (2017-01-02).
      So, older installations raised the error: "no such table: pragma_function_list"
      causing t/14.tz_coordinates.t to fail entirely.
      The detection logic is now version-aware, based on $DBD::SQLite::sqlite_version:
        - SQLite >= 3.35.0 (2021-03-12): pragma_function_list is queried for 'sqrt'.
          The check runs before any UDF is registered, so a hit is guaranteed to be a
          native function. A miss means the build omitted -DSQLITE_ENABLE_MATH_FUNCTIONS;
          Perl UDFs are registered as fallback.
        - SQLite >= 3.16.0 and < 3.35.0: pragma_function_list exists but the math
          functions cannot be present regardless of the build flags;
          Perl UDFs are registered unconditionally without querying the pragma.
        - SQLite < 3.16.0: pragma_function_list is not available as a table-valued
          function, so Perl UDFs are registered unconditionally.
      UDFs via sqlite_create_function() are available on all SQLite >= 3.0.0, so
      coordinate-based timezone resolution now works transparently on all supported
      SQLite versions.
      Reported via CPAN Testers on Perl 5.20.0, 5.22.2 and 5.24.0 on x86_64-linux
      (Debian Wheezy, system SQLite < 3.16.0).
      Thanks to Slaven Rezić for the detailed test reports.
    - Added .gitlab-ci.yml. The pipeline covers Perl 5.10.1 through 5.40 on Linux,
      plus a dedicated job that builds SQLite 3.15.2 from the official autoconf tarball
      (the last release before 3.16.0) to ensure the unconditional UDF registration path
      is exercised in CI.

v0.6.0 2026-04-17T22:30:20+0900
    - Added extended_aliases table to tz.sqlite3 (schema v0.6.0).
      abbreviations not stored as TZif type entries in the IANA database, such as BDT,
      CEST, HAEC, JST, and the full set of NATO military single-letter zones.
      Each abbreviation maps to one or more canonical IANA zone names.
      One row per (abbreviation, zone_id) pair with 'is_primary' marking the most
      commonly accepted canonical zone when multiple candidates exist.
      Two triggers enforce that at most one is_primary = 1 row exists per abbreviation
      (on INSERT and on UPDATE OF is_primary).
      355 timezone abbreviations, 535 (abbreviation, zone) pairs, covering real-world
    - Updated tz.sqlite3 with new schema and latest data (tzdata 2026a).
    - Extended resolve_abbreviation() in DateTime::Lite::TimeZone with an optional
      'extended' boolean argument.
      When true and the abbreviation is absent from the IANA types table, the method
      falls back to querying the new table 'extended_aliases'.
      Extended results carry utc_offset => undef and is_dst => undef (the extended
      alias table maps names only); the new 'is_primary' and 'extended' keys are added
      to each result hashref.
      The ambiguity flag for extended results is based on candidate count and
      'is_primary': 'ambiguous' is set to 0 when exactly one 'is_primary' exists,
      otherwise, 'ambiguous' is set to 1.
      Existing callers are unaffected: without the option 'extended' set to a true value,
      the behaviour is identical to the previous version v0.5.0.
    - Added the property 'extended' set to 0 in all IANA results returned from
      resolve_abbreviation() for consistency with extended alias results.
    - Added cached prepared statement resolve_abbreviation_extended for the
      'extended_aliases' table query, consistent with existing statement caching.
    - Updated POD for resolve_abbreviation() to document the boolean option 'extended',
      'utc_offset', and the result fields 'extended' and 'is_primary', and the 'ambiguity'
      semantics for extended results.
    - Added t/16.extended_aliases.t with 6 subtests covering:
      - IANA abbreviation with the boolean option 'extended' as a no-op (CEST)
      - unambiguous single-zone extended alias (AFT -> Asia/Kabul)
      - multi-zone extended alias with 'is_primary' (AMST)
      - unknown abbreviation with and without the boolean option 'extended'
      - JST IANA offset verification; and
      - round-trip: using the returned 'zone_name' to instantiate a
        DateTime::Lite::TimeZone object.
    - Extended resolve_abbreviation() with period-based filtering and deterministic
      sort order:
      - Results are now sorted by most-recently-used first
        (MAX(trans_time) DESC), so the currently-active or most-recently-active zone
        appears first. The new result field 'last_trans_time' exposes the Unix epoch
        of that most recent transition.
      - Added optional 'period' argument to restrict results to zones whose most recent
        matching transition falls within a given time window.
        Accepts a single value or an array reference of values for multiple AND-combined
        conditions. Each value may be prefixed with a comparison operator: >, >=, <, <=.
        ISO date strings such as '1950-01-01' are converted to Unix epoch via SQLite
        strftime(); plain integers are passed as CAST(? AS INTEGER) to ensure
        arithmetic comparison.
        The special value 'current' returns only zones where the abbreviation is active
        right now, by checking that the most recent matching transition is the zone's
        overall most recent transition and is in the past.
    - Added t/17.resolve_abbreviation_period.t with 10 subtests for abbreviated timezone
      resolution.

v0.5.0 2026-04-16T15:26:07+0900
    - Added the method resolve_abbreviation() in DateTime::Lite::TimeZone
    - Corrected method _nearest_zone() to use $class instead of $self
    - Implemented a check for the availability of SQLite math functions
      (SQLite version >= 3.35.0; March 2021)
    - Added a private method, _dbh_add_user_defined_functions(), to add the missing math
      functions with User Defined Functions if missing. _dbh_add_user_defined_functions()
      is only called by relevant methods, so those math functions are added only when
      necessary.

v0.4.0 2026-04-14T10:27:25+0900
    - Fixed t/12.tz_database.t on Windows (Strawberry Perl): replaced
      POSIX::mktime with Time::Local::timegm for pre-1970 timestamps,
      which POSIX::mktime cannot handle on Windows.
    - Added start_of( $unit ) mutator, which modifies the datetime object in place
      to the first instant of the given unit.
      Supported units are second, minute, hour, day, week, local_week, month, quarter,
      year, decade, and century.
      Delegates to truncate() for most units; decade and century are handled independently.
      Returns the modified object on success.
    - Added end_of( $unit ) mutator: modifies the datetime object in place to
      the last nanosecond of the given unit (i.e. the nanosecond before the
      start of the next unit).
      Supports the same units as start_of().
      Handles variable-length units such as months and leap years correctly without
      hardcoding boundary values.
      Returns the modified object on success.
    - Added test suite t/15.start_end_of.t covering all supported units for both
      start_of() and end_of(), including edge cases such as February in leap and
      non-leap years, quarter boundaries, decade and century boundaries, timezone and
      locale preservation, and invalid unit handling.
    - Added POD documentation for posix_tz_lookup() under a new =head1 LOW-LEVEL XS UTILITIES
      section in DateTime::Lite, documenting the XS function signature, all supported
      POSIX TZ string rule forms (Jn, n, Mm.w.d), the RFC 9636 extensions for TZif v3+,
      and the structure of the returned hashref.

v0.3.0 2026-04-13T07:30:23+0900
    - Added GPS coordinate-based timezone resolution to DateTime::Lite::TimeZone::new().
      Passing C<latitude> and C<longitude> (decimal degrees) instead of C<name>
      finds the nearest canonical IANA timezone using the haversine great-circle
      distance against the reference coordinates stored in the C<zones> table of C<tz.sqlite3> (derived from
      IANA C<zone1970.tab>). No new dependencies.
      Note: this is an approximation based on one representative point per zone;
      it is accurate for most locations but may give incorrect results near timezone
      boundaries or in enclaves. For more boundary-precise resolution, see
      L<Geo::Location::TimeZoneFinder>.
    - Added private method _nearest_zone() to DateTime::Lite::TimeZone, implementing
      the haversine query entirely within SQLite via asin(), sqrt(), cos() and sin()
      functions. Only canonical zones with coordinates are considered.
    - Added input validation for latitude (-90..90) and longitude (-180..180), with
      proper error() returns rather than die().
    - Updated DateTime::Lite::TimeZone POD to document the coordinate-based resolution,
      its approximation caveat, and the valid parameter ranges.
    - Added t/14.tz_coordinates.t with 10 subtests covering: Tokyo, Paris, New York,
      Taipei, Sydney, Buenos Aires (southern hemisphere); integration with
      DateTime::Lite->now(); and input validation for missing, out-of-range, and
      non-numeric parameters.

v0.2.0 2026-04-11T20:48:09+0900
    - BCP47 -u-tz- locale extension support: if the locale tag carries a
      Unicode timezone extension, such as C<he-IL-u-ca-hebrew-tz-jeruslm>,
      and no explicit C<time_zone> argument is supplied to the constructor,
      the corresponding IANA canonical timezone name is resolved automatically
      via C<Locale::Unicode->tz_id2names()>.
      Explicit C<time_zone> always takes priority.
      No new dependencies: the BCP47 timezone to Olson canonical timezone
      resolution uses the static in-memory hash in C<Locale::Unicode>, so no
      SQLite query, and thus super fast.
    - _set_locale() rewritten: now uses C<Scalar::Util::blessed()> instead
      of C<ref()> to distinguish objects from unblessed references; This was
      a carry over from the way DateTime is doing.
      Returns the C<DateTime::Locale::FromCLDR> object instead of C<undef> to allow
      the BCP47 timezone resolution in C<_new()>.
      Unblessed references now return a proper error instead of an undefined behaviour;
      replaced C<die()> with C<pass_error()> for consistency with the no-die philosophy.
    - Added support for 'local' timezone name in DateTime::Lite::TimeZone::new().
      The local timezone is resolved automatically without any external module
      dependency, using OS-specific detection strategies:
        - Unix/Linux/macOS/FreeBSD/OpenBSD/NetBSD/Solaris/AIX/HP-UX/OS2/Cygwin:
          $ENV{TZ}, /etc/localtime symlink or binary match, /etc/timezone,
          /etc/TIMEZONE, /etc/sysconfig/clock, /etc/default/init.
        - Windows (MSWin32, NetWare): $ENV{TZ} then Windows Registry via
          Win32::TieRegistry (optional, not a hard dependency).
        - Android: $ENV{TZ}, getprop persist.sys.timezone, then 'UTC'.
        - VMS: TZ, SYS$TIMEZONE_RULE, SYS$TIMEZONE_NAME, UCX$TZ, TCPIP$TZ.
        - Symbian, EPOC, MS-DOS, Mac OS 9 and earlier: $ENV{TZ} only.
    - Added OS dispatch aliases for darwin, cygwin, freebsd, openbsd, netbsd, solaris,
      aix, hpux, os2, netware, symbian, epoc, dos, macos.
    - Updated DateTime::Lite::TimeZone POD to document 'local' resolution strategy per OS.
    - Added t/10.timezone.t subtest for 'local' timezone resolution via $ENV{TZ}.

v0.1.0 2026-04-10T06:12:47+0900
    - Initial release.
    - Full port of DateTime 1.66 API.
    - Dependencies reduced from ~10 non-core to 3 (DateTime::TimeZone,
      DateTime::Locale::FromCLDR, Locale::Unicode).
    - Specio, Params::ValidationCompiler, Try::Tiny, namespace::autoclean all
      eliminated.
    - XS layer ported from DateTime.xs with 4 new functions:
      _rd_to_epoch, _epoch_to_rd, _normalize_nanoseconds, _compare_rd.
    - Pure-Perl fallback (DateTime::Lite::PP) for environments without a C compiler.
    - No die() in normal error paths: DateTime::Lite::Exception used throughout,
      errors returned as undef in scalar context.
    - DateTime::Lite::Infinite provides singleton Future/Past objects.
    - Locale data via DateTime::Locale::FromCLDR + Locale::Unicode::Data (SQLite backend,
      no generated Perl locale files).
    - Includes DateTime::Lite::TimeZone based thoroughly on the IANA Olson data
      accessible with a SQLite database.
    - Replaced the shallow Perl clone() with an XS deep copy (DateTime-Lite.xs):
      - Root object scalar fields are copied with newSVsv().
      - Nested blessed hashrefs (tz, locale) are copied with a new static
        helper dtl_clone_flat_hv(), then re-blessed into the original stash.
        The RV is created first with newRV_noinc() before calling sv_bless(),
        which requires an RV not a raw HV pointer.
      - The local_c cache (plain hashref) is also deep-copied, eliminating
        any shared state between original and clone.
      - Non-HV references (formatter objects, etc.) are shallow-copied with
        newSVsv(), acceptable since formatters are effectively immutable.
    - Implemented a TZif footer POSIX parser in XS, based on IANA code (public domain).
    - New file dtl_posix.h: self-contained C header containing eight functions
      derived from tzcode localtime.c (is_digit, getzname, getqzname, getnum,
      getsecs, getoffset, getrule, transtime) plus dtl_year_to_jan1() and the
      public entry point dtl_posix_tz_lookup(). All symbols prefixed dtl_;
      no dynamic allocation, no system calls, no global state.
    - New XS function DateTime::Lite::posix_tz_lookup(class_or_self,
      unix_secs, tz_string): parses any POSIX TZ footer and returns
      { offset, is_dst, short_name } or undef. Handles Jn, n, Mm.w.d
      rules; quoted <name> abbreviations; fractional offsets; negative and
      >24 h transition times (RFC 9636 s3.3.2 v3+ extensions); southern-
      hemisphere DST (start > end).
    - DateTime::Lite::TimeZone::_posix_tz_lookup() is now a thin Perl wrapper that
      delegates to the XS function.
    - Implemented process-level memory cache to DateTime::Lite::TimeZone.
      The cache is opt-in and activated either per-call or class-wide:
        use_cache_mem => 1 argument to new() enables it for that call;
        DateTime::Lite::TimeZone->enable_mem_cache enables it globally.
      Three new class methods: enable_mem_cache(), disable_mem_cache(),
      clear_mem_cache(). Cache is keyed by both input name and canonical
      name after alias resolution, so "US/Eastern" and "America/New_York"
      share the same cached object.
    - Implemented three-layer span cache to DateTime::Lite::TimeZone, active
      when use_cache_mem => 1 or enable_mem_cache() is set:
        Layer 1 - object cache: TimeZone->new() returns the cached object
          keyed by zone name and canonical name, bypassing SQLite entirely.
        Layer 2 - span cache: _lookup_span() and _lookup_span_local() store
          the last matched span boundaries per cached TZ object. Subsequent
          calls within the same span return the cached result in ~0.5 us
          instead of ~45 us (SQL query). SQL extended with utc_start/utc_end
          and local_start/local_end columns for range checks.
        Layer 3 - POSIX footer cache: For zones with a footer TZ string
          (e.g. America/New_York), the DST rule calculation result is cached
          by calendar day. Pre-check added BEFORE the SQL query so future
          dates skip the database entirely on subsequent calls.