Algorithm-ToNumberMunger
view release on metacpan or search on metacpan
t/mungers-datetime-strptime.t view on Meta::CPAN
#!perl
# The datetime munger's strptime path, exercised across the common real-world
# stamp formats. Every format here is deliberately NOT fast-eligible (it uses a
# code outside %Y %m %d %H %M %S, or omits one of the six), so parsing MUST go
# through Time::Piece->strptime -- and we prove that two ways: white-box, by
# asserting _compile_fast_format rejects the format, and black-box, by counting
# actual Time::Piece::strptime calls through a wrapper.
use 5.006;
use strict;
use warnings;
use Test::More;
BEGIN {
eval { require Time::Piece; 1 }
or plan skip_all => 'Time::Piece not available';
}
use Algorithm::ToNumberMunger;
my $M = 'Algorithm::ToNumberMunger';
# Count real strptime calls. The munger resolves Time::Piece->strptime at call
# time, so wrapping the glob here is seen by code inside Mungers.pm too.
my $STRPTIME_CALLS = 0;
{
no warnings 'redefine';
my $orig = \&Time::Piece::strptime;
*Time::Piece::strptime = sub { $STRPTIME_CALLS++; goto &$orig };
}
# One reference instant expressed in many notations: Sunday 2026-07-05
# 13:37:42 UTC. The expected numbers are literals (not re-derived via
# Time::Piece) so the module is never its own oracle.
my $EPOCH = 1783258662;
# Each row: [ description, format, stamp, { part => expected } ].
my @FORMATS = (
[
'Apache CLF access log (%b month name)',
'%d/%b/%Y:%H:%M:%S',
'05/Jul/2026:13:37:42',
{
epoch => $EPOCH,
year => 2026,
mon => 7,
mday => 5,
hour => 13,
min => 37,
sec => 42,
wday => 0
},
],
[
'RFC 2822 email Date header (%a weekday, %b month)',
'%a, %d %b %Y %H:%M:%S',
'Sun, 05 Jul 2026 13:37:42',
{ epoch => $EPOCH, hour => 13, wday => 0 },
],
[
'RFC 7231 HTTP-date (fixed GMT literal)',
'%a, %d %b %Y %H:%M:%S GMT',
'Sun, 05 Jul 2026 13:37:42 GMT',
{ epoch => $EPOCH, hour => 13 },
],
[
'RFC 850 cookie date (%A full weekday, %y two-digit year)',
'%A, %d-%b-%y %H:%M:%S', 'Sunday, 05-Jul-26 13:37:42',
{ epoch => $EPOCH, year => 2026 }, # POSIX rule: 26 => 2026
],
[
'asctime/ctime (%e space-padded day)',
'%a %b %e %H:%M:%S %Y',
'Sun Jul 5 13:37:42 2026',
{ epoch => $EPOCH, mday => 5 },
],
[
'US 12-hour clock (%I with %p AM/PM)',
'%m/%d/%Y %I:%M:%S %p',
'07/05/2026 01:37:42 PM',
{ epoch => $EPOCH, hour => 13 },
],
[
'ISO 8601 with numeric zone (%z, offset applied)', '%Y-%m-%dT%H:%M:%S%z',
'2026-07-05T15:37:42+0200', { epoch => $EPOCH, wday => 0 },
],
[ 'two-digit year ISO-ish (%y)', '%y-%m-%d %H:%M:%S', '26-07-05 13:37:42', { epoch => $EPOCH, year => 2026 }, ],
[
'syslog RFC 3164 (%b, no year -- strptime defaults it to 1970)',
'%b %d %H:%M:%S',
'Jul 05 13:37:42',
{ epoch => 16033062, year => 1970, mon => 7, mday => 5, hour => 13 },
],
[
'European dotted, minute precision (no %S)',
'%d.%m.%Y %H:%M',
'05.07.2026 13:37',
{ epoch => $EPOCH - 42, sec => 0, hour => 13 },
],
[
'ISO date-only (no time codes)', '%Y-%m-%d',
'2026-07-05', { epoch => 1783209600, mday => 5, hour => 0, frac_day => 0 },
],
[ 'ISO 8601 basic compact date (%Y%m%d)', '%Y%m%d', '20260705', { epoch => 1783209600, mon => 7 }, ],
[ 'epoch seconds (%s)', '%s', "$EPOCH", { epoch => $EPOCH, year => 2026, hour => 13 }, ],
);
for my $row (@FORMATS) {
my ( $desc, $format, $stamp, $expect ) = @$row;
# White-box guarantee: the fast engine must refuse this format, leaving
# strptime as the only way the munger can parse it.
is( Algorithm::ToNumberMunger::_compile_fast_format($format),
undef, "$desc: format is not fast-eligible" );
for my $part ( sort keys %$expect ) {
my $code = $M->build( { munger => 'datetime', format => $format, part => $part } );
$STRPTIME_CALLS = 0;
my $got = $code->($stamp);
ok( $STRPTIME_CALLS > 0, "$desc: $part parse called strptime" );
ok( abs( $got - $expect->{$part} ) < 1e-9, "$desc: $part value" )
or diag("got $got, expected $expect->{$part}");
# The one-slot memo must serve a repeat without re-parsing, and a new
# stamp must go back through strptime rather than being served stale.
$STRPTIME_CALLS = 0;
is( $code->($stamp), $got, "$desc: $part memo repeat value" );
is( $STRPTIME_CALLS, 0, "$desc: $part memo repeat skips strptime" );
} ## end for my $part ( sort keys %$expect )
} ## end for my $row (@FORMATS)
# ---- same instant, every full-precision notation => one epoch ---------------
# All the formats above that carry full date+time+zone information describe the
# identical moment, so their epochs must agree exactly across notations.
{
my @full = grep { exists $_->[3]{epoch} && $_->[3]{epoch} == $EPOCH } @FORMATS;
cmp_ok( scalar @full, '>=', 7, 'several full-precision notations to compare' );
for my $row (@full) {
my ( $desc, $format, $stamp ) = @$row;
my $code = $M->build( { munger => 'datetime', format => $format, part => 'epoch' } );
is( $code->($stamp), $EPOCH, "epoch agrees across notations: $desc" );
}
}
# ---- leap day through strptime ----------------------------------------------
{
my $ep = $M->build(
{
munger => 'datetime',
format => '%d/%b/%Y:%H:%M:%S',
part => 'epoch'
}
);
my $yd = $M->build(
{
munger => 'datetime',
format => '%d/%b/%Y:%H:%M:%S',
part => 'yday'
}
);
$STRPTIME_CALLS = 0;
is( $ep->('29/Feb/2024:23:59:59'), 1709251199, 'leap-day epoch' );
is( $yd->('29/Feb/2024:23:59:59'), 59, 'leap-day yday' );
ok( $STRPTIME_CALLS >= 2, 'leap-day stamps went through strptime' );
( run in 1.685 second using v1.01-cache-2.11-cpan-0b5f733616e )