File Coverage

blib/lib/Date/EzDate.pm
Criterion Covered Total %
statement 487 590 82.5
branch 265 360 73.6
condition 67 104 64.4
subroutine 54 64 84.3
pod 9 15 60.0
total 882 1133 77.8


line stmt bran cond sub pod time code
1             package Date::EzDate;
2 1     1   1684 use strict;
  1         1  
  1         28  
3 1     1   4 use Carp;
  1         1  
  1         63  
4 1     1   5 use vars qw($VERSION @ltimefields $overload $default_warning);
  1         3  
  1         104  
5              
6             # version
7             $VERSION = '1.16';
8              
9              
10             # documentation at end of file
11              
12             # debugging tools
13             # use Debug::ShowStuff ':all';
14              
15              
16             # object overloading
17             use overload
18 7     7   158 '""' => sub{$_[0]->{'default'}}, # stringification
19 1         9 '<=>' => \&compare, # comparison
20             '+' => \&addition, # addition
21             '-' => \&subtraction, # subtraction
22 1     1   1086 fallback => 1; # operations not defined here
  1         834  
23              
24              
25             # constants and globals
26 1     1   74 use constant WARN_NONE => 0;
  1         2  
  1         64  
27 1     1   4 use constant WARN_STDERR => 1;
  1         0  
  1         31  
28 1     1   4 use constant WARN_CROAK => 2;
  1         1  
  1         57  
29             $default_warning = WARN_STDERR;
30             $overload = 'epochday';
31              
32             # psuedo-constants
33             @ltimefields = qw[sec min hour dayofmonth monthnum year weekdaynum yearday dst];
34              
35              
36             #------------------------------------------------------------------------------
37             # export
38             #
39 1     1   4 use vars qw[@EXPORT_OK %EXPORT_TAGS @ISA];
  1         1  
  1         1051  
