DateTime-Lite
view release on metacpan or search on metacpan
my $err = DateTime::Lite->error; # class method
my $err = $dt->error; # instance method
The exception object stringifies to a human-readable message including file and line number.
`error` detects the context is chaining, or object, and thus instead of returning `undef`, it will return a dummy instance of `DateTime::Lite::Null` to avoid the typical perl error `Can't call method '%s' on an undefined value`.
So for example:
$dt->now( %bad_arguments )->subtract( %params );
If there was an error in `now`, the chain will execute, but the last one, `subtract` in this example, will return `undef`, so you can and even should check the return value:
$dt->now( %bad_arguments )->subtract( %params ) ||
die( $dt->error );
# PERFORMANCE
This section compares `DateTime::Lite` with the reference implementation [DateTime](https://metacpan.org/pod/DateTime) 1.66 on four axes: module footprint, load time, memory, and CPU throughput. The figures below were recorded on an `aarch64` machine...
The goal of this comparison is not to disparage [DateTime](https://metacpan.org/pod/DateTime), which is a mature, feature-complete, and battle-tested library, but to make the trade-offs explicit so you can choose the right tool for your context.
## Module footprint
The number of files loaded into `%INC`, directly and indirectly through dependencies, when `use DateTime` or `use DateTime::Lite` is evaluated:
DateTime 1.66 DateTime::Lite
------- ------------- --------------
use Module 137 67
TimeZone class alone 105 47
Runtime prereqs (META) 23 11
`DateTime` depends on [Specio](https://metacpan.org/pod/Specio), [Params::ValidationCompiler](https://metacpan.org/pod/Params%3A%3AValidationCompiler), [namespace::autoclean](https://metacpan.org/pod/namespace%3A%3Aautoclean), and several supporting ...
`DateTime::TimeZone` loads 105 modules because it ships one `.pm` file per IANA zone, such as `DateTime::TimeZone::America::New_York`, all loaded on the first `new()` call. `DateTime::Lite::TimeZone` loads, directly and indirectly, 47 modules and sto...
## Load time
Measured as `time()` around a cold `require` (modules not yet in `%INC`):
DateTime 1.66 DateTime::Lite
------- ------------- --------------
require Module 48 ms 32 ms
require TimeZone standalone 180 ms 100 ms
Startup time matters in short-lived scripts (cron jobs, CLI tools, CGI) where the process initialisation is a significant fraction of total runtime. For a long-running Apache2/mod\_perl2, Plack, or Mojolicious service, this cost is paid once and amor...
## Memory (RSS after loading)
Measured in a clean Perl process immediately after `use Module`:
DateTime 1.66 DateTime::Lite
------- ------------- --------------
use Module (~28 MB) ~28 MB ~37 MB
TimeZone class only ~19 MB ~16 MB
The `use Module` row is somewhat misleading on its own: `DateTime::Lite` loads `DBD::SQLite`, which embeds a complete compiled SQLite engine (~14 MB of native code) regardless of how many timezone objects you create. When measuring the `TimeZone` cla...
`DateTime::TimeZone` pre-loads all IANA Olson definitions into memory on the first `new()` call (roughly 3-4 MB of compiled Perl structures on top of the module overhead). `DateTime::Lite::TimeZone` queries a compact SQLite database on demand and kee...
## CPU throughput (10,000 iterations, µs per call)
DateTime 1.66 DateTime::Lite
------- ------------- --------------
new( UTC ) ~13 µs ~10 µs
new( named zone, string ) ~25 µs ~64 µs (*)
new( named zone, all caches enabled ) ~25 µs ~14 µs
now( UTC ) ~11 µs ~10 µs
year + month + day + epoch ~0.5 µs ~0.4 µs
clone + add( days + hours ) ~35 µs ~25 µs
strftime ~3.5 µs ~3.6 µs
TimeZone->new (warm, no mem cache) ~2 µs ~19 µs (*)
TimeZone->new (mem cache enabled) ~2 µs ~0.4 µs
Rows marked `(*)` reflect the default behaviour without the memory cache.
With `DateTime::Lite::TimeZone->enable_mem_cache` active, `TimeZone-`new> drops to ~0.4 µs and `new(named zone)` drops to ~14 µs, which is faster than `DateTime` (~25 µs). See ["TimeZone caching model"](#timezone-caching-model) for the full explan...
For UTC construction, `now()`, accessors, arithmetic, and formatting, `DateTime::Lite` is equivalent or faster. The XS-accelerated clone and the lighter validation layer account for the gain in arithmetic.
## TimeZone caching model
This is the single most important trade-off to understand.
**DateTime::TimeZone** loads the complete set of IANA time zone rules into RAM the first time any named zone is constructed (~180 ms startup, ~4 MB of in-memory hash structures). Every subsequent `DateTime::TimeZone->new( name => $name )` call is ser...
**DateTime::Lite::TimeZone** stores the same IANA data in a compact SQLite database (`tz.sqlite3`, included in the distribution). The first call for a given zone name runs a query (~22 ms) and populates a per-instance cache; subsequent calls for the ...
**Optional memory cache:** `DateTime::Lite::TimeZone` also provides an opt-in process-level memory cache that matches or beats `DateTime::TimeZone` on per-call speed:
# Enable once at application start-up:
DateTime::Lite::TimeZone->enable_mem_cache;
# Or per call:
my $tz = DateTime::Lite::TimeZone->new(
name => 'America/New_York',
use_cache_mem => 1,
);
With the memory cache active, repeated `new()` calls for the same zone return the cached object from a plain hash lookup in about 0.8 µs:
DateTime::TimeZone DateTime::Lite::TimeZone
------ ----------------- ------------------------
Cold first call ~225 ms ~22 ms
Warm (no mem cache) ~2 µs ~19 µs
Warm (mem cache only) ~2 µs ~0.4 µs
Warm (mem+span+footer cache) ~2 µs ~0.4 µs
new(named zone, all caches) ~25 µs ~14 µs
Practical guidance:
- For long-lived services constructing datetime objects with named zones, call `DateTime::Lite::TimeZone->enable_mem_cache` once at startup.
This activates three layers of caching:
- 1. the object cache (avoids SQLite construction);
- 2. the span cache (avoids the UTC offset query); and
- 3. the footer cache (avoids the POSIX DST rule calculation).
With all layers warm, `new(named zone)` costs ~14 µs, which is faster than `DateTime` (~25 µs).
- If you prefer explicit control, pass `use_cache_mem => 1` on each individual `new()` call, or construct one `TimeZone` object and reuse it:
my $tz = DateTime::Lite::TimeZone->new( name => 'America/New_York' );
my $dt = DateTime::Lite->new( ..., time_zone => $tz );
- For batch processing (log parsing, ETL, report generation) where timezone construction is a small fraction of total I/O time, the difference is imperceptible regardless of which option you choose.
- For short-lived scripts and command-line tools, `DateTime::Lite` wins on both startup time (~120 ms vs ~320 ms) and memory (~19 MB vs ~28 MB).
## Running the benchmark
A self-contained benchmark script is included in the distribution:
cd DateTime-Lite-vX.X.X
perl Makefile.PL && make # make sure the XS code is compiled
perl -Iblib/lib -Iblib/arch scripts/benchmark.pl
# More iterations for stable numbers:
perl -Iblib/lib -Iblib/arch scripts/benchmark.pl --iterations 50000
# Machine-readable CSV output:
perl -Iblib/lib -Iblib/arch scripts/benchmark.pl --csv > results.csv
# USAGE
## 0-based Versus 1-based Numbers
`DateTime::Lite` follows a simple rule for 0-based vs. 1-based numbers.
Month, day of month, day of week, and day of year are **1-based**. Every 1-based method also has a `_0` variant. For example, `day_of_week` returns 1 (Monday) through 7 (Sunday), while `day_of_week_0` returns 0 through 6.
All _time_-related values (hour, minute, second) are **0-based**.
Years are neither, as they can be positive or negative. There is a year 0.
There is no `quarter_0` method.
## Floating DateTimes
The default time zone for new `DateTime::Lite` objects (except where stated otherwise) is the `floating` time zone. This concept comes from the iCal standard. A floating datetime is not anchored to any particular time zone and does not include leap s...
Date math and comparison between a floating datetime and one with a real time zone produce results of limited validity, because one includes leap seconds and the other does not.
If you plan to use objects with a real time zone, it is strongly recommended that you **do not** mix them with floating datetimes.
## Determining the Local Time Zone Can Be Slow
If `$ENV{TZ}` is not set, looking up the local time zone may involve reading several files in `/etc`. If you know the local time zone will not change during your program's lifetime and you need many objects for that zone, cache it once:
my $local_tz = DateTime::Lite::TimeZone->new( name => 'local' );
my $dt = DateTime::Lite->new( ..., time_zone => $local_tz );
`DateTime::Lite::TimeZone` also provides a process-level cache that eliminates
this cost entirely:
DateTime::Lite::TimeZone->enable_mem_cache;
my $dt = DateTime::Lite->new( ..., time_zone => 'local' );
## Far Future DST
For dates very far in the future (thousands of years from now), `DateTime` with named time zones can consume large amounts of memory because `DateTime::TimeZone` pre-computes all DST transitions from the present to that
date.
`DateTime::Lite` is not affected by this problem. `DateTime::Lite::TimeZone` uses a compact SQLite database and a POSIX footer TZ string to derive the correct offset for any future date without expanding the full transition table.
## Globally Setting a Default Time Zone
**Warning: this is very dangerous. Use at your own risk.**
You can force `DateTime::Lite` to use a specific default time zone by setting:
$ENV{PERL_DATETIME_DEFAULT_TZ} = 'America/New_York';
This affects all code that creates a `DateTime::Lite` object, including any CPAN modules you use. Audit your dependencies before using this in production.
## Upper and Lower Bounds
Internally, dates are stored as the number of days before or after `0001-01-01`, held in a Perl integer. The usable range depends on your platform's integer size (`$Config{ivsize}`):
( run in 0.543 second using v1.01-cache-2.11-cpan-71847e10f99 )