File Coverage

blib/lib/HTTP/Date.pm
Criterion Covered Total %
statement 66 71 92.9
branch 45 60 75.0
condition 25 25 100.0
subroutine 6 6 100.0
pod 5 5 100.0
total 147 167 88.0


line stmt bran cond sub pod time code
1             package HTTP::Date;
2              
3 1     1   4387 use strict;
  1         6  
  1         1399  
4              
5             our $VERSION = '6.04';
6              
7             require Exporter;
8             our @ISA = qw(Exporter);
9             our @EXPORT = qw(time2str str2time);
10             our @EXPORT_OK = qw(parse_date time2iso time2isoz);
11              
12             require Time::Local;
13              
14             our ( @DoW, @MoY, %MoY );
15             @DoW = qw(Sun Mon Tue Wed Thu Fri Sat);
16             @MoY = qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec);
17             @MoY{@MoY} = ( 1 .. 12 );
18              
19             my %GMT_ZONE = ( GMT => 1, UTC => 1, UT => 1, Z => 1 );
20              
21             sub time2str (;$) {
22 1     1 1 148 my $time = shift;
23 1 50       4 $time = time unless defined $time;
24 1         8 my ( $sec, $min, $hour, $mday, $mon, $year, $wday ) = gmtime($time);
25 1         13 sprintf(
26             "%s, %02d %s %04d %02d:%02d:%02d GMT",
27             $DoW[$wday],
28             $mday, $MoY[$mon], $year + 1900,
29             $hour, $min, $sec
30             );
31             }
32              
33             sub str2time ($;$) {
34 115     115 1 7632 my $str = shift;
35 115 100       176 return undef unless defined $str;
36              
37             # fast exit for strictly conforming string
38 114 100       180 if ( $str
39             =~ /^[SMTWF][a-z][a-z], (\d\d) ([JFMAJSOND][a-z][a-z]) (\d\d\d\d) (\d\d):(\d\d):(\d\d) GMT$/
40             ) {
41 1         2 return eval {
42 1         5 my $t = Time::Local::timegm( $6, $5, $4, $1, $MoY{$2} - 1, $3 );
43 1 50       37 $t < 0 ? undef : $t;
44             };
45             }
46              
47 113         148 my @d = parse_date($str);
48 113 100       189 return undef unless @d;
49 107         112 $d[1]--; # month
50              
51 107         126 my $tz = pop(@d);
52 107 100       161 unless ( defined $tz ) {
53 51 100       74 unless ( defined( $tz = shift ) ) {
54 15         20 return eval {
55 15         22 my $frac = $d[-1];
56 15         18 $frac -= ( $d[-1] = int($frac) );
57 15         36 my $t = Time::Local::timelocal( reverse @d ) + $frac;
58 10 50       491 $t < 0 ? undef : $t;
59             };
60             }
61             }
62              
63 92         95 my $offset = 0;
64 92 100       212 if ( $GMT_ZONE{ uc $tz } ) {
    50          
65              
66             # offset already zero
67             }
68             elsif ( $tz =~ /^([-+])?(\d\d?):?(\d\d)?$/ ) {
69 27         52 $offset = 3600 * $2;
70 27 50       58 $offset += 60 * $3 if $3;
71 27 100 100     76 $offset *= -1 if $1 && $1 eq '-';
72             }
73             else {
74 0 0       0 eval { require Time::Zone } || return undef;
  0         0  
75 0         0 $offset = Time::Zone::tz_offset($tz);
76 0 0       0 return undef unless defined $offset;
77             }
78              
79 92         108 return eval {
80 92         99 my $frac = $d[-1];
81 92         115 $frac -= ( $d[-1] = int($frac) );
82 92         165 my $t = Time::Local::timegm( reverse @d ) + $frac;
83 92 50       2057 $t < 0 ? undef : $t - $offset;
84             };
85             }
86              
87             sub parse_date ($) {
88 116     116 1 388 local ($_) = shift;
89 116 50       151 return unless defined;
90              
91             # More lax parsing below
92 116         231 s/^\s+//; # kill leading space
93 116         221 s/^(?:Sun|Mon|Tue|Wed|Thu|Fri|Sat)[a-z]*,?\s*//i; # Useless weekday
94              
95 116         142 my ( $day, $mon, $yr, $hr, $min, $sec, $tz, $ampm );
96              
97             # Then we are able to check for most of the formats with this regexp
98             (
99 116 100 100     1100 ( $day, $mon, $yr, $hr, $min, $sec, $tz )
      100        
      100        
      100        
100             = /^
101             (\d\d?) # day
102             (?:\s+|[-\/])
103             (\w+) # month
104             (?:\s+|[-\/])
105             (\d+) # year
106             (?:
107             (?:\s+|:) # separator before clock
108             (\d\d?):(\d\d) # hour:min
109             (?::(\d\d))? # optional seconds
110             )? # optional clock
111             \s*
112             ([-+]?\d{2,4}|(?![APap][Mm]\b)[A-Za-z]+)? # timezone
113             \s*
114             (?:\(\w+\)|\w{3,})? # ASCII representation of timezone.
115             \s*$
116             /x
117             )
118              
119             ||
120              
121             # Try the ctime and asctime format
122             (
123             ( $mon, $day, $hr, $min, $sec, $tz, $yr )
124             = /^
125             (\w{1,3}) # month
126             \s+
127             (\d\d?) # day
128             \s+
129             (\d\d?):(\d\d) # hour:min
130             (?::(\d\d))? # optional seconds
131             \s+
132             (?:([A-Za-z]+)\s+)? # optional timezone
133             (\d+) # year
134             \s*$ # allow trailing whitespace
135             /x
136             )
137              
138             ||
139              
140             # Then the Unix 'ls -l' date format
141             (
142             ( $mon, $day, $yr, $hr, $min, $sec )
143             = /^
144             (\w{3}) # month
145             \s+
146             (\d\d?) # day
147             \s+
148             (?:
149             (\d\d\d\d) | # year
150             (\d{1,2}):(\d{2}) # hour:min
151             (?::(\d\d))? # optional seconds
152             )
153             \s*$
154             /x
155             )
156              
157             ||
158              
159             # ISO 8601 format '1996-02-29 12:00:00 -0100' and variants
160             (
161             ( $yr, $mon, $day, $hr, $min, $sec, $tz )
162             = /^
163             (\d{4}) # year
164             [-\/]?
165             (\d\d?) # numerical month
166             [-\/]?
167             (\d\d?) # day
168             (?:
169             (?:\s+|[-:Tt]) # separator before clock
170             (\d\d?):?(\d\d) # hour:min
171             (?::?(\d\d(?:\.\d*)?))? # optional seconds (and fractional)
172             )? # optional clock
173             \s*
174             ([-+]?\d\d?:?(:?\d\d)?
175             |Z|z)? # timezone (Z is "zero meridian", i.e. GMT)
176             \s*$
177             /x
178             )
179              
180             ||
181              
182             # Windows 'dir' 11-12-96 03:52PM
183             (
184             ( $mon, $day, $yr, $hr, $min, $ampm )
185             = /^
186             (\d{2}) # numerical month
187             -
188             (\d{2}) # day
189             -
190             (\d{2}) # year
191             \s+
192             (\d\d?):(\d\d)([APap][Mm]) # hour:min AM or PM
193             \s*$
194             /x
195             )
196              
197             || return; # unrecognized format
198              
199             # Translate month name to number
200             $mon
201             = $MoY{$mon}
202 113   100     600 || $MoY{"\u\L$mon"}
203             || ( $mon =~ /^\d\d?$/ && $mon >= 1 && $mon <= 12 && int($mon) )
204             || return;
205              
206             # If the year is missing, we assume first date before the current,
207             # because of the formats we support such dates are mostly present
208             # on "ls -l" listings.
209 110 100 100     538 unless ( defined $yr ) {
210 1         2 my $cur_mon;
211 1         17 ( $cur_mon, $yr ) = (localtime)[ 4, 5 ];
212 1         3 $yr += 1900;
213 1         2 $cur_mon++;
214 1 50       3 $yr-- if $mon > $cur_mon;
215             }
216             elsif ( length($yr) < 3 ) {
217              
218             # Find "obvious" year
219             my $cur_yr = (localtime)[5] + 1900;
220             my $m = $cur_yr % 100;
221             my $tmp = $yr;
222             $yr += $cur_yr - $m;
223             $m -= $tmp;
224             $yr += ( $m > 0 ) ? 100 : -100
225             if abs($m) > 50;
226             }
227              
228             # Make sure clock elements are defined
229 110 100       152 $hr = 0 unless defined($hr);
230 110 100       132 $min = 0 unless defined($min);
231 110 100       150 $sec = 0 unless defined($sec);
232              
233             # Compensate for AM/PM
234 110 100       142 if ($ampm) {
235 11         13 $ampm = uc $ampm;
236 11 100 100     32 $hr = 0 if $hr == 12 && $ampm eq 'AM';
237 11 100 100     28 $hr += 12 if $ampm eq 'PM' && $hr != 12;
238             }
239              
240 110 100       342 return ( $yr, $mon, $day, $hr, $min, $sec, $tz )
241             if wantarray;
242              
243 2 50       5 if ( defined $tz ) {
244 0 0       0 $tz = "Z" if $tz =~ /^(GMT|UTC?|[-+]?0+)$/;
245             }
246             else {
247 2         5 $tz = "";
248             }
249 2         14 return sprintf(
250             "%04d-%02d-%02d %02d:%02d:%02d%s",
251             $yr, $mon, $day, $hr, $min, $sec, $tz
252             );
253             }
254              
255             sub time2iso (;$) {
256 11     11 1 76 my $time = shift;
257 11 100       20 $time = time unless defined $time;
258 11         138 my ( $sec, $min, $hour, $mday, $mon, $year ) = localtime($time);
259 11         94 sprintf(
260             "%04d-%02d-%02d %02d:%02d:%02d",
261             $year + 1900, $mon + 1, $mday, $hour, $min, $sec
262             );
263             }
264              
265             sub time2isoz (;$) {
266 2     2 1 15 my $time = shift;
267 2 100       6 $time = time unless defined $time;
268 2         7 my ( $sec, $min, $hour, $mday, $mon, $year ) = gmtime($time);
269 2         9 sprintf(
270             "%04d-%02d-%02d %02d:%02d:%02dZ",
271             $year + 1900, $mon + 1, $mday, $hour, $min, $sec
272             );
273             }
274              
275             1;
276              
277             # ABSTRACT: HTTP::Date - date conversion routines
278             #
279              
280             __END__
281              
282             =pod
283              
284             =encoding UTF-8
285              
286             =head1 NAME
287              
288             HTTP::Date - HTTP::Date - date conversion routines
289              
290             =head1 VERSION
291              
292             version 6.04
293              
294             =head1 SYNOPSIS
295              
296             use HTTP::Date;
297              
298             $string = time2str($time); # Format as GMT ASCII time
299             $time = str2time($string); # convert ASCII date to machine time
300              
301             =head1 DESCRIPTION
302              
303             This module provides functions that deal the date formats used by the
304             HTTP protocol (and then some more). Only the first two functions,
305             time2str() and str2time(), are exported by default.
306              
307             =over 4
308              
309             =item time2str( [$time] )
310              
311             The time2str() function converts a machine time (seconds since epoch)
312             to a string. If the function is called without an argument or with an
313             undefined argument, it will use the current time.
314              
315             The string returned is in the format preferred for the HTTP protocol.
316             This is a fixed length subset of the format defined by RFC 1123,
317             represented in Universal Time (GMT). An example of a time stamp
318             in this format is:
319              
320             Sun, 06 Nov 1994 08:49:37 GMT
321              
322             =item str2time( $str [, $zone] )
323              
324             The str2time() function converts a string to machine time. It returns
325             C<undef> if the format of $str is unrecognized, otherwise whatever the
326             C<Time::Local> functions can make out of the parsed time. Dates
327             before the system's epoch may not work on all operating systems. The
328             time formats recognized are the same as for parse_date().
329              
330             The function also takes an optional second argument that specifies the
331             default time zone to use when converting the date. This parameter is
332             ignored if the zone is found in the date string itself. If this
333             parameter is missing, and the date string format does not contain any
334             zone specification, then the local time zone is assumed.
335              
336             If the zone is not "C<GMT>" or numerical (like "C<-0800>" or
337             "C<+0100>"), then the C<Time::Zone> module must be installed in order
338             to get the date recognized.
339              
340             =item parse_date( $str )
341              
342             This function will try to parse a date string, and then return it as a
343             list of numerical values followed by a (possible undefined) time zone
344             specifier; ($year, $month, $day, $hour, $min, $sec, $tz). The $year
345             will be the full 4-digit year, and $month numbers start with 1 (for January).
346              
347             In scalar context the numbers are interpolated in a string of the
348             "YYYY-MM-DD hh:mm:ss TZ"-format and returned.
349              
350             If the date is unrecognized, then the empty list is returned (C<undef> in
351             scalar context).
352              
353             The function is able to parse the following formats:
354              
355             "Wed, 09 Feb 1994 22:23:32 GMT" -- HTTP format
356             "Thu Feb 3 17:03:55 GMT 1994" -- ctime(3) format
357             "Thu Feb 3 00:00:00 1994", -- ANSI C asctime() format
358             "Tuesday, 08-Feb-94 14:15:29 GMT" -- old rfc850 HTTP format
359             "Tuesday, 08-Feb-1994 14:15:29 GMT" -- broken rfc850 HTTP format
360              
361             "03/Feb/1994:17:03:55 -0700" -- common logfile format
362             "09 Feb 1994 22:23:32 GMT" -- HTTP format (no weekday)
363             "08-Feb-94 14:15:29 GMT" -- rfc850 format (no weekday)
364             "08-Feb-1994 14:15:29 GMT" -- broken rfc850 format (no weekday)
365              
366             "1994-02-03 14:15:29 -0100" -- ISO 8601 format
367             "1994-02-03 14:15:29" -- zone is optional
368             "1994-02-03" -- only date
369             "1994-02-03T14:15:29" -- Use T as separator
370             "19940203T141529Z" -- ISO 8601 compact format
371             "19940203" -- only date
372              
373             "08-Feb-94" -- old rfc850 HTTP format (no weekday, no time)
374             "08-Feb-1994" -- broken rfc850 HTTP format (no weekday, no time)
375             "09 Feb 1994" -- proposed new HTTP format (no weekday, no time)
376             "03/Feb/1994" -- common logfile format (no time, no offset)
377              
378             "Feb 3 1994" -- Unix 'ls -l' format
379             "Feb 3 17:03" -- Unix 'ls -l' format
380              
381             "11-15-96 03:52PM" -- Windows 'dir' format
382              
383             The parser ignores leading and trailing whitespace. It also allow the
384             seconds to be missing and the month to be numerical in most formats.
385              
386             If the year is missing, then we assume that the date is the first
387             matching date I<before> current month. If the year is given with only
388             2 digits, then parse_date() will select the century that makes the
389             year closest to the current date.
390              
391             =item time2iso( [$time] )
392              
393             Same as time2str(), but returns a "YYYY-MM-DD hh:mm:ss"-formatted
394             string representing time in the local time zone.
395              
396             =item time2isoz( [$time] )
397              
398             Same as time2str(), but returns a "YYYY-MM-DD hh:mm:ssZ"-formatted
399             string representing Universal Time.
400              
401             =back
402              
403             =head1 SEE ALSO
404              
405             L<perlfunc/time>, L<Time::Zone>
406              
407             =head1 AUTHOR
408              
409             Gisle Aas <gisle@activestate.com>
410              
411             =head1 COPYRIGHT AND LICENSE
412              
413             This software is copyright (c) 1995-2019 by Gisle Aas.
414              
415             This is free software; you can redistribute it and/or modify it under
416             the same terms as the Perl 5 programming language system itself.
417              
418             =cut