File Coverage

blib/lib/Astro/Coord/ECI/Utils.pm
Criterion Covered Total %
statement 354 411 86.1
branch 102 168 60.7
condition 29 62 46.7
subroutine 75 80 93.7
pod 38 38 100.0
total 598 759 78.7


line stmt bran cond sub pod time code
1             =head1 NAME
2              
3             Astro::Coord::ECI::Utils - Utility routines for astronomical calculations
4              
5             =head1 SYNOPSIS
6              
7             use Astro::Coord::ECI::Utils qw{ :all };
8             my $now = time ();
9             print "The current Julian day is ", julianday ($now);
10              
11             =head2 UN-DEPRECATION NOTICE
12              
13             In version 0.131, C and C were
14             deprecated in favor of C and
15             C.
16              
17             I later realized that this was wrong. The problem with it was that
18             astronomical dates are often given in the Julian calendar before October
19             15 1582, which the previously-recognized subroutines do not handle.
20              
21             As of version 0.132 the deprecation of these subroutines is retracted,
22             and all reference to it (other than this notice) will be removed.
23              
24             =head1 DESCRIPTION
25              
26             This module was written to provide a home for all the constants and
27             utility subroutines used by B and its descendants.
28             What ended up here was anything that was essentially a subroutine, not
29             a method.
30              
31             Because figuring out how to convert to and from Perl time bids fair to
32             become complicated, this module is also responsible for figuring out how
33             that is done, and exporting whatever is needful to export. See C<:time>
34             below for the gory details.
35              
36             This module exports nothing by default. But all the constants,
37             variables, and subroutines documented below are exportable, and the
38             following export tags may be used:
39              
40             =over
41              
42             =item :all
43              
44             This imports everything exportable into your name space.
45              
46             =item :greg_time
47              
48             This imports all time routines except the discouraged routines
49             C and C.
50              
51             =item :mainstream
52              
53             This imports everything not deprecated into your name space.
54              
55             =item :params
56              
57             This imports the parameter validation routines C<__classisa> and
58             C<__instance>.
59              
60             =item :ref
61              
62             This imports all the C<*_REF> constants.
63              
64             =item :time
65              
66             This imports the time routines into your name space. If
67             L is available, it will be loaded, and both
68             this tag and C<:all> will import C, C, C,
69             C, C, and C into your name
70             space. Otherwise, C will be loaded, and both
71             this tag and C<:all> will import C, C,
72             C, and C into your name space.
73              
74             =item :vector
75              
76             This imports the vector arithmetic routines. It includes anything whose
77             name begins with C<'vector_'>.
78              
79             =back
80              
81             Under Perl 5.6 you may find that, if you use any of the above tags, you
82             need to specify them first in your import list.
83              
84             =head2 The following constants are exportable:
85              
86             AU = number of kilometers in an astronomical unit
87             JD_OF_EPOCH = the Julian Day of Perl epoch 0
88             LIGHTYEAR = number of kilometers in a light year
89             PARSEC = number of kilometers in a parsec
90             PERL2000 = January 1 2000, 12 noon universal, in Perl time
91             PI = the circle ratio, computed as atan2 (0, -1)
92             PIOVER2 = half the circle ratio
93             SECSPERDAY = the number of seconds in a day
94             SECS_PER_SIDERIAL_DAY = seconds in a siderial day
95             SECS_PER_TROPICAL_YEAR = seconds in a tropical year
96             SPEED_OF_LIGHT = speed of light in kilometers per second
97             TWOPI = twice the circle ratio
98              
99             ARRAY_REF = 'ARRAY'
100             CODE_REF = 'CODE'
101             HASH_REF = 'HASH'
102             SCALAR_REF = 'SCALAR'
103              
104             =head2 The following global variables are exportable:
105              
106             =head3 $DATETIMEFORMAT
107              
108             This variable represents the POSIX::strftime format used to convert
109             times to strings. The default value is '%a %b %d %Y %H:%M:%S' to be
110             consistent with the behavior of gmtime (or, to be precise, the
111             behavior of ctime as documented on my system).
112              
113             =head3 $JD_GREGORIAN
114              
115             This variable represents the Julian Day of the switch from Julian to
116             Gregorian calendars. This is used by date2jd(), jd2date(), and the
117             routines which depend on them, for deciding whether the date is to be
118             interpreted as in the Julian or Gregorian calendar. Its initial setting
119             is 2299160.5, which represents midnight October 15 1582 in the Gregorian
120             calendar, which is the date that calendar was first adopted. This is
121             slightly different than the value of 2299161 (noon of the same day) used
122             by Jean Meeus.
123              
124             If you are interested in historical calculations, you may wish to reset
125             this appropriately. If you use date2jd to calculate the new value, be
126             aware of the effect the current setting of $JD_GREGORIAN has on the
127             interpretation of the date you give.
128              
129             =head2 In addition, the following subroutines are exportable:
130              
131             =over 4
132              
133             =cut
134              
135             package Astro::Coord::ECI::Utils;
136              
137 18     18   346893 use strict;
  18         36  
  18         724  
138 18     18   92 use warnings;
  18         41  
  18         1767  
139              
140             our $VERSION = '0.134';
141             our @ISA = qw{Exporter};
142              
143 18     18   132 use Carp;
  18         34  
  18         1950  
144             ## use Config;
145             ## use Data::Dumper;
146 18     18   10086 use POSIX qw{ floor modf strftime };
  18         152115  
  18         122  
147 18     18   32700 use Scalar::Util qw{ blessed };
  18         47  
  18         10287  
