File Coverage

blib/lib/Astro/App/Satpass2/ParseTime/ISO8601.pm
Criterion Covered Total %
statement 51 68 75.0
branch 26 44 59.0
condition 12 17 70.5
subroutine 10 13 76.9
pod 5 5 100.0
total 104 147 70.7


line stmt bran cond sub pod time code
1             package Astro::App::Satpass2::ParseTime::ISO8601;
2              
3 9     9   4562 use strict;
  9         19  
  9         399  
4 9     9   44 use warnings;
  9         20  
  9         616  
5              
6 9         1514 use Astro::App::Satpass2::Utils qw{
7             back_end __back_end_class_name_of_record __parse_class_and_args
8             HAVE_DATETIME
9             @CARP_NOT
10 9     9   47 };
  9         21  
11 9         718 use Astro::Coord::ECI::Utils 0.112 qw{
12 9     9   60 looks_like_number SECSPERDAY greg_time_gm greg_time_local };
  9         203  
13              
14 9     9   53 use parent qw{ Astro::App::Satpass2::ParseTime };
  9         29  
  9         55  
15              
16             our $VERSION = '0.057_01';
17              
18             my $package = __PACKAGE__;
19              
20             sub attribute_names {
21 14     14 1 30 my ( $self ) = @_;
22 14         66 return ( $self->SUPER::attribute_names(), qw{ back_end } );
23             }
24              
25             sub class_name_of_record {
26 0     0 1 0 my ( $self ) = @_;
27 0         0 return $self->__back_end_class_name_of_record(
28             $self->SUPER::class_name_of_record() );
29             }
30              
31             my $zone_re = qr{ (?i: ( Z | UT | GMT ) |
32             ( [+-] ) ( [0-9]{1,2} ) :? ( [0-9]{1,2} )? ) }smx;
33              
34             sub delegate {
35 18     18 1 3902 return __PACKAGE__;
36             }
37              
38             {
39              
40             my %special_day_offset = (
41             yesterday => -SECSPERDAY(),
42             today => 0,
43             tomorrow => SECSPERDAY(),
44             );
45              
46             my $era_ad = sub { return $_[0] };
47             my $era_bc = sub { return 1 - $_[0] };
48             my %era_cvt = (
49             AD => $era_ad,
50             BC => $era_bc,
51             BCE => $era_bc,
52             CE => $era_ad,
53             );
54              
55             # Note that we have to reverse sort the keys because otherwise 'BC'
56             # gets matched before we have a chance to try 'BCE'.
57             my $era_re = qr< (?: @{[
58             join ' | ', reverse sort keys %era_cvt
59             ]} ) >smxi;
60              
61             my $make_epoch = HAVE_DATETIME ? sub {
62             my ( $self, $zone, $offset, @date ) = @_;
63             $zone ||= 'local';
64             if ( defined( my $special = $special_day_offset{$date[0]} ) ) {
65             my $dt = DateTime->today(
66             time_zone => $zone,
67             );
68             splice @date, 0, 3, $dt->year(), $dt->month(), $dt->day();
69             $offset += $special;
70             }
71             my %dt_arg;
72             @dt_arg{ qw<
73             year month day hour minute second nanosecond
74             > } = @date;
75             $dt_arg{nanosecond} *= 1_000_000_000;
76             $dt_arg{time_zone} = $zone;
77             $self->{_back_end}
78             and return $self->{_back_end}{class}->new(
79             %dt_arg,
80             @{ $self->{_back_end}{arg} },
81             )->epoch() + $offset;
82             return DateTime->new( %dt_arg )->epoch() + $offset;
83             } : sub {
84             my ( undef, $zone, $offset, @date ) = @_;
85             if ( defined( my $special = $special_day_offset{$date[0]} )
86             ) {
87             my @today = $zone ? gmtime : localtime;
88             splice @date, 0, 3, @today[ 5, 4, 3 ];
89             $date[0] += 1900;
90             $offset += $special;
91             } else {
92             --$date[1];
93             }
94             $offset += pop @date;
95             if ( defined $zone ) {
96             'UTC' eq $zone
97             and return greg_time_gm( reverse @date ) + $offset;
98             local $ENV{TZ} = $zone; # The best we can do.
99             return greg_time_local( reverse @date ) + $offset;
100             } else {
101             return greg_time_local( reverse @date ) + $offset;
102             }
103             };
104              
105             sub parse_time_absolute {
106 88     88 1 224 my ( $self, $string ) = @_;
107              
108 88         230 my @date;
109              
110             my $special_only;
111              
112             # ISO 8601 date
113 88 50       1733 if ( $string =~ m< \A
    0          
114             ( ( [0-9]+ ) \s* ( $era_re ) [^0-9]? | # year $1, $2 era $3
115             [0-9]{4} [^0-9]? |
116             [0-9]+ [^0-9] )
117             (?: ( [0-9]{1,2} ) [^0-9]? # month: $4
118             (?: ( [0-9]{1,2} ) [^0-9]? # day: $5
119             )?
120             )?
121             >smxg ) {
122              
123 88 50       364 if ( $3 ) {
124 0         0 @date = ( $era_cvt{ uc $3 }->( $2 + 0 ), $4, $5 );
125             } else {
126 88         513 @date = ( $1, $4, $5 );
127 88         366 $date[0] =~ s/ [^0-9] \z //smx;
128 88 100       330 $date[0] < 70
129             and $date[0] += 2000;
130 88 50       267 $date[0] < 100
131             and $date[0] += 1900;
132             }
133              
134 88 100       237 defined $date[1]
135             or $date[1] = 1;
136 88 100       272 defined $date[2]
137             or $date[2] = 1;
138              
139             # special-case 'yesterday', 'today', and 'tomorrow'.
140             } elsif ( $string =~ m{ \A
141             ( yesterday | today | tomorrow ) \b [^0-9]? # day: $1
142             }smxgi ) {
143             # Handle this when we make the epoch, since we do not yet
144             # know the zone.
145 0         0 @date = ( lc $1, 0, 0 );
146 0         0 $special_only = 1;
147              
148             } else {
149              
150 0         0 return;
151              
152             }
153              
154 88 100       462 if ( $string =~ m< \G
155             ( [0-9]{1,2} ) [^0-9+-]? # hour: $1
156             (?: ( [0-9]{1,2} ) [^0-9+-]? # minute: $2
157             (?: ( [0-9]{1,2} ) [^0-9+-]? # second: $3
158             ( [0-9]* ) # fract: $4
159             )?
160             )?
161             >smxgc ) {
162 68 50 100     543 push @date, $1, $2 || 0, $3 || 0, $4 ? ".$4" : 0;
      100        
163 68         133 $special_only = 0;
164             } else {
165 20         62 push @date, ( 0 ) x 4;
166             }
167              
168             # We might have gobbled part of the zone.
169 88 100 66     718 not $special_only
170             and $string =~ m/ \G (?<= [^0-9] ) /smxgc
171             and pos $string -= 1;
172 88         634 my ( $zone ) = $string =~ m/ \G ( .* ) /smxgc;
173              
174 88         310 my ( $z, $offset ) = $self->_interpret_zone( $zone );
175 88 50       403 defined $offset
176             or return;
177              
178 88         286 return $make_epoch->( $self, $z, $offset, @date );
179             }
180              
181             }
182              
183             sub _interpret_zone {
184 88     88   251 my ( $self, $zone, $fatal ) = @_;
185 88 50       398 defined $zone
186             and $zone =~ s/ \A \s+ //smx;
187             $zone
188 88 50       262 or return ( @{ $self->{$package}{tz} || [ undef, 0 ] } );
  27 100       204  
189 61 50       482 if ( $zone =~ m/ \A $zone_re \z /smxo ) {
190 61 100       480 $1
191             and return ( UTC => 0 );
192 5   50     37 my $offset = ( ( $3 || 0 ) * 60 + ( $4 || 0 ) ) * 60;
      100        
193 5 100 66     31 $2
194             and '-' eq $2
195             or $offset = - $offset;
196 5         22 return ( UTC => $offset );
197             } else {
198 0         0 HAVE_DATETIME
199             or return ( $zone => 0 ); # On the user's head be it.
200              
201 0 0       0 DateTime::TimeZone->is_valid_name( $zone )
202             and return ( $zone => 0 );
203              
204 0 0       0 $fatal
205             and $self->wail( "Invalid time zone '$zone'" );
206 0         0 return;
207             }
208             }
209              
210             sub tz {
211 7     7 1 34 my ( $self, @args ) = @_;
212 7 50       31 if ( @args ) {
213 7 50 33     31 if ( defined $args[0] && $args[0] ne '' ) {
214             $self->{$package}{tz} = [
215 0         0 $self->_interpret_zone( $args[0], 1 ) ];
216             } else {
217 7         21 delete $self->{$package}{tz};
218             }
219             }
220 7         54 return $self->SUPER::tz( @args );
221             }
222              
223             sub __back_end_default {
224 0     0     my ( undef, $cls ) = @_; # Invocant ($self) unused
225 0 0         defined $cls
226             and return $cls;
227 0           return 'DateTime';
228             }
229              
230             sub __back_end_validate {
231 0     0     my ( undef, $cls, @arg ) = @_; # Invocant ($self) unused
232 0           $cls->now( @arg );
233 0           return;
234             }
235              
236             1;
237              
238             =head1 NAME
239              
240             Astro::App::Satpass2::ParseTime::ISO8601 - Astro::App::Satpass2 minimal ISO-8601 parser
241              
242             =head1 SYNOPSIS
243              
244             No user-serviceable parts inside.
245              
246             =head1 DETAILS
247              
248             This class parses ISO-8601 dates. It does not do ordinal days or weeks,
249             but it is rather permissive on punctuation, and permits the convenience
250             dates C<'yesterday'>, C<'today'>, and C<'tomorrow'>.
251              
252             This class understands ISO-8601 time zone specifications of the form
253             'Z', 'UT', 'GMT' and C<[+-]\d{1,2}:?\d{,2}>, but it knows nothing about
254             shifts for summer time. So C<2009/7/1 12:00:00 -5> is 5:00 PM GMT, not
255             4:00 PM. Other zones will be accepted, but may not do what you want. See
256             below.
257              
258             As an extension to the ISO-8601 standard, years can be followed by an
259             era specification, which is one of C<'AD'>, C<'BC'>, C<'BCE'>, or
260             C<'CE'> without regard to case. The era indicator may be separated from
261             the year by white space, and be followed by a non-digit separator
262             character.
263              
264             Unless the era is specified, years less than C<70> will have C<2000>
265             added, and years at least equal to C<70> but less than C<100> will have
266             C<1900> added.
267              
268             If L can be loaded, it will be used to get an epoch
269             from the parsed date, including zone.
270              
271             If L can B be loaded, L
272             will be used to get an epoch from the parsed date.
273             L has its own quirks when it sees a year in the
274             distant past. Zones other than C, C, C, and numeric offsets
275             will be handled by setting C<$ENV{TZ}> to the specified zone before
276             converting from local time to epoch. If this works for you, fine. If
277             not, tough. B
278              
279             =head1 METHODS
280              
281             This class supports no public methods over and above those documented in
282             its superclass
283             L.
284              
285             =head1 SUPPORT
286              
287             Support is by the author. Please file bug reports at
288             L,
289             L, or in
290             electronic mail to the author.
291              
292             =head1 AUTHOR
293              
294             Thomas R. Wyant, III F
295              
296             =head1 COPYRIGHT AND LICENSE
297              
298             Copyright (C) 2009-2026 by Thomas R. Wyant, III
299              
300             This program is free software; you can redistribute it and/or modify it
301             under the same terms as Perl 5.10.0. For more details, see the full text
302             of the licenses in the files F and F.
303              
304             This program is distributed in the hope that it will be useful, but
305             without any warranty; without even the implied warranty of
306             merchantability or fitness for a particular purpose.
307              
308             =cut
309              
310             __END__