File Coverage

blib/lib/Time/Str.pm
Criterion Covered Total %
statement 252 254 99.2
branch 129 142 90.8
condition 70 86 81.4
subroutine 35 35 100.0
pod 3 26 11.5
total 489 543 90.0


line stmt bran cond sub pod time code
1             package Time::Str;
2 17     17   1789298 use strict;
  17         32  
  17         578  
3 17     17   144 use warnings;
  17         26  
  17         774  
4 17     17   292 use v5.10;
  17         55  
5              
6             our $VERSION = '0.03';
7             our @EXPORT_OK = qw[ time2str str2time str2date ];
8             our %EXPORT_TAGS = ( all => \@EXPORT_OK );
9              
10 17     17   107 use Exporter qw[import];
  17         45  
  17         545  
11 17     17   61 use Carp qw[croak];
  17         26  
  17         936  
12 17     17   8039 use POSIX qw[floor];
  17         136537  
  17         81  
13              
14             my $DefaultPivotYear = 1950;
15              
16             sub MIN_TIME () { -62135596800 } # 0001-01-01T00:00:00Z
17             sub MAX_TIME () { 253402300799 } # 9999-12-31T23:59:59Z
18              
19             sub NANOS_PER_SECOND () { 1_000_000_000 }
20              
21             BEGIN {
22 17     17   81709 *DEFAULT_PRECISION = (length pack('F', 0) > 8) ? sub () {9} : sub () {6};
23             }
24              
25             my %MonthIndexMap = qw(
26             i 1 jan 1 january 1
27             ii 2 feb 2 february 2
28             iii 3 mar 3 march 3
29             iv 4 apr 4 april 4
30             v 5 may 5
31             vi 6 jun 6 june 6
32             vii 7 jul 7 july 7
33             viii 8 aug 8 august 8
34             ix 9 sep 9 september 9 sept 9
35             x 10 oct 10 october 10
36             xi 11 nov 11 november 11
37             xii 12 dec 12 december 12
38             );
39              
40             my %MeridiemMap = qw(
41             am AM a.m. AM
42             pm PM p.m. PM
43             );
44              
45             #
46             # Generic DateTime
47             #
48             # Parses a broad set of real-world date and time formats, accepting only
49             # those that can be parsed deterministically. Numeric-only dates must use
50             # Y-M-D order with separators. Any other ordering requires the month to
51             # be given as a name or Roman numeral. Every date must include a four-digit
52             # year. Optional time components include hours, minutes, seconds, fractional
53             # seconds, AM/PM, and time zones. Parsing is structurally deterministic;
54             # semantic validation occurs after matching.
55             #
56             # ISO 8601 - Date and time format:
57             # 2012-12-24
58             # 2012-12-24T15:30
59             # 2012-12-24T15:30+01
60             # 2012-12-24T15:30:45,500+01
61             #
62             # RFC 3339 - Internet timestamps:
63             # 2012-12-24T15:30:45+01:00
64             # 2012-12-24T15:30:45.500+01:00
65             #
66             # RFC 9557 - Timestamps with additional information:
67             # 2012-12-24T15:30:45.500+01:00[Europe/Stockholm]
68             #
69             # RFC 2822 - Internet Message Format:
70             # Mon, 24 Dec 2012 15:30:45 +0100
71             # Mon, 24 Dec 2012 15:30 +0100
72             # 24 Dec 2012 15:30:45 +0100
73             # 24 Dec 2012 15:30 +0100
74             #
75             # RFC 2616 - HTTP-date:
76             # Mon, 24 Dec 2012 15:30:45 GMT
77             #
78             # RFC 9051 - IMAP date-time:
79             # 24-Dec-2012 15:30:45 +0100
80             #
81             # ISO 9075 - SQL timestamp w/ and w/o zone:
82             # 2012-12-24 15:30:45
83             # 2012-12-24 15:30:45 +01:00
84             # 2012-12-24 15:30:45.500
85             # 2012-12-24 15:30:45.500 +01:00
86             #
87             # ECMAScript Date.prototype.toString:
88             # Mon Dec 24 2012 15:30:45 GMT+0100 (Central European Time)
89             #
90             # Long-form Textual:
91             # Monday, 24 December 2012, 15:30 GMT+1
92             # Monday, 24th December 2012 at 3:30 pm UTC+1 (CET)
93             # Monday, December 24, 2012, 3:30 PM
94             # December 24th, 2012 at 3:30 PM
95             #
96             # Short-form Variations:
97             # Dec/24/2012 03:30:45 PM
98             # 24. XII. 2012 12PM UTC+1 (CET)
99             # 24DEC2012 12:30:45.500 UTC+1
100             # 24.Dec.2012 15:30:45
101             #
102             my $GenericDateTime_Rx = qr{
103             (?(DEFINE)
104             (? (?i: Mon|Tue|Tues|Wed|Thu|Thurs|Fri|Sat|Sun))
105             (? (?i: Monday|Tuesday|Wednesday|Thursday|Friday|
106             Saturday|Sunday))
107             (? (?&DayNameShort) | (?&DayNameLong))
108             (? (?&DayName) [.]?[,]? [ ])
109             (? (?i: Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Sept|Oct|Nov|Dec))
110             (? (?i: January|February|March|April|May|June|
111             July|August|September|October|November|December))
112             (? (?&MonthNameShort) | (?&MonthNameLong))
113             (? (?i: I|II|III|IV|V|VI|VII|VIII|IX|X|XI|XII))
114             (? (?&MonthName) | (?&MonthRoman))
115             (? (?i: st|nd|rd|th))
116             (? (?: [AaPp] (?: [Mm] | [.][Mm][.])))
117             (? (?: [+-] (?: [0-9]{4} | [0-9]{1,2} (?: [:][0-9]{2})? )))
118             (? [A-Z][A-Za-z][A-Z]{1,4})
119             )
120              
121             \A
122              
123             # Note: Day names and ordinal suffixes (e.g., "Mon", "th") are matched by the
124             # regex but are not validated against the parsed date. (XXX reconsider this!)
125              
126             (?&DayNamePrefix)?
127              
128             (?:
129             (?:
130             (? [0-9]{4})
131             ([-./]) (? (?&MonthName) | [0-9]{1,2})
132             \g{-2} (? [0-9]{1,2})
133             |
134             (? [0-9]{1,2})
135             ([-./]) (? (?&MonthTextual))
136             \g{-2} (? [0-9]{4})
137             |
138             (? (?&MonthName))
139             ([-./]) (? [0-9]{1,2})
140             \g{-2} (? [0-9]{4})
141             )
142             |
143             (?:
144             (? [0-9]{1,2}) (?: (?&OrdinalSuffix) | [.] )?
145             [ ] (? (?&MonthTextual)) [.,]?
146             [ ] (? [0-9]{4})
147             |
148             (? (?&MonthName)) [.,]?
149             [ ] (? [0-9]{1,2}) (?&OrdinalSuffix)? [,]?
150             [ ] (? [0-9]{4})
151             )
152             |
153             (?:
154             (? [0-9]{4}) (? (?&MonthName)) (? [0-9]{1,2})
155             | (? [0-9]{1,2}) (? (?&MonthTextual)) (? [0-9]{4})
156             )
157             )
158              
159             (?:
160              
161             (?: (?: [ ] (?: [Aa][Tt][ ] )? ) | (?: [,][ ]) | [Tt] )
162              
163             # Note: Dot-separated times (HH.MM or HH.MM.SS) are not accepted; only HH:MM
164             # or HH:MM:SS are allowed. This avoids ambiguity where ISO 8601 decimal hours
165             # or minutes could be misinterpreted as hour–minute or minute–second notation.
166              
167             (?:
168             (? [0-9]{1,2})
169             [:] (? [0-9]{2}) (?: [:] (? [0-9]{2})
170             (?: [.,] (? [0-9]{1,9}) )?)?
171              
172             (?: [ ]? (? (?&Meridiem)) )?
173             |
174             (? [0-9]{1,2})
175             [ ]? (? (?&Meridiem))
176             )
177              
178             (?:
179              
180             [ ]?
181              
182             (?:
183             (? (?&TimeZoneOffset))
184             | (? (?:GMT|UTC)) (?: (? (?&TimeZoneOffset)) )?
185             | (? [Zz])
186             | (? (?&TimeZoneAbbrev))
187             )
188            
189             # Annotation tags as defined in RFC 9557 (IXDTF) and Java’s [ZoneID].
190             (?:
191             (? (?: \[ [^\[\]]+ \] )+ )
192             )?
193              
194             # Accept parenthesized comment (typically time-zone abbreviations
195             # or descriptive zone names).
196             (?:
197             [ ] (?: \( [^()]+ \) )
198             )?
199              
200             )?
201             )?
202              
203             \z
204             }x;
205              
206             # ITU-T X.680 (ISO/IEC 8824-1) Abstract Syntax Notation One (ASN.1)
207             #
208             #
209             #
210             # ASN.1 GeneralizedTime
211             # YYYYMMDDhh[mm[ss]][(.|,)fraction][Z|±hh[mm]]
212             #
213             my $ASN1GT_Rx = qr{
214             \A
215              
216             (? [0-9]{4})
217             (? [0-9]{2})
218             (? [0-9]{2})
219             (? [0-9]{2}) (?: (? [0-9]{2})
220             (?: (? [0-9]{2}))?)?
221              
222             (?: [.,] (? [0-9]{1,9}))?
223              
224             (?:
225             (? [+-][0-9]{2} (?: [0-9]{2})? )
226             | (? [Z])
227             )?
228             \z
229             }x;
230              
231             # ASN.1 UTCTime
232             # YYMMDDhhmm[ss](Z|±hhmm)
233             #
234             my $ASN1UT_Rx = qr{
235             \A
236              
237             (? [0-9]{2})
238             (? [0-9]{2})
239             (? [0-9]{2})
240             (? [0-9]{2})
241             (? [0-9]{2}) (?: (? [0-9]{2}))?
242             (?:
243             (? [+-][0-9]{4})
244             | (? [Z])
245             )
246             \z
247             }x;
248              
249             # W3 Consortium Date and Time Formats
250             #
251             #
252             # YYYY
253             # YYYY-MM
254             # YYYY-MM-DD
255             # YYYY-MM-DDThh:mm:ss[.fraction](Z|±hh:mm)
256             #
257             my $W3CDTF_Rx = qr{
258             \A
259              
260             (? [0-9]{4})
261              
262             (?: [-] (? [0-9]{2})
263             (?: [-] (? [0-9]{2})
264             (?: [T] (? [0-9]{2})
265             [:] (? [0-9]{2})
266             [:] (? [0-9]{2}) (?: [.] (? [0-9]{1,9}) )?
267             (?:
268             (? [+-][0-9]{2}[:][0-9]{2})
269             | (? [Z])
270             )
271             )?)?)?
272             \z
273             }x;
274              
275             # RFC 2616 Hypertext Transfer Protocol (HTTP/1.1)
276             #
277             #
278             #
279             # DDD, DD MMM YYYY hh:mm:ss GMT # IMF-fixdate
280             # DDDD, DD-MMM-YY hh:mm:ss GMT # RFC 850
281             # DDD MMM (_D|DD) hh:mm:ss YYYY # ANSI C's ctime
282             #
283             my $RFC2616_Rx = qr{
284             (?(DEFINE)
285             (? (?: Mon|Tue|Wed|Thu|Fri|Sat|Sun))
286             (? (?: Monday|Tuesday|Wednesday|Thursday|Friday|
287             Saturday|Sunday))
288             (? (?: Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec))
289             )
290             \A
291             (?:
292             # IMF-fixdate
293             (?:
294             (?&DayNameShort) [,]
295             [ ] (? [0-9]{2})
296             [ ] (? (?&MonthNameShort))
297             [ ] (? [0-9]{4})
298             [ ] (? [0-9]{2})
299             [:] (? [0-9]{2})
300             [:] (? [0-9]{2})
301             [ ] (? GMT)
302             )
303             | # RFC 850
304             (?:
305             (?&DayNameLong) [,]
306             [ ] (? [0-9]{2})
307             [-] (? (?&MonthNameShort))
308             [-] (? [0-9]{2})
309             [ ] (? [0-9]{2})
310             [:] (? [0-9]{2})
311             [:] (? [0-9]{2})
312             [ ] (? GMT)
313             )
314             | # ANSI C's ctime
315             (?:
316             (?&DayNameShort)
317             [ ] (? (?&MonthNameShort))
318             (?:
319             (?: [ ]{2} (? [0-9]{1}))
320             | (?: [ ]{1} (? [0-9]{2}))
321             )
322             [ ] (? [0-9]{2})
323             [:] (? [0-9]{2})
324             [:] (? [0-9]{2})
325             [ ] (? [0-9]{4})
326             )
327             )
328             \z
329             }x;
330              
331             # RFC 2822 Internet Message Format
332             #
333             #
334             #
335             # [DDD,] D MMM YYYY hh:mm[:ss] (±hhmm|UT|UTC|GMT|ZONE)
336             #
337             my $RFC2822_Rx = qr{
338             (?(DEFINE)
339             (? (?i: Mon|Tue|Wed|Thu|Fri|Sat|Sun))
340             (? (?i: Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec))
341             (? [A-Z][A-Za-z][A-Z]{1,4})
342             (? \( (?: \\\( | \\\) | [^()] | (?&NestedComment) )* \) )
343             )
344             \A
345             (?: \s* (?&DayName)[,] )?
346             \s* (? [0-9]{1,2})
347             \s+ (? (?&MonthName))
348             \s+ (? [0-9]{4})
349             \s+ (? [0-9]{2})
350             [:] (? [0-9]{2}) (?: [:](? [0-9]{2}))?
351             \s+
352             (?:
353             (? [+-][0-9]{4})
354             | (? UT[C]?|GMT)
355             | (? (?&TimeZoneAbbrev))
356             )
357             (?: \s+ (?&NestedComment) )?
358             \z
359             }x;
360              
361             # RFC 3339 Date and Time on the Internet: Timestamps
362             #
363             #
364             # YYYY-MM-DD(T|t|space)hh:mm:ss[.fraction](Z|z|±hh:mm)
365             #
366             my $RFC3339_Rx = qr{
367             \A
368             (? [0-9]{4})
369             [-] (? [0-9]{2})
370             [-] (? [0-9]{2})
371             [Tt ] (? [0-9]{2})
372             [:] (? [0-9]{2})
373             [:] (? [0-9]{2}) (?: [.] (? [0-9]{1,9}) )?
374             (?:
375             (? [+-][0-9]{2}[:][0-9]{2})
376             | (? [Zz])
377             )
378             \z
379             }x;
380              
381             # RFC 4287 Atom Format
382             #
383             #
384             # YYYY-MM-DDThh:mm:ss[.fraction](Z|±hh:mm)
385             #
386             my $RFC4287_Rx = qr{
387             \A
388             (? [0-9]{4})
389             [-] (? [0-9]{2})
390             [-] (? [0-9]{2})
391             [T] (? [0-9]{2})
392             [:] (? [0-9]{2})
393             [:] (? [0-9]{2}) (?: [.] (? [0-9]{1,9}) )?
394             (?:
395             (? [+-][0-9]{2}[:][0-9]{2})
396             | (? [Z])
397             )
398             \z
399             }x;
400              
401             # RFC 5280 PKIX Certificate and CRL Profile (x509)
402             #
403             #
404             # Validity; constrained ASN.1 UTCTime or GeneralizedTime.
405             #
406             # YYMMDDhhmmzzZ
407             # YYYYMMDDhhmmssZ
408             #
409             my $RFC5280_Rx = qr{
410             \A
411             (? [0-9]{2}|[0-9]{4})
412             (? [0-9]{2})
413             (? [0-9]{2})
414             (? [0-9]{2})
415             (? [0-9]{2})
416             (? [0-9]{2})
417             (? [Z])
418             \z
419             }x;
420              
421             # RFC 5545 iCalendar
422             #
423             #
424             #
425             # YYYYMMDD
426             # YYYYMMDDThhmmss[Z]
427             #
428             my $RFC5545_Rx = qr{
429             \A
430             (? [0-9]{4})
431             (? [0-9]{2})
432             (? [0-9]{2})
433             (?:
434             [T]
435             (? [0-9]{2})
436             (? [0-9]{2})
437             (? [0-9]{2})
438             (? [Z])?
439             )?
440             \z
441             }x;
442              
443             # ISO 9075 Database Language SQL — Part 2: Foundation (SQL/Foundation)
444             #
445             #
446             # YYYY-MM-DD
447             # YYYY-MM-DD hh:mm:ss[.fraction]
448             # YYYY-MM-DD hh:mm:ss[.fraction] ±hh:mm
449             #
450             my $ISO9075_Rx = qr{
451             \A
452             (? [0-9]{4})
453             [-] (? [0-9]{2})
454             [-] (? [0-9]{2})
455             (?:
456             [ ] (? [0-9]{2})
457             [:] (? [0-9]{2})
458             [:] (? [0-9]{2}) (?: [.] (? [0-9]{1,9}) )?
459             (?:
460             [ ] (? [+-][0-9]{2}[:][0-9]{2})
461             )?
462             )?
463             \z
464             }x;
465              
466             # Common Log Format
467             #
468             #
469             #
470             # DD/MMM/YYYY:hh:mm:ss[.fraction] ±hhmm
471             #
472             my $CommonLogFormat_Rx = qr{
473             (?(DEFINE)
474             (? (?: Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec))
475             )
476             \A
477             (? [0-9]{2})
478             [/] (? (?&MonthName))
479             [/] (? [0-9]{4})
480             [:] (? [0-9]{2})
481             [:] (? [0-9]{2})
482             [:] (? [0-9]{2}) (?: [.] (? [0-9]{1,9}) )?
483             [ ] (? [+-][0-9]{4})
484             \z
485             }x;
486              
487             # ANSI/ISO C ctime
488             #
489             #
490             #
491             # DDD MMM (_D|DD) hh:mm:ss YYYY
492             #
493             my $ANSIC_Rx = qr{
494             (?(DEFINE)
495             (? (?: Mon|Tue|Wed|Thu|Fri|Sat|Sun))
496             (? (?: Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec))
497             )
498             \A
499             (?:
500             (?&DayName)
501             [ ] (? (?&MonthName))
502             (?:
503             (?: [ ]{2} (? [0-9]{1}))
504             | (?: [ ]{1} (? [0-9]{2}))
505             )
506             [ ] (? [0-9]{2})
507             [:] (? [0-9]{2})
508             [:] (? [0-9]{2})
509             [ ] (? [0-9]{4})
510             )
511             \z
512             }x;
513              
514             # Unix Date
515             #
516             #
517             # The date command output format.
518             #
519             # DDD MMM (_D|DD) hh:mm:ss (±hhmm|UTC|GMT|ZONE) YYYY
520             # DDD MMM (_D|DD) hh:mm:ss YYYY (±hhmm|UTC|GMT|ZONE)
521             #
522             my $UnixDate_Rx = qr{
523             (?(DEFINE)
524             (? (?: Mon|Tue|Wed|Thu|Fri|Sat|Sun))
525             (? (?: Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec))
526             (? [A-Z][A-Za-z][A-Z]{1,4})
527             )
528             \A
529             (?:
530             (?&DayName)
531             [ ] (? (?&MonthName))
532             (?:
533             (?: [ ]{2} (? [0-9]{1}))
534             | (?: [ ]{1} (? [0-9]{2}))
535             )
536             [ ] (? [0-9]{2})
537             [:] (? [0-9]{2})
538             [:] (? [0-9]{2})
539             [ ]
540             (?:
541             (?:
542             (? [+-][0-9]{4})
543             | (? UTC|GMT)
544             | (? (?&TimeZoneAbbrev))
545             )
546             [ ] (? [0-9]{4})
547             |
548             (? [0-9]{4})
549             [ ]
550             (?:
551             (? [+-][0-9]{4})
552             | (? UTC|GMT)
553             | (? (?&TimeZoneAbbrev))
554             )
555             )
556             )
557             \z
558             }x;
559              
560             # Git Date
561             #
562             #
563             # The default date format used by Git.
564             #
565             # DDD MMM D hh:mm:ss YYYY ±hhmm
566             #
567             my $GitDate_Rx = qr{
568             (?(DEFINE)
569             (? (?: Mon|Tue|Wed|Thu|Fri|Sat|Sun))
570             (? (?: Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec))
571             )
572             \A
573             (?&DayName)
574             [ ] (? (?&MonthName))
575             [ ] (? [0-9]{1,2})
576             [ ] (? [0-9]{2})
577             [:] (? [0-9]{2})
578             [:] (? [0-9]{2})
579             [ ] (? [0-9]{4})
580             [ ] (? [+-][0-9]{4})
581             \z
582             }x;
583              
584             # Ruby Date
585             #
586             # Popularized by Ruby on Rails and Twitter.
587             #
588             # DDD MMM DD hh:mm:ss ±hhmm YYYY
589             #
590             my $RubyDate_Rx = qr{
591             (?(DEFINE)
592             (? (?: Mon|Tue|Wed|Thu|Fri|Sat|Sun))
593             (? (?: Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec))
594             )
595             \A
596             (?&DayName)
597             [ ] (? (?&MonthName))
598             [ ] (? [0-9]{2})
599             [ ] (? [0-9]{2})
600             [:] (? [0-9]{2})
601             [:] (? [0-9]{2})
602             [ ] (? [+-][0-9]{4})
603             [ ] (? [0-9]{4})
604             \z
605             }x;
606              
607             sub leap_year {
608 7     7 0 16 my ($y) = @_;
609 7   66     273 return ($y % 4 == 0 && ($y % 100 != 0 || $y % 400 == 0));
610             }
611              
612             # 1 <= $m <= 12
613             sub month_days {
614 49     49 0 118 my ($y, $m) = @_;
615 49 100 100     147 return 29 if $m == 2 && leap_year($y);
616 43         334 return (0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)[$m];
617             }
618              
619             sub valid_ymd {
620 720     720 0 1440 my ($y, $m, $d) = @_;
621 720   66     6517 return ($y >= 1 && $y <= 9999)
622             && ($m >= 1 && $m <= 12)
623             && ($d >= 1 && ($d <= 28 || $d <= month_days($y, $m)));
624             }
625              
626             sub valid_hm {
627 237     237 0 426 my ($h, $m) = @_;
628 237   66     1365 return ($h >= 0 && $h <= 23
629             && $m >= 0 && $m <= 59);
630             }
631              
632             sub valid_hms {
633 598     598 0 1100 my ($h, $m, $s) = @_;
634 598   100     4839 return ($h >= 0 && $h <= 23
635             && $m >= 0 && $m <= 59
636             && $s >= 0 && $s <= 60);
637             }
638              
639             sub expand_two_digit_year {
640 41 50   41 0 80 @_ == 2 or croak q/Usage: expand_two_digit_year(yy, pivot_year)/;
641 41         82 my ($yy, $pivot_year) = @_;
642              
643 41 50 33     146 ($pivot_year >= 0 && $pivot_year <= 9899)
644             or croak q/Parameter 'pivot_year' is out of range (0-9899)/;
645              
646 17     17   8920 use integer;
  17         251  
  17         151  
647 41         59 my $century = $pivot_year / 100;
648 41         63 my $base = $century * 100;
649 41         78 my $pivot_offset = $pivot_year - $base;
650              
651 41         80 my $year = $base + $yy;
652 41 100       80 if ($yy < $pivot_offset) {
653 25         39 $year += 100;
654             }
655 41         85 return $year;
656             }
657              
658             sub meridiem_to_24h {
659 22 50   22 0 34 @_ == 2 or croak q/Usage: meridiem_to_24h(hour, meridiem)/;
660 22         33 my ($hour, $meridiem) = @_;
661              
662 22 50 33     56 ($hour >= 1 && $hour <= 12)
663             or croak q/Parameter 'hour' is out of range (1-12)/;
664              
665 22 50 66     55 ($meridiem eq 'AM' || $meridiem eq 'PM')
666             or croak q/Parameter 'meridiem' is not AM or PM/;
667              
668 22 100       85 return $meridiem eq 'AM' ? ($hour == 12 ? 0 : $hour)
    100          
    100          
669             : ($hour == 12 ? 12 : $hour + 12);
670             }
671              
672             sub parse_numeric_offset {
673 237 50   237 0 451 @_ == 1 or croak q/Usage: parse_numeric_offset(string)/;
674 237         430 my ($string) = @_;
675              
676 237         566 $string =~ s/://; # ±H ±HH ±H:MM ±HH:MM ±HHMM
677 237 50       1594 my ($sign, $h, $m) = ($string =~ m/^([+-])([0-9]{1,2})([0-9]{2})?$/)
678             or croak q/Unable to parse: timezone offset is invalid/;
679              
680 237   100     616 $m //= 0;
681 237 100       471 valid_hm($h, $m)
682             or croak qq/Unable to parse: timezone offset is out of range ($string)/;
683              
684 236         520 my $offset = $h * 60 + $m;
685 236 100       451 if ($sign eq '-') {
686 72         95 $offset *= -1;
687             }
688              
689 236         452 return $offset;
690             }
691              
692             my %RegexpMap = (
693             ansic => $ANSIC_Rx,
694             asn1gt => $ASN1GT_Rx,
695             asn1ut => $ASN1UT_Rx,
696             atom => $RFC4287_Rx,
697             clf => $CommonLogFormat_Rx,
698             ctime => $ANSIC_Rx,
699             email => $RFC2822_Rx,
700             generic => $GenericDateTime_Rx,
701             git => $GitDate_Rx,
702             http => $RFC2616_Rx,
703             ical => $RFC5545_Rx,
704             imf => $RFC2822_Rx,
705             iso9075 => $ISO9075_Rx,
706             rfc2616 => $RFC2616_Rx,
707             rfc2822 => $RFC2822_Rx,
708             rfc3339 => $RFC3339_Rx,
709             rfc4287 => $RFC4287_Rx,
710             rfc5280 => $RFC5280_Rx,
711             rfc5322 => $RFC2822_Rx,
712             rfc5545 => $RFC5545_Rx,
713             rfc7231 => $RFC2616_Rx,
714             ruby => $RubyDate_Rx,
715             sql => $ISO9075_Rx,
716             unix => $UnixDate_Rx,
717             w3c => $W3CDTF_Rx,
718             w3cdtf => $W3CDTF_Rx,
719             x509 => $RFC5280_Rx,
720             );
721              
722             sub str2date {
723 729 100   729 1 3337880 @_ & 1 or croak q/Usage: str2date(string [, format => 'RFC3339' ])/;
724 727         1937 my ($string, %p) = @_;
725              
726 727         1066 my $format = 'rfc3339';
727 727         945 my $pivot_year = $DefaultPivotYear;
728 727         930 my $regexp = $RFC3339_Rx;
729              
730 727 100       1556 if (exists $p{format}) {
731 717         5380 $format = lc delete $p{format};
732 717         1226 $regexp = $RegexpMap{$format};
733              
734 717 100       1556 (defined $regexp)
735             or croak qq/Parameter 'format' is unknown: '$format'/;
736             }
737              
738 726 100       1299 if (exists $p{pivot_year}) {
739 8         16 $pivot_year = delete $p{pivot_year};
740              
741 8 100 100     192 ($pivot_year >= 0 && $pivot_year <= 9899)
742             or croak q/Parameter 'pivot_year' is out of range (0-9899)/;
743             }
744              
745 724 100       1280 if (%p) {
746 2         135 croak "Unknown named parameter: " . join ', ', sort keys %p;
747             }
748              
749 722 100 66     13128 (defined $string && $string =~ $regexp)
750             or croak qq/Unable to parse: string does not match the $format format/;
751              
752 720         15649 my %r = %+;
753              
754 720 100 100     5397 if (exists $r{month} && $r{month} !~ /^[0-9]/) {
755 248         587 $r{month} = $MonthIndexMap{ lc $r{month} };
756             }
757              
758 720 50       1365 if (exists $r{year}) {
759              
760 720 100       1428 if (length $r{year} == 2) {
761 41         104 $r{year} = expand_two_digit_year($r{year}, $pivot_year);
762             }
763              
764 720 100 100     2387 valid_ymd($r{year}, $r{month} // 1, $r{day} // 1)
      100        
765             or croak q/Unable to parse: date is out of range/;
766             }
767              
768 714 100       1415 if (exists $r{hour}) {
769              
770 598 100       1221 if (exists $r{meridiem}) {
771 22 50 33     62 ($r{hour} >= 1 && $r{hour} <= 12)
772             or croak q/Unable to parse: hour is out of range for 12-hour clock/;
773              
774 22         55 $r{hour} = meridiem_to_24h($r{hour}, $MeridiemMap{ lc delete $r{meridiem} });
775             }
776              
777 598 100 100     1815 valid_hms($r{hour}, $r{minute} // 0, $r{second} // 0)
      100        
778             or croak q/Unable to parse: time of day is out of range/;
779              
780 592 100       1241 if (exists $r{fraction}) {
781 238         428 my $f = delete $r{fraction};
782 238         622 my $ns = $f * (10 ** (9 - length $f));
783              
784 238 100       461 if (exists $r{second}) {
    100          
785             # HH.MM.SS.fraction
786 220         462 $r{nanosecond} = $ns;
787             }
788             elsif (exists $r{minute}) {
789             # HH.MM.fraction
790 10         18 my $total_ns = $ns * 60;
791 10         32 $r{second} = int($total_ns / NANOS_PER_SECOND);
792 10         20 my $nsec = $total_ns % NANOS_PER_SECOND;
793 10 100       25 if ($nsec != 0) {
794 6         15 $r{nanosecond} = $nsec;
795             }
796             }
797             else {
798             # HH.fraction
799 8         18 my $total_ns = $ns * 3600;
800 8         27 my $min = int($total_ns / (60 * NANOS_PER_SECOND));
801 8         26 $r{minute} = $min;
802 8         15 $total_ns -= $min * 60 * NANOS_PER_SECOND;
803 8         16 my $sec = int($total_ns / NANOS_PER_SECOND);
804 8         16 my $nsec = $total_ns % NANOS_PER_SECOND;
805 8 100 66     37 if ($sec != 0 || $nsec != 0) {
806 2         4 $r{second} = $sec;
807 2 100       6 if ($nsec != 0) {
808 1         4 $r{nanosecond} = $nsec;
809             }
810             }
811             }
812             }
813              
814 592 100       1066 if (exists $r{tz_offset}) {
815 237         486 $r{tz_offset} = parse_numeric_offset($r{tz_offset});
816             }
817              
818 591 100       1038 if (exists $r{tz_utc}) {
819 314   100     1058 $r{tz_offset} //= 0;
820             }
821             }
822              
823 707 100 100     1654 if ($regexp == $RFC2616_Rx && !$r{tz_utc}) {
824 2         5 $r{tz_utc} = 'GMT';
825 2         2 $r{tz_offset} = 0;
826             }
827              
828             {
829 707         932 local @r{qw(tz_utc tz_abbrev tz_annotation)};
  707         2347  
830 707         4625 $_ += 0 for values %r;
831             }
832 707 50       2551 return wantarray ? %r : \%r;
833             }
834              
835             sub str2time {
836 140 100   140 1 86862 @_ & 1 or croak q/Usage: str2time(string [, format => 'RFC3339' ])/;
837 138         472 my ($string, %p) = @_;
838              
839 138         208 my $precision = DEFAULT_PRECISION;
840              
841 138 100       321 if (exists $p{precision}) {
842 52         92 $precision = delete $p{precision};
843 52 100 100     373 ($precision >= 0 && $precision <= 9)
844             or croak(q/Parameter 'precision' is out of range (0-9)/);
845             }
846              
847 136         404 my $r = str2date($string, %p);
848              
849             (exists $r->{tz_offset})
850 136 100       618 or croak q/Unable to convert to time: no timezone offset or UTC designator/;
851              
852 134         531 my ($Y, $M, $D, $h, $m, $s) = @$r{qw(year month day hour minute second)};
853 134   50     238 $m //= 0;
854 134   100     248 $s //= 0;
855              
856 134         155 my $rdn = do {
857 17     17   21957 use integer;
  17         35  
  17         75  
858 134 100       289 if ($M < 3) {
859 58         113 $Y--, $M += 12;
860             }
861 134         387 (1461 * $Y) / 4 - $Y / 100 + $Y / 400
862             + $D + ((979 * $M - 2918) >> 5) - 306;
863             };
864 134         280 my $sod = ($h * 60 + $m) * 60 + $s;
865 134         283 my $time = ($rdn - 719163) * 86400 + $sod - $r->{tz_offset} * 60;
866 134 100       284 if (exists $r->{nanosecond}) {
867 50         82 my $scale = 10 ** $precision;
868 50         120 my $fraction = int($r->{nanosecond} * $scale / 1E9);
869 50         93 $time += $fraction / $scale;
870             }
871 134         658 return $time;
872             }
873              
874             {
875             my @DoW = qw[Sun Mon Tue Wed Thu Fri Sat];
876             my @MoY = qw[Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec];
877              
878             sub format_offset_basic {
879 39     39 0 63 my ($offset, $zulu) = @_;
880              
881 39 100       91 if ($offset == 0) {
882 25         75 return $zulu;
883             }
884             else {
885 14 100       29 my $sign = $offset < 0 ? -1 : 1;
886 14         20 my $min = abs $offset;
887 14         75 return sprintf '%+.4d', $sign * int($min / 60) * 100 + $min % 60;
888             }
889             }
890              
891             sub format_offset_extended {
892 39     39 0 69 my ($offset, $zulu) = @_;
893              
894 39 100       96 if ($offset == 0) {
895 31         67 return $zulu;
896             }
897             else {
898 8 100       69 my $sign = $offset < 0 ? ord '-' : ord '+';
899 8         54 my $min = abs $offset;
900 8         56 return sprintf '%c%.2d:%.2d', $sign, int($min / 60), $min % 60;
901             }
902             }
903              
904             sub format_ASN1UT {
905 5     5 0 9 my ($time, $offset) = @_;
906              
907 5         8 $time += $offset * 60;
908 5         30 my ($sec, $min, $hour, $mday, $mon, $year) = gmtime $time;
909 5         15 my $zstr = format_offset_basic($offset, 'Z');
910 5         39 return sprintf '%02d%02d%02d%02d%02d%02d%s',
911             ($year + 1900) % 100, $mon + 1, $mday, $hour, $min, $sec, $zstr;
912             }
913              
914             sub format_ASN1GT {
915 11     11 0 29 my ($time, $offset, $fraction) = @_;
916              
917 11         48 $time += $offset * 60;
918 11         51 my ($sec, $min, $hour, $mday, $mon, $year) = gmtime $time;
919 11         32 my $zstr = format_offset_basic($offset, 'Z');
920 11         86 return sprintf '%04d%02d%02d%02d%02d%02d%s%s',
921             $year + 1900, $mon + 1, $mday, $hour, $min, $sec, $fraction, $zstr;
922             }
923            
924             sub format_CLF {
925 5     5 0 10 my ($time, $offset, $fraction) = @_;
926              
927 5         6 $time += $offset * 60;
928 5         12 my ($sec, $min, $hour, $mday, $mon, $year) = gmtime $time;
929 5         11 my $zstr = format_offset_basic($offset, '+0000');
930 5         26 return sprintf '%02d/%s/%04d:%02d:%02d:%02d%s %s',
931             $mday, $MoY[$mon], $year + 1900, $hour, $min, $sec, $fraction, $zstr;
932             }
933              
934             sub format_RFC2616 {
935 5     5 0 7 my ($time, $offset) = @_;
936              
937 5         18 my ($sec, $min, $hour, $mday, $mon, $year, $wday) = gmtime $time;
938 5         43 return sprintf '%s, %02d %s %04d %02d:%02d:%02d GMT',
939             $DoW[$wday], $mday, $MoY[$mon], $year + 1900, $hour, $min, $sec;
940             }
941              
942             sub format_RFC2822 {
943 3     3 0 5 my ($time, $offset) = @_;
944              
945 3         3 $time += $offset * 60;
946 3         9 my ($sec, $min, $hour, $mday, $mon, $year, $wday) = gmtime $time;
947 3         7 my $zstr = format_offset_basic($offset, '+0000');
948 3         14 return sprintf '%s, %d %s %04d %02d:%02d:%02d %s',
949             $DoW[$wday], $mday, $MoY[$mon], $year + 1900, $hour, $min, $sec, $zstr;
950             }
951              
952             sub format_RFC3339 {
953 35     35 0 65 my ($time, $offset, $fraction) = @_;
954              
955 35         55 $time += $offset * 60;
956 35         187 my ($sec, $min, $hour, $mday, $mon, $year) = gmtime $time;
957 35         98 my $zstr = format_offset_extended($offset, 'Z');
958 35         252 return sprintf '%04d-%02d-%02dT%02d:%02d:%02d%s%s',
959             $year + 1900, $mon + 1, $mday, $hour, $min, $sec, $fraction, $zstr;
960             }
961              
962             sub TIME_20500101_000000 () { 2524608000 }
963              
964             sub format_RFC5280 {
965 4     4 0 8 my ($time) = @_;
966              
967 4 100       6 if ($time < TIME_20500101_000000) {
968 2         4 return format_ASN1UT($time, 0);
969             }
970             else {
971 2         5 return format_ASN1GT($time, 0, '');
972             }
973             }
974              
975             sub format_RFC5545 {
976 3     3 0 5 my ($time) = @_;
977              
978 3         11 my ($sec, $min, $hour, $mday, $mon, $year) = gmtime $time;
979 3         17 return sprintf '%04d%02d%02dT%02d%02d%02dZ',
980             $year + 1900, $mon + 1, $mday, $hour, $min, $sec;
981             }
982              
983             sub format_ISO9075 {
984 4     4 0 11 my ($time, $offset, $fraction) = @_;
985              
986 4         9 $time += $offset * 60;
987 4         33 my ($sec, $min, $hour, $mday, $mon, $year) = gmtime $time;
988 4         13 my $zstr = format_offset_extended($offset, '+00:00');
989 4         32 return sprintf '%04d-%02d-%02d %02d:%02d:%02d%s %s',
990             $year + 1900, $mon + 1, $mday, $hour, $min, $sec, $fraction, $zstr;
991             }
992              
993             sub format_ANSIC {
994 3     3 0 8 my ($time) = @_;
995 3         28 return scalar gmtime $time;
996             }
997              
998             sub format_UnixDate {
999 5     5 0 9 my ($time, $offset) = @_;
1000              
1001 5         5 $time += $offset * 60;
1002 5         16 my ($sec, $min, $hour, $mday, $mon, $year, $wday) = gmtime $time;
1003 5         33 my $zstr = format_offset_basic($offset, 'UTC');
1004 5         31 return sprintf '%s %s %2d %02d:%02d:%02d %s %04d',
1005             $DoW[$wday], $MoY[$mon], $mday, $hour, $min, $sec, $zstr, $year + 1900;
1006             }
1007              
1008             sub format_RubyDate {
1009 5     5 0 12 my ($time, $offset) = @_;
1010              
1011 5         8 $time += $offset * 60;
1012 5         22 my ($sec, $min, $hour, $mday, $mon, $year, $wday) = gmtime $time;
1013 5         18 my $zstr = format_offset_basic($offset, '+0000');
1014 5         47 return sprintf '%s %s %02d %02d:%02d:%02d %s %04d',
1015             $DoW[$wday], $MoY[$mon], $mday, $hour, $min, $sec, $zstr, $year + 1900;
1016             }
1017              
1018             sub format_GitDate {
1019 5     5 0 8 my ($time, $offset) = @_;
1020              
1021 5         7 $time += $offset * 60;
1022 5         13 my ($sec, $min, $hour, $mday, $mon, $year, $wday) = gmtime $time;
1023 5         11 my $zstr = format_offset_basic($offset, '+0000');
1024 5         28 return sprintf '%s %s %d %02d:%02d:%02d %04d %s',
1025             $DoW[$wday], $MoY[$mon], $mday, $hour, $min, $sec, $year + 1900, $zstr;
1026             }
1027             }
1028              
1029             my %FormatMap = (
1030             ansic => \&format_ANSIC,
1031             asn1gt => \&format_ASN1GT,
1032             asn1ut => \&format_ASN1UT,
1033             atom => \&format_RFC3339,
1034             clf => \&format_CLF,
1035             ctime => \&format_ANSIC,
1036             email => \&format_RFC2822,
1037             git => \&format_GitDate,
1038             http => \&format_RFC2616,
1039             ical => \&format_RFC5545,
1040             imf => \&format_RFC2822,
1041             iso9075 => \&format_ISO9075,
1042             rfc2616 => \&format_RFC2616,
1043             rfc2822 => \&format_RFC2822,
1044             rfc3339 => \&format_RFC3339,
1045             rfc4287 => \&format_RFC3339,
1046             rfc5280 => \&format_RFC5280,
1047             rfc5322 => \&format_RFC2822,
1048             rfc5545 => \&format_RFC5545,
1049             rfc7231 => \&format_RFC2616,
1050             ruby => \&format_RubyDate,
1051             sql => \&format_ISO9075,
1052             unix => \&format_UnixDate,
1053             w3c => \&format_RFC3339,
1054             w3cdtf => \&format_RFC3339,
1055             x509 => \&format_RFC5280,
1056             );
1057              
1058             sub time2str {
1059 104 100   104 1 76419 @_ & 1 or croak(q/Usage: time2str(time [, format => 'RFC3339' ])/);
1060 102         340 my ($time, %p) = @_;
1061              
1062 102 100 100     694 ($time >= MIN_TIME && $time < MAX_TIME + 1)
1063             or croak(q/Parameter 'time' is out of range (0001-01-01T00:00:00Z to 9999-12-31T23:59:59Z)/);
1064              
1065 100         217 my $formatter = \&format_RFC3339;
1066              
1067 100 100       269 if (exists $p{format}) {
1068 90         157 my $format = delete $p{format};
1069              
1070 90         252 $formatter = $FormatMap{ lc $format };
1071 90 100       269 (defined $formatter)
1072             or croak(qq/Parameter 'format' is unknown: '$format'/);
1073             }
1074              
1075 99         195 my ($offset, $precision, $nanosecond) = (0);
1076              
1077 99 100       249 if (exists $p{offset}) {
1078 26         55 $offset = delete $p{offset};
1079 26 100 100     233 ($offset >= -1439 && $offset <= 1439)
1080             or croak(q/Parameter 'offset' is out of range (-1439 to 1439)/);
1081             }
1082              
1083 97 100       218 if (exists $p{precision}) {
1084 42         70 $precision = delete $p{precision};
1085 42 100 100     338 ($precision >= 0 && $precision <= 9)
1086             or croak(q/Parameter 'precision' is out of range (0-9)/);
1087             }
1088              
1089 95 100       198 if (exists $p{nanosecond}) {
1090 17         25 $nanosecond = delete $p{nanosecond};
1091 17 100 100     218 ($nanosecond >= 0 && $nanosecond <= 999_999_999)
1092             or croak(q/Parameter 'nanosecond' is out of range (0-999999999)/);
1093             }
1094              
1095 93 100       178 if (%p) {
1096 2         149 croak "Unknown named parameter: " . join ', ', sort keys %p;
1097             }
1098              
1099 91 100 100     1740 if (!defined $nanosecond && int $time != $time) {
1100 29         118 my $sec = floor($time);
1101 29         71 my $frac = $time - $sec;
1102 29   50     81 my $scale = 10 ** ($precision // DEFAULT_PRECISION);
1103              
1104 29         39 $time = $sec;
1105 29         86 $frac = floor($frac * $scale + 0.5) / $scale;
1106 29         65 $nanosecond = floor($frac * NANOS_PER_SECOND + 0.5);
1107              
1108 29 100       85 if ($nanosecond >= NANOS_PER_SECOND) {
1109 4         10 $nanosecond -= NANOS_PER_SECOND;
1110 4         10 $time++;
1111             }
1112             }
1113              
1114 91 100       226 if ($offset) {
1115 24         46 my $local_time = $time + $offset * 60;
1116              
1117 24 100 100     232 ($local_time >= MIN_TIME && $local_time <= MAX_TIME)
1118             or croak(q/Parameter 'time' is out of range for the given offset/);
1119             }
1120              
1121 89         185 my $fraction = '';
1122 89 100 66     303 if (defined $nanosecond || defined $precision) {
1123              
1124 44 100       80 if (!defined $precision) {
1125 4 100       18 if ($nanosecond == 0) {
    50          
    0          
1126 3         7 $precision = 0;
1127             }
1128             elsif (($nanosecond % 1_000_000) == 0) {
1129 1         3 $precision = 3;
1130             }
1131             elsif (($nanosecond % 1_000) == 0) {
1132 0         0 $precision = 6;
1133             }
1134             else {
1135 0         0 $precision = 9;
1136             }
1137             }
1138              
1139 44 100       83 if ($precision != 0) {
1140 37   50     98 $nanosecond //= 0;
1141 37         175 $fraction = sprintf '.%.*d',
1142             $precision, int($nanosecond / (10 ** (9 - $precision)));
1143             }
1144             }
1145 89         220 return $formatter->($time, $offset, $fraction);
1146             }
1147              
1148             1;