40             @ISA = 'Exporter';
41              
42             @EXPORT_OK = qw[
43             date_range_string
44             time_range_string
45             day_lumps
46             ];
47              
48             %EXPORT_TAGS = ('all' => [@EXPORT_OK]);
49             #
50             # export
51             #------------------------------------------------------------------------------
52              
53              
54             #------------------------------------------------------------------------------
55             # new
56             #
57             sub new {
58 81     81 1 13864 my ($class, $init, %opts) = @_;
59 81         81 my ($rv, %tiehash);
60            
61             # if date is an EzDate object
62 81 50 66     656 if ($init && UNIVERSAL::isa($init, 'Date::EzDate'))
63 0         0 { return $init->clone }
64            
65             # if string doesn't have any content, undef it
66 81 50 66     507 if (
67             defined($init) &&
68             (! ($init =~ m|\S|s) )
69             ) {
70 0         0 undef $init;
71             }
72            
73             # preprocess full time
74 81 50 33     441 $class->can('preprocess_time_string') and
75             defined($init) and
76             $init = $class->preprocess_time_string($init);
77            
78 81 50       432 tie(%tiehash, $class . '::Tie', $init, %opts) or return undef;
79            
80 81         245 $rv = bless(\%tiehash, $class);
81 81 50       299 $rv->can('after_create') and $rv->after_create();
82            
83 81         197 return $rv;
84             }
85             #
86             # new
87             #------------------------------------------------------------------------------
88              
89              
90             #------------------------------------------------------------------------------
91             # clone
92             #
93             sub clone {
94 21     21 1 486 my ($self) = @_;
95 21         76 my $ob = $self->tie_ob;
96 21         26 my ($rv);
97            
98 21         181 $rv = ref($self)->new([
99             $ob->{'sec'},
100             $ob->{'min'},
101             $ob->{'hour'},
102             $ob->{'dayofmonth'},
103             $ob->{'monthnum'},
104             $ob->{'year'},
105             $ob->{'weekdaynum'},
106             $ob->{'yearday'},
107             $ob->{'dst'},
108             $ob->{'epochsec'}
109             ]);
110            
111 21         137 %{$ob->{'formats'}} and
  21         58  
112 21 50       44 $rv->tie_ob->{'formats'} = {%{$ob->{'formats'}}};
113            
114 21         109 return $rv;
115             }
116             #
117             # clone
118             #------------------------------------------------------------------------------
119              
120              
121             #------------------------------------------------------------------------------
122             # format subs
123             #
124 0     0 1 0 sub set_warnings {return $_[0]->tie_ob->{'warnings'} = $_[1]}
125 0     0 1 0 sub zero_hour_ampm {return $_[0]->tie_ob->{'zero_hour_ampm'} = $_[1]}
126              
127 1     1 1 18 sub set_format {return $_[0]->tie_ob->set_format(@_[1..$#_])}
128              
129             sub get_format {
130 0     0 0 0 my ($self, $key) = @_;
131 0         0 my $ob = $self->tie_ob;
132 0         0 $key =~ s|\s||gs;
133 0         0 $key = lc($key);
134 0         0 return join('', @{$ob->{'formats'}->{$key}});
  0         0  
135             }
136              
137 0     0 0 0 sub del_format {return delete $_[0]->{$_[1]}}
138 46     46 0 44 sub tie_ob{return tied(%{$_[0]})}
  46         130  
139             #
140             # format subs
141             #------------------------------------------------------------------------------
142              
143              
144             #------------------------------------------------------------------------------
145             # next_month
146             #
147             sub next_month {
148 3     3 1 913 my ($self, $jump) = @_;
149 3         6 my $ob = $self->tie_ob;
150 3         6 return $ob->next_month($jump);
151             }
152             #
153             # next_month
154             #------------------------------------------------------------------------------
155              
156              
157             #------------------------------------------------------------------------------
158             # compare
159             #
160             sub compare {
161 1     1 0 5 my ($left, $right) = @_;
162 1 50       3 ref($right) or $right = Date::EzDate->new($right);
163 1         4 $left->{$overload} <=> $right->{$overload};
164             }
165             #
166             # compare
167             #------------------------------------------------------------------------------
168              
169              
170             #------------------------------------------------------------------------------
171             # addition and subtraction
172             #
173             sub addition {
174 1     1 0 10 my ($self, $val) = @_;
175 1         4 $self->{$overload} += $val;
176 1         4 return $self;
177             }
178              
179             sub subtraction {
180 1     1 0 341 my ($self, $val) = @_;
181 1         4 $self->{$overload} -= $val;
182 1         4 return $self;
183             }
184             #
185             # addition and subtraction
186             #------------------------------------------------------------------------------
187              
188              
189             #------------------------------------------------------------------------------
190             # date_range_string
191             #
192             sub date_range_string {
193 9     9 1 457 my ($class, $start, $end);
194            
195             # if first argument is a reference, use it as start
196 9 50       37 if (UNIVERSAL::isa($_[0], 'Date::EzDate')) {
197 0         0 ($start, $end) = @_;
198 0         0 $class = ref($start);
199             }
200            
201             # else this sub is being called statically
202             else {
203 9         8 my (@args);
204            
205             # expand array references
206 9         17 foreach my $arg (@_) {
207 14 100       30 if (UNIVERSAL::isa($arg, 'ARRAY'))
208 4         11 { push @args, @$arg }
209             else
210 10         15 { push @args, $arg }
211             }
212            
213             # if called using a class
214 9 50       50 if ( int(@args/2)!=(@args/2) )
215 0         0 { ($class, $start, $end) = @args }
216            
217             # else called w/o an explicit class
218             else {
219 9         10 $class = 'Date::EzDate';
220 9         17 ($start, $end) = @args;
221             }
222             }
223            
224            
225             # both $start and $end must be defined
226 9 50 33     37 unless ( defined($start) && defined($end) )
227 0         0 { croak 'date_range_string requires defined start and end dates' }
228            
229             # set as EzDate objects
230 9 100       24 ref($start) or $start = $class->new($start);
231 9 100       22 ref($end) or $end = $class->new($end);
232            
233             # rearrange as necessary
234 9 50       34 if ($start->{'epoch day'} > $end->{'epoch day'}){
235 0         0 ($start, $end) = ($end, $start);
236             }
237            
238             # if same year
239 9 100       26 if ($start->{'year'} == $end->{'year'}) {
240             # if same month
241 8 100       20 if ($start->{'monthnum'} == $end->{'monthnum'}) {
242             # same day
243 7 100       23 if ($start->{'day of month'} == $end->{'day of month'}) {
244 2         4 return $start->{'{month short} {day of month no zero}, {year}'};
245             }
246            
247             # else different days
248             else {
249 5         8 my $format = '{day of month no zero}';
250            
251             return
252 5         17 $end->{'month short'} . ' ' .
253             $start->{$format} .
254             '-' .
255             $end->{$format} .
256             ', ' .
257             $end->{'year'};
258             }
259             }
260            
261             # else different months
262             else {
263 1         2 my $format = '{month short} {day of month no zero}';
264            
265             return
266 1         3 $start->{$format} .
267             '-' .
268             $end->{$format} .
269             ', ' .
270             $end->{'year'};
271             }
272             }
273            
274             # else different years, so do full range
275             else {
276 1         2 my $format = '{month short} {day of month no zero}, {year}';
277            
278             return
279 1         2 $start->{$format} .
280             '-' .
281             $end->{$format};
282             }
283             }
284             #
285             # date_range_string
286             #------------------------------------------------------------------------------
287              
288              
289             #------------------------------------------------------------------------------
290             # time_range_string
291             #
292             sub time_range_string {
293 2     2 1 190 my ($class, $start, $end, $rv);
294            
295             # if first argument is a reference, use it as start
296 2 50       8 if (ref $_[0]) {
297 0         0 ($start, $end) = @_;
298 0         0 $class = ref($start);
299             }
300            
301             # else this sub is being called statically
302             else {
303             # if called using a class
304 2 50       11 if ( int(@_/2)!=(@_/2) )
305 0         0 { ($class, $start, $end) = @_ }
306            
307             # else called w/o an explicit class
308             else {
309 2         2 $class = 'Date::EzDate';
310 2         4 ($start, $end) = @_;
311             }
312             }
313            
314             # both $start and $end must be defined
315 2 50 33     41 unless ( defined($start) && defined($end) )
316 0         0 { croak 'time_range_string requires defined start and end dates' }
317            
318             # set as EzDate objects
319 2 50       12 ref($start) or $start = $class->new($start);
320 2 50       16 ref($end) or $end = $class->new($end);
321            
322             # rearrange as necessary
323 2 50       9 if ($start->{'epochsec'} > $end->{'epochsec'}){
324 0         0 ($start, $end) = ($end, $start);
325             }
326            
327 2         7 $rv = $start->{'ampm hour no zero'} . ':' . $start->{'min'};
328            
329             # if same ampm
330 2 100       7 if ($start->{'ampm'} ne $end->{'ampm'})
331 1         3 { $rv .= $start->{'ampm'} }
332            
333             $rv .=
334 2         6 '-' .
335             $end->{'ampm hour no zero'} . ':' . $end->{'min'} .
336             $end->{'ampm'};
337            
338 2         37 return $rv;
339             }
340             #
341             # time_range_string
342             #------------------------------------------------------------------------------
343              
344              
345             #------------------------------------------------------------------------------
346             # day_lumps
347             #
348             sub day_lumps {
349 1     1 1 185 my (@org) = @_;
350 1         2 my ($class, @rv, $current);
351 1         2 $class = 'Date::EzDate';
352            
353 1         2 foreach my $date (@org) {
354 8 50       30 ref($date) or $date = $class->new($date);
355            
356 8 100       18 if ($current) {
357 7 100       41 if ( $date->{'epochday'} == ($current->[1]->{'epochday'} + 1) ) {
358 5         18 $current->[1] = $date;
359             }
360            
361             else {
362 2         3 push @rv, $current;
363 2         8 $current = [ $date, $date ];
364             }
365             }
366            
367             else {
368 1         4 $current = [ $date, $date ];
369             }
370             }
371            
372             # add last current
373 1 50       4 $current and push @rv, $current;
374            
375 1         21 return @rv;
376             }
377             #
378             # day_lumps
379             #------------------------------------------------------------------------------
380              
381              
382              
383             ###############################################################################
384             # Date::EzDate::Tie
385             #
386             package Date::EzDate::Tie;
387 1     1   6 use strict;
  1         1  
  1         29  
388 1     1   3 use Carp 'croak', 'carp';
  1         1  
  1         57  
389 1     1   493 use Tie::Hash;
  1         670  
  1         22  
390 1     1   418 use Time::Local;
  1         1304  
  1         77  
391 1     1   8 use re 'taint';
  1         1  
  1         48  
392 1     1   601 use POSIX;
  1         5357  
  1         9  
393              
394             # debugging tools
395             # use Debug::ShowStuff ':all';
396              
397 1         500 use vars qw(
398             @ISA
399             %WeekDayNums
400             %MonthNums
401             @MonthDays
402             @MonthLong
403             @MonthShort
404             @WeekDayLong
405             @WeekDayLong
406             @WeekDayShort
407             @DayOfMonthRd
408             %PCodes
409             $pcode
410             $epoch_offset
411             @OrdWords $OrdWordsRx %OrdWordsNums
412             @OrdNums $OrdNumsRx
413 1     1   3345 );
  1         2  
414              
415             @ISA = 'Tie::StdHash';
416              
417              
418             # globals
419             @WeekDayShort = qw[Sun Mon Tue Wed Thu Fri Sat];
420             @WeekDayLong = qw[Sunday Monday Tuesday Wednesday Thursday Friday Saturday];
421             @MonthShort = qw[Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec];
422             @MonthLong = qw[January February March April May June July August September October November December];
423             @MonthDays = qw[31 x 31 30 31 30 31 31 30 31 30 31];
424             @WeekDayNums{qw[sun mon tue wed thu fri sat]}=(0..6);
425             @MonthNums{qw[jan feb mar apr may jun jul aug sep oct nov dec]}=(0..11);
426              
427              
428             # ordinals
429             @OrdWords = qw[Zeroth First Second Third Fourth Fifth Sixth Seventh Eighth Ninth
430             Tenth Eleventh Twelfth Thirteenth Fourteenth Fifteenth Sixteenth Seventeenth Eighteenth Ninteenth
431             Twentieth Twentyfirst Twentysecond Twentythird Twentyfourth Twentyfifth Twentysixth Twentyseventh
432             Twentyeighth Twentyninth Thirtieth Thirtyfirst];
433             $OrdWordsRx = '\b(' . join('|', @OrdWords[1..$#OrdWords]) . ')\b';
434             foreach my $i (1..$#OrdWords)
435             {$OrdWordsNums{lc($OrdWords[$i])} = $i}
436             @OrdNums = qw[0th 1st 2nd 3rd 4th 5th 6th 7th 8th 9th
437             10th 11th 12th 13th 14th 15th 16th 17th 18th 19th
438             20th 21st 22nd 23rd 24th 25th 26th 27th
439             28th 29th 30th 31st];
440              
441             # percent code regex
442             $pcode = '^\%[\w\%]$';
443              
444             %PCodes = qw[
445             yearlong year
446             yearshort yeartwodigits
447             month monthlong
448             weekday weekdayshort
449             dayofyear yearday
450             dayofyearbase1 yeardaybase1
451             %Y year
452             %y yeartwodigits
453             %a weekdayshort
454             %A weekdaylong
455             %d dayofmonth
456             %D %m/%d/%y
457             %H hour
458             %h monthshort
459             %b ampmhournozero
460             %B hournozero
461             %e monthnumbase1nozero
462             %f dayofmonthnozero
463             %j yeardaybase1
464             %k ampmhour
465             %m monthnumbase1
466             %M min
467             %P ampmuc
468             %p ampmlc
469             %s epochsec
470             %S sec
471             %w weekdaynum
472             %y yeartwodigits
473             %T %H:%M:%S
474             %n newline
475             %t tab
476             %% percent
477             ];
478             $PCodes{'%c'} = '{weekdayshort} %h %d %H:%M:%S %Y';
479             $PCodes{'%r'} = '%k:%M:%S %P';
480              
481              
482             # constants
483 1     1   8 use constant t_60 => 60;
  1         1  
  1         90  
484 1     1   6 use constant t_60_60 => 3600;
  1         2  
  1         82  
485 1     1   7 use constant t_60_60_24 => 86400;
  1         2  
  1         71  
486 1     1   6 use constant WARN_NONE => Date::EzDate::WARN_NONE;
  1         1  
  1         71  
487 1     1   6 use constant WARN_STDERR => Date::EzDate::WARN_STDERR;
  1         2  
  1         72  
488 1     1   6 use constant WARN_CROAK => Date::EzDate::WARN_CROAK;
  1         2  
  1         52  
489              
490 1     1   6 use constant DST_ADJUST_NO => 0;
  1         2  
  1         57  
491 1     1   6 use constant DST_ADJUST_YES => 1;
  1         2  
  1         8082  
492              
493              
494             #------------------------------------------------------------------------------
495             # TIEHASH
496             #
497             sub TIEHASH {
498 81     81   135 my ($class, $time, %opts)=@_;
499 81         188 my $self = bless ({}, $class);
500            
501             # TESTING
502             # println '$class->TIEHASH(', $time, ')';
503             # my $indent = indent();
504            
505             # set some non-date properties
506 81 50       280 $self->{'zero_hour_ampm'} = defined($opts{'zero_hour_ampm'}) ? $opts{'zero_hour_ampm'} : 1;
507 81         135 $self->{'formats'} = {};
508 81         237 $self->{'settings'} = {'dst_kludge' => 1};
509            
510             # default builtin formats
511 81         194 $self->set_format('fullday', '{month short} {day of month no zero}, {year}');
512 81         175 $self->set_format('fulldate', '{fullday}');
513 81         119 $self->set_format('dayandtime', '{month short} {day of month}, {year} {ampmhour no zero}:{minute}{ampm}');
514 81         135 $self->set_format('default', '{dayandtime}');
515            
516            
517             # if clone
518 81 100       151 if (ref $time){
519 21 50       82 if (! UNIVERSAL::isa($time, 'ARRAY') )
520 0         0 { croak '$time is not an array' }
521            
522 21         30 @{$self}{@Date::EzDate::ltimefields, 'epochsec'}=@{$time};
  21         216  
  21         52  
523             }
524            
525             else {
526             # calculate and set properties of current time
527             # set time from timefromfull
528 60         189 $self->setfromtime(DST_ADJUST_NO, time());
529            
530 60 100       120 if (defined $time) {
531 59         187 $time = $self->timefromfull($time);
532            
533 59 50       106 defined($time) or return undef;
534            
535 59         135 $self->setfromtime(DST_ADJUST_NO, $time);
536             }
537             }
538            
539 81         356 return $self;
540             }
541             #
542             # TIEHASH
543             #------------------------------------------------------------------------------
544              
545              
546             #------------------------------------------------------------------------------
547             # setfromtime
548             #
549             sub setfromtime {
550 1794     1794   1873 my ($self, $dst_adjust, $time) = @_;
551 1794         1391 my ($dst_before, @timevals);
552 1794 50       3383 $self->{'settings'}->{'dst_kludge'} or $dst_adjust = 0;
553            
554             # TESTING
555             # println '$self->setfromtime';
556             # my $indent = indent();
557             # println '$dst_adjust: ', $dst_adjust;
558             # println '$time: ', $time;
559            
560             # if we should adjust the hour if necessary
561 1794 100       2578 if ($dst_adjust)
562 1628         1785 { $dst_before = $self->{'dst'} }
563            
564             # set current epoch second
565 1794         1674 $self->{'epochsec'} = $time;
566            
567             # get current time values
568 1794         29831 @timevals = localtime($time);
569            
570 1794 50       3879 if (! @timevals)
571 0         0 { croak "cannot: get localtime values from this epoch value: $time" }
572            
573 1794         2354 @{$self}{@Date::EzDate::ltimefields} = @timevals;
  1794         6405  
574            
575 1794 50 66     10089 if (
      66        
576             $dst_adjust &&
577             defined($dst_before) &&
578             ($dst_before != $self->{'dst'})
579             ) {
580            
581 0 0       0 if ($dst_before)
582 0         0 { $self->{'epochsec'} += t_60_60 }
583             else
584 0         0 { $self->{'epochsec'} -= t_60_60 }
585            
586 0         0 @{$self}{@Date::EzDate::ltimefields}=localtime($self->{'epochsec'});
  0         0  
587             }
588            
589 1794         6920 $self->{'year'} += 1900;
590             }
591             #
592             # setfromtime
593             #------------------------------------------------------------------------------
594              
595              
596             #------------------------------------------------------------------------------
597             # set_format
598             #
599             sub set_format {
600 325     325   386 my ($self, $name, $format) = @_;
601            
602             # normalize name
603 325         443 $name =~ s|\s||g;
604 325         302 $name =~ tr/A-Z/a-z/;
605            
606 325         415 $self->{'formats'}->{$name} = format_split($format);
607             }
608              
609             sub format_split {
610 425     425   3403 my @rv = split(m/(\{[^\{\}]*\}|\%.)/, $_[0]);
611            
612 425         679 foreach my $el (@rv) {
613 2553 100       4990 if ($el =~ m|^\{.*\}$|s)
  956         1060  
614             {normalize_key($el)}
615             }
616            
617 425         1167 return \@rv;
618             }
619             #
620             # set_format
621             #------------------------------------------------------------------------------
622              
623              
624             #------------------------------------------------------------------------------
625             # warn
626             #
627             sub warn {
628 0     0   0 my $self = shift;
629 0 0       0 my $level = defined($self->{'warnings'}) ? $self->{'warnings'} : $Date::EzDate::default_warning;
630            
631 0 0       0 $level or return undef;
632            
633 0 0       0 if ($level == WARN_STDERR) {
634 0         0 carp 'WARNING: ', @_;
635 0         0 return undef;
636             }
637            
638 0         0 croak @_;
639             }
640             #
641             # warn
642             #------------------------------------------------------------------------------
643              
644              
645             #------------------------------------------------------------------------------
646             # normalize_key
647             #
648             sub normalize_key {
649 11780     11780   14573 $_[0] =~ s|\s||gs;
650 11780 100       22757 $_[0] =~ tr/A-Z/a-z/ unless $_[0] =~ m|^\%\w$|;
651 11780         11454 $_[0] =~ s|ordinal|ord|sg;
652            
653 11780         9127 $_[0] =~ s|hours|hour|sg;
654             # $_[0] =~ s|days\b|day|sg;
655            
656 11780         8977 $_[0] =~ s|minute|min|sg;
657 11780         10362 $_[0] =~ s|mins|min|sg;
658            
659 11780         9818 $_[0] =~ s|second|sec|sg;
660 11780         9853 $_[0] =~ s|secs|sec|sg;
661            
662 11780         10988 $_[0] =~ s|number|num|sg;
663             }
664             #
665             # normalize_key
666             #------------------------------------------------------------------------------
667              
668              
669             #------------------------------------------------------------------------------
670             # next_month
671             #
672             sub next_month {
673 3     3   4 my ($self, $jumps) = @_;
674 3         7 return $self->STORE('monthnum', $self->{'monthnum'} + $jumps);
675             }
676             #
677             # next_month
678             #------------------------------------------------------------------------------
679              
680              
681             #------------------------------------------------------------------------------
682             # STORE
683             #
684             sub STORE {
685 1715     1715   3008 my ($self, $key, $val) = @_;
686 1715         1520 my $orgkey = $key;
687 1715         1330 my $orgval = $val;
688            
689             # error checking
690 1715 50       2707 if (! defined $val)
  0         0  
691             {return $self->warn('Must send a defined value when setting a property of an EzDate object')}
692            
693             # if value contains {, assume they're assigning a format
694 1715 50       3622 $val =~ m|[\{\%]| and return $self->set_format($key, $val);
695            
696             # clean a little
697 1715         2129 normalize_key($key);
698            
699             # get key from aliases if necessary
700 1715         2433 $key = $self->get_alias($key, 'strip_no_zero'=>1);
701            
702            
703             # dayofmonth, weekdaynum, yearday
704 1715 100 100     22416 if ($key =~ m/^(dayofmonth|weekdaynum|dayofweeknum|yearday)$/s) {
  3 100 100     17  
    100 100        
    100 100        
    100 100        
    100 66        
    100 66        
    100 33        
    50 33        
    50 33        
    50          
    50          
    100          
    50          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    50          
705             # warn if setting day of month greater than month has days
706 36 50 66     180 if (
707             ($key eq 'dayofmonth') &&
708             ($val > daysinmonth($self->{'monthnum'}, $self->{'year'}))
709             ) {
710 0         0 $self->warn(
711             "setting day of month ($val) to higher than days in month (",
712             daysinmonth($self->{'monthnum'}, $self->{'year'}),
713             ')',
714             );
715             }
716            
717             $self->setfromtime(
718 36         173 DST_ADJUST_YES,
719             $self->{'epochsec'} - ($self->{$key} * t_60_60_24) + ($val * t_60_60_24)
720             );
721             }
722            
723             elsif ($key eq 'sec')
724             {$self->setfromtime(DST_ADJUST_NO, $self->{'epochsec'} - $self->{'sec'} + $val)}
725            
726             elsif ($key eq 'min') {
727 3         19 $self->setfromtime(
728             DST_ADJUST_NO,
729             $self->{'epochsec'} - ($self->{'min'} * 60) + ($val * 60)
730             )
731             }
732            
733             elsif ($key eq 'minofday') {
734 1         105 $self->setfromtime (
735             DST_ADJUST_NO,
736             $self->{'epochsec'} - ($self->{'hour'} * t_60_60) - ($self->{'min'} * 60) + ($val * 60)
737             )
738             }
739            
740             elsif ($key eq 'hour') {
741 6         35 $val = timelocal(
742             $self->{'sec'},
743             $self->{'min'},
744             $val,
745             $self->{'dayofmonth'},
746             $self->{'monthnum'},
747             $self->{'year'},
748             );
749            
750 6         285 $self->setfromtime(DST_ADJUST_NO, $val);
751             }
752            
753             # hour and minute
754             elsif ( ($key eq 'clocktime') || ($key =~ m|^mil(itary)?time$|) ) {
755 3         10 my ($changed, $hour, $min, $sec) = $self->gettime($val);
756            
757 3 50       11 unless (defined $hour)
  0         0  
758             {$hour = $self->{'hour'}}
759 3 50       11 unless (defined $min)
  0         0  
760             {$min = $self->{'min'}}
761 3 100       11 unless (defined $sec)
  2         5  
762             {$sec = $self->{'sec'}}
763            
764             $self->setfromtime
765             (
766 3         27 0,
767             $self->{'epochsec'}
768            
769             - ($self->{'sec'})
770             - ($self->{'min'} * 60)
771             - ($self->{'hour'} * t_60_60)
772            
773             + ($sec)
774             + ($min * 60)
775             + ($hour * t_60_60)
776             );
777             }
778            
779             elsif ($key eq 'ampmhour') {
780 3 100       12 if ($self->{'hour'} >= 12)
  2         7  
781             {$val += 12}
782 3         9 $self->STORE('hour', $val);
783             }
784            
785             elsif (
786             ($key eq 'ampm') ||
787             ($key eq 'ampmlc') ||
788             ($key eq 'ampmuc')
789 0         0 ) {
790 25         24 my ($multiplier);
791            
792 25 100       59 if (length($val) == 1)
  1         3  
793             {$val .= 'm'}
794 25         35 $val = lc($val);
795            
796             # error checking
797 25 50 66     127 unless ( ($val eq 'am') || ($val eq 'pm') )
  0         0  
798             {return $self->warn('ampm may only be set to am or pm')}
799            
800             # if no change, we're done
801 25 100       60 if ($self->{'hour'} < 12) {
802 21 100       56 if ($val eq 'am') {return}
  1         10  
803             }
804             else {
805 4 50       14 if ($val eq 'pm') {return}
  0         0  
806             }
807            
808 24 100       52 if ($val eq 'am')
  4         6  
809 20         24 {$multiplier = -1}
810             else
811             {$multiplier = 1}
812            
813 24         70 $self->setfromtime(DST_ADJUST_YES, $self->{'epochsec'} + (12 * t_60_60 * $multiplier) );
814             }
815            
816             elsif ($key eq 'dst')
817 0         0 {return $self->warn('dst property is read-only')}
818            
819             elsif ($key eq 'epochsec')
820 0         0 {$self->setfromtime(DST_ADJUST_NO, $val)}
821            
822             elsif ($key eq 'epochmin')
823 0         0 {$self->setfromtime(DST_ADJUST_NO, $self->{'epochsec'} - ($self->getepochmin * 60) + ($val * 60) )}
824            
825             elsif ($key eq 'epochhour')
826             {$self->setfromtime(DST_ADJUST_NO, $self->{'epochsec'} - ($self->getepochhour * t_60_60) + ($val * t_60_60) )}
827            
828             elsif ($key eq 'epochday') {
829 1543         1288 my ($oldhour, $oldepochsec, $oldmin);
830            
831 1543         2449 $self->setfromtime(
832             DST_ADJUST_YES,
833             $self->{'epochsec'} - ($self->getepochday * t_60_60_24) + (int($val) * t_60_60_24)
834             );
835             }
836            
837             # ordinals
838             elsif ($key =~ m/dayofmonthord(word|num)?/) {
839             # if numeric
840 0 0       0 if ($val =~ s|^(\d+)\s*\w*$|$1|s)
  0         0  
841             {$self->STORE('dayofmonth', $val)}
842            
843             # else word
844             else {
845 0         0 my $nval = $val;
846 0         0 $nval =~ tr/A-Z/a-z/;
847 0         0 $nval =~ s|\W||gs;
848            
849             # if no such ordinal exists
850 0 0       0 unless ($nval = $OrdWordsNums{$nval})
  0         0  
851             {return $self->warn("Invalid ordinal: $val")}
852            
853 0         0 $self->STORE('dayofmonth', $nval);
854             }
855             }
856            
857             elsif ($key eq 'year') {
858 24         30 my ($maxday, $targetday);
859            
860             # if same year, nothing to do
861 24 50       73 if ($self->{'year'} == $val)
  0         0  
862             {return}
863            
864             # make sure day of month isn't greater than maximum day of target month
865 24         56 $maxday = daysinmonth($self->{'monthnum'}, $val);
866            
867 24 50       80 if ($self->{'dayofmonth'} > $maxday) {
  24         44  
868 0         0 $self->warn(
869             "Changing the year sets day of month ($self->{'dayofmonth'}) to higher than days in month ($maxday); ",
870             "setting the day down to $maxday"
871             );
872 0         0 $targetday = $maxday;
873             }
874             else
875             {$targetday = $self->{'dayofmonth'}}
876            
877 24         97 $val = timelocal($self->{'sec'}, $self->{'min'}, $self->{'hour'}, $targetday, $self->{'monthnum'}, $val);
878 24         1294 $self->setfromtime(DST_ADJUST_YES, $val);
879             }
880            
881 3         14 elsif ($key =~ m/^year(two|2)digit/) {
882 2         13 $val =~ s|^.*(..)$|$1|;
883 2         12 $self->STORE('year', substr($self->{'year'}, 0, 2) . zeropad_2($val));
884             }
885            
886             elsif ($key =~ m/^monthnumbase(one|1)/)
887             {$self->STORE('monthnum', $val - 1)}
888            
889            
890             #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
891             # monthnum
892             #
893 23         109 elsif ($key eq 'monthnum') {
894 31         48 my ($target, $epoch_second);
895 31         49 my $month = $self->{'monthnum'};
896 31         47 my $year = $self->{'year'};
897 31         42 my $orgday = $self->{'dayofmonth'};
898 31         31 my $dayofmonth = $self->{'dayofmonth'};
899 31         38 my $jumps = $val - $month;
900            
901             # if nothing to do
902 31 50       63 $jumps or return;
903            
904 31         19 $target = $jumps;
905 31         45 $target = abs($target);
906            
907             # jumping forward
908 31 100       54 if ($jumps > 0) {
909 23         50 foreach (1..$target) {
910             # if end of year
911 35 100       63 if ($month == 11) {
  33         57  
912 2         3 $month = 0;
913 2         4 $year++;
914             }
915             else
916             {$month++}
917             }
918             }
919            
920             # jumping backward
921             else {
922 8         25 foreach (1..$target) {
923             # if beginning of year
924 69 100       79 if ($month == 0) {
  67         65  
925 2         4 $month = 11;
926 2         2 $year--;
927             }
928             else
929             {$month--}
930             }
931             }
932            
933            
934             # adjust day for shorter month (if necessary)
935 31 100       84 if ($dayofmonth > 28) {
936 9         20 my $dim = daysinmonth($month, $year);
937            
938 9 100       28 if ($dim < $dayofmonth)
939 2         3 { $dayofmonth = $dim }
940             }
941            
942             # get epoch second from timelocal
943 31         172 $epoch_second = timelocal($self->{'sec'}, $self->{'min'}, $self->{'hour'}, $dayofmonth, $month, $year);
944 31         1860 $self->setfromtime(DST_ADJUST_NO, $epoch_second);
945             }
946             #
947             # monthnum
948             #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
949            
950            
951             elsif ( ($key eq 'monthshort') || ($key eq 'monthlong') )
952             {$self->STORE('monthnum', $MonthNums{lc(substr($val, 0, 3))})}
953            
954             elsif (
955             ($key eq 'weekdayshort') ||
956             ($key eq 'weekdaylong') ||
957             ($key eq 'dayofweekshort') ||
958             ($key eq 'dayofweeklong')
959 3         12 ) {
960 5         27 $self->STORE(
961             'weekdaynum',
962             $WeekDayNums{lc(substr($val, 0, 3))})
963             }
964            
965             # year day
966             elsif ($key =~ m/^yeardaybase(one|1)$/)
967             {$self->STORE('yearday', $val - 1)}
968            
969             # default, full, dmy
970 0         0 elsif ( ($key eq 'default') || ($key eq 'full') || ($key eq 'dayandtime') || ($key eq 'dmy') ){
971 1         2 my (%opts);
972            
973 1 50       3 if ( $key eq 'dmy')
  1         3  
974             {$opts{'dateonly'} = 1}
975            
976 1         4 $self->setfromtime(DST_ADJUST_YES, $self->timefromfull($val, %opts));
977             }
978            
979             else
980             {return $self->warn("Do not understand key: $orgkey")}
981             }
982             #
983             # STORE
984             #------------------------------------------------------------------------------
985              
986              
987             #------------------------------------------------------------------------------
988             # FETCH
989             #
990             sub FETCH {
991 9651     9651   391700 my ($self, $key, %opts) = @_;
992 9651         8689 my $orgkey = $key;
993            
994             # get key from aliases if necessary
995 9651         12257 $key = $self->get_alias($key);
996            
997            
998             #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
999             # nested properties
1000             #
1001 9651 100 100     39777 if ( (! ref $key) && ($key =~ m|[\{\%]|) && ($key !~ m|$pcode|o) )
  100   66     219  
1002             {$key = format_split($key)}
1003            
1004 9651 100       12829 if (ref $key) {
1005 115         406 my @rv = @$key;
1006            
1007 115         149 foreach my $el (@rv) {
1008             # if this is one of the format elements
1009             # then fetch the value of the given key
1010 869 100 100     4235 if (
1011             ($el =~ s|\{([^\}]+)\}|$1|) || # if it is enclosed in {}
1012             ($el =~ m|$pcode|o) # if it is a %x code
1013             ) {
1014 434         577 $el =~ s|['"\s]||g;
1015 434         776 $el = $self->FETCH($el, normalized=>1);
1016             }
1017             }
1018            
1019 115         741 return join('', @rv);
1020             }
1021             #
1022             # nested properties
1023             #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1024            
1025            
1026             # clean up key
1027 9536 100       19174 $opts{'normalized'} or normalize_key($key);
1028            
1029            
1030             # already or mostly calculated
1031 9536 100       15766 if (exists $self->{$key}) {
1032 5333 100       13827 if ($key =~ m/^(dayofmonth|monthnum|hour|min|sec)$/)
  3650         5820  
1033             {return zeropad_2($self->{$key})}
1034            
1035 1683         4717 return $self->{$key};
1036             }
1037            
1038             # nozero's
1039 4203 100       7261 if ($key =~ s/no(zero|0)//)
  190         421  
1040             {return $self->FETCH($key) + 0}
1041            
1042             # day of month ord
1043 4013 50       5821 if ($key =~ m|^dayofmonthord(word)?$|)
  0         0  
1044             {return $OrdWords[$self->{'dayofmonth'}]}
1045 4013 50       5771 if ($key eq 'dayofmonthordnum')
  0         0  
1046             {return $OrdNums[$self->{'dayofmonth'}]}
1047            
1048             # weekday
1049 4013 100       5805 if ($key =~ m/^(weekdayshort|dayofweekshort|dayofweek)$/)
  60         312  
1050             {return $WeekDayShort[$self->{'weekdaynum'}]}
1051 3953 100       5268 if ($key =~ m/^(weekdaylong|dayofweeklong)$/)
  41         242  
1052             {return $WeekDayLong[$self->{'weekdaynum'}]}
1053 3912 50       5467 if ($key eq 'dayofweeknum')
  0         0  
1054             {return $self->{'weekdaynum'}}
1055            
1056             # month
1057 3912 100       5338 if ($key eq 'monthshort') {
1058 82 50       153 defined($self->{'monthnum'}) or die 'undefined monthnum';
1059 82         344 return $MonthShort[$self->{'monthnum'}];
1060             }
1061            
1062 3830 100       5380 if ($key eq 'monthlong')
1063 21         120 { return $MonthLong[$self->{'monthnum'}] }
1064 3809 100       5817 if ($key =~ m/^monthnumbase(one|1)/)
1065 80         263 { return zeropad_2($self->{'monthnum'} + 1) }
1066            
1067 3729 100       5102 if ($key =~ m/^yeardaybase(one|1)/)
1068 61         219 { return zeropad_open($self->{'yearday'} + 1, 3) }
1069            
1070             # year
1071 3668 100       4974 if ($key =~ m/^yeartwodigit/)
1072 60         343 { return substr($self->{'year'}, 2) }
1073            
1074             # epochs
1075 3608 50       5105 if ($key eq 'epochmin')
1076 0         0 { return $self->getepochmin }
1077 3608 50       5133 if ($key eq 'epochhour')
1078 0         0 { return $self->getepochhour }
1079 3608 100       4899 if ($key eq 'epochday')
1080 1589         2454 { return $self->getepochday }
1081            
1082             # leapyear
1083 2019 100       2882 if ($key =~ m/^(is)?leapyear/)
  40         133  
1084             {return isleapyear($self->{'year'})}
1085            
1086             # days in month
1087 1979 100       2742 if ($key eq 'daysinmonth')
  40         137  
1088             {return daysinmonth($self->{'monthnum'}, $self->{'year'}) }
1089            
1090             # DMY: eg 15JAN2001
1091 1939 100       2701 if ($key eq 'dmy')
  1         3  
1092             {return zeropad_2($self->{'dayofmonth'}) . uc($MonthShort[$self->{'monthnum'}]) . $self->{'year'} }
1093            
1094             # full
1095 1938 100       3015 if ($key eq 'full') {
1096             return
1097 1524         5828 $WeekDayShort[$self->{'weekdaynum'}] . ' ' .
1098             $MonthShort[$self->{'monthnum'}] . ' ' .
1099             $self->{'dayofmonth'} . ', ' .
1100             $self->{'year'} . ' ' .
1101             zeropad_2($self->{'hour'}) . ':' .
1102             zeropad_2($self->{'min'}) . ':' .
1103             zeropad_2($self->{'sec'});
1104             }
1105            
1106             # dayparam: A convenient string for passing as a parameter. Consists
1107             # of just alphanumerics, is easily human readable, and is completely unambiguous.
1108 414 50       777 if ($key eq 'dayparam') {
1109             return
1110 0         0 zeropad_2($self->{'dayofmonth'}) .
1111             lc($MonthShort[$self->{'monthnum'}]) .
1112             $self->{'year'};
1113             }
1114            
1115             # military time, aka "miltime"
1116 414 100       869 if ($key =~ m|^mil(itary)?time$|)
  43         157  
1117             {return zeropad_2($self->{'hour'}) . zeropad_2($self->{'min'}) }
1118            
1119             # iso8601
1120 371 50       591 if ($key eq 'iso8601') {
1121             return
1122 0         0 $self->{'year'} . '-' .
1123             zeropad_2($self->{'monthnum'}+1) . '-' .
1124             zeropad_2($self->{'dayofmonth'}) . ' ' .
1125            
1126             zeropad_2($self->{'hour'}) . ':' .
1127             zeropad_2($self->{'min'}) . ':' .
1128             zeropad_2($self->{'sec'});
1129             }
1130            
1131             # minuteofday, aka minofday
1132 371 100       636 if ($key eq 'minofday')
  42         233  
1133             {return $self->{'min'} + ($self->{'hour'} * 60) }
1134            
1135             # variable
1136 329         332 my ($ampm);
1137            
1138             # calculate ampm, which is needed in most results from here down
1139 329 100       753 $ampm = ($self->{'hour'} >= 12) ? 'pm' : 'am';
1140            
1141             # am/pm
1142 329 100 100     1262 if ( ($key eq 'ampm') || ($key eq 'ampmlc') )
  74         302  
1143             {return $ampm}
1144            
1145             # AM/PM uppercase
1146 255 100       437 if ($key eq 'ampmuc')
  60         319  
1147             {return uc($ampm)}
1148            
1149             # variable
1150 195         224 my ($ampmhour);
1151            
1152             # calculate ampmhour, which is needed from here down
1153 195 100 100     953 if ( ($self->{'hour'} == 0) || ($self->{'hour'} == 12) )
  6 100       6  
1154 1         1 {$ampmhour = 12}
1155             elsif ($self->{'hour'} > 12)
1156 188         284 {$ampmhour = $self->{'hour'} - 12}
1157             else
1158             {$ampmhour = $self->{'hour'}}
1159            
1160             # am/pm hour
1161 195 100       429 if ($key eq 'ampmhour')
  113         278  
1162             {return zeropad_2($ampmhour)}
1163            
1164             # hour and minute with ampm
1165 82 100 66     372 if (
1166             ($key eq 'clocktime') ||
1167             ($key eq 'clocktimestrict')
1168             ) {
1169 22         68 my $minofday = $self->FETCH('minofday');
1170            
1171 22 50       68 if ($key eq 'clocktime') {
1172 22 50       88 if ($minofday == 0)
  0         0  
1173             {return 'midnight'}
1174 22 50       48 if ($minofday == 12 * t_60)
  0         0  
1175             {return 'noon'}
1176             }
1177            
1178             return
1179 22         53 $self->FETCH('ampmhournozero') .
1180             ':' . zeropad_2($self->{'min'}) . ' ' .
1181             $ampm;
1182             }
1183            
1184             # character codes
1185 60 100       238 $key eq 'newline' && return "\n";
1186 40 100       165 $key eq 'tab' && return "\t";
1187 20 50       65 $key eq 'leftbrace' && return '{';
1188 20 50       53 $key eq 'rightbrace' && return '}';
1189 20 50       122 $key eq 'percent' && return '%';
1190            
1191             # else we don't know what property is needed
1192 0         0 return $self->warn("do not know this format: $orgkey");
1193             }
1194             #
1195             # FETCH
1196             #------------------------------------------------------------------------------
1197              
1198              
1199             #------------------------------------------------------------------------------
1200             # DELETE
1201             #
1202             sub DELETE {
1203 0     0   0 my ($self, $key) = @_;
1204            
1205 0         0 $key =~ s|\s||gs;
1206 0         0 $key = lc($key);
1207            
1208 0         0 return delete $self->{'formats'}->{$key};
1209             }
1210             #
1211             # DELETE
1212             #------------------------------------------------------------------------------
1213              
1214              
1215 0     0   0 sub del_format {return delete $_[0]->tie_ob->{'formats'}->{$_[1]}}
1216              
1217              
1218             #------------------------------------------------------------------------------
1219             # isleapyear
1220             #
1221             sub isleapyear {
1222 82     82   133 my ($year) = @_;
1223            
1224 82 50 33     672 return 1 if ( ($year % 4 == 0) && ( ($year % 100) || ($year % 400 == 0) ) );
      66        
1225 21         105 return 0;
1226             }
1227             #
1228             # isleapyear
1229             #------------------------------------------------------------------------------
1230              
1231              
1232             #------------------------------------------------------------------------------
1233             # get_alias
1234             #
1235             sub get_alias {
1236 12309     12309   14463 my ($self, $key, %opts) = @_;
1237            
1238             # normalize
1239 12309 100       26260 unless ($key =~ m|[\{\%]|) {
1240 11349         13463 $key =~ s|\s||g;
1241 11349         11033 $key = lc($key);
1242            
1243             # strip "nozero" if that option was sent
1244 11349 100       20157 $opts{'strip_no_zero'} and $key =~ s|nozero||g;
1245             }
1246            
1247             # if this has an alias
1248 12309 100       19488 if (exists $PCodes{$key})
  943         2426  
1249             {return $self->get_alias($PCodes{$key}, %opts)}
1250            
1251             # if this is a named format
1252 11366 100       20130 if (exists $self->{'formats'}->{$key})
  15         32  
1253             {return $self->{'formats'}->{$key}}
1254            
1255 11351         19137 return $key;
1256             }
1257             #
1258             # get_alias
1259             #------------------------------------------------------------------------------
1260              
1261              
1262              
1263             #------------------------------------------------------------------------------
1264             # getepochday
1265             #
1266             sub getepochday {
1267 3132     3132   3019 my ($self, $epochsec) = @_;
1268            
1269             # TESTING
1270             # println '$date->getepochday';
1271             # my $indent = indent();
1272            
1273             # get epochsecond of date object
1274 3132 50       5873 defined($epochsec) or $epochsec = $self->{'epochsec'};
1275            
1276             # calculate $epoch_offset
1277             # $epoch_offset represents the number of seconds
1278             # into the epoch day that the actual epoch moment occurs
1279 3132 100       4291 unless (defined $epoch_offset) {
1280 1         2 my %date;
1281 1         18 @date{@Date::EzDate::ltimefields} = localtime(0);
1282 1         5 $epoch_offset =
1283             ( ($date{'hour'} * t_60_60) + ($date{'min'} * 60) + $date{'sec'});
1284             }
1285            
1286             # TESTING
1287             # println '$epochsec: ', $epochsec;
1288             # println '$epoch_offset: ', $epoch_offset;
1289            
1290             # return
1291 3132         13442 return floor( ($epochsec + $epoch_offset) / t_60_60_24);
1292             }
1293             #
1294             # getepochday
1295             #------------------------------------------------------------------------------
1296              
1297              
1298             #------------------------------------------------------------------------------
1299             # getepochhour, getepochmin
1300             #
1301             sub getepochhour {
1302 0     0   0 my ($self) = @_;
1303 0         0 return int($self->{'epochsec'} / t_60_60);
1304             }
1305              
1306             sub getepochmin {
1307 0     0   0 my ($self) = @_;
1308 0         0 return int($self->{'epochsec'} / 60);
1309             }
1310             #
1311             # getepochhour, getepochmin
1312             #------------------------------------------------------------------------------
1313              
1314              
1315              
1316             #------------------------------------------------------------------------------
1317             # daysinmonth
1318             #
1319             sub daysinmonth {
1320 101     101   161 my ($monthnum, $year) = @_;
1321            
1322 101 100       211 if ($monthnum != 1)
  59         294  
1323             {return $MonthDays[$monthnum]}
1324 42 100       94 if (isleapyear($year))
  41         130  
1325             {return 29}
1326 1         2 return 28;
1327             }
1328             #
1329             # daysinmonth
1330             #------------------------------------------------------------------------------
1331              
1332              
1333             #------------------------------------------------------------------------------
1334             # timefromfull
1335             #
1336             sub timefromfull {
1337 60     60   145 my ($self, $val, %opts) = @_;
1338 60         55 my ($hour, $min, $sec, $day, $month, $year, $rv);
1339 60         75 my $orgval = $val;
1340            
1341             # error checking
1342 60 50       131 if (! defined $val)
  0         0  
1343             {return $self->warn("did not get a time string")}
1344            
1345             # quick return: if they just put in an integer
1346 60 50       278 if ($val =~ m/^\d+$/)
  0         0  
1347             {return $val}
1348            
1349            
1350             # alias hour am/pm to hour:00 am/pm
1351             # $self->{'zero_hour_ampm'} and $val =~ s/(^|[^:\d])(\d+)\s*([ap]m)/$1$2:00:00 $3/gis;
1352 60 50       550 $self->{'zero_hour_ampm'} and $val =~ s/(^|[^:\d])(\d+)\s*([ap]m?\b)/$1$2:00:00 $3/gis;
1353            
1354             # special case: ##:##.#####
1355             # In some time formats, the hour, min, second is
1356             # followed by fractional seconds. We don't handle those
1357             # fractions, so we'll just remove them.
1358 60         171 $val =~ s/(\d+\:\d+)\.[\d\-]+/$1/g;
1359            
1360            
1361             # Another special case: A.M. to AM and P.M. to PM
1362 60         99 $val =~ s/a\.m\b/am/gis;
1363 60         81 $val =~ s/p\.m\b/pm/gis;
1364            
1365             # normalize
1366 60         106 $val =~ tr/A-Z/a-z/;
1367 60         295 $val =~ s/[^\w:]/ /g;
1368 60         237 $val =~ s/\s*:\s*/:/g;
1369            
1370             # change ordinals to numbers
1371 60         454 $val =~ s|$OrdWordsRx|$OrdWordsNums{$1}|gis;
1372 60         180 $val =~ s/(\d)(th|rd|st|nd)\b/$1/gis;
1373            
1374            
1375             # noon to 12:00:00
1376             # midnight to 00:00:00
1377 60         137 $val =~ s/\bnoon\b/ 12:00:00 /gis;
1378 60         104 $val =~ s/\bmidnight\b/ 00:00:00 /gis;
1379            
1380             # normalize some more
1381 60         166 $val =~ s/(\d)([a-z])/$1 $2/g;
1382 60         119 $val =~ s/([a-z])(\d)/$1 $2/g;
1383 60         247 $val =~ s/\s+/ /g;
1384 60         136 $val =~ s/^\s*//;
1385 60         268 $val =~ s/\s*$//;
1386            
1387            
1388             # today, tomorrow, and yesterday
1389 60 50 33     290 if ( ($val eq 'today') || ($val eq 'now') )
  0         0  
1390             {return time()}
1391 60 50       110 if ($val eq 'tomorrow')
  0         0  
1392             {return time() + t_60_60_24}
1393 60 50       114 if ($val eq 'yesterday')
  0         0  
1394             {return time() - t_60_60_24}
1395            
1396             # normalize further
1397 60         186 $val =~ s/([a-z]{3})[a-z]+/$1/gs;
1398            
1399             # remove weekday
1400 60         379 $val =~ s/((sun)|(mon)|(tue)|(wed)|(thu)|(fri)|(sat))\s*//;
1401 60         260 $val =~ s/\s*$//;
1402            
1403            
1404             # attempt to get time
1405 60 100       155 unless ($opts{'dateonly'}) {
1406 59         174 ($val, $hour, $min, $sec) = $self->gettime($val, 'skipjustdigits'=>1);
1407             }
1408            
1409             # attempt to get date
1410 60 50       141 unless ($opts{'timeonly'}) {
1411 60 100       130 if (length $val)
  56         128  
1412             {($val, $day, $month, $year) = getdate($val)}
1413             }
1414            
1415             # trim
1416 60         175 $val =~ s/^\s*//;
1417            
1418             # attempt to get time again
1419 60 100       128 unless ($opts{'dateonly'}) {
1420 59 100 66     225 if (length($val) && (! defined($hour)) ) {
1421 34         92 ($val, $hour, $min, $sec) = $self->gettime($val, 'skipjustdigits'=>1, 'croakonfail'=>1);
1422             }
1423             }
1424            
1425            
1426 60 50 33     263 if (defined($val) && length($val))
  0         0  
1427             {return $self->warn("Did not recognize date/time pattern ($val): $orgval")}
1428            
1429             # if we didn't get a day, hour, year, or month we didn't recognize the pattern
1430 60 0 66     200 unless (
      33        
      33        
1431 0         0 defined($hour) ||
1432             defined($day) ||
1433             defined($month) ||
1434             defined($year)
1435             )
1436             {return undef}
1437            
1438            
1439             # default everything that isn't defined
1440 60 100       102 unless (defined $hour)
  22         27  
1441             {$hour = $self->{'hour'}}
1442 60 100       105 unless (defined $min)
  22         23  
1443             {$min = $self->{'min'}}
1444 60 100       94 unless (defined $sec)
  27         27  
1445             {$sec = $self->{'sec'}}
1446            
1447 60 100       89 unless (defined $month)
  4         4  
1448             {$month = $self->{'monthnum'}}
1449 60 100       88 unless (defined $year)
  4         4  
1450             {$year = $self->{'year'}}
1451 60 100       112 unless (defined $day)
  4         8  
1452             {$day = maxday($self->{'dayofmonth'}, $month, $year)}
1453              
1454             # set year to four digits
1455 60 50       121 if (length($year) == 2)
  0         0  
1456             {$year = substr($self->{'year'}, 0, 2) . $year}
1457            
1458            
1459             # BUG: this is where ezdate crashes on out-of-bounds dates
1460 60         91 eval {
1461 60         227 $rv = timelocal($sec, $min, $hour, $day, $month, $year);
1462             };
1463            
1464            
1465 60 50       3453 if (! defined $rv)
1466 0         0 { $self->warn("Do not recognize date format: $orgval") }
1467            
1468 60         131 return $rv;
1469            
1470             # get date sub
1471             # attempt to get date
1472             # supported date formats
1473             # 14 Jan 2001
1474             # 14 JAN 01
1475             # 14JAN2001
1476             # Jan 14, 2001
1477             # Jan 14, 01
1478             # 01-14-01
1479             # 1-14-01
1480             # 1-7-01
1481             # 01-14-2001
1482             sub getdate {
1483 56     56   79 my ($val, $day, $month, $year) = @_;
1484            
1485             # Tue Jun 12 13:03:28 2012
1486 56 50       524 if ($val =~ s/^([a-z]+) (\d+) (\S+) (\d+)$/$3/) {
    100          
    50          
    0          
    0          
    0          
1487 0         0 $year = $4;
1488 0         0 $month = $MonthNums{$1};
1489 0         0 $day = $2;
1490             }
1491            
1492             # 14 Jan 2001
1493             # 14 JAN 01
1494             # 14JAN2001 # will be normalized to have spaces
1495             elsif ($val =~ s/^(\d+) ([a-z]+) (\d+)//) {
1496 2         5 $day = $1;
1497 2         5 $month = $MonthNums{$2};
1498 2         4 $year = $3;
1499             }
1500            
1501             # Jan 14, 2001
1502             # Jan 14, 01
1503             elsif ($val =~ s/^([a-z]+) (\d+) (\d+)//) {
1504 54         140 $month = $MonthNums{$1};
1505 54         100 $day = $2;
1506 54         80 $year = $3;
1507             }
1508            
1509             # Jan 2001
1510             # Jan 01
1511             elsif ($val =~ s/^([a-z]+) (\d+)//) {
1512 0         0 $month = $MonthNums{$1};
1513 0         0 $year = $2;
1514             }
1515            
1516             # 2001-01-14
1517             elsif ($val =~ s/^(\d{4}) (\d+) (\d+)//) {
1518 0         0 $year = $1;
1519 0         0 $month = $2 - 1;
1520 0         0 $day = $3;
1521             }
1522            
1523             # 01-14-01
1524             # 1-14-01
1525             # 1-7-01
1526             # 01-14-2001
1527             elsif ($val =~ s/^(\d+) (\d+) (\d+)//) {
1528 0         0 $month = $1 - 1;
1529 0         0 $day = $2;
1530 0         0 $year = $3;
1531             }
1532              
1533 56         205 return ($val, $day, $month, $year);
1534             }
1535              
1536             sub ampmhour {
1537 35     35   79 my ($hour, $ampm)=@_;
1538            
1539             # if 12
1540 35 100       176 if ($hour == 12) {
  12 100       21  
1541             # if am, set to 0
1542 3 50       15 if ($ampm =~ m/^a/)
  0         0  
1543             {$hour = 0}
1544             }
1545            
1546             # else if pm, add 12
1547             elsif ($ampm =~ m/^p/)
1548             {$hour += 12}
1549 35         71 return $hour;
1550             }
1551             }
1552             #
1553             # timefromfull
1554             #------------------------------------------------------------------------------
1555              
1556              
1557             #------------------------------------------------------------------------------
1558             # gettime
1559             #
1560             # supported time formats:
1561             # 5pm
1562             # 5:34 pm
1563             # 17:34
1564             # 17:34:13
1565             # 5:34:13
1566             # 5:34:13 pm
1567             # 2330 (military time)
1568             #
1569             sub gettime {
1570 96     96   257 my ($self, $str, %opts)= @_;
1571 96         78 my ($hour, $min, $sec);
1572            
1573             # clean up a little
1574 96         126 $str =~ s/^://;
1575 96         133 $str =~ s/:$//;
1576 96         235 $str =~ s/(\d)(am|pm)/$1 $2/;
1577            
1578            
1579             # 5:34:13 pm
1580             # 5:34:13 p
1581 96 100 66     888 if ($str =~ s/^(\d+):(\d+):(\d+) (a|p)(m|\b)\s*//) {
    100          
    100          
    50          
    50          
    100          
    50          
1582              
1583 30         71 $hour = ampmhour($1, $4);
1584 30         49 $min = $2;
1585 30         41 $sec = $3;
1586             }
1587            
1588             # 17:34:13
1589             elsif ($str =~ s/^(\d+):(\d+):(\d+)\s*//) {
1590 4         7 $hour = $1;
1591 4         4 $min = $2;
1592 4         6 $sec = $3;
1593             }
1594            
1595             # 5:34 pm
1596             elsif ($str =~ s/^(\d+):(\d+) (a|p)m?\s*//) {
1597 5         11 $hour = ampmhour($1, $3);
1598 5         12 $min = $2;
1599             }
1600            
1601             # 17:34
1602 0         0 elsif ($str =~ s/^(\d+):(\d+)\s*//) {
1603 0         0 $hour = $1;
1604 0         0 $min = $2;
1605             }
1606            
1607             # 5 pm
1608             elsif ($str =~ s/^(\d+) (a|p)m?\b\s*//)
1609             {$hour = ampmhour($1, $2)}
1610            
1611             # elsif just digits
1612 0         0 elsif ( (! $opts{'skipjustdigits'}) && ($str =~ m/^\d+$/) ) {
1613 2         7 $str = zeropad_open($str, 4);
1614 2         6 $hour = substr($str, 0, 2);
1615 2         5 $min = substr($str, 2, 2);
1616             }
1617            
1618             # else don't recognize format
1619             elsif ($opts{'croakonfail'})
1620             {return $self->warn("don't recognize time format: $str")}
1621              
1622 96         383 return ($str, $hour, $min, $sec);
1623             }
1624             #
1625             # gettime
1626             #------------------------------------------------------------------------------
1627              
1628              
1629              
1630             #------------------------------------------------------------------------------
1631             # maxday
1632             #
1633             # if the input day is too high for given months,
1634             # returns the highest possible day for that month,
1635             # otherwise returns the input day
1636             #
1637             sub maxday {
1638 4     4   5 my ($day, $month, $year) = @_;
1639 4         8 my $maxday = daysinmonth($month, $year);
1640            
1641 4 50       9 $day > $maxday and return $maxday;
1642 4         5 return $day;
1643             }
1644             #
1645             # maxday
1646             #------------------------------------------------------------------------------
1647              
1648              
1649             #------------------------------------------------------------------------------
1650             # zeropad_open, zeropad_2
1651             #
1652             sub zeropad_open {
1653 63     63   86 my ($rv, $length) = @_;
1654 63   50     155 $length ||= 2;
1655             # return ('0' x ($length - length($rv))) . $rv;
1656 63         519 return sprintf "%0${length}d", $rv;
1657             }
1658              
1659             sub zeropad_2 {
1660 8526     8526   34189 return sprintf "%02d", $_[0];
1661             }
1662             #
1663             # zeropad_open, zeropad_2
1664             #------------------------------------------------------------------------------
1665              
1666              
1667             #------------------------------------------------------------------------------
1668             # clone
1669             #
1670             sub clone {
1671 0     0     my ($ob) = @_;
1672            
1673 0           return ref($ob)->TIEHASH([
1674             $ob->{'sec'},
1675             $ob->{'min'},
1676             $ob->{'hour'},
1677             $ob->{'dayofmonth'},
1678             $ob->{'monthnum'},
1679             $ob->{'year'},
1680             $ob->{'weekdaynum'},
1681             $ob->{'yearday'},
1682             $ob->{'dst'},
1683             $ob->{'epochsec'}
1684             ]);
1685             }
1686             #
1687             # clone
1688             #------------------------------------------------------------------------------
1689              
1690              
1691             #
1692             # Date::EzDate::Tie
1693             ###############################################################################
1694              
1695              
1696             # return true
1697             1;
1698             __END__