File Coverage

blib/lib/Time/Strptime/Format.pm
Criterion Covered Total %
statement 142 144 98.6
branch 52 60 86.6
condition 28 40 70.0
subroutine 23 23 100.0
pod 2 2 100.0
total 247 269 91.8


line stmt bran cond sub pod time code
1             package Time::Strptime::Format;
2 4     4   163285 use strict;
  4         15  
  4         107  
3 4     4   21 use warnings;
  4         8  
  4         88  
4 4     4   999 use utf8;
  4         29  
  4         21  
5 4     4   1858 use integer;
  4         53  
  4         22  
6              
7 4     4   139 use B;
  4         9  
  4         161  
8 4     4   21 use Carp ();
  4         7  
  4         73  
9 4     4   1672 use Time::Local qw/timegm timegm_nocheck/;
  4         7137  
  4         546  
10 4     4   984 use Encode qw/encode_utf8/;
  4         20953  
  4         528  
11 4     4   1769 use DateTime::Locale;
  4         787126  
  4         154  
12 4     4   32 use List::MoreUtils qw/uniq/;
  4         9  
  4         59  
13 4     4   3672 use POSIX qw/strftime LC_ALL/;
  4         11241  
  4         33  
14 4     4   7644 use Time::Strptime::TimeZone;
  4         12  
  4         193  
15              
16 4   33 4   25 use constant DEBUG => exists $ENV{PERL_TIME_STRPTIME_DEBUG} && $ENV{PERL_TIME_STRPTIME_DEBUG};
  4         8  
  4         8613  
