| line | stmt | bran | cond | sub | pod | time | code | 
| 1 |  |  |  |  |  |  |  | 
| 2 |  |  |  |  |  |  | use warnings; | 
| 3 | 5 |  |  | 5 |  | 431447 | use strict; | 
|  | 5 |  |  |  |  | 31 |  | 
|  | 5 |  |  |  |  | 147 |  | 
| 4 | 5 |  |  | 5 |  | 23 |  | 
|  | 5 |  |  |  |  | 9 |  | 
|  | 5 |  |  |  |  | 107 |  | 
| 5 |  |  |  |  |  |  | use constant DEBUG => 0; | 
| 6 | 5 |  |  | 5 |  | 33 |  | 
|  | 5 |  |  |  |  | 8 |  | 
|  | 5 |  |  |  |  | 366 |  | 
| 7 |  |  |  |  |  |  | # ABSTRACT: Parse a date/time string using the best method available | 
| 8 |  |  |  |  |  |  |  | 
| 9 |  |  |  |  |  |  | # VERSION | 
| 10 |  |  |  |  |  |  | our $VERSION = "0.089"; | 
| 11 |  |  |  |  |  |  |  | 
| 12 |  |  |  |  |  |  |  | 
| 13 |  |  |  |  |  |  | use base qw/Exporter/; | 
| 14 | 5 |  |  | 5 |  | 25 | our @EXPORT_OK | 
|  | 5 |  |  |  |  | 8 |  | 
|  | 5 |  |  |  |  | 553 |  | 
| 15 |  |  |  |  |  |  | = qw/datetime parse parse_datetime parse_date new_datetime new_date date/; | 
| 16 |  |  |  |  |  |  |  | 
| 17 |  |  |  |  |  |  | use DateTime; | 
| 18 | 5 |  |  | 5 |  | 3439 | use DateTime::Format::Natural; | 
|  | 5 |  |  |  |  | 2067817 |  | 
|  | 5 |  |  |  |  | 201 |  | 
| 19 | 5 |  |  | 5 |  | 2452 | use DateTime::Format::Flexible; | 
|  | 5 |  |  |  |  | 167091 |  | 
|  | 5 |  |  |  |  | 276 |  | 
| 20 | 5 |  |  | 5 |  | 2541 |  | 
|  | 5 |  |  |  |  | 1007650 |  | 
|  | 5 |  |  |  |  | 105 |  | 
| 21 |  |  |  |  |  |  | # use DateTime::Format::DateParse; # Unfortunately, not as useful to use because of that default "local" time zone business. | 
| 22 |  |  |  |  |  |  | use DateTimeX::Easy::DateParse;    # Using this instead, hrm. | 
| 23 | 5 |  |  | 5 |  | 2332 | use Scalar::Util qw/blessed/; | 
|  | 5 |  |  |  |  | 13 |  | 
|  | 5 |  |  |  |  | 145 |  | 
| 24 | 5 |  |  | 5 |  | 26 | use Carp; | 
|  | 5 |  |  |  |  | 7 |  | 
|  | 5 |  |  |  |  | 191 |  | 
| 25 | 5 |  |  | 5 |  | 25 |  | 
|  | 5 |  |  |  |  | 8 |  | 
|  | 5 |  |  |  |  | 8011 |  | 
| 26 |  |  |  |  |  |  | my $have_ICal; | 
| 27 |  |  |  |  |  |  | eval { | 
| 28 |  |  |  |  |  |  | require DateTime::Format::ICal; | 
| 29 |  |  |  |  |  |  | $have_ICal = 1; | 
| 30 |  |  |  |  |  |  | }; | 
| 31 |  |  |  |  |  |  |  | 
| 32 |  |  |  |  |  |  | my $have_DateManip; | 
| 33 |  |  |  |  |  |  | eval { | 
| 34 |  |  |  |  |  |  | require DateTime::Format::DateManip; | 
| 35 |  |  |  |  |  |  | $have_DateManip = 1; | 
| 36 |  |  |  |  |  |  | }; | 
| 37 |  |  |  |  |  |  | my $natural_parser = DateTime::Format::Natural->new; | 
| 38 |  |  |  |  |  |  |  | 
| 39 |  |  |  |  |  |  | my %_truncate_range = qw/ | 
| 40 |  |  |  |  |  |  | month year | 
| 41 |  |  |  |  |  |  | day month | 
| 42 |  |  |  |  |  |  | hour day | 
| 43 |  |  |  |  |  |  | minute hour | 
| 44 |  |  |  |  |  |  | second minute | 
| 45 |  |  |  |  |  |  | nanosecond second | 
| 46 |  |  |  |  |  |  | /; | 
| 47 |  |  |  |  |  |  | my %_delta_range = ( | 
| 48 |  |  |  |  |  |  | month  => [qw/years months/], | 
| 49 |  |  |  |  |  |  | day    => [qw/months days/], | 
| 50 |  |  |  |  |  |  | hour   => [qw/days hours/], | 
| 51 |  |  |  |  |  |  | minute => [qw/hours minutes/], | 
| 52 |  |  |  |  |  |  | second => [qw/minutes seconds/], | 
| 53 |  |  |  |  |  |  | ); | 
| 54 |  |  |  |  |  |  | my %_first_or_last = qw/ | 
| 55 |  |  |  |  |  |  | first       first | 
| 56 |  |  |  |  |  |  | last        last | 
| 57 |  |  |  |  |  |  | begin       first | 
| 58 |  |  |  |  |  |  | beginning   first | 
| 59 |  |  |  |  |  |  | start       first | 
| 60 |  |  |  |  |  |  | end         last | 
| 61 |  |  |  |  |  |  | ending      last | 
| 62 |  |  |  |  |  |  | /; | 
| 63 |  |  |  |  |  |  |  | 
| 64 |  |  |  |  |  |  | my @_parser_order = qw/ | 
| 65 |  |  |  |  |  |  | Flexible | 
| 66 |  |  |  |  |  |  | DateParse | 
| 67 |  |  |  |  |  |  | Natural | 
| 68 |  |  |  |  |  |  | /; | 
| 69 |  |  |  |  |  |  | unshift @_parser_order, qw/ICal/ if $have_ICal; | 
| 70 |  |  |  |  |  |  | push @_parser_order, qw/DateManip/ if $have_DateManip; | 
| 71 |  |  |  |  |  |  | my %_parser_source = ( | 
| 72 |  |  |  |  |  |  | ICal => sub { | 
| 73 |  |  |  |  |  |  | return DateTime::Format::ICal->parse_datetime(shift); | 
| 74 |  |  |  |  |  |  | }, | 
| 75 |  |  |  |  |  |  |  | 
| 76 |  |  |  |  |  |  | DateParse => sub { | 
| 77 |  |  |  |  |  |  | return DateTimeX::Easy::DateParse->parse_datetime(shift); | 
| 78 |  |  |  |  |  |  | }, | 
| 79 |  |  |  |  |  |  |  | 
| 80 |  |  |  |  |  |  | Natural => sub { | 
| 81 |  |  |  |  |  |  | local $SIG{__WARN__} = sub { | 
| 82 |  |  |  |  |  |  | }; # Make sure ::Natural/Date::Calc stay quiet... don't really like this, oh well... | 
| 83 |  |  |  |  |  |  | my $dt = $natural_parser->parse_datetime(shift); | 
| 84 |  |  |  |  |  |  | return unless $natural_parser->success; | 
| 85 |  |  |  |  |  |  | return $dt; | 
| 86 |  |  |  |  |  |  | }, | 
| 87 |  |  |  |  |  |  |  | 
| 88 |  |  |  |  |  |  | Flexible => sub { | 
| 89 |  |  |  |  |  |  | my $parse = shift; | 
| 90 |  |  |  |  |  |  | my $time_zone; | 
| 91 |  |  |  |  |  |  |  | 
| 92 |  |  |  |  |  |  | # First, try to extract out any timezone information | 
| 93 |  |  |  |  |  |  | { | 
| 94 |  |  |  |  |  |  | ################################################## | 
| 95 |  |  |  |  |  |  | # 2008-09-16 13:23:57 Eastern Daylight (?:Time)? # | 
| 96 |  |  |  |  |  |  | ################################################## | 
| 97 |  |  |  |  |  |  | if ($parse | 
| 98 |  |  |  |  |  |  | =~ s/\s+(?:(Eastern|Central|Mountain|Pacific)\s+(?:Daylight|Standard)(?:\s+Time)?).*$// | 
| 99 |  |  |  |  |  |  | ) | 
| 100 |  |  |  |  |  |  | { | 
| 101 |  |  |  |  |  |  | $time_zone = "US/$1"; | 
| 102 |  |  |  |  |  |  | } | 
| 103 |  |  |  |  |  |  | ################################## | 
| 104 |  |  |  |  |  |  | # 2008-09-16 13:23:57 US/Eastern # | 
| 105 |  |  |  |  |  |  | ################################## | 
| 106 |  |  |  |  |  |  | elsif ($parse =~ s/\s+([A-Za-z][A-Za-z0-9\/\._]*)\s*$//) | 
| 107 |  |  |  |  |  |  | {    # Look for a timezone-like string at the end of $parse | 
| 108 |  |  |  |  |  |  | $time_zone = $1; | 
| 109 |  |  |  |  |  |  | $parse     = "$parse $time_zone" and undef $time_zone | 
| 110 |  |  |  |  |  |  | if $time_zone | 
| 111 |  |  |  |  |  |  | && $time_zone =~ m/^[ap]\.?m\.?$/i | 
| 112 |  |  |  |  |  |  | ;    # Put back AM/PM if we accidentally slurped it out | 
| 113 |  |  |  |  |  |  | } | 
| 114 |  |  |  |  |  |  | ######################################################### | 
| 115 |  |  |  |  |  |  | # 2008-09-16 13:23:57 Eastern Daylight Time (GMT+05:00) # | 
| 116 |  |  |  |  |  |  | ######################################################### | 
| 117 |  |  |  |  |  |  | elsif ($parse | 
| 118 |  |  |  |  |  |  | =~ s/(?:\s+[A-Z]\w+)*\s+\(?(?:GMT|UTC)?([-+]\d{2}:\d{2})\)?\s*$// | 
| 119 |  |  |  |  |  |  | ) | 
| 120 |  |  |  |  |  |  | { | 
| 121 |  |  |  |  |  |  | $time_zone = $1; | 
| 122 |  |  |  |  |  |  | } | 
| 123 |  |  |  |  |  |  |  | 
| 124 |  |  |  |  |  |  | # Flexible can't seem to parse (GMT+0:500) | 
| 125 |  |  |  |  |  |  | #            elsif ($parse =~ s/(?:\s+[A-Z]\w+)*(\s+\(GMT[-+]\d{2}:\d{2}\)\s*)$//) { | 
| 126 |  |  |  |  |  |  | #                $parse = "$parse $1"; | 
| 127 |  |  |  |  |  |  | #            } | 
| 128 |  |  |  |  |  |  | ############################# | 
| 129 |  |  |  |  |  |  | # 2008-09-16 13:23:57 +0500 # | 
| 130 |  |  |  |  |  |  | ############################# | 
| 131 |  |  |  |  |  |  | elsif ($parse =~ s/\s+([-+]\d{3,})\s*$//) { | 
| 132 |  |  |  |  |  |  | $time_zone = $1; | 
| 133 |  |  |  |  |  |  | } | 
| 134 |  |  |  |  |  |  | } | 
| 135 |  |  |  |  |  |  | return unless my $dt = DateTime::Format::Flexible->build($parse); | 
| 136 |  |  |  |  |  |  | if ($time_zone) { | 
| 137 |  |  |  |  |  |  | $dt->set_time_zone("floating"); | 
| 138 |  |  |  |  |  |  | $dt->set_time_zone($time_zone); | 
| 139 |  |  |  |  |  |  | } | 
| 140 |  |  |  |  |  |  | return $dt; | 
| 141 |  |  |  |  |  |  | }, | 
| 142 |  |  |  |  |  |  |  | 
| 143 |  |  |  |  |  |  | DateManip => sub { | 
| 144 |  |  |  |  |  |  | return DateTime::Format::DateManip->parse_datetime(shift); | 
| 145 |  |  |  |  |  |  | }, | 
| 146 |  |  |  |  |  |  | ); | 
| 147 |  |  |  |  |  |  |  | 
| 148 |  |  |  |  |  |  | shift if $_[0] && $_[0] eq __PACKAGE__; | 
| 149 |  |  |  |  |  |  |  | 
| 150 | 61 | 100 | 66 | 61 | 1 | 64526 | my $parse; | 
| 151 |  |  |  |  |  |  | $parse = shift if @_ % 2; | 
| 152 | 61 |  |  |  |  | 79 |  | 
| 153 | 61 | 100 |  |  |  | 137 | my %in = @_; | 
| 154 |  |  |  |  |  |  | $parse = delete $in{parse} if exists $in{parse}; | 
| 155 | 61 |  |  |  |  | 100 | my $truncate                  = delete $in{truncate}; | 
| 156 | 61 | 100 |  |  |  | 106 | my $soft_time_zone_conversion = delete $in{soft_time_zone_conversion}; | 
| 157 | 61 |  |  |  |  | 74 | my $time_zone_if_floating     = delete $in{default_time_zone}; | 
| 158 | 61 |  |  |  |  | 69 | $time_zone_if_floating = delete $in{time_zone_if_floating} | 
| 159 | 61 |  |  |  |  | 67 | if exists $in{time_zone_if_floating}; | 
| 160 |  |  |  |  |  |  | my $parser_order   = delete $in{parser_order}; | 
| 161 | 61 | 100 |  |  |  | 91 | my $parser_exclude = delete $in{parser_exclude}; | 
| 162 | 61 |  |  |  |  | 66 | my $ambiguous      = 1; | 
| 163 | 61 |  |  |  |  | 63 | $ambiguous = delete $in{ambiguous} if exists $in{ambiguous}; | 
| 164 | 61 |  |  |  |  | 66 |  | 
| 165 | 61 | 100 |  |  |  | 82 | my ($time_zone); | 
| 166 |  |  |  |  |  |  | $time_zone = delete $in{tz}       if exists $in{tz}; | 
| 167 | 61 |  |  |  |  | 60 | $time_zone = delete $in{timezone} if exists $in{timezone}; | 
| 168 | 61 | 50 |  |  |  | 84 | $time_zone = delete $in{time_zone} | 
| 169 | 61 | 100 |  |  |  | 84 | if exists $in{time_zone} | 
| 170 |  |  |  |  |  |  | ;    # "time_zone" takes precedence over "timezone" | 
| 171 |  |  |  |  |  |  |  | 
| 172 | 61 | 100 |  |  |  | 97 | my @delta; | 
| 173 |  |  |  |  |  |  |  | 
| 174 | 61 |  |  |  |  | 64 | my $original_parse = $parse; | 
| 175 |  |  |  |  |  |  | my $parse_dt; | 
| 176 | 61 |  |  |  |  | 61 | if ($parse) { | 
| 177 | 61 |  |  |  |  | 62 | if (blessed $parse && $parse->isa("DateTime")) | 
| 178 | 61 | 50 |  |  |  | 95 | {    # We have a DateTime object as $parse | 
| 179 | 61 | 100 | 66 |  |  | 192 | $parse_dt = $parse; | 
| 180 |  |  |  |  |  |  | } | 
| 181 | 6 |  |  |  |  | 7 | else { | 
| 182 |  |  |  |  |  |  |  | 
| 183 |  |  |  |  |  |  | if (1) { | 
| 184 |  |  |  |  |  |  | my $got_ambiguous; | 
| 185 | 55 |  |  |  |  | 54 | my ($last_delta); | 
| 186 | 55 |  |  |  |  | 58 | while ($parse | 
| 187 |  |  |  |  |  |  | =~ s/^\s*(start|first|last|(?:begin|end)(?:ning)?)\s+(year|month|day|hour|minute|second)\s+of\s+//i | 
| 188 | 55 |  |  |  |  | 220 | ) | 
| 189 |  |  |  |  |  |  | { | 
| 190 |  |  |  |  |  |  | my $first_or_last = $1; | 
| 191 |  |  |  |  |  |  | $first_or_last = $_first_or_last{ lc $first_or_last }; | 
| 192 | 8 |  |  |  |  | 17 | my $period = $2; | 
| 193 | 8 |  |  |  |  | 17 | $last_delta->{add} = ["${period}s" => 1] if $last_delta; | 
| 194 | 8 |  |  |  |  | 23 | push @delta, | 
| 195 | 8 | 50 |  |  |  | 14 | $last_delta = my $delta = { period => $period }; | 
| 196 | 8 |  |  |  |  | 33 | if ($first_or_last ne "first") { | 
| 197 |  |  |  |  |  |  | $delta->{last}     = 1; | 
| 198 | 8 | 100 |  |  |  | 16 | $delta->{subtract} = ["${period}s" => 1]; | 
| 199 | 6 |  |  |  |  | 8 | } | 
| 200 | 6 |  |  |  |  | 23 | else { | 
| 201 |  |  |  |  |  |  | $delta->{first} = 1; | 
| 202 |  |  |  |  |  |  | } | 
| 203 | 2 |  |  |  |  | 6 | } | 
| 204 |  |  |  |  |  |  | my $last_parse = $parse; | 
| 205 |  |  |  |  |  |  | my $period; | 
| 206 | 55 |  |  |  |  | 66 | if ($parse | 
| 207 | 55 |  |  |  |  | 52 | =~ s/^\s*(start|this|next|first|last|(?:begin|end)(?:ning)?)\s+(year|month|day|hour|minute|second)(?:\s+of\s+)?// | 
| 208 | 55 | 50 |  |  |  | 193 | ) | 
|  |  | 100 |  |  |  |  |  | 
|  |  | 100 |  |  |  |  |  | 
| 209 |  |  |  |  |  |  | { | 
| 210 |  |  |  |  |  |  | $period = $2; | 
| 211 |  |  |  |  |  |  | $last_delta->{add} = ["${period}s" => 1] | 
| 212 | 0 |  |  |  |  | 0 | if $last_delta && $last_delta->{last}; | 
| 213 |  |  |  |  |  |  | push @delta, { truncate => $period }; | 
| 214 | 0 | 0 | 0 |  |  | 0 | $parse = $last_parse unless $parse; | 
| 215 | 0 |  |  |  |  | 0 | } | 
| 216 | 0 | 0 |  |  |  | 0 | elsif ($parse | 
| 217 |  |  |  |  |  |  | =~ s/^\s*(year|month|day|hour|minute|second)\s+of\s+//i) | 
| 218 |  |  |  |  |  |  | { | 
| 219 |  |  |  |  |  |  | $period = $1; | 
| 220 |  |  |  |  |  |  | $last_delta->{add} = ["${period}s" => 1] | 
| 221 | 4 |  |  |  |  | 6 | if $last_delta && $last_delta->{last}; | 
| 222 |  |  |  |  |  |  | push @delta, { truncate => $period }; | 
| 223 | 4 | 100 | 66 |  |  | 18 | } | 
| 224 | 4 |  |  |  |  | 8 | elsif (@delta) { | 
| 225 |  |  |  |  |  |  | $got_ambiguous = 1; | 
| 226 |  |  |  |  |  |  | $period        = $last_delta->{period}; | 
| 227 | 4 |  |  |  |  | 5 | my $truncate = $_truncate_range{$period}; | 
| 228 | 4 |  |  |  |  | 5 | push @delta, my $delta = { truncate => $truncate }; | 
| 229 | 4 |  |  |  |  | 7 | my $delta_range = $_delta_range{$period}; | 
| 230 | 4 |  |  |  |  | 7 | if ($delta_range) { | 
| 231 | 4 |  |  |  |  | 7 | my ($add, $subtract) = @$delta_range; | 
| 232 | 4 | 50 |  |  |  | 6 | if ($last_delta->{last}) { | 
| 233 | 4 |  |  |  |  | 8 | $last_delta->{add} = ["${add}" => 1]; | 
| 234 | 4 | 100 |  |  |  | 7 | } | 
| 235 | 3 |  |  |  |  | 7 | } | 
| 236 |  |  |  |  |  |  | } | 
| 237 |  |  |  |  |  |  |  | 
| 238 |  |  |  |  |  |  | croak | 
| 239 |  |  |  |  |  |  | "Can't parse \"$original_parse\" since it's too ambiguous" | 
| 240 |  |  |  |  |  |  | if $got_ambiguous && !$ambiguous; | 
| 241 | 55 | 100 | 100 |  |  | 129 | } | 
| 242 |  |  |  |  |  |  |  | 
| 243 |  |  |  |  |  |  | my @parser_order | 
| 244 |  |  |  |  |  |  | = $parser_order | 
| 245 |  |  |  |  |  |  | ? ( | 
| 246 | 54 | 0 |  |  |  | 133 | ref $parser_order eq "ARRAY" | 
|  |  | 50 |  |  |  |  |  | 
| 247 |  |  |  |  |  |  | ? @$parser_order | 
| 248 |  |  |  |  |  |  | : ($parser_order)) | 
| 249 |  |  |  |  |  |  | : @_parser_order; | 
| 250 |  |  |  |  |  |  | my (%parser_exclude); | 
| 251 |  |  |  |  |  |  | %parser_exclude | 
| 252 | 54 |  |  |  |  | 58 | = map { $_ => 1 } | 
| 253 |  |  |  |  |  |  | ( | 
| 254 | 54 | 0 |  |  |  | 63 | ref $parser_exclude eq "ARRAY" | 
|  | 0 | 50 |  |  |  | 0 |  | 
| 255 |  |  |  |  |  |  | ? @$parser_exclude | 
| 256 |  |  |  |  |  |  | : ($parser_exclude)) | 
| 257 |  |  |  |  |  |  | if $parser_exclude; | 
| 258 |  |  |  |  |  |  | my %parser_source = %_parser_source; | 
| 259 |  |  |  |  |  |  | if (DEBUG) { | 
| 260 | 54 |  |  |  |  | 166 | warn "Parse $parse\n"; | 
| 261 | 54 |  |  |  |  | 61 | } | 
| 262 |  |  |  |  |  |  |  | 
| 263 |  |  |  |  |  |  | # TODO Kinda hackish | 
| 264 |  |  |  |  |  |  | if ($parse =~ m/^[1-9]\d{3}$/) | 
| 265 |  |  |  |  |  |  | {    # If it's a four digit year... yeah, arbitrary | 
| 266 | 54 | 100 |  |  |  | 125 | $parse_dt = DateTime->new(year => $parse); | 
| 267 |  |  |  |  |  |  | } | 
| 268 | 3 |  |  |  |  | 12 | while (!$parse_dt && @parser_order) { | 
| 269 |  |  |  |  |  |  | my $parser = shift @parser_order; | 
| 270 | 54 |  | 66 |  |  | 790 | next if $parser_exclude{$parser}; | 
| 271 | 116 |  |  |  |  | 9335 |  | 
| 272 | 116 | 50 |  |  |  | 191 | # warn "Try $parser:\n" if DEBUG; | 
| 273 |  |  |  |  |  |  | my $parser_code = $parser_source{$parser}; | 
| 274 |  |  |  |  |  |  | eval { $parse_dt = $parser_code->($parse); }; | 
| 275 | 116 |  |  |  |  | 137 | if (DEBUG) { | 
| 276 | 116 |  |  |  |  | 115 | if ($@) { | 
|  | 116 |  |  |  |  | 248 |  | 
| 277 | 116 |  |  |  |  | 37664 | warn "FAIL $parser: $@\n"; | 
| 278 |  |  |  |  |  |  | } | 
| 279 |  |  |  |  |  |  | elsif ($parse_dt) { | 
| 280 |  |  |  |  |  |  | warn "PASS $parser: $parse_dt\n"; | 
| 281 |  |  |  |  |  |  | } | 
| 282 |  |  |  |  |  |  | else { | 
| 283 |  |  |  |  |  |  | warn "FAIL $parser\n"; | 
| 284 |  |  |  |  |  |  | } | 
| 285 |  |  |  |  |  |  | } | 
| 286 |  |  |  |  |  |  | undef $parse_dt if $@; | 
| 287 |  |  |  |  |  |  | } | 
| 288 | 116 | 100 |  |  |  | 375 | } | 
| 289 |  |  |  |  |  |  | return unless $parse_dt; | 
| 290 |  |  |  |  |  |  | } | 
| 291 | 60 | 50 |  |  |  | 282 |  | 
| 292 |  |  |  |  |  |  | my %DateTime; | 
| 293 |  |  |  |  |  |  | $DateTime{time_zone} = "floating"; | 
| 294 | 60 |  |  |  |  | 210 | if ($parse_dt) { | 
| 295 | 60 |  |  |  |  | 97 | $DateTime{$_} = $parse_dt->$_ | 
| 296 | 60 | 50 |  |  |  | 82 | for qw/year month day hour minute second nanosecond time_zone/; | 
| 297 |  |  |  |  |  |  | } | 
| 298 | 60 |  |  |  |  | 305 | @DateTime{ keys %in } = values %in; | 
| 299 |  |  |  |  |  |  |  | 
| 300 | 60 |  |  |  |  | 1605 | return unless my $dt = DateTime->new(%DateTime); | 
| 301 |  |  |  |  |  |  |  | 
| 302 | 60 | 50 |  |  |  | 176 | if ($time_zone) { | 
| 303 |  |  |  |  |  |  | if ($soft_time_zone_conversion) { | 
| 304 | 60 | 100 | 66 |  |  | 15016 | $dt->set_time_zone("floating"); | 
|  |  | 100 |  |  |  |  |  | 
| 305 | 13 | 100 |  |  |  | 23 | } | 
| 306 | 1 |  |  |  |  | 4 | $dt->set_time_zone($time_zone); | 
| 307 |  |  |  |  |  |  | } | 
| 308 | 13 |  |  |  |  | 200 | elsif ($time_zone_if_floating && $dt->time_zone->is_floating) { | 
| 309 |  |  |  |  |  |  | $dt->set_time_zone($time_zone_if_floating); | 
| 310 |  |  |  |  |  |  | } | 
| 311 | 1 |  |  |  |  | 8 |  | 
| 312 |  |  |  |  |  |  | if ($truncate) { | 
| 313 |  |  |  |  |  |  | $truncate = $truncate->[1]         if ref $truncate eq "ARRAY"; | 
| 314 | 60 | 100 |  |  |  | 4326 | $truncate = (values %$truncate)[0] if ref $truncate eq "HASH"; | 
|  |  | 100 |  |  |  |  |  | 
| 315 | 1 | 50 |  |  |  | 10 | $dt->truncate(to => $truncate); | 
| 316 | 1 | 50 |  |  |  | 3 | } | 
| 317 | 1 |  |  |  |  | 34 | elsif (@delta) { | 
| 318 |  |  |  |  |  |  | if (DEBUG) { | 
| 319 |  |  |  |  |  |  | require YAML; | 
| 320 | 7 |  |  |  |  | 8 | warn "$original_parse => $parse => $dt"; | 
| 321 |  |  |  |  |  |  | } | 
| 322 |  |  |  |  |  |  | for my $delta (reverse @delta) { | 
| 323 |  |  |  |  |  |  | warn YAML::Dump($delta) if DEBUG; | 
| 324 | 7 |  |  |  |  | 14 | if ($delta->{truncate}) { | 
| 325 | 14 |  |  |  |  | 1268 | $dt->truncate(to => $delta->{truncate}); | 
| 326 | 14 | 100 |  |  |  | 21 | } | 
| 327 | 7 |  |  |  |  | 15 | else { | 
| 328 |  |  |  |  |  |  | $dt->add(@{ $delta->{add} })           if $delta->{add}; | 
| 329 |  |  |  |  |  |  | $dt->subtract(@{ $delta->{subtract} }) if $delta->{subtract}; | 
| 330 | 7 | 100 |  |  |  | 17 | } | 
|  | 5 |  |  |  |  | 13 |  | 
| 331 | 7 | 100 |  |  |  | 3757 | } | 
|  | 5 |  |  |  |  | 16 |  | 
| 332 |  |  |  |  |  |  | } | 
| 333 |  |  |  |  |  |  |  | 
| 334 |  |  |  |  |  |  | return $dt; | 
| 335 |  |  |  |  |  |  | } | 
| 336 | 60 |  |  |  |  | 4631 | *parse          = \&new; | 
| 337 |  |  |  |  |  |  | *parse_date     = \&new; | 
| 338 |  |  |  |  |  |  | *parse_datetime = \&new; | 
| 339 |  |  |  |  |  |  | *date           = \&new; | 
| 340 |  |  |  |  |  |  | *datetime       = \&new; | 
| 341 |  |  |  |  |  |  | *new_date       = \&new; | 
| 342 |  |  |  |  |  |  | *new_datetime   = \&new; | 
| 343 |  |  |  |  |  |  |  | 
| 344 |  |  |  |  |  |  | 1;    # End of DateTimeX::Easy | 
| 345 |  |  |  |  |  |  |  | 
| 346 |  |  |  |  |  |  |  | 
| 347 |  |  |  |  |  |  | =pod | 
| 348 |  |  |  |  |  |  |  | 
| 349 |  |  |  |  |  |  | =encoding UTF-8 | 
| 350 |  |  |  |  |  |  |  | 
| 351 |  |  |  |  |  |  | =head1 NAME | 
| 352 |  |  |  |  |  |  |  | 
| 353 |  |  |  |  |  |  | DateTimeX::Easy - Parse a date/time string using the best method available | 
| 354 |  |  |  |  |  |  |  | 
| 355 |  |  |  |  |  |  | =head1 VERSION | 
| 356 |  |  |  |  |  |  |  | 
| 357 |  |  |  |  |  |  | version 0.090 | 
| 358 |  |  |  |  |  |  |  | 
| 359 |  |  |  |  |  |  | =head1 SYNOPSIS | 
| 360 |  |  |  |  |  |  |  | 
| 361 |  |  |  |  |  |  | # Make DateTimeX object for "now": | 
| 362 |  |  |  |  |  |  | my $dt = DateTimeX::Easy->new("today"); | 
| 363 |  |  |  |  |  |  |  | 
| 364 |  |  |  |  |  |  | # Same thing: | 
| 365 |  |  |  |  |  |  | my $dt = DateTimeX::Easy->new("now"); | 
| 366 |  |  |  |  |  |  |  | 
| 367 |  |  |  |  |  |  | # Uses ::F::Natural's coolness (similar in capability to Date::Manip) | 
| 368 |  |  |  |  |  |  | my $dt = DateTimeX::Easy->new("last monday"); | 
| 369 |  |  |  |  |  |  |  | 
| 370 |  |  |  |  |  |  | # ... but in 1969: | 
| 371 |  |  |  |  |  |  | my $dt = DateTimeX::Easy->new("last monday", year => 1969); | 
| 372 |  |  |  |  |  |  |  | 
| 373 |  |  |  |  |  |  | # ... at the 100th nanosecond: | 
| 374 |  |  |  |  |  |  | my $dt = DateTimeX::Easy->new("last monday", year => 1969, nanosecond => 100); | 
| 375 |  |  |  |  |  |  |  | 
| 376 |  |  |  |  |  |  | # ... in US/Eastern: (This will NOT do a timezone conversion) | 
| 377 |  |  |  |  |  |  | my $dt = DateTimeX::Easy->new("last monday", year => 1969, nanosecond => 100, timezone => "US/Eastern"); | 
| 378 |  |  |  |  |  |  |  | 
| 379 |  |  |  |  |  |  | # This WILL do a proper timezone conversion: | 
| 380 |  |  |  |  |  |  | my $dt = DateTimeX::Easy->new("last monday", year => 1969, nanosecond => 100, timezone => "US/Pacific"); | 
| 381 |  |  |  |  |  |  | $dt->set_time_zone("US/Eastern"); | 
| 382 |  |  |  |  |  |  |  | 
| 383 |  |  |  |  |  |  | # Custom DateTimeX ability: | 
| 384 |  |  |  |  |  |  | my $dt = DateTimeX::Easy->new("last second of last month"); | 
| 385 |  |  |  |  |  |  | $dt = DateTimeX::Easy->new("last second of first month of last year"); | 
| 386 |  |  |  |  |  |  | $dt = DateTimeX::Easy->new("last second of first month of 2000"); | 
| 387 |  |  |  |  |  |  |  | 
| 388 |  |  |  |  |  |  | =head1 DESCRIPTION | 
| 389 |  |  |  |  |  |  |  | 
| 390 |  |  |  |  |  |  | DateTimeX::Easy makes DateTime object creation quick and easy. It uses a | 
| 391 |  |  |  |  |  |  | variety of DateTime::Format packages to do the bulk of the parsing, with some | 
| 392 |  |  |  |  |  |  | custom tweaks to smooth out the rough edges (mainly concerning timezone | 
| 393 |  |  |  |  |  |  | detection and selection). | 
| 394 |  |  |  |  |  |  |  | 
| 395 |  |  |  |  |  |  | =head1 PARSING | 
| 396 |  |  |  |  |  |  |  | 
| 397 |  |  |  |  |  |  | Currently, DateTimeX::Easy will attempt to parse input in the following order: | 
| 398 |  |  |  |  |  |  |  | 
| 399 |  |  |  |  |  |  | =over | 
| 400 |  |  |  |  |  |  |  | 
| 401 |  |  |  |  |  |  | =item DateTime - Is the input a DateTime object? | 
| 402 |  |  |  |  |  |  |  | 
| 403 |  |  |  |  |  |  | =item ICal - Was DT::F::ICal able to parse the input? | 
| 404 |  |  |  |  |  |  |  | 
| 405 |  |  |  |  |  |  | =item DateParse - Was DT::F::DateParse able to parse the input? | 
| 406 |  |  |  |  |  |  |  | 
| 407 |  |  |  |  |  |  | A caveat, I actually use a modified version of DateParse in order to avoid | 
| 408 |  |  |  |  |  |  | DateParse's default timezone selection. | 
| 409 |  |  |  |  |  |  |  | 
| 410 |  |  |  |  |  |  | =item Natural - Was DT::F::Natural able to parse the input? | 
| 411 |  |  |  |  |  |  |  | 
| 412 |  |  |  |  |  |  | Since this module barfs pretty loudly on strange input, we use a silent | 
| 413 |  |  |  |  |  |  | $SIG{__WARN__} to hide errors. | 
| 414 |  |  |  |  |  |  |  | 
| 415 |  |  |  |  |  |  | =item Flexible - Was DT::F::Flexible able to parse the input? | 
| 416 |  |  |  |  |  |  |  | 
| 417 |  |  |  |  |  |  | This step also looks at the string to see if there is any timezone information | 
| 418 |  |  |  |  |  |  | at the end. | 
| 419 |  |  |  |  |  |  |  | 
| 420 |  |  |  |  |  |  | =item DateManip - Was DT::F::DateManip able to parse the input? | 
| 421 |  |  |  |  |  |  |  | 
| 422 |  |  |  |  |  |  | DateManip isn't very nice with preserving the input timezone, but it's here as | 
| 423 |  |  |  |  |  |  | a last resort. | 
| 424 |  |  |  |  |  |  |  | 
| 425 |  |  |  |  |  |  | =back | 
| 426 |  |  |  |  |  |  |  | 
| 427 |  |  |  |  |  |  | =head1 "last second of first month of year of 2005" | 
| 428 |  |  |  |  |  |  |  | 
| 429 |  |  |  |  |  |  | DateTimeX::Easy also provides additional parsing and transformation for input | 
| 430 |  |  |  |  |  |  | like: | 
| 431 |  |  |  |  |  |  |  | 
| 432 |  |  |  |  |  |  | "first day of last month" | 
| 433 |  |  |  |  |  |  | "last day of last month" | 
| 434 |  |  |  |  |  |  | "last day of this month" | 
| 435 |  |  |  |  |  |  | "last day of next month" | 
| 436 |  |  |  |  |  |  | "last second of first month of last year" | 
| 437 |  |  |  |  |  |  | "ending day of month of 2007-10-02" | 
| 438 |  |  |  |  |  |  | "last second of first month of year of 2005" | 
| 439 |  |  |  |  |  |  | "last second of last month of year of 2005" | 
| 440 |  |  |  |  |  |  | "beginning day of month of 2007-10-02" | 
| 441 |  |  |  |  |  |  | "last month of year of 2007" | 
| 442 |  |  |  |  |  |  |  | 
| 443 |  |  |  |  |  |  | It will look at each sequence of "<first|last> of <period>" and do ->add, | 
| 444 |  |  |  |  |  |  | ->subtract, and ->truncate operations on the parsed DateTime object | 
| 445 |  |  |  |  |  |  |  | 
| 446 |  |  |  |  |  |  | Also, It's best to be as explicit as possible; the following will work: | 
| 447 |  |  |  |  |  |  |  | 
| 448 |  |  |  |  |  |  | "last month of 2007" | 
| 449 |  |  |  |  |  |  | "last second of last month of 2005" | 
| 450 |  |  |  |  |  |  | "beginning day of 2007-10-02" | 
| 451 |  |  |  |  |  |  |  | 
| 452 |  |  |  |  |  |  | This won't, though: | 
| 453 |  |  |  |  |  |  |  | 
| 454 |  |  |  |  |  |  | "last day of 2007" | 
| 455 |  |  |  |  |  |  |  | 
| 456 |  |  |  |  |  |  | You'll have to do this instead: | 
| 457 |  |  |  |  |  |  |  | 
| 458 |  |  |  |  |  |  | "last day of year of 2007" | 
| 459 |  |  |  |  |  |  |  | 
| 460 |  |  |  |  |  |  | The reason is that the date portion is opaque to the parser. It doesn't know | 
| 461 |  |  |  |  |  |  | whether it has "2007" or "2007-10" or "now" as the last input. To fix this, you | 
| 462 |  |  |  |  |  |  | can give a hint to the parser, like "<period> of <date/time>" (as in "year of | 
| 463 |  |  |  |  |  |  | 2007" above). | 
| 464 |  |  |  |  |  |  |  | 
| 465 |  |  |  |  |  |  | WARNING: This feature is still somewhat new, so there may be bugs lurking | 
| 466 |  |  |  |  |  |  | about. Please forward failing tests/scenarios. | 
| 467 |  |  |  |  |  |  |  | 
| 468 |  |  |  |  |  |  | =head1 METHODS | 
| 469 |  |  |  |  |  |  |  | 
| 470 |  |  |  |  |  |  | =head2 DateTimeX::Easy->new( ... ) | 
| 471 |  |  |  |  |  |  |  | 
| 472 |  |  |  |  |  |  | =head2 DateTimeX::Easy->parse( ... ) | 
| 473 |  |  |  |  |  |  |  | 
| 474 |  |  |  |  |  |  | =head2 DateTimeX::Easy->parse_date( ... ) | 
| 475 |  |  |  |  |  |  |  | 
| 476 |  |  |  |  |  |  | =head2 DateTimeX::Easy->parse_datetime( ... ) | 
| 477 |  |  |  |  |  |  |  | 
| 478 |  |  |  |  |  |  | =head2 DateTimeX::Easy->date( ... ) | 
| 479 |  |  |  |  |  |  |  | 
| 480 |  |  |  |  |  |  | =head2 DateTimeX::Easy->datetime( ... ) | 
| 481 |  |  |  |  |  |  |  | 
| 482 |  |  |  |  |  |  | =head2 DateTimeX::Easy->new_date( ... ) | 
| 483 |  |  |  |  |  |  |  | 
| 484 |  |  |  |  |  |  | =head2 DateTimeX::Easy->new_datetime( ... ) | 
| 485 |  |  |  |  |  |  |  | 
| 486 |  |  |  |  |  |  | Parse the given date/time specification using ::F::Flexible or ::F::Natural and use the result to create a L<DateTime> object. Returns a L<DateTime> object. | 
| 487 |  |  |  |  |  |  |  | 
| 488 |  |  |  |  |  |  | You can pass the following in: | 
| 489 |  |  |  |  |  |  |  | 
| 490 |  |  |  |  |  |  | parse       # The string or DateTime object to parse. | 
| 491 |  |  |  |  |  |  |  | 
| 492 |  |  |  |  |  |  | year        # A year to override the result of parsing | 
| 493 |  |  |  |  |  |  | month       # A month to override the result of parsing | 
| 494 |  |  |  |  |  |  | day         # A day to override the result of parsing | 
| 495 |  |  |  |  |  |  | hour        # A hour to override the result of parsing | 
| 496 |  |  |  |  |  |  | minute      # A minute to override the result of parsing | 
| 497 |  |  |  |  |  |  | second      # A second to override the result of parsing | 
| 498 |  |  |  |  |  |  |  | 
| 499 |  |  |  |  |  |  | truncate    # A truncation parameter (e.g. year, day, month, week, etc.) | 
| 500 |  |  |  |  |  |  |  | 
| 501 |  |  |  |  |  |  | time_zone   # - Can be: | 
| 502 |  |  |  |  |  |  | timezone    # * A timezone (e.g. US/Pacific, UTC, etc.) | 
| 503 |  |  |  |  |  |  | tz          # * A DateTime special timezone (e.g. floating, local) | 
| 504 |  |  |  |  |  |  | # | 
| 505 |  |  |  |  |  |  | # - If neither "tz", "timezone", nor "time_zone" is set, then it'll use whatever is parsed. | 
| 506 |  |  |  |  |  |  | # - If no timezone is parsed, then the default is floating. | 
| 507 |  |  |  |  |  |  | # - If the given timezone is different from the parsed timezone, | 
| 508 |  |  |  |  |  |  | #   then a time conversion will take place (unless "soft_time_zone_conversion" is set). | 
| 509 |  |  |  |  |  |  | # - Either "time_zone", "timezone", "tz" will work (in that order), with "time_zone" having highest precedence | 
| 510 |  |  |  |  |  |  | # - See below for examples! | 
| 511 |  |  |  |  |  |  |  | 
| 512 |  |  |  |  |  |  | soft_time_zone_conversion   # Set this flag to 1 if you don't want the time to change when a given timezone is | 
| 513 |  |  |  |  |  |  | # different from a parsed timezone. For example, "10:00 UTC" soft converted to | 
| 514 |  |  |  |  |  |  | # PST8PDT would be "10:00 PST8PDT". | 
| 515 |  |  |  |  |  |  |  | 
| 516 |  |  |  |  |  |  | time_zone_if_floating       # The value of this option should be a valid timezone. If this option is set, then a DateTime object | 
| 517 |  |  |  |  |  |  | # with a floating timezone has it's timezone set to the value. | 
| 518 |  |  |  |  |  |  | default_time_zone           # Same as "time_zone_if_floating" | 
| 519 |  |  |  |  |  |  |  | 
| 520 |  |  |  |  |  |  | ambiguous   # Set this flag to 0 if you want to disallow ambiguous input like: | 
| 521 |  |  |  |  |  |  | # "last day of 2007" or "first minute of April" | 
| 522 |  |  |  |  |  |  | # This will require you to specify them as "last day of year of 2007" and "first minute of month of April" | 
| 523 |  |  |  |  |  |  | # instead. This flag is 1 (false) by default. | 
| 524 |  |  |  |  |  |  |  | 
| 525 |  |  |  |  |  |  | ... and anything else that you want to pass to the DateTime->new constructor | 
| 526 |  |  |  |  |  |  |  | 
| 527 |  |  |  |  |  |  | If C<truncate> is specified, then DateTime->truncate will be run after object creation. | 
| 528 |  |  |  |  |  |  |  | 
| 529 |  |  |  |  |  |  | Furthermore, you can simply pass the value for "parse" as the first positional argument of the DateTimeX::Easy call, e.g.: | 
| 530 |  |  |  |  |  |  |  | 
| 531 |  |  |  |  |  |  | # This: | 
| 532 |  |  |  |  |  |  | DateTimeX::Easy->new("today", year => 2008, truncate => "hour"); | 
| 533 |  |  |  |  |  |  |  | 
| 534 |  |  |  |  |  |  | # ... is the same as this: | 
| 535 |  |  |  |  |  |  | DateTimeX::Easy->new(parse => "today", year => 2008, truncate => "hour"); | 
| 536 |  |  |  |  |  |  |  | 
| 537 |  |  |  |  |  |  | Timezone processing can be a little complicated.  Here are some examples: | 
| 538 |  |  |  |  |  |  |  | 
| 539 |  |  |  |  |  |  | DateTimeX::Easy->parse("today"); # Will use a floating timezone | 
| 540 |  |  |  |  |  |  |  | 
| 541 |  |  |  |  |  |  | DateTimeX::Easy->parse("2007-07-01 10:32:10"); # Will ALSO use a floating timezone | 
| 542 |  |  |  |  |  |  |  | 
| 543 |  |  |  |  |  |  | DateTimeX::Easy->parse("2007-07-01 10:32:10 US/Eastern"); # Will use US/Eastern as a timezone | 
| 544 |  |  |  |  |  |  |  | 
| 545 |  |  |  |  |  |  | DateTimeX::Easy->parse("2007-07-01 10:32:10"); # Will use the floating timezone | 
| 546 |  |  |  |  |  |  |  | 
| 547 |  |  |  |  |  |  | DateTimeX::Easy->parse("2007-07-01 10:32:10", time_zone_if_floating => "local"); # Will use the local timezone | 
| 548 |  |  |  |  |  |  |  | 
| 549 |  |  |  |  |  |  | DateTimeX::Easy->parse("2007-07-01 10:32:10 UTC", time_zone => "US/Pacific"); # Will convert from UTC to US/Pacific | 
| 550 |  |  |  |  |  |  |  | 
| 551 |  |  |  |  |  |  | my $dt = DateTime->now->set_time_zone("US/Eastern"); | 
| 552 |  |  |  |  |  |  | DateTimeX::Easy->parse($dt); # Will use US/Eastern as the timezone | 
| 553 |  |  |  |  |  |  |  | 
| 554 |  |  |  |  |  |  | DateTimeX::Easy->parse($dt, time_zone => "floating"); # Will use a floating timezone | 
| 555 |  |  |  |  |  |  |  | 
| 556 |  |  |  |  |  |  | DateTimeX::Easy->parse($dt, time_zone => "US/Pacific", soft_time_zone_conversion => 1); | 
| 557 |  |  |  |  |  |  | # Will use US/Pacific as the timezone with NO conversion | 
| 558 |  |  |  |  |  |  | # For example, "22:00 US/Eastern" will become "22:00 PST8PDT" | 
| 559 |  |  |  |  |  |  |  | 
| 560 |  |  |  |  |  |  | DateTimeX::Easy->parse($dt)->set_time_zone("US/Pacific"); # Will use US/Pacific as the timezone WITH conversion | 
| 561 |  |  |  |  |  |  | # For example, "22:00 US/Eastern" will become "19:00 PST8PDT" | 
| 562 |  |  |  |  |  |  |  | 
| 563 |  |  |  |  |  |  | DateTimeX::Easy->parse($dt, time_zone => "US/Pacific"); # Will ALSO use US/Pacific as the timezone WITH conversion | 
| 564 |  |  |  |  |  |  |  | 
| 565 |  |  |  |  |  |  | =head1 EXPORT | 
| 566 |  |  |  |  |  |  |  | 
| 567 |  |  |  |  |  |  | =head2 parse( ... ) | 
| 568 |  |  |  |  |  |  |  | 
| 569 |  |  |  |  |  |  | =head2 parse_date( ... ) | 
| 570 |  |  |  |  |  |  |  | 
| 571 |  |  |  |  |  |  | =head2 parse_datetime( ... ) | 
| 572 |  |  |  |  |  |  |  | 
| 573 |  |  |  |  |  |  | =head2 date( ... ) | 
| 574 |  |  |  |  |  |  |  | 
| 575 |  |  |  |  |  |  | =head2 datetime( ... ) | 
| 576 |  |  |  |  |  |  |  | 
| 577 |  |  |  |  |  |  | =head2 new_date( ... ) | 
| 578 |  |  |  |  |  |  |  | 
| 579 |  |  |  |  |  |  | =head2 new_datetime( ... ) | 
| 580 |  |  |  |  |  |  |  | 
| 581 |  |  |  |  |  |  | Same syntax as above. See above for more information. | 
| 582 |  |  |  |  |  |  |  | 
| 583 |  |  |  |  |  |  | =head1 MOTIVATION | 
| 584 |  |  |  |  |  |  |  | 
| 585 |  |  |  |  |  |  | Although I really like using DateTime for date/time handling, I was often | 
| 586 |  |  |  |  |  |  | frustrated by its inability to parse even the simplest of date/time strings. | 
| 587 |  |  |  |  |  |  | There does exist a wide variety of DateTime::Format::* modules, but they all | 
| 588 |  |  |  |  |  |  | have different interfaces and different capabilities.  Coming from a | 
| 589 |  |  |  |  |  |  | Date::Manip background, I wanted something that gave me the power of ParseDate | 
| 590 |  |  |  |  |  |  | while still returning a DateTime object.  Most importantly, I wanted explicit | 
| 591 |  |  |  |  |  |  | control of the timezone setting at every step of the way. DateTimeX::Easy is | 
| 592 |  |  |  |  |  |  | the result. | 
| 593 |  |  |  |  |  |  |  | 
| 594 |  |  |  |  |  |  | =head1 THANKS | 
| 595 |  |  |  |  |  |  |  | 
| 596 |  |  |  |  |  |  | Dave Rolsky and crew for writing L<DateTime> | 
| 597 |  |  |  |  |  |  |  | 
| 598 |  |  |  |  |  |  | =head1 SEE ALSO | 
| 599 |  |  |  |  |  |  |  | 
| 600 |  |  |  |  |  |  | L<DateTime> | 
| 601 |  |  |  |  |  |  |  | 
| 602 |  |  |  |  |  |  | L<DateTime::Format::Natural> | 
| 603 |  |  |  |  |  |  |  | 
| 604 |  |  |  |  |  |  | L<DateTime::Format::Flexible> | 
| 605 |  |  |  |  |  |  |  | 
| 606 |  |  |  |  |  |  | L<DateTime::Format::DateManip> | 
| 607 |  |  |  |  |  |  |  | 
| 608 |  |  |  |  |  |  | L<DateTime::Format::ParseDate> | 
| 609 |  |  |  |  |  |  |  | 
| 610 |  |  |  |  |  |  | L<DateTime::Format::ICal> | 
| 611 |  |  |  |  |  |  |  | 
| 612 |  |  |  |  |  |  | L<Date::Manip> | 
| 613 |  |  |  |  |  |  |  | 
| 614 |  |  |  |  |  |  | =head1 AUTHOR | 
| 615 |  |  |  |  |  |  |  | 
| 616 |  |  |  |  |  |  | Robert Krimen <rokr@cpan.org> | 
| 617 |  |  |  |  |  |  |  | 
| 618 |  |  |  |  |  |  | =head1 COPYRIGHT AND LICENSE | 
| 619 |  |  |  |  |  |  |  | 
| 620 |  |  |  |  |  |  | This software is copyright (c) 2022 by Robert Krimen and others, see the git log. | 
| 621 |  |  |  |  |  |  |  | 
| 622 |  |  |  |  |  |  | This is free software; you can redistribute it and/or modify it under | 
| 623 |  |  |  |  |  |  | the same terms as the Perl 5 programming language system itself. | 
| 624 |  |  |  |  |  |  |  | 
| 625 |  |  |  |  |  |  | =cut |