148              
149             my @greg_time_routines;
150              
151             BEGIN {
152              
153             # NOTE WELL
154             #
155             # The logic here should be consistent with the optional-module text
156             # emitted by inc/Astro/Coord/ECI/Recommend.pm.
157             #
158              
159             eval {
160 18         2655 require Time::y2038;
161 0         0 Time::y2038->import( qw{ gmtime localtime } );
162              
163             # sub time_gm
164             *time_gm = sub {
165 0         0 my @date = @_;
166 0         0 $date[5] = _year_adjust_y2038( $date[5] );
167 0         0 return Time::y2038::timegm( @date );
168 0         0 };
169             # greg_time_local
170             *greg_time_gm = sub {
171 0         0 my @date = @_;
172 0         0 $date[5] -= 1900;
173 0         0 return Time::y2038::timegm( @date );
174 0         0 };
175              
176             # sub time_local
177             *time_local = sub {
178 0         0 my @date = @_;
179 0         0 $date[5] = _year_adjust_y2038( $date[5] );
180 0         0 return Time::y2038::timelocal( @date );
181 0         0 };
182             # sub greg_time_local
183             *greg_time_local = sub {
184 0         0 my @date = @_;
185 0         0 $date[5] -= 1900;
186 0         0 return Time::y2038::timelocal( @date );
187 0         0 };
188              
189 0         0 @greg_time_routines = qw{
190             gmtime localtime
191             greg_time_gm greg_time_local
192             __tle_year_to_Gregorian_year
193             };
194              
195 0         0 1;
196 18 50   18   84 } or do {
197 18         8383 require Time::Local;
198              
199             # sub time_gm
200 18         47464 *time_gm = Time::Local->can( 'timegm' );
201             # sub greg_time_gm
202             *greg_time_gm = Time::Local->can( 'timegm_modern' ) || sub {
203             my @date = @_;
204             $date[5] = _year_adjust_greg( $date[5] );
205             return Time::Local::timegm( @date );
206 18   33     123 };
207              
208             # sub time_local
209 18         72 *time_local = Time::Local->can( 'timelocal' );
210             # sub greg_time_local
211             *greg_time_local = Time::Local->can( 'timelocal_modern' ) || sub {
212             my @date = @_;
213             $date[5] = _year_adjust_greg( $date[5] );
214             return Time::Local::timelocal( @date );
215 18   33     94 };
216              
217 18         1572 @greg_time_routines = qw{
218             greg_time_gm greg_time_local
219             __tle_year_to_Gregorian_year
220             };
221              
222             };
223             }
224              
225             # This subroutine is used to convert year numbers to Perl years in
226             # accordance with the documentation in the 5.24.0 version of
227             # Time::Local. It is intended to be called by the Time::y2038 code,
228             # which expects Perl years.
229              
230             {
231             # The following code is lifted verbatim from Time::Local 1.25.
232             # Because that code bases the window used for expanding two-digit
233             # years on the local year as of the time the module was loaded, I do
234             # too.
235              
236             my $ThisYear = ( localtime() )[5];
237             my $Breakpoint = ( $ThisYear + 50 ) % 100;
238             my $NextCentury = $ThisYear - $ThisYear % 100;
239             $NextCentury += 100 if $Breakpoint < 50;
240             my $Century = $NextCentury - 100;
241              
242             # The above code is lifted verbatim from Time::Local 1.25.
243              
244 18         11605 use constant NOT_GREG =>
245 18     18   185 '%d not interpreted as Gregorian year by Time::Local::timegm';
  18         34  
246              
247             # Adujst the year so that the Time::y2038 implementation of
248             # time_gm() and time_local() mirrors the Time::Local timegm() and
249             # timelocal() behavior. Kinda sorta.
250             sub _year_adjust_y2038 {
251 0     0   0 my ( $year ) = @_;
252              
253 0 0       0 $year < 0
254             and return $year;
255              
256 0 0       0 $year >= 1000
257             and return $year - 1900;
258              
259             # The following line of code is lifted verbatim from Time::Local
260             # 1.25.
261 0 0       0 $year += ( $year > $Breakpoint ) ? $Century : $NextCentury;
262              
263 0         0 return $year;
264             }
265             }
266              
267             # Adjust a Gregorian year so that Time::Local timegm() and timelocal()
268             # return epochs in that year.
269             sub _year_adjust_greg {
270 0     0   0 my ( $year ) = @_;
271 0 0       0 return $year >= 1000 ? $year : $year - 1900;
272             }
273              
274             our @CARP_NOT = qw{
275             Astro::Coord::ECI
276             Astro::Coord::ECI::Mixin
277             Astro::Coord::ECI::Moon
278             Astro::Coord::ECI::Star
279             Astro::Coord::ECI::Sun
280             Astro::Coord::ECI::TLE
281             Astro::Coord::ECI::TLE::Set
282             Astro::Coord::ECI::Utils
283             };
284              
285             our @EXPORT;
286             my @all_external = ( qw{
287             AU $DATETIMEFORMAT $JD_GREGORIAN JD_OF_EPOCH LIGHTYEAR PARSEC
288             PERL2000 PI PIOVER2
289             SECSPERDAY SECS_PER_SIDERIAL_DAY SECS_PER_TROPICAL_YEAR
290             SPEED_OF_LIGHT TWOPI
291             ARRAY_REF CODE_REF HASH_REF SCALAR_REF
292             acos add_magnitudes asin
293             atmospheric_extinction date2epoch date2jd
294             decode_space_track_json_time deg2rad distsq dynamical_delta
295             embodies epoch2datetime find_first_true
296             fold_case __format_epoch_time_usec
297             format_space_track_json_time gm_strftime intensity_to_magnitude
298             jcent2000 jd2date jd2datetime jday2000 julianday
299             keplers_equation load_module local_strftime
300             looks_like_number max min mod2pi mod360
301             omega position_angle
302             rad2deg rad2dms rad2hms tan theta0 thetag vector_cross_product
303             vector_dot_product vector_magnitude vector_unitize __classisa
304             __default_station __instance __subroutine_deprecation
305             __sprintf
306             },
307             qw{ time_gm time_local }, @greg_time_routines );
308             our @EXPORT_OK = (
309             qw{ @CARP_NOT }, # Package-private, undocumented
310             @all_external,
311             );
312              
313             my %deprecated_export = map { $_ => 1 } qw{
314             };
315              
316             our %EXPORT_TAGS = (
317             all => \@all_external,
318             greg_time => \@greg_time_routines,
319             mainstream => [ grep { ! $deprecated_export{$_} } @all_external ],
320             params => [ qw{ __classisa __instance } ],
321             ref => [ grep { m/ [[:upper:]]+ _REF \z /smx } @all_external ],
322             time => [ qw{ gm_strftime local_strftime time_gm time_local },
323             @greg_time_routines ],
324             vector => [ grep { m/ \A vector_ /smx } @all_external ],
325             );
326              
327 18     18   229 use constant AU => 149597870; # 1 astronomical unit, per
  18         41  
  18         1308  
328             # Meeus, Appendix I pg 407.
329 18     18   131 use constant LIGHTYEAR => 9.4607e12; # 1 light-year, per Meeus,
  18         35  
  18         935  
330             # Appendix I pg 407.
331 18     18   88 use constant PARSEC => 30.8568e12; # 1 parsec, per Meeus,
  18         32  
  18         982  
332             # Appendix I pg 407.
333 18     18   100 use constant PERL2000 => greg_time_gm( 0, 0, 12, 1, 0, 2000 );
  18         28  
  18         86  
334 18     18   2008 use constant PI => atan2 (0, -1);
  18         32  
  18         961  
335 18     18   87 use constant PIOVER2 => PI / 2;
  18         36  
  18         784  
336 18     18   84 use constant SECSPERDAY => 86400;
  18         32  
  18         838  
337 18     18   78 use constant SECS_PER_SIDERIAL_DAY => 86164.0905; # Appendix I, page 408.
  18         34  
  18         886  
338             # Meeus Appendix I
339 18     18   88 use constant SECS_PER_TROPICAL_YEAR => 365.242190 * SECSPERDAY;
  18         36  
  18         776  
340 18     18   80 use constant SPEED_OF_LIGHT => 299792.458; # KM/sec, per NIST.
  18         31  
  18         826  
341             ### use constant SOLAR_RADIUS => 1392000 / 2; # Meeus, Appendix I, page 407.
342 18     18   84 use constant TWOPI => PI * 2;
  18         27  
  18         913  
343              
344 18     18   87 use constant ARRAY_REF => ref [];
  18         42  
  18         1288  
345 18     18   94 use constant CODE_REF => ref sub {};
  18         31  
  18         1046  
346 18     18   82 use constant HASH_REF => ref {};
  18         32  
  18         985  
347 18     18   142 use constant SCALAR_REF => ref \0;
  18         91  
  18         7625  
348              
349             =item $angle = acos ($value)
350              
351             This subroutine calculates the arc in radians whose cosine is the given
352             value.
353              
354             =cut
355              
356             sub acos {
357 919 50   919 1 4262 abs ($_[0]) > 1 and confess <
358             Programming error - Trying to take the arc cosine of a number greater
359             than 1.
360             eod
361 919         6840 return atan2 (sqrt (1 - $_[0] * $_[0]), $_[0])
362             }
363              
364             =item $mag = add_magnitudes( $mag1, $mag2, ... );
365              
366             This subroutine computes the total magnitude of a list of individual
367             magnitudes. The algorithm comes from Jean Meeus' "Astronomical
368             Algorithms", Second Edition, Chapter 56, Page 393.
369              
370             =cut
371              
372             sub add_magnitudes {
373 1     1 1 902 my @arg = @_;
374 1         4 my $sum = 0;
375 1         3 foreach my $mag ( @arg ) {
376 3         15 $sum += 10 ** ( -0.4 * $mag );
377             }
378 1         7 return -2.5 * log( $sum ) / log( 10 );
379             }
380              
381             =item $angle = asin ($value)
382              
383             This subroutine calculates the arc in radians whose sine is the given
384             value.
385              
386             =cut
387              
388 51640     51640 1 135825 sub asin {return atan2 ($_[0], sqrt (1 - $_[0] * $_[0]))}
389              
390             =item $magnitude = atmospheric_extinction ($elevation, $height);
391              
392             This subroutine calculates the typical atmospheric extinction in
393             magnitudes at the given elevation above the horizon in radians and the
394             given height above sea level in kilometers.
395              
396             The algorithm comes from Daniel W. E. Green's article "Magnitude
397             Corrections for Atmospheric Extinction", which was published in
398             the July 1992 issue of "International Comet Quarterly", and is
399             available online at
400             L. The text of
401             this article makes it clear that the actual value of the
402             atmospheric extinction can vary greatly from the typical
403             values given even in the absence of cloud cover.
404              
405             =cut
406              
407             # Note that the "constant" 0.120 in Aaer (aerosol scattering) is
408             # based on a compromise value A0 = 0.050 in Green's equation 3
409             # (not exhibited here), which can vary from 0.035 in the winter to
410             # 0.065 in the summer. This makes a difference of a couple tenths
411             # at 20 degrees elevation, but a couple magnitudes at the
412             # horizon. Green also remarks that the 1.5 denominator in the
413             # same equation (a.k.a. the scale height) can be up to twice
414             # that.
415              
416             sub atmospheric_extinction {
417 3     3 1 2734 my ($elevation, $height) = @_;
418 3         13 my $cosZ = cos (PIOVER2 - $elevation);
419 3         12 my $X = 1/($cosZ + 0.025 * exp (-11 * $cosZ)); # Green 1
420 3         10 my $Aray = 0.1451 * exp (-$height / 7.996); # Green 2
421 3         9 my $Aaer = 0.120 * exp (-$height / 1.5); # Green 4
422 3         10 return ($Aray + $Aaer + 0.016) * $X; # Green 5, 6
423             }
424              
425             =item $jd = date2jd ($sec, $min, $hr, $day, $mon, $yr)
426              
427             This subroutine converts the given date to the corresponding Julian day.
428             The inputs are a Perl date and time; $mon is in the range 0 -
429             11, and $yr is from 1900, with earlier years being negative. The year 1
430             BC is represented as -1900.
431              
432             If less than 6 arguments are provided, zeroes will be prepended to the
433             argument list as needed.
434              
435             The date is presumed to be in the Gregorian calendar. If the resultant
436             Julian Day is before $JD_GREGORIAN, the date is reinterpreted as being
437             from the Julian calendar.
438              
439             The only validation is that the month be between 0 and 11 inclusive, and
440             that the year be not less than -6612 (4713 BC). Fractional days are
441             accepted.
442              
443             The algorithm is from Jean Meeus' "Astronomical Algorithms", second
444             edition, chapter 7 ("Julian Day"), pages 60ff, but the month is
445             zero-based, not 1-based, and years are 1900-based.
446              
447             =cut
448              
449             our $DATETIMEFORMAT;
450             our $JD_GREGORIAN;
451             BEGIN {
452 18     18   95 $DATETIMEFORMAT = '%a %b %d %Y %H:%M:%S';
453 18         5602 $JD_GREGORIAN = 2299160.5;
454             }
455              
456             sub date2jd {
457 21     21 1 2003 my @args = @_;
458 21         168 unshift @args, 0 while @args < 6;
459 21         113 my ($sec, $min, $hr, $day, $mon, $yr) = @args;
460 21         73 $mon++; # Algorithm expects month 1-12.
461 21         55 $yr += 1900; # Algorithm expects zero-based year.
462 21 50       75 ($yr < -4712) and croak "Error - Invalid year $yr";
463 21 50 33     168 ($mon < 1 || $mon > 12) and croak "Error - Invalid month $mon";
464 21 100       89 if ($mon < 3) {
465 20         40 --$yr;
466 20         60 $mon += 12;
467             }
468 21         129 my $A = floor ($yr / 100);
469 21         113 my $B = 2 - $A + floor ($A / 4);
470 21   50     396 my $jd = floor (365.25 * ($yr + 4716)) +
      50        
      100        
471             floor (30.6001 * ($mon + 1)) + $day + $B - 1524.5 +
472             ((($sec || 0) / 60 + ($min || 0)) / 60 + ($hr || 0)) / 24;
473 21 100 50     110 $jd < $JD_GREGORIAN and
      50        
      50        
474             $jd = floor (365.25 * ($yr + 4716)) +
475             floor (30.6001 * ($mon + 1)) + $day - 1524.5 +
476             ((($sec || 0) / 60 + ($min || 0)) / 60 + ($hr || 0)) / 24;
477 21         47708 return $jd;
478             }
479              
480 18     18   161 use constant JD_OF_EPOCH => date2jd (gmtime (0));
  18         33  
  18         115  
