File Coverage

blib/lib/Time/Str.pm
Criterion Covered Total %
statement 245 247 99.1
branch 127 140 90.7
condition 70 86 81.4
subroutine 33 33 100.0
pod 3 24 12.5
total 478 530 90.1


line stmt bran cond sub pod time code
1             package Time::Str;
2 15     15   1630756 use strict;
  15         29  
  15         533  
3 15     15   128 use warnings;
  15         21  
  15         677  
4 15     15   237 use v5.10;
  15         53  
5              
6             our $VERSION = '0.02';
7             our @EXPORT_OK = qw[ time2str str2time str2date ];
8             our %EXPORT_TAGS = ( all => \@EXPORT_OK );
9              
10 15     15   95 use Exporter qw[import];
  15         33  
  15         627  
11 15     15   76 use Carp qw[croak];
  15         25  
  15         906  
12 15     15   6969 use POSIX qw[floor];
  15         127906  
  15         86  
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 15     15   75626 *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             # ISO 9075 Database Language SQL — Part 2: Foundation (SQL/Foundation)
402             #
403             #
404             # YYYY-MM-DD
405             # YYYY-MM-DD hh:mm:ss[.fraction]
406             # YYYY-MM-DD hh:mm:ss[.fraction] ±hh:mm
407             #
408             my $ISO9075_Rx = qr{
409             \A
410             (? [0-9]{4})
411             [-] (? [0-9]{2})
412             [-] (? [0-9]{2})
413             (?:
414             [ ] (? [0-9]{2})
415             [:] (? [0-9]{2})
416             [:] (? [0-9]{2}) (?: [.] (? [0-9]{1,9}) )?
417             (?:
418             [ ] (? [+-][0-9]{2}[:][0-9]{2})
419             )?
420             )?
421             \z
422             }x;
423              
424             # Common Log Format
425             #
426             #
427             #
428             # DD/MMM/YYYY:hh:mm:ss[.fraction] ±hhmm
429             #
430             my $CommonLogFormat_Rx = qr{
431             (?(DEFINE)
432             (? (?: Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec))
433             )
434             \A
435             (? [0-9]{2})
436             [/] (? (?&MonthName))
437             [/] (? [0-9]{4})
438             [:] (? [0-9]{2})
439             [:] (? [0-9]{2})
440             [:] (? [0-9]{2}) (?: [.] (? [0-9]{1,9}) )?
441             [ ] (? [+-][0-9]{4})
442             \z
443             }x;
444              
445             # ANSI/ISO C ctime
446             #
447             #
448             #
449             # DDD MMM (_D|DD) hh:mm:ss YYYY
450             #
451             my $ANSIC_Rx = qr{
452             (?(DEFINE)
453             (? (?: Mon|Tue|Wed|Thu|Fri|Sat|Sun))
454             (? (?: Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec))
455             )
456             \A
457             (?:
458             (?&DayName)
459             [ ] (? (?&MonthName))
460             (?:
461             (?: [ ]{2} (? [0-9]{1}))
462             | (?: [ ]{1} (? [0-9]{2}))
463             )
464             [ ] (? [0-9]{2})
465             [:] (? [0-9]{2})
466             [:] (? [0-9]{2})
467             [ ] (? [0-9]{4})
468             )
469             \z
470             }x;
471              
472             # Unix Date
473             #
474             #
475             # The date command output format.
476             #
477             # DDD MMM (_D|DD) hh:mm:ss (±hhmm|UTC|GMT|ZONE) YYYY
478             # DDD MMM (_D|DD) hh:mm:ss YYYY (±hhmm|UTC|GMT|ZONE)
479             #
480             my $UnixDate_Rx = qr{
481             (?(DEFINE)
482             (? (?: Mon|Tue|Wed|Thu|Fri|Sat|Sun))
483             (? (?: Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec))
484             (? [A-Z][A-Za-z][A-Z]{1,4})
485             )
486             \A
487             (?:
488             (?&DayName)
489             [ ] (? (?&MonthName))
490             (?:
491             (?: [ ]{2} (? [0-9]{1}))
492             | (?: [ ]{1} (? [0-9]{2}))
493             )
494             [ ] (? [0-9]{2})
495             [:] (? [0-9]{2})
496             [:] (? [0-9]{2})
497             [ ]
498             (?:
499             (?:
500             (? [+-][0-9]{4})
501             | (? UTC|GMT)
502             | (? (?&TimeZoneAbbrev))
503             )
504             [ ] (? [0-9]{4})
505             |
506             (? [0-9]{4})
507             [ ]
508             (?:
509             (? [+-][0-9]{4})
510             | (? UTC|GMT)
511             | (? (?&TimeZoneAbbrev))
512             )
513             )
514             )
515             \z
516             }x;
517              
518             # Git Date
519             #
520             #
521             # The default date format used by Git.
522             #
523             # DDD MMM D hh:mm:ss YYYY ±hhmm
524             #
525             my $GitDate_Rx = qr{
526             (?(DEFINE)
527             (? (?: Mon|Tue|Wed|Thu|Fri|Sat|Sun))
528             (? (?: Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec))
529             )
530             \A
531             (?&DayName)
532             [ ] (? (?&MonthName))
533             [ ] (? [0-9]{1,2})
534             [ ] (? [0-9]{2})
535             [:] (? [0-9]{2})
536             [:] (? [0-9]{2})
537             [ ] (? [0-9]{4})
538             [ ] (? [+-][0-9]{4})
539             \z
540             }x;
541              
542             # Ruby Date
543             #
544             # Popularized by Ruby on Rails and Twitter.
545             #
546             # DDD MMM DD hh:mm:ss ±hhmm YYYY
547             #
548             my $RubyDate_Rx = qr{
549             (?(DEFINE)
550             (? (?: Mon|Tue|Wed|Thu|Fri|Sat|Sun))
551             (? (?: Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec))
552             )
553             \A
554             (?&DayName)
555             [ ] (? (?&MonthName))
556             [ ] (? [0-9]{2})
557             [ ] (? [0-9]{2})
558             [:] (? [0-9]{2})
559             [:] (? [0-9]{2})
560             [ ] (? [+-][0-9]{4})
561             [ ] (? [0-9]{4})
562             \z
563             }x;
564              
565             sub leap_year {
566 7     7 0 61 my ($y) = @_;
567 7   66     269 return ($y % 4 == 0 && ($y % 100 != 0 || $y % 400 == 0));
568             }
569              
570             # 1 <= $m <= 12
571             sub month_days {
572 45     45 0 119 my ($y, $m) = @_;
573 45 100 100     184 return 29 if $m == 2 && leap_year($y);
574 39         384 return (0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)[$m];
575             }
576              
577             sub valid_ymd {
578 694     694 0 1676 my ($y, $m, $d) = @_;
579 694   66     7020 return ($y >= 1 && $y <= 9999)
580             && ($m >= 1 && $m <= 12)
581             && ($d >= 1 && ($d <= 28 || $d <= month_days($y, $m)));
582             }
583              
584             sub valid_hm {
585 237     237 0 468 my ($h, $m) = @_;
586 237   66     1501 return ($h >= 0 && $h <= 23
587             && $m >= 0 && $m <= 59);
588             }
589              
590             sub valid_hms {
591 575     575 0 1060 my ($h, $m, $s) = @_;
592 575   100     4999 return ($h >= 0 && $h <= 23
593             && $m >= 0 && $m <= 59
594             && $s >= 0 && $s <= 60);
595             }
596              
597             sub expand_two_digit_year {
598 32 50   32 0 79 @_ == 2 or croak q/Usage: expand_two_digit_year(yy, pivot_year)/;
599 32         49 my ($yy, $pivot_year) = @_;
600              
601 32 50 33     78 ($pivot_year >= 0 && $pivot_year <= 9899)
602             or croak q/Parameter 'pivot_year' is out of range (0-9899)/;
603              
604 15     15   8134 use integer;
  15         270  
  15         168  
605 32         42 my $century = $pivot_year / 100;
606 32         35 my $base = $century * 100;
607 32         34 my $pivot_offset = $pivot_year - $base;
608              
609 32         57 my $year = $base + $yy;
610 32 100       51 if ($yy < $pivot_offset) {
611 20         23 $year += 100;
612             }
613 32         52 return $year;
614             }
615              
616             sub meridiem_to_24h {
617 22 50   22 0 38 @_ == 2 or croak q/Usage: meridiem_to_24h(hour, meridiem)/;
618 22         41 my ($hour, $meridiem) = @_;
619              
620 22 50 33     63 ($hour >= 1 && $hour <= 12)
621             or croak q/Parameter 'hour' is out of range (1-12)/;
622              
623 22 50 66     60 ($meridiem eq 'AM' || $meridiem eq 'PM')
624             or croak q/Parameter 'meridiem' is not AM or PM/;
625              
626 22 100       68 return $meridiem eq 'AM' ? ($hour == 12 ? 0 : $hour)
    100          
    100          
627             : ($hour == 12 ? 12 : $hour + 12);
628             }
629              
630             sub parse_numeric_offset {
631 237 50   237 0 497 @_ == 1 or croak q/Usage: parse_numeric_offset(string)/;
632 237         406 my ($string) = @_;
633              
634 237         645 $string =~ s/://; # ±H ±HH ±H:MM ±HH:MM ±HHMM
635 237 50       1570 my ($sign, $h, $m) = ($string =~ m/^([+-])([0-9]{1,2})([0-9]{2})?$/)
636             or croak q/Unable to parse: timezone offset is invalid/;
637              
638 237   100     529 $m //= 0;
639 237 100       422 valid_hm($h, $m)
640             or croak qq/Unable to parse: timezone offset is out of range ($string)/;
641              
642 236         553 my $offset = $h * 60 + $m;
643 236 100       449 if ($sign eq '-') {
644 72         105 $offset *= -1;
645             }
646              
647 236         454 return $offset;
648             }
649              
650             my %RegexpMap = (
651             ansic => $ANSIC_Rx,
652             asn1gt => $ASN1GT_Rx,
653             asn1ut => $ASN1UT_Rx,
654             atom => $RFC4287_Rx,
655             clf => $CommonLogFormat_Rx,
656             ctime => $ANSIC_Rx,
657             email => $RFC2822_Rx,
658             generic => $GenericDateTime_Rx,
659             git => $GitDate_Rx,
660             http => $RFC2616_Rx,
661             imf => $RFC2822_Rx,
662             iso9075 => $ISO9075_Rx,
663             rfc2616 => $RFC2616_Rx,
664             rfc2822 => $RFC2822_Rx,
665             rfc3339 => $RFC3339_Rx,
666             rfc4287 => $RFC4287_Rx,
667             rfc5322 => $RFC2822_Rx,
668             rfc7231 => $RFC2616_Rx,
669             ruby => $RubyDate_Rx,
670             sql => $ISO9075_Rx,
671             unix => $UnixDate_Rx,
672             w3c => $W3CDTF_Rx,
673             w3cdtf => $W3CDTF_Rx,
674             );
675              
676             sub str2date {
677 703 100   703 1 3329567 @_ & 1 or croak q/Usage: str2date(string [, format => 'RFC3339' ])/;
678 701         2009 my ($string, %p) = @_;
679              
680 701         1201 my $format = 'rfc3339';
681 701         1090 my $pivot_year = $DefaultPivotYear;
682 701         947 my $regexp = $RFC3339_Rx;
683              
684 701 100       1564 if (exists $p{format}) {
685 691         1472 $format = lc delete $p{format};
686 691         1350 $regexp = $RegexpMap{$format};
687              
688 691 100       1462 (defined $regexp)
689             or croak qq/Parameter 'format' is unknown: '$format'/;
690             }
691              
692 700 100       1422 if (exists $p{pivot_year}) {
693 8         11 $pivot_year = delete $p{pivot_year};
694              
695 8 100 100     164 ($pivot_year >= 0 && $pivot_year <= 9899)
696             or croak q/Parameter 'pivot_year' is out of range (0-9899)/;
697             }
698              
699 698 100       1407 if (%p) {
700 2         175 croak "Unknown named parameter: " . join ', ', sort keys %p;
701             }
702              
703 696 100 66     15277 (defined $string && $string =~ $regexp)
704             or croak qq/Unable to parse: string does not match the $format format/;
705              
706 694         16695 my %r = %+;
707              
708 694 100 100     5829 if (exists $r{month} && $r{month} !~ /^[0-9]/) {
709 248         734 $r{month} = $MonthIndexMap{ lc $r{month} };
710             }
711              
712 694 50       1370 if (exists $r{year}) {
713              
714 694 100       1570 if (length $r{year} == 2) {
715 32         59 $r{year} = expand_two_digit_year($r{year}, $pivot_year);
716             }
717              
718 694 100 100     2703 valid_ymd($r{year}, $r{month} // 1, $r{day} // 1)
      100        
719             or croak q/Unable to parse: date is out of range/;
720             }
721              
722 688 100       1546 if (exists $r{hour}) {
723              
724 575 100       1143 if (exists $r{meridiem}) {
725 22 50 33     69 ($r{hour} >= 1 && $r{hour} <= 12)
726             or croak q/Unable to parse: hour is out of range for 12-hour clock/;
727              
728 22         70 $r{hour} = meridiem_to_24h($r{hour}, $MeridiemMap{ lc delete $r{meridiem} });
729             }
730              
731 575 100 100     1943 valid_hms($r{hour}, $r{minute} // 0, $r{second} // 0)
      100        
732             or croak q/Unable to parse: time of day is out of range/;
733              
734 569 100       1137 if (exists $r{fraction}) {
735 238         438 my $f = delete $r{fraction};
736 238         591 my $ns = $f * (10 ** (9 - length $f));
737              
738 238 100       479 if (exists $r{second}) {
    100          
739             # HH.MM.SS.fraction
740 220         450 $r{nanosecond} = $ns;
741             }
742             elsif (exists $r{minute}) {
743             # HH.MM.fraction
744 10         19 my $total_ns = $ns * 60;
745 10         37 $r{second} = int($total_ns / NANOS_PER_SECOND);
746 10         19 my $nsec = $total_ns % NANOS_PER_SECOND;
747 10 100       24 if ($nsec != 0) {
748 6         15 $r{nanosecond} = $nsec;
749             }
750             }
751             else {
752             # HH.fraction
753 8         53 my $total_ns = $ns * 3600;
754 8         21 my $min = int($total_ns / (60 * NANOS_PER_SECOND));
755 8         23 $r{minute} = $min;
756 8         13 $total_ns -= $min * 60 * NANOS_PER_SECOND;
757 8         15 my $sec = int($total_ns / NANOS_PER_SECOND);
758 8         17 my $nsec = $total_ns % NANOS_PER_SECOND;
759 8 100 66     28 if ($sec != 0 || $nsec != 0) {
760 2         4 $r{second} = $sec;
761 2 100       7 if ($nsec != 0) {
762 1         3 $r{nanosecond} = $nsec;
763             }
764             }
765             }
766             }
767              
768 569 100       1012 if (exists $r{tz_offset}) {
769 237         519 $r{tz_offset} = parse_numeric_offset($r{tz_offset});
770             }
771              
772 568 100       1064 if (exists $r{tz_utc}) {
773 293   100     893 $r{tz_offset} //= 0;
774             }
775             }
776              
777 681 100 100     1754 if ($regexp == $RFC2616_Rx && !$r{tz_utc}) {
778 2         5 $r{tz_utc} = 'GMT';
779 2         3 $r{tz_offset} = 0;
780             }
781              
782             {
783 681         945 local @r{qw(tz_utc tz_abbrev tz_annotation)};
  681         2342  
784 681         4865 $_ += 0 for values %r;
785             }
786 681 50       2617 return wantarray ? %r : \%r;
787             }
788              
789             sub str2time {
790 134 100   134 1 86089 @_ & 1 or croak q/Usage: str2time(string [, format => 'RFC3339' ])/;
791 132         433 my ($string, %p) = @_;
792              
793 132         202 my $precision = DEFAULT_PRECISION;
794              
795 132 100       350 if (exists $p{precision}) {
796 52         95 $precision = delete $p{precision};
797 52 100 100     364 ($precision >= 0 && $precision <= 9)
798             or croak(q/Parameter 'precision' is out of range (0-9)/);
799             }
800              
801 130         363 my $r = str2date($string, %p);
802              
803             (exists $r->{tz_offset})
804 130 100       489 or croak q/Unable to convert to time: no timezone offset or UTC designator/;
805              
806 128         378 my ($Y, $M, $D, $h, $m, $s) = @$r{qw(year month day hour minute second)};
807 128   50     242 $m //= 0;
808 128   100     242 $s //= 0;
809              
810 128         152 my $rdn = do {
811 15     15   19869 use integer;
  15         27  
  15         61  
812 128 100       257 if ($M < 3) {
813 55         127 $Y--, $M += 12;
814             }
815 128         425 (1461 * $Y) / 4 - $Y / 100 + $Y / 400
816             + $D + ((979 * $M - 2918) >> 5) - 306;
817             };
818 128         222 my $sod = ($h * 60 + $m) * 60 + $s;
819 128         348 my $time = ($rdn - 719163) * 86400 + $sod - $r->{tz_offset} * 60;
820 128 100       245 if (exists $r->{nanosecond}) {
821 50         83 my $scale = 10 ** $precision;
822 50         153 my $fraction = int($r->{nanosecond} * $scale / 1E9);
823 50         109 $time += $fraction / $scale;
824             }
825 128         653 return $time;
826             }
827              
828             {
829             my @DoW = qw[Sun Mon Tue Wed Thu Fri Sat];
830             my @MoY = qw[Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec];
831              
832             sub format_offset_basic {
833 35     35 0 77 my ($offset, $zulu) = @_;
834              
835 35 100       86 if ($offset == 0) {
836 21         48 return $zulu;
837             }
838             else {
839 14 100       27 my $sign = $offset < 0 ? -1 : 1;
840 14         22 my $min = abs $offset;
841 14         65 return sprintf '%+.4d', $sign * int($min / 60) * 100 + $min % 60;
842             }
843             }
844              
845             sub format_offset_extended {
846 39     39 0 114 my ($offset, $zulu) = @_;
847              
848 39 100       133 if ($offset == 0) {
849 31         67 return $zulu;
850             }
851             else {
852 8 100       37 my $sign = $offset < 0 ? ord '-' : ord '+';
853 8         14 my $min = abs $offset;
854 8         77 return sprintf '%c%.2d:%.2d', $sign, int($min / 60), $min % 60;
855             }
856             }
857              
858             sub format_ASN1UT {
859 3     3 0 5 my ($time, $offset) = @_;
860              
861 3         4 $time += $offset * 60;
862 3         10 my ($sec, $min, $hour, $mday, $mon, $year) = gmtime $time;
863 3         5 my $zstr = format_offset_basic($offset, 'Z');
864 3         17 return sprintf '%02d%02d%02d%02d%02d%02d%s',
865             ($year + 1900) % 100, $mon + 1, $mday, $hour, $min, $sec, $zstr;
866             }
867              
868             sub format_ASN1GT {
869 9     9 0 20 my ($time, $offset, $fraction) = @_;
870              
871 9         18 $time += $offset * 60;
872 9         43 my ($sec, $min, $hour, $mday, $mon, $year) = gmtime $time;
873 9         25 my $zstr = format_offset_basic($offset, 'Z');
874 9         59 return sprintf '%04d%02d%02d%02d%02d%02d%s%s',
875             $year + 1900, $mon + 1, $mday, $hour, $min, $sec, $fraction, $zstr;
876             }
877            
878             sub format_CLF {
879 5     5 0 7 my ($time, $offset, $fraction) = @_;
880              
881 5         7 $time += $offset * 60;
882 5         14 my ($sec, $min, $hour, $mday, $mon, $year) = gmtime $time;
883 5         8 my $zstr = format_offset_basic($offset, '+0000');
884 5         25 return sprintf '%02d/%s/%04d:%02d:%02d:%02d%s %s',
885             $mday, $MoY[$mon], $year + 1900, $hour, $min, $sec, $fraction, $zstr;
886             }
887              
888             sub format_RFC2616 {
889 5     5 0 6 my ($time, $offset) = @_;
890              
891 5         17 my ($sec, $min, $hour, $mday, $mon, $year, $wday) = gmtime $time;
892 5         32 return sprintf '%s, %02d %s %04d %02d:%02d:%02d GMT',
893             $DoW[$wday], $mday, $MoY[$mon], $year + 1900, $hour, $min, $sec;
894             }
895              
896             sub format_RFC2822 {
897 3     3 0 4 my ($time, $offset) = @_;
898              
899 3         3 $time += $offset * 60;
900 3         10 my ($sec, $min, $hour, $mday, $mon, $year, $wday) = gmtime $time;
901 3         8 my $zstr = format_offset_basic($offset, '+0000');
902 3         28 return sprintf '%s, %d %s %04d %02d:%02d:%02d %s',
903             $DoW[$wday], $mday, $MoY[$mon], $year + 1900, $hour, $min, $sec, $zstr;
904             }
905              
906             sub format_RFC3339 {
907 35     35 0 107 my ($time, $offset, $fraction) = @_;
908              
909 35         56 $time += $offset * 60;
910 35         132 my ($sec, $min, $hour, $mday, $mon, $year) = gmtime $time;
911 35         89 my $zstr = format_offset_extended($offset, 'Z');
912 35         210 return sprintf '%04d-%02d-%02dT%02d:%02d:%02d%s%s',
913             $year + 1900, $mon + 1, $mday, $hour, $min, $sec, $fraction, $zstr;
914             }
915              
916             sub format_ISO9075 {
917 4     4 0 12 my ($time, $offset, $fraction) = @_;
918              
919 4         7 $time += $offset * 60;
920 4         17 my ($sec, $min, $hour, $mday, $mon, $year) = gmtime $time;
921 4         12 my $zstr = format_offset_extended($offset, '+00:00');
922 4         31 return sprintf '%04d-%02d-%02d %02d:%02d:%02d%s %s',
923             $year + 1900, $mon + 1, $mday, $hour, $min, $sec, $fraction, $zstr;
924             }
925              
926             sub format_ANSIC {
927 3     3 0 9 my ($time) = @_;
928 3         29 return scalar gmtime $time;
929             }
930              
931             sub format_UnixDate {
932 5     5 0 10 my ($time, $offset) = @_;
933              
934 5         7 $time += $offset * 60;
935 5         18 my ($sec, $min, $hour, $mday, $mon, $year, $wday) = gmtime $time;
936 5         13 my $zstr = format_offset_basic($offset, 'UTC');
937 5         38 return sprintf '%s %s %2d %02d:%02d:%02d %s %04d',
938             $DoW[$wday], $MoY[$mon], $mday, $hour, $min, $sec, $zstr, $year + 1900;
939             }
940              
941             sub format_RubyDate {
942 5     5 0 7 my ($time, $offset) = @_;
943              
944 5         7 $time += $offset * 60;
945 5         14 my ($sec, $min, $hour, $mday, $mon, $year, $wday) = gmtime $time;
946 5         10 my $zstr = format_offset_basic($offset, '+0000');
947 5         27 return sprintf '%s %s %02d %02d:%02d:%02d %s %04d',
948             $DoW[$wday], $MoY[$mon], $mday, $hour, $min, $sec, $zstr, $year + 1900;
949             }
950              
951             sub format_GitDate {
952 5     5 0 12 my ($time, $offset) = @_;
953              
954 5         10 $time += $offset * 60;
955 5         22 my ($sec, $min, $hour, $mday, $mon, $year, $wday) = gmtime $time;
956 5         14 my $zstr = format_offset_basic($offset, '+0000');
957 5         44 return sprintf '%s %s %d %02d:%02d:%02d %04d %s',
958             $DoW[$wday], $MoY[$mon], $mday, $hour, $min, $sec, $year + 1900, $zstr;
959             }
960             }
961              
962             my %FormatMap = (
963             ansic => \&format_ANSIC,
964             asn1gt => \&format_ASN1GT,
965             asn1ut => \&format_ASN1UT,
966             atom => \&format_RFC3339,
967             clf => \&format_CLF,
968             ctime => \&format_ANSIC,
969             email => \&format_RFC2822,
970             git => \&format_GitDate,
971             http => \&format_RFC2616,
972             imf => \&format_RFC2822,
973             iso9075 => \&format_ISO9075,
974             rfc2616 => \&format_RFC2616,
975             rfc2822 => \&format_RFC2822,
976             rfc3339 => \&format_RFC3339,
977             rfc4287 => \&format_RFC3339,
978             rfc5322 => \&format_RFC2822,
979             rfc7231 => \&format_RFC2616,
980             ruby => \&format_RubyDate,
981             sql => \&format_ISO9075,
982             unix => \&format_UnixDate,
983             w3c => \&format_RFC3339,
984             w3cdtf => \&format_RFC3339,
985             );
986              
987             sub time2str {
988 97 100   97 1 63521 @_ & 1 or croak(q/Usage: time2str(time [, format => 'RFC3339' ])/);
989 95         315 my ($time, %p) = @_;
990              
991 95 100 100     816 ($time >= MIN_TIME && $time < MAX_TIME + 1)
992             or croak(q/Parameter 'time' is out of range (0001-01-01T00:00:00Z to 9999-12-31T23:59:59Z)/);
993              
994 93         181 my $formatter = \&format_RFC3339;
995              
996 93 100       219 if (exists $p{format}) {
997 83         176 my $format = delete $p{format};
998              
999 83         222 $formatter = $FormatMap{ lc $format };
1000 83 100       264 (defined $formatter)
1001             or croak(qq/Parameter 'format' is unknown: '$format'/);
1002             }
1003              
1004 92         208 my ($offset, $precision, $nanosecond) = (0);
1005              
1006 92 100       246 if (exists $p{offset}) {
1007 26         40 $offset = delete $p{offset};
1008 26 100 100     322 ($offset >= -1439 && $offset <= 1439)
1009             or croak(q/Parameter 'offset' is out of range (-1439 to 1439)/);
1010             }
1011              
1012 90 100       182 if (exists $p{precision}) {
1013 42         105 $precision = delete $p{precision};
1014 42 100 100     385 ($precision >= 0 && $precision <= 9)
1015             or croak(q/Parameter 'precision' is out of range (0-9)/);
1016             }
1017              
1018 88 100       168 if (exists $p{nanosecond}) {
1019 17         32 $nanosecond = delete $p{nanosecond};
1020 17 100 100     260 ($nanosecond >= 0 && $nanosecond <= 999_999_999)
1021             or croak(q/Parameter 'nanosecond' is out of range (0-999999999)/);
1022             }
1023              
1024 86 100       172 if (%p) {
1025 2         145 croak "Unknown named parameter: " . join ', ', sort keys %p;
1026             }
1027              
1028 84 100 100     350 if (!defined $nanosecond && int $time != $time) {
1029 29         145 my $sec = floor($time);
1030 29         46 my $frac = $time - $sec;
1031 29   50     77 my $scale = 10 ** ($precision // DEFAULT_PRECISION);
1032              
1033 29         39 $time = $sec;
1034 29         89 $frac = floor($frac * $scale + 0.5) / $scale;
1035 29         71 $nanosecond = floor($frac * NANOS_PER_SECOND + 0.5);
1036              
1037 29 100       83 if ($nanosecond >= NANOS_PER_SECOND) {
1038 4         9 $nanosecond -= NANOS_PER_SECOND;
1039 4         8 $time++;
1040             }
1041             }
1042              
1043 84 100       217 if ($offset) {
1044 24         45 my $local_time = $time + $offset * 60;
1045              
1046 24 100 100     254 ($local_time >= MIN_TIME && $local_time <= MAX_TIME)
1047             or croak(q/Parameter 'time' is out of range for the given offset/);
1048             }
1049              
1050 82         190 my $fraction = '';
1051 82 100 66     288 if (defined $nanosecond || defined $precision) {
1052              
1053 44 100       84 if (!defined $precision) {
1054 4 100       16 if ($nanosecond == 0) {
    50          
    0          
1055 3         8 $precision = 0;
1056             }
1057             elsif (($nanosecond % 1_000_000) == 0) {
1058 1         3 $precision = 3;
1059             }
1060             elsif (($nanosecond % 1_000) == 0) {
1061 0         0 $precision = 6;
1062             }
1063             else {
1064 0         0 $precision = 9;
1065             }
1066             }
1067              
1068 44 100       117 if ($precision != 0) {
1069 37   50     87 $nanosecond //= 0;
1070 37         194 $fraction = sprintf '.%.*d',
1071             $precision, int($nanosecond / (10 ** (9 - $precision)));
1072             }
1073             }
1074 82         270 return $formatter->($time, $offset, $fraction);
1075             }
1076              
1077             1;