File Coverage

blib/lib/XML/RSS/Timing.pm
Criterion Covered Total %
statement 200 217 92.1
branch 94 118 79.6
condition 55 104 52.8
subroutine 31 42 73.8
pod 13 26 50.0
total 393 507 77.5


line stmt bran cond sub pod time code
1              
2             require 5;
3              
4             # This file contains embedded documentation in POD format.
5             # Use 'perldoc' to read it.
6              
7             package XML::RSS::Timing;
8 8     8   62472 use strict;
  8         20  
  8         292  
9 8     8   47 use Carp ();
  8         20  
  8         157  
10 8     8   43 use vars qw($VERSION);
  8         21  
  8         329  
11 8     8   8028 use Time::Local ();
  8         15596  
  8         468  
12              
13             $VERSION = '1.07';
14 8 50   8   229 BEGIN { *DEBUG = sub () {0} unless defined &DEBUG; } # set DEBUG level
15              
16 8     8   60 use constant HOUR_SEC => 60 * 60;
  8         20  
  8         452  
17 8     8   40 use constant DAY_SEC => 60 * 60 * 24;
  8         17  
  8         362  
18 8     8   40 use constant WEEK_SEC => 60 * 60 * 24 * 7;
  8         14  
  8         422  
19 8     8   44 use constant MONTH_SEC => 60 * 60 * 24 * 28;
  8         15  
  8         390  
20 8     8   40 use constant YEAR_SEC => 60 * 60 * 24 * 365;
  8         15  
  8         12652  
21              
22 8     8   45 use constant HOURS_IN_WEEK => 24 * 7;
  8         13  
  8         27206  
