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   2040 use strict;
  9         29  
  9         317  
4 9     9   57 use warnings;
  9         20  
  9         314  
5              
6 9         1151 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   59 };
  9         30  
11 9         707 use Astro::Coord::ECI::Utils 0.112 qw{
12 9     9   85 looks_like_number SECSPERDAY greg_time_gm greg_time_local };
  9         229  
13              
14 9     9   77 use parent qw{ Astro::App::Satpass2::ParseTime };
  9         19  
  9         67  
15              
16             our $VERSION = '0.051_01';
17              
18             my $package = __PACKAGE__;
19              
20             sub attribute_names {
21 14     14 1 31 my ( $self ) = @_;
22 14         73 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 1528 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 209 my ( $self, $string ) = @_;
107              
108 88         191 my @date;
109              
110             my $special_only;
111              
112             # ISO 8601 date
113 88 50       1174 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       286 if ( $3 ) {
124 0         0 @date = ( $era_cvt{ uc $3 }->( $2 + 0 ), $4, $5 );
125             } else {
126 88         385 @date = ( $1, $4, $5 );
127 88         402 $date[0] =~ s/ [^0-9] \z //smx;
128 88 100       324 $date[0] < 70
129             and $date[0] += 2000;
130 88 50       296 $date[0] < 100
131             and $date[0] += 1900;
132             }
133              
134 88 100       238 defined $date[1]
135             or $date[1] = 1;
136 88 100       237 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       427 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     526 push @date, $1, $2 || 0, $3 || 0, $4 ? ".$4" : 0;
      100        
163 68         144 $special_only = 0;
164             } else {
165 20         53 push @date, ( 0 ) x 4;
166             }
167              
168             # We might have gobbled part of the zone.
169 88 100 66     563 not $special_only
170             and $string =~ m/ \G (?<= [^0-9] ) /smxgc
171             and pos $string -= 1;
172 88         533 my ( $zone ) = $string =~ m/ \G ( .* ) /smxgc;
173              
174 88         318 my ( $z, $offset ) = $self->_interpret_zone( $zone );
175 88 50       282 defined $offset
176             or return;
177              
178 88         260 return $make_epoch->( $self, $z, $offset, @date );
179             }
180              
181             }
182              
183             sub _interpret_zone {
184 88     88   219 my ( $self, $zone, $fatal ) = @_;
185 88 50       317 defined $zone
186             and $zone =~ s/ \A \s+ //smx;
187             $zone
188 88 50       222 or return ( @{ $self->{$package}{tz} || [ undef, 0 ] } );
  27 100       156  
189 61 50       442 if ( $zone =~ m/ \A $zone_re \z /smxo ) {
190 61 100       306 $1
191             and return ( UTC => 0 );
192 5   50     34 my $offset = ( ( $3 || 0 ) * 60 + ( $4 || 0 ) ) * 60;
      100        
193 5 100 66     27 $2
194             and '-' eq $2
195             or $offset = - $offset;
196 5         18 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 20 my ( $self, @args ) = @_;
212 7 50       39 if ( @args ) {
213 7 50 33     45 if ( defined $args[0] && $args[0] ne '' ) {
214             $self->{$package}{tz} = [
215 0         0 $self->_interpret_zone( $args[0], 1 ) ];
216             } else {
217 7         26 delete $self->{$package}{tz};
218             }
219             }
220 7         73 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-2023 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 directory LICENSES.
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__