481              
482             =item $epoch = date2epoch ($sec, $min, $hr, $day, $mon, $yr)
483              
484             This is a convenience routine that converts the given date to seconds
485             since the epoch, going through date2jd() to do so. The arguments are the
486             same as those of date2jd().
487              
488             If less than 6 arguments are provided, zeroes will be prepended to the
489             argument list as needed.
490              
491             The functionality is similar to C, but the
492             arguments will be interpreted according to the Julian calendar if the
493             date is before L<$JD_GREGORIAN|/$JD_GREGORIAN>.
494              
495             =cut
496              
497             sub date2epoch {
498 1     1 1 1004 my @args = @_;
499 1         7 __subroutine_deprecation();
500 1         9 unshift @args, 0 while @args < 6;
501 1         4 my ($sec, $min, $hr, $day, $mon, $yr) = @args;
502 1   50     7 return (date2jd ($day, $mon, $yr) - JD_OF_EPOCH) * SECSPERDAY +
      50        
      50        
503             (($hr || 0) * 60 + ($min || 0)) * 60 + ($sec || 0);
504             }
505              
506             =item $time = decode_space_track_json_time( $string )
507              
508             This subroutine decodes a time in the format Space Track uses in their
509             JSON code. This is ISO-8601-ish, but with a possible fractional part and
510             no zone.
511              
512             =cut
513              
514             sub decode_space_track_json_time {
515 0     0 1 0 my ( $string ) = @_;
516 0 0       0 $string =~ m{ \A \s*
517             ( [0-9]+ ) [^0-9]+ ( [0-9]+ ) [^0-9]+ ( [0-9]+ ) [^0-9]+
518             ( [0-9]+ ) [^0-9]+ ( [0-9]+ ) [^0-9]+ ( [0-9]+ ) (?: ( [.] [0-9]* ) )? \s* \z }smx
519             or return;
520 0         0 my @time = ( $1, $2, $3, $4, $5, $6 );
521 0         0 my $frac = $7;
522 0         0 $time[0] = __tle_year_to_Gregorian_year( $time[0] );
523 0         0 $time[1] -= 1;
524 0         0 my $rslt = greg_time_gm( reverse @time );
525 0 0 0     0 defined $frac
526             and $frac ne '.'
527             and $rslt += $frac;
528 0         0 return $rslt;
529             }
530              
531             # my ( $self, $station, @args ) = __default_station( @_ )
532             #
533             # This exportable subroutine checks whether the second argument embodies
534             # an Astro::Coord::ECI object. If so, the arguments are returned
535             # verbatim. If not, the 'station' attribute of the invocant is inserted
536             # after the first argument and the result returned. If the 'station'
537             # attribute of the invocant has not been set, an exception is thrown.
538              
539             sub __default_station {
540 25     25   82 my ( $self, @args ) = @_;
541 25 100       107 if ( ! embodies( $args[0], 'Astro::Coord::ECI' ) ) {
542 8 50       43 my $sta = $self->get( 'station' )
543             or croak 'Station attribute not set';
544 8         29 unshift @args, $sta;
545             }
546 25         113 return ( $self, @args );
547             }
548              
549             =item $rad = deg2rad ($degr)
550              
551             This subroutine converts degrees to radians. If the argument is
552             C, C will be returned.
553              
554             =cut
555              
556 796580 100   796580 1 2209457 sub deg2rad { return defined $_[0] ? $_[0] * PI / 180 : undef }
557              
558             =item $value = distsq (\@coord1, \@coord2)
559              
560             This subroutine calculates the square of the distance between the two
561             sets of Cartesian coordinates. We do not take the square root here
562             because of cases (e.g. the law of cosines) where we would just have
563             to square the result again.
564              
565             B that the subroutine does B assume three-dimensional
566             coordinates. If @coord1 and @coord2 have six entries, you will get a
567             six-dimensional distance.
568              
569             =cut
570              
571             sub distsq {
572 1     1 1 578 my ( $x, $y ) = @_;
573             ARRAY_REF eq ref $x
574             and ARRAY_REF eq ref $y
575 1 50 33     8 and @{ $x } == @{ $y }
  1   33     2  
  1         4  
576             or confess <<'EOD';
577             Programming error - Both arguments to distsq must be references to
578             arrays of the same length.
579             EOD
580              
581 1         2 my $sum = 0;
582 1         2 my $size = @$x;
583 1         4 for (my $inx = 0; $inx < $size; $inx++) {
584 2         4 my $delta = $x->[$inx] - $y->[$inx];
585 2         4 $sum += $delta * $delta;
586             }
587 1         3 return $sum
588             }
589              
590             =item $seconds = dynamical_delta ($time);
591              
592             This method returns the difference between dynamical and universal time
593             at the given universal time. That is,
594              
595             $dynamical = $time + dynamical_delta ($time)
596              
597             if $time is universal time.
598              
599             The algorithm is from Jean Meeus' "Astronomical Algorithms", 2nd
600             Edition, Chapter 10, page 78. Meeus notes that this is actually an
601             observed quantity, and the algorithm is an approximation.
602              
603             =cut
604              
605             sub dynamical_delta {
606 90114     90114 1 181425 my ($time) = @_;
607 90114         357185 my $year = (gmtime $time)[5] + 1900;
608 90114         219586 my $t = ($year - 2000) / 100;
609 90114         166950 my $correction = .37 * ($year - 2100); # Meeus' correction to (10.2)
610 90114         414453 return (25.3 * $t + 102) * $t + 102 # Meeus (10.2)
611             + $correction; # Meeus' correction.
612             }
613              
614             =item $boolean = embodies ($thingy, $class)
615              
616             This subroutine represents a safe way to call the 'represents' method on
617             $thingy. You get back true if and only if $thingy->can('represents')
618             does not throw an exception and returns true, and
619             $thingy->represents($class) returns true. Otherwise it returns false.
620             Any exception is trapped and dismissed.
621              
622             This subroutine is called 'embodies' because it was too confusing to
623             call it 'represents', both for the author and for the Perl interpreter.
624              
625             =cut
626              
627             sub embodies {
628 70367     70367 1 152994 my ($thingy, $class) = @_;
629 70367         122080 my $code = eval {$thingy->can('represents')};
  70367         282779  
630 70367 100       236639 return $code ? $code->($thingy, $class) : undef;
631             }
632              
633             =item ($sec, $min, $hr, $day, $mon, $yr, $wday, $yday, 0) = epoch2datetime ($epoch)
634              
635             This convenience subroutine converts the given time in seconds from the
636             system epoch to the corresponding date and time. It is implemented in
637             terms of jd2date (), with the year and month returned from that
638             subroutine. The day is a whole number, with the fractional part
639             converted to hours, minutes, and seconds. The $wday is the day of the
640             week, with Sunday being 0. The $yday is the day of the year, with
641             January 1 being 0. The trailing 0 is the summer time (or daylight saving
642             time) indicator which is always 0 to be consistent with gmtime.
643              
644             If called in scalar context, it returns the date formatted by
645             POSIX::strftime, using the format string in $DATETIMEFORMAT.
646              
647             The functionality is similar to C, but the result will
648             be in the Julian calendar if the date is before
649             L<$JD_GREGORIAN|/$JD_GREGORIAN>.
650              
651             The input must convert to a non-negative Julian date. The exact lower
652             limit depends on the system, but is computed by -(JD_OF_EPOCH * 86400).
653             For Unix systems with an epoch of January 1 1970, this is -210866760000.
654              
655             Additional algorithms for day of week and day of year come from Jean
656             Meeus' "Astronomical Algorithms", 2nd Edition, Chapter 7 (Julian Day),
657             page 65.
658              
659             =cut
660              
661             sub epoch2datetime {
662 27     27 1 5714 my ($time) = @_;
663 27         97 __subroutine_deprecation();
664 27         112 my $day = floor ($time / SECSPERDAY);
665 27         62 my $sec = $time - $day * SECSPERDAY;
666 27         100 ($day, my $mon, my $yr, undef, my $leap) = jd2date (
667             my $jd = $day + JD_OF_EPOCH);
668 27         95 $day = floor ($day + .5);
669 27         98 my $min = floor ($sec / 60);
670 27         56 $sec = $sec - $min * 60;
671 27         60 my $hr = floor ($min / 60);
672 27         47 $min = $min - $hr * 60;
673 27         54 my $wday = ($jd + 1.5) % 7;
674 27         117 my $yd = floor (275 * ($mon + 1) / 9) - (2 - $leap) * floor (($mon +
675             10) / 12) + $day - 31;
676 27 50       171 wantarray and return ($sec, $min, $hr, $day, $mon, $yr, $wday, $yd,
677             0);
678             # FIXME this seems to be needed under Perl 5.39.8, but not under
679             # earlier Perls.
680 0         0 local $ENV{TZ} = 'UTC';
681 0         0 return POSIX::strftime( $DATETIMEFORMAT, $sec, $min, $hr, $day,
682             $mon, $yr, $wday, $yd );
683             }
684              
685             =item $time = find_first_true ($start, $end, \&test, $limit);
686              
687             This function finds the first time between $start and $end for which
688             test ($time) is true. The resolution is $limit, which defaults to 1 if
689             not specified. If the times are reversed (i.e. the start time is after
690             the end time) the time returned is the last time test ($time) is true.
691              
692             The C function is called with the Perl time as its only
693             argument. It is assumed to be false for the first part of the interval,
694             and true for the rest. If this assumption is violated, the result of
695             this subroutine should be considered meaningless.
696              
697             The calculation is done by, essentially, a binary search; the interval
698             is repeatedly split, the function is evaluated at the midpoint, and a
699             new interval selected based on whether the result is true or false.
700              
701             Actually, nothing in this function says the independent variable has to
702             be time.
703              
704             =cut
705              
706             sub find_first_true {
707 421     421 1 1928 my ($begin, $end, $test, $limit) = @_;
708 421   100     2137 $limit ||= 1;
709 421 50       1255 defined $begin
710             or confess 'Programming error - $begin undefined';
711 421 50       1343 defined $end
712             or confess 'Programming error - $end undefined';
713 421 100       1193 if ($limit >= 1) {
714 375 50       1084 if ($begin <= $end) {
715 375         1303 $begin = floor ($begin);
716 375 50       1377 $end = floor ($end) == $end ? $end : floor ($end) + 1;
717             } else {
718 0         0 $end = floor ($end);
719 0 0       0 $begin = floor ($begin) == $begin ? $begin : floor ($begin) + 1;
720             }
721             }
722             my $iterator = (
723             $end > $begin ?
724 3993     3993   12452 sub {$end - $begin > $limit} :
725 0     0   0 sub {$begin - $end > $limit}
726 421 50       2531 );
727 421         1242 while ($iterator->()) {
728 3572 100       14171 my $mid = $limit >= 1 ?
729             floor (($begin + $end) / 2) : ($begin + $end) / 2;
730 3572 100       11192 ($begin, $end) = ($test->($mid)) ?
731             ($begin, $mid) : ($mid, $end);
732             }
733 421         3354 return $end;
734             }
735              
736             =item $folded = fold_case( $text );
737              
738             This function folds the case of its input, kinda sorta. It maps to
739             C if that is available, otherwise it maps to C.
740              
741             =cut
742              
743             *fold_case = CORE->can( 'fc' ) || sub ($) { return lc $_[0] };
744              
745             =item $fmtd = format_space_track_json_time( time() )
746              
747             This function takes as input a Perl time, and returns that time
748             in a format consistent with the Space Track JSON data. This is
749             ISO-8601-ish, in Universal time, but without the zone indicated.
750              
751             B that Space Track does not represent fractional seconds, even in
752             the epoch. This subroutine deals with this by truncating the epoch to
753             seconds, and leaving the fractional seconds to the caller to deal with.
754              
755             =cut
756              
757             sub format_space_track_json_time {
758 1     1 1 52 my ( $time ) = @_;
759 1 50 33     9 defined $time
760             and $time =~ m/ \S /smx
761             or return;
762 1         6 my ( undef, $sec ) = modf( $time );
763 1         5 my @parts = ( gmtime $sec )[ reverse 0 .. 5 ];
764 1         2 $parts[0] += 1900;
765 1         2 $parts[1] += 1;
766 1         6 return sprintf '%04d-%02d-%02d %02d:%02d:%02d', @parts;
767             }
768              
769             =item $fmtd = __format_epoch_time_usec( time(), '%F %T' )
770              
771             This function takes as input a Perl time with a possible fractional
772             part, and returns that time as GMT in the given C format, but
773             with seconds expressed to the nearest microsecond.
774              
775             =cut
776              
777             sub __format_epoch_time_usec {
778 1     1   736 my ( $epoch, $date_format ) = @_;
779 1         4 return gm_strftime( $date_format, $epoch, 6 );
780             }
781              
782             =item print gm_strftime( $format, $epoch, $places )
783              
784             This subroutine takes as input a strftime-compatible format and an
785             epoch, and returns the GMT, formatted per the format.
786              
787             Optional argument C<$places> is the default number of decimal places for
788             seconds. If defined, it must be either C<''> or an unsigned integer.
789              
790             You can also specify an optional C<'.d'> (where the 'd' is one or more
791             decimal digits) before any format specification that generates seconds.
792             Examples include C<'%.3S'> or C<'%.6T'>. Such a specification overrides
793             the C<$places> argument, if any.
794              
795             =cut
796              
797             sub gm_strftime {
798 10     10 1 71 my ( $format, $epoch ) = _pre_strftime( @_ );
799 10         43 my @gmt = gmtime $epoch;
800 10         15 pop @gmt;
801             # FIXME this seems to be needed under Perl 5.39.8, but not under
802             # earlier Perls.
803 10         96 local $ENV{TZ} = 'UTC';
804 10         255 return POSIX::strftime( $format, @gmt );
805             }
806              
807             =item $epoch = greg_time_gm( $sec, $min, $hr, $day, $mon, $yr );
808              
809             This exportable subroutine is a wrapper for either
810             C (if that module is installed),
811             C (if that is available), or
812             C (if not.)
813              
814             This subroutine interprets years as Gregorian years.
815              
816             The difference between this and C is that C
817             interprets the year the way C does. For that
818             reason, this subroutine is preferred over c.
819              
820             This wrapper is needed because the routines have subtly different
821             signatures. L C interprets years
822             strictly as Perl years. L C
823             interprets them strictly as Gregorian years. L
824             C interprets them differently depending on the value of the
825             year. Years greater than or equal to 1000 are Gregorian years, but all
826             others are Perl years, except for the range 0 to 99 inclusive, which are
827             within 50 years of the current year.
828              
829             If you are doing historical calculations, see
830             L
831             in the L documentation
832             for a discussion of input and output time conversion.
833              
834             =item $epoch = greg_time_local( $sec, $min, $hr, $day, $mon, $yr );
835              
836             This exportable subroutine is a wrapper for either
837             C (if that module is installed),
838             C (if that is available), or
839             C (if not.)
840              
841             This subroutine interprets years as Gregorian years.
842              
843             The difference between this and c is that C
844             interprets the year the way C does. For that
845             reason, this subroutine is preferred over c.
846              
847             This wrapper is needed for the same reason C is
848             needed.
849              
850             If you are doing historical calculations, see
851             L
852             in the L documentation
853             for a discussion of input and output time conversion.
854              
855             =item $difference = intensity_to_magnitude ($ratio)
856              
857             This function converts a ratio of light intensities to a difference in
858             stellar magnitudes. The algorithm comes from Jean Meeus' "Astronomical
859             Algorithms", Second Edition, Chapter 56, Page 395.
860              
861             Note that, because of the way magnitudes work (a more negative number
862             represents a brighter star) you get back a positive result for an
863             intensity ratio less than 1, and a negative result for an intensity
864             ratio greater than 1.
865              
866             =cut
867              
868             { # Begin local symbol block
869             my $intensity_to_mag_factor; # Calculate only if needed.
870             sub intensity_to_magnitude {
871 1   50 1 1 969 return - ($intensity_to_mag_factor ||= 2.5 / log (10)) * log ($_[0]);
872             }
873             }
874              
875             =item ($day, $mon, $yr, $greg, $leap) = jd2date ($jd)
876              
877             This subroutine converts the given Julian day to the corresponding date.
878             The returns are year - 1900, month (0 to 11), day (which may have a
879             fractional part), a Gregorian calendar indicator which is true if the
880             date is in the Gregorian calendar and false if it is in the Julian
881             calendar, and a leap (or bissextile) year indicator which is true if the
882             year is a leap year and false otherwise. The year 1 BC is returned as
883             -1900 (i.e. as year 0), and so on. The date will probably have a
884             fractional part (e.g. 2006 1 1.5 for noon January first 2006).
885              
886             If the $jd is before $JD_GREGORIAN, the date will be in the Julian
887             calendar; otherwise it will be in the Gregorian calendar.
888              
889             The input may not be less than 0.
890              
891             The algorithm is from Jean Meeus' "Astronomical Algorithms", second
892             edition, chapter 7 ("Julian Day"), pages 63ff, but the month is
893             zero-based, not 1-based, and the year is 1900-based.
894              
895             =cut
896              
897             sub jd2date {
898 36     36 1 9173 my ($time) = @_;
899 36         74 my $mod_jd = $time + 0.5;
900 36         104 my $Z = floor ($mod_jd);
901 36         72 my $F = $mod_jd - $Z;
902 36 100       111 my $A = (my $julian = $Z < $JD_GREGORIAN) ? $Z : do {
903 30         75 my $alpha = floor (($Z - 1867216.25)/36524.25);
904 30         137 $Z + 1 + $alpha - floor ($alpha / 4);
905             };
906 36         69 my $B = $A + 1524;
907 36         92 my $C = floor (($B - 122.1) / 365.25);
908 36         86 my $D = floor (365.25 * $C);
909 36         99 my $E = floor (($B - $D) / 30.6001);
910 36         101 my $day = $B - $D - floor (30.6001 * $E) + $F;
911 36 100       99 my $mon = $E < 14 ? $E - 1 : $E - 13;
912 36 100       83 my $yr = $mon > 2 ? $C - 4716 : $C - 4715;
913 36 50       217 return ($day, $mon - 1, $yr - 1900, !$julian, ($julian ? !($yr % 4) : (
    100          
    100          
914             $yr % 400 ? $yr % 100 ? !($yr % 4) : 0 : 1)));
915             ## % 400 ? 1 : $yr % 100 ? 0 : !($yr % 4))));
916             }
917              
918             =item ($sec, $min, $hr, $day, $mon, $yr, $wday, $yday, 0) = jd2datetime ($jd)
919              
920             This convenience subroutine converts the given Julian day to the
921             corresponding date and time. All this really does is converts its
922             argument to seconds since the system epoch, and pass off to
923             epoch2datetime().
924              
925             The input may not be less than 0.
926              
927             =cut
928              
929             sub jd2datetime {
930 21     21 1 19707 my ($time) = @_;
931 21         88 return epoch2datetime(($time - JD_OF_EPOCH) * SECSPERDAY);
932             }
933              
934             =item $century = jcent2000 ($time);
935              
936             Several of the algorithms in Jean Meeus' "Astronomical Algorithms"
937             are expressed in terms of the number of Julian centuries from epoch
938             J2000.0 (e.g equations 12.1, 22.1). This subroutine encapsulates
939             that calculation.
940              
941             =cut
942              
943 309904     309904 1 666648 sub jcent2000 {return jday2000 ($_[0]) / 36525}
944              
945             =item $jd = jday2000 ($time);
946              
947             This subroutine converts a Perl date to the number of Julian days
948             (and fractions thereof) since Julian 2000.0. This quantity is used
949             in a number of the algorithms in Jean Meeus' "Astronomical
950             Algorithms".
951              
952             The computation makes use of information from Jean Meeus' "Astronomical
953             Algorithms", 2nd Edition, Chapter 7, page 62.
954              
955             =cut
956              
957 398154     398154 1 1147713 sub jday2000 {return ($_[0] - PERL2000) / SECSPERDAY}
958              
959             =item $jd = julianday ($time);
960              
961             This subroutine converts a Perl date to a Julian day number.
962              
963             The computation makes use of information from Jean Meeus' "Astronomical
964             Algorithms", 2nd Edition, Chapter 7, page 62.
965              
966             =cut
967              
968 2     2 1 1922 sub julianday {return jday2000($_[0]) + 2_451_545.0}
969              
970             =item $ea = keplers_equation( $M, $e, $prec );
971              
972             This subroutine solves Kepler's equation for the given mean anomaly
973             C<$M> in radians, eccentricity C<$e> and precision C<$prec> in radians.
974             It returns the eccentric anomaly in radians, to the given precision.
975              
976             The C<$prec> argument is optional, and defaults to the radian equivalent
977             of 0.001 degrees.
978              
979             The algorithm is Meeus' equation 30.7, with John M. Steele's amendment
980             for large values for the correction, given on page 205 of Meeus' book,
981              
982             This subroutine is B used in the computation of satellite orbits,
983             since the models have their own implementation.
984              
985             =cut
986              
987             sub keplers_equation {
988 1     1 1 603 my ( $mean_anomaly, $eccentricity, $precision ) = @_;
989 1 50       3 defined $precision
990             or $precision = 1.74533e-5; # Radians, = 0.001 degrees
991 1         2 my $curr = $mean_anomaly;
992 1         1 my $prev;
993             # Meeus' equation 30.7, page 199.
994             {
995 1         2 $prev = $curr;
  3         4  
996 3         8 my $delta = ( $mean_anomaly + $eccentricity * sin( $curr
997             ) - $curr ) / ( 1 - $eccentricity * cos $curr );
998             # Steele's correction, page 205
999 3         7 $curr = $curr + max( -.5, min( .5, $delta ) );
1000 3 100       9 $precision < abs( $curr - $prev )
1001             and redo;
1002             }
1003 1         2 return $curr;
1004             }
1005              
1006             =item $rslt = load_module ($module_name)
1007              
1008             This convenience method loads the named module (using 'require'),
1009             throwing an exception if the load fails. If the load succeeds, it
1010             returns the result of the 'require' built-in, which is required to be
1011             true for a successful load. Results are cached, and subsequent attempts
1012             to load the same module simply give the cached results.
1013              
1014             =cut
1015              
1016             { # Local symbol block. Oh, for 5.10 and state variables.
1017             my %error;
1018             my %rslt;
1019             sub load_module {
1020 243     243 1 481 my ($module) = @_;
1021 243 50       561 exists $error{$module} and croak $error{$module};
1022 243 100       736 exists $rslt{$module} and return $rslt{$module};
1023             # I considered Module::Load here, but it appears not to support
1024             # .pmc files. No, it's not an issue at the moment, but it may be
1025             # if Perl 6 becomes a reality.
1026             $rslt{$module} = eval "require $module"
1027 48 50       4060 or croak( $error{$module} = $@ );
1028 48         284 return $rslt{$module};
1029             }
1030             } # End local symbol block.
1031              
1032             =item print local_strftime( $format, $epoch, $places )
1033              
1034             This subroutine takes as input a strftime-compatible format and an
1035             epoch, and returns the local time, formatted per the format.
1036              
1037             Optional argument C<$places> is the default number of decimal places for
1038             seconds. If defined, it must be either C<''> or an unsigned integer.
1039              
1040             You can also specify an optional C<'.d'> (where the 'd' is one or more
1041             decimal digits) before any format specification that generates seconds.
1042             Examples include C<'%.3S'> or C<'%.6T'>. Such a specification overrides
1043             the C<$places> argument, if any.
1044              
1045             =cut
1046              
1047             sub local_strftime {
1048 0     0 1 0 my ( $format, $epoch ) = _pre_strftime( @_ );
1049 0         0 return POSIX::strftime( $format, localtime $epoch );
1050             }
1051              
1052             =item $boolean = looks_like_number ($string);
1053              
1054             This subroutine returns true if the input looks like a number. It uses
1055             Scalar::Util::looks_like_number if that is available, otherwise it uses
1056             its own code, which is lifted verbatim from Scalar::Util 1.19, which in
1057             turn leans heavily on perlfaq4.
1058              
1059             =cut
1060              
1061             unless (eval {require Scalar::Util; Scalar::Util->import
1062             ('looks_like_number'); 1}) {
1063 18     18   166 no warnings qw{once};
  18         36  
  18         7565  
1064             *looks_like_number = sub {
1065             local $_ = shift;
1066              
1067             # checks from perlfaq4
1068             return 0 if !defined($_) || ref($_);
1069             return 1 if (/^[+-]?[0-9]+$/); # is a +/- integer
1070             return 1 if (/^([+-]?)(?=[0-9]|\.[0-9])[0-9]*(\.[0-9]*)?([Ee]([+-]?[0-9]+))?$/); # a C float
1071             return 1 if ($] >= 5.008 and /^(Inf(inity)?|NaN)$/i)
1072             or ($] >= 5.006001 and /^Inf$/i);
1073              
1074             return 0;
1075             };
1076             }
1077              
1078             =item $maximum = max (...);
1079              
1080             This subroutine returns the maximum of its arguments. If List::Util can
1081             be loaded and 'max' imported, that's what you get. Otherwise you get a
1082             pure Perl implementation.
1083              
1084             =cut
1085              
1086             unless (eval {require List::Util; List::Util->import ('max'); 1}) {
1087 18     18   174 no warnings qw{once};
  18         33  
  18         2926  
1088             *max = sub {
1089             my $rslt;
1090             foreach (@_) {
1091             defined $_ or next;
1092             if (!defined $rslt || $_ > $rslt) {
1093             $rslt = $_;
1094             }
1095             }
1096             $rslt;
1097             };
1098             }
1099              
1100             =item $minimum = min (...);
1101              
1102             This subroutine returns the minimum of its arguments. If List::Util can
1103             be loaded and 'min' imported, that's what you get. Otherwise you get a
1104             pure Perl implementation.
1105              
1106             =cut
1107              
1108             unless (eval {require List::Util; List::Util->import ('min'); 1}) {
1109 18     18   124 no warnings qw{once};
  18         42  
  18         53104  
1110             *min = sub {
1111             my $rslt;
1112             foreach (@_) {
1113             defined $_ or next;
1114             if (!defined $rslt || $_ < $rslt) {
1115             $rslt = $_;
1116             }
1117             }
1118             $rslt;
1119             };
1120             }
1121              
1122             =item $theta = mod2pi ($theta)
1123              
1124             This subroutine reduces the given angle in radians to the range 0 <=
1125             $theta < TWOPI.
1126              
1127             =cut
1128              
1129 712402     712402 1 2148962 sub mod2pi {return $_[0] - floor ($_[0] / TWOPI) * TWOPI}
1130              
1131             =item $theta = mod360( $theta )
1132              
1133             This subroutine reduces the given angle in degrees to the range 0 <=
1134             $theta < 360. This is B equivalent to C<$theta % 360> because the
1135             latter loses the fractional part of $theta. It is B equivalent to
1136             C because the result of this subroutine is never
1137             negative.
1138              
1139             =cut
1140              
1141 1     1 1 900 sub mod360 { return $_[0] - floor( $_[0] / 360 ) * 360 }
1142              
1143             =item $radians = omega ($time);
1144              
1145             This subroutine calculates the ecliptic longitude of the ascending node
1146             of the Moon's mean orbit at the given B time.
1147              
1148             The algorithm comes from Jean Meeus' "Astronomical Algorithms", 2nd
1149             Edition, Chapter 22, pages 143ff.
1150              
1151             =cut
1152              
1153             sub omega {
1154 1     1 1 931 my $T = jcent2000 (shift); # Meeus (22.1)
1155 1         8 return mod2pi (deg2rad ((($T / 450000 + .0020708) * $T -
1156             1934.136261) * $T + 125.04452));
1157             }
1158              
1159             =item $pa = position_angle( $alpha1, $delta1, $alpha2, $delta2 );
1160              
1161             This low-level subroutine calculates the position angle in right
1162             ascension of the second body with respect to the first, given the first
1163             body's right ascension and declination and the second body's right
1164             ascension and declination in that order, B.
1165              
1166             The return is the position angle B, in the range
1167             C<< -PI <= $pa < PI >>.
1168              
1169             The algorithm comes from Jean Meeus' "Astronomical Algorithms", 2nd
1170             Edition, page 116, but his algorithm is for the position angle of the
1171             first body with respect to the second (i.e. the roles of the two bodies
1172             are reversed). The order of arguments for this subroutine is consistent
1173             with The IDL Astronomy User's Library at
1174             L, function C. The NASA
1175             page for this, L, is
1176             obsolete and no longer updated, but also more descriptive.
1177              
1178             This is exposed because in principal you could calculate the position
1179             angle in any spherical coordinate system, you would just need to get the
1180             order of arguments right (e.g. azimuth, elevation or longitude,
1181             latitude).
1182              
1183             =cut
1184              
1185             sub position_angle {
1186 1     1 1 1433 my ( $alpha1, $delta1, $alpha2, $delta2 ) = @_;
1187 1         3 my $delta_alpha = $alpha2 - $alpha1;
1188 1         6 return atan2( sin( $delta_alpha ),
1189             cos( $delta1 ) * tan( $delta2 ) -
1190             sin( $delta1 ) * cos( $delta_alpha ) );
1191             }
1192              
1193             =item $degrees = rad2deg ($radians)
1194              
1195             This subroutine converts the given angle in radians to its equivalent
1196             in degrees. If the argument is C, C will be returned.
1197              
1198             =cut
1199              
1200 29011 100   29011 1 112724 sub rad2deg { return defined $_[0] ? $_[0] / PI * 180 : undef }
1201              
1202             =item $deg_min_sec = rad2dms( $radians, $decimals, $degree_sign )
1203              
1204             This subroutine converts the given angle in radians to its equivalent in
1205             degrees, minutes and seconds, represented as a string. Degrees will be
1206             suppressed if zero, and minutes will be suppressed if both degrees and
1207             minutes are zero. If degrees are present the default delimiter is a
1208             degree sign. The delimiters for minutes and seconds are C<'> and C<">
1209             respectively, with the C<"> appearing before the decimal point.
1210              
1211             The optional C<$decimals> argument specifies the number of decimal
1212             places in the seconds value, and defaults to C<3>.
1213              
1214             The optional C<$degree_sign> argument specifies the degree sign. The
1215             default is a Unicode degree sign, C<"\N{DEGREE SIGN}">, a.k.a.
1216             C<"\N{U+00B0}">.
1217              
1218             =cut
1219              
1220             sub rad2dms {
1221 2     2 1 1024 my ( $rad, $dp, $ds ) = @_;
1222 2 100       10 defined $rad
1223             or return $rad;
1224 1 50       4 defined $dp
1225             or $dp = 3;
1226 1 50       3 defined $ds
1227             or $ds = "\N{U+00B0}";
1228 1         5 my $wid = $dp + 3;
1229              
1230 1 50       24 ( $rad, my $sgn ) = $rad < 0 ? ( -$rad, '-' ) : ( $rad, '' );
1231 1         5 my $sec = rad2deg( $rad );
1232 1         8 ( $sec, my $deg ) = modf( $sec );
1233 1         5 ( $sec, my $min ) = modf( $sec * 60 );
1234 1         3 $sec *= 60;
1235 1         2 my $rslt;
1236 1 50       5 if ( $deg ) {
    0          
1237 1         14 $rslt = sprintf qq<%d$ds%02d'%$wid.${dp}f>, $deg, $min, $sec;
1238             } elsif ( $min ) {
1239 0         0 $rslt = sprintf qq<%d'%$wid.${dp}f>, $min, $sec;
1240             } else {
1241 0         0 $rslt = sprintf qq<%.${dp}f>, $sec;
1242             }
1243 1 50       11 $rslt =~ s/ [.] /"./smx
1244             or $rslt .= q<">;
1245 1         7 return "$sgn$rslt";
1246             }
1247              
1248             =item $hr_min_sec = rad2hms( $radians, $decimals )
1249              
1250             This subroutine converts the given angle in radians to its equivalent in
1251             hours, minutes and seconds (presumably of right ascension), represented
1252             as a string. Hours will be suppressed if zero, and minutes will be
1253             suppressed if both hours and minutes are zero. The delimiters for hours,
1254             minutes, and seconds are C<'h'>, C<'m'>, and C<'s'> respectively, with
1255             the C<'s'> appearing before the decimal point.
1256              
1257             The optional C<$decimals> argument specifies the number of decimal
1258             places in the seconds value, and defaults to C<3>.
1259              
1260             =cut
1261              
1262             sub rad2hms {
1263 2     2 1 1666 my ( $rad, $dp ) = @_;
1264 2 100       10 defined $rad
1265             or return $rad;
1266 1 50       6 defined $dp
1267             or $dp = 3;
1268              
1269 1         3 my $wid = $dp + 3;
1270 1 50       5 ( $rad, my $sgn ) = $rad < 0 ? ( -$rad, '-' ) : ( $rad, '' );
1271 1         4 my $sec = $rad * 12 / PI;
1272 1         5 ( $sec, my $hr ) = modf( $sec );
1273 1         6 ( $sec, my $min ) = modf( $sec * 60 );
1274 1         3 $sec *= 60;
1275 1         3 my $rslt;
1276 1 50       3 if ( $hr ) {
    0          
1277 1         13 $rslt = sprintf "%dh%02dm%$wid.${dp}f", $hr, $min, $sec;
1278             } elsif ( $min ) {
1279 0         0 $rslt = sprintf "%dm%$wid.${dp}f", $min, $sec;
1280             } else {
1281 0         0 $rslt = sprintf "%.${dp}f", $sec;
1282             }
1283 1 50       8 $rslt =~ s/ [.] /s./smx
1284             or $rslt .= 's';
1285 1         5 return "$sgn$rslt";
1286             }
1287              
1288             =item $value = tan ($angle)
1289              
1290             This subroutine computes the tangent of the given angle in radians.
1291              
1292             =cut
1293              
1294 28721     28721 1 97904 sub tan {return sin ($_[0]) / cos ($_[0])}
1295              
1296             =item $value = theta0 ($time);
1297              
1298             This subroutine returns the Greenwich hour angle of the mean equinox at
1299             0 hours universal on the day whose time is given (i.e. the argument is
1300             a standard Perl time).
1301              
1302             =cut
1303              
1304             sub theta0 {
1305 1     1 1 935 my ($time) = @_;
1306 1         8 my @t = gmtime $time;
1307 1         3 $t[5] += 1900;
1308 1         7 return thetag( greg_time_gm( 0, 0, 0, @t[3 .. 5] ) );
1309             }
1310              
1311             =item $value = thetag ($time);
1312              
1313             This subroutine returns the Greenwich hour angle of the mean equinox at
1314             the given time.
1315              
1316             The algorithm comes from Jean Meeus' "Astronomical Algorithms", 2nd
1317             Edition, equation 12.4, page 88.
1318              
1319             =cut
1320              
1321             # Meeus, pg 88, equation 12.4, converted to radians and Perl dates.
1322              
1323             sub thetag {
1324 88246     88246 1 168993 my ($time) = @_;
1325 88246         193285 my $T = jcent2000 ($time);
1326 88246         192999 return mod2pi (4.89496121273579 + 6.30038809898496 *
1327             jday2000 ($time))
1328             + (6.77070812713916e-06 - 4.5087296615715e-10 * $T) * $T * $T;
1329             }
1330              
1331             # time_gm and time_local are actually created at the top of the module.
1332              
1333             =item $epoch = time_gm( $sec, $min, $hr, $day, $mon, $yr );
1334              
1335             This exportable subroutine is a wrapper for either
1336             C (if that module is installed), or
1337             C (if not.)
1338              
1339             This subroutine interprets years the same way C
1340             does.
1341              
1342             This wrapper is needed because the routines have subtly different
1343             signatures. L C interprets years
1344             strictly as Perl years. L C
1345             interprets years differently depending on the value of the year; greater
1346             than 999 as Gregorian years, but other years are Perl years, except for
1347             the years 0 to 99 inclusive, which are interpreted as within 50 years of
1348             the current year.
1349              
1350             This subroutine is discouraged in favor of C, which
1351             always interprets years as Gregorian years.
1352              
1353             =item $epoch = time_local( $sec, $min, $hr, $day, $mon, $yr );
1354              
1355             This exportable subroutine is a wrapper for either
1356             C (if that module is installed), or
1357             C (if not.)
1358              
1359             This subroutine interprets years the same way
1360             C does.
1361              
1362             This wrapper is needed for the same reason C is
1363             needed.
1364              
1365             This subroutine is discouraged in favor of C, which
1366             always interprets years as Gregorian years.
1367              
1368             =item $a = vector_cross_product( $b, $c );
1369              
1370             This subroutine computes and returns the vector cross product of $b and
1371             $c. Vectors are represented by array references. The cross product is
1372             only defined if both arrays have 3 elements.
1373              
1374             =cut
1375              
1376             sub vector_cross_product {
1377 3     3 1 1633 my ( $b, $c ) = @_;
1378 3 50 33     5 @{ $b } == 3 and @{ $c } == 3
  3         10  
  3         8  
1379             or confess 'Programming error - vector_cross_product arguments',
1380             ' must be references to arrays of length 3';
1381             return [
1382 3         13 $b->[1] * $c->[2] - $b->[2] * $c->[1],
1383             $b->[2] * $c->[0] - $b->[0] * $c->[2],
1384             $b->[0] * $c->[1] - $b->[1] * $c->[0],
1385             ];
1386             }
1387              
1388             =item $a = vector_dot_product( $b, $c );
1389              
1390             This subroutine computes and returns the vector dot product of $b and
1391             $c. Vectors are represented by array references. The dot product is only
1392             defined if both arrays have the same number of elements.
1393              
1394             =cut
1395              
1396             sub vector_dot_product {
1397 50698     50698 1 93257 my ( $b, $c ) = @_;
1398 50698 50       77596 @{ $b } == @{ $c }
  50698         82236  
  50698         115769  
1399             or confess 'Programming error - vector_dot_product arguments ',
1400             'must be references to arrays of the same length';
1401 50698         80758 my $prod = 0;
1402 50698         75335 my $size = @{ $b } - 1;
  50698         79658  
1403 50698         97612 foreach my $inx ( 0 .. $size ) {
1404 152094         284952 $prod += $b->[$inx] * $c->[$inx];
1405             }
1406 50698         180453 return $prod;
1407             }
1408              
1409             =item $m = vector_magnitude( $x );
1410              
1411             This subroutine computes and returns the magnitude of vector $x. The
1412             vector is represented by an array reference.
1413              
1414             =cut
1415              
1416             sub vector_magnitude {
1417 50185     50185 1 103937 my ( $x ) = @_;
1418 50185 50       136704 ARRAY_REF eq ref $x
1419             or confess 'Programming error - vector_magnitude argument ',
1420             'must be a reference to an array';
1421 50185         81862 my $mag = 0;
1422 50185         87074 my $size = @{ $x } - 1;
  50185         95878  
1423 50185         119901 foreach my $inx ( 0 .. $size ) {
1424 150552         315144 $mag += $x->[$inx] * $x->[$inx];
1425             }
1426 50185         130843 return sqrt $mag;
1427             }
1428              
1429             =item $u = vector_unitize( $x );
1430              
1431             This subroutine computes and returns a unit vector pointing in the same
1432             direction as $x. The vectors are represented by array references.
1433              
1434             =cut
1435              
1436             sub vector_unitize {
1437 2     2 1 1093 my ( $x ) = @_;
1438 2 50       6 ARRAY_REF eq ref $x
1439             or confess 'Programming error - vector_unitize argument ',
1440             'must be a reference to an array';
1441 2         5 my $mag = vector_magnitude( $x );
1442 2         4 return [ map { $_ / $mag } @{ $x } ];
  4         11  
  2         3  
1443             }
1444              
1445             # __classisa( 'Foo', 'Bar' );
1446             #
1447             # Returns true if Foo->isa( 'Bar' ) is true, and false otherwise.
1448             # In particular, returns false if the first argument is a
1449             # reference.
1450              
1451             sub __classisa {
1452 73374     73374   165256 my ( $invocant, $class ) = @_;
1453 73374 50       172288 ref $invocant and return;
1454 73374 50       149366 defined $invocant or return;
1455 73374         440878 return $invocant->isa( $class );
1456             }
1457              
1458             # __instance( $foo, 'Bar' );
1459             #
1460             # Returns true if $foo is an instance of 'Bar', and false
1461             # otherwise. In particular, returns false if $foo is not a
1462             # reference, or if it is not blessed.
1463              
1464             sub __instance {
1465 115     115   196 my ( $object, $class ) = @_;
1466 115 50       253 ref $object or return;
1467 115 50       222 blessed( $object ) or return;
1468 115         525 return $object->isa( $class );
1469             }
1470              
1471             # $epoch_time = __parse_time_iso_8601
1472             #
1473             # Parse ISO 8601 date/time. I do not intend to expose this, since
1474             # it will probably go away when the satpass script is dropped. It
1475             # would actually be in that script except for the fact that it can
1476             # be more easily tested here, and because of the possibility that
1477             # it will be used in App::Satpass2.
1478             {
1479              
1480             my %special_day_offset = (
1481             yesterday => -SECSPERDAY(),
1482             today => 0,
1483             tomorrow => SECSPERDAY(),
1484             );
1485              
1486             sub __parse_time_iso_8601 {
1487 51     51   194664 my ( $string ) = @_;
1488              
1489 51         96 my @zone;
1490 51 100       629 $string =~ s/ \s* (?: ( Z ) |
1491             ( [+-] ) ( [0-9]{2} ) :? ( [0-9]{2} )? ) \z //smx
1492             and @zone = ( $1, $2, $3, $4 );
1493 51         90 my @date;
1494              
1495             # ISO 8601 date
1496 51 50       475 if ( $string =~ m{ \A
    0          
1497             ( [0-9]{4} [^0-9]? | [0-9]{2} [^0-9] ) # year: $1
1498             (?: ( [0-9]{1,2} ) [^0-9]? # month: $2
1499             (?: ( [0-9]{1,2} ) (?: \s* | [^0-9]? ) # day: $3
1500             (?: ( [0-9]{1,2} ) [^0-9]? # hour: $4
1501             (?: ( [0-9]{1,2} ) [^0-9]? # minute: $5
1502             (?: ( [0-9]{1,2} ) [^0-9]? # second: $6
1503             ( [0-9]* ) # fract: $7
1504             )?
1505             )?
1506             )?
1507             )?
1508             )?
1509             \z
1510             }smx ) {
1511 51         308 @date = ( $1, $2, $3, $4, $5, $6, $7, undef );
1512              
1513             # special-case 'yesterday', 'today', and 'tomorrow'.
1514             } elsif ( $string =~ m< \A
1515             ( yesterday | today | tomorrow ) # day: $1
1516             (?: [^0-9]* ( [0-9]{1,2} ) [^0-9]? # hour: $2
1517             (?: ( [0-9]{1,2} ) [^0-9]? # minute: $3
1518             (?: ( [0-9]{1,2} ) [^0-9]? # second: $4
1519             ( [0-9]* ) # fract: $5
1520             )?
1521             )?
1522             )?
1523             \z >smx ) {
1524 0 0       0 my @today = @zone ? gmtime : localtime;
1525             @date = ( $today[5] + 1900, $today[4] + 1, $today[3], $2, $3,
1526 0         0 $4, $5, $special_day_offset{$1} );
1527              
1528             } else {
1529              
1530 0         0 return;
1531              
1532             }
1533              
1534 51   50     215 my $offset = pop @date || 0;
1535 51 100 100     168 if ( @zone && !$zone[0] ) {
1536 5         15 my ( undef, $sign, $hr, $min ) = @zone;
1537 5   100     56 $offset -= $sign . ( ( $hr * 60 + ( $min || 0 ) ) * 60 )
1538             }
1539              
1540 51         109 foreach ( @date ) {
1541 357 100       778 defined $_ and s/ [^0-9]+ //smxg;
1542             }
1543              
1544 51         123 $date[0] = __tle_year_to_Gregorian_year( $date[0] );
1545              
1546 51 100       140 defined $date[1] and --$date[1];
1547 51 100       105 defined $date[2] or $date[2] = 1;
1548 51         84 my $frc = pop @date;
1549              
1550 51         84 foreach ( @date ) {
1551 306 100       542 defined $_ or $_ = 0;
1552             }
1553              
1554 51         73 my $time;
1555 51 100       82 if ( @zone ) {
1556 28         76 $time = greg_time_gm( reverse @date );
1557             } else {
1558 23         67 $time = greg_time_local( reverse @date );
1559             }
1560              
1561 51 50 66     2215 if ( defined $frc && $frc ne '') {
1562 0         0 my $denom = 1 . ( 0 x length $frc );
1563 0         0 $time += $frc / $denom;
1564             }
1565              
1566 51         176 return $time + $offset;
1567             }
1568              
1569             }
1570              
1571             sub _pre_strftime {
1572 14     14   607 my ( $format, $epoch, $places ) = @_;
1573 14         37 my $seconds = POSIX::floor( $epoch );
1574 14         22 my $frac = $epoch - $seconds;
1575 14         146 $format =~ s( ( %+ ) ( [.] [0-9]* )? ( [DFrRSTV] ) )
1576 18         48 ( _pre_strftime_mung_fmt( $1, $2, $3, $frac, $places ) )smxge;
1577 14         79 return ( $format, $seconds );
1578             };
1579              
1580             {
1581             # The test of __format_epoch_time_usec() (which uses format '%F %T')
1582             # failed under Windows, at least undef Strawberry, returning the
1583             # empty string. Expanding %F fixed this, so I decided to expand all
1584             # the 'equivalent to' format strings I could find.
1585              
1586             my %mung = (
1587             'D' => 'm/%%d/%%y',
1588             'F' => 'Y-%%m-%%d',
1589             'r' => 'I:%%M:%%S%s %%p',
1590             'R' => 'H:%%M',
1591             'S' => 'S%s',
1592             'T' => 'H:%%M:%%S%s',
1593             'V' => 'e-%%b-%%Y',
1594             );
1595              
1596             sub _pre_strftime_mung_fmt {
1597 18     18   67 my ( $percent, $places, $fmt, $frac, $dflt_places ) = @_;
1598             length( $percent ) % 2
1599 18 50 33     86 and $mung{$fmt}
1600             or return "$percent$places$fmt";
1601 18         21 my $f = '';
1602 18 100       33 defined $places
1603             or $places = $dflt_places;
1604 18 100       27 if ( defined $places ) {
1605 5 100       14 index( $places, '.' ) == 0
1606             or substr $places, 0, 0, '.';
1607 5 50       8 $places eq '.'
1608             and $places = '';
1609 5         25 $f = sprintf "%${places}f", $frac;
1610 5         15 $f =~ s/ \A 0+ //smx;
1611             }
1612 18         37 return $percent . __sprintf( $mung{$fmt}, $f );
1613             }
1614             }
1615              
1616             sub __sprintf {
1617 54     54   148 my ( $tplt, @args ) = @_;
1618 54 100       153 defined $tplt
1619             or return undef; ## no critic (ProhibitExplicitReturnUndef)
1620 18     18   170 no if $] gt '5.021002', qw{ warnings redundant };
  18         35  
  18         8292  
