File Coverage

blib/lib/Date/Utility.pm
Criterion Covered Total %
statement 297 310 95.8
branch 87 98 88.7
condition 38 50 76.0
subroutine 85 87 97.7
pod 25 25 100.0
total 532 570 93.3


line stmt bran cond sub pod time code
1             package Date::Utility;
2              
3 8     8   1057076 use 5.006;
  8         124  
4 8     8   46 use strict;
  8         20  
  8         214  
5 8     8   56 use warnings;
  8         16  
  8         311  
6 8     8   55 use feature qw(state);
  8         21  
  8         1266  
7              
8             =head1 NAME
9              
10             Date::Utility - A class that represents a datetime in various format
11              
12             =cut
13              
14             our $VERSION = '1.10';
15              
16             =head1 SYNOPSIS
17              
18             use Date::Utility;
19              
20             Date::Utility->new(); # Use current time
21             Date::Utility->new(1249637400);
22             Date::Utility->new('dd-mmm-yy');
23             Date::Utility->new('dd-mmm-yyyy');
24             Date::Utility->new('dd-Mmm-yy hh:mm:ssGMT');
25             Date::Utility->new('dd-Mmm-yy hhhmm');
26             Date::Utility->new('YYYY-MM-DD');
27             Date::Utility->new('YYYYMMDD');
28             Date::Utility->new('YYYYMMDDHHMMSS');
29             Date::Utility->new('YYYY-MM-DD HH:MM:SS');
30             Date::Utility->new('YYYY-MM-DDTHH:MM:SSZ');
31              
32             =head1 DESCRIPTION
33              
34             A class that represents a datetime in various format
35              
36             =cut
37              
38 8     8   6520 use Moose;
  8         4222278  
  8         60  
39 8     8   63911 use Carp qw( confess croak );
  8         25  
  8         714  
40 8     8   58 use POSIX qw( floor );
  8         19  
  8         79  
41 8     8   16843 use Scalar::Util qw(looks_like_number);
  8         22  
  8         407  
42 8     8   7592 use Tie::Hash::LRU;
  8         5692  
  8         321  
43 8     8   9082 use Time::Local qw(timegm);
  8         22470  
  8         664  
44 8     8   76 use Try::Tiny;
  8         1777  
  8         2046  
45 8     8   5608 use Time::Duration::Concise::Localize;
  8         138887  
  8         362  
46 8     8   72 use POSIX qw(floor);
  8         19  
  8         62  
