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