The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package main;

use strict;
use warnings;
use utf8;

use Benchmark qw( timethese );
use DateTime::Format::Strptime;
use Try::Tiny;
use Test::More;
use Test::Fatal;

timethese( 100, { test => \&test } );

sub test {
for my $test ( _tests_from_data() ) {
    subtest(
        qq{$test->{name}},
        sub {
            _utf8_output();

            my $parser;
            is(
                exception {
                    $parser = DateTime::Format::Strptime->new(
                        pattern => $test->{pattern},
                        (
                            $test->{locale}
                            ? ( locale => $test->{locale} )
                            : ()
                        ),
                        on_error => 'croak',
                    );
                },
                undef,
                "no exception building parser for $test->{pattern}"
            ) or return;

            # Thursday changed from "Thu" to "Thu." and December went from
            # "Dec" to "Dec." between CLDR versions.
            $test->{input}
                =~ s/AU_THU/DateTime::Locale->load('en-AU')->day_format_abbreviated->[3]/e;
            $test->{input}
                =~ s/AU_DEC/DateTime::Locale->load('en-AU')->month_format_abbreviated->[11]/e;

            ( my $real_input = $test->{input} ) =~ s/\\n/\n/g;

            my $dt;
            is(
                exception { $dt = $parser->parse_datetime( $real_input ) },
                undef,
                "no exception parsing $test->{input}"
            ) or return;

            _test_dt_methods( $dt, $test->{expect} );

            unless ( $test->{skip_round_trip} ) {
                is(
                    $parser->format_datetime($dt),
                    $real_input,
                    'round trip via strftime produces original input'
                );
            }
        }
    );
}

subtest(
    'parsing whitespace',
    sub {
        my $parser = DateTime::Format::Strptime->new(
            pattern  => '%n%Y%t%m%n',
            on_error => 'croak',
        );

        my $dt = $parser->parse_datetime(<<"EOF");
\t
  2015
12
EOF

        my %expect = (
            year  => 2015,
            month => 12,
        );
        _test_dt_methods( $dt, \%expect );
    }
);

subtest(
    'parser time zone is set on returned object',
    sub {
        my $parser = DateTime::Format::Strptime->new(
            pattern   => '%Y %H:%M:%S %Z',
            time_zone => 'America/New_York',
            on_error  => 'croak',
        );

        my $dt     = $parser->parse_datetime('2003 23:45:56 MDT');
        my %expect = (
            year                => 2003,
            hour                => 0,
            minute              => 45,
            second              => 56,
            time_zone_long_name => 'America/New_York',
        );

        _test_dt_methods( $dt, \%expect );
    }
);
}

my $d;
sub _tests_from_data {
    my @tests;

    $d ||= do { local $/; <DATA> };

    my $test_re = qr/
        \[(.+?)\]\n              # test name
        (.+?)\n                  # pattern
        (.+?)\n                  # input
        (?:locale\ =\ (.+?)\n)?  # optional locale
        (skip\ round\ trip\n)?   # skip a round trip?
        (.+?)\n                  # k-v pairs for expected values
        (?:\n|\z)                # end of test
                    /xs;

    while ( $d =~ /$test_re/g ) {
        push @tests, {
            name            => $1,
            pattern         => $2,
            input           => $3,
            locale          => $4,
            skip_round_trip => $5,
            expect          => {
                map { split /\s+=>\s+/ } split /\n/, $6,
            },
        };
    }

    return @tests;
}

sub _utf8_output {
    binmode $_, ':encoding(UTF-8)'
        for map { Test::Builder->new->$_ }
        qw( output failure_output todo_output );
}

sub _test_dt_methods {
    my $dt     = shift;
    my $expect = shift;

    for my $meth ( sort keys %{$expect} ) {
        is(
            $dt->$meth,
            $expect->{$meth},
            "$meth is $expect->{$meth}"
        );
    }
}

__DATA__
[ISO8601]
%Y-%m-%dT%H:%M:%S
2015-10-08T15:39:44
year   => 2015
month  => 10
day    => 8
hour   => 15
minute => 39
second => 44

[date with 4-digit year]
%Y-%m-%d
1998-12-31
year  => 1998
month => 12
day   => 31

[date with 2-digit year]
%y-%m-%d
98-12-31
year  => 1998
month => 12
day   => 31

[date with leading space on month]
%e-%b-%Y
 3-Jun-2010
year  => 2010
month => 6
day   => 3

[year and day of year]
%Y years %j days
1998 years 312 days
year  => 1998
month => 11
day   => 8

[date with abbreviated month]
%b %d %Y
Jan 24 2003
year  => 2003
month => 1
day   => 24

[date with abbreviated month is case-insensitive]
%b %d %Y
jAN 24 2003
skip round trip
year  => 2003
month => 1
day   => 24

[date with full month]
%B %d %Y
January 24 2003
year  => 2003
month => 1
day   => 24

[date with full month is case-insensitive]
%B %d %Y
jAnUAry 24 2003
skip round trip
year  => 2003
month => 1
day   => 24

[24 hour time]
%H:%M:%S
23:45:56
year   => 1
month  => 1
day    => 1
hour   => 23
minute => 45
second => 56

[12 hour time (PM)]
%l:%M:%S %p
11:45:56 PM
year   => 1
month  => 1
day    => 1
hour   => 23
minute => 45
second => 56