23              
24             my @day_names = (
25             "Sunday", "Monday", "Tuesday", "Wednesday",
26             "Thursday", "Friday", "Saturday",
27             );
28              
29             my %day_name2number;
30             @day_name2number{@day_names} = (0..6);
31             # and going the other way, just look at $day_names[ daynumber ]
32              
33             ###########################################################################
34              
35              
36             =head1 NAME
37              
38             XML::RSS::Timing - understanding RSS skipHours, skipDays, sy:update*
39              
40             =head1 SYNOPSIS
41              
42             ...after getting an RSS/RDF feed that contains the following:
43             3
44             hourly
45             1970-01-01T08:20+00:00
46              
47             use XML::RSS::Timing;
48             my $timing = XML::RSS::Timing->new;
49             $timing->lastPolled( time() );
50             $timing->updatePeriod( 'hourly' );
51             $timing->updateFrequency( 3 );
52             $timing->updateBase( '1970-01-01T08:20+00:00' );
53            
54             # Find out the soonest I can expect new content:
55             my $then = $timing->nextUpdate;
56             print "I can next poll the feed after $then (",
57             scalar(localtime($then)), " local time)\n";
58            
59             Polling it before C<$then> is unlikely to return any new content, according
60             to the C elements' values.
61              
62             =head1 DESCRIPTION
63              
64             RSS/RDF modules can use the elements C, C, C,
65             C, C, and C
66             to express what days/times they won't update, so
67             that RSS/RDF clients can conserve network resources by not bothering to
68             poll a feed more than once during such a period.
69              
70             This Perl module is for taking in the RSS/RDF C, C,
71             C, and C elements' values, and figuring out when they
72             say new content might be available.
73              
74             Note:
75             This module doesn't depend on XML::RSS, nor in fact have any
76             particular relationship with it.
77              
78              
79             =head1 OVERVIEW
80              
81             There are two perspectives on this problem:
82              
83             =over
84              
85             =item The "When To Ignore Until?" Perspective
86              
87             With this perspective, you have just polled the given RSS/RDF feed
88             (regardless of whether its content turns out to be new), and you want to
89             see if the feed says you can skip polling it until some other future
90             time. With this perspective, you extract the C fields'
91             values and/or the C, C, and C values and pass
92             them to a new XML::RSS::Timing object, and then ask when you should
93             avoid polling this until. And in the end you'll probably do this:
94              
95             my $wait_until = $timing->nextUpdate;
96             $wait_until = time() + $Default_Polling_Delay
97             # where $Default_Polling_Delay is some reader-defined value
98             if $wait_until <= time();
99              
100             ...and then file away C<$wait_until>'s value in some internal table
101             that is consulted before polling things, like so:
102              
103             foreach my $feed (@FeedObjects) {
104             next if $feed->wait_until > time();
105             # Don't poll it, there'll be nothing new
106            
107             ...Else go ahead and poll it, there could be something new...
108             }
109              
110             =item The "Is It Time Yet?" Perspective
111              
112             With this perspective, you polled the RSS feed at some time in the past,
113             and are now considering whether its C fields' values and/or
114             the C and C values (which you stored somewhere) say
115             you can I poll the feed (or whether there'd be no point, if the
116             C fields say you shouldn't expect any new content). With
117             this perspective, you use code like this:
118              
119             ...after calling ->skipHours and/or ->updatePeriod, etc
120             $timing->lastPolled( $when_last_polled );
121             if( time() < $timing->nextUpdate ) {
122             # ...Don't poll it, there'll be nothing new...
123             } else {
124             ... go ahead and poll it, there could be something new...
125             }
126              
127             Of the two perspectives, this second one seems less efficient to me,
128             but your mileage may vary.
129              
130             =back
131              
132             =head1 METHODS
133              
134             This class defines the following methods:
135              
136             =over
137              
138             =cut
139              
140             ###########################################################################
141              
142             =item C<< $timing = XML::RSS::Timing->new(); >>
143              
144             This constructor method creates a new object to be used on figuring feed
145             timing. You should use a new object for each feed you're considering.
146              
147             =cut
148              
149             sub new { # Vanilla constructor
150 154     154 1 35270 my $self = $_[0];
151 154   33     887 $self = bless { }, ref($self) || $self;
152 154         331 $self->init();
153 154         302 return $self;
154             }
155              
156             #--------------------------------------------------------------------------
157              
158             sub init {
159 154     154 0 183 my $self = $_[0];
160 154         310 $self->use_exceptions(1);
161 154         321 $self->updateBase('1970-01-01T00:00+00:00');
162 154         199 return;
163             }
164              
165             ###########################################################################
166              
167             =item C<< $timing->skipHours( I ) >>
168              
169             This adds to this C<$timing> object the given list of hours from
170             the given feed's C element. Hours are expressed as
171             integers between 0 to 23 inclusive.
172              
173             =cut
174              
175             sub skipHours {
176 283 100   283 1 662 return @{ $_[0]{'skipHours'} || [] } if @_ == 1; # as a read list-accessor
  199 100       926  
177              
178 84         220 my( $self, @hours ) = @_;
179 84         140 foreach my $h (@hours) {
180 287 50 33     3307 return $self->boom("Usage: \$timingobj->skipHours( hournumbers... )" )
      33        
      33        
      33        
181             unless defined $h and length $h and $h =~ m/^\d\d?$/s
182             and $h >= 0 and $h <= 23; # Don't use 24 for midnight. use 0.
183             }
184 84         112 push @{ $self->{'skipHours'} }, @hours;
  84         274  
185 84         200 return;
186             }
187              
188             #--------------------------------------------------------------------------
189              
190             =item C<< $timing->skipDays( I ) >>
191              
192             This adds to this C<$timing> object the given list of days from
193             the given feed's C element. The day name strings have
194             to be from the set:
195             "Sunday", "Monday", "Tuesday", "Wednesday",
196             "Thursday", "Friday", "Saturday".
197              
198             =cut
199              
200             sub skipDays {
201 223 100   223 1 529 return @{ $_[0]{'skipDays'} || [] } if @_ == 1; # as a read list-accessor
  151 100       692  
202            
203 72         143 my( $self, @daynames ) = @_;
204 72         103 foreach my $d (@daynames) {
205 141 50 33     1110 return $self->boom("Usage: \$timingobj->skipDays( daynames... )" )
206             unless defined $d and length $d;
207 141 50       359 return $self->boom("Usage: \$timingobj->skipDays( daynames... ) -- \"$d\" isn't a day name" )
208             unless exists $day_name2number{$d};
209             }
210 72         106 push @{ $self->{'skipDays'} }, @daynames;
  72         212  
211 72         163 return;
212             }
213              
214             #--------------------------------------------------------------------------
215              
216 0     0 0 0 sub skipHours_clear { delete $_[0]{'skipHours'}; return; }
  0         0  
217 0     0 0 0 sub skipDays_clear { delete $_[0]{'skipDays' }; return; }
  0         0  
218              
219             #==========================================================================
220              
221             =item C<< $timing->updateFrequency( I ) >>
222              
223             This sets the given C<$timing> object's
224             updateFrequency value from the feed's (optional) C
225             element. This has to be a nonzero positive integer.
226              
227             =cut
228              
229             sub updateFrequency {
230 178     178 1 327 my($self, $freq) = @_;
231 178 100       797 return $self->{'updateFrequency'} if @_ == 1; # as a read accessor
232              
233 44 50 33     353 return $self->boom( "Usage: \$timingobj->updateFrequency( integer )" )
      33        
234             unless @_ == 2 and defined($freq) and $freq =~ m/^\d{1,5}$/s;
235             # sanity limit: 1-99999
236            
237 44         63 $freq += 0; # numerify the string
238 44   50     111 $self->{'updateFrequency'} = $freq || 1;
239 44         90 return $self->{'updateFrequency'};
240             }
241              
242             #==========================================================================
243              
244             =item C<< $timing->updateBase( I ) >>
245              
246             This sets the given C<$timing> object's
247             updateFrequency value from the feed's (optional) C
248             element. This has to be a date in one of these formats:
249              
250             1997
251             1997-07
252             1997-07-16
253             1997-07-16T19:20
254             1997-07-16T19:20Z
255             1997-07-16T19:20+01:00
256             1997-07-16T19:20:30+01:00
257             1997-07-16T19:20:30.45+01:00
258              
259             The default value is "1970-01-01T00:00Z".
260              
261             =cut
262              
263             sub updateBase {
264 209     209 1 392 my($self, $base) = @_;
265 209 50       429 return $self->{'updateBase'} if @_ == 1; # as a read accessor
266 209 50 33     1314 return $self->boom("Usage: \$timingobj->updateBase( 'yyyy-mm-ddThh:mm' )")
      33        
267             unless @_ == 2 and defined($base) and length($base);
268              
269 209         415 my $date = $self->_iso_date_to_epoch($base);
270              
271 209 50       497 return $self->boom("\"$base\" isn't a valid time format.")
272             unless defined $date;
273              
274 209         327 $self->{'updateBase_sec'} = $date;
275 209         379 $self->{'updateBase'} = $base;
276 209         216 DEBUG and print "Setting updateBase to $base and updateBase_sec to $date\n";
277              
278 209         327 return $base;
279             }
280              
281             #==========================================================================
282              
283             =item C<< $timing->updatePeriod( I ) >>
284              
285             This sets the given C<$timing> object's
286             updatePeriod value from the feed's (optional) C
287             element. This has to be a string from the set:
288             "hourly", "daily", "weekly", "monthly", "yearly".
289              
290             =cut
291              
292             sub updatePeriod {
293 65     65 1 224 my($self, $period) = @_;
294 65 50       144 return $self->{'updatePeriod'} if @_ == 1; # as a read accessor
295            
296 65 50 33     431 return $self->boom("Usage: \$timingobj->updatePeriod( interval_string )")
      33        
297             unless @_ == 2 and defined($period) and length($period);
298            
299 65         71 my $sec;
300            
301 65 100       205 if( $period eq 'hourly' ) { $sec = HOUR_SEC }
  18 100       22  
    100          
    100          
    50          
302 18         23 elsif( $period eq 'daily' ) { $sec = DAY_SEC }
303 18         23 elsif( $period eq 'weekly' ) { $sec = WEEK_SEC }
304 7         10 elsif( $period eq 'yearly' ) { $sec = YEAR_SEC;
305 7         21 $self->_complain("updatePeriod of 'yearly' is somewhat ill-advised");
306             }
307 4         6 elsif( $period eq 'monthly') { $sec = MONTH_SEC;
308 4         13 $self->_complain("updatePeriod of 'monthly' is ill-advised");
309             }
310             else {
311 0         0 $self->boom("updatePeriod value \"$period\" is invalid.\n"
312             . "Use (hourly|daily|weekly|monthly|yearly)" );
313             }
314            
315 65         65 DEBUG and print "Setting update period to $sec ($period)\n";
316 65         97 $self->{'updatePeriod_sec'} = $sec;
317            
318 65         223 return $self->{'updatePeriod'} = $period;
319             }
320              
321             #--------------------------------------------------------------------------
322              
323             =item C<< $timing->lastPolled( I ) >>
324              
325             This sets the time when you last polled this feed. If you don't set
326             this, the current time (C) will be used.
327              
328             Note that by "polling", I mean not just requesting the feed, but
329             requesting the feed and getting a successful response (regardless of
330             whether it's an HTTP 200 "OK" response or an HTTP 304 "Not Modified"
331             response). If you request a feed and get any sort of error, then don't
332             count that as actually polling the feed.
333              
334             =cut
335              
336             sub lastPolled {
337 545 100   545 1 1297 $_[0]{'lastPolled'} = $_[1] if @_ > 1; # Simple read/write scalar accessor
338 545         1328 $_[0]{'lastPolled'};
339             }
340              
341             #==========================================================================
342              
343             =item C<< $timing->ttl( I ) >>
344              
345             This sets the given C<$timing> object's "ttl" value from the feed's
346             (optional) C element. This has to be a nonzero positive integer.
347             It represents the minimum number of I that a reader can go between
348             times it polls the given feed. It is a somewhat obsolescent (but common)
349             predecessor to the C fields.
350              
351             ("TTL" stands for "time to live", a term borrowed from DNS cache jargon.)
352              
353             =cut
354              
355             sub ttl {
356 152     152 1 186 my($self, $ttl) = @_;
357 152 100       779 return $self->{'ttl'} if @_ == 1; # as a read accessor
358              
359 2 50 33     20 return $self->boom( "Usage: \$timingobj->ttl( integer )" )
      33        
360             unless @_ == 2 and defined($ttl) and $ttl =~ m/^\d{1,6}$/s;
361             # sanity limit: six digits (almost two years!)
362            
363 2         4 $ttl += 0; # numerify the string
364 2         4 $self->{'ttl'} = $ttl;
365 2         3 return $ttl;
366             # "All those moments will be lost in time, like tears in rain. Time to die."
367             # -- Roy Batty in /Blade Runner/
368             }
369              
370             #==========================================================================
371              
372             =item C<< $timing->maxAge( I ) >>
373              
374             This sets the given C<$timing> object's "maxAge" value.
375             This has to be a nonzero positive integer.
376              
377             This value comes not from the feed, but is an (optional) attribute of
378             your client: it denotes the I amount of time (in seconds) that
379             your client will go between polling, I whatever this feed
380             says.
381              
382             For example, if a feed says it updates only once a year, C is a
383             two months, then this timing object will act as if the feed really said
384             to update every two months.
385              
386             If you set this, you should probably set it only to a large value, like
387             the number of seconds in two months (62*24*60*60). By default, this is
388             not set, meaning no maximum is enforced. (So if a feed says to update
389             only once a year, then that's what this timing object faithfully
390             implements.)
391              
392             =cut
393              
394             sub maxAge {
395 137     137 1 158 my($self, $max) = @_;
396 137 100       366 return $self->{'maxAge'} if @_ == 1; # as a read accessor
397              
398 3 50 33     36 return $self->boom( "Usage: \$timingobj->maxAge( integer )" )
      33        
399             unless @_ == 2 and defined($max) and $max =~ m/^\d{1,9}$/s;
400             # sanity limit: nine digits (about thirty years!)
401            
402 3         4 $max += 0; # numerify the string
403 3         6 $self->{'maxAge'} = $max;
404 3         7 return $max;
405             # "All those moments will be lost in time, like tears in rain. Time to die."
406             # -- Roy Batty in /Blade Runner/
407             }
408              
409              
410             #==========================================================================
411              
412             =item C<< $timing->minAge( I ) >>
413              
414             This sets the given C<$timing> object's "minAge" value.
415             This has to be a nonzero positive integer.
416              
417             This value comes not from the feed, but is an (optional) attribute of your
418             client: it denotes the I amount of time (in seconds) that your
419             client will go between polling, I whatever this feed says.
420              
421             For example, if a feed says it can update every 5 minutes, but your
422             C is a half hour, then this timing object will act as if the feed
423             really said to update only half hour at most.
424              
425             If you set minAge, you should probably set it only to a smallish value, like
426             the number of seconds in an hour (60*60). By default, this is
427             not set, meaning no minimum is enforced.
428              
429             =cut
430              
431             sub minAge {
432 142     142 1 162 my($self, $min) = @_;
433 142 100       1034 return $self->{'minAge'} if @_ == 1; # as a read accessor
434              
435 5 50 33     37 return $self->boom( "Usage: \$timingobj->minAge( integer )" )
      33        
436             unless @_ == 2 and defined($min) and $min =~ m/^\d{1,9}$/s;
437             # sanity limit: nine digits (about thirty years!)
438            
439 5         5 $min += 0; # numerify the string
440 5         7 $self->{'minAge'} = $min;
441 5         9 return $min;
442             }
443              
444             #==========================================================================
445              
446             =item C<< $epochtime = $timing->nextUpdate(); >>
447              
448             This method returns the time (in seconds since the epoch) that's the soonest
449             that this feed could return new content.
450              
451             Note that this doesn't mean you have to actually poll the feed right
452             at that second! (That's why this is called "nextUpdate", not something like
453             "nextPoll".) Instead, I presume your RSS-reader will do something like
454              
455              
456              
457             run at random intervals
458             and will just look for what feeds' nextUpdate times are less than C
459             .)
460              
461             Note that C might return the same as this
462             feed's C value, in the case of a feed without any ttl/sy:*/update*
463             information and where you haven't specified a C.
464              
465             =cut
466              
467             sub nextUpdate {
468 137     137 1 317 my($self) = @_;
469             # Returns a time when we can next poll this feed
470            
471 137 50       241 $self->lastPolled( time() ) unless defined $self->lastPolled;
472              
473 137 100 100     438 unless(
      100        
      66        
474             defined($self->{'updatePeriod_sec'})
475             or $self->ttl
476             or $self->skipHours or $self->skipDays
477             ) {
478 3         5 DEBUG and print "No constraints. Can update whenever.\n";
479 3   50     5 return $self->lastPolled() + ($self->minAge || 0);
480             }
481              
482 134 100 100     1678 if( ($self->{'updateBase_sec'} || 0) > $self->lastPolled) {
483 6         10 DEBUG and print "updateBase is in the future!\n";
484 6         8 $self->{'updateBase_sec'} = $self->lastPolled;
485             # Having an updateBase in the future would do strange things to
486             # our math.
487             }
488              
489 134         285 my $then = $self->_unskipped_time_after(
490             $self->_enforce_min_max(
491             $self->_reckon_next_update_starts()
492             )
493             );
494 134         148 DEBUG and printf "Next open time is %s (%s GMT = %s local)\n",
495             $then, scalar(gmtime( $then )), scalar(localtime( $then ));
496 134         316 return $then;
497             }
498              
499             # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
500             sub _enforce_min_max {
501             # If we have a maxAge attribute, and if the given time violates that
502             # constraint, then enforce that.
503             # If we have a maxAge attribute, and if the given time violates that
504             # constraint, then enforce that.
505             # Otherwise just pass thru the given time.
506             #
507 134     134   170 my($self, $later) = @_;
508            
509 134         215 my $min = $self->minAge;
510 134 100       249 if($min) {
511 4         7 my $soon = $min + $self->lastPolled();
512 4         5 DEBUG and printf " MinTime: %s (%s). Cf later %s (%s)\n",
513             $soon, scalar(gmtime($soon)), $later, scalar(gmtime($later));
514 4 50       9 $later = $soon if $soon > $later; # take the later of the two
515             }
516              
517 134         269 my $max = $self->maxAge;
518 134 100       244 if($max) {
519 9         18 my $far = $max + $self->lastPolled();
520 9         10 DEBUG and printf " MaxTime: %s (%s). Cf later %s (%s)\n",
521             $far, scalar(gmtime($far)), $later, scalar(gmtime($later));
522 9 100       23 $later = $far if $far < $later; # take the earlier of the two
523             }
524              
525 134         294 return $later;
526             }
527              
528             # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
529              
530             sub _unskipped_time_after {
531 134     134   163 my($self, $start_time) = @_;
532              
533             # Now see when the next moment is which isn't excluded
534             # by a skipHours or skipDays constraint.
535              
536 134         144 my $then = $start_time;
537              
538 134         134 my(@hour_is_skippable, @day_is_skippable);
539 134         228 foreach my $h ($self->skipHours) {
540 287         494 $hour_is_skippable[ $h ] = 1;
541             }
542 134         307 foreach my $d ($self->skipDays ) {
543 141         295 $day_is_skippable[ $day_name2number{$d} ] = 1;
544             }
545              
546 134         207 my($s,$m,$h,$d, $start_hour, $start_day);
547              
548 134         141 while(1) {
549              
550 1266         2520 ($s,$m,$h, $d) = (gmtime($then))[ 0,1,2, 6 ];
551             # That moment's hournumber and daynumber (and minutes and seconds)
552              
553 1266 100 66     4018 if(!defined $start_hour) {
    50          
554 134         139 $start_hour = $h;
555 134         145 $start_day = $d;
556             } elsif($h == $start_hour and $d == $start_day) {
557             # The whole week was skipped!
558 0         0 $self->_complain("Aborting after revisiting $h h on $day_names[$d]");
559 0         0 return $start_time;
560             }
561            
562 1266 100 100     3041 unless( $day_is_skippable[$d] or $hour_is_skippable[$h] ) {
563 134         121 DEBUG and print " Accepting $h H on $day_names[$d] (",
564             scalar(gmtime($then)), ")!\n";
565 134         345 return $then;
566             }
567            
568 1132         917 DEBUG > 1 and print " Skipping $h H on $day_names[$d] (",
569             scalar(gmtime($then)), ")\n";
570 1132         1277 $then += (HOUR_SEC - ($s + 60 * $m));
571             # Get to the start of the next hour.
572            
573             # And loop around again
574             }
575              
576             }
577              
578             # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
579              
580             sub _reckon_next_update_starts {
581 134     134   164 my($self) = @_;
582            
583 134   100     1019 my $interval = int(
      100        
584             ($self->{'updatePeriod_sec'} || 0)
585             / ($self->updateFrequency || 1)
586             );
587             # So if we update 5 times daily, our "interval" is (DAY_SEC / 5) seconds
588              
589 134         247 my $last_update = $self->lastPolled;
590              
591 134 100 100     312 if( $interval ) {
    100          
592             # OK, fall thru...
593             } elsif( ($self->ttl || 0) > 0 ) {
594 9         11 my $ttl = $self->ttl;
595 9         10 DEBUG and print "No updateWhatever fields, but using TTL: $ttl minutes\n";
596 9         21 return $last_update + ($ttl * 60); # just conv to seconds
597             } else {
598 60         142 return $last_update;
599             }
600            
601 65   100     184 my $base = $self->{'updateBase_sec'} || 0;
602 65         123 my $start_of_current_interval
603             = int( ($last_update-$base) / $interval) * $interval + $base;
604            
605 65         80 my $then = $start_of_current_interval + $interval;
606            
607 65         59 if(DEBUG) {
608             print " Update interval: $interval s\n",
609             " Update base : $base s\n",
610             " The current interval started on $start_of_current_interval s\n";
611             printf " = (scalar gmtime(%s * %s + %s))\n",
612             ( $start_of_current_interval - $base ) / $interval, $interval, $base;
613             print " The next interval starts on $then s\n";
614             printf " = (scalar gmtime(%s * %s + %s))\n",
615             ( $then - $base ) / $interval, $interval, $base;
616             }
617            
618 65         169 return $then;
619             }
620              
621             #--------------------------------------------------------------------------
622              
623             sub _iso_date_to_epoch {
624 486     486   6039 my($self, $date) = @_;
625 486 50       964 return undef unless defined $date;
626              
627 486 100       3965 if(
628             my( $Y,$M,$D, $h,$m, $s, $s_fract, $tz_sign, $tz_h, $tz_m ) =
629             $date =~
630             # This regexp matches basically ISO 8601 except that the "Z" is optional.
631            
632             m<^
633             (\d\d\d\d) # year
634             (?:
635             -([01]\d) # month
636             (?:
637             -([0123]\d) # day
638             (?:
639             T([012]\d):([012345]\d) # hh:mm
640             (?:
641             :([0123456]\d) # seconds
642             (?:
643             (\.\d+) # fractions of a second
644             )?
645             )?
646             #
647             # And now the TZ:
648             #
649             (?:
650             Z # Zulu
651             |
652             (?: # or by offset:
653             ([-+])
654             ([012]\d):([012345]\d) # hh:mm, with leading '+' or '-'
655             )
656             )?
657             )?
658             )?
659             )?
660             $
661             >sx
662              
663             ) {
664              
665 472         481 if(DEBUG) {
666             printf "# Date %s matches => %s-%s-%s T%s:%s:%s.%s TZ: %s%s:%s\n",
667             $date,
668             map defined($_) ? $_ : "_",
669             ( $Y,$M,$D, $h,$m, $s, $s_fract, $tz_sign, $tz_h, $tz_m )
670             ;
671             }
672            
673 472 100       975 $M = 1 unless defined $M;
674 472 100       794 $D = 1 unless defined $D;
675 472 100       744 $h = 0 unless defined $h;
676 472 100       821 $m = 0 unless defined $m;
677 472 100       904 $s = 0 unless defined $s;
678              
679 472 100 100     2102 return $self->boom("Year out of range: $Y") if $Y < 1902 or $Y > 2037;
680 470 100 66     1942 return $self->boom("Month out of range: $M") if $M < 1 or $M > 12;
681 468 100 66     1711 return $self->boom("Day out of range: $D") if $D < 1 or $D > 31;
682 467 50 33     1829 return $self->boom("Hour out of range: $h") if $h < 0 or $h > 23;
683 467 50 33     1632 return $self->boom("Minute out of range: $m") if $h < 0 or $h > 59;
684 467 50 33     1717 return $self->boom("Second out of range: $s") if $h < 0 or $h > 60;
685              
686 467         551 my $tz_offset = 0;
687 467 100       835 if(defined $tz_sign) {
688 201         408 $tz_offset = ($tz_h * 60 + $tz_m) * 60;
689 201 100       437 $tz_offset = 0 - $tz_offset if $tz_sign eq '-';
690             }
691              
692 467         543 my $time = eval { Time::Local::timegm( $s,$m,$h, $D,$M-1,$Y-1900 ) };
  467         1582  
693 467 100       10834 return $self->boom("Couldn't convert $date to an exact moment")
694             unless defined $time;
695              
696 466 50 66     1068 $time++ if $s_fract and $s_fract >= .5;
697 466         587 $time -= $tz_offset;
698 466         1237 return $time;
699             } else {
700 14         15 DEBUG and print "# Date $date doesn't match.\n";
701 14         33 return undef;
702             }
703             }
704              
705             #--------------------------------------------------------------------------
706              
707             =item C<< $timing->use_exceptions( 0 ) >>
708              
709             =item C<< $timing->use_exceptions( 1 ) >>
710              
711             This sets whether this object will (with a 1) or won't (with a 0) use
712             exceptions (C's) to signal errors, or whether it will simply
713             muddle through and collect them in C.
714              
715             Basically, errors can come from passing invalid parameters to this
716             module's methods, such as passing "friday" to C (instead of
717             "Friday"), or passing 123 to C (instead of an integer
718             in the range 0-23), etc.
719              
720             B
721              
722             =cut
723              
724             sub use_exceptions {
725 306 50   306 1 1774 $_[0]{'_die'} = $_[1] if @_ > 1; # Simple read/write scalar accessor
726 306         539 $_[0]{'_die'};
727             }
728              
729             #--------------------------------------------------------------------------
730              
731             =item C<< @complaints = $timing->complaints() >>
732              
733             This returns a list of any errors that were encountered in dealing with
734             this C<$timing> object. Errors can result from blocking exceptions
735             (if C is off), or from non-fatal warnings of interest
736             while debugging (like if C was told to skip all 24 hours).
737              
738             If there were no complaints, this will simply return an empty list.
739              
740             =cut
741              
742 6 100   6 1 24 sub complaints { return @{ $_[0]->{'complaints'} || [] }; }
  6         34  