1621 49         418 return sprintf $tplt, @args;
1622             }
1623              
1624             {
1625             my %deprecate = (
1626             epoch2datetime => {
1627             level => 0,
1628             method => 'core subroutine gmtime',
1629             },
1630             date2epoch => {
1631             level => 0,
1632             method => 'subroutine Time::Local::timegm_posix',
1633             },
1634             );
1635              
1636             sub __subroutine_deprecation {
1637 28     28   370 ( my $sub = ( caller 1 )[3] ) =~ s/ .* :: //smx;
1638 28 50       149 my $info = $deprecate{$sub} or return;
1639             $info->{level}
1640 28 50       101 or return;
1641 0         0 my $msg = "Subroutine $sub() deprecated in favor of @{[
1642 0   0     0 $info->{method} || $sub ]}()";
1643 0 0       0 $info->{level} >= 3
1644             and croak $msg;
1645 0         0 carp $msg;
1646             $info->{level} == 1
1647 0 0       0 and $info->{level} = 0;
1648 0         0 return;
1649             }
1650             }
1651              
1652             =item $year = __tle_year_to_Gregorian_year( $year )
1653              
1654             The TLE data contain the year in two-digit form. NORAD decided to deal
1655             with Y2K by decreeing that year numbers lower than 57 (the launch of
1656             Sputnik 1) are converted to Gregorian by adding 2000. Years numbers of
1657             57 or greater are still converted to Gregorian by adding 1900. This
1658             subroutine encapsulates this logic. Years of 100 or greater are returned
1659             unmodified.
1660              
1661             This subroutine is B to this package, and can be changed or
1662             revoked without notice.
1663              
1664             =cut
1665              
1666             sub __tle_year_to_Gregorian_year {
1667 97     97   195 my ( $year ) = @_;
1668 97 100       451 return $year < 57 ? $year + 2000 :
    100          
1669             $year < 100 ? $year + 1900 : $year;
1670             }
1671              
1672             1;
1673              
1674             __END__