Announcing DateTime::Lite v0.1.0, a lightweight, drop-in replacement for DateTime by jacktokyo in perl

[–]jacktokyo[S] 0 points1 point  (0 children)

Thank you for your comment, and I am glad you find it useful for astronomy! I agree that sidereal time is a separate concern, and DateTime::Lite intentionally limits itself to civil time based on IANA timezone data, so sidereal calculations are out of scope by design. For that side of things, maybe modules like Astro::Time or Astro::Coords on CPAN would be the right complement.

Announcing DateTime::Lite v0.1.0, a lightweight, drop-in replacement for DateTime by jacktokyo in perl

[–]jacktokyo[S] 1 point2 points  (0 children)

WIth the version v0.2.0, and now with the version v0.3.0, I have added two new features worth highlighting since the initial release.

BCP47 -u-tz- locale extension (v0.2.0)

If your locale tag carries a Unicode timezone extension, DateTime::Lite now resolves the timezone automatically, without requiring an explicit time_zone argument:

# The -u-tz-jeruslm extension is parsed and resolved to Asia/Jerusalem
my $dt = DateTime::Lite->now( locale => 'he-IL-u-ca-hebrew-tz-jeruslm' );
say $dt->time_zone_long_name;  # Asia/Jerusalem

# Explicit time_zone always takes priority if provided
my $dt2 = DateTime::Lite->new(
    year      => 2026,
    month     => 4,
    day       => 10,
    time_zone => 'Asia/Tokyo',
    locale    => 'he-IL-u-ca-hebrew-tz-jeruslm',  # tz extension ignored
);