17              
18             our $VERSION = 1.04;
19              
20             our %DEFAULT_HANDLER = (
21             A => [SKIP => sub {
22             my $self = shift;
23             my $wide = $self->{locale}->day_format_wide;
24             my $abbr = $self->{locale}->day_format_abbreviated;
25             return [map quotemeta, uniq map { lc, uc, $_ } map { $wide->[$_], $abbr->[$_] } 0..6];
26             }],
27             a => [extend => q{%A} ],
28             B => [localed_month => sub {
29             my $self = shift;
30              
31             unless (exists $self->{format_table}{localed_month}) {
32             my %format_table;
33              
34             my $wide = $self->{locale}->month_format_wide;
35             my $abbr = $self->{locale}->month_format_abbreviated;
36             for my $month (0..11) {
37             for my $key ($wide->[$month], $abbr->[$month]) {
38             $format_table{$key} = $month + 1;
39             $format_table{lc $key} = $month + 1;
40             $format_table{uc $key} = $month + 1;
41             }
42             }
43             $self->{format_table}{localed_month} = \%format_table;
44             }
45              
46             return [map quotemeta, keys %{ $self->{format_table}{localed_month} }];
47             } ],
48             b => [extend => q{%B}],
49             C => ['UNSUPPORTED'],
50             c => ['UNSUPPORTED'],
51             D => [extend => q{%m/%d/%Y} ],
52             d => [day => ['0[1-9]','[12][0-9]','3[01]'] ],
53             e => [day => [' [1-9]','[12][0-9]','3[01]'] ],
54             F => [extend => q{%Y-%m-%d} ],
55             G => ['UNSUPPORTED'],
56             g => ['UNSUPPORTED'],
57             H => [hour24 => ['[01][0-9]','2[0-3]'] ],
58             h => [extend => q{%B} ],
59             I => [hour12 => ['0[1-9]', '1[0-2]'] ],
60             j => [day365 => ['00[1-9]', '0[1-9][0-9]', '[12][0-9][0-9]','3[0-5][0-9]','36[0-6]'] ],
61             k => [hour24 => ['[ 1][0-9]','2[0-3]'] ],
62             l => [hour12 => [' [1-9]', '1[0-2]'] ],
63             M => [minute => q{[0-5][0-9]} ],
64             m => [month => ['0[1-9]','1[0-2]'] ],
65             n => [SKIP => q{\s+} ],
66             p => [localed_pm => sub {
67             my $self = shift;
68             unless (exists $self->{format_table}{localed_pm}) {
69             for my $pm (0, 1) {
70             my $key = $self->{locale}->am_pm_abbreviated->[$pm];
71             $self->{format_table}{localed_pm}{$key} = $pm;
72             }
73             }
74             return [map quotemeta, keys %{ $self->{format_table}{localed_pm} }];
75             }],
76             R => [extend => q{%H:%M} ],
77             r => [extend => q{%I:%M:%S %p} ],
78             S => [second => ['[0-5][0-9]','60'] ],
79             s => [epoch => q{[0-9]+} ],
80             T => [extend => q{%H:%M:%S} ],
81             t => [char => "\t" ],
82             U => ['UNSUPPORTED'],
83             u => ['UNSUPPORTED'],
84             V => ['UNSUPPORTED'],
85             v => [extend => q{%e-%b-%Y} ],
86             W => ['UNSUPPORTED'],
87             w => ['UNSUPPORTED'],
88             X => ['UNSUPPORTED'],
89             x => ['UNSUPPORTED'],
90             Y => [year => q{[0-9]{4}}],
91             y => ['UNSUPPORTED'],
92             Z => [timezone => ['[-A-Z0-9]+', '[A-Z][a-z]+(?:/[A-Z][a-z]+)+']],
93             z => [offset => ['[-+][0-9]{4}', 'Z']],
94             );
95              
96             our %FIXED_OFFSET = (
97             GMT => 0,
98             UTC => 0,
99             Z => 0,
100             );
101              
102             sub new {
103 27     27 1 46817 my ($class, $format, $options) = @_;
104 27   100     68 $options ||= +{};
105              
106             my $self = bless +{
107             format => $format,
108             time_zone => Time::Strptime::TimeZone->new($options->{time_zone}),
109             locale => DateTime::Locale->load($options->{locale} || 'C'),
110             strict => $options->{strict} || 0,
111             _handler => +{
112             %DEFAULT_HANDLER,
113 27 50 100     118 %{ $options->{handler} || {} },
  27   50     4598  
114             },
115             } => $class;
116              
117             # compile and cache
118 27         120 $self->_parser();
119              
120 27         73 return $self;
121             }
122              
123             sub parse {
124 27     27 1 40 my $self = shift;
125 27         42 goto $self->_parser;
126             }
127              
128             sub _parser {
129 54     54   66 my $self = shift;
130 54   66     610 return $self->{_parser} ||= $self->_compile_format;
131             }
132              
133             sub _compile_format {
134 27     27   34 my $self = shift;
135 27         39 my $format = $self->{format};
136              
137 27         33 my $parser = do {
138             # setlocale
139 27         36 my $time_zone = $self->{time_zone};
140              
141             # assemble format to regexp
142 27         29 my $handlers = join '', keys %{ $self->{_handler} };
  27         167  
143 27         61 my @types;
144 27         229 $format =~ s{([^%]*)?%([${handlers}])([^%]*)?}{
145 107   100     358 my $prefix = quotemeta($1||'');
146 107   100     233 my $suffix = quotemeta($3||'');
147 107         183 $prefix.$self->_assemble_format($2, \@types).$suffix
148             }geo;
149 133         231 my %types_table = map { $_ => 1 } map {
150 27         61 my $t = $_;
  133         139  
151 133         152 $t =~ s/^localed_//;
152 133         185 $t;
153             } @types;
154              
155             # define vars
156 194         406 my $vars = join ', ', uniq map { '$'.$_ } map {
157 27         54 my $t = $_;
  187         201  
158 187 100       291 $t =~ s/^localed_// ? ($_, $t) : $_;
159             } @types, 'offset', 'epoch';
160 27         90 my $captures = join ', ', map { '$'.$_ } @types;
  133         196  
161              
162             # generate base src
163 27         52 local $" = ' ';
164 27         58 my $parser_src = <<EOD;
165             my ($vars);
166             \$offset = 0;
167             sub {
168             ($captures) = \$_[0] =~ m{\\A$format\\z}mo
169             or Carp::croak 'cannot parse datetime. text: "'.\$_[0].'", format: '.\%s;
170             \%s
171             (\$epoch, \$offset);
172             };
173             EOD
174              
175             # generate formatter src
176 27         37 my $formatter_src = '';
177 27         46 for my $type (@types) {
178 133         184 $formatter_src .= $self->_gen_stash_initialize_src($type);
179             }
180 27         51 $formatter_src .= $self->_gen_calc_epoch_src(\%types_table);
181 27         53 $formatter_src .= $self->_gen_calc_offset_src(\%types_table);
182              
183 27         210 my $combined_src = sprintf $parser_src, B::perlstring(B::perlstring($self->{format})), $formatter_src;
184 27         63 $self->{parser_src} = $combined_src;
185 27         31 warn encode_utf8 "[DEBUG] src: $combined_src" if DEBUG;
186              
187 27   100     74 my $format_table = $self->{format_table} || {};
188 27         4928 eval $combined_src; ## no critic
189             };
190 27 50       70 die $@ if $@;
191              
192 27         87 return $parser;
193             }
194              
195             sub _assemble_format {
196 163     163   325 my ($self, $c, $types) = @_;
197 163         199 my ($type, $val) = @{ $self->{_handler}->{$c} };
  163         298  
198 163 50       276 die "unsupported: \%$c. patches welcome :)" if $type eq 'UNSUPPORTED';
199              
200             # normalize
201 163 100       258 if (ref $val) {
202 92 100       140 $val = $self->$val($type) if ref $val eq 'CODE';
203 92 50       252 $val = join '|', @$val if ref $val eq 'ARRAY';
204             }
205              
206             # assemble to regexp
207 163 100       252 if ($type eq 'extend') {
208 22         23 my $handlers = join '', keys %{ $self->{_handler} };
  22         116  
209 22         586 $val =~ s{([^%]*)?%([${handlers}])([^%]*)?}{
210 56   50     177 my $prefix = quotemeta($1||'');
211 56   100     115 my $suffix = quotemeta($3||'');
212 56         97 $prefix.$self->_assemble_format($2, $types).$suffix
213             }ge;
214 22         104 return $val;
215             }
216             else {
217 141 100       224 return "(?:$val)" if $type eq 'SKIP';
218 137 100       213 return quotemeta $val if $type eq 'char';
219              
220 133         170 push @$types => $type;
221 133         563 return "($val)";
222             }
223             }
224              
225             sub _gen_stash_initialize_src {
226 133     133   171 my ($self, $type) = @_;
227              
228 133 100       228 if ($type eq 'timezone') {
    100          
229 1         4 return <<'EOD';
230             $time_zone->set_timezone($timezone);
231             EOD
232             }
233             elsif ($type =~ /^localed_([a-z]+)$/) {
234 7         24 return <<EOD;
235             \$${1} = \$format_table->{localed_${1}}->{\$localed_${1}};
236             EOD
237             }
238             else {
239 125         209 return ''; # default: none
240             }
241             }
242              
243             sub _gen_calc_epoch_src {
244 27     27   41 my ($self, $types_table) = @_;
245              
246 27 50       52 my $timegm = $self->{strict} ? 'timegm' : 'timegm_nocheck';
247              
248             # hour24&minute&second
249             # year&day365 or year&month&day
250 27 100       48 my $second = $types_table->{second} ? '$second' : 0;
251 27 100       40 my $minute = $types_table->{minute} ? '$minute' : 0;
252 27         50 my $hour = $self->_gen_calc_hour_src($types_table);
253 27 100 66     127 if ($types_table->{epoch}) {
    100 66        
    100 33        
    50 33        
254 3         7 return ''; # nothing to do
255             }
256             elsif ($types_table->{year} && $types_table->{month} && $types_table->{day}) {
257 20         57 return <<EOD;
258             \$epoch = $timegm($second, $minute, $hour, \$day, \$month - 1, \$year);
259             EOD
260             }
261             elsif ($types_table->{year} && $types_table->{month}) {
262 1         6 return <<EOD;
263             \$epoch = $timegm($second, $minute, $hour, 1, \$month - 1, \$year);
264             EOD
265             }
266             elsif ($types_table->{year} && $types_table->{day365}) {
267 3         12 return <<EOD;
268             \$epoch = $timegm($second, $minute, $hour, 1, 0, \$year) + (\$day365 - 1) * 60 * 60 * 24;
269             EOD
270             }
271              
272 0         0 die 'unknown case. types: '. join ', ', keys %$types_table;
273             }
274              
275             sub _gen_calc_offset_src {
276 27     27   40 my ($self, $types_table) = @_;
277              
278 27         32 my $src = '';
279              
280 27 100       44 my $second = $types_table->{second} ? '$second' : 0;
281 27 100       39 my $minute = $types_table->{minute} ? '$minute' : 0;
282 27         44 my $hour = $self->_gen_calc_hour_src($types_table);
283              
284 27         61 my $fixed_offset = $self->_fixed_offset($types_table);
285 27 100       102 if (defined $fixed_offset) {
    100          
286 20 50       38 if ($fixed_offset != 0) {
287 0         0 $src .= sprintf <<'EOD', $fixed_offset;
288             $offset -= %d;
289             EOD
290             }
291             }
292             elsif ($types_table->{offset}) {
293 6         12 $src .= <<'EOD';
294             $offset = $offset eq 'Z' ? 0 : (abs($offset) == $offset ? 1 : -1) * (60 * 60 * substr($offset, 1, 2) + 60 * substr($offset, 3, 2));
295             EOD
296             }
297             else {
298 1         3 $src .= <<EOD;
299             \$offset = \$time_zone->offset(\$epoch);
300             EOD
301             }
302              
303 27 100 100     70 if (!defined $fixed_offset && !$types_table->{epoch}) {
304 4         8 $src .= <<'EOD'
305             $epoch -= $offset;
306             EOD
307             }
308              
309 27         50 return $src;
310             }
311              
312             sub _gen_calc_hour_src {
313 54     54   71 my ($self, $types_table) = @_;
314              
315 54 100 66     104 if ($types_table->{hour24}) {
    100          
316 26         43 return '$hour24';
317             }
318             elsif ($types_table->{hour12} && $types_table->{pm}) {
319 8         14 return '(0,12)[$pm] + ($hour12 % 12)';
320             }
321             else {
322 20         32 return '0';
323             }
324             }
325              
326             sub _fixed_offset {
327 27     27   38 my ($self, $types_table) = @_;
328 27 100       59 return if $types_table->{offset};
329 21 100       44 return if $types_table->{timezone};
330 20 50       77 return if not exists $FIXED_OFFSET{$self->{time_zone}->name};
331 20         102 return $FIXED_OFFSET{$self->{time_zone}->name};
332             }
333              
334             1;
335             __END__
336              
337             =encoding utf-8
338              
339             =for stopwords strptime
340              
341             =head1 NAME
342              
343             Time::Strptime::Format - L<strptime(3)> format compiler and parser.
344              
345             =head1 SYNOPSIS
346              
347             use Time::Strptime::Format;
348              
349             # OO style
350             my $fmt = Time::Strptime::Format->new('%Y-%m-%d %H:%M:%S');
351             my ($epoch_o, $offset_o) = $fmt->parse('2014-01-01 00:00:00');
352              
353             =head1 DESCRIPTION
354              
355             This is L<Time::Strptime> engine.
356              
357             =head1 METHODS
358              
359             This class offers the following methods.
360              
361             =head2 Time::Strptime::Format->new($format, \%args)
362              
363             This methods creates a new format object. It accepts the following arguments:
364              
365             =over 4
366              
367             =item * time_zone
368              
369             The default time zone to use for objects returned from parsing.
370              
371             =item * locale
372              
373             The locale to use for objects returned from parsing.
374              
375             =item * strict
376              
377             Strict range check for date and time.
378              
379             Example. C<"2016-02-31"> is wrong date string, but Time::Strptime parses as C<2016-02-31> in default.
380              
381             =back
382              
383             =head2 $strptime->parse($string)
384              
385             Given a string in the pattern specified in the constructor, this method will return the epoch and offset.
386             If given a string that doesn't match the pattern, the formatter will throw the error.
387              
388             =head1 STRPTIME PATTERN TOKENS
389              
390             The following tokens are allowed in the pattern string for strptime:
391              
392             =over 4
393              
394             =item * %%
395              
396             The % character.
397              
398             =item * %a or %A
399              
400             The weekday name according to the current locale, in abbreviated form or the full name. (ignored)
401              
402             =item * %b or %B or %h
403              
404             The month name according to the current locale, in abbreviated form or the full name.
405              
406             =item * %d or %e
407              
408             The day of month (01-31). This will parse single digit numbers as well.
409              
410             =item * %D
411              
412             Equivalent to %m/%d/%y. (This is the American style date, very confusing to non-Americans, especially since %d/%m/%y is widely used in Europe. The ISO 8601 standard pattern is %F.)
413              
414             =item * %F
415              
416             Equivalent to %Y-%m-%d. (This is the ISO style date)
417              
418             =item * %H
419              
420             The hour (00-23). This will parse single digit numbers as well.
421              
422             =item * %I
423              
424             The hour on a 12-hour clock (1-12).
425              
426             =item * %j
427              
428             The day number in the year (1-366).
429              
430             =item * %m
431              
432             The month number (01-12). This will parse single digit numbers as well.
433              
434             =item * %M
435              
436             The minute (00-59). This will parse single digit numbers as well.
437              
438             =item * %n
439              
440             Arbitrary white-space. (ignored)
441              
442             =item * %p
443              
444             The equivalent of AM or PM according to the locale in use. (See L<DateTime::Locale>)
445              
446             =item * %r
447              
448             Equivalent to %I:%M:%S %p.
449              
450             =item * %R
451              
452             Equivalent to %H:%M.
453              
454             =item * %s
455              
456             Number of seconds since the Epoch.
457              
458             =item * %S
459              
460             The second (0-60; 60 may occur for leap seconds.).
461              
462             =item * %t
463              
464             Tab space. (ignored)
465              
466             =item * %T
467              
468             Equivalent to %H:%M:%S.
469              
470             =item * %Y
471              
472             A 4-digit year, including century (for example, 1991).
473              
474             =item * %z
475              
476             An RFC-822/ISO 8601 standard time zone specification. (e.g. +1100)
477              
478             =item * %Z
479              
480             The time zone name. (e.g. EST)
481              
482             =back
483              
484             =head1 LICENSE
485              
486             Copyright (C) karupanerura.
487              
488             This library is free software; you can redistribute it and/or modify
489             it under the same terms as Perl itself.
490              
491             =head1 AUTHOR
492              
493             karupanerura E<lt>karupa@cpan.orgE<gt>
494              
495             =cut