47              
48             my %popular;
49             my $lru = tie %popular, 'Tie::Hash::LRU', 300;
50              
51             has epoch => (
52             is => 'ro',
53             isa => 'Int',
54             required => 1,
55             );
56              
57             has [qw(
58             datetime_ddmmmyy_hhmmss_TZ
59             datetime_ddmmmyy_hhmmss
60             datetime_yyyymmdd_hhmmss
61             datetime_yyyymmdd_hhmmss_TZ
62             datetime_iso8601
63             date
64             datetime
65             date_ddmmyy
66             date_ddmmyyyy
67             date_ddmmmyy
68             date_yyyymmdd
69             date_ddmmmyyyy
70             date_ddmonthyyyy
71             days_in_month
72             db_timestamp
73             day_as_string
74             full_day_name
75             month_as_string
76             full_month_name
77             http_expires_format
78             iso8601
79             time
80             time_hhmm
81             time_hhmmss
82             time_cutoff
83             timezone
84             second
85             minute
86             hour
87             day_of_month
88             quarter_of_year
89             month
90             year
91             _gmtime_attrs
92             year_in_two_digit
93             day_of_week
94             day_of_year
95             days_since_epoch
96             seconds_after_midnight
97             is_a_weekend
98             is_a_weekday
99             )
100             ] => (
101             is => 'ro',
102             lazy_build => 1,
103             );
104              
105             sub _build__gmtime_attrs {
106 409     409   827 my $self = shift;
107 409         815 my %params;
108              
109 409         4724 @params{qw(second minute hour day_of_month month year day_of_week day_of_year)} = gmtime($self->{epoch});
110              
111 409         11028 return \%params;
112             }
113              
114             =head1 ATTRIBUTES
115              
116             =head2 second
117              
118             =cut
119              
120             sub _build_second {
121 8     8   79 my $self = shift;
122              
123 8         239 return sprintf '%02d', $self->_gmtime_attrs->{second};
124             }
125              
126             =head2 minute
127              
128             =cut
129              
130             sub _build_minute {
131 8     8   14 my $self = shift;
132              
133 8         201 return sprintf '%02d', $self->_gmtime_attrs->{minute};
134             }
135              
136             =head2 hour
137              
138             =cut
139              
140             sub _build_hour {
141 8     8   20 my $self = shift;
142              
143 8         240 return sprintf '%02d', $self->_gmtime_attrs->{hour};
144             }
145              
146             =head2 day_of_month
147              
148             =cut
149              
150             sub _build_day_of_month {
151 408     408   799 my $self = shift;
152              
153 408         10287 return $self->_gmtime_attrs->{day_of_month};
154             }
155              
156             =head2 month
157              
158             =cut
159              
160             sub _build_month {
161 408     408   1026 my $self = shift;
162              
163 408         10056 my $gm_mon = $self->_gmtime_attrs->{month};
164              
165 408         9321 return ++$gm_mon;
166             }
167              
168             =head2 quarter_of_year
169              
170             =cut
171              
172             sub _build_quarter_of_year {
173 3     3   6 my $self = shift;
174              
175 3         81 return int(($self->month - 0.0000001) / 3) + 1;
176              
177             }
178              
179             =head2 day_of_week
180              
181             return day of week begin with 0
182              
183             =cut
184              
185             sub _build_day_of_week {
186 684     684   18201 return ((shift->{epoch} / 86400) + 4) % 7;
187             }
188              
189             =head2 day_of_year
190              
191             =cut
192              
193             sub _build_day_of_year {
194 3     3   10 my $self = shift;
195              
196 3         87 return $self->_gmtime_attrs->{day_of_year} + 1;
197             }
198              
199             =head2 year
200              
201             =cut
202              
203             sub _build_year {
204 408     408   877 my $self = shift;
205              
206 408         11126 return $self->_gmtime_attrs->{year} + 1900;
207             }
208              
209             =head2 time
210              
211             =cut
212              
213             sub _build_time {
214 3     3   6 my $self = shift;
215              
216 3         77 return $self->hour . 'h' . $self->minute;
217             }
218              
219             =head2 time_hhmm
220              
221             Returns time in hh:mm format
222              
223             =cut
224              
225             sub _build_time_hhmm {
226 13     13   24 my $self = shift;
227              
228 13         313 return join(':', ($self->hour, $self->minute));
229             }
230              
231             =head2 time_hhmmss
232              
233             Returns time in hh:mm:ss format
234              
235             =cut
236              
237             sub _build_time_hhmmss {
238 13     13   38 my $self = shift;
239              
240 13         339 return join(':', ($self->time_hhmm, $self->second));
241             }
242              
243             =head2 time_cutoff
244              
245             Set the timezone for cutoff to UTC
246              
247             =cut
248              
249             sub _build_time_cutoff {
250 3     3   7 my $self = shift;
251              
252 3         78 return 'UTC ' . $self->time_hhmm;
253             }
254              
255             =head2 year_in_two_digit
256              
257             Returns year in two digit format. Example: 15
258              
259             =cut
260              
261             sub _build_year_in_two_digit {
262 3     3   6 my $self = shift;
263 3         73 my $two_digit_year = $self->year - 2000;
264              
265 3 100       12 if ($two_digit_year < 0) {
266 1         3 $two_digit_year += 100;
267             }
268              
269 3         83 return sprintf '%02d', $two_digit_year;
270             }
271              
272             =head2 timezone
273              
274             Set the timezone to GMT
275              
276             =cut
277              
278             sub _build_timezone {
279 6     6   144 return 'GMT';
280             }
281              
282             =head2 datetime
283              
284             See, db_timestamp
285              
286             =cut
287              
288             sub _build_datetime {
289 3     3   7 my $self = shift;
290              
291 3         95 return $self->db_timestamp;
292             }
293              
294             =head2 datetime_ddmmmyy_hhmmss_TZ
295              
296             Returns datetime in "dd-mmm-yy hh:mm:ssGMT" format
297              
298             =cut
299              
300             sub _build_datetime_ddmmmyy_hhmmss_TZ {
301 3     3   8 my $self = shift;
302              
303 3         88 return $self->date_ddmmmyy . ' ' . $self->time_hhmmss . $self->timezone;
304             }
305              
306             =head2 datetime_ddmmmyy_hhmmss
307              
308             Returns datetime in "dd-mmm-yy hh:mm:ss" format
309              
310             =cut
311              
312             sub _build_datetime_ddmmmyy_hhmmss {
313 0     0   0 my $self = shift;
314              
315 0         0 return $self->date_ddmmmyy . ' ' . $self->time_hhmmss;
316             }
317              
318             =head2 date_ddmmmyyyy
319              
320             Returns date in dd-mmm-yyyy format
321              
322             =cut
323              
324             sub _build_date_ddmmmyyyy {
325 3     3   7 my $self = shift;
326              
327 3         81 return join('-', ($self->day_of_month, $self->month_as_string, $self->year));
328             }
329              
330             =head2 date_ddmonthyyyy
331              
332             Returns date in dd-month-yyyy format
333              
334             =cut
335              
336             sub _build_date_ddmonthyyyy {
337 3     3   7 my $self = shift;
338              
339 3         83 return join(' ', ($self->day_of_month, $self->full_month_name, $self->year));
340             }
341              
342             =head2 date
343              
344             Returns datetime in YYYY-MM-DD format
345              
346             =cut
347              
348             sub _build_date {
349 818     818   1359 my $self = shift;
350              
351 818         21587 return $self->date_yyyymmdd;
352             }
353              
354             =head2 date_ddmmmyy
355              
356             Returns datetime in dd-Mmm-yy format
357              
358             =cut
359              
360             sub _build_date_ddmmmyy {
361 3     3   7 my $self = shift;
362              
363 3         76 return join('-', ($self->day_of_month, $self->month_as_string, $self->year_in_two_digit));
364             }
365              
366             =head2 days_since_epoch
367              
368             Returns number of days since 1970-01-01
369              
370             =cut
371              
372             sub _build_days_since_epoch {
373 8     8   16 my $self = shift;
374              
375 8         255 return floor($self->{epoch} / 86400);
376             }
377              
378             =head2 seconds_after_midnight
379              
380             Returns number of seconds after midnight of the same day.
381              
382             =cut
383              
384             sub _build_seconds_after_midnight {
385 3     3   8 my $self = shift;
386              
387 3         84 return $self->{epoch} % 86400;
388             }
389              
390             =head2 is_a_weekend
391              
392             =cut
393              
394             sub _build_is_a_weekend {
395 3     3   9 my $self = shift;
396              
397 3 100 100     78 return ($self->day_of_week == 0 || $self->day_of_week == 6) ? 1 : 0;
398             }
399              
400             =head2 is_a_weekday
401              
402             =cut
403              
404             sub _build_is_a_weekday {
405 3     3   8 my $self = shift;
406              
407 3 100       84 return ($self->is_a_weekend) ? 0 : 1;
408             }
409              
410             my $EPOCH_RE = qr/^-?[0-9]{1,13}$/;
411              
412             =head2 new
413              
414             Returns a Date::Utility object.
415              
416             =cut
417              
418             sub new {
419 224637     224637 1 314158498 my ($self, $params_ref) = @_;
420 224637         449119 my $new_params = {};
421              
422 224637 100       2439829 if (not defined $params_ref) {
    100          
    100          
    100          
423 4         22 $new_params->{epoch} = time;
424             } elsif (ref $params_ref eq 'Date::Utility') {
425 1         4 return $params_ref;
426             } elsif (ref $params_ref eq 'HASH') {
427 47 100 100     395 if (not($params_ref->{'datetime'} or $params_ref->{epoch})) {
    100 66        
    100          
428 1         19 confess 'Must pass either datetime or epoch to the Date object constructor';
429             } elsif ($params_ref->{'datetime'} and $params_ref->{epoch}) {
430 1         11 confess 'Must pass only one of datetime or epoch to the Date object constructor';
431             } elsif ($params_ref->{epoch}) {
432             #strip other potential parameters
433 7         23 $new_params->{epoch} = $params_ref->{epoch};
434              
435             } else {
436             #strip other potential parameters
437 38         120 $new_params = _parse_datetime_param($params_ref->{'datetime'});
438             }
439             } elsif ($params_ref =~ $EPOCH_RE) {
440 200417         681051 $new_params->{epoch} = $params_ref;
441             } else {
442 24168         56253 $new_params = _parse_datetime_param($params_ref);
443             }
444              
445 224620         1554996 my $obj = $popular{$new_params->{epoch}};
446              
447 224620 100       716209 if (not $obj) {
448 200909         6362741 $obj = $self->_new($new_params);
449 200909         7639005 $popular{$new_params->{epoch}} = $obj;
450             }
451              
452 224620         1300185 return $obj;
453              
454             }
455              
456             =head2 _parse_datetime_param
457              
458             User may supplies datetime parameters but it currently only supports the following formats:
459             dd-mmm-yy ddhddGMT, dd-mmm-yy, dd-mmm-yyyy, dd-Mmm-yy hh:mm:ssGMT, YYYY-MM-DD, YYYYMMDD, YYYYMMDDHHMMSS, yyyy-mm-dd hh:mm:ss, yyyy-mm-ddThh:mm:ss or yyyy-mm-ddThh:mm:ssZ.
460              
461              
462             =cut
463              
464             my $mon_re = qr/j(?:an|u[nl])|feb|ma[ry]|a(?:pr|ug)|sep|oct|nov|dec/i;
465             my $sub_second = qr/^[0-9]+\.[0-9]+$/;
466             my $date_only = qr/^([0-3]?[0-9])-($mon_re)-([0-9]{2}|[0-9]{4})$/;
467             my $time_only_tz = qr/([0-2]?[0-9])[h:]([0-5][0-9])(?::)?([0-5][0-9])?(?:GMT)?/;
468             my $date_with_time = qr /^([0-3]?[0-9])-($mon_re)-([0-9]{2}) $time_only_tz$/;
469             my $numeric_date_regex = qr/([12][0-9]{3})-?([01]?[0-9])-?([0-3]?[0-9])/;
470             my $numeric_date_only = qr/^$numeric_date_regex$/;
471             my $fully_specced = qr/^([12][0-9]{3})-?([01]?[0-9])-?([0-3]?[0-9])(?:T|\s)?([0-2]?[0-9]):?([0-5]?[0-9]):?([0-5]?[0-9])(\.[0-9]+)?(?:Z)?$/;
472             my $numeric_date_only_dd_mm_yyyy = qr/^([0-3]?[0-9])-([01]?[0-9])-([12][0-9]{3})$/;
473             my $datetime_yyyymmdd_hhmmss_TZ = qr/^$numeric_date_regex $time_only_tz$/;
474              
475             sub _parse_datetime_param {
476 24206     24206   40994 my $datetime = shift;
477              
478             # If it's date only, take the epoch at midnight.
479 24206         44052 my ($hour, $minute, $second) = (0, 0, 0);
480 24206         35786 my ($day, $month, $year);
481              
482             # The ordering of these regexes is an attempt to match early
483             # to avoid extra comparisons. If our mix of supplied datetimes changes
484             # it might be worth revisiting this.
485 24206 50       212860 if ($datetime =~ $sub_second) {
    100          
    100          
    100          
    100          
    100          
    100          
486             # We have an epoch with sub second precision which we can't handle
487 0         0 return {epoch => int($datetime)};
488             } elsif ($datetime =~ $date_only) {
489 13         49 $day = $1;
490 13         48 $month = month_abbrev_to_number($2);
491 13         30 $year = $3;
492             } elsif ($datetime =~ $date_with_time) {
493 36         140 $day = $1;
494 36         121 $month = month_abbrev_to_number($2);
495 36         82 $year = $3;
496 36         92 $hour = $4;
497 36         67 $minute = $5;
498 36 100       109 if (defined $6) {
499 26         51 $second = $6;
500             }
501             } elsif ($datetime =~ $numeric_date_only) {
502 24074         59965 $day = $3;
503 24074         37881 $month = $2;
504 24074         41705 $year = $1;
505             } elsif ($datetime =~ $numeric_date_only_dd_mm_yyyy) {
506 2         6 $day = $1;
507 2         5 $month = $2;
508 2         8 $year = $3;
509             } elsif ($datetime =~ $fully_specced) {
510 70         209 $day = $3;
511 70         137 $month = $2;
512 70         134 $year = $1;
513 70         138 $hour = $4;
514 70         125 $minute = $5;
515 70         145 $second = $6;
516             } elsif ($datetime =~ $datetime_yyyymmdd_hhmmss_TZ) {
517 1         4 $year = $1;
518 1         4 $month = $2;
519 1         2 $day = $3;
520 1         3 $hour = $4;
521 1         3 $minute = $5;
522 1         2 $second = $6;
523             }
524             # Type constraints mean we can't ever end up in here.
525             else {
526 10         126 confess "Invalid datetime format: $datetime";
527             }
528              
529             # Now that we've extracted out values, let's turn them into an epoch.
530             # The all of following adjustments seem kind of gross:
531 24196 100       58082 if (length $year == 2) {
532 45 100 100     209 if ($year > 30 and $year < 70) {
533 1         16 croak 'Date::Utility only supports two-digit years from 1970-2030. We got [' . $year . ']';
534             }
535              
536 44 100       129 $year += ($year <= 30) ? 2000 : 1900;
537             }
538              
539 24195         87261 my $epoch = timegm($second, $minute, $hour, $day, $month - 1, $year);
540              
541             return {
542 24192         821329 epoch => $epoch,
543             second => sprintf("%02d", $second),
544             minute => sprintf("%02d", $minute),
545             hour => sprintf("%02d", $hour),
546             day_of_month => $day + 0,
547             month => $month + 0,
548             year => $year + 0,
549             };
550             }
551              
552             =head2 days_between
553              
554             Returns number of days between two dates.
555              
556             =cut
557              
558             sub days_between {
559 8     8 1 35 my ($self, $date) = @_;
560              
561 8 50       25 if (not $date) {
562 0         0 Carp::croak('Date parameter not defined');
563             }
564 8         271 return $self->days_since_epoch - $date->days_since_epoch;
565             }
566              
567             =head2 is_before
568              
569             Returns a boolean which indicates whether this date object is earlier in time than the supplied date object.
570              
571             =cut
572              
573             sub is_before {
574 5     5 1 4272 my ($self, $date) = @_;
575              
576 5 50       14 if (not $date) {
577 0         0 Carp::croak('Date parameter not defined');
578             }
579 5 100       30 return ($self->{epoch} < $date->{epoch}) ? 1 : undef;
580             }
581              
582             =head2 is_after
583              
584             Returns a boolean which indicates whether this date object is later in time than the supplied date object.
585              
586             =cut
587              
588             sub is_after {
589 5     5 1 3576 my ($self, $date) = @_;
590              
591 5 50       14 if (not $date) {
592 0         0 Carp::croak('Date parameter not defined');
593             }
594 5 100       29 return ($self->{epoch} > $date->{epoch}) ? 1 : undef;
595             }
596              
597             =head2 is_same_as
598              
599             Returns a boolean which indicates whether this date object is the same time as the supplied date object.
600              
601             =cut
602              
603             sub is_same_as {
604 13     13 1 3264 my ($self, $date) = @_;
605              
606 13 50       33 if (not $date) {
607 0         0 Carp::croak('Date parameter not defined');
608             }
609 13 100       85 return ($self->{epoch} == $date->{epoch}) ? 1 : undef;
610             }
611              
612             =head2 day_as_string
613              
614             Returns the name of the current day in short form. Example: Sun.
615              
616             =cut
617              
618             sub _build_day_as_string {
619 3     3   7 my $self = shift;
620              
621 3         85 return substr($self->full_day_name, 0, 3);
622             }
623              
624             =head2 full_day_name
625              
626             Returns the name of the current day. Example: Sunday
627              
628             =cut
629              
630             # 0..6: Sunday first.
631             my @day_names = qw(Sunday Monday Tuesday Wednesday Thursday Friday Saturday);
632             my %days_to_num = map {
633             my $day = lc $day_names[$_];
634             (
635             substr($day, 0, 3) => $_, # Three letter abbreviation
636             $day => $_, # Full day name
637             $_ => $_, # Number as number
638             );
639             } 0 .. $#day_names;
640              
641             sub _build_full_day_name {
642 403     403   809 my $self = shift;
643              
644 403         9674 return $day_names[$self->day_of_week];
645             }
646              
647             =head2 month_as_string
648              
649             Returns the name of current month in short form. Example: Jan
650              
651             =cut
652              
653             sub _build_month_as_string {
654 403     403   872 my $self = shift;
655              
656 403         9390 return month_number_to_abbrev($self->month);
657             }
658              
659             =head2 full_month_name
660              
661             Returns the full name of current month. Example: January
662              
663             =cut
664              
665             sub _build_full_month_name {
666 3     3   7 my $self = shift;
667              
668 3         72 return month_number_to_fullname($self->month);
669             }
670              
671             =head2 http_expires_format
672              
673             Returns datetime in this format: Fri, 27 Nov 2009 02:12:02 GMT
674              
675             =cut
676              
677             sub _build_http_expires_format {
678 3     3   8 my $self = shift;
679              
680             return
681 3         80 $self->day_as_string . ', '
682             . sprintf('%02d', $self->day_of_month) . ' '
683             . $self->month_as_string . ' '
684             . $self->year . ' '
685             . $self->time_hhmmss . ' '
686             . $self->timezone;
687             }
688              
689             =head2 date_ddmmyy
690              
691             Returns date in this format "dd-mm-yy" (28-02-10)
692              
693             =cut
694              
695             sub _build_date_ddmmyy {
696 3     3   8 my $self = shift;
697              
698 3         84 return join('-', (sprintf('%02d', $self->day_of_month), sprintf('%02d', $self->month), sprintf('%02d', $self->year_in_two_digit)));
699             }
700              
701             =head2 date_ddmmyyyy
702              
703             Returns date in this format "dd-mm-yyyy" (28-02-2010)
704              
705             =cut
706              
707             sub _build_date_ddmmyyyy {
708 3     3   7 my $self = shift;
709              
710 3         83 return join('-', (sprintf('%02d', $self->day_of_month), sprintf('%02d', $self->month), $self->year));
711             }
712              
713             =head2 date_yyyymmdd
714              
715             Returns date in this format "yyyy-mm-dd" (2010-03-02)
716              
717             =cut
718              
719             sub _build_date_yyyymmdd {
720 848     848   1632 my $self = shift;
721              
722 848         20027 return join('-', ($self->year, sprintf('%02d', $self->month), sprintf('%02d', $self->day_of_month)));
723             }
724              
725             =head2 datetime_yyyymmdd_hhmmss
726              
727             Returns: "yyyy-mm-dd hh:mm:ss" (2010-03-02 05:09:40)
728              
729             =cut
730              
731             sub _build_datetime_yyyymmdd_hhmmss {
732 9     9   18 my $self = shift;
733              
734 9         260 return join(' ', ($self->date_yyyymmdd, $self->time_hhmmss));
735             }
736              
737             sub _build_db_timestamp {
738 3     3   8 my $self = shift;
739              
740 3         87 return $self->datetime_yyyymmdd_hhmmss;
741             }
742              
743             =head2 datetime_iso8601 iso8601
744              
745             Since all internal representations are in UTC
746             Returns "yyyy-mm-ddThh:mm:ssZ" (2010-02-02T05:09:40Z)
747              
748             =cut
749              
750             sub _build_datetime_iso8601 {
751 7     7   16 my $self = shift;
752              
753 7         191 return $self->date_yyyymmdd . 'T' . $self->time_hhmmss . 'Z';
754             }
755              
756             sub _build_iso8601 {
757 3     3   9 my $self = shift;
758              
759 3         83 return $self->datetime_iso8601;
760             }
761              
762             =head2 datetime_yyyymmdd_hhmmss_TZ
763              
764             Returns datetime in this format "yyyy-mm-dd hh:mm:ssGMT" (2010-03-02 05:09:40GMT)
765              
766             =cut
767              
768             sub _build_datetime_yyyymmdd_hhmmss_TZ {
769 6     6   14 my $self = shift;
770              
771 6         171 return $self->datetime_yyyymmdd_hhmmss . $self->timezone;
772             }
773              
774             =head2 days_in_month
775              
776             =cut
777              
778             sub _build_days_in_month {
779 22     22   49 my ($self) = @_;
780              
781 22         526 my $month = $self->month;
782             # 30 days hath September, April, June and November.
783 22         89 my %shorties = (
784             9 => 30,
785             4 => 30,
786             6 => 30,
787             11 => 30
788             );
789             # All the rest have 31
790 22   100     93 my $last_day = $shorties{$month} || 31;
791             # Except February.
792 22 100       57 if ($month == 2) {
793 5         130 my $year = $self->year;
794 5 100 66     38 $last_day = (($year % 4 or not $year % 100) and ($year % 400)) ? 28 : 29;
795             }
796              
797 22         573 return $last_day;
798             }
799              
800             =head2 timezone_offset
801              
802             Returns a TimeInterval which represents the difference between UTC and the time in certain timezone
803              
804             =cut
805              
806             =head2 is_dst_in_zone
807              
808             Returns a boolean which indicates whether a certain zone is in DST at the given epoch
809              
810             =cut
811              
812             {
813 8     8   37716 use DateTime;
  8         4195716  
  8         464  
814 8     8   84 use DateTime::TimeZone;
  8         20  
  8         19034  
815              
816             my $bignum = 20000000;
817              
818             my %cache;
819             my $cache_for = sub {
820             my $tm = shift;
821             my $tzname = shift;
822             my $k = int $tm / $bignum;
823              
824             if (my $val = $cache{"$k $tzname"}) {
825             return $val;
826             }
827              
828             my $z = DateTime::TimeZone->new(name => $tzname);
829             my $start_of_interval = $k * $bignum;
830             my $dt = DateTime->from_epoch(epoch => $start_of_interval);
831             my $rdoff = $dt->utc_rd_as_seconds - $start_of_interval;
832              
833             my ($span_start, $span_end, undef, undef, $off, $is_dst, $name) = @{$z->_span_for_datetime(utc => $dt)};
834             $_ -= $rdoff for ($span_start, $span_end);
835              
836             my @val = ([$span_start, $span_end, $off, $is_dst, $name]);
837              
838             while ($span_end < ($k + 1) * $bignum) {
839             $dt = DateTime->from_epoch(epoch => $span_end);
840              
841             ($span_start, $span_end, undef, undef, $off, $is_dst, $name) = @{$z->_span_for_datetime(utc => $dt)};
842             $_ -= $rdoff for ($span_start, $span_end);
843              
844             push @val, [$span_start, $span_end, $off, $is_dst, $name];
845             }
846              
847             return $cache{"$k $tzname"} = \@val;
848             };
849              
850             sub timezone_offset {
851 48     48 1 34186 my ($self, $tzname) = @_;
852              
853 48 50 33     358 if ($tzname eq 'UTC' or $tzname eq 'Z') {
854 0         0 return Time::Duration::Concise::Localize->new(interval => DateTime::TimeZone::UTC->offset_for_datetime);
855             }
856 48         157 my $tm = $self->{epoch};
857 48         152 my $spans = $cache_for->($tm, $tzname);
858              
859 48         126 for my $sp (@$spans) {
860 86 100       203 if ($tm < $sp->[1]) {
861 48         365 return Time::Duration::Concise::Localize->new(interval => $sp->[2]);
862             }
863             }
864              
865 0         0 die "time $tm not found in span";
866             }
867              
868             sub is_dst_in_zone {
869 200048     200048 1 466640 my ($self, $tzname) = @_;
870              
871 200048 50 33     799375 if ($tzname eq 'UTC' or $tzname eq 'Z') {
872 0         0 return DateTime::TimeZone::UTC->is_dst_for_datetime;
873             }
874 200048         369009 my $tm = $self->{epoch};
875 200048         453334 my $spans = $cache_for->($tm, $tzname);
876              
877 200048         550461 for my $sp (@$spans) {
878 303031 100       920367 if ($tm < $sp->[1]) {
879 200048         749488 return $sp->[3];
880             }
881             }
882              
883 0         0 die "time $tm not found in span";
884             }
885             }
886              
887             =head2 plus_time_interval
888              
889             Returns a new Date::Utility plus the supplied Time::Duration::Concise::Localize. Negative TimeIntervals will move backward.
890              
891             Will also attempt to create a TimeInterval from a supplied code, if possible.
892              
893             =cut
894              
895             sub plus_time_interval {
896 416     416 1 369153 my ($self, $ti) = @_;
897              
898 416         1975 return $self->_move_time_interval($ti, 1);
899             }
900              
901             =head2 minus_time_interval
902              
903             Returns a new Date::Utility minus the supplied Time::Duration::Concise::Localize. Negative TimeIntervals will move forward.
904              
905             Will also attempt to create a TimeInterval from a supplied code, if possible.
906              
907             =cut
908              
909             sub minus_time_interval {
910 13     13 1 3618 my ($self, $ti) = @_;
911              
912 13         35 return $self->_move_time_interval($ti, -1);
913             }
914              
915             sub _move_time_interval {
916 429     429   1449 my ($self, $ti, $dir) = @_;
917              
918 429 50       1746 unless (ref($ti)) {
919 429 100       2268 if ($ti =~ s/([\d.]+)y//) {
920 4         29 my $new_date = $self->_plus_years($dir * $1);
921 3 50       99 return $ti ? $new_date->_move_time_interval($ti, $dir) : $new_date;
922             }
923 425 100       1601 if ($ti =~ s/([\d.]+)mo//i) {
924 18         74 my $new_date = $self->_plus_months($dir * $1);
925 17 50       538 return $ti ? $new_date->_move_time_interval($ti, $dir) : $new_date;
926             }
927 407     407   34322 try { $ti = Time::Duration::Concise::Localize->new(interval => $ti) }
928             catch {
929 0   0 0   0 $ti //= 'undef';
930 0         0 confess "Couldn't create a TimeInterval from the code '$ti': $_";
931 407         4237 };
932             }
933 407         28103 my $sec = $ti->seconds;
934 406 100       4869 return ($sec == 0) ? $self : Date::Utility->new($self->{epoch} + $dir * $sec);
935             }
936              
937             =head2 months_ahead
938              
939             Returns the month ahead or backward from the supplied month in the format of Mmm-yy.
940             It could hanlde backward or forward move from the supplied month.
941              
942             =cut
943              
944             sub months_ahead {
945 17     17 1 1077 my ($self, $months_ahead) = @_;
946              
947             # Use 0-11 range to make the math easier.
948 17         546 my $current_month = $self->month - 1;
949 17         397 my $current_year = $self->year;
950              
951             # take the current month number, add the offset, and shift back to 1-12
952 17         44 my $new_month = ($current_month + $months_ahead) % 12 + 1;
953              
954             # we need to know how many years to go forward
955 17         61 my $years_ahead = POSIX::floor(($current_month + $months_ahead) / 12);
956              
957             # use sprintf to add leading zero, and then shift into the range 0-99
958 17         74 my $new_year = sprintf '%02d', (($current_year + $years_ahead) % 100);
959              
960 17         45 return month_number_to_abbrev($new_month) . '-' . $new_year;
961             }
962              
963             =head2 move_to_nth_dow
964              
965             Takes an integer as an ordinal and a day of week representation
966              
967             The following are all equivalent:
968             C<move_to_nth_dow(3, 'Monday')>
969             C<move_to_nth_dow(3, 'Mon')>
970             C<move_to_nth_dow(3, 1)>
971              
972             Returning the 3rd Monday of the month represented by the object or
973             C<undef> if it does not exist.
974              
975             An exception is thrown on improper day of week representations.
976              
977             =cut
978              
979             sub move_to_nth_dow {
980 8009     8009 1 27086 my ($self, $nth, $dow_abb) = @_;
981              
982 8009   50     17552 $dow_abb //= 'undef'; # For nicer error reporting below.
983              
984 8009   66     26008 my $dow = $days_to_num{lc $dow_abb} // croak 'Invalid day of week. We got [' . $dow_abb . ']';
985              
986 8007         201651 my $dow_first = (7 - ($self->day_of_month - 1 - $self->day_of_week)) % 7;
987 8007         19158 my $dom = ($dow + 7 - $dow_first) % 7 + ($nth - 1) * 7 + 1;
988              
989 8007     8007   55812 return try { Date::Utility->new(join '-', $self->year, $self->month, $dom) };
  8007         576424  
990             }
991              
992             =head1 STATIC METHODS
993              
994             =head2 month_number_to_abbrev
995              
996             Static method returns a standard mapping from month numbers to our 3
997             character abbreviated format.
998              
999             =cut
1000              
1001             my %number_abbrev_map = (
1002             1 => 'Jan',
1003             2 => 'Feb',
1004             3 => 'Mar',
1005             4 => 'Apr',
1006             5 => 'May',
1007             6 => 'Jun',
1008             7 => 'Jul',
1009             8 => 'Aug',
1010             9 => 'Sep',
1011             10 => 'Oct',
1012             11 => 'Nov',
1013             12 => 'Dec',
1014             );
1015              
1016             my %abbrev_number_map = reverse %number_abbrev_map;
1017              
1018             sub month_number_to_abbrev {
1019              
1020             # Deal with leading zeroes.
1021 426     426 1 1036 my $which = int shift;
1022              
1023 426         10555 return $number_abbrev_map{$which};
1024             }
1025              
1026             =head2 month_abbrev_to_number
1027              
1028             Static method returns a standard mapping from 3
1029             character abbreviated format to month numbers
1030              
1031             =cut
1032              
1033             sub month_abbrev_to_number {
1034              
1035             # Deal with case issues
1036 53     53 1 191 my $which = ucfirst lc shift;
1037              
1038 53         196 return $abbrev_number_map{$which};
1039             }
1040              
1041             =head1 STATIC METHODS
1042              
1043             =head2 month_number_to_fullname
1044              
1045             Static method returns a standard mapping from month numbers to fullname.
1046              
1047             =cut
1048              
1049             my %number_fullname_map = (
1050             1 => 'January',
1051             2 => 'February',
1052             3 => 'March',
1053             4 => 'April',
1054             5 => 'May',
1055             6 => 'June',
1056             7 => 'July',
1057             8 => 'August',
1058             9 => 'September',
1059             10 => 'October',
1060             11 => 'November',
1061             12 => 'December',
1062             );
1063              
1064             sub month_number_to_fullname {
1065              
1066 9     9 1 119 return $number_fullname_map{int shift};
1067             }
1068              
1069             =head2 is_epoch_timestamp
1070              
1071             Check if a given datetime is an epoch timestemp, i.e. an integer of under 14 digits.
1072              
1073             =cut
1074              
1075             sub is_epoch_timestamp {
1076 6   100 6 1 197 return (shift // '') =~ $EPOCH_RE;
1077             }
1078              
1079             =head2 is_ddmmmyy
1080              
1081             Check if a given "date" is in dd-Mmm-yy format (e.g. 1-Oct-10)
1082              
1083             =cut
1084              
1085             sub is_ddmmmyy {
1086 6     6 1 14 my $date = shift;
1087              
1088 6 100 100     66 return (defined $date and $date =~ /^\d{1,2}\-\w{3}-\d{2}$/) ? 1 : undef;
1089             }
1090              
1091             =head2 truncate_to_day
1092              
1093             Returns a Date::Utility object with the time part truncated out of it.
1094              
1095             For instance, '2011-12-13 23:24:25' will return a new Date::Utility
1096             object representing '2011-12-13 00:00:00'
1097              
1098             =cut
1099              
1100             sub truncate_to_day {
1101 5     5 1 1052 my ($self) = @_;
1102              
1103 5         10 my $epoch = $self->{epoch};
1104 5         10 my $rem = $epoch % 86400;
1105 5 50       13 return $self if $rem == 0;
1106 5         16 return Date::Utility->new($epoch - $rem);
1107             }
1108              
1109             =head2 truncate_to_month
1110              
1111             Returns a Date::Utility object with the day and time part truncated out of it.
1112              
1113             For instance, '2011-12-13 23:24:25' will return a new Date::Utility
1114             object representing '2011-12-01 00:00:00'
1115              
1116             =cut
1117              
1118             sub truncate_to_month {
1119 1     1 1 11 my ($self) = @_;
1120 1         1886 return Date::Utility->new(sprintf("%04d-%02d-01", $self->year, $self->month));
1121             }
1122              
1123             =head2 truncate_to_hour
1124              
1125             Returns a Date::Utility object with the minutes and seconds truncated out of it.
1126              
1127             For instance, '2011-12-13 23:24:25' will return a new Date::Utility
1128             object representing '2011-12-13 23:00:00'
1129              
1130             =cut
1131              
1132             sub truncate_to_hour {
1133 5     5 1 1037 my ($self) = @_;
1134 5         153 return Date::Utility->new(sprintf("%04d-%02d-%02d %02d:00:00", $self->year, $self->month, $self->day_of_month, $self->hour));
1135             }
1136              
1137             =head2 today
1138              
1139             Returns Date::Utility object for the start of the current day. Much faster than
1140             Date::Utility->new, as it will return the same object till the end of the day.
1141              
1142             =cut
1143              
1144             my ($today_obj, $today_ends_at, $today_starts_at);
1145              
1146             sub today {
1147 6     6 1 1358 my $time = time;
1148 6 100 100     59 if (not $today_obj or $time > $today_ends_at or $time < $today_starts_at) {
      100        
1149             # UNIX time assume that day is always 86400 seconds,
1150             # that makes life easier
1151 4         17 $time = 86400 * int($time / 86400);
1152 4         13 $today_obj = Date::Utility->new($time);
1153 4         14 $today_starts_at = $time;
1154 4         10 $today_ends_at = $time + 86399;
1155             }
1156 6         106 return $today_obj;
1157             }
1158              
1159             =head2 plus_years
1160              
1161             Takes the following argument as named parameter:
1162              
1163             =over 4
1164              
1165             =item * C<years> - number of years to be added. (Integer)
1166              
1167             =back
1168              
1169             Returns a new L<Date::Utility> object plus the given years. If the day is greater than days in the new month, it will take the day of end month.
1170             e.g.
1171              
1172             print Date::Utility->new('2000-02-29')->plus_years(1)->date_yyyymmdd;
1173             # will print 2001-02-28
1174              
1175             =cut
1176              
1177             sub plus_years {
1178 8     8 1 27 my ($self, $years) = @_;
1179 8 100 66     65 die "Need an integer years number"
1180             unless looks_like_number($years)
1181             and $years == int($years);
1182 7         212 return $self->_create_trimmed_date($self->year + $years, $self->month, $self->day_of_month);
1183             }
1184              
1185             *_plus_years = \&plus_years;
1186              
1187             =head2 minus_years
1188              
1189             Takes the following argument as named parameter:
1190              
1191             =over 4
1192              
1193             =item * C<years> - number of years to be subracted. (Integer)
1194              
1195             =back
1196              
1197             Returns a new L<Date::Utility> object minus the given years. If the day is greater than days in the new month, it will take the day of end month.
1198             e.g.
1199              
1200             print Date::Utility->new('2000-02-29')->minus_years(1)->date_yyyymmdd;
1201             # will print 1999-02-28
1202              
1203             =cut
1204              
1205             sub minus_years {
1206 2     2 1 13 my ($self, $years) = @_;
1207 2         10 return $self->_plus_years(-$years);
1208             }
1209              
1210             *_minus_years = \&minus_years;
1211              
1212             =head2 plus_months
1213              
1214             Takes the following argument as named parameter:
1215              
1216             =over 4
1217              
1218             =item * C<years> - number of months to be added. (Integer)
1219              
1220             =back
1221              
1222             Returns a new L<Date::Utility> object plus the given months. If the day is greater than days in the new month, it will take the day of end month.
1223             e.g.
1224              
1225             print Date::Utility->new('2000-01-31')->plus_months(1)->date_yyyymmdd;
1226             # will print 2000-02-28
1227              
1228             =cut
1229              
1230             sub plus_months {
1231 22     22 1 85 my ($self, $months) = @_;
1232 22 100 66     135 (looks_like_number($months) && $months == int($months)) || die "Need an integer months number";
1233 21         624 my $new_year = $self->year;
1234 21         490 my $new_month = $self->month + $months;
1235 21 100 100     85 if ($new_month < 1 || $new_month > 12) {
1236 5         21 $new_year += floor($new_month / 12);
1237 5         12 $new_month = $new_month % 12;
1238 5 100       12 if ($new_month < 1) { # when date is 2011-01-01, and $months is -13, then here $new_month will be 0, so hanndle this case here.
1239 1         3 $new_year--;
1240 1         3 $new_month += 12;
1241             }
1242             }
1243 21         512 my $new_day = $self->day_of_month;
1244 21         56 return $self->_create_trimmed_date($new_year, $new_month, $new_day);
1245             }
1246              
1247             *_plus_months = \&plus_months;
1248              
1249             =head2 minus_months
1250              
1251             Takes the following argument as named parameter:
1252              
1253             =over 4
1254              
1255             =item * C<years> - number of months to be subracted. (Integer)
1256              
1257             =back
1258              
1259             Returns a new L<Date::Utility> object minus the given months. If the day is greater than days in the new month, it will take the day of end month.
1260             e.g.
1261              
1262             print Date::Utility->new('2000-03-31')->minus_months(1)->date_yyyymmdd;
1263             # will print 2000-02-28
1264              
1265             =cut
1266              
1267             sub minus_months {
1268 2     2 1 11 my ($self, $months) = @_;
1269 2         8 return $self->_plus_months(-$months);
1270             }
1271              
1272             *_minus_months = \&minus_months;
1273              
1274             =head2 create_trimmed_date
1275              
1276             Takes the following argument as named parameter:
1277              
1278             =over 4
1279              
1280             =item * C<year> - calendar year of the date (Integer)
1281              
1282             =item * C<month> - calendar month of the date. (Integer)
1283              
1284             =item * C<day> - day of the month of the date. (Integer)
1285              
1286             =back
1287              
1288             Returns a valid L<Date::Utility> object whose date part is same with the given year, month and day and time part is not changed. If the day is greater than the max day in that month , then use that max day as the day in the new object.
1289              
1290             =cut
1291              
1292             sub create_trimmed_date {
1293 30     30 1 71 my ($self, $year, $month, $day) = @_;
1294 30         144 my $max_day = __PACKAGE__->new(sprintf("%04d-%02d-01", $year, $month))->days_in_month;
1295 30 100       65 $day = $day < $max_day ? $day : $max_day;
1296 30         695 my $date_string = sprintf("%04d-%02d-%02d %02d:%02d:%02d", $year, $month, $day, $self->hour, $self->minute, $self->second);
1297 30         107 return __PACKAGE__->new($date_string);
1298             }
1299              
1300             *_create_trimmed_date = \&create_trimmed_date;
1301              
1302 8     8   89 no Moose;
  8         22  
  8         130  
1303              
1304             __PACKAGE__->meta->make_immutable(
1305             constructor_name => '_new',
1306             replace_constructor => 1
1307             );
1308             1;
1309             __END__
1310              
1311             =head1 DEPENDENCIES
1312              
1313             =over 4
1314              
1315             =item L<Moose>
1316              
1317             =item L<DateTime>
1318              
1319             =item L<POSIX>
1320              
1321             =item L<Scalar::Util>
1322              
1323             =item L<Tie::Hash::LRU>
1324              
1325             =item L<Time::Local>
1326              
1327             =item L<Try::Tiny>
1328              
1329             =back
1330              
1331              
1332             =head1 AUTHOR
1333              
1334             Binary.com, C<< <support at binary.com> >>
1335              
1336             =head1 BUGS
1337              
1338             Please report any bugs or feature requests to C<bug-date-utility at rt.cpan.org>, or through
1339             the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Date-Utility>. I will be notified, and then you'll
1340             automatically be notified of progress on your bug as I make changes.
1341              
1342             =head1 SUPPORT
1343              
1344             You can find documentation for this module with the perldoc command.
1345              
1346             perldoc Date::Utility
1347              
1348              
1349             You can also look for information at:
1350              
1351             =over 4
1352              
1353             =item * RT: CPAN's request tracker (report bugs here)
1354              
1355             L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=Date-Utility>
1356              
1357             =item * AnnoCPAN: Annotated CPAN documentation
1358              
1359             L<http://annocpan.org/dist/Date-Utility>
1360              
1361             =item * CPAN Ratings
1362              
1363             L<http://cpanratings.perl.org/d/Date-Utility>
1364              
1365             =item * Search CPAN
1366              
1367             L<http://search.cpan.org/dist/Date-Utility/>
1368              
1369             =back
1370              
1371             =head1 LICENSE AND COPYRIGHT
1372              
1373             Copyright 2015 Binary.com.
1374              
1375             This program is free software; you can redistribute it and/or modify it
1376             under the terms of the the Artistic License (2.0). You may obtain a
1377             copy of the full license at:
1378              
1379             L<http://www.perlfoundation.org/artistic_license_2_0>
1380              
1381             Any use, modification, and distribution of the Standard or Modified
1382             Versions is governed by this Artistic License. By using, modifying or
1383             distributing the Package, you accept this license. Do not use, modify,
1384             or distribute the Package, if you do not accept this license.
1385              
1386             If your Modified Version has been derived from a Modified Version made
1387             by someone other than you, you are nevertheless required to ensure that
1388             your Modified Version complies with the requirements of this license.
1389              
1390             This license does not grant you the right to use any trademark, service
1391             mark, tradename, or logo of the Copyright Holder.
1392              
1393             This license includes the non-exclusive, worldwide, free-of-charge
1394             patent license to make, have made, use, offer to sell, sell, import and
1395             otherwise transfer the Package with respect to any patent claims
1396             licensable by the Copyright Holder that are necessarily infringed by the
1397             Package. If you institute patent litigation (including a cross-claim or
1398             counterclaim) against any party alleging that the Package constitutes
1399             direct or contributory patent infringement, then this Artistic License
1400             to you shall terminate on the date that such litigation is filed.
1401              
1402             Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER
1403             AND CONTRIBUTORS "AS IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES.
1404             THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
1405             PURPOSE, OR NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY
1406             YOUR LOCAL LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR
1407             CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR
1408             CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE,
1409             EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
1410              
1411              
1412             =cut