Algorithm-ToNumberMunger
view release on metacpan or search on metacpan
t/mungers.t view on Meta::CPAN
is( $d->(undef), -1, 'ip_class default for undef' );
eval { $M->build( { munger => 'ip_class', default => 'x' } ) };
like( $@, qr/'default' must be numeric/, 'ip_class validates default at build time' );
}
# ---- cidr -------------------------------------------------------------------
{
my $c = $M->build(
{
munger => 'cidr',
nets => [ '10.10.0.0/16', '10.0.0.0/8', '2001:db8:5::/48' ],
default => -1,
}
);
is( $c->('10.10.3.4'), 0, 'cidr: most-specific first match wins' );
is( $c->('10.99.0.1'), 1, 'cidr: falls through to the wider net' );
is( $c->('2001:db8:5::7'), 2, 'cidr: v6 net matches' );
is( $c->('2001:db8:6::7'), -1, 'cidr: v6 outside the /48 takes default' );
is( $c->('192.168.1.1'), -1, 'cidr: unmatched v4 takes default' );
is( $c->('not-an-ip'), -1, 'cidr: garbage takes default' );
# a v4 address is never tested against v6 nets (and vice versa)
my $v6only = $M->build( { munger => 'cidr', nets => ['::/0'], default => -1 } );
is( $v6only->('8.8.8.8'), -1, 'cidr: ::/0 does not swallow v4' );
my $v4any = $M->build( { munger => 'cidr', nets => ['0.0.0.0/0'], default => -1 } );
is( $v4any->('8.8.8.8'), 0, 'cidr: 0.0.0.0/0 matches any v4' );
my $strict = $M->build( { munger => 'cidr', nets => ['10.0.0.0/8'] } );
eval { $strict->('192.168.1.1') };
like( $@, qr/none of the listed networks/, 'cidr croaks on no match without default' );
eval { $strict->('nope') };
like( $@, qr/not a parseable IP address/, 'cidr croaks on garbage without default' );
eval { $M->build( { munger => 'cidr', nets => [] } ) };
like( $@, qr/non-empty 'nets'/, 'cidr rejects empty nets' );
eval { $M->build( { munger => 'cidr', nets => ['10.0.0.0'] } ) };
like( $@, qr/'address\/prefix' form/, 'cidr rejects a bare address' );
eval { $M->build( { munger => 'cidr', nets => ['10.0.0.0/33'] } ) };
like( $@, qr/prefix length must be 0-32/, 'cidr rejects an oversized v4 prefix' );
eval { $M->build( { munger => 'cidr', nets => ['wat/8'] } ) };
like( $@, qr/unparseable address/, 'cidr rejects an unparseable net address' );
}
# ---- datetime -------------------------------------------------------------
SKIP: {
eval { require Time::Piece; 1 }
or skip 'Time::Piece not available', 3;
my $ep = $M->build( { munger => 'datetime', format => '%Y-%m-%dT%H:%M:%S', part => 'epoch' } );
# 2026-07-06T12:00:00 UTC via strptime (Time::Piece strptime is UTC)
my $t = Time::Piece->strptime( '2026-07-06T12:00:00', '%Y-%m-%dT%H:%M:%S' );
is( $ep->('2026-07-06T12:00:00'), $t->epoch, 'datetime epoch part' );
my $hr = $M->build( { munger => 'datetime', format => '%Y-%m-%dT%H:%M:%S', part => 'hour' } );
is( $hr->('2026-07-06T12:00:00'), 12, 'datetime hour part' );
my $fd = $M->build( { munger => 'datetime', format => '%Y-%m-%dT%H:%M:%S', part => 'frac_day' } );
is( $fd->('2026-07-06T12:00:00'), 0.5, 'datetime frac_day at noon' );
my $fw = $M->build( { munger => 'datetime', format => '%Y-%m-%dT%H:%M:%S', part => 'frac_week' } );
# 2026-07-05 is a Sunday (wday 0): midnight Sunday is the week origin.
is( $fw->('2026-07-05T00:00:00'), 0, 'datetime frac_week at Sunday midnight' );
# 2026-07-06 is Monday (wday 1) noon: (1*86400 + 43200)/604800.
is( $fw->('2026-07-06T12:00:00'), ( 86400 + 43200 ) / 604800, 'datetime frac_week at Monday noon' );
# cyclic parts: noon is frac_day 0.5 -> sin(pi)=0, cos(pi)=-1.
my $sd = $M->build( { munger => 'datetime', format => '%Y-%m-%dT%H:%M:%S', part => 'sin_day' } );
my $cd = $M->build( { munger => 'datetime', format => '%Y-%m-%dT%H:%M:%S', part => 'cos_day' } );
ok( abs( $sd->('2026-07-06T12:00:00') - 0 ) < 1e-9, 'sin_day at noon ~ 0' );
ok( abs( $cd->('2026-07-06T12:00:00') + 1 ) < 1e-9, 'cos_day at noon ~ -1' );
# continuity across midnight: sin/cos barely move over a one-minute wrap.
# a 60s wrap moves sin by ~2*pi*(60/86400) ~ 0.0044, vs the ~1.0 jump
# frac_day would show at the same seam.
my $near_mid_a = $sd->('2026-07-06T23:59:30');
my $near_mid_b = $sd->('2026-07-07T00:00:30');
ok( abs( $near_mid_a - $near_mid_b ) < 1e-2, 'sin_day is continuous across midnight' );
my $cw = $M->build( { munger => 'datetime', format => '%Y-%m-%dT%H:%M:%S', part => 'cos_week' } );
# Sunday midnight is frac_week 0 -> cos(0) = 1.
ok( abs( $cw->('2026-07-05T00:00:00') - 1 ) < 1e-9, 'cos_week at week origin ~ 1' );
} ## end SKIP:
# ---- hash (XS or PP; both must agree with these fixed FNV-1a values) -------
{
diag( 'hash munger path: HAVE_XS = ' . $Algorithm::ToNumberMunger::HAVE_XS );
my $raw = $M->build( { munger => 'hash' } );
# Known FNV-1a 32-bit vectors.
is( $raw->(''), 2166136261, 'fnv1a empty string' );
is( $raw->('a'), 3826002220, 'fnv1a "a"' );
is( $raw->('foobar'), 3214735720, 'fnv1a "foobar"' );
my $b = $M->build( { munger => 'hash', buckets => 100 } );
ok( $b->('anything') >= 0 && $b->('anything') < 100, 'hash respects buckets' );
# Same key + same bucket count is stable; seed decorrelates.
my $s0 = $M->build( { munger => 'hash', buckets => 1_000_000, seed => 0 } );
my $s7 = $M->build( { munger => 'hash', buckets => 1_000_000, seed => 7 } );
isnt( $s0->('shared'), $s7->('shared'), 'seed changes the hash' );
eval { $M->build( { munger => 'hash', buckets => 0 } ) };
like( $@, qr/positive integer/, 'hash rejects zero buckets' );
}
# ---- chain ------------------------------------------------------------------
{
my $tld = $M->build(
{
munger => 'chain',
steps => [ { op => 'lc' }, { op => 'split', on => '.', index => -1 } ],
then => { munger => 'length' },
}
);
is( $tld->('WWW.Example.COM'), 3, 'chain: lc + split[-1] + length takes the last label' );
is( $tld->('nodots'), 6, 'chain: split with no separator keeps the whole string' );
is( $tld->(undef), 0, 'chain: undef enters as the empty string' );
my $oob = $M->build(
{
munger => 'chain',
steps => [ { op => 'split', on => '.', index => 5 } ],
then => { munger => 'length' },
}
);
is( $oob->('a.b'), 0, 'chain: out-of-range split index yields empty' );
my $cap = $M->build(
{
munger => 'chain',
steps => [ { op => 'capture', pattern => 'req-(0x[0-9a-f]+)', ignore_case => 1 } ],
then => { munger => 'num', base => 16 },
}
);
is( $cap->('REQ-0x2F done'), 47, 'chain: capture feeds num base 16' );
my $nogroup = $M->build(
{
munger => 'chain',
steps => [ { op => 'capture', pattern => 'xyz' } ],
then => { munger => 'length' },
( run in 1.379 second using v1.01-cache-2.11-cpan-7fcb06a456a )