File Coverage

blib/lib/Date/Utility.pm
Criterion Covered Total %
statement 296 307 96.4
branch 87 98 88.7
condition 38 48 79.1
subroutine 83 84 98.8
pod 25 25 100.0
total 529 562 94.1


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