File Coverage

blib/lib/Date/ICal.pm
Criterion Covered Total %
statement 266 306 86.9
branch 95 128 74.2
condition 22 31 70.9
subroutine 38 45 84.4
pod 23 30 76.6
total 444 540 82.2


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