File Coverage

lib/Sisimai/DateTime.pm
Criterion Covered Total %
statement 110 111 99.1
branch 72 80 90.0
condition 46 65 70.7
subroutine 12 12 100.0
pod 2 5 40.0
total 242 273 88.6


line stmt bran cond sub pod time code
1             package Sisimai::DateTime;
2 85     85   152148 use v5.26;
  85         322  
3 85     85   449 use strict;
  85         181  
  85         3045  
4 85     85   455 use warnings;
  85         150  
  85         4613  
5 85     85   55412 use Time::Piece;
  85         1154175  
  85         543  
6              
7             sub TZ_OFFSET() { 54000 } # Max time zone offset, 54000 seconds
8 85         8869 use constant MonthName => {
9             'full' => [qw|January February March April May June July August September October November December|],
10             'abbr' => [qw|Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec|],
11 85     85   17234 };
  85         207  
12 85         17181 use constant DayOfWeek => {
13             'full' => [qw|Sunday Monday Tuesday Wednesday Thursday Friday Saturday|],
14             'abbr' => [qw|Sun Mon Tue Wed Thu Fri Sat|],
15 85     85   547 };
  85         208  
16 85         224355 use constant TimeZones => {
17             # http://en.wikipedia.org/wiki/List_of_time_zone_abbreviations
18             #'ACDT' => '+1030', # Australian Central Daylight Time UTC+10:30
19             #'ACST' => '+0930', # Australian Central Standard Time UTC+09:30
20             #'ACT' => '+0800', # ASEAN Common Time UTC+08:00
21             'ADT' => '-0300', # Atlantic Daylight Time UTC-03:00
22             #'AEDT' => '+1100', # Australian Eastern Daylight Time UTC+11:00
23             #'AEST' => '+1000', # Australian Eastern Standard Time UTC+10:00
24             #'AFT' => '+0430', # Afghanistan Time UTC+04:30
25             'AKDT' => '-0800', # Alaska Daylight Time UTC-08:00
26             'AKST' => '-0900', # Alaska Standard Time UTC-09:00
27             #'AMST' => '+0500', # Armenia Summer Time UTC+05:00
28             #'AMT' => '+0400', # Armenia Time UTC+04:00
29             #'ART' => '-0300', # Argentina Time UTC+03:00
30             #'AST' => '+0300', # Arab Standard Time (Kuwait, Riyadh) UTC+03:00
31             #'AST' => '+0400', # Arabian Standard Time (Abu Dhabi, Muscat) UTC+04:00
32             #'AST' => '+0300', # Arabic Standard Time (Baghdad) UTC+03:00
33             'AST' => '-0400', # Atlantic Standard Time UTC-04:00
34             #'AWDT' => '+0900', # Australian Western Daylight Time UTC+09:00
35             #'AWST' => '+0800', # Australian Western Standard Time UTC+08:00
36             #'AZOST'=> '-0100', # Azores Standard Time UTC-01:00
37             #'AZT' => '+0400', # Azerbaijan Time UTC+04:00
38             #'BDT' => '+0800', # Brunei Time UTC+08:00
39             #'BIOT' => '+0600', # British Indian Ocean Time UTC+06:00
40             #'BIT' => '-1200', # Baker Island Time UTC-12:00
41             #'BOT' => '-0400', # Bolivia Time UTC-04:00
42             #'BRT' => '-0300', # Brasilia Time UTC-03:00
43             #'BST' => '+0600', # Bangladesh Standard Time UTC+06:00
44             #'BST' => '+0100', # British Summer Time (British Standard Time from Feb 1968 to Oct 1971) UTC+01:00
45             #'BTT' => '+0600', # Bhutan Time UTC+06:00
46             #'CAT' => '+0200', # Central Africa Time UTC+02:00
47             #'CCT' => '+0630', # Cocos Islands Time UTC+06:30
48             'CDT' => '-0500', # Central Daylight Time (North America) UTC-05:00
49             #'CEDT' => '+0200', # Central European Daylight Time UTC+02:00
50             #'CEST' => '+0200', # Central European Summer Time UTC+02:00
51             #'CET' => '+0100', # Central European Time UTC+01:00
52             #'CHAST'=> '+1245', # Chatham Standard Time UTC+12:45
53             #'CIST' => '-0800', # Clipperton Island Standard Time UTC-08:00
54             #'CKT' => '-1000', # Cook Island Time UTC-10:00
55             #'CLST' => '-0300', # Chile Summer Time UTC-03:00
56             #'CLT' => '-0400', # Chile Standard Time UTC-04:00
57             #'COST' => '-0400', # Colombia Summer Time UTC-04:00
58             #'COT' => '-0500', # Colombia Time UTC-05:00
59             'CST' => '-0600', # Central Standard Time (North America) UTC-06:00
60             #'CST' => '+0800', # China Standard Time UTC+08:00
61             #'CVT' => '-0100', # Cape Verde Time UTC-01:00
62             #'CXT' => '+0700', # Christmas Island Time UTC+07:00
63             #'ChST' => '+1000', # Chamorro Standard Time UTC+10:00
64             # 'DST' => '' # Daylight saving time Depending
65             #'DFT' => '+0100', # AIX specific equivalent of Central European Time UTC+01:00
66             #'EAST' => '-0600', # Easter Island Standard Time UTC-06:00
67             #'EAT' => '+0300', # East Africa Time UTC+03:00
68             #'ECT' => '-0400', # Eastern Caribbean Time (does not recognise DST) UTC-04:00
69             #'ECT' => '-0500', # Ecuador Time UTC-05:00
70             'EDT' => '-0400', # Eastern Daylight Time (North America) UTC-04:00
71             #'EEDT' => '+0300', # Eastern European Daylight Time UTC+03:00
72             #'EEST' => '+0300', # Eastern European Summer Time UTC+03:00
73             #'EET' => '+0200', # Eastern European Time UTC+02:00
74             'EST' => '+0500', # Eastern Standard Time (North America) UTC-05:00
75             #'FJT' => '+1200', # Fiji Time UTC+12:00
76             #'FKST' => '-0400', # Falkland Islands Standard Time UTC-04:00
77             #'GALT' => '-0600', # Galapagos Time UTC-06:00
78             #'GET' => '+0400', # Georgia Standard Time UTC+04:00
79             #'GFT' => '-0300', # French Guiana Time UTC-03:00
80             #'GILT' => '+1200', # Gilbert Island Time UTC+12:00
81             #'GIT' => '-0900', # Gambier Island Time UTC-09:00
82             'GMT' => '+0000', # Greenwich Mean Time UTC
83             #'GST' => '-0200', # South Georgia and the South Sandwich Islands UTC-02:00
84             #'GYT' => '-0400', # Guyana Time UTC-04:00
85             'HADT' => '-0900', # Hawaii-Aleutian Daylight Time UTC-09:00
86             'HAST' => '-1000', # Hawaii-Aleutian Standard Time UTC-10:00
87             #'HKT' => '+0800', # Hong Kong Time UTC+08:00
88             #'HMT' => '+0500', # Heard and McDonald Islands Time UTC+05:00
89             'HST' => '-1000', # Hawaii Standard Time UTC-10:00
90             #'IRKT' => '+0800', # Irkutsk Time UTC+08:00
91             #'IRST' => '+0330', # Iran Standard Time UTC+03:30
92             #'IST' => '+0530', # Indian Standard Time UTC+05:30
93             #'IST' => '+0100', # Irish Summer Time UTC+01:00
94             #'IST' => '+0200', # Israel Standard Time UTC+02:00
95             'JST' => '+0900', # Japan Standard Time UTC+09:00
96             #'KRAT' => '+0700', # Krasnoyarsk Time UTC+07:00
97             #'KST' => '+0900', # Korea Standard Time UTC+09:00
98             #'LHST' => '+1030', # Lord Howe Standard Time UTC+10:30
99             #'LINT' => '+1400', # Line Islands Time UTC+14:00
100             #'MAGT' => '+1100', # Magadan Time UTC+11:00
101             'MDT' => '-0600', # Mountain Daylight Time(North America) UTC-06:00
102             #'MIT' => '-0930', # Marquesas Islands Time UTC-09:30
103             #'MSD' => '+0400', # Moscow Summer Time UTC+04:00
104             #'MSK' => '+0300', # Moscow Standard Time UTC+03:00
105             #'MST' => '+0800', # Malaysian Standard Time UTC+08:00
106             'MST' => '-0700', # Mountain Standard Time(North America) UTC-07:00
107             #'MST' => '+0630', # Myanmar Standard Time UTC+06:30
108             #'MUT' => '+0400', # Mauritius Time UTC+04:00
109             #'NDT' => '-0230', # Newfoundland Daylight Time UTC-02:30
110             #'NFT' => '+1130', # Norfolk Time[1] UTC+11:30
111             #'NPT' => '+0545', # Nepal Time UTC+05:45
112             #'NST' => '-0330', # Newfoundland Standard Time UTC-03:30
113             #'NT' => '-0330', # Newfoundland Time UTC-03:30
114             #'OMST' => '+0600', # Omsk Time UTC+06:00
115             'PDT' => '-0700', # Pacific Daylight Time(North America) UTC-07:00
116             #'PETT' => '+1200', # Kamchatka Time UTC+12:00
117             #'PHOT' => '+1300', # Phoenix Island Time UTC+13:00
118             #'PKT' => '+0500', # Pakistan Standard Time UTC+05:00
119             'PST' => '-0800', # Pacific Standard Time (North America) UTC-08:00
120             #'PST' => '+0800', # Philippine Standard Time UTC+08:00
121             #'RET' => '+0400', # Reunion Time UTC+04:00
122             #'SAMT' => '+0400', # Samara Time UTC+04:00
123             #'SAST' => '+0200', # South African Standard Time UTC+02:00
124             #'SBT' => '+1100', # Solomon Islands Time UTC+11:00
125             #'SCT' => '+0400', # Seychelles Time UTC+04:00
126             #'SLT' => '+0530', # Sri Lanka Time UTC+05:30
127             #'SST' => '-1100', # Samoa Standard Time UTC-11:00
128             #'SST' => '+0800', # Singapore Standard Time UTC+08:00
129             #'TAHT' => '-1000', # Tahiti Time UTC-10:00
130             #'THA' => '+0700', # Thailand Standard Time UTC+07:00
131             'UT' => '-0000', # Coordinated Universal Time UTC
132             'UTC' => '-0000', # Coordinated Universal Time UTC
133             #'UYST' => '-0200', # Uruguay Summer Time UTC-02:00
134             #'UYT' => '-0300', # Uruguay Standard Time UTC-03:00
135             #'VET' => '-0430', # Venezuelan Standard Time UTC-04:30
136             #'VLAT' => '+1000', # Vladivostok Time UTC+10:00
137             #'WAT' => '+0100', # West Africa Time UTC+01:00
138             #'WEDT' => '+0100', # Western European Daylight Time UTC+01:00
139             #'WEST' => '+0100', # Western European Summer Time UTC+01:00
140             #'WET' => '-0000', # Western European Time UTC
141             #'YAKT' => '+0900', # Yakutsk Time UTC+09:00
142             #'YEKT' => '+0500', # Yekaterinburg Time UTC+05:00
143 85     85   611 };
  85         241  
