File Coverage

blib/lib/Time/Str.pm
Criterion Covered Total %
statement 243 245 99.1
branch 124 136 91.1
condition 73 91 80.2
subroutine 35 35 100.0
pod 3 26 11.5
total 478 533 89.6


line stmt bran cond sub pod time code
1             package Time::Str;
2 18     18   2185260 use strict;
  18         35  
  18         634  
3 18     18   169 use warnings;
  18         25  
  18         918  
4 18     18   284 use v5.10;
  18         54  
5              
6             our $VERSION = '0.04';
7             our @EXPORT_OK = qw[ time2str str2time str2date ];
8             our %EXPORT_TAGS = ( all => \@EXPORT_OK );
9              
10 18     18   100 use Exporter qw[import];
  18         82  
  18         670  
11 18     18   87 use Carp qw[croak];
  18         28  
  18         1061  
12 18     18   9080 use POSIX qw[floor];
  18         165082  
  18         96  
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 18     18   114166 *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             # YYMMDDhhmmzzZ
405             # YYYYMMDDhhmmssZ
406             #
407             my $RFC5280_Rx = qr{
408             \A
409             (? [0-9]{2}|[0-9]{4})
410             (? [0-9]{2})
411             (? [0-9]{2})
412             (? [0-9]{2})
413             (? [0-9]{2})
414             (? [0-9]{2})
415             (? [Z])
416             \z
417             }x;
418              
419             # RFC 5545 iCalendar
420             #
421             #
422             #
423             # YYYYMMDD
424             # YYYYMMDDThhmmss[Z]
425             #
426             my $RFC5545_Rx = qr{
427             \A
428             (? [0-9]{4})
429             (? [0-9]{2})
430             (? [0-9]{2})
431             (?:
432             [T]
433             (? [0-9]{2})
434             (? [0-9]{2})
435             (? [0-9]{2})
436             (? [Z])?
437             )?
438             \z
439             }x;
440              
441             # ISO 8601
442             #
443             #
444             # Calendar date with optional time of day, a profile of ISO 8601.
445             #
446             # YYYY-MM-DD
447             # YYYY-MM-DDThh[:mm[:ss]][(.|,)fraction][Z|±hh[:mm]]
448             # YYYYMMDD
449             # YYYYMMDDThh[mm[ss]][(.|,)fraction][Z|±hh[mm]]
450              
451             my $ISO8601_Rx = qr{
452             \A
453              
454             (? [0-9]{4})
455              
456             (?: # Extended format
457             (?:
458             [-] (? [0-9]{2})
459             [-] (? [0-9]{2})
460             (?: [T] (? [0-9]{2}) (?: [:] (? [0-9]{2})
461             (?: [:] (? [0-9]{2}))?)?
462              
463             (?: [.,] (? [0-9]{1,9}))?
464              
465             (?:
466             (? [+-][0-9]{2} (?: [:][0-9]{2})? )
467             | (? [Z])
468             )?
469             )?
470             )
471             | # Basic format
472             (?:
473             (? [0-9]{2})
474             (? [0-9]{2})
475             (?: [T] (? [0-9]{2}) (?: (? [0-9]{2})
476             (?: (? [0-9]{2}))?)?
477              
478             (?: [.,] (? [0-9]{1,9}))?
479              
480             (?:
481             (? [+-][0-9]{2} (?: [0-9]{2})? )
482             | (? [Z])
483             )?
484             )?
485             )
486             )
487             \z
488             }x;
489              
490             # ISO 9075 Database Language SQL — Part 2: Foundation (SQL/Foundation)
491             #
492             #
493             # YYYY-MM-DD
494             # YYYY-MM-DD hh:mm:ss[.fraction]
495             # YYYY-MM-DD hh:mm:ss[.fraction] ±hh:mm
496             #
497             my $ISO9075_Rx = qr{
498             \A
499             (? [0-9]{4})
500             [-] (? [0-9]{2})
501             [-] (? [0-9]{2})
502             (?:
503             [ ] (? [0-9]{2})
504             [:] (? [0-9]{2})
505             [:] (? [0-9]{2}) (?: [.] (? [0-9]{1,9}) )?
506             (?:
507             [ ] (? [+-][0-9]{2}[:][0-9]{2})
508             )?
509             )?
510             \z
511             }x;
512              
513             # Common Log Format
514             #
515             #
516             #
517             # DD/MMM/YYYY:hh:mm:ss[.fraction] ±hhmm
518             #
519             my $CommonLogFormat_Rx = qr{
520             (?(DEFINE)
521             (? (?: Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec))
522             )
523             \A
524             (? [0-9]{2})
525             [/] (? (?&MonthName))
526             [/] (? [0-9]{4})
527             [:] (? [0-9]{2})
528             [:] (? [0-9]{2})
529             [:] (? [0-9]{2}) (?: [.] (? [0-9]{1,9}) )?
530             [ ] (? [+-][0-9]{4})
531             \z
532             }x;
533              
534             # ANSI/ISO C ctime
535             #
536             #
537             #
538             # DDD MMM (_D|DD) hh:mm:ss YYYY
539             #
540             my $ANSIC_Rx = qr{
541             (?(DEFINE)
542             (? (?: Mon|Tue|Wed|Thu|Fri|Sat|Sun))
543             (? (?: Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec))
544             )
545             \A
546             (?:
547             (?&DayName)
548             [ ] (? (?&MonthName))
549             (?:
550             (?: [ ]{2} (? [0-9]{1}))
551             | (?: [ ]{1} (? [0-9]{2}))
552             )
553             [ ] (? [0-9]{2})
554             [:] (? [0-9]{2})
555             [:] (? [0-9]{2})
556             [ ] (? [0-9]{4})
557             )
558             \z
559             }x;
560              
561             # Unix Date
562             #
563             #
564             # The date command output format.
565             #
566             # DDD MMM (_D|DD) hh:mm:ss (±hhmm|UTC|GMT|ZONE) YYYY
567             # DDD MMM (_D|DD) hh:mm:ss YYYY (±hhmm|UTC|GMT|ZONE)
568             #
569             my $UnixDate_Rx = qr{
570             (?(DEFINE)
571             (? (?: Mon|Tue|Wed|Thu|Fri|Sat|Sun))
572             (? (?: Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec))
573             (? [A-Z][A-Za-z][A-Z]{1,4})
574             )
575             \A
576             (?:
577             (?&DayName)
578             [ ] (? (?&MonthName))
579             (?:
580             (?: [ ]{2} (? [0-9]{1}))
581             | (?: [ ]{1} (? [0-9]{2}))
582             )
583             [ ] (? [0-9]{2})
584             [:] (? [0-9]{2})
585             [:] (? [0-9]{2})
586             [ ]
587             (?:
588             (?:
589             (? [+-][0-9]{4})
590             | (? UTC|GMT)
591             | (? (?&TimeZoneAbbrev))
592             )
593             [ ] (? [0-9]{4})
594             |
595             (? [0-9]{4})
596             [ ]
597             (?:
598             (? [+-][0-9]{4})
599             | (? UTC|GMT)
600             | (? (?&TimeZoneAbbrev))
601             )
602             )
603             )
604             \z
605             }x;
606              
607             # Git Date
608             #
609             #
610             # The default date format used by Git.
611             #
612             # DDD MMM D hh:mm:ss YYYY ±hhmm
613             #
614             my $GitDate_Rx = qr{
615             (?(DEFINE)
616             (? (?: Mon|Tue|Wed|Thu|Fri|Sat|Sun))
617             (? (?: Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec))
618             )
619             \A
620             (?&DayName)
621             [ ] (? (?&MonthName))
622             [ ] (? [0-9]{1,2})
623             [ ] (? [0-9]{2})
624             [:] (? [0-9]{2})
625             [:] (? [0-9]{2})
626             [ ] (? [0-9]{4})
627             [ ] (? [+-][0-9]{4})
628             \z
629             }x;
630              
631             # Ruby Date
632             #
633             # Popularized by Ruby on Rails and Twitter.
634             #
635             # DDD MMM DD hh:mm:ss ±hhmm YYYY
636             #
637             my $RubyDate_Rx = qr{
638             (?(DEFINE)
639             (? (?: Mon|Tue|Wed|Thu|Fri|Sat|Sun))
640             (? (?: Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec))
641             )
642             \A
643             (?&DayName)
644             [ ] (? (?&MonthName))
645             [ ] (? [0-9]{2})
646             [ ] (? [0-9]{2})
647             [:] (? [0-9]{2})
648             [:] (? [0-9]{2})
649             [ ] (? [+-][0-9]{4})
650             [ ] (? [0-9]{4})
651             \z
652             }x;
653              
654             sub leap_year {
655 7     7 0 48 my ($y) = @_;
656 7   66     315 return ($y % 4 == 0 && ($y % 100 != 0 || $y % 400 == 0));
657             }
658              
659             # 1 <= $m <= 12
660             sub month_days {
661 52     52 0 122 my ($y, $m) = @_;
662 52 100 100     215 return 29 if $m == 2 && leap_year($y);
663 46         389 return (0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)[$m];
664             }
665              
666             sub valid_ymd {
667 807     807 0 1898 my ($y, $m, $d) = @_;
668 807   66     8621 return ($y >= 1 && $y <= 9999)
669             && ($m >= 1 && $m <= 12)
670             && ($d >= 1 && ($d <= 28 || $d <= month_days($y, $m)));
671             }
672              
673             sub valid_hm {
674 265     265 0 492 my ($h, $m) = @_;
675 265   66     1858 return ($h >= 0 && $h <= 23
676             && $m >= 0 && $m <= 59);
677             }
678              
679             sub valid_hms {
680 679     679 0 1333 my ($h, $m, $s) = @_;
681 679   100     6514 return ($h >= 0 && $h <= 23
682             && $m >= 0 && $m <= 59
683             && $s >= 0 && $s <= 60);
684             }
685              
686             sub expand_two_digit_year {
687 41 50   41 0 94 @_ == 2 or croak q/Usage: expand_two_digit_year(yy, pivot_year)/;
688 41         112 my ($yy, $pivot_year) = @_;
689              
690 41 50 33     201 ($pivot_year >= 0 && $pivot_year <= 9899)
691             or croak q/Parameter 'pivot_year' is out of range [0, 9899]/;
692              
693 18     18   10725 use integer;
  18         285  
  18         131  
694 41         72 my $century = $pivot_year / 100;
695 41         73 my $base = $century * 100;
696 41         57 my $pivot_offset = $pivot_year - $base;
697              
698 41         91 my $year = $base + $yy;
699 41 100       89 if ($yy < $pivot_offset) {
700 25         44 $year += 100;
701             }
702 41         93 return $year;
703             }
704              
705             sub meridiem_to_24h {
706 22 50   22 0 57 @_ == 2 or croak q/Usage: meridiem_to_24h(hour, meridiem)/;
707 22         55 my ($hour, $meridiem) = @_;
708              
709 22 50 33     79 ($hour >= 1 && $hour <= 12)
710             or croak q/Parameter 'hour' is out of range [1, 12]/;
711              
712 22 50 66     120 ($meridiem eq 'AM' || $meridiem eq 'PM')
713             or croak q/Parameter 'meridiem' is not AM or PM/;
714              
715 22 100       107 return $meridiem eq 'AM' ? ($hour == 12 ? 0 : $hour)
    100          
    100          
716             : ($hour == 12 ? 12 : $hour + 12);
717             }
718              
719             sub parse_numeric_offset {
720 265 50   265 0 583 @_ == 1 or croak q/Usage: parse_numeric_offset(string)/;
721 265         519 my ($string) = @_;
722              
723 265         716 $string =~ s/://; # ±H ±HH ±H:MM ±HH:MM ±HHMM
724 265 50       1839 my ($sign, $h, $m) = ($string =~ m/^([+-])([0-9]{1,2})([0-9]{2})?$/)
725             or croak q/Unable to parse: timezone offset is invalid/;
726              
727 265   100     797 $m //= 0;
728 265 100       546 valid_hm($h, $m)
729             or croak qq/Unable to parse: timezone offset is out of range/;
730              
731 264         738 my $offset = $h * 60 + $m;
732 264 100       596 if ($sign eq '-') {
733 86         128 $offset *= -1;
734             }
735              
736 264         592 return $offset;
737             }
738              
739             my %RegexpMap = (
740             ansic => $ANSIC_Rx,
741             asn1gt => $ASN1GT_Rx,
742             asn1ut => $ASN1UT_Rx,
743             atom => $RFC4287_Rx,
744             clf => $CommonLogFormat_Rx,
745             ctime => $ANSIC_Rx,
746             email => $RFC2822_Rx,
747             generic => $GenericDateTime_Rx,
748             git => $GitDate_Rx,
749             http => $RFC2616_Rx,
750             ical => $RFC5545_Rx,
751             imf => $RFC2822_Rx,
752             iso8601 => $ISO8601_Rx,
753             iso9075 => $ISO9075_Rx,
754             rfc2616 => $RFC2616_Rx,
755             rfc2822 => $RFC2822_Rx,
756             rfc3339 => $RFC3339_Rx,
757             rfc4287 => $RFC4287_Rx,
758             rfc5280 => $RFC5280_Rx,
759             rfc5322 => $RFC2822_Rx,
760             rfc5545 => $RFC5545_Rx,
761             rfc7231 => $RFC2616_Rx,
762             ruby => $RubyDate_Rx,
763             sql => $ISO9075_Rx,
764             unix => $UnixDate_Rx,
765             w3c => $W3CDTF_Rx,
766             w3cdtf => $W3CDTF_Rx,
767             x509 => $RFC5280_Rx,
768             );
769              
770             sub str2date {
771 815 100   815 1 4132896 @_ & 1 or croak q/Usage: str2date(string [, format => 'RFC3339' ])/;
772 813         2466 my ($string, %p) = @_;
773              
774 813         1861 my ($format, $regexp, $pivot_year) = ('rfc3339', $RFC3339_Rx);
775              
776 813         2794 while (my ($name, $v) = each %p) {
777 813 100       1837 if ($name eq 'format') {
    100          
778 804         1598 $format = lc $v;
779 804         1885 $regexp = $RegexpMap{$format};
780 804 100       3336 (defined $regexp)
781             or croak qq/Parameter 'format' is unknown: '$v'/;
782             }
783             elsif ($name eq 'pivot_year') {
784 8         13 $pivot_year = $v;
785 8 100 100     240 ($pivot_year >= 0 && $pivot_year <= 9899)
786             or croak q/Parameter 'pivot_year' is out of range [0, 9899]/;
787             }
788             else {
789 1         144 croak qq/Unknown named parameter: '$name'/;
790             }
791             }
792              
793 809 100 66     17826 (defined $string && $string =~ $regexp)
794             or croak qq/Unable to parse: string does not match the $format format/;
795              
796 807         20158 my %r = %+;
797              
798 807 100       4793 if (length $r{year} == 2) {
799 41   66     208 $r{year} = expand_two_digit_year($r{year}, $pivot_year // $DefaultPivotYear);
800             }
801              
802 807 100 100     4238 if (exists $r{month} && $r{month} !~ /^[0-9]/) {
803 248         828 $r{month} = $MonthIndexMap{ lc $r{month} };
804             }
805              
806 807 100 100     3261 valid_ymd($r{year}, $r{month} // 1, $r{day} // 1)
      100        
807             or croak q/Unable to parse: date is out of range/;
808              
809 801 100       1845 if (exists $r{hour}) {
810              
811 679 100       1392 if (exists $r{meridiem}) {
812 22 50 33     116 ($r{hour} >= 1 && $r{hour} <= 12)
813             or croak q/Unable to parse: hour is out of range for 12-hour clock/;
814              
815 22         76 $r{hour} = meridiem_to_24h($r{hour}, $MeridiemMap{ lc delete $r{meridiem} });
816             }
817              
818 679 100 100     2458 valid_hms($r{hour}, $r{minute} // 0, $r{second} // 0)
      100        
819             or croak q/Unable to parse: time of day is out of range/;
820              
821 673 100       1433 if (exists $r{fraction}) {
822 277         598 my $f = delete $r{fraction};
823 277         699 my $ns = $f * (10 ** (9 - length $f));
824              
825 277 100       541 if (exists $r{second}) {
    100          
826             # HH.MM.SS.fraction
827 246         474 $r{nanosecond} = $ns;
828             }
829             elsif (exists $r{minute}) {
830             # HH.MM.fraction
831 16         28 my $total_ns = $ns * 60;
832 16         40 $r{second} = int($total_ns / NANOS_PER_SECOND);
833 16         48 my $nsec = $total_ns % NANOS_PER_SECOND;
834 16 100       38 if ($nsec != 0) {
835 6         13 $r{nanosecond} = $nsec;
836             }
837             }
838             else {
839             # HH.fraction
840 15         24 my $total_ns = $ns * 3600;
841 15         30 my $min = int($total_ns / (60 * NANOS_PER_SECOND));
842 15         29 $r{minute} = $min;
843 15         26 $total_ns -= $min * 60 * NANOS_PER_SECOND;
844 15         20 my $sec = int($total_ns / NANOS_PER_SECOND);
845 15         23 my $nsec = $total_ns % NANOS_PER_SECOND;
846 15 100 66     88 if ($sec != 0 || $nsec != 0) {
847 2         3 $r{second} = $sec;
848 2 100       7 if ($nsec != 0) {
849 1         5 $r{nanosecond} = $nsec;
850             }
851             }
852             }
853             }
854              
855 673 100       1319 if (exists $r{tz_offset}) {
856 265         691 $r{tz_offset} = parse_numeric_offset($r{tz_offset});
857             }
858              
859 672 100       1272 if (exists $r{tz_utc}) {
860 361   100     1262 $r{tz_offset} //= 0;
861             }
862             }
863              
864 794 100 100     2069 if ($regexp == $RFC2616_Rx && !$r{tz_utc}) {
865 2         5 $r{tz_utc} = 'GMT';
866 2         4 $r{tz_offset} = 0;
867             }
868              
869             {
870 794         1217 local @r{qw(tz_utc tz_abbrev tz_annotation)};
  794         2870  
871 794         5929 $_ += 0 for values %r;
872             }
873 794 50       3642 return wantarray ? %r : \%r;
874             }
875              
876             sub str2time {
877 159 100   159 1 113232 @_ & 1 or croak q/Usage: str2time(string [, format => 'RFC3339' ])/;
878 157         549 my ($string, %p) = @_;
879              
880 157         302 my $precision;
881              
882 157 100       478 if (exists $p{precision}) {
883 60         110 $precision = delete $p{precision};
884 60 100 100     548 ($precision >= 0 && $precision <= 9)
885             or croak q/Parameter 'precision' is out of range [0, 9]/;
886             }
887              
888 155         422 my $r = str2date($string, %p);
889              
890             (exists $r->{tz_offset})
891 155 100       935 or croak q/Unable to convert: no timezone offset or UTC designator/;
892              
893 153         527 my ($Y, $M, $D, $h, $m, $s) = @$r{qw(year month day hour minute second)};
894 153   50     308 $m //= 0;
895 153   100     281 $s //= 0;
896              
897 153         204 my $rdn = do {
898 18     18   26046 use integer;
  18         33  
  18         74  
899 153 100       303 if ($M < 3) {
900 67         135 $Y--, $M += 12;
901             }
902 153         447 (1461 * $Y) / 4 - $Y / 100 + $Y / 400
903             + $D + ((979 * $M - 2918) >> 5) - 306;
904             };
905 153         280 my $sod = ($h * 60 + $m) * 60 + $s;
906 153         345 my $time = ($rdn - 719163) * 86400 + $sod - $r->{tz_offset} * 60;
907 153 100       347 if (exists $r->{nanosecond}) {
908 58   50     178 my $scale = 10 ** ($precision // DEFAULT_PRECISION);
909 58         148 my $fraction = int($r->{nanosecond} * $scale / 1E9);
910 58         127 $time += $fraction / $scale;
911             }
912 153         1064 return $time;
913             }
914              
915             {
916             my @DoW = qw[Sun Mon Tue Wed Thu Fri Sat];
917             my @MoY = qw[Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec];
918              
919             sub format_offset_basic {
920 39     39 0 127 my ($offset, $zulu) = @_;
921              
922 39 100       148 if ($offset == 0) {
923 25         62 return $zulu;
924             }
925             else {
926 14 100       41 my $sign = $offset < 0 ? -1 : 1;
927 14         49 my $min = abs $offset;
928 14         238 return sprintf '%+.4d', $sign * int($min / 60) * 100 + $min % 60;
929             }
930             }
931              
932             sub format_offset_extended {
933 49     49 0 103 my ($offset, $zulu) = @_;
934              
935 49 100       98 if ($offset == 0) {
936 38         91 return $zulu;
937             }
938             else {
939 11 100       30 my $sign = $offset < 0 ? ord '-' : ord '+';
940 11         21 my $min = abs $offset;
941 11         101 return sprintf '%c%.2d:%.2d', $sign, int($min / 60), $min % 60;
942             }
943             }
944              
945             sub format_ASN1UT {
946 5     5 0 9 my ($time, $offset) = @_;
947              
948 5         10 $time += $offset * 60;
949 5         38 my ($sec, $min, $hour, $mday, $mon, $year) = gmtime $time;
950 5         14 my $zstr = format_offset_basic($offset, 'Z');
951 5         41 return sprintf '%02d%02d%02d%02d%02d%02d%s',
952             ($year + 1900) % 100, $mon + 1, $mday, $hour, $min, $sec, $zstr;
953             }
954              
955             sub format_ASN1GT {
956 11     11 0 33 my ($time, $offset, $fraction) = @_;
957              
958 11         20 $time += $offset * 60;
959 11         45 my ($sec, $min, $hour, $mday, $mon, $year) = gmtime $time;
960 11         27 my $zstr = format_offset_basic($offset, 'Z');
961 11         95 return sprintf '%04d%02d%02d%02d%02d%02d%s%s',
962             $year + 1900, $mon + 1, $mday, $hour, $min, $sec, $fraction, $zstr;
963             }
964            
965             sub format_CLF {
966 5     5 0 19 my ($time, $offset, $fraction) = @_;
967              
968 5         39 $time += $offset * 60;
969 5         28 my ($sec, $min, $hour, $mday, $mon, $year) = gmtime $time;
970 5         25 my $zstr = format_offset_basic($offset, '+0000');
971 5         52 return sprintf '%02d/%s/%04d:%02d:%02d:%02d%s %s',
972             $mday, $MoY[$mon], $year + 1900, $hour, $min, $sec, $fraction, $zstr;
973             }
974              
975             sub format_RFC2616 {
976 5     5 0 11 my ($time, $offset) = @_;
977              
978 5         25 my ($sec, $min, $hour, $mday, $mon, $year, $wday) = gmtime $time;
979 5         80 return sprintf '%s, %02d %s %04d %02d:%02d:%02d GMT',
980             $DoW[$wday], $mday, $MoY[$mon], $year + 1900, $hour, $min, $sec;
981             }
982              
983             sub format_RFC2822 {
984 3     3 0 9 my ($time, $offset) = @_;
985              
986 3         31 $time += $offset * 60;
987 3         16 my ($sec, $min, $hour, $mday, $mon, $year, $wday) = gmtime $time;
988 3         12 my $zstr = format_offset_basic($offset, '+0000');
989 3         30 return sprintf '%s, %d %s %04d %02d:%02d:%02d %s',
990             $DoW[$wday], $mday, $MoY[$mon], $year + 1900, $hour, $min, $sec, $zstr;
991             }
992              
993             sub format_RFC3339 {
994 45     45 0 122 my ($time, $offset, $fraction) = @_;
995              
996 45         85 $time += $offset * 60;
997 45         201 my ($sec, $min, $hour, $mday, $mon, $year) = gmtime $time;
998 45         132 my $zstr = format_offset_extended($offset, 'Z');
999 45         336 return sprintf '%04d-%02d-%02dT%02d:%02d:%02d%s%s',
1000             $year + 1900, $mon + 1, $mday, $hour, $min, $sec, $fraction, $zstr;
1001             }
1002              
1003             sub TIME_20500101 () { 2524608000 }
1004              
1005             sub format_RFC5280 {
1006 4     4 0 6 my ($time) = @_;
1007              
1008 4 100       6 if ($time < TIME_20500101) {
1009 2         5 return format_ASN1UT($time, 0);
1010             }
1011             else {
1012 2         4 return format_ASN1GT($time, 0, '');
1013             }
1014             }
1015              
1016             sub format_RFC5545 {
1017 3     3 0 5 my ($time) = @_;
1018              
1019 3         14 my ($sec, $min, $hour, $mday, $mon, $year) = gmtime $time;
1020 3         22 return sprintf '%04d%02d%02dT%02d%02d%02dZ',
1021             $year + 1900, $mon + 1, $mday, $hour, $min, $sec;
1022             }
1023              
1024             sub format_ISO9075 {
1025 4     4 0 6 my ($time, $offset, $fraction) = @_;
1026              
1027 4         5 $time += $offset * 60;
1028 4         12 my ($sec, $min, $hour, $mday, $mon, $year) = gmtime $time;
1029 4         8 my $zstr = format_offset_extended($offset, '+00:00');
1030 4         21 return sprintf '%04d-%02d-%02d %02d:%02d:%02d%s %s',
1031             $year + 1900, $mon + 1, $mday, $hour, $min, $sec, $fraction, $zstr;
1032             }
1033              
1034             sub format_ANSIC {
1035 3     3 0 6 my ($time) = @_;
1036 3         25 return scalar gmtime $time;
1037             }
1038              
1039             sub format_UnixDate {
1040 5     5 0 9 my ($time, $offset) = @_;
1041              
1042 5         10 $time += $offset * 60;
1043 5         20 my ($sec, $min, $hour, $mday, $mon, $year, $wday) = gmtime $time;
1044 5         16 my $zstr = format_offset_basic($offset, 'UTC');
1045 5         47 return sprintf '%s %s %2d %02d:%02d:%02d %s %04d',
1046             $DoW[$wday], $MoY[$mon], $mday, $hour, $min, $sec, $zstr, $year + 1900;
1047             }
1048              
1049             sub format_RubyDate {
1050 5     5 0 8 my ($time, $offset) = @_;
1051              
1052 5         5 $time += $offset * 60;
1053 5         16 my ($sec, $min, $hour, $mday, $mon, $year, $wday) = gmtime $time;
1054 5         11 my $zstr = format_offset_basic($offset, '+0000');
1055 5         32 return sprintf '%s %s %02d %02d:%02d:%02d %s %04d',
1056             $DoW[$wday], $MoY[$mon], $mday, $hour, $min, $sec, $zstr, $year + 1900;
1057             }
1058              
1059             sub format_GitDate {
1060 5     5 0 7 my ($time, $offset) = @_;
1061              
1062 5         6 $time += $offset * 60;
1063 5         17 my ($sec, $min, $hour, $mday, $mon, $year, $wday) = gmtime $time;
1064 5         10 my $zstr = format_offset_basic($offset, '+0000');
1065 5         29 return sprintf '%s %s %d %02d:%02d:%02d %04d %s',
1066             $DoW[$wday], $MoY[$mon], $mday, $hour, $min, $sec, $year + 1900, $zstr;
1067             }
1068             }
1069              
1070             my %FormatMap = (
1071             ansic => \&format_ANSIC,
1072             asn1gt => \&format_ASN1GT,
1073             asn1ut => \&format_ASN1UT,
1074             atom => \&format_RFC3339,
1075             clf => \&format_CLF,
1076             ctime => \&format_ANSIC,
1077             email => \&format_RFC2822,
1078             git => \&format_GitDate,
1079             http => \&format_RFC2616,
1080             ical => \&format_RFC5545,
1081             imf => \&format_RFC2822,
1082             iso8601 => \&format_RFC3339,
1083             iso9075 => \&format_ISO9075,
1084             rfc2616 => \&format_RFC2616,
1085             rfc2822 => \&format_RFC2822,
1086             rfc3339 => \&format_RFC3339,
1087             rfc4287 => \&format_RFC3339,
1088             rfc5280 => \&format_RFC5280,
1089             rfc5322 => \&format_RFC2822,
1090             rfc5545 => \&format_RFC5545,
1091             rfc7231 => \&format_RFC2616,
1092             ruby => \&format_RubyDate,
1093             sql => \&format_ISO9075,
1094             unix => \&format_UnixDate,
1095             w3c => \&format_RFC3339,
1096             w3cdtf => \&format_RFC3339,
1097             x509 => \&format_RFC5280,
1098             );
1099              
1100             sub time2str {
1101 113 100   113 1 76924 @_ & 1 or croak(q/Usage: time2str(time [, format => 'RFC3339' ])/);
1102 111         447 my ($time, %p) = @_;
1103              
1104 111 100 100     822 ($time >= MIN_TIME && $time < MAX_TIME + 1)
1105             or croak q/Parameter 'time' is out of range/;
1106              
1107 109         309 my ($formatter, $offset, $precision, $nanosecond) = (\&format_RFC3339, 0);
1108              
1109 109         409 while (my ($name, $v) = each %p) {
1110 197 100       564 if ($name eq 'format') {
    100          
    100          
    100          
1111 100         296 $formatter = $FormatMap{lc $v};
1112 100 100       509 (defined $formatter)
1113             or croak qq/Parameter 'format' is unknown: '$v'/;
1114             }
1115             elsif ($name eq 'precision') {
1116 45         66 $precision = $v;
1117 45 100 100     517 ($precision >= 0 && $precision <= 9)
1118             or croak q/Parameter 'precision' is out of range [0, 9]/;
1119             }
1120             elsif ($name eq 'nanosecond') {
1121 22         45 $nanosecond = $v;
1122 22 100 100     260 ($nanosecond >= 0 && $nanosecond <= 999_999_999)
1123             or croak q/Parameter 'nanosecond' is out of range [0, 999_999_999]/;
1124             }
1125             elsif ($name eq 'offset') {
1126 29         78 $offset = $v;
1127 29 100 100     391 ($offset >= -1439 && $offset <= 1439)
1128             or croak q/Parameter 'offset' is out of range [-1439, 1439]/;
1129             }
1130             else {
1131 1         66 croak qq/Unknown named parameter: '$name'/;
1132             }
1133             }
1134              
1135 101 100 100     445 if (!defined $nanosecond && int $time != $time) {
1136 29         114 my $sec = floor($time);
1137 29         121 my $frac = $time - $sec;
1138 29   50     86 my $scale = 10 ** ($precision // DEFAULT_PRECISION);
1139              
1140 29         47 $time = $sec;
1141 29         98 $frac = floor($frac * $scale + 0.5) / $scale;
1142 29         141 $nanosecond = floor($frac * NANOS_PER_SECOND + 0.5);
1143              
1144 29 100       74 if ($nanosecond >= NANOS_PER_SECOND) {
1145 4         11 $nanosecond -= NANOS_PER_SECOND;
1146 4         32 $time++;
1147             }
1148             }
1149              
1150 101 100       236 if ($offset) {
1151 27         58 my $local_time = $time + $offset * 60;
1152              
1153 27 100 100     315 ($local_time >= MIN_TIME && $local_time <= MAX_TIME)
1154             or croak q/Parameter 'time' is out of range for the given offset/;
1155             }
1156              
1157 99         185 my $fraction = '';
1158 99 100 66     328 if (defined $nanosecond || defined $precision) {
1159              
1160 49 100       98 if (!defined $precision) {
1161 6 100       45 if ($nanosecond == 0) {
    50          
    0          
1162 4         11 $precision = 0;
1163             }
1164             elsif (($nanosecond % 1_000_000) == 0) {
1165 2         6 $precision = 3;
1166             }
1167             elsif (($nanosecond % 1_000) == 0) {
1168 0         0 $precision = 6;
1169             }
1170             else {
1171 0         0 $precision = 9;
1172             }
1173             }
1174              
1175 49 100       129 if ($precision != 0) {
1176 41   50     98 $nanosecond //= 0;
1177 41         209 $fraction = sprintf '.%.*d',
1178             $precision, int($nanosecond / (10 ** (9 - $precision)));
1179             }
1180             }
1181 99         245 return $formatter->($time, $offset, $fraction);
1182             }
1183              
1184             1;