File Coverage

blib/lib/Date/ICal/Duration.pm
Criterion Covered Total %
statement 152 159 95.6
branch 53 68 77.9
condition 29 33 87.8
subroutine 21 21 100.0
pod 11 11 100.0
total 266 292 91.1


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