743             # Simple list read-accessor
744              
745             ###########################################################################
746              
747             sub boom {
748 6     6 0 29 my($self, @error) = @_;
749 6 50       39 if( $self->{'_die'} ) {
750 0         0 Carp::confess(join '', @error)
751             } else {
752 3         9 $self->_complain(@error);
753             }
754 3         10 return;
755             }
756              
757             #--------------------------------------------------------------------------
758              
759             sub _complain {
760 14     14   27 my($self, @complaint) = @_;
761 14         21 push @{ $self->{'complaints'} }, join '', @complaint;
  14         49  
762 14         18 DEBUG and print join '', @complaint, "\n";
763 14         28 return;
764             }
765              
766             ###########################################################################
767              
768             # Aliases for the more Perly foo_bar_baz style. See "perldoc perlstyle"
769              
770 0     0 0   sub skip_days { shift->skipDays( @_) }
771 0     0 0   sub skip_hours { shift->skipHours( @_) }
772 0     0 0   sub update_base { shift->updateBase( @_) }
773 0     0 0   sub update_period { shift->updatePeriod( @_) }
774 0     0 0   sub update_frequency { shift->updateFrequency(@_) }
775 0     0 0   sub next_update { shift->nextUpdate( @_) }
776 0     0 0   sub last_polled { shift->lastPolled( @_) }
777 0     0 0   sub max_age { shift->maxAge( @_) }
778 0     0 0   sub min_age { shift->minAge( @_) }
779              
780             ###########################################################################
781             1;
782             __END__