File Coverage

blib/lib/Date/ICal/Duration.pm
Criterion Covered Total %
statement 149 156 95.5
branch 53 68 77.9
condition 29 33 87.8
subroutine 20 20 100.0
pod 11 11 100.0
total 262 288 90.9


line stmt bran cond sub pod time code
1             package Date::ICal::Duration;
2              
3 12     12   70667 use strict;
  12         31  
  12         379  
4 12     12   67 use Carp;
  12         23  
  12         638  
5              
6 12     12   72 use vars qw($VERSION );
  12         31  
  12         23906  
7             $VERSION = (qw'$Revision: 1.61 $')[1];
8              
9             # Documentation {{{
10              
11             =head1 NAME
12              
13             Date::ICal::Duration - durations in iCalendar format, for math purposes.
14              
15             =head1 VERSION
16              
17             $Revision: 1.61 $
18              
19             =head1 SYNOPSIS
20              
21             use Date::ICal::Duration;
22              
23             $d = Date::ICal::Duration->new( ical => '-P1W3DT2H3M45S' );
24              
25             $d = Date::ICal::Duration->new( weeks => 1,
26             days => 1,
27             hours => 6,
28             minutes => 15,
29             seconds => 45);
30              
31             # a one hour duration, without other components
32             $d = Date::ICal::Duration->new( seconds => "3600");
33              
34             # Read-only accessors:
35             $d->weeks;
36             $d->days;
37             $d->hours;
38             $d->minutes;
39             $d->seconds;
40             $d->sign;
41              
42             # TODO: Resolve sign() discussion from rk-devel and update synopsis.
43            
44             $d->as_seconds (); # returns just seconds
45             $d->as_elements (); # returns a hash of elements, like the accessors above
46             $d->as_ical(); # returns an iCalendar duration string
47            
48             =head1 DESCRIPTION
49              
50             This is a trivial class for representing duration objects, for doing math
51             in Date::ICal
52              
53             =head1 AUTHOR
54              
55             Rich Bowen, and the Reefknot team. Alas, Reefknot is no more. See
56             L or L for more modern modules.
57              
58             Last touched by $Author: rbowen $
59              
60             =head1 METHODS
61              
62             Date::ICal::Duration has the following methods available:
63              
64             =head2 new
65              
66             A new Date::ICal::Duration object can be created with an iCalendar string :
67              
68             my $ical = Date::ICal::Duration->new ( ical => 'P3W2D' );
69             # 3 weeks, 2 days, positive direction
70             my $ical = Date::ICal::Duration->new ( ical => '-P6H3M30S' );
71             # 6 hours, 3 minutes, 30 seconds, negative direction
72            
73             Or with a number of seconds:
74              
75             my $ical = Date::ICal::Duration->new ( seconds => "3600" );
76             # one hour positive
77              
78             Or, better still, create it with components
79              
80             my $date = Date::ICal::Duration->new (
81             weeks => 6,
82             days => 2,
83             hours => 7,
84             minutes => 15,
85             seconds => 47,
86             sign => "+"
87             );
88              
89             The sign defaults to "+", but "+" and "-" are legal values.
90            
91             =cut
92              
93             #}}}
94              
95             #{{{ sub new
96              
97             sub new {
98 11     11 1 3687 my ($class, %args) = @_;
99 11         22 my $verified = {};
100 11         18 my $self = {};
101 11         23 bless $self, $class;
102              
103 11         17 my $seconds_only = 1; # keep track of whether we were given length in seconds only
104 11 100       30 $seconds_only = 0 unless (defined $args{'seconds'});
105              
106             # If one of the attributes is negative, then they all must be
107             # negative. Otherwise, we're not sure what this means.
108 11         24 foreach (qw(hours minutes seconds days weeks)) {
109 55 100       113 if (defined($args{$_}) ) {
110             # make sure this argument is all digits, optional - sign
111 13 50       68 if ($args{$_} =~ m/-?[0-9]+$/) {
112 13 100       31 if ($args{$_} < 0) {
113 6         14 $args{sign} = '-';
114 6         13 $args{$_} = abs($args{$_});
115             }
116 13         29 $verified->{$_} = $args{$_};
117 13 100       32 unless ($_ eq 'seconds') {
118 7         12 $seconds_only = 0;
119             }
120             } else {
121 0         0 carp ("Parameter $_ contains non-numeric value " . $args{$_} . "\n");
122             }
123             }
124             }
125              
126 11 100       27 if (defined ($args{sign}) ) {
127              
128             # make sure this argument + or -
129 2 50       11 if ($args{sign} =~ m/[+-]/) {
130             # if so, assign it
131 2 50       10 $self->{sign} = ($args{sign} eq "+") ? 1 : -1;
132 2 50       7 $verified->{sign} = ($args{sign} eq "+") ? '+' : '-';
133             } else {
134             carp ("Parameter sign contains a value other than + or - : "
135 0         0 . $args{sign} . "\n");
136             }
137            
138             }
139              
140             # If a number is given, convert it to hours, minutes, and seconds,
141             # but *don't* extract days -- we want it to represent an absolute
142             # amount of time, regardless of timezone
143 11 100       35 if ($seconds_only) { # if we were given an integer time_t
    100          
    50          
144 1         7 $self->_set_from_seconds($args{'seconds'});
145             } elsif (defined ($args{'ical'}) ) {
146             # A standard duration string
147             #warn "setting from ical\n";
148 4         19 $self->_set_from_ical($args{'ical'});
149             } elsif (not $seconds_only) {
150             #warn "setting from components";
151             #use Data::Dumper; warn Dumper $verified;
152 6         13 $self->_set_from_components($verified);
153             }
154            
155 11 100       36 return undef unless %args;
156            
157 10         51 return $self;
158             }
159              
160             #}}}
161              
162             # Accessors {{{
163              
164             =head2 sign, weeks, days, hours, minutes, seconds
165              
166             Read-only accessors for the elements of the object.
167              
168             =cut
169              
170             #}}}
171              
172             # {{{ sub sign
173              
174             sub sign {
175 2     2 1 13 my ($self) = @_;
176 2         9 return $self->{sign};
177             }
178              
179             #}}}
180              
181             # {{{ sub weeks
182              
183             sub weeks {
184 5     5 1 15 my ($self) = @_;
185 5         10 my $w = ${$self->_wd}[0];
  5         10  
186 5 100       19 return unless $w;
187 3         21 return $self->{sign} * $w;
188             }
189              
190             #}}}
191              
192             # {{{ sub days
193              
194             sub days {
195 4     4 1 12 my ($self) = @_;
196 4         10 my $d = ${$self->_wd}[1];
  4         10  
197 4 50       16 return unless $d;
198 4         16 return $self->{sign} * $d;
199              
200             } #}}}
201              
202             #{{{ sub hours
203              
204             sub hours {
205 4     4 1 34 my ($self) = @_;
206 4         7 my $h = ${$self->_hms}[0];
  4         10  
207 4 50       13 return unless $h;
208 4         51 return $self->{sign} * $h;
209             }
210              
211             #}}}
212              
213             # {{{ sub minutes
214              
215             sub minutes {
216 4     4 1 26 my ($self) = @_;
217 4         7 my $m = ${$self->_hms}[1];
  4         12  
218 4 50       20 return unless $m;
219 4         26 return $self->{sign} * $m;
220             }
221              
222             #}}}
223              
224             # {{{ sub seconds
225              
226             sub seconds {
227 4     4 1 20 my ($self) = @_;
228 4         7 my $s = ${$self->_hms}[2];
  4         10  
229 4 50       13 return unless $s;
230 4         21 return $self->{sign} * $s;
231             }
232              
233             #}}}
234              
235             # sub as_seconds {{{
236              
237             =head2 as_seconds
238              
239             Returns the duration in raw seconds.
240              
241             WARNING -- this folds in the number of days, assuming that they are always 86400
242             seconds long (which is not true twice a year in areas that honor daylight
243             savings time). If you're using this for date arithmetic, consider using the
244             I method from a L object, as this will behave better.
245             Otherwise, you might experience some error when working with times that are
246             specified in a time zone that observes daylight savings time.
247              
248              
249             =cut
250              
251             sub as_seconds {
252 6     6 1 1461 my ($self) = @_;
253              
254 6   50     25 my $nsecs = $self->{nsecs} || 0;
255 6   100     23 my $ndays = $self->{ndays} || 0;
256 6   50     19 my $sign = $self->{sign} || 1;
257 6         30 return $sign*($nsecs+($ndays*24*60*60));
258             }
259              
260             #}}}
261              
262             # sub as_days {{{
263              
264             =head2 as_days
265              
266             $days = $duration->as_days;
267              
268             Returns the duration as a number of days. Not to be confused with the
269             C method, this method returns the total number of days, rather
270             than mod'ing out the complete weeks. Thus, if we have a duration of 33
271             days, C will return 4, C will return 5, but C will
272             return 33.
273              
274             Note that this is a lazy convenience function which is just weeks*7 +
275             days.
276              
277             =cut
278              
279             sub as_days {
280 3     3 1 42 my ($self) = @_;
281 3         11 my $wd = $self->_wd;
282 3         18 return $self->{sign} * ( $wd->[0]*7 + $wd->[1] );
283             }# }}}
284              
285             #{{{ sub as_ical
286              
287             =head2 as_ical
288              
289             Return the duration in an iCalendar format value string (e.g., "PT2H0M0S")
290              
291             =cut
292              
293             sub as_ical {
294 7     7 1 1648 my ($self) = @_;
295              
296 7         11 my $tpart = '';
297              
298 7 50       20 if (my $ar_hms = $self->_hms) {
299 7         33 $tpart = sprintf('T%dH%dM%dS', @$ar_hms);
300             }
301              
302 7         15 my $ar_wd = $self->_wd();
303            
304 7         16 my $dpart = '';
305 7 100       18 if (defined $ar_wd) {
306 5         11 my ($weeks, $days) = @$ar_wd;
307 5 100 66     24 if ($weeks && $days) {
    50          
308 2         7 $dpart = sprintf('%dW%dD', $weeks, $days);
309             } elsif ($weeks) { # (if days = 0)
310 0         0 $dpart = sprintf('%dW', $weeks);
311             } else {
312 3         11 $dpart = sprintf('%dD', $days);
313             }
314             }
315              
316             # put a sign in the return value if necessary
317 7 100       27 my $value = join('', (($self->{sign} < 0) ? '-' : ''),
318             'P', $dpart, $tpart);
319              
320             # remove any zero components from the time string (-P10D0H -> -P10D)
321 7         39 $value =~ s/(?<=[^\d])0[WDHMS]//g;
322              
323             # return either the time value or PT0S (if the time value is zero).
324 7 100       60 return (($value !~ /PT?$/) ? $value : 'PT0S');
325             }
326              
327             #}}}
328              
329             #{{{ sub as_elements
330              
331             =head2 as_elements
332              
333             Returns the duration as a hashref of elements.
334              
335             =cut
336              
337             sub as_elements {
338 1     1 1 6 my ($self) = @_;
339            
340             # get values for all the elements
341 1         2 my $wd = $self->_wd;
342 1         5 my $hms = $self->_hms;
343            
344             my $return = {
345             sign => $self->{sign},
346 1         3 weeks => ${$wd}[0],
347 1         2 days => ${$wd}[1],
348 1         2 hours => ${$hms}[0],
349 1         2 minutes => ${$hms}[1],
350 1         2 seconds => ${$hms}[2],
  1         3  
351             };
352 1         3 return $return;
353             }
354              
355             #}}}
356              
357             # INTERNALS {{{
358              
359             =head1 INTERNALS
360              
361             head2 GENERAL MODEL
362              
363             Internally, we store 3 data values: a number of days, a number of seconds (anything
364             shorter than a day), and a sign (1 or -1). We are assuming that a day is 24 hours for
365             purposes of this module; yes, we know that's not completely accurate because of
366             daylight-savings-time switchovers, but it's mostly correct. Suggestions are welcome.
367              
368             NOTE: The methods below SHOULD NOT be relied on to stay the same in future versions.
369              
370             =head2 _set_from_ical ($self, $duration_string)
371              
372             Converts a RFC2445 DURATION format string to the internal storage format.
373              
374             =cut
375              
376             #}}}
377              
378             # sub _set_from_ical (internal) {{{
379              
380             sub _set_from_ical {
381 4     4   28 my ($self, $str) = @_;
382              
383 4         10 my $parsed_values = _parse_ical_string($str);
384            
385 4         10 return $self->_set_from_components($parsed_values);
386             } # }}}
387              
388             # sub _parse_ical_string (internal) {{{
389              
390             =head2 _parse_ical_string ($string)
391              
392             Regular expression for parsing iCalendar into usable values.
393              
394             =cut
395              
396             sub _parse_ical_string {
397 7     7   1651 my ($str) = @_;
398            
399             # RFC 2445 section 4.3.6
400             #
401             # dur-value = (["+"] / "-") "P" (dur-date / dur-time / dur-week)
402             # dur-date = dur-day [dur-time]
403             # dur-time = "T" (dur-hour / dur-minute / dur-second)
404             # dur-week = 1*DIGIT "W"
405             # dur-hour = 1*DIGIT "H" [dur-minute]
406             # dur-minute = 1*DIGIT "M" [dur-second]
407             # dur-second = 1*DIGIT "S"
408             # dur-day = 1*DIGIT "D"
409              
410 7         76 my ($sign_str, $magic, $weeks, $days, $hours, $minutes, $seconds) =
411             $str =~ m{
412             ([\+\-])? (?# Sign)
413             (P) (?# 'P' for period? This is our magic character)
414             (?:
415             (?:(\d+)W)? (?# Weeks)
416             (?:(\d+)D)? (?# Days)
417             )?
418             (?:T (?# Time prefix)
419             (?:(\d+)H)? (?# Hours)
420             (?:(\d+)M)? (?# Minutes)
421             (?:(\d+)S)? (?# Seconds)
422             )?
423             }x;
424              
425 7 50       67 if (!defined($magic)) {
426 0         0 carp "Invalid duration: $str";
427 0         0 return undef;
428             }
429              
430             # make sure the sign gets set, and turn it into an integer multiplier
431 7   50     35 $sign_str ||= "+";
432 7 50       21 my $sign = ($sign_str eq "-") ? -1 : 1;
433            
434 7         29 my $return = {};
435 7         24 $return->{'weeks'} = $weeks;
436 7         15 $return->{'days'} = $days;
437 7         12 $return->{'hours'} = $hours;
438 7         15 $return->{'minutes'} = $minutes;
439 7         10 $return->{'seconds'} = $seconds;
440 7         12 $return->{'sign'} = $sign;
441              
442 7         18 return $return;
443             } # }}}
444              
445             # sub _set_from_components (internal) {{{
446              
447             =head2 _set_from_components ($self, $hashref)
448              
449             Converts from a hashref to the internal storage format.
450             The hashref can contain elements "sign", "weeks", "days", "hours", "minutes", "seconds".
451              
452             =cut
453              
454             sub _set_from_components {
455 10     10   22 my ($self, $args) = @_;
456              
457             # Set up some easier-to-read variables
458 10         17 my ($sign, $weeks, $days, $hours, $minutes, $seconds);
459 10         17 $sign = $args->{'sign'};
460 10         16 $weeks = $args->{'weeks'};
461 10         17 $days = $args->{'days'};
462 10         16 $hours = $args->{'hours'};
463 10         15 $minutes = $args->{'minutes'};
464 10         17 $seconds = $args->{'seconds'};
465            
466 10 100 100     45 $self->{sign} = (defined($sign) && $sign eq '-') ? -1 : 1;
467              
468 10 100 100     42 if (defined($weeks) or defined($days)) {
469 7   100     37 $self->_wd([$weeks || 0, $days || 0]);
      100        
470             }
471              
472 10 100 100     49 if (defined($hours) || defined($minutes) || defined($seconds)) {
      100        
473 9   100     55 $self->_hms([$hours || 0, $minutes || 0, $seconds || 0]);
      100        
      100        
474             }
475              
476 10         27 return $self;
477             } # }}}
478              
479             # sub _set_from_ical (internal) {{{
480              
481             =head2 _set_from_ical ($self, $num_seconds)
482              
483             Sets internal data storage properly if we were only given seconds as a parameter.
484              
485             =cut
486              
487             sub _set_from_seconds {
488 1     1   5 my ($self, $seconds) = @_;
489            
490 1 50       15 $self->{sign} = (($seconds < 0) ? -1 : 1);
491             # find the number of days, if any
492 1         7 my $ndays = int ($seconds / (24*60*60));
493             # now, how many hours/minutes/seconds are there, after
494             # days are taken out?
495 1         4 my $nsecs = $seconds % (24*60*60);
496 1         2 $self->{ndays} = abs($ndays);
497 1         12 $self->{nsecs} = abs($nsecs);
498              
499              
500 1         3 return $self;
501             } # }}}
502              
503             # sub _hms (internal) {{{
504              
505             =head2 $self->_hms();
506              
507             Return an arrayref to hours, minutes, and second components, or undef
508             if nsecs is undefined. If given an arrayref, computes the new nsecs value
509             for the duration.
510              
511             =cut
512              
513             sub _hms {
514 29     29   54 my ($self, $hms_arrayref) = @_;
515              
516 29 100       68 if (defined($hms_arrayref)) {
517 9         41 my $new_sec_value = $hms_arrayref->[0]*3600 +
518             $hms_arrayref->[1]*60 + $hms_arrayref->[2];
519 9         21 $self->{nsecs} = ($new_sec_value);
520             }
521              
522 29         44 my $nsecs = $self->{nsecs};
523 29 50       56 if (defined($nsecs)) {
524 29         64 my $hours = int($nsecs/3600);
525 29         54 my $minutes = int(($nsecs-$hours*3600)/60);
526 29         45 my $seconds = $nsecs % 60;
527 29         93 return [ $hours, $minutes, $seconds ];
528             } else {
529 0         0 print "returning undef\n";
530 0         0 return undef;
531             }
532             } # }}}
533              
534             # sub _wd (internal) {{{
535              
536             =head2 $self->_wd()
537              
538             Return an arrayref to weeks and day components, or undef if ndays
539             is undefined. If Given an arrayref, computs the new ndays value
540             for the duration.
541              
542             =cut
543              
544             sub _wd {
545 27     27   54 my ($self, $wd_arrayref) = @_;
546              
547             #print "entering _wd\n";
548            
549 27 100       60 if (defined($wd_arrayref)) {
550            
551 7         15 my $new_ndays = $wd_arrayref->[0]*7 + $wd_arrayref->[1];
552 7         14 $self->{ndays} = $new_ndays;
553             }
554            
555             #use Data::Dumper; print Dumper $self->{ndays};
556            
557 27 100       63 if (defined(my $ndays= $self->{ndays})) {
558 25         59 my $weeks = int($ndays/7);
559 25         42 my $days = $ndays % 7;
560 25         75 return [ $weeks, $days ];
561             } else {
562 2         4 return undef;
563             }
564             } # }}}
565              
566             1;