File Coverage

blib/lib/Date/ICal.pm
Criterion Covered Total %
statement 263 303 86.8
branch 95 128 74.2
condition 22 31 70.9
subroutine 37 44 84.0
pod 23 30 76.6
total 440 536 82.0


line stmt bran cond sub pod time code
1             # $Rev: 680 $
2             package Date::ICal;
3 11     11   770390 use strict;
  11         128  
  11         399  
4              
5 11     11   61 use vars qw($VERSION $localzone $localoffset @months @leapmonths %add_units);
  11         20  
  11         1187  
6             $VERSION = '2.'.(qw'$Rev: 680 $')[1];
7 11     11   121 use Carp;
  11         53  
  11         808  
8 11     11   5883 use Time::Local;
  11         25649  
  11         642  
9 11     11   4892 use Date::Leapyear qw();
  11         2693  
  11         267  
10 11     11   5362 use Date::ICal::Duration;
  11         30  
  11         568  
11 11         244 use overload '<=>' => 'compare',
12             'fallback' => 1,
13             '-' => \&subtract,
14 11     11   13872 '+' => \&add_overload;
  11         11374  
15              
16             $localzone = $ENV{TZ} || 0;
17             $localoffset = _calc_local_offset();
18              
19             # Documentation {{{
20              
21             =head1 NAME
22              
23             Date::ICal - Perl extension for ICalendar date objects.
24              
25             =head1 VERSION
26              
27             $Revision: 680 $
28              
29             =head1 SYNOPSIS
30              
31             use Date::ICal;
32              
33             $ical = Date::ICal->new( ical => '19971024T120000' );
34             $ical = Date::ICal->new( epoch => time );
35             $ical = Date::ICal->new( year => 1964,
36             month => 10, day => 16, hour => 16,
37             min => 12, sec => 47 );
38              
39             $hour = $ical->hour;
40             $year = $ical->year;
41              
42             $ical_string = $ical->ical;
43             $epoch_time = $ical->epoch;
44              
45             $ical2 = $ical + $duration;
46              
47             (Where $duration is either a duration string, like 'P2W3DT7H9M', or a
48             Date::ICal::Duration (qv) object.
49              
50             $ical += 'P6DT12H';
51              
52             $duration = $ical - $ical2;
53             $ical3 = $ical - $duration;
54              
55             =head1 DESCRIPTION
56              
57             Date::ICal talks the ICal date format, and is intended to be a base class for
58             other date/calendar modules that know about ICal time format also.
59              
60             =head1 AUTHOR
61              
62             Rich Bowen, and the Reefknot team. Alas, Reefknot is no more. See
63             L for more modern and accurate modules.
64              
65             Last touched by $Author: michal-josef-spacek $
66              
67             =head1 METHODS
68              
69             Date::ICal has the following methods available:
70              
71             =head2 new
72              
73             A new Date::ICal object can be created with any valid ICal string:
74              
75             my $ical = Date::ICal->new( ical => '19971024T120000' );
76             # will default to the timezone specified in $TZ, see below
77              
78             Or with any epoch time:
79              
80             my $ical = Date::ICal->new( epoch => time );
81              
82             Or, better still, create it with components
83              
84             my $date = Date::ICal->new(
85             day => 25,
86             month => 10,
87             year => 1066,
88             hour => 7,
89             min => 15,
90             sec => 47
91             );
92              
93             If you call new without any arguments, you'll get a Date::ICal object that is
94             set to the time right now.
95              
96             my $ical = Date::ICal->new();
97              
98             If you already have an object in Date::ICal, or some other subclass
99             thereof, you can create a new Date::ICal (or subclass) object using
100             that object to start with. This is particularly useful for converting
101             from one calendar to another:
102              
103             # Direct conversion from Discordian to ISO dates
104             my $disco = Date::Discordian->new( disco => '12 Chaos, YOLD 3177' );
105             my $iso = Date::ISO->new( $disco );
106             print $iso->iso;
107              
108             new() handles timezones. It defaults times to UTC (Greenwich
109             Mean Time, also called Zulu). If you want to set up a time
110             that's in the US "Pacific" timezone, which is GMT-8, use something
111             like:
112            
113             my $ical = Date::ICal->new( ical => '19971024T120000',
114             offset => "-0800");
115            
116             Note that as of version 1.44, new() tries to be intelligent
117             about figuring out your local time zone. If you enter
118             a time that's not *explicitly* in UTC, it looks at
119             the environment variable $TZ, if it exists, to determine
120             your local offset. If $TZ isn't set, new() will complain.
121              
122             =cut
123              
124             #}}}
125              
126             #{{{ sub new
127              
128             sub new {
129 557     557 1 277711 my $class = shift;
130 557         1115 my ( $self, %args, $sec, $min, $hour, $day, $month, $year, $tz );
131              
132             # $zflag indicates whether or not this time is natively in UTC
133 557         865 my $zflag = 0;
134              
135             # First argument can be a Date::ICal (or subclass thereof) object
136 557 50       1175 if ( ref $_[0] ) {
137 0         0 $args{ical} = $_[0]->ical;
138             } else {
139 557         1405 %args = @_;
140             }
141              
142 557         974 $self = {};
143              
144             # Date is specified as epoch#{{{
145 557 100       1522 if ( defined( $args{epoch} ) ) {
    100          
    100          
146              
147             ( $sec, $min, $hour, $day, $month, $year ) =
148 7         49 ( gmtime( $args{epoch} ) )[ 0, 1, 2, 3, 4, 5 ];
149 7         18 $year += 1900;
150 7         11 $month++;
151              
152 7         10 $zflag = 1; # epoch times are by definition in GMT
153             } #}}}
154              
155             # Date is specified as ical string#{{{
156             elsif ( defined( $args{ical} ) ) {
157              
158             # Timezone, if any
159 548         2952 $args{ical} =~ s/^(?:TZID=([^:]+):)?//;
160 548         1412 $tz = $1;
161              
162             # Split up ical string
163             ( $year, $month, $day, $hour, $min, $sec, $zflag ) =
164 548         3852 $args{ical} =~ /^(?:(\d{4})(\d\d)(\d\d))
165             (?:T(\d\d)?(\d\d)?(\d\d)?)?
166             (Z)?$/x;
167              
168             # TODO: figure out what to do if we get a TZID.
169             # I'd suggest we store it for use by modules that care
170             # about TZID names. But we don't want this module
171             # to deal with timezone names, only offsets, I think.
172             # --srl
173              
174             } #}}}
175              
176             # Time specified as components#{{{
177             elsif ( defined( $args{day} ) ) {
178              
179             # Choke if missing arguments
180 1         4 foreach my $attrib(qw(day month year )) {
181 3 50       9 warn "Attribute $attrib required" unless defined $args{$attrib};
182             }
183 1         2 foreach my $attrib(qw( hour min sec )) {
184 3 50       9 $args{$attrib} = 0 unless defined $args{$attrib};
185             }
186              
187             # And then just use what was passed in
188             ( $sec, $min, $hour, $day, $month, $year ) =
189 1         5 @args{ 'sec', 'min', 'hour', 'day', 'month', 'year' };
190              
191             } #}}}
192              
193             else { # Just use current gmtime#{{{
194              
195             # Since we are defaulting, this qualifies as UTC
196 1         2 $zflag = 1;
197              
198 1         7 ( $sec, $min, $hour, $day, $month, $year ) = ( gmtime(time) )[ 0 .. 5 ];
199 1         3 $year += 1900;
200 1         2 $month++;
201             } #}}}
202              
203 557         1518 $self->{julian} = greg2jd( $year, $month, $day );
204 557         1257 $self->{julsec} = time_as_seconds( $hour, $min, $sec );
205 557         1025 bless $self, $class;
206              
207 557 100       1427 if ( exists( $args{offset} ) ) {
    100          
208             # We should complain if they're trying to set a non-UTC
209             # offset on a time that's inherently UTC. -jv
210 9 100 100     38 if ($zflag && ($args{offset} != 0)) {
211             carp "Time had conflicting offset and UTC info. Using UTC"
212 1 50       7 unless $ENV{HARNESS_ACTIVE};
213             } else {
214              
215             # Set up the offset for this datetime.
216 8   100     33 $self->offset( $args{offset} || 0 );
217             }
218             } elsif ( !$zflag ) {
219              
220             # Check if the timezone has changed since the last time we checked.
221             # Apparently this happens on some systems. Patch from Mike
222             # Heins. Ask him.
223 14   50     57 my $tz = $ENV{TZ} || '0';
224 14 50       29 my $loc = $tz eq $localzone ? $localoffset : _calc_local_offset();
225 14 50       37 $self->offset($loc) if defined $self;
226             }
227              
228 557         1884 return $self;
229             }
230              
231             #}}}
232              
233             #{{{ sub ical
234              
235             =head2 ical
236              
237             $ical_string = $ical->ical;
238              
239             Retrieves, or sets, the date on the object, using any valid ICal date/time
240             string. Output is in UTC (ends with a "Z") by default. To get
241             output in localtime relative to the current machine, do:
242            
243             $ical_string = $ical->ical( localtime => 1 );
244              
245             To get output relative to an arbitrary offset, do:
246              
247             $ical_string = $ical->ical( offset => '+0545' );
248              
249             =cut
250              
251             sub ical {
252 543     543 1 3892 my $self = shift;
253 543 50       1275 if ( 1 & @_ ) { # odd number of parameters?
254 0         0 carp "Bad args: expected named parameter list";
255 0         0 shift; # avoid warning from %args=@_ assignment
256             }
257 543         1949 my %args = @_;
258 543         657 my $ical;
259              
260 543 100       1071 if ( exists $args{localtime} ) {
261             carp "can't have localtime and offset together, using localtime offset"
262 1 50       4 if exists $args{offset};
263              
264             # make output in localtime format by setting $args{offset}
265 1         3 $args{offset} = $self->offset;
266             }
267              
268 543 100       916 if ( exists $args{offset} ) {
269              
270             # make output based on an arbitrary offset
271             # No Z on the end!
272 1         2 my $julian = $self->{julian};
273 1         2 my $julsec = $self->{julsec};
274 1         4 my $adjust = offset_to_seconds( $args{offset} );
275 1         4 $self->add( seconds => $adjust );
276 1         10 $ical =
277             sprintf( '%04d%02d%02dT%02d%02d%02d', $self->year, $self->month,
278             $self->day, $self->hour, $self->minute, $self->second, );
279 1         3 $self->{julian} = $julian;
280 1         3 $self->{julsec} = $julsec;
281             } else {
282              
283             # make output in UTC by default
284             # if we were originally given this time in offset
285             # form, we'll need to adjust it for output
286 542 100 66     1143 if ( $self->hour || $self->min || $self->sec ) {
      66        
287 23         54 $ical =
288             sprintf( '%04d%02d%02dT%02d%02d%02dZ', $self->year, $self->month,
289             $self->day, $self->hour, $self->minute, $self->second );
290             } else {
291 519         944 $ical =
292             sprintf( '%04d%02d%02dZ', $self->year, $self->month, $self->day );
293             }
294             }
295              
296 543         2741 return $ical;
297             } #}}}
298              
299             #{{{ sub epoch
300              
301             =head2 epoch
302              
303             $epoch_time = $ical->epoch;
304            
305             $ical->epoch( 98687431 );
306              
307             Sets, or retrieves, the epoch time represented by the object, if it is
308             representable as such. (Dates before 1971 or after 2038 will not have an epoch
309             representation.)
310              
311             Internals note: The ICal representation of the date is considered the only
312             authoritative one. This means that we may need to reconstruct the epoch time
313             from the ICal representation if we are not sure that they are in synch. We'll
314             need to do clever things to keep track of when the two may not be in synch.
315             And, of course, the same will go for any subclasses of this class.
316              
317             =cut
318              
319             sub epoch {
320 7     7 1 36 my $self = shift;
321 7         12 my $class = ref($self);
322              
323 7         13 my $epoch;
324              
325 7 100       18 if ( $epoch = shift ) { # Passed in a new value
326              
327 1         5 my $newepoch = $class->new( epoch => $epoch );
328 1         5 $self->{julian} = $newepoch->{julian};
329 1         4 $self->{julsec} = $newepoch->{julsec};
330              
331             }
332              
333             else { # Calculate epoch from components, if possible
334              
335 6         17 $epoch =
336             timegm( $self->sec, $self->min, $self->hour, $self->day,
337             ( $self->month ) - 1, $self->year );
338             }
339              
340 7         285 return $epoch;
341             }
342              
343             #}}}
344              
345             #{{{ sub offset_to_seconds
346              
347             =head2 offset_to_seconds
348              
349             $seconds_plus_or_minus = offset_to_seconds($offset);
350            
351             Changes -0600 to -21600. Not object method, no side-effects.
352              
353             =cut
354              
355             sub offset_to_seconds {
356 27     27 1 73 my $offset = shift;
357              
358             # Relocated from offset for re-use
359 27         40 my $newoffset;
360              
361 27 100       76 if ( $offset eq '0' ) {
    100          
362 20         58 $newoffset = 0;
363             } elsif ( $offset =~ /^([+-])(\d\d)(\d\d)\z/ )
364             {
365 6         21 my ( $sign, $hours, $minutes ) = ( $1, $2, $3 );
366              
367             # convert to seconds, ignoring the possibility of leap seconds
368             # or daylight-savings-time shifts
369 6         16 $newoffset = $hours * 60 * 60 + $minutes * 60;
370 6 100       16 $newoffset *= -1 if $sign eq '-';
371             } else {
372 1         179 carp("You gave an offset, $offset, that makes no sense");
373 1         61 return undef;
374             }
375 26         45 return $newoffset;
376             }
377              
378             #}}}
379              
380             #{{{ sub offset_from_seconds
381              
382             =head2 offset_from_seconds
383              
384             $seconds_plus_or_minus = offset_from_seconds($offset_in_seconds);
385            
386             Changes -18000 (seconds) to -0600 (hours, minutes).
387             Not object method, no side-effects.
388              
389             =cut
390              
391             sub offset_from_seconds {
392 25     25 1 56 my $secoffset = shift;
393 25         35 my $hhmmoffset = 0;
394              
395 25 100       98 if ( $secoffset ne '0' ) {
396 13         27 my ( $sign, $secs ) = ( "", "" );
397 13         69 ( $sign, $secs ) = $secoffset =~ /([+-])?(\d+)/;
398              
399             # throw in a + to make this look like an offset if positive
400 13 100       33 $sign = "+" unless $sign;
401              
402             # NOTE: the following code will return "+0000" if you give it a number
403             # of seconds that are a multiple of a day. However, for speed reasons
404             # I'm not going to write in a comparison to reformat that back to 0.
405             #
406 13         35 my $hours = $secs / ( 60 * 60 );
407 13         20 $hours = $hours % 24;
408 13         22 my $mins = ( $secs % ( 60 * 60 ) ) / 60;
409 13         53 $hhmmoffset = sprintf( '%s%02d%02d', $sign, $hours, $mins );
410              
411             }
412              
413 25         78 return $hhmmoffset;
414             }
415              
416             #}}}
417              
418             #{{{ sub offset
419              
420             =head2 offset
421              
422             $offset = $ical->offset;
423            
424             # We need tests for these.
425             $ical->offset( '+1100' ); # a number of hours and minutes: UTC+11
426             $ical->offset( 0 ); # reset to UTC
427              
428             Sets or retrieves the offset from UTC for this time. This allows
429             timezone support, assuming you know what your local (or non-local)
430             UTC offset is. Defaults to 0.
431              
432             Internals note: all times are internally stored in UTC, even though they
433             may have some offset information. Offsets are internally stored in
434             signed integer seconds.
435              
436             BE CAREFUL about using this function on objects that were initialized
437             with an offset. If you started an object with:
438            
439             my $d = new(ical=>'19700101120000', offset=>'+0100');
440              
441             and you then call:
442              
443             $d->offset('+0200');
444            
445             you'll be saying "Yeah, I know I *said* it was in +0100, but really I
446             want it to be in +0200 now and forever." Which may be your intention,
447             if you're trying to transpose a whole set of dates to another timezone---
448             but you can also do that at the presentation level, with
449             the ical() method. Either way will work.
450              
451             =cut
452              
453             sub offset {
454 33     33 1 645 my ( $self, $offset ) = @_;
455 33         52 my $newoffset = undef;
456              
457 33 100       62 if ( defined($offset) ) { # Passed in a new value
458 26         50 $newoffset = offset_to_seconds($offset);
459              
460 26 100       75 unless ( defined $newoffset ) { return undef; }
  1         6  
461              
462             # since we're internally storing in GMT, we need to
463             # adjust the time we were given by the offset so that
464             # the internal date/time will be right.
465              
466 25 100       145 if ( $self->{offset} ) {
467              
468             # figure out whether there's a difference between
469             # the existing offset and the offset we were given.
470             # If so, adjust appropriately.
471 2         6 my $offsetdiff = $self->{offset} - $newoffset;
472              
473 2 50       6 if ($offsetdiff) {
474 2         3 $self->{offset} = $newoffset;
475 2         6 $self->add( seconds => $offsetdiff );
476             } else {
477              
478             # leave the offset the way it is
479             }
480             } else {
481 23         71 $self->add( seconds => -$newoffset );
482 23         42 $self->{offset} = $newoffset;
483             }
484              
485             } else {
486 7 100       16 if ( $self->{offset} ) {
487 5         11 $offset = offset_from_seconds( $self->{offset} );
488             } else {
489 2         3 $offset = 0;
490             }
491             }
492              
493 32         77 return $offset;
494             }
495              
496             #}}}
497              
498             # sub add {{{
499              
500             =head2 add
501              
502             $self->add( year => 3, month => 2, week => 1, day => 12,
503             hour => 1, min => 34, sec => 59 );
504             $date->add( duration => 'P1WT1H1M1S' ); # add 1 wk, 1 hr, 1 min, and 1 sec
505              
506             Adds a duration to a Date::ICal object.
507              
508             Supported paraters are: duration, eom_mode, year, month, week, day,
509             hour, min, sec or seconds.
510              
511             'duration' is a ICalendar duration string (see duration_value).
512              
513             If a value is undefined or omitted, 1 is assumed:
514              
515             $ical->add( 'min' ); # add a minute
516              
517             The result will be normalized. That is, the output time will have
518             meaningful values, rather than being 48:73 pm on the 34th of
519             hexadecember.
520              
521             Adding months or years can be done via three different methods,
522             specified by the eom_mode parameter, which then applies to all
523             additions (or subtractions) of months or years following it in the
524             parameter list.
525              
526             The default, eom_mode => 'wrap', means adding months or years that
527             result in days beyond the end of the new month will roll over into the
528             following month. For instance, adding one year to Feb 29 will result
529             in Mar 1.
530              
531             If you specify eom_mode => 'limit', the end of the month is never
532             crossed. Thus, adding one year to Feb 29, 2000 will result in Feb 28,
533             2001. However, adding three more years will result in Feb 28, 2004,
534             not Feb 29.
535              
536             If you specify eom_mode => 'preserve', the same calculation is done as
537             for 'limit' except that if the original date is at the end of the
538             month the new date will also be. For instance, adding one month to
539             Feb 29, 2000 will result in Mar 31, 2000.
540              
541             All additions are performed in the order specified. For instance,
542             with the default setting of eom_mode => 'wrap', adding one day and one
543             month to Feb 29 will result in Apr 1, while adding one month and one
544             day will result in Mar 30.
545              
546             =cut
547              
548             sub add {
549 561     561 1 1914 my $self = shift;
550 561 50       1193 carp "Date::ICal::add was called without an attribute arg" unless @_;
551             ( $self->{julian}, $self->{julsec}) =
552 561         1335 _add($self->{julian}, $self->{julsec}, @_);
553 561         1169 return $self;
554             }
555              
556             #}}}
557              
558             # sub _add {{{
559              
560             =begin internal
561              
562             Add (or subtract) to a date/time. First two parameters are
563             the jd and secs of the day. For the rest, see the add method.
564             Returns the adjusted jd and secs.
565              
566             =end internal
567              
568             =cut
569              
570             # for each unit, specify what it changes by (0=day, 1=second, 2=month)
571             # and by what factor
572              
573             %add_units = (year=>[2,12], month=>[2,1], week=>[0,7], day=>[0,1],
574             hour=>[1,3600], min=>[1,60], sec=>[1,1], seconds=>[1,1]);
575              
576             sub _add {
577 561     561   1191 my ($jd, $secs) = splice(@_, 0, 2);
578 561         840 my $eom_mode = 0;
579 561         777 my ($add, $unit, $count);
580              
581             # loop through unit=>count parameters
582 561         1563 while (($unit, $count) = splice(@_, 0, 2)) {
583              
584 573 100       1281 if ($unit eq 'duration') { # add a duration string
    50          
585 8         9 my %dur;
586 8         18 @dur{'day','sec','month'} = duration_value($count);
587              
588             # pretend these were passed to us as e.g. month=>1, day=>1, sec=>1.
589             # since months/years come first in the duration string, we
590             # put them first.
591 8 100       46 unshift @_, map $dur{$_} ? ($_,$dur{$_}) : (),
592             'month', 'day', 'sec';
593 8         33 next;
594             } elsif ($unit eq 'eom_mode') {
595 0 0       0 if ($count eq 'wrap') { $eom_mode = 0 }
  0 0       0  
    0          
596 0         0 elsif ($count eq 'limit') { $eom_mode = 1 }
597 0         0 elsif ($count eq 'preserve') { $eom_mode = 2 }
598 0         0 else { carp "Unrecognized eom_mode, $count, ignored" }
599             } else {
600 565 50       1706 unless ($add = $add_units{$unit}) {
601 0         0 carp "Unrecognized time unit, $unit, skipped";
602 0         0 next;
603             }
604              
605 565 50       1098 $count = 1 if !defined $count; # count defaults to 1
606 565         1208 $count *= $add->[1]; # multiply by the factor for this unit
607              
608 565 100       1323 if ($add->[0] == 0) { # add to days
    100          
609 21         65 $jd += $count;
610             } elsif ($add->[0] == 1) { # add to seconds
611 43         136 $secs += $count;
612             } else { # add to months
613 501         744 my ($y, $mo, $d);
614              
615 501         1148 _normalize_seconds( $jd, $secs );
616 501 50       805 if ($eom_mode == 2) { # sticky eom mode
617             # if it is the last day of the month, make it the 0th
618             # day of the following month (which then will normalize
619             # back to the last day of the new month).
620 0         0 ($y, $mo, $d) = jd2greg( $jd+1 );
621 0         0 --$d;
622             } else {
623 501         870 ($y, $mo, $d) = jd2greg( $jd );
624             }
625              
626 501 50 33     1307 if ($eom_mode && $d > 28) { # limit day to last of new month
627             # find the jd of the last day of our target month
628 0         0 $jd = greg2jd( $y, $mo+$count+1, 0 );
629              
630             # what day of the month is it? (discard year and month)
631 0         0 my $lastday = scalar jd2greg( $jd );
632              
633             # if our original day was less than the last day,
634             # use that instead
635 0 0       0 $jd -= $lastday - $d if $lastday > $d;
636             } else {
637 501         1008 $jd = greg2jd( $y, $mo+$count, $d );
638             }
639             }
640             }
641             }
642              
643 561         1156 _normalize_seconds( $jd, $secs );
644             }
645              
646             #}}}
647              
648             # sub add_overload {{{
649              
650             =head2 add_overload
651              
652             $date = $date1 + $duration;
653              
654             Where $duration is either a duration string, or a Date::ICal::Duration
655             object.
656              
657             $date += 'P2DT4H7M';
658              
659             Adds a duration to a date object. Returns a new object, or, in the case
660             of +=, modifies the existing object.
661              
662             =cut
663              
664             sub add_overload {
665 0     0 1 0 my $one = shift;
666 0         0 my $two = shift;
667              
668 0         0 my $ret = $one->clone;
669              
670 0 0       0 if ( ref $two ) {
671 0         0 $ret->add( duration => $two->as_ical );
672             } else {
673 0         0 $ret->add( duration => $two );
674             }
675              
676 0         0 return $ret;
677             } # }}}
678              
679             # sub _normalize_seconds {{{
680              
681             =begin internal
682              
683             ($jd, $secs) = _normalize_seconds( $jd, $secs );
684              
685             Corrects seconds that have gone into following or previous day(s).
686             Adjusts the passed days and seconds as well as returning them.
687              
688             =end internal
689              
690             =cut
691              
692             sub _normalize_seconds {
693 1062     1062   1349 my $adj;
694              
695 1062 100       1751 if ($_[1] < 0) {
696 3         11 $adj = int( ($_[1]-86399)/86400 );
697             } else {
698 1059         1871 $adj = int( $_[1]/86400 );
699             }
700 1062         2715 ($_[0] += $adj), ($_[1] -= $adj*86400);
701             }
702              
703             #}}}
704              
705             # sub duration_value {{{
706              
707             =head2 duration_value
708              
709             Given a duration string, this function returns the number of days,
710             seconds, and months represented by that duration. In that order. Seems
711             odd to me. This should be considered an internal function, and you
712             should expect the API to change in the very near future.
713              
714             =cut
715              
716             sub duration_value {
717 8     8 1 22 my $str = shift;
718              
719 8         66 my @temp = $str =~ m{
720             ([\+\-])? (?# Sign)
721             (P) (?# 'P' for period? This is our magic character)
722             (?:
723             (?:(\d+)Y)? (?# Years)
724             (?:(\d+)M)? (?# Months)
725             (?:(\d+)W)? (?# Weeks)
726             (?:(\d+)D)? (?# Days)
727             )?
728             (?:T (?# Time prefix)
729             (?:(\d+)H)? (?# Hours)
730             (?:(\d+)M)? (?# Minutes)
731             (?:(\d+)S)? (?# Seconds)
732             )?
733             }x;
734 8         30 my ( $sign, $magic ) = @temp[ 0 .. 1 ];
735             my ( $years, $months, $weeks, $days, $hours, $mins, $secs ) =
736 8 100       24 map { defined($_) ? $_ : 0 } @temp[ 2 .. $#temp ];
  56         108  
737              
738 8 50       22 unless ( defined($magic) ) {
739 0         0 carp "Invalid duration: $str";
740 0         0 return undef;
741             }
742 8 100 66     25 $sign = ( ( defined($sign) && $sign eq '-' ) ? -1 : 1 );
743              
744 8         20 my $s = $sign * ( $secs + ( $mins * 60 ) + ( $hours * 3600 ) );
745 8         12 my $d = $sign * ( $days + ( $weeks * 7 ) );
746 8         14 my $m = $sign * ( $months + ( $years * 12 ) );
747 8         29 return ( $d, $s, $m );
748             }
749              
750             #}}}
751              
752             # sub subtract {{{
753              
754             =head2 subtract
755              
756             $duration = $date1 - $date2;
757              
758             Subtract one Date::ICal object from another to give a duration - the
759             length of the interval between the two dates. The return value is a
760             Date::ICal::Duration object (qv) and allows you to get at each of the
761             individual components, or the entire duration string:
762              
763             $d = $date1 - $X;
764              
765             Note that $X can be any of the following:
766              
767             If $X is another Date::ICal object (or subclass thereof) then $d will be
768             a Date::ICal::Duration object.
769              
770             $week = $d->weeks; # how many weeks apart?
771             $days = $d->as_days; # How many days apart?
772              
773             If $X is a duration string, or a Date::ICal::Diration object, then $d
774             will be an object in the same class as $date1;
775              
776             $newdate = $date - $duration;
777              
778             =cut
779              
780             sub subtract {
781 7     7 1 55 my ( $date1, $date2, $reversed ) = @_;
782 7         9 my $dur;
783              
784             # If the order of the arguments was reversed, overload tells us
785             # about it in the third argument.
786 7 50       14 if ($reversed) {
787 0         0 ( $date2, $date1 ) = ( $date1, $date2 );
788             }
789              
790 7 100 66     40 if (ref $date1 && ref $date2) {
    50 33        
791             # If $date1 is a Date::ICal object, and $date2 is a Duration object,
792             # then we should subtract and get a date.
793 5 100       11 if ((ref $date2) eq 'Date::ICal::Duration') {
794 2         6 my $seconds = $date2->as_seconds;
795 2         7 my $ret = $date1->clone;
796 2         8 $ret->add( seconds => -1 * $seconds );
797 2         20 return $ret;
798              
799             } else {
800             # If $date2 is a Date::ICal object, or some class thereof, we should
801             # subtract and get a duration
802              
803 3         10 my $days = $date1->{julian} - $date2->{julian};
804 3         5 my $secs = $date1->{julsec} - $date2->{julsec};
805              
806 3         14 return Date::ICal::Duration->new(
807             days => $days,
808             seconds => $secs
809             );
810             }
811             } elsif ( ref $date1 &&
812             ( $dur = Date::ICal::Duration->new( ical => $date2 ) )
813             ) {
814             # If $date1 is a Date::ICal object, and $date2 is a duration string,
815             # we should subtract and get a date
816 2         11 return $date1 - $dur; # Is that cheating?
817              
818             # Otherwise, return undef
819             } else {
820 0         0 warn "Invalid arguments. You can subtract a date from a date, or a duration from a date";
821 0         0 return;
822             }
823              
824             } # }}}
825              
826             # sub clone {{{
827              
828             =head2 clone
829              
830             $copy = $date->clone;
831              
832             Returns a replica of the date object, including all attributes.
833              
834             =cut
835              
836             sub clone {
837 2     2 1 3 my $self = shift;
838 2         4 my $class = ref $self;
839 2         7 my %hash = %$self;
840 2         5 my $new = \%hash;
841 2         4 bless $new, $class;
842 2         4 return $new;
843             } # }}}
844              
845             # sub compare {{{
846              
847             =head2 compare
848              
849             $cmp = $date1->compare($date2);
850              
851             @dates = sort {$a->compare($b)} @dates;
852              
853             Compare two Date::ICal objects. Semantics are compatible with
854             sort; returns -1 if $a < $b, 0 if $a == $b, 1 if $a > $b.
855              
856             =cut
857              
858             sub compare {
859 13     13 1 83 my ( $self, $otherdate ) = (@_);
860              
861 13 50       25 unless ( defined($otherdate) ) { return undef }
  0         0  
862              
863             # One or more days different
864              
865 13 100       49 if ( $self->{julian} < $otherdate->{julian} ) {
    100          
    100          
    100          
866 3         17 return -1;
867             } elsif ( $self->{julian} > $otherdate->{julian} )
868             {
869 3         12 return 1;
870              
871             # They are the same day
872             } elsif ( $self->{julsec} < $otherdate->{julsec} )
873             {
874 3         18 return -1;
875             } elsif ( $self->{julsec} > $otherdate->{julsec} )
876             {
877 3         18 return 1;
878             }
879              
880             # # if we got all this way and haven't yet returned, the units are equal.
881 1         3 return 0;
882             }
883              
884             #}}}
885              
886             # internal stuff {{{
887              
888             =begin internal
889              
890             @months = months($year);
891              
892             Returns the Julian day at the end of a month, correct for that year.
893              
894             =end internal
895              
896             =cut
897              
898             # precalculate these values at module load time so that we don't
899             # have to do it repeatedly during runtime.
900             #
901             BEGIN {
902              
903             # + 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
904 11     11   30647 @months = ( 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365 );
905 11         119 @leapmonths = @months;
906              
907 11         60 for ( 2 .. 12 ) {
908 121         3039 $leapmonths[$_] = $months[$_] + 1;
909             }
910             }
911              
912             sub months {
913 0 0   0 0 0 return Date::Leapyear::isleap(shift) ? @leapmonths : @months;
914             }
915              
916             =begin internal
917              
918             time_as_seconds( $args{hour}, $args{min}, $args{sec} );
919              
920             Returns the time of day as the number of seconds in the day.
921              
922             =end internal
923              
924             =cut
925              
926             # }}}
927              
928             # sub time_as_seconds {{{
929              
930             sub time_as_seconds {
931 557     557 0 993 my ( $hour, $min, $sec ) = @_;
932              
933 557   100     2191 $hour ||= 0;
934 557   100     1835 $min ||= 0;
935 557   100     1742 $sec ||= 0;
936              
937 557         1040 my $secs = $hour * 3600 + $min * 60 + $sec;
938 557         956 return $secs;
939             } #}}}
940              
941             # sub day {{{
942              
943             =head2 day
944              
945             my $day = $date->day;
946              
947             Returns the day of the month.
948              
949             Day is in the range 1..31
950              
951             =cut
952              
953             sub day {
954 559     559 1 752 my $self = shift;
955 559         1028 return ( jd2greg( $self->{julian} ) )[2];
956             } # }}}
957              
958             # sub month {{{
959              
960             =head2 month
961              
962             my $month = $date->month;
963              
964             Returns the month of the year.
965              
966             Month is returned as a number in the range 1..12
967              
968             =cut
969              
970             sub month {
971 562     562 1 826 my $self = shift;
972 562         940 return ( jd2greg( $self->{julian} ) )[1];
973             } # }}}
974              
975             # sub mon {{{
976              
977 0     0 0 0 sub mon { return month(@_); }
978              
979             #}}}
980              
981             # sub year {{{
982              
983             =head2 year
984              
985             my $year = $date->year;
986              
987             Returns the year.
988              
989             =cut
990              
991             sub year {
992 558     558 1 838 my $self = shift;
993 558         1022 return ( jd2greg( $self->{julian} ) )[0];
994             } # }}}
995              
996             # sub jd2greg {{{
997              
998             =head2 jd2greg
999              
1000             ($year, $month, $day) = jd2greg( $jd );
1001              
1002             Convert number of days on or after Jan 1, 1 CE (Gregorian) to
1003             gregorian year,month,day.
1004              
1005             =cut
1006              
1007             sub jd2greg {
1008 11     11   5776 use integer;
  11         170  
  11         79  
1009 232619     232619 1 753164 my $d = shift;
1010 232619         271987 my $yadj = 0;
1011 232619         283594 my ( $c, $y, $m );
1012              
1013             # add 306 days to make relative to Mar 1, 0; also adjust $d to be within
1014             # a range (1..2**28-1) where our calculations will work with 32bit ints
1015 232619 100       435623 if ( $d > 2**28 - 307 ) {
    100          
1016              
1017             # avoid overflow if $d close to maxint
1018 1         5 $yadj = ( $d - 146097 + 306 ) / 146097 + 1;
1019 1         2 $d -= $yadj * 146097 - 306;
1020             } elsif ( ( $d += 306 ) <= 0 )
1021             {
1022 115210         153227 $yadj =
1023             -( -$d / 146097 + 1 ); # avoid ambiguity in C division of negatives
1024 115210         144944 $d -= $yadj * 146097;
1025             }
1026              
1027             $c =
1028 232619         295920 ( $d * 4 - 1 ) / 146097; # calc # of centuries $d is after 29 Feb of yr 0
1029 232619         289366 $d -= $c * 146097 / 4; # (4 centuries = 146097 days)
1030 232619         278319 $y = ( $d * 4 - 1 ) / 1461; # calc number of years into the century,
1031 232619         288642 $d -= $y * 1461 / 4; # again March-based (4 yrs =~ 146[01] days)
1032 232619         293038 $m =
1033             ( $d * 12 + 1093 ) / 367; # get the month (3..14 represent March through
1034 232619         287435 $d -= ( $m * 367 - 1094 ) / 12; # February of following year)
1035 232619         301248 $y += $c * 100 + $yadj * 400; # get the real year, which is off by
1036 232619 100       364487 ++$y, $m -= 12 if $m > 12; # one if month is January or February
1037 232619         422682 return ( $y, $m, $d );
1038             } #}}}
1039              
1040             # sub greg2jd {{{
1041              
1042             =head2 greg2jd
1043              
1044             $jd = greg2jd( $year, $month, $day );
1045              
1046             Convert gregorian year,month,day to days on or after Jan 1, 1 CE
1047             (Gregorian). Normalization is performed (e.g. month of 28 means
1048             April two years after given year) for month < 1 or > 12 or day < 1
1049             or > last day of month.
1050              
1051             =cut
1052              
1053             sub greg2jd {
1054 11     11   1887 use integer;
  11         24  
  11         67  
1055 231501     231501 1 1322846 my ( $y, $m, $d ) = @_;
1056 231501         278731 my $adj;
1057              
1058             # make month in range 3..14 (treat Jan & Feb as months 13..14 of prev year)
1059 231501 100       421876 if ( $m <= 2 ) {
    100          
1060 38722         49896 $y -= ( $adj = ( 14 - $m ) / 12 );
1061 38722         47718 $m += 12 * $adj;
1062             } elsif ( $m > 14 )
1063             {
1064 400         599 $y += ( $adj = ( $m - 3 ) / 12 );
1065 400         583 $m -= 12 * $adj;
1066             }
1067              
1068             # make year positive (oh, for a use integer 'sane_div'!)
1069 231501 100       357324 if ( $y < 0 ) {
1070 115214         150781 $d -= 146097 * ( $adj = ( 399 - $y ) / 400 );
1071 115214         141543 $y += 400 * $adj;
1072             }
1073              
1074             # add: day of month, days of previous 0-11 month period that began w/March,
1075             # days of previous 0-399 year period that began w/March of a 400-multiple
1076             # year), days of any 400-year periods before that, and 306 days to adjust
1077             # from Mar 1, year 0-relative to Jan 1, year 1-relative (whew)
1078              
1079 231501         500777 $d += ( $m * 367 - 1094 ) / 12 + $y % 100 * 1461 / 4 +
1080             ( $y / 100 * 36524 + $y / 400 ) - 306;
1081             } # }}}
1082              
1083             # sub days_this_year {{{
1084              
1085             =head2 days_this_year
1086              
1087             $yday = Date::ICal::days_this_year($day, $month, $year);
1088              
1089             Returns the number of days so far this year. Analogous to the yday
1090             attribute of gmtime (or localtime) except that it works outside of the
1091             epoch.
1092              
1093             =cut
1094              
1095             sub days_this_year {
1096 0     0 1 0 my ( $d, $m, $y ) = @_;
1097 0         0 my @mlist = &months($y);
1098 0         0 return $mlist[$m - 1] + $d - 1;
1099             } #}}}
1100              
1101             # sub day_of_week {{{
1102              
1103             =head2 day_of_week
1104              
1105             my $day_of_week = $date->day_of_week
1106              
1107             Returns the day of week as 0..6 (0 is Sunday, 6 is Saturday).
1108              
1109             =cut
1110              
1111             sub day_of_week {
1112 0     0 1 0 my $self = shift;
1113 0         0 return $self->{julian} % 7;
1114             } #}}}
1115              
1116             # sub hour {{{
1117              
1118             =head2 hour
1119              
1120             my $hour = $date->hour
1121              
1122             Returns the hour of the day.
1123              
1124             Hour is in the range 0..23
1125              
1126             =cut
1127              
1128             sub hour {
1129 582     582 1 826 my $self = shift;
1130 582         1095 return ( $self->parsetime )[2];
1131             } # }}}
1132              
1133             # sub min {{{
1134              
1135             =head2 min
1136              
1137             my $min = $date->min;
1138              
1139             Returns the minute.
1140              
1141             Minute is in the range 0..59
1142              
1143             =cut
1144              
1145             sub min {
1146 557     557 1 755 my $self = shift;
1147 557         935 return ( $self->parsetime )[1];
1148             }
1149              
1150 30     30 0 59 sub minute { return min(@_); }
1151              
1152             # }}}
1153              
1154             # sub sec {{{
1155              
1156             =head2 sec
1157              
1158             my $sec = $date->sec;
1159              
1160             Returns the second.
1161              
1162             Second is in the range 0..60. The value of 60 is (maybe) needed for
1163             leap seconds. But I'm not sure if we're going to go there.
1164              
1165             =cut
1166              
1167             sub sec {
1168 555     555 1 830 my $self = shift;
1169 555         881 return ( $self->parsetime )[0];
1170             }
1171              
1172 27     27 0 59 sub second { return sec(@_); }
1173              
1174             # }}}
1175              
1176             # sub parsetime {{{
1177              
1178             =begin internal
1179              
1180             ( $sec, $min, $hour ) = parsetime( $seconds );
1181              
1182             Given the number of seconds so far today, returns the seconds,
1183             minutes, and hours of the current time.
1184              
1185             =end internal
1186              
1187             =cut
1188              
1189             sub parsetime {
1190 1694     1694 0 2136 my $self = shift;
1191 1694         2395 my $time = $self->{julsec};
1192              
1193 1694         2466 my $hour = int( $time / 3600 );
1194 1694         2268 $time -= $hour * 3600;
1195              
1196 1694         2309 my $min = int( $time / 60 );
1197 1694         2103 $time -= $min * 60;
1198              
1199 1694         5303 return ( int($time), $min, $hour );
1200             } # }}}
1201              
1202             # sub julian/jd #{{{
1203              
1204             =head2 julian
1205              
1206             my $jd = $date->jd;
1207              
1208             Returns a listref, containing two elements. The date as a julian day,
1209             and the time as the number of seconds since midnight. This should not
1210             be thought of as a real julian day, because it's not. The module is
1211             internally consistent, and that's enough.
1212              
1213             This method really only is here for compatibility with previous
1214             versions, as the jd method is now thrown over for plain hash references.
1215              
1216             See the file INTERNALS for more information about this internal
1217             format.
1218              
1219             =cut
1220              
1221             sub jd {
1222 0     0 0 0 my $self = shift;
1223              
1224 0 0       0 if ( my $jd = shift ) {
1225 0         0 ( $self->{julian}, $self->{julsec} ) = @$jd;
1226             }
1227              
1228 0         0 return [ $self->{julian}, $self->{julsec} ];
1229             }
1230              
1231 0     0 1 0 sub julian { return jd(@_) }
1232              
1233             # INTERNAL ONLY: figures out what the UTC offset (in HHMM) is
1234             # is for the current machine.
1235             sub _calc_local_offset {
1236              
1237 11     11   5627 use Time::Local;
  11         30  
  11         1481  
1238 11     11   92 my @t = gmtime;
1239              
1240 11         49 my $local = timelocal(@t);
1241 11         1266 my $gm = timegm(@t);
1242              
1243 11         256 my $secdiff = $gm - $local;
1244 11         104 return offset_from_seconds($secdiff);
1245             }
1246              
1247             #}}}
1248              
1249             1;
1250              
1251             # More docs {{{
1252              
1253             =head1 TODO
1254              
1255             =over 4
1256              
1257             =item - add gmtime and localtime methods, perhaps?
1258              
1259             =item - Fix the INTERNALS file so that it actually reflects reality
1260              
1261             =back
1262              
1263             =head1 INTERNALS
1264              
1265             Please see the file INTERNALS for discussion on the internals.
1266              
1267             =head1 AUTHOR
1268              
1269             Rich Bowen (DrBacchus) rbowen@rcbowen.com
1270              
1271             And the rest of the Reefknot team. See the source for a full
1272             list of patch contributors and version-by-version notes.
1273              
1274             =head1 SEE ALSO
1275              
1276             datetime@perl.org mailing list
1277              
1278             L
1279              
1280             Time::Local
1281              
1282             Net::ICal
1283              
1284             =cut
1285              
1286             #}}}
1287