144              
145             sub monthname {
146             # Month name list
147             # @param [Integer] argv1 Require full name or not
148             # @return [Array, String] Month name list or month name
149             # @example Get the names of each month
150             # monthname() #=> ['Jan', 'Feb', ...]
151             # monthname(1) #=> ['January', 'February', 'March', ...]
152 3     3 0 442514 my $class = shift;
153 3   100     16 my $argv1 = shift // 0;
154 3 100       16 return MonthName->{ $argv1 ? 'full' : 'abbr' };
155             }
156              
157             sub parse {
158             # Parse date string; strptime() wrapper
159             # @param [String] argv1 Date string
160             # @return [String] Converted date string
161             # @see http://en.wikipedia.org/wiki/ISO_8601
162             # @see http://www.ietf.org/rfc/rfc3339.txt
163             # @example Parse date string and convert to generic format string
164             # parse("2015-11-03T23:34:45 Tue") #=> Tue, 3 Nov 2015 23:34:45 +0900
165             # parse("Tue, Nov 3 2015 2:2:2") #=> Tue, 3 Nov 2015 02:02:02 +0900
166 3644     3644 1 50602 my $class = shift;
167 3644   100     9772 my $argv1 = shift || return "";
168              
169             # "Apr 29", -> "Apr 29" "Thu,13" -> "Thu, 13"
170 3643         10165 my $datestring = $argv1; s/[,](\d+)/, $1/, s/(\d{1,2}),/$1/ for $datestring;
  3643         26580  
171 3643         17691 my @timetokens = split(' ', $datestring);
172 3643         8990 my $parseddate = ''; # [String] Canonified Date/Time string
173 3643         5457 my $afternoon1 = 0; # [Integer] After noon flag
174 3643         6312 my $altervalue = {}; # [Hash] To store alternative values
175 3643         34999 my $v = {
176             'Y' => "", # [Integer] Year
177             'M' => "", # [String] Month Abbr.
178             'd' => "", # [Integer] Day
179             'a' => "", # [String] Day of week, Abbr.
180             'T' => "", # [String] Time
181             'z' => "", # [Integer] Timezone offset
182             };
183              
184 3643         8573 for my $p ( @timetokens ) {
185             # Parse each piece of time
186 21835 100 100     140252 if( $p =~ /\A[A-Z][a-z]{2,}[,]?\z/ ) {
    100 66        
    100          
    100          
    100          
    100          
187             # Day of week or Day of week; Thu, Apr, ...
188 7067 100       21741 substr($p, -1, 1, '') if substr($p, -1, 1) eq ','; # "Thu," => "Thu"
189 7067 100       14962 $p = substr($p, 0, 3) if length $p > 3;
190              
191 7067 100       18728 if( grep { $p eq $_ } DayOfWeek->{'abbr'}->@* ) {
  49469 100       89118  
192             # Day of week; Mon, Thu, Sun,...
193 3465         10316 $v->{'a'} = $p;
194              
195 43224         72335 } elsif( grep { $p eq $_ } MonthName->{'abbr'}->@* ) {
196             # Month name abbr.; Apr, May, ...
197 3599         11595 $v->{'M'} = $p;
198             }
199             } elsif( $p =~ /\A\d{1,4}\z/ ) {
200             # Year or Day; 2005, 31, 04, 1, ...
201 7200         21132 $p = int $p;
202 7200 100       18615 if( $p > 31 ) {
203             # The piece is the value of an year
204 3555         9879 $v->{'Y'} = $p;
205              
206             } else {
207             # The piece is the value of a day
208 3645 100       10783 if( $v->{'d'} ) {
209             # 2-digit year?
210 46 50       280 $altervalue->{'Y'} = $p unless $v->{'Y'};
211              
212             } else {
213             # The value is "day"
214 3599         10192 $v->{'d'} = $p;
215             }
216             }
217             } elsif( $p =~ /\A([0-2]\d):([0-5]\d):([0-5]\d)\z/ || $p =~ /\A(\d{1,2})[-:](\d{1,2})[-:](\d{1,2})\z/ ) {
218             # Time; 12:34:56, 03:14:15, ...
219             # Arrival-Date: 2014-03-26 00-01-19
220 3639 100 100     62882 $v->{'T'} = sprintf("%02d:%02d:%02d", $1, $2, $3) if( $1 < 24 && $2 < 60 && $3 < 60 );
      100        
221              
222             } elsif( $p =~ /\A([0-2]\d):([0-5]\d)\z/ ) {
223             # Time; 12:34 => 12:34:00
224 2 50 33     17 $v->{'T'} = sprintf("%02d:%02d:00", $1, $2) if( $1 < 24 && $2 < 60 );
225              
226             } elsif( $p =~ /\A(\d\d?):(\d\d?)\z/ ) {
227             # Time: 1:4 => 01:04:00
228 1         6 $v->{'T'} = sprintf("%02d:%02d:00", $1, $2);
229              
230             } elsif( lc $p eq 'am' || lc $p eq 'pm' ) {
231             # AM or PM
232 12         31 $afternoon1 = 1;
233              
234             } else {
235             # Timezone offset and others
236 3914 100       15145 if( $p =~ /\A[-+][01]\d{3}\z/ ) {
    100          
237             # Timezone offset; +0000, +0900, -1000, ...
238 3521   33     20280 $v->{'z'} ||= $p;
239              
240             } elsif( $p =~ /\A[(]?[A-Z]{2,5}[)]?\z/ ) {
241             # Timezone abbreviation; JST, GMT, UTC, ...
242 337   50     1617 $v->{'z'} ||= __PACKAGE__->abbr2tz($p) || '+0000';
      66        
243              
244             } else {
245             # Other date format
246 56 100       482 if( $p =~ m|\A(\d{4})[-/](\d{1,2})[-/](\d{1,2})\z| ) {
    100          
    100          
    100          
247             # Mail.app(MacOS X)'s faked Bounce, Arrival-Date: 2010-06-18 17:17:52 +0900
248 35         193 $v->{'Y'} = int $1;
249 35         180 $v->{'M'} = MonthName->{'abbr'}->[int($2) - 1];
250 35         140 $v->{'d'} = int $3;
251              
252             } elsif( $p =~ m|\A(\d{4})[-/](\d{1,2})[-/](\d{1,2})T([0-2]\d):([0-5]\d):([0-5]\d)\z| ) {
253             # ISO 8601; 2000-04-29T01:23:45
254 1         4 $v->{'Y'} = int $1;
255 1         4 $v->{'M'} = MonthName->{'abbr'}->[int($2) - 1];
256 1 50       4 $v->{'d'} = int $3 if $3 < 32;
257              
258 1 50 33     11 $v->{'T'} = sprintf("%02d:%02d:%02d", $4, $5, $6) if( $4 < 24 && $5 < 60 && $6 < 60 );
      33        
259              
260             } elsif( $p =~ m|\A(\d{1,2})/(\d{1,2})/(\d{1,2})\z| ) {
261             # 4/29/01 11:34:45 PM
262 6         45 $v->{'M'} = MonthName->{'abbr'}->[int($1) - 1];
263 6         22 $v->{'d'} = int $2;
264 6         19 $v->{'Y'} = int($3) + 2000;
265 6 50       86 $v->{'Y'} -= 100 if $v->{'Y'} > Time::Piece->new->year() + 1;
266              
267             } elsif( $p =~ m|\A(\d{1,2})[-/](\d{1,2})[-/](\d{4})| ) {
268             # 29-04-2017 22:22
269 1 50       6 $v->{'d'} = int $1 if $1 < 32;
270 1         4 $v->{'M'} = MonthName->{'abbr'}->[int($2) - 1];
271 1         3 $v->{'Y'} = int($3);
272             }
273             }
274             }
275             } # End of while()
276              
277 3643 100 100     22172 if( $v->{'T'} && $afternoon1 ) {
278             # +12
279 12         33 my $t0 = $v->{'T'};
280 12         47 my @t1 = split(':', $v->{'T'});
281 12         65 $v->{'T'} = sprintf("%02d:%02d:%02d", $t1[0] + 12, $t1[1], $t1[2]);
282 12 100       48 $v->{'T'} = $t0 if $t1[0] > 12;
283             }
284              
285 3643   100     15881 $v->{'a'} ||= 'Thu'; # There is no day of week
286 3643 100 100     24801 $v->{'Y'} += 1900 if length($v->{'Y'}) > 0 && int($v->{'Y'}) < 200; # 99 -> 1999, 102 -> 2002
287 3643   66     12451 $v->{'z'} ||= __PACKAGE__->second2tz(Time::Piece->new->tzoffset);
288              
289             # Adjust 2-digit Year
290 3643 100 66     16458 if( exists $altervalue->{'Y'} && ! $v->{'Y'} ) {
291             # Check alternative value(Year)
292 46 50       162 if( $altervalue->{'Y'} >= 82 ) {
293             # SMTP was born in 1982
294 0   0     0 $v->{'Y'} ||= 1900 + $altervalue->{'Y'};
295              
296             } else {
297             # 20XX
298 46   33     232 $v->{'Y'} ||= 2000 + $altervalue->{'Y'};
299             }
300             }
301              
302             # Check each piece
303 3643 100       13113 if( grep { $_ eq "" } values %$v ) {
  21858         39889  
304             # Strange date format
305 5         53 printf(STDERR " ***warning: Strange date format [%s]\n", $datestring);
306 5         24 return "";
307             }
308              
309             # Build date string
310             # Thu, 29 Apr 2004 10:01:11 +0900
311 3638 100 100     19222 return "" if $v->{'Y'} < 1902 || $v->{'Y'} > 2037; # -(2^31) ~ (2^31)
312 3636         39252 return sprintf("%s, %s %s %s %s %s", $v->{'a'}, $v->{'d'}, $v->{'M'}, $v->{'Y'}, $v->{'T'}, $v->{'z'});
313             }
314              
315             sub abbr2tz {
316             # Abbreviation -> Tiemzone
317             # @param [String] argv1 Abbr. e.g.) JST, GMT, PDT
318             # @return [String] +0900, +0000, -0600 or an empty string if the argument is invalid
319             # format or not supported abbreviation
320             # @example Get the timezone string of "JST"
321             # abbr2tz('JST') #=> '+0900'
322 77     77 1 776 my $class = shift;
323 77   100     229 my $argv1 = shift || return "";
324 76         442 return TimeZones->{ $argv1 };
325             }
326              
327             sub tz2second {
328             # Convert to second
329             # @param [String] argv1 Timezone string e.g) +0900
330             # @return [Integer] Seconds or -1 it the argument is invalid format string
331             # @see second2tz
332             # @example Convert '+0900' to seconds
333             # tz2second('+0900') #=> 32400
334 3687     3687 0 79342 my $class = shift;
335 3687   100     13027 my $argv1 = shift || return -1;
336              
337 3661 100       16037 if( $argv1 =~ /\A([-+])(\d)(\d)(\d{2})\z/ ) {
    100          
338 3613         5746 my $ztime = 0;
339 3613         44219 my $digit = {
340             'operator' => $1,
341             'hour-10' => $2,
342             'hour-01' => $3,
343             'minutes' => $4
344             };
345 3613         18131 $ztime += ( $digit->{'hour-10'} * 10 + $digit->{'hour-01'} ) * 3600;
346 3613         9111 $ztime += ( $digit->{'minutes'} * 60 );
347 3613 100       15652 $ztime *= -1 if $digit->{'operator'} eq '-';
348              
349 3613 100       10329 return -1 if abs($ztime) > TZ_OFFSET;
350 3611         15269 return $ztime;
351              
352             } elsif( $argv1 =~ /\A[A-Za-z]+\z/ ) {
353 1         8 return __PACKAGE__->tz2second(TimeZones->{ $argv1 });
354              
355             } else {
356 47         323 return -1;
357             }
358             }
359              
360             sub second2tz {
361             # Convert to Timezone string
362             # @param [Integer] argv1 Second to be converted
363             # @return [String] Timezone offset string
364             # @see tz2second
365             # @example Get timezone offset string of specified seconds
366             # second2tz(12345) #=> '+0325'
367 64     64 0 13233 my $class = shift;
368 64   100     245 my $argv1 = shift // return '+0000';
369 63         246 my $digit = {'operator' => '+'};
370              
371 63 50 66     375 return '' if( ref($argv1) && ref($argv1) ne 'Time::Seconds' );
372 63 100       780 return '' if( abs($argv1) > TZ_OFFSET ); # UTC+14 + 1(DST?)
373 61 100       2614 $digit->{'operator'} = '-' if $argv1 < 0;
374              
375 61         908 $digit->{'hours'} = int(abs($argv1) / 3600);
376 61         1114 $digit->{'minutes'} = int((abs($argv1) % 3600) / 60);
377 61         1814 return sprintf("%s%02d%02d", $digit->{'operator'}, $digit->{'hours'}, $digit->{'minutes'});
378             }
379              
380             1;
381             __END__