[12 hour time (am) and am/pm is case-insensitive]
%l:%M:%S %p
11:45:56 am
skip round trip
year   => 1
month  => 1
day    => 1
hour   => 11
minute => 45
second => 56

[24-hour time]
%T
23:34:45
hour   => 23
minute => 34
second => 45

[12-hour time]
%r
11:34:45 PM
hour   => 23
minute => 34
second => 45

[24-hour time without second]
%R
23:34
hour   => 23
minute => 34
second => 0

[US style date]
%D
11/30/03
year  => 2003
month => 11
day   => 30

[ISO style date]
%F
2003-11-30
year  => 2003
month => 11
day   => 30

[nanosecond with no length]
%H:%M:%S.%N
23:45:56.123456789
hour       => 23
minute     => 45
second     => 56
nanosecond => 123456789

[nanosecond with length of 6]
%H:%M:%S.%6N
23:45:56.123456
hour       => 23
minute     => 45
second     => 56
nanosecond => 123456000

[nanosecond with length of 3]
%H:%M:%S.%3N
23:45:56.123
hour       => 23
minute     => 45
second     => 56
nanosecond => 123000000

[nanosecond with no length but < 9 digits]
%H:%M:%S.%N
23:45:56.543
skip round trip
hour       => 23
minute     => 45
second     => 56
nanosecond => 543000000

[time zone as numeric offset]
%H:%M:%S %z
23:45:56 +1000
hour       => 23
minute     => 45
second     => 56
offset     => 36000

[time zone as abbreviation]
%H:%M:%S %Z
23:45:56 AEST
skip round trip
hour       => 23
minute     => 45
second     => 56
offset     => 36000

[time zone as Olson name]
%H:%M:%S %O
23:45:56 America/Chicago
hour   => 23
minute => 45
second => 56
time_zone_long_name => America/Chicago

[escaped percent]
%Y%%%m%%%d
2015%05%14
year  => 2015
month => 5
day   => 14

[escaped percent followed by letter token]
%Y%%%m%%%d%%H
2015%05%14%H
year  => 2015
month => 5
day   => 14

[every pattern]
%a %b %B %C %d %e %h %H %I %j %k %l %m %M %n %N %O %p %P %S %U %u %w %W %y %Y %s %G %g %z %Z %%Y %%
Wed Nov November 20 05  5 Nov 21 09 309 21  9 11 34 \n 123456789 America/Denver PM pm 45 44 3 3 44 03 2003 1068093285 2003 03 -0700 MST %Y %
year   => 2003
month  => 11
day    => 5
hour   => 21
minute => 34
second => 45
nanosecond => 123456789
time_zone_long_name => America/Denver

[Australian date]
%x
31/12/98
locale = en-AU
skip round trip
year  => 1998
month => 12
day   => 31

[Australian time]
%X
13:34:56
locale = en-AU
skip round trip
hour   => 13
minute => 34
second => 56

[Australian date/time]
%c
AU_THU 31 AU_DEC 1998 13:34:56 AEDT
locale = en-AU
skip round trip
year   => 1998
month  => 12
day    => 31
hour   => 13
minute => 34
second => 56
offset => 39600

[US date]
%x
12/31/1998
locale = en-US
skip round trip
year  => 1998
month => 12
day   => 31

[US time]
%X
01:34:56 PM
locale = en-US
skip round trip
hour   => 13
minute => 34
second => 56

[US date/time]
%c
Thu 31 Dec 1998 01:34:56 PM MST
locale = en-US
skip round trip
year   => 1998
month  => 12
day    => 31
hour   => 13
minute => 34
second => 56
offset => -25200

[UK date]
%x
31/12/98
locale = en-GB
skip round trip
year  => 1998
month => 12
day   => 31

[UK time]
%X
13:34:56
locale = en-GB
skip round trip
hour   => 13
minute => 34
second => 56

[UK date/time]
%c
Thu 31 Dec 1998 13:34:56 GMT
locale = en-GB
skip round trip
year   => 1998
month  => 12
day    => 31
hour   => 13
minute => 34
second => 56
offset => 0

[French (France) date]
%x
31/12/1998
locale = fr-FR
skip round trip
year  => 1998
month => 12
day   => 31

[French (France) time]
%X
13:34:56
locale = fr-FR
skip round trip
hour   => 13
minute => 34
second => 56

[French (France) date/time]
%c
jeu. 31 Déc. 1998 13:34:56 CEST
locale = fr-FR
skip round trip
year   => 1998
month  => 12
day    => 31
hour   => 13
minute => 34
second => 56
offset => 7200

[French (Generic) date]
%x
12/31/98
locale = fr
skip round trip
year  => 1998
month => 12
day   => 31

[French (Generic) time]
%X
13:34:56
locale = fr
skip round trip
hour   => 13
minute => 34
second => 56

[French (Generic) date/time]
%c
jeu. Déc. 31 13:34:56 1998
locale = fr
skip round trip
year   => 1998
month  => 12
day    => 31
hour   => 13
minute => 34
second => 56

[epoch without time zone]
%s
42
epoch => 42
time_zone_long_name => floating

[epoch with time zone]
%s %Z
42 UTC
epoch  => 42
offset => 0

[epoch with nanosecond]
%s %N
42 000000034
epoch => 42
nanosecond => 34