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   2363 use strict;
  1         2  
  1         41  
3 1     1   6 use Carp;
  1         1  
  1         93  
4 1     1   7 use vars qw($VERSION @ltimefields $overload $default_warning);
  1         5  
  1         166  
5              
6             # version
7             $VERSION = '1.15';
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   135 '""' => sub{$_[0]->{'default'}}, # stringification
19 1         14 '<=>' => \&compare, # comparison
20             '+' => \&addition, # addition
21             '-' => \&subtraction, # subtraction
22 1     1   1519 fallback => 1; # operations not defined here
  1         1243  
23              
24              
25             # constants and globals
26 1     1   113 use constant WARN_NONE => 0;
  1         2  
  1         89  
27 1     1   7 use constant WARN_STDERR => 1;
  1         2  
  1         56  
28 1     1   7 use constant WARN_CROAK => 2;
  1         1  
  1         101  
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   7 use vars qw[@EXPORT_OK %EXPORT_TAGS @ISA];
  1         2  
  1         1847  
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 12599 my ($class, $init, %opts) = @_;
59 81         84 my ($rv, %tiehash);
60            
61             # if date is an EzDate object
62 81 50 66     613 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     493 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     458 $class->can('preprocess_time_string') and
75             defined($init) and
76             $init = $class->preprocess_time_string($init);
77            
78 81 50       390 tie(%tiehash, $class . '::Tie', $init, %opts) or return undef;
79            
80 81         258 $rv = bless(\%tiehash, $class);
81 81 50       310 $rv->can('after_create') and $rv->after_create();
82            
83 81         194 return $rv;
84             }
85             #
86             # new
87             #------------------------------------------------------------------------------
88              
89              
90             #------------------------------------------------------------------------------
91             # clone
92             #
93             sub clone {
94 21     21 1 432 my ($self) = @_;
95 21         64 my $ob = $self->tie_ob;
96 21         23 my ($rv);
97            
98 21         134 $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         102 %{$ob->{'formats'}} and
  21         69  
112 21 50       42 $rv->tie_ob->{'formats'} = {%{$ob->{'formats'}}};
113            
114 21         108 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 16 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 42 sub tie_ob{return tied(%{$_[0]})}
  46         127  
139             #
140             # format subs
141             #------------------------------------------------------------------------------
142              
143              
144             #------------------------------------------------------------------------------
145             # next_month
146             #
147             sub next_month {
148 3     3 1 1080 my ($self, $jump) = @_;
149 3         7 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 9 my ($left, $right) = @_;
162 1 50       5 ref($right) or $right = Date::EzDate->new($right);
163 1         9 $left->{$overload} <=> $right->{$overload};
164             }
165             #
166             # compare
167             #------------------------------------------------------------------------------
168              
169              
170             #------------------------------------------------------------------------------
171             # addition and subtraction
172             #
173             sub addition {
174 1     1 0 11 my ($self, $val) = @_;
175 1         5 $self->{$overload} += $val;
176 1         10 return $self;
177             }
178              
179             sub subtraction {
180 1     1 0 394 my ($self, $val) = @_;
181 1         5 $self->{$overload} -= $val;
182 1         6 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 512 my ($class, $start, $end);
194            
195             # if first argument is a reference, use it as start
196 9 50       48 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         11 my (@args);
204            
205             # expand array references
206 9         16 foreach my $arg (@_) {
207 14 100       36 if (UNIVERSAL::isa($arg, 'ARRAY'))
208 4         11 { push @args, @$arg }
209             else
210 10         19 { push @args, $arg }
211             }
212            
213             # if called using a class
214 9 50       38 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         12 $class = 'Date::EzDate';
220 9         14 ($start, $end) = @args;
221             }
222             }
223            
224            
225             # both $start and $end must be defined
226 9 50 33     43 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       26 ref($start) or $start = $class->new($start);
231 9 100       25 ref($end) or $end = $class->new($end);
232            
233             # rearrange as necessary
234 9 50       37 if ($start->{'epoch day'} > $end->{'epoch day'}){
235 0         0 ($start, $end) = ($end, $start);
236             }
237            
238             # if same year
239 9 100       33 if ($start->{'year'} == $end->{'year'}) {
240             # if same month
241 8 100       17 if ($start->{'monthnum'} == $end->{'monthnum'}) {
242             # same day
243 7 100       15 if ($start->{'day of month'} == $end->{'day of month'}) {
244 2         6 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         12 $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         3 my $format = '{month short} {day of month no zero}';
264            
265             return
266 1         2 $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         3 my $format = '{month short} {day of month no zero}, {year}';
277            
278             return
279 1         5 $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 220 my ($class, $start, $end, $rv);
294            
295             # if first argument is a reference, use it as start
296 2 50       7 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       15 if ( int(@_/2)!=(@_/2) )
305 0         0 { ($class, $start, $end) = @_ }
306            
307             # else called w/o an explicit class
308             else {
309 2         3 $class = 'Date::EzDate';
310 2         4 ($start, $end) = @_;
311             }
312             }
313            
314             # both $start and $end must be defined
315 2 50 33     14 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       10 ref($start) or $start = $class->new($start);
320 2 50       7 ref($end) or $end = $class->new($end);
321            
322             # rearrange as necessary
323 2 50       12 if ($start->{'epochsec'} > $end->{'epochsec'}){
324 0         0 ($start, $end) = ($end, $start);
325             }
326            
327 2         6 $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         8 '-' .
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 192 my (@org) = @_;
350 1         2 my ($class, @rv, $current);
351 1         1 $class = 'Date::EzDate';
352            
353 1         3 foreach my $date (@org) {
354 8 50       32 ref($date) or $date = $class->new($date);
355            
356 8 100       17 if ($current) {
357 7 100       27 if ( $date->{'epochday'} == ($current->[1]->{'epochday'} + 1) ) {
358 5         17 $current->[1] = $date;
359             }
360            
361             else {
362 2         4 push @rv, $current;
363 2         7 $current = [ $date, $date ];
364             }
365             }
366            
367             else {
368 1         3 $current = [ $date, $date ];
369             }
370             }
371            
372             # add last current
373 1 50       4 $current and push @rv, $current;
374            
375 1         26 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   8 use strict;
  1         2  
  1         48  
388 1     1   6 use Carp 'croak', 'carp';
  1         2  
  1         89  
389 1     1   773 use Tie::Hash;
  1         1077  
  1         38  
390 1     1   710 use Time::Local;
  1         2051  
  1         75  
391 1     1   7 use re 'taint';
  1         2  
  1         56  
392 1     1   686 use POSIX;
  1         5542  
  1         7  
393              
394             # debugging tools
395             # use Debug::ShowStuff ':all';
396              
397 1         573 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   3226 );
  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         2  
  1         96  
484 1     1   7 use constant t_60_60 => 3600;
  1         2  
  1         56  
485 1     1   8 use constant t_60_60_24 => 86400;
  1         1  
  1         75  
486 1     1   7 use constant WARN_NONE => Date::EzDate::WARN_NONE;
  1         1  
  1         85  
487 1     1   7 use constant WARN_STDERR => Date::EzDate::WARN_STDERR;
  1         1  
  1         73  
488 1     1   7 use constant WARN_CROAK => Date::EzDate::WARN_CROAK;
  1         2  
  1         55  
489              
490 1     1   7 use constant DST_ADJUST_NO => 0;
  1         1  
  1         62  
491 1     1   7 use constant DST_ADJUST_YES => 1;
  1         1  
  1         8431  
492              
493              
494             #------------------------------------------------------------------------------
495             # TIEHASH
496             #
497             sub TIEHASH {
498 81     81   140 my ($class, $time, %opts)=@_;
499 81         194 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       472 $self->{'zero_hour_ampm'} = defined($opts{'zero_hour_ampm'}) ? $opts{'zero_hour_ampm'} : 1;
507 81         136 $self->{'formats'} = {};
508 81         179 $self->{'settings'} = {'dst_kludge' => 1};
509            
510             # default builtin formats
511 81         162 $self->set_format('fullday', '{month short} {day of month no zero}, {year}');
512 81         151 $self->set_format('fulldate', '{fullday}');
513 81         151 $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       141 if (ref $time){
519 21 50       96 if (! UNIVERSAL::isa($time, 'ARRAY') )
520 0         0 { croak '$time is not an array' }
521            
522 21         22 @{$self}{@Date::EzDate::ltimefields, 'epochsec'}=@{$time};
  21         163  
  21         42  
523             }
524            
525             else {
526             # calculate and set properties of current time
527             # set time from timefromfull
528 60         139 $self->setfromtime(DST_ADJUST_NO, time());
529            
530 60 100       113 if (defined $time) {
531 59         186 $time = $self->timefromfull($time);
532            
533 59 50       117 defined($time) or return undef;
534            
535 59         111 $self->setfromtime(DST_ADJUST_NO, $time);
536             }
537             }
538            
539 81         324 return $self;
540             }
541             #
542             # TIEHASH
543             #------------------------------------------------------------------------------
544              
545              
546             #------------------------------------------------------------------------------
547             # setfromtime
548             #
549             sub setfromtime {
550 1794     1794   1978 my ($self, $dst_adjust, $time) = @_;
551 1794         1174 my ($dst_before, @timevals);
552 1794 50       3204 $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       2520 if ($dst_adjust)
562 1628         1758 { $dst_before = $self->{'dst'} }
563            
564             # set current epoch second
565 1794         1704 $self->{'epochsec'} = $time;
566            
567             # get current time values
568 1794         26853 @timevals = localtime($time);
569            
570 1794 50       3753 if (! @timevals)
571 0         0 { croak "cannot: get localtime values from this epoch value: $time" }
572            
573 1794         2206 @{$self}{@Date::EzDate::ltimefields} = @timevals;
  1794         6311  
574            
575 1794 50 66     9088 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         6663 $self->{'year'} += 1900;
590             }
591             #
592             # setfromtime
593             #------------------------------------------------------------------------------
594              
595              
596             #------------------------------------------------------------------------------
597             # set_format
598             #
599             sub set_format {
600 325     325   361 my ($self, $name, $format) = @_;
601            
602             # normalize name
603 325         384 $name =~ s|\s||g;
604 325         283 $name =~ tr/A-Z/a-z/;
605            
606 325         363 $self->{'formats'}->{$name} = format_split($format);
607             }
608              
609             sub format_split {
610 425     425   3002 my @rv = split(m/(\{[^\{\}]*\}|\%.)/, $_[0]);
611            
612 425         573 foreach my $el (@rv) {
613 2553 100       4909 if ($el =~ m|^\{.*\}$|s)
  956         1040  
614             {normalize_key($el)}
615             }
616            
617 425         1042 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   14003 $_[0] =~ s|\s||gs;
650 11780 100       21107 $_[0] =~ tr/A-Z/a-z/ unless $_[0] =~ m|^\%\w$|;
651 11780         11119 $_[0] =~ s|ordinal|ord|sg;
652            
653 11780         9079 $_[0] =~ s|hours|hour|sg;
654             # $_[0] =~ s|days\b|day|sg;
655            
656 11780         9396 $_[0] =~ s|minute|min|sg;
657 11780         9848 $_[0] =~ s|mins|min|sg;
658            
659 11780         9085 $_[0] =~ s|second|sec|sg;
660 11780         9121 $_[0] =~ s|secs|sec|sg;
661            
662 11780         10627 $_[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   3 my ($self, $jumps) = @_;
674 3         8 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   2872 my ($self, $key, $val) = @_;
686 1715         1509 my $orgkey = $key;
687 1715         1224 my $orgval = $val;
688            
689             # error checking
690 1715 50       2682 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       3609 $val =~ m|[\{\%]| and return $self->set_format($key, $val);
695            
696             # clean a little
697 1715         1971 normalize_key($key);
698            
699             # get key from aliases if necessary
700 1715         2715 $key = $self->get_alias($key, 'strip_no_zero'=>1);
701            
702            
703             # dayofmonth, weekdaynum, yearday
704 1715 100 100     22023 if ($key =~ m/^(dayofmonth|weekdaynum|dayofweeknum|yearday)$/s) {
  3 100 100     11  
    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     164 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         161 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         13 $self->setfromtime(
728             DST_ADJUST_NO,
729             $self->{'epochsec'} - ($self->{'min'} * 60) + ($val * 60)
730             )
731             }
732            
733             elsif ($key eq 'minofday') {
734 1         7 $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         27 $val = timelocal(
742             $self->{'sec'},
743             $self->{'min'},
744             $val,
745             $self->{'dayofmonth'},
746             $self->{'monthnum'},
747             $self->{'year'},
748             );
749            
750 6         261 $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       12 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       8 unless (defined $sec)
  2         5  
762             {$sec = $self->{'sec'}}
763            
764             $self->setfromtime
765             (
766 3         21 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       11 if ($self->{'hour'} >= 12)
  2         4  
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         27 my ($multiplier);
791            
792 25 100       60 if (length($val) == 1)
  1         2  
793             {$val .= 'm'}
794 25         39 $val = lc($val);
795            
796             # error checking
797 25 50 66     124 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       63 if ($self->{'hour'} < 12) {
802 21 100       47 if ($val eq 'am') {return}
  1         3  
803             }
804             else {
805 4 50       15 if ($val eq 'pm') {return}
  0         0  
806             }
807            
808 24 100       56 if ($val eq 'am')
  4         7  
809 20         26 {$multiplier = -1}
810             else
811             {$multiplier = 1}
812            
813 24         71 $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         1187 my ($oldhour, $oldepochsec, $oldmin);
830            
831 1543         2447 $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       66 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         52 $maxday = daysinmonth($self->{'monthnum'}, $val);
866            
867 24 50       54 if ($self->{'dayofmonth'} > $maxday) {
  24         40  
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         96 $val = timelocal($self->{'sec'}, $self->{'min'}, $self->{'hour'}, $targetday, $self->{'monthnum'}, $val);
878 24         1095 $self->setfromtime(DST_ADJUST_YES, $val);
879             }
880            
881 3         13 elsif ($key =~ m/^year(two|2)digit/) {
882 2         15 $val =~ s|^.*(..)$|$1|;
883 2         11 $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         92 elsif ($key eq 'monthnum') {
894 31         40 my ($target, $epoch_second);
895 31         47 my $month = $self->{'monthnum'};
896 31         52 my $year = $self->{'year'};
897 31         37 my $orgday = $self->{'dayofmonth'};
898 31         37 my $dayofmonth = $self->{'dayofmonth'};
899 31         36 my $jumps = $val - $month;
900            
901             # if nothing to do
902 31 50       61 $jumps or return;
903            
904 31         23 $target = $jumps;
905 31         38 $target = abs($target);
906            
907             # jumping forward
908 31 100       51 if ($jumps > 0) {
909 23         55 foreach (1..$target) {
910             # if end of year
911 35 100       51 if ($month == 11) {
  33         64  
912 2         5 $month = 0;
913 2         4 $year++;
914             }
915             else
916             {$month++}
917             }
918             }
919            
920             # jumping backward
921             else {
922 8         26 foreach (1..$target) {
923             # if beginning of year
924 69 100       80 if ($month == 0) {
  67         59  
925 2         1 $month = 11;
926 2         10 $year--;
927             }
928             else
929             {$month--}
930             }
931             }
932            
933            
934             # adjust day for shorter month (if necessary)
935 31 100       87 if ($dayofmonth > 28) {
936 9         21 my $dim = daysinmonth($month, $year);
937            
938 9 100       29 if ($dim < $dayofmonth)
939 2         4 { $dayofmonth = $dim }
940             }
941            
942             # get epoch second from timelocal
943 31         130 $epoch_second = timelocal($self->{'sec'}, $self->{'min'}, $self->{'hour'}, $dayofmonth, $month, $year);
944 31         1607 $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         84 ) {
960 5         26 $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         5  
974             {$opts{'dateonly'} = 1}
975            
976 1         5 $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   318381 my ($self, $key, %opts) = @_;
992 9651         7928 my $orgkey = $key;
993            
994             # get key from aliases if necessary
995 9651         12575 $key = $self->get_alias($key);
996            
997            
998             #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
999             # nested properties
1000             #
1001 9651 100 100     36370 if ( (! ref $key) && ($key =~ m|[\{\%]|) && ($key !~ m|$pcode|o) )
  100   66     208  
1002             {$key = format_split($key)}
1003            
1004 9651 100       14214 if (ref $key) {
1005 115         402 my @rv = @$key;
1006            
1007 115         156 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     3467 if (
1011             ($el =~ s|\{([^\}]+)\}|$1|) || # if it is enclosed in {}
1012             ($el =~ m|$pcode|o) # if it is a %x code
1013             ) {
1014 434         490 $el =~ s|['"\s]||g;
1015 434         644 $el = $self->FETCH($el, normalized=>1);
1016             }
1017             }
1018            
1019 115         735 return join('', @rv);
1020             }
1021             #
1022             # nested properties
1023             #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1024            
1025            
1026             # clean up key
1027 9536 100       18715 $opts{'normalized'} or normalize_key($key);
1028            
1029            
1030             # already or mostly calculated
1031 9536 100       15160 if (exists $self->{$key}) {
1032 5333 100       13281 if ($key =~ m/^(dayofmonth|monthnum|hour|min|sec)$/)
  3650         6010  
1033             {return zeropad_2($self->{$key})}
1034            
1035 1683         4237 return $self->{$key};
1036             }
1037            
1038             # nozero's
1039 4203 100       6855 if ($key =~ s/no(zero|0)//)
  190         363  
1040             {return $self->FETCH($key) + 0}
1041            
1042             # day of month ord
1043 4013 50       5489 if ($key =~ m|^dayofmonthord(word)?$|)
  0         0  
1044             {return $OrdWords[$self->{'dayofmonth'}]}
1045 4013 50       5561 if ($key eq 'dayofmonthordnum')
  0         0  
1046             {return $OrdNums[$self->{'dayofmonth'}]}
1047            
1048             # weekday
1049 4013 100       5622 if ($key =~ m/^(weekdayshort|dayofweekshort|dayofweek)$/)
  60         240  
1050             {return $WeekDayShort[$self->{'weekdaynum'}]}
1051 3953 100       5098 if ($key =~ m/^(weekdaylong|dayofweeklong)$/)
  41         173  
1052             {return $WeekDayLong[$self->{'weekdaynum'}]}
1053 3912 50       5256 if ($key eq 'dayofweeknum')
  0         0  
1054             {return $self->{'weekdaynum'}}
1055            
1056             # month
1057 3912 100       5104 if ($key eq 'monthshort') {
1058 82 50       149 defined($self->{'monthnum'}) or die 'undefined monthnum';
1059 82         264 return $MonthShort[$self->{'monthnum'}];
1060             }
1061            
1062 3830 100       5307 if ($key eq 'monthlong')
1063 21         94 { return $MonthLong[$self->{'monthnum'}] }
1064 3809 100       5822 if ($key =~ m/^monthnumbase(one|1)/)
1065 80         238 { return zeropad_2($self->{'monthnum'} + 1) }
1066            
1067 3729 100       5359 if ($key =~ m/^yeardaybase(one|1)/)
1068 61         210 { return zeropad_open($self->{'yearday'} + 1, 3) }
1069            
1070             # year
1071 3668 100       5479 if ($key =~ m/^yeartwodigit/)
1072 60         297 { return substr($self->{'year'}, 2) }
1073            
1074             # epochs
1075 3608 50       5106 if ($key eq 'epochmin')
1076 0         0 { return $self->getepochmin }
1077 3608 50       4552 if ($key eq 'epochhour')
1078 0         0 { return $self->getepochhour }
1079 3608 100       4774 if ($key eq 'epochday')
1080 1589         2563 { return $self->getepochday }
1081            
1082             # leapyear
1083 2019 100       3095 if ($key =~ m/^(is)?leapyear/)
  40         115  
1084             {return isleapyear($self->{'year'})}
1085            
1086             # days in month
1087 1979 100       2572 if ($key eq 'daysinmonth')
  40         118  
1088             {return daysinmonth($self->{'monthnum'}, $self->{'year'}) }
1089            
1090             # DMY: eg 15JAN2001
1091 1939 100       2626 if ($key eq 'dmy')
  1         4  
1092             {return zeropad_2($self->{'dayofmonth'}) . uc($MonthShort[$self->{'monthnum'}]) . $self->{'year'} }
1093            
1094             # full
1095 1938 100       2873 if ($key eq 'full') {
1096             return
1097 1524         5291 $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       599 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       797 if ($key =~ m|^mil(itary)?time$|)
  43         117  
1117             {return zeropad_2($self->{'hour'}) . zeropad_2($self->{'min'}) }
1118            
1119             # iso8601
1120 371 50       571 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       594 if ($key eq 'minofday')
  42         184  
1133             {return $self->{'min'} + ($self->{'hour'} * 60) }
1134            
1135             # variable
1136 329         285 my ($ampm);
1137            
1138             # calculate ampm, which is needed in most results from here down
1139 329 100       704 $ampm = ($self->{'hour'} >= 12) ? 'pm' : 'am';
1140            
1141             # am/pm
1142 329 100 100     1157 if ( ($key eq 'ampm') || ($key eq 'ampmlc') )
  74         234  
1143             {return $ampm}
1144            
1145             # AM/PM uppercase
1146 255 100       387 if ($key eq 'ampmuc')
  60         255  
1147             {return uc($ampm)}
1148            
1149             # variable
1150 195         155 my ($ampmhour);
1151            
1152             # calculate ampmhour, which is needed from here down
1153 195 100 66     818 if ( ($self->{'hour'} == 0) || ($self->{'hour'} == 12) )
  4 100       4  
1154 1         2 {$ampmhour = 12}
1155             elsif ($self->{'hour'} > 12)
1156 190         217 {$ampmhour = $self->{'hour'} - 12}
1157             else
1158             {$ampmhour = $self->{'hour'}}
1159            
1160             # am/pm hour
1161 195 100       309 if ($key eq 'ampmhour')
  113         201  
1162             {return zeropad_2($ampmhour)}
1163            
1164             # hour and minute with ampm
1165 82 100 66     340 if (
1166             ($key eq 'clocktime') ||
1167             ($key eq 'clocktimestrict')
1168             ) {
1169 22         58 my $minofday = $self->FETCH('minofday');
1170            
1171 22 50       58 if ($key eq 'clocktime') {
1172 22 50       46 if ($minofday == 0)
  0         0  
1173             {return 'midnight'}
1174 22 50       51 if ($minofday == 12 * t_60)
  0         0  
1175             {return 'noon'}
1176             }
1177            
1178             return
1179 22         39 $self->FETCH('ampmhournozero') .
1180             ':' . zeropad_2($self->{'min'}) . ' ' .
1181             $ampm;
1182             }
1183            
1184             # character codes
1185 60 100       176 $key eq 'newline' && return "\n";
1186 40 100       144 $key eq 'tab' && return "\t";
1187 20 50       57 $key eq 'leftbrace' && return '{';
1188 20 50       39 $key eq 'rightbrace' && return '}';
1189 20 50       100 $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   112 my ($year) = @_;
1223            
1224 82 50 33     687 return 1 if ( ($year % 4 == 0) && ( ($year % 100) || ($year % 400 == 0) ) );
      66        
1225 21         238 return 0;
1226             }
1227             #
1228             # isleapyear
1229             #------------------------------------------------------------------------------
1230              
1231              
1232             #------------------------------------------------------------------------------
1233             # get_alias
1234             #
1235             sub get_alias {
1236 12309     12309   13033 my ($self, $key, %opts) = @_;
1237            
1238             # normalize
1239 12309 100       24073 unless ($key =~ m|[\{\%]|) {
1240 11349         11701 $key =~ s|\s||g;
1241 11349         10494 $key = lc($key);
1242            
1243             # strip "nozero" if that option was sent
1244 11349 100       17796 $opts{'strip_no_zero'} and $key =~ s|nozero||g;
1245             }
1246            
1247             # if this has an alias
1248 12309 100       17753 if (exists $PCodes{$key})
  943         2346  
1249             {return $self->get_alias($PCodes{$key}, %opts)}
1250            
1251             # if this is a named format
1252 11366 100       17852 if (exists $self->{'formats'}->{$key})
  15         26  
1253             {return $self->{'formats'}->{$key}}
1254            
1255 11351         18152 return $key;
1256             }
1257             #
1258             # get_alias
1259             #------------------------------------------------------------------------------
1260              
1261              
1262              
1263             #------------------------------------------------------------------------------
1264             # getepochday
1265             #
1266             sub getepochday {
1267 3132     3132   2750 my ($self, $epochsec) = @_;
1268            
1269             # TESTING
1270             # println '$date->getepochday';
1271             # my $indent = indent();
1272            
1273             # get epochsecond of date object
1274 3132 50       5595 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       4387 unless (defined $epoch_offset) {
1280 1         1 my %date;
1281 1         16 @date{@Date::EzDate::ltimefields} = localtime(0);
1282 1         6 $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         12299 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   154 my ($monthnum, $year) = @_;
1321            
1322 101 100       245 if ($monthnum != 1)
  59         251  
1323             {return $MonthDays[$monthnum]}
1324 42 100       94 if (isleapyear($year))
  41         110  
1325             {return 29}
1326 1         4 return 28;
1327             }
1328             #
1329             # daysinmonth
1330             #------------------------------------------------------------------------------
1331              
1332              
1333             #------------------------------------------------------------------------------
1334             # timefromfull
1335             #
1336             sub timefromfull {
1337 60     60   111 my ($self, $val, %opts) = @_;
1338 60         71 my ($hour, $min, $sec, $day, $month, $year, $rv);
1339 60         96 my $orgval = $val;
1340            
1341             # error checking
1342 60 50       129 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       272 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       539 $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         173 $val =~ s/(\d+\:\d+)\.[\d\-]+/$1/g;
1359            
1360            
1361             # Another special case: A.M. to AM and P.M. to PM
1362 60         97 $val =~ s/a\.m\b/am/gis;
1363 60         79 $val =~ s/p\.m\b/pm/gis;
1364            
1365             # normalize
1366 60         82 $val =~ tr/A-Z/a-z/;
1367 60         319 $val =~ s/[^\w:]/ /g;
1368 60         222 $val =~ s/\s*:\s*/:/g;
1369            
1370             # change ordinals to numbers
1371 60         460 $val =~ s|$OrdWordsRx|$OrdWordsNums{$1}|gis;
1372 60         186 $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         124 $val =~ s/\bnoon\b/ 12:00:00 /gis;
1378 60         94 $val =~ s/\bmidnight\b/ 00:00:00 /gis;
1379            
1380             # normalize some more
1381 60         167 $val =~ s/(\d)([a-z])/$1 $2/g;
1382 60         131 $val =~ s/([a-z])(\d)/$1 $2/g;
1383 60         208 $val =~ s/\s+/ /g;
1384 60         120 $val =~ s/^\s*//;
1385 60         247 $val =~ s/\s*$//;
1386            
1387            
1388             # today, tomorrow, and yesterday
1389 60 50 33     264 if ( ($val eq 'today') || ($val eq 'now') )
  0         0  
1390             {return time()}
1391 60 50       118 if ($val eq 'tomorrow')
  0         0  
1392             {return time() + t_60_60_24}
1393 60 50       100 if ($val eq 'yesterday')
  0         0  
1394             {return time() - t_60_60_24}
1395            
1396             # normalize further
1397 60         172 $val =~ s/([a-z]{3})[a-z]+/$1/gs;
1398            
1399             # remove weekday
1400 60         348 $val =~ s/((sun)|(mon)|(tue)|(wed)|(thu)|(fri)|(sat))\s*//;
1401 60         230 $val =~ s/\s*$//;
1402            
1403            
1404             # attempt to get time
1405 60 100       152 unless ($opts{'dateonly'}) {
1406 59         151 ($val, $hour, $min, $sec) = $self->gettime($val, 'skipjustdigits'=>1);
1407             }
1408            
1409             # attempt to get date
1410 60 50       135 unless ($opts{'timeonly'}) {
1411 60 100       121 if (length $val)
  56         113  
1412             {($val, $day, $month, $year) = getdate($val)}
1413             }
1414            
1415             # trim
1416 60         169 $val =~ s/^\s*//;
1417            
1418             # attempt to get time again
1419 60 100       120 unless ($opts{'dateonly'}) {
1420 59 100 66     214 if (length($val) && (! defined($hour)) ) {
1421 34         68 ($val, $hour, $min, $sec) = $self->gettime($val, 'skipjustdigits'=>1, 'croakonfail'=>1);
1422             }
1423             }
1424            
1425            
1426 60 50 33     244 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     184 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       110 unless (defined $hour)
  22         28  
1441             {$hour = $self->{'hour'}}
1442 60 100       121 unless (defined $min)
  22         20  
1443             {$min = $self->{'min'}}
1444 60 100       94 unless (defined $sec)
  27         29  
1445             {$sec = $self->{'sec'}}
1446            
1447 60 100       97 unless (defined $month)
  4         6  
1448             {$month = $self->{'monthnum'}}
1449 60 100       112 unless (defined $year)
  4         3  
1450             {$year = $self->{'year'}}
1451 60 100       93 unless (defined $day)
  4         9  
1452             {$day = maxday($self->{'dayofmonth'}, $month, $year)}
1453              
1454             # set year to four digits
1455 60 50       137 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         87 eval {
1461 60         200 $rv = timelocal($sec, $min, $hour, $day, $month, $year);
1462             };
1463            
1464            
1465 60 50       3773 if (! defined $rv)
1466 0         0 { $self->warn("Do not recognize date format: $orgval") }
1467            
1468 60         143 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   74 my ($val, $day, $month, $year) = @_;
1484            
1485             # Tue Jun 12 13:03:28 2012
1486 56 50       463 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         4 $day = $1;
1497 2         4 $month = $MonthNums{$2};
1498 2         2 $year = $3;
1499             }
1500            
1501             # Jan 14, 2001
1502             # Jan 14, 01
1503             elsif ($val =~ s/^([a-z]+) (\d+) (\d+)//) {
1504 54         129 $month = $MonthNums{$1};
1505 54         91 $day = $2;
1506 54         70 $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         198 return ($val, $day, $month, $year);
1534             }
1535              
1536             sub ampmhour {
1537 35     35   71 my ($hour, $ampm)=@_;
1538            
1539             # if 12
1540 35 100       219 if ($hour == 12) {
  12 100       23  
1541             # if am, set to 0
1542 3 50       13 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         62 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   234 my ($self, $str, %opts)= @_;
1571 96         80 my ($hour, $min, $sec);
1572            
1573             # clean up a little
1574 96         134 $str =~ s/^://;
1575 96         126 $str =~ s/:$//;
1576 96         226 $str =~ s/(\d)(am|pm)/$1 $2/;
1577            
1578            
1579             # 5:34:13 pm
1580             # 5:34:13 p
1581 96 100 66     830 if ($str =~ s/^(\d+):(\d+):(\d+) (a|p)(m|\b)\s*//) {
    100          
    100          
    50          
    50          
    100          
    50          
1582              
1583 30         69 $hour = ampmhour($1, $4);
1584 30         47 $min = $2;
1585 30         33 $sec = $3;
1586             }
1587            
1588             # 17:34:13
1589             elsif ($str =~ s/^(\d+):(\d+):(\d+)\s*//) {
1590 4         6 $hour = $1;
1591 4         4 $min = $2;
1592 4         5 $sec = $3;
1593             }
1594            
1595             # 5:34 pm
1596             elsif ($str =~ s/^(\d+):(\d+) (a|p)m?\s*//) {
1597 5         10 $hour = ampmhour($1, $3);
1598 5         9 $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         8 $str = zeropad_open($str, 4);
1614 2         7 $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         346 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   6 my ($day, $month, $year) = @_;
1639 4         9 my $maxday = daysinmonth($month, $year);
1640            
1641 4 50       11 $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   74 my ($rv, $length) = @_;
1654 63   50     118 $length ||= 2;
1655             # return ('0' x ($length - length($rv))) . $rv;
1656 63         420 return sprintf "%0${length}d", $rv;
1657             }
1658              
1659             sub zeropad_2 {
1660 8526     8526   30976 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__