File Coverage

blib/lib/DateTime/TimeZone/ICal.pm
Criterion Covered Total %
statement 24 99 24.2
branch 0 34 0.0
condition 0 10 0.0
subroutine 8 18 44.4
pod 7 8 87.5
total 39 169 23.0


line stmt bran cond sub pod time code
1             package DateTime::TimeZone::_ICal;
2              
3 2     2   17306 use parent 'DateTime::TimeZone';
  2         298  
  2         12  
4              
5             # kill the DateTime::TimeZone constructor
6             sub new {
7 0   0 0     bless {}, shift || __PACKAGE__
8             }
9              
10             package DateTime::TimeZone::ICal;
11              
12 2     2   20314 use 5.010;
  2         6  
  2         50  
13 2     2   8 use strict;
  2         7  
  2         67  
14 2     2   8 use warnings FATAL => 'all';
  2         4  
  2         87  
15              
16 2     2   1140 use Moo;
  2         19204  
  2         10  
17 2     2   3807 use namespace::autoclean;
  2         16449  
  2         13  
18              
19 2     2   110 use base 'DateTime::TimeZone::_ICal';
  2         3  
  2         717  
20              
21 2     2   876 use DateTime::TimeZone::ICal::Spec;
  2         5  
  2         2163  
22              
23             with 'DateTime::TimeZone::ICal::Parsing';
24              
25              
26             =head1 NAME
27              
28             DateTime::TimeZone::ICal - iCal VTIMEZONE entry to DateTime::TimeZone
29              
30             =head1 VERSION
31              
32             Version 0.04
33              
34             =cut
35              
36             our $VERSION = '0.04';
37              
38             =head1 SYNOPSIS
39              
40             use Data::ICal;
41             use DateTime::Format::ICal;
42             use DateTime::TimeZone::ICal;
43              
44             my $ical = Data::ICal->new(filename => 'foo.ics');
45              
46             # generate a table of time zones
47             my (%tz, @events);
48             for my $entry (@{$ical->entries}) {
49             my $type = $entry->ical_entry_type;
50             if ($type eq 'VTIMEZONE') {
51             my $dtz = DateTime::TimeZone::ICal->from_ical_entry($entry);
52             $tz{$dtz->name} = $dtz;
53             }
54             elsif ($type eq 'VEVENT') {
55             push @events, $entry;
56             }
57             # ... handle other iCal objects ...
58             }
59              
60             # now we can use this dictionary of time zones elsewhere:
61              
62             for my $event (@events) {
63             # get a property that is a date
64             my ($dtstart) = @{$event->property('dtstart')};
65              
66             # get the time zone key from the property parameters
67             my $tzid = $dtstart->parameters->{TZID};
68              
69             # convert the date in the ordinary fashion
70             my $dt = DateTime::Format::ICal->parse_datetime($dtstart->value);
71              
72             # the datetime will be 'floating', therefore unaffected
73             $dt->set_time_zone($tz{$tzid}) if $tzid and $tz{$tzid};
74              
75             # ... do other processing ...
76             }
77              
78             =head1 DESCRIPTION
79              
80             Conforming iCal documents (L
81             5545|https://tools.ietf.org/html/rfc5545>) have three ways to
82             represent C values: UTC, local, and specified through the
83             C mechanism. C I in relevant properties are
84             references to the same C I in one of a list of
85             C objects, where the information about UTC offsets and
86             their recurrence is embedded in the document.
87              
88             In practice, many generators of iCal documents use, as C keys,
89             valid labels from L
90             database|http://www.iana.org/time-zones>, but others, notably
91             Microsoft Outlook, do not. RFC 5545 explicitly declines to specify a
92             naming convention, so it is sometimes necessary to construct the time
93             zone offsets and daylight savings changes from the C data
94             itself, rather than just inferring it from the name. That's where this
95             module comes in.
96              
97             =head1 METHODS
98              
99             The only differences in interface for this module are its constructor
100             and one method, L. The rest of the interface should
101             work exactly the same way as L, so please consult
102             its documentation for other functionality.
103              
104             This module overrides the following methods:
105              
106             =over 4
107              
108             =item
109              
110             L
111              
112             =item
113              
114             L
115              
116             =cut
117              
118             has category => (
119             is => 'ro',
120             # isa => 'Undef',
121             default => sub { undef },
122             );
123              
124             =item
125              
126             L
127              
128             =cut
129              
130             has is_floating => (
131             is => 'ro',
132             # isa => 'Bool',
133             default => sub { 0 },
134             );
135              
136             =item
137              
138             L
139              
140             =cut
141              
142             has is_olson => (
143             is => 'ro',
144             # isa => 'Bool',
145             default => sub { 0 },
146             );
147              
148             =item
149              
150             L
151              
152             =cut
153              
154             # this private generic accessor drives all other accessors
155             sub _spec_for {
156 0     0     my ($self, $dt) = @_;
157              
158 0           my %spec;
159             # find the most appropriate standard and daylight entries
160 0           for my $k (qw(standard daylight)) {
161             # this will pick the spec closest to the datetime
162 0 0         my @specs = @{$self->$k} or next;
  0            
163              
164             # no sense in this rigmarole if there's only one spec
165 0 0         if (@specs == 1) {
166 0           $spec{$k} = $specs[0];
167 0           next;
168             }
169              
170 0           my ($spec) = sort { DateTime::Duration->compare
  0            
171             ($dt - $a->dtstart, $dt - $b->dtstart, $dt)
172 0           } grep { $dt >= $_->dtstart } @specs;
173              
174             # get the oldest one if the datetime is too old
175 0 0         ($spec) = reverse @specs unless $spec;
176              
177             # now assign it
178 0           $spec{$k} = $spec;
179             }
180              
181 0 0         if ($spec{daylight}) {
182             # now we find which of the recurrences is closest;
183 0           my ($sr, $dr) = map { $spec{$_}->recurrence } qw(standard daylight);
  0            
184              
185             # XXX HO-LEE-CRAP these set methods are slow.
186             #my $sd = $sr->current($dt) if $sr;
187             #my $dd = $dr->current($dt) if $dr;
188              
189             # NO.
190             # # if dd is bigger than sd then we are in daylight savings
191             # if ($sd && $dd && $dd > $sd) {
192              
193             # the above is accurate but it's unacceptably, RIDICULOUSLY slow.
194              
195             # instead we're going to assume that daylight savings time is
196             # pinned to the year
197              
198             # just take the first instance of each and then subtract th
199 0 0 0       if ($sr && $dr) {
200             #my $sd = ($sr->min->utc_rd_values
201 0           my ($sm, $dm) = ($sr->min, $dr->min);
202              
203 0           my $sd = ($sm->utc_rd_values)[1] / 86400 + $sm->day_of_year;
204 0           my $dd = ($dm->utc_rd_values)[1] / 86400 + $dm->day_of_year;
205 0           my $nd = ($dt->utc_rd_values)[1] / 86400 + $dt->day_of_year;
206              
207             #warn "$sd $dd $nd";
208              
209 0 0 0       if ($nd >= $dd && $nd <= $sd) {
210 0 0         return wantarray ? ($spec{daylight}, 1) : $spec{daylight};
211             }
212             }
213             }
214              
215 0           return $spec{standard};
216             }
217              
218             sub offset_for_datetime {
219 0     0 1   my ($self, $dt) = @_;
220              
221 0           my $spec = $self->_spec_for($dt);
222              
223 0           $self->offset_as_seconds($spec->tzoffsetto);
224             }
225              
226             =item
227              
228             L
229              
230             =cut
231              
232             sub offset_for_local_datetime {
233 0     0 1   my ($self, $dt) = @_;
234              
235 0           my $spec = $self->_spec_for($dt);
236              
237 0           $self->offset_as_seconds($spec->tzoffsetto);
238             }
239              
240             =item
241              
242             L
243              
244             =cut
245              
246             sub is_dst_for_datetime {
247 0     0 1   my ($self, $dt) = @_;
248              
249 0           my (undef, $dst) = $self->_spec_for($dt);
250              
251 0 0         $dst ? 1 : 0;
252             }
253              
254             =item
255              
256             L
257              
258             =cut
259              
260             sub short_name_for_datetime {
261 0     0 1   my ($self, $dt) = @_;
262              
263 0           my $spec = $self->_spec_for($dt);
264              
265 0           $spec->tzname;
266             }
267              
268             =item
269              
270             L
271              
272             =cut
273              
274             sub is_utc {
275 0     0 1   my $self = shift;
276 0 0         if (@{$self->daylight} == 0) {
  0            
277 0 0         if (my ($latest) = @{$self->standard}) {
  0            
278 0           return $self->offset_as_seconds($latest->tzoffsetto) == 0;
279             }
280             }
281             }
282              
283             =item
284              
285             L
286              
287             =cut
288              
289             sub has_dst_changes {
290 0     0 1   return scalar @{shift->daylight};
  0            
291             }
292              
293             =back
294              
295             =head2 new %PARAMS
296              
297             The constructor has been modified to permit the assembly of a time
298             zone specification from piecemeal data. These are the following
299             initialization parameters:
300              
301             =over 4
302              
303             =item tzid
304              
305             This is the C of the iCal entry. Note that the accessor to
306             retrieve the value from an instantiated object is C, for
307             congruence with L.
308              
309             =cut
310              
311             has name => (
312             is => 'ro',
313             init_arg => 'tzid',
314             default => sub { 'VTIMEZONE' },
315             );
316              
317             =item standard
318              
319             This is an C reference of L
320             instances, or otherwise of C references congruent to that
321             module's constructor, which will be coerced into said objects. This
322             parameter is I, and there must be I one member in
323             the C.
324              
325             =cut
326              
327             has standard => (
328             is => 'ro',
329             # isa => sub { die unless ref $_[1] eq
330             # isa => 'DateTime::TimeZone::ICal::Part',
331             required => 1,
332             default => sub { [] },
333             );
334              
335              
336             =item daylight
337              
338             Same deal but for Daylight Savings Time. This parameter is optional,
339             as is its contents.
340              
341             =cut
342              
343             has daylight => (
344             is => 'ro',
345             default => sub { [] },
346             );
347              
348             =back
349              
350             In practice you may not need to ever use this constructor directly,
351             but it may come in handy for instances where you need to compose
352             non-standard time zone behaviour from scratch.
353              
354             =cut
355              
356             sub BUILD {
357 0     0 0   my $self = shift;
358              
359 0           for my $speclist ($self->standard, $self->daylight) {
360             # now sort them
361 0           @$speclist = sort { $b->dtstart <=> $a->dtstart } @$speclist;
  0            
362             }
363             }
364              
365             =head2 from_ical_entry $ENTRY [, $USE_DATA ]
366              
367             This class method converts a L object of type
368             C into a L object. It will
369             C if the input is malformed, so wrap it in an C or
370             equivalent if you expect that possibility.
371              
372             This method attempts to check if an existing L can
373             be instantiated from the C, thus skipping over any local
374             processing. This behaviour can be overridden with the C<$USE_DATA>
375             flag.
376              
377             =cut
378              
379             sub from_ical_entry {
380 0     0 1   my ($class, $entry, $use_data) = @_;
381              
382 0           my %out;
383              
384 0           for my $name ($entry->mandatory_unique_properties,
385             $entry->optional_unique_properties) {
386 0 0         my ($prop) = @{$entry->property($name) || []};
  0            
387 0           $prop = $class->parse_val($prop);
388 0           $out{$name} = $prop;
389             }
390              
391             # search for an existing time zone unless overridden
392 0 0         unless ($use_data) {
393 0           my $tz = eval { DateTime::TimeZone->new(name => $out{tzid}) };
  0            
394 0 0         return $tz if $tz;
395             }
396              
397 0           for my $name ($entry->mandatory_repeatable_properties,
398             $entry->optional_repeatable_properties) {
399 0 0         my @prop = @{$entry->property($name) || []};
  0            
400 0           @prop = map { $class->parse_val($_) } @prop;
  0            
401 0           $out{$name} = \@prop;
402             }
403              
404 0           for my $spec (@{$entry->entries}) {
  0            
405 0           my $type = lc $spec->ical_entry_type;
406 0 0         if ($type =~ /^(?:standard|daylight)$/) {
407 0   0       my $x = $out{$type} ||= [];
408              
409 0           my $y = DateTime::TimeZone::ICal::Spec->from_ical_entry($spec);
410              
411 0 0         push @$x, $y if $y;
412             }
413             }
414              
415 0 0         $class = ref $class if ref $class;
416              
417 0           return $class->new(\%out);
418             }
419              
420             =head1 AUTHOR
421              
422             Dorian Taylor, C<< >>
423              
424             =head1 BUGS
425              
426             Please report any bugs or feature requests to
427             C, or through the web
428             interface at
429             L.
430             I will be notified, and then you'll automatically be notified of
431             progress on your bug as I make changes.
432              
433             =head1 SUPPORT
434              
435             You can find documentation for this module with the perldoc command.
436              
437             perldoc DateTime::TimeZone::ICal
438              
439              
440             You can also look for information at:
441              
442             =over 4
443              
444             =item * RT: CPAN's request tracker (report bugs here)
445              
446             L
447              
448             =item * AnnoCPAN: Annotated CPAN documentation
449              
450             L
451              
452             =item * CPAN Ratings
453              
454             L
455              
456             =item * Search CPAN
457              
458             L
459              
460             =back
461              
462             =head1 SEE ALSO
463              
464             =over 4
465              
466             =item L
467              
468             =item L
469              
470             =item L
471              
472             =item L
473              
474             =item L
475              
476             =back
477              
478             =head1 LICENSE AND COPYRIGHT
479              
480             Copyright 2015 Dorian Taylor.
481              
482             Licensed under the Apache License, Version 2.0 (the "License"); you
483             may not use this file except in compliance with the License. You may
484             obtain a copy of the License at
485             L
486              
487             Unless required by applicable law or agreed to in writing, software
488             distributed under the License is distributed on an "AS IS" BASIS,
489             WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
490             implied. See the License for the specific language governing
491             permissions and limitations under the License.
492              
493             =cut
494              
495             1; # End of DateTime::TimeZone::ICal