File Coverage

blib/lib/Date/EzDate.pm
Criterion Covered Total %
statement 487 590 82.5
branch 265 360 73.6
condition 66 104 63.4
subroutine 54 64 84.3
pod 9 15 60.0
total 881 1133 77.7


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