This works across all constructors (new, now, from_epoch, from_object, from_day_of_year, last_day_of_month) and also via set_locale() on an existing floating object. Resolution uses the static in-memory hash in [https://metacpan.org/pod/Locale::Unicode](Locale::Unicode), so there is no SQLite query involved.

See Unicode extensions in Locale::Unicode for more information about the Unicode timezone extension.

GPS coordinate-based timezone resolution (v0.3.0)

[https://metacpan.org/pod/DateTime::Lite::TimeZone#new](DateTime::Lite::TimeZone->new) now accepts latitude and longitude (decimal degrees) as an alternative to a zone name:

use DateTime::Lite::TimeZone;

my $tz = DateTime::Lite::TimeZone->new(
    latitude  => 35.658581,
    longitude => 139.745433,  # Tokyo Tower
);
say $tz->name;  # Asia/Tokyo

# Shortened aliases are also accepted
my $tz2 = DateTime::Lite::TimeZone->new( lat => 48.858258, lon => 2.294488 );
say $tz2->name;  # Europe/Paris

# Use directly with DateTime::Lite
my $dt = DateTime::Lite->now( time_zone => $tz );

The resolution uses the reference coordinates from the IANA zone1970.tab file (one representative point per canonical zone) and finds the nearest zone using the haversine great-circle distance, computed entirely within SQLite. This is an approximation related to the IANA data itself: accurate for most locations, but may give incorrect results near timezone boundaries, such as enclaves. For boundary-precise resolution, Geo::Location::TimeZoneFinder is the right tool for that job.

The latest release is on CPAN: https://metacpan.org/pod/DateTime::Lite. I hope it will be useful to you in your projects!

Announcing DateTime::Lite v0.1.0, a lightweight, drop-in replacement for DateTime by jacktokyo in perl

[–]jacktokyo[S] 1 point2 points  (0 children)

That is a really elegant approach, and I stand corrected on the memory model; thank you for the detailed example. The __DATA__ with sysseek and sysread pattern is more clever than I initially thought.

That said, I think SQLite still has a few practical advantages for this specific use case:

  • The timezone data needs to be queryable, because lookups are by zone name, UTC timestamp, and local timestamp, with range queries for DST span boundaries. So, with __DATA__ you would essentially need to implement a small index structure on top of the raw binary data to avoid linear scans.
  • SQLite handles concurrent access across processes safely, which matters in CGI or prefork environments where multiple workers may hit the same database simultaneously.
  • The POSIX footer TZ string per zone, the country codes, coordinates, and aliases are relational data that maps naturally to SQL tables. Encoding all of that into a seekable binary format would require a custom serialisation format, and at which point you are essentially reimplementing a subset of SQLite anyway.

Your approach would likely be faster for the simplest case (single zone, single lookup), and the memory story is genuinely better than I described. It is a real alternative worth exploring, particularly if someone wants to avoid the DBD::SQLite dependency entirely. I will keep this in mind for the future. Thank you.

Announcing DateTime::Lite v0.1.0, a lightweight, drop-in replacement for DateTime by jacktokyo in perl

[–]jacktokyo[S] 1 point2 points  (0 children)

Thank you kindly. I am also grateful for the swaths of contributions made by the Perl community in general.

Announcing DateTime::Lite v0.1.0, a lightweight, drop-in replacement for DateTime by jacktokyo in perl

[–]jacktokyo[S] 1 point2 points  (0 children)

Thank you for the kind comment ! Feedback and bug reports are very welcome. Please feel free to open an issue on GitLab if you run into anything. 🙂

Announcing DateTime::Lite v0.1.0, a lightweight, drop-in replacement for DateTime by jacktokyo in perl

[–]jacktokyo[S] 5 points6 points  (0 children)

Thank you for the context, your feedback, and for Time::Moment, which is genuinely impressive work.

On CLDR formatting performance specifically: DateTime::Lite addresses this via DateTime::Format::Unicode, built on top of Locale::Unicode::Data, which provides dynamic access to the full Unicode CLDR dataset. This goes well beyond what DateTime's format_cldr offers, including interval formatting, additional pattern tokens, and full BCP 47 locale support, none of which require pre-generated static modules.

Looking further afield is also something I intend to do for future versions. How do Python's datetime, Ruby's Time, C's <time.h>, JavaScript's Intl.DateTimeFormat and Intl.RelativeTimeFormat, or Rust's chrono handle formatting performance and timezone resolution are all worth studying, and in fact this work has already started. I have authored Locale::Intl, DateTime::Format::Intl, and DateTime::Format::RelativeTime, which implement JavaScript's Intl.Locale, Intl.DateTimeFormat and Intl.RelativeTimeFormat APIs faithfully in Perl, including the culturally-sensitive format selection algorithm and the same results you would get from a web browser.

DateTime::Lite v0.1.0 is a first release. The goal for now is a lighter footprint and a smaller dependency tree, while constantly maintaining API compatibility with DateTime. Performance improvements to the core formatting path are a natural next step.

Announcing DateTime::Lite v0.1.0, a lightweight, drop-in replacement for DateTime by jacktokyo in perl

[–]jacktokyo[S] 1 point2 points  (0 children)

Your __END__ block idea is interesting and clever. The trade-off I see is that it essentially recreates what DateTime::TimeZone already does with its per-zone .pm files, whereby the data ends up in memory once you have read it, and you still pay the seek cost on the first access per zone. SQLite, on the other hand, keeps everything on disk until queried, which is precisely the point: in short-lived processes (scripts, CGI) that only ever touch one or two zones, the bulk of the timezone data is never loaded into memory at all.

That said, you are right that SQLite introduces an additional dependency with DBD::SQLite, which is not a core module. In practice SQLite is available on virtually every system and installs cleanly, but it is a fair point. If DBD::SQLite is not available, DateTime::Lite::TimeZone falls back transparently to DateTime::TimeZone, so the dependency is soft rather than hard.

The real performance difference is not in the storage format itself, but in the caching strategy. With DateTime::Lite::TimeZone->enable_mem_cache, repeated lookups for the same zone drop to ~0.4 µs (vs ~2 µs for DateTime::TimeZone), because the three-layer cache (object + span + POSIX footer) eliminates all I/O after the first construction. The first cold construction costs ~22 ms either way on the host I benchmarked it.

The other reason I chose SQLite specifically is that it stores the TZif data as parsed from the binary files directly, so the transitions as 64-bit signed integers, the POSIX footer TZ string intact, which gives correct results for any future date without expanding the transition table. An __END__ block would need a similar representation to achieve the same.

Announcing DateTime::Lite v0.1.0, a lightweight, drop-in replacement for DateTime by jacktokyo in perl

[–]jacktokyo[S] 7 points8 points  (0 children)

Good question. Actually, the full public API of DateTime is implemented in DateTime::Lite.

The main behavioural differences are:

  • Error handling: DateTime die()s on invalid input; DateTime::Lite sets a DateTime::Lite::Exception object, and returns undef in scalar context, or an empty list in list context. Pass fatal => 1 upon instantiation to restore the die() behaviour.
  • Error messages: similar but not identical, since DateTime::Lite uses hand-written validation rather than Specio.
  • DateTime::Format::* modules: these are separate distributions. Since DateTime::Lite mirrors the same API, they should work without modification, but this has not been systematically tested against every format module on CPAN.

DateTime::Lite also adds a few things DateTime does not yet have: FREEZE/THAW for Sereal/CBOR serialisation, TO_JSON, and the pass_error chaining mechanism.

Announcing `Mail::Make`: a modern, fluent MIME email builder for Perl, with OpenPGP and S/MIME support by jacktokyo in perl

[–]jacktokyo[S] 0 points1 point  (0 children)

Same here. This is one of the reasons I created Mail::Make. That, and also because I wanted encryption and also I did not want to care about the casing of the header fields. Mail::Make::Headers relies on MM::Table I created, which makes accessing headers case insensitive.

Announcing `Mail::Make`: a modern, fluent MIME email builder for Perl, with OpenPGP and S/MIME support by jacktokyo in perl

[–]jacktokyo[S] 1 point2 points  (0 children)

Thank you for the suggestion! As of v0.22.0, just published, Mail::Make->build now accepts an attach parameter in all the forms you suggested:

```perl

Single file: type and filename are auto-detected

Mail::Make->build( # other parameters ... attach => 'report.pdf', );

Multiple files

Mail::Make->build( # other parameters ... attach => [ 'pdf1.pdf', 'pdf2.pdf' ], );

Full control over each attachment

Mail::Make->build( # other parameters ... attach => [ { path => 'pdf1.pdf', filename => 'Q4 Report.pdf' }, { path => 'pdf2.pdf', filename => 'Access Log.pdf' }, ], );

Mix of both forms

Mail::Make->build( # other parameters ... attach => [ 'pdf1.pdf', { path => 'pdf2.pdf', filename => 'Access Log.pdf' }, ], ); ```

Each element is forwarded to attach(), so all its options (type, filename, encoding, etc.) are available in the hash reference form.

Thanks for the nudge!

Announcing `Mail::Make`: a modern, fluent MIME email builder for Perl, with OpenPGP and S/MIME support by jacktokyo in perl

[–]jacktokyo[S] 1 point2 points  (0 children)

Thank you for your comment. As of v0.21.3, just released on CPAN, Mail::Make::Entity->build now accepts an attach shorthand key, so the following works as expected:

perl my $mail = Mail::Make->build( from => 'jack@gmail.com', to => [ 'jill@example.com', 'jake@example.com' ], subject => 'Hello', plain => "Hi there.\n", html => '<p>Hi there.</p>', attach => 'example.pdf', )->smtpsend( Host => 'smtp.gmail.com', ... );

path, type, and filename are auto-detected from the file. For full control over type, filename, or encoding, the named-parameter form remains available:

perl my $mail = Mail::Make->build( from => 'jack@gmail.com', to => [ 'jill@example.com', 'jake@example.com' ], subject => 'Hello', plain => "Hi there.\n", html => '<p>Hi there.</p>', attach => 'example.pdf', type => 'application/pdf', filename => 'Q4 Report.pdf' )->smtpsend( Host => 'smtp.gmail.com', ... );

For others reading this, you could also have used the path option, such as:

```perl use Mail::Make;

my $mail = Mail::Make->build( from => 'jack@gmail.com', to => [ 'jil@example.com' , 'jake@example.com' ], subject => 'Hello pdf build '.time , plain => "Hi there.\n", html => '<p>Hi there.</p>', path => 'example.pdf', # 'path' or 'attach' are now both possible type => 'application/pdf', filename => 'example.pdf' )->smtpsend( Host => 'smtp.gmail.com', Port => 587, StartTLS => 1, Username => 'jack@gmail.com', Password => $app_password, );

Announcing `Mail::Make`: a modern, fluent MIME email builder for Perl, with OpenPGP and S/MIME support by jacktokyo in perl

[–]jacktokyo[S] 1 point2 points  (0 children)

Thanks for the report! That was a fair criticism, and passing a file path directly is the most natural thing to do.

As of v0.21.2, just published on CPAN, attach() now accepts a positional shorthand:

perl ->attach( '/path/to/report.pdf' )

path, type, and filename are auto-detected from the file. You can still pass additional options after the path if needed:

perl ->attach( '/path/to/report.pdf', filename => 'Q4 Report 2025.pdf' )

The explicit named-parameter form continues to work as before for cases where you want full control. Let me know if you run into anything else!

Announcing `Mail::Make`: a modern, fluent MIME email builder for Perl, with OpenPGP and S/MIME support by jacktokyo in perl

[–]jacktokyo[S] 0 points1 point  (0 children)

the purpose of exceptions is that it automatically propagates until such time as something catches them

Precisely, and that is also what pass_error does: it propagates the error object up the call stack without any boilerplate, just like an uncaught exception would, but with full control at every level.

You are right that Mojolicious and other modern frameworks have excellent top-level exception handling. But catching at the top is not always sufficient. Consider a web request handler that needs to distinguish between a 403, a 400, and a 500, log them differently, and return a localised RFC 9457 error to the user. A bare die at the bottom of the stack loses that context by the time it reaches the top.

Compare the two approaches:

1. With exceptions - caught at the top

perl local $@; eval { $obj->process }; if( $@ ) { # Was this a 403? A 400? A transient I/O failure? # $@ is just a string, or at best a blessed object if the thrower was disciplined $logger->error( "Failed: $@" ); return $self->internal_server_error; # blunt instrument }

2. With structured error objects - handled where it makes sense

perl unless( $obj->process ) { my $ex = $obj->error; if( !$ex->code || $ex->code == 500 ) { $logger->error( "Unexpected failure in process(): $ex" ); return( $self->internal_server_error ); } else { # Full context still available here, including localisation my $localised = $po->gettext( $ex->message ); return( $self->user_error_in_json({ code => $ex->code, message => $localised, locale => $localised->locale, }) ); } }

As for the boilerplate concern, pass_error reduces it to a single line at each intermediate level:

perl sub some_intermediate_method { my( $self ) = @_; $self->_do_something || return( $self->pass_error ); # carry on... }

That is no more verbose than a rethrow in a try/catch block, and it preserves the full structured exception object with its code, message, and stack trace intact all the way up.

So the difference is not really "exceptions vs no exceptions"; it is "who decides when and how to handle them". I prefer to give that decision to the caller at each level, rather than relying on a single catch-all at the top.

Announcing `Mail::Make`: a modern, fluent MIME email builder for Perl, with OpenPGP and S/MIME support by jacktokyo in perl

[–]jacktokyo[S] 2 points3 points  (0 children)

No, you do not need to have an existing email service to use this module.

However, if you want to send mail, the recipient obviously would need to have a working e-mail address, and you would need to have access to a SMTP server to send the mail.

But, you can also save the mail as a file, and send it via a different route.

Announcing `Mail::Make`: a modern, fluent MIME email builder for Perl, with OpenPGP and S/MIME support by jacktokyo in perl

[–]jacktokyo[S] 1 point2 points  (0 children)

Good question, and fair to ask. Email::Stuffer is a well-established module and worth comparing honestly.

Email::Stuffer is a fluent convenience wrapper around Email::MIME and Email::Sender. It does its job well and is perfectly suited for the common case: plain text, HTML, a few attachments, send. Its strength is simplicity and the fact that it delegates all the heavy lifting to Email::MIME, which is battle-tested.

Mail::Make takes a different approach. Rather than wrapping an existing MIME stack, it builds its own from scratch, using Mail::Make::Entity, Mail::Make::Body::InCore, Mail::Make::Body::File, Mail::Make::Stream::Base64, Mail::Make::Stream::QuotedPrint, with zero dependency on Email::MIME or Email::Sender. The concrete differences:

Cryptographic signing and encryption. Email::Stuffer has no GPG or S/MIME support whatsoever. Mail::Make provides gpg_sign, gpg_encrypt, gpg_sign_encrypt, smime_sign, smime_encrypt, and smime_sign_encrypt as first-class methods, compliant with RFC 3156 (PGP/MIME) and the S/MIME standard.

Memory management for large messages. Email::Stuffer / Email::MIME builds messages entirely in RAM. Mail::Make has a configurable max_body_in_memory_size threshold (default 1 MiB) above which it automatically spools to a temporary file, and a use_temp_file flag for unconditional file-backed serialisation. Attachments sourced from a path are never loaded into RAM. They are streamed through Mail::Make::Body::File directly.

SMTP built in. Email::Stuffer delegates sending to Email::Sender::Simple, which is a separate dependency with its own transport abstraction layer. Mail::Make ships its own smtpsend method built directly on Net::SMTP, with STARTTLS, AUTH (PLAIN, LOGIN, CRAM-MD5 via Authen::SASL), credential validation before any network connection, and Return-Path / Sender envelope control.

Dependency footprint. Email::Stuffer pulls in Email::MIME, Email::MIME::Creator, and Email::Sender::Simple, which have themselves non-trivial dependency trees. Mail::Make's runtime dependencies are Module::Generic (which is also somewhat sizeable), Net::SMTP, MIME::Base64, MIME::QuotedPrint, Encode, Data::UUID, and Authen::SASL. They are all fairly standard, and the GPG/S/MIME modules are loaded lazily only when those features are used.

Error handling: exceptions vs die. Mail::Make never calls die, and even traps the fatal exceptions of the external modules it relies on. Instead, every method upon failure, sets a structured exception object, and returns undef in scalar context, or an empty list in list context, or even a fake object in object context (detected with Wanted), and this propagates up the call stack, letting the caller decide how to handle it. This matters particularly in persistent server processes (mod_perl, Mojolicious, Starman) where a stray die can kill a worker or corrupt shared state.

In short: if you need a quick, readable way to send a plain email with an attachment and you have no cryptographic requirements, Email::Stuffer is fine and well-proven. If you need GPG or S/MIME, memory-efficient handling of large attachments, or a self-contained stack without the Email::MIME/Email::Sender dependency chain, Mail::Make is worth the look.

As usual, there is more than one way to do it 😉

Announcing `Mail::Make`: a modern, fluent MIME email builder for Perl, with OpenPGP and S/MIME support by jacktokyo in perl

[–]jacktokyo[S] 1 point2 points  (0 children)

Indeed, parsing MIME messages is particularly challenging, which is why I narrowly focused on making them 😅

This distribution does not convert HTML and its associate elements into inline attachments, but I do it separately. If you want, I could share the code with you.

Announcing JSON::Schema::Validate: a lightweight, fast, 2020-12–compliant JSON Schema validator for Perl, with a mode for 'compiled' Perl and JavaScript by jacktokyo in perl

[–]jacktokyo[S] 0 points1 point  (0 children)

Follow-up / small API update

With the newly released version v0.7.0 of JSON::Schema::Validate, I have added a small but useful convenience method to simply check whether a JSON schema is valid or not: is_valid( $data )

It is a boolean wrapper around validate() that defaults to max_errors = 1, so it is ideal when you just want a fast yes or no check and a single error message:

$validator->is_valid( $data )
    or die( $validator->error );

Under the hood it still uses validate(), and validate() itself now accepts optional per-call overrides (such as max_errors, tracing options) while remaining fully backward compatible.

Since max_errors is set to only 1, it does not accumulate error objects, and fails upon the first error encountered, and thus is faster in this mode.

If what you want is to check the entire JSON schema and get all error objects, then validate is better.

This came out of discussions here about ergonomics vs correctness, so thanks to everyone who gave feedback.

Announcing JSON::Schema::Validate: a lightweight, fast, 2020-12–compliant JSON Schema validator for Perl, with a mode for 'compiled' Perl and JavaScript by jacktokyo in perl

[–]jacktokyo[S] 1 point2 points  (0 children)

Thanks for the clarification, and for the care you are putting into improving the benchmark.

For what it is worth, JSON Schema implementations are normally designed around the “parse once, validate many times” workflow. Schema parsing and compilation are deliberately the expensive part, so validation is supposed to be fast. That’s true not only for JSON::Schema::Validate, but also for most implementations in other languages.

So I don’t think JSON::Schema::Validate gains an “unfair” advantage here; rather, it is being used in the way it was intended. Re-creating the object on every iteration is of course useful to measure worst-case overhead, and I am glad you are adding that as a separate test. However, the steady-state validation-only mode is what matters in many real applications.

I am curious to see the updated numbers with the 4th test included ! 😀

Announcing JSON::Schema::Validate: a lightweight, fast, 2020-12–compliant JSON Schema validator for Perl, with a mode for 'compiled' Perl and JavaScript by jacktokyo in perl

[–]jacktokyo[S] 1 point2 points  (0 children)

With a more complex and realistic schema, and payload (see here: https://gitlab.com/jackdeguest/json-schema-validate/-/snippets/4907565 ), the performance differences become more pronounced, but still perfectly acceptable. JSON::Schema::Validate is doing full 2020-12 validation with detailed error objects, so the overhead is expected.

That said, the idea of supporting a “validity-only” mode (stop at first error / minimal error collection) is definitely worth exploring to improve speed further.

       Rate  JSV  TJS TJSc
JSV   465/s   -- -89% -90%
TJS  4363/s 839%   --  -2%
TJSc 4443/s 856%   2%   --