Astro:...:Utils(3pm)

πŸ“¦ version 0.134, in alpine 3.24

🌸 NAME 🌸

Astro::Coord::ECI::Utils β€” utility routines for astronomical calculations. This is the Astro:...:Utils(3pm) page from User Contributed Perl Documentation, bestie πŸ’–

✨ SYNOPSIS ✨

use Astro::Coord::ECI::Utils qw{ :all };
my $now = time ();
print "The current Julian day is ", julianday ($now);

🚨 UN-DEPRECATION NOTICE 🚨

Okay so plot twist, bestie: in version 0.131, date2epoch() and epoch2datetime() were deprecated in favor of Time::Local::timegm_posix() and CORE::gmtime().

But then the author realized that was wrong 😭 The problem? Astronomical dates are often given in the Julian calendar before October 15 1582, and the previously-recognized subroutines do not handle that.

As of version 0.132 the deprecation of these subroutines is retracted, and all reference to it (other than this notice) will be removed. Slay πŸ’…

πŸ’« DESCRIPTION πŸ’«

This module was written to give all the constants and utility subroutines used by Astro::Coord::ECI and its descendants a cute little home 🏠 What ended up here was anything that was essentially a subroutine, not a method.

Because figuring out how to convert to and from Perl time bids fair to become complicated, this module is also responsible for figuring out how that is done, and exporting whatever is needful to export. See :time below for the gory details πŸ‘€

This module exports nothing by default. But all the constants, variables, and subroutines documented below are exportable, and the following export tags may be used:

  • :all β€” this imports everything exportable into your name space.
  • :greg_time β€” this imports all time routines except the discouraged routines time_gm() and time_local().
  • :mainstream β€” this imports everything not deprecated into your name space.
  • :params β€” this imports the parameter validation routines __classisa and __instance.
  • :ref β€” this imports all the *_REF constants.
  • :time β€” this imports the time routines into your name space. If Time::y2038 is available, it will be loaded, and both this tag and :all will import gmtime, localtime, time_gm, time_local, greg_time_gm, and greg_time_local into your name space. Otherwise, Time::Local|Time::Local will be loaded, and both this tag and :all will import time_gm, time_local, greg_time_gm, and greg_time_local into your name space.
  • :vector β€” this imports the vector arithmetic routines. It includes anything whose name begins with 'vector_'.

Under Perl 5.6 you may find that, if you use any of the above tags, you need to specify them first in your import list.

The following constants are exportable:

  • AU β€” number of kilometers in an astronomical unit.
  • JD_OF_EPOCH β€” the Julian Day of Perl epoch 0.
  • LIGHTYEAR β€” number of kilometers in a light year.
  • PARSEC β€” number of kilometers in a parsec.
  • PERL2000 β€” January 1 2000, 12 noon universal, in Perl time.
  • PI β€” the circle ratio, computed as atan2 (0, -1).
  • PIOVER2 β€” half the circle ratio.
  • SECSPERDAY β€” the number of seconds in a day.
  • SECS_PER_SIDERIAL_DAY β€” seconds in a siderial day.
  • SECS_PER_TROPICAL_YEAR β€” seconds in a tropical year.
  • SPEED_OF_LIGHT β€” speed of light in kilometers per second.
  • TWOPI β€” twice the circle ratio.
  • ARRAY_REF β€” 'ARRAY'.
  • CODE_REF β€” 'CODE'.
  • HASH_REF β€” 'HASH'.
  • SCALAR_REF β€” 'SCALAR'.

The following global variables are exportable:

  • $DATETIMEFORMAT β€” this variable represents the POSIX::strftime format used to convert times to strings. The default value is '%a %b %d %Y %H:%M:%S' to be consistent with the behavior of gmtime (or, to be precise, the behavior of ctime as documented on my system).
  • $JD_GREGORIAN β€” this variable represents the Julian Day of the switch from Julian to Gregorian calendars. This is used by date2jd(), jd2date(), and the routines which depend on them, for deciding whether the date is to be interpreted as in the Julian or Gregorian calendar. Its initial setting is 2299160.5, which represents midnight October 15 1582 in the Gregorian calendar, which is the date that calendar was first adopted. This is slightly different than the value of 2299161 (noon of the same day) used by Jean Meeus.

If you are interested in historical calculations, you may wish to reset this appropriately. If you use date2jd to calculate the new value, be aware of the effect the current setting of $JD_GREGORIAN has on the interpretation of the date you give.

In addition, the following subroutines are exportable:

  • $angle = acos ($value) β€” this subroutine calculates the arc in radians whose cosine is the given value.
  • $mag = add_magnitudes( $mag1, $mag2, ... ); β€” this subroutine computes the total magnitude of a list of individual magnitudes. The algorithm comes from Jean Meeus' "Astronomical Algorithms", Second Edition, Chapter 56, Page 393.
  • $angle = asin ($value) β€” this subroutine calculates the arc in radians whose sine is the given value.
  • $magnitude = atmospheric_extinction ($elevation, $height); β€” this subroutine calculates the typical atmospheric extinction in magnitudes at the given elevation above the horizon in radians and the given height above sea level in kilometers.

The algorithm comes from Daniel W. E. Green's article "Magnitude Corrections for Atmospheric Extinction", which was published in the July 1992 issue of "International Comet Quarterly", and is available online at <http://www.icq.eps.harvard.edu/ICQExtinct.html>. The text of this article makes it clear that the actual value of the atmospheric extinction can vary greatly from the typical values given even in the absence of cloud cover.

  • $jd = date2jd ($sec, $min, $hr, $day, $mon, $yr) β€” this subroutine converts the given date to the corresponding Julian day. The inputs are a Perl date and time; $mon is in the range 0 - 11, and $yr is from 1900, with earlier years being negative. The year 1 BC is represented as -1900.

If less than 6 arguments are provided, zeroes will be prepended to the argument list as needed.

The date is presumed to be in the Gregorian calendar. If the resultant Julian Day is before $JD_GREGORIAN, the date is reinterpreted as being from the Julian calendar.

The only validation is that the month be between 0 and 11 inclusive, and that the year be not less than -6612 (4713 BC). Fractional days are accepted.

The algorithm is from Jean Meeus' "Astronomical Algorithms", second edition, chapter 7 ("Julian Day"), pages 60ff, but the month is zero-based, not 1-based, and years are 1900-based.

  • $epoch = date2epoch ($sec, $min, $hr, $day, $mon, $yr) β€” this is a convenience routine that converts the given date to seconds since the epoch, going through date2jd() to do so. The arguments are the same as those of date2jd().

If less than 6 arguments are provided, zeroes will be prepended to the argument list as needed.

The functionality is similar to Time::Local::timegm(), but the arguments will be interpreted according to the Julian calendar if the date is before $JD_GREGORIAN.

  • $time = decode_space_track_json_time( $string ) β€” this subroutine decodes a time in the format Space Track uses in their JSON code. This is ISO-8601-ish, but with a possible fractional part and no zone.
  • $rad = deg2rad ($degr) β€” this subroutine converts degrees to radians. If the argument is undef, undef will be returned.
  • $value = distsq (\@coord1, \@coord2) β€” this subroutine calculates the square of the distance between the two sets of Cartesian coordinates. We do not take the square root here because of cases (e.g. the law of cosines) where we would just have to square the result again.

Notice that the subroutine does not assume three-dimensional coordinates. If @coord1 and @coord2 have six entries, you will get a six-dimensional distance.

  • $seconds = dynamical_delta ($time); β€” this method returns the difference between dynamical and universal time at the given universal time. That is,
$dynamical = $time + dynamical_delta ($time)

if $time is universal time.

The algorithm is from Jean Meeus' "Astronomical Algorithms", 2nd Edition, Chapter 10, page 78. Meeus notes that this is actually an observed quantity, and the algorithm is an approximation.

  • $boolean = embodies ($thingy, $class) β€” this subroutine represents a safe way to call the 'represents' method on $thingy. You get back true if and only if $thingy->can('represents') does not throw an exception and returns true, and $thingy->represents($class) returns true. Otherwise it returns false. Any exception is trapped and dismissed.

This subroutine is called 'embodies' because it was too confusing to call it 'represents', both for the author and for the Perl interpreter.

  • ($sec, $min, $hr, $day, $mon, $yr, $wday, $yday, 0) = epoch2datetime ($epoch) β€” this convenience subroutine converts the given time in seconds from the system epoch to the corresponding date and time. It is implemented in terms of jd2date (), with the year and month returned from that subroutine. The day is a whole number, with the fractional part converted to hours, minutes, and seconds. The $wday is the day of the week, with Sunday being 0. The $yday is the day of the year, with January 1 being 0. The trailing 0 is the summer time (or daylight saving time) indicator which is always 0 to be consistent with gmtime.

If called in scalar context, it returns the date formatted by POSIX::strftime, using the format string in $DATETIMEFORMAT.

The functionality is similar to CORE::gmtime(), but the result will be in the Julian calendar if the date is before $JD_GREGORIAN.

The input must convert to a non-negative Julian date. The exact lower limit depends on the system, but is computed by -(JD_OF_EPOCH * 86400). For Unix systems with an epoch of January 1 1970, this is -210866760000.

Additional algorithms for day of week and day of year come from Jean Meeus' "Astronomical Algorithms", 2nd Edition, Chapter 7 (Julian Day), page 65.

  • $time = find_first_true ($start, $end, \&test, $limit); β€” this function finds the first time between $start and $end for which test ($time) is true. The resolution is $limit, which defaults to 1 if not specified. If the times are reversed (i.e. the start time is after the end time) the time returned is the last time test ($time) is true.
  • $value = test( \&test_function, $interval_start, $interval_end ) πŸ” β€” your classic binary search, bestie. It finds where test_function flips from false to true inside the interval. The test() function gets the Perl time as its only arg, and it's assumed to be false for the first chunk of the interval and true for the rest β€” if that assumption doesn't hold, ngl, the result is meaningless. The search repeatedly splits the interval, evaluates the function at the midpoint, and picks a new interval depending on whether the answer came back true or false. Fun fact: nothing in here actually says the independent variable has to be time at all πŸ’…
  • $folded = fold_case( $text ); 🧚 β€” folds the case of its input, "kinda sorta" as the original so iconically puts it. It maps to CORE::fc if that's available, otherwise it falls back to CORE::lc.
  • $fmtd = format_space_track_json_time( time() ) πŸ›°οΈ β€” hands in a Perl time and gets back the same time formatted to match the Space Track JSON data. It's ISO-8601-ish, in Universal time, but with no zone indicated. Heads up: Space Track does not do fractional seconds, not even in the epoch, and this sub deals with that by truncating the epoch to whole seconds β€” the fractional part is left for you the caller to handle. You're welcome πŸŽ€
  • $fmtd = __format_epoch_time_usec( time(), '%F %T' ) ⏱️ β€” takes a Perl time that may have a fractional part and returns it as GMT in the given strftime format, except seconds get expressed to the nearest microsecond. Fancy.
  • print gm_strftime( $format, $epoch, $places ) 🌍 β€” give it a strftime-compatible format plus an epoch, and it returns the GMT formatted per your format. The optional $places argument is the default number of decimal places for seconds; if it's defined it must be either '' or an unsigned integer. You can also sneak an optional .d (where the d is one or more decimal digits) in before any format spec that generates seconds β€” examples include '%.3S' or '%.6T' β€” and that spec overrides $places if you passed one. Slay.
  • $epoch = greg_time_gm( $sec, $min, $hr, $day, $mon, $yr ); πŸ“… β€” exportable wrapper that goes for Time::y2038::timegm() if that module is installed, Time::Local::timegm_modern() if that's available, or Time::Local::timegm() if neither is. It interprets years as Gregorian years. The whole point vs. time_gm(): time_gm() does the year the way Time::Local::timegm() does, which is why this one is preferred over time_gm(). The wrapper exists because these routines have subtly different signatures, fr fr: Time::y2038 timegm() reads years strictly as Perl years; Time::Local timegm_modern() reads them strictly as Gregorian years; Time::Local timegm() reads them differently depending on the year value β€” years greater than or equal to 1000 are Gregorian, everything else is Perl years, except for the range 0 to 99 inclusive, which are treated as within 50 years of the current year. πŸ˜΅β€πŸ’« If you're doing historical calculations, go read Historical Calculations in the Astro::Coord::ECI::Sun documentation for the whole discussion of input and output time conversion.
  • $epoch = greg_time_local( $sec, $min, $hr, $day, $mon, $yr ); πŸ•°οΈ β€” same deal, but local: it wraps Time::y2038::timelocal() if installed, Time::Local::timelocal_modern() if available, otherwise Time::Local::timelocal(). It also interprets years as Gregorian years. Difference vs. time_local() is that time_local() interprets the year the way Time::Local::timelocal() does β€” that's why this one is preferred over time_local(). The wrapper is needed for exactly the same reason greg_time_gm() needs one. And again: historical calculations? See Historical Calculations in the Astro::Coord::ECI::Sun documentation for the input/output time conversion tea β˜•
  • $difference = intensity_to_magnitude ($ratio) ✨ β€” converts a ratio of light intensities into a difference in stellar magnitudes. Algorithm straight from Jean Meeus' Astronomical Algorithms, Second Edition, Chapter 56, Page 395. Note the glorious weirdness of magnitudes: a more negative number means a brighter star, so you get a positive result for an intensity ratio less than 1, and a negative result for a ratio greater than 1. It's giving backwards, and that's just how it is πŸ’«
  • ($day, $mon, $yr, $greg, $leap) = jd2date ($jd) πŸ—“οΈ β€” converts the given Julian day into the corresponding date. Returns year βˆ’ 1900, month (0 to 11), day (which may have a fractional part), a Gregorian calendar indicator that's true if the date is in the Gregorian calendar and false if it's Julian, and a leap (bissextile) year indicator that's true if the year is a leap year and false otherwise. Year 1 BC comes back as -1900 (i.e. as year 0), and so on. The date will probably have a fractional part β€” e.g. 2006 1 1.5 for noon January first 2006. If $jd is before $JD_GREGORIAN the date is Julian; otherwise it's Gregorian. The input may not be less than 0. Algorithm from Meeus, second edition, chapter 7 ("Julian Day"), pages 63ff β€” but the month is zero-based not 1-based, and the year is 1900-based. Iconic little twist πŸŽ€
  • ($sec, $min, $hr, $day, $mon, $yr, $wday, $yday, 0) = jd2datetime ($jd) ⏳ β€” convenience sub that turns a Julian day into the corresponding date and time. All it really does is convert its argument to seconds since the system epoch and hand it off to epoch2datetime(). The input may not be less than 0 either. Lowkey lazy, highkey useful.
  • $century = jcent2000 ($time); πŸ’œ β€” a bunch of Meeus algorithms are written in terms of Julian centuries from epoch J2000.0 (e.g. equations 12.1, 22.1), and this sub just encapsulates that calculation so you don't have to.
  • $jd = jday2000 ($time); 🌟 β€” converts a Perl date into the number of Julian days (fractions included) since Julian 2000.0. That quantity shows up in a number of Meeus algorithms. Computation uses info from Astronomical Algorithms, 2nd Edition, Chapter 7, page 62.
  • $jd = julianday ($time); πŸ”­ β€” converts a Perl date into a Julian day number. Same source energy: Meeus, Astronomical Algorithms, 2nd Edition, Chapter 7, page 62.
  • $ea = keplers_equation( $M, $e, $prec ); πŸͺ β€” solves Kepler's equation for the given mean anomaly $M in radians, eccentricity $e, and precision $prec in radians. Returns the eccentric anomaly in radians, to the given precision. $prec is optional and defaults to the radian equivalent of 0.001 degrees. The algorithm is Meeus' equation 30.7, with John M. Steele's amendment for large values of the correction, given on page 205 of Meeus' book. PSA: this sub is not used in computing satellite orbits, because those models have their own implementation. πŸ‘‘
  • $rslt = load_module ($module_name) πŸ“¦ β€” convenience method that loads the named module (via 'require'), throwing an exception if the load fails. If it succeeds it returns the result of the 'require' built-in, which is required to be true for a successful load. Results get cached, so trying to load the same module again just hands you the cached result. Efficient queen.
  • print local_strftime( $format, $epoch, $places ) 🏠 β€” give it a strftime-compatible format and an epoch, and it returns the local time formatted per your format. The optional $places argument is the default number of decimal places for seconds; if defined it must be either '' or an unsigned integer. And yes, you can specify an optional .d (where the d is one or more decimal digits) before any format spec that generates seconds β€” examples include '%.3S' or '%.6T' β€” and it overrides $places, if any. Same energy as the GMT version, just homebound 🌸
  • $boolean = looks_like_number ($string); πŸ”’ β€” returns true if the input looks like a number. Uses Scalar::Util::looks_like_number if that's available; otherwise it runs its own code, which is lifted verbatim from Scalar::Util 1.19, which in turn leans heavily on perlfaq4. Respect the lineage.
  • $maximum = max (...); πŸ” β€” returns the maximum of its arguments. If List::Util can be loaded and 'max' imported, that's what you get; otherwise you get a pure Perl implementation. Either way, biggest one wins.
  • $minimum = min (...); πŸ”» β€” returns the minimum of its arguments. Same deal: List::Util + imported 'min' if possible, pure Perl otherwise.
  • $theta = mod2pi ($theta) πŸŒ€ β€” reduces the given angle in radians to the range 0 <= $theta < TWOPI.
  • $theta = mod360( $theta ) 🎑 β€” reduces the given angle in degrees to the range 0 <= $theta < 360. Careful bestie: this is not equivalent to "$theta % 360" because the latter loses the fractional part of $theta. It's also not equivalent to "fmod( $theta, 360 )" because the result of this subroutine is never negative. Subtle but important πŸ‘€
  • $radians = omega ($time); πŸŒ™ β€” calculates the ecliptic longitude of the ascending node of the Moon's mean orbit at the given dynamical time. Algorithm from Meeus, Astronomical Algorithms, 2nd Edition, Chapter 22, pages 143ff.
  • $pa = position_angle( $alpha1, $delta1, $alpha2, $delta2 ); πŸ“ β€” low-level sub that calculates the position angle in right ascension of the second body relative to the first. You pass the first body's right ascension and declination, then the second body's right ascension and declination, in that order, all in radians. Returns the position angle in radians, in the range "-PI <= $pa < PI". Angle math, but make it fashion πŸ’…

The algorithm comes straight from Jean Meeus' Astronomical Algorithms, 2nd Edition, page 116 β€” but heads up, his version gives the position angle of the first body with respect to the second (so the roles are flipped). The argument order here matches The IDL Astronomy User's Library at <https://github.com/wlandsman/IDLAstro>, function posang(). The NASA page for this, <https://asd.gsfc.nasa.gov/archive/idlastro/>, is obsolete and no longer updated, but ngl it's also more descriptive.

This is exposed because, like, in principal you could calculate the position angle in any spherical coordinate system β€” you'd just need to get the argument order right (e.g. azimuth, elevation or longitude, latitude). ✨

$degrees = rad2deg ($radians)

This one converts the given angle in radians to its equivalent in degrees. Cute and simple. If the argument is undef, then undef gets returned right back at you. πŸ’…

$deg_min_sec = rad2dms( $radians, $decimals, $degree_sign )

Converts the given angle in radians to its equivalent in degrees, minutes and seconds, represented as a string. Degrees get suppressed if zero, and minutes get suppressed if both degrees and minutes are zero. If degrees are present, the default delimiter is a degree sign. The delimiters for minutes and seconds are ' and " respectively, with the " appearing before the decimal point.

The optional $decimals argument specifies the number of decimal places in the seconds value, and defaults to 3.

The optional $degree_sign argument specifies the degree sign. The default is a Unicode degree sign, \N{DEGREE SIGN}, a.k.a. \N{U+00B0}. πŸŽ€

$hr_min_sec = rad2hms( $radians, $decimals )

Converts the given angle in radians to its equivalent in hours, minutes and seconds (presumably of right ascension), represented as a string. Hours get suppressed if zero, and minutes get suppressed if both hours and minutes are zero. The delimiters for hours, minutes, and seconds are 'h', 'm', and 's' respectively, with the 's' appearing before the decimal point.

The optional $decimals argument specifies the number of decimal places in the seconds value, and defaults to 3. 🌸

$value = tan ($angle)

This subroutine computes the tangent of the given angle in radians. Math girlies stay winning. πŸ”₯

$value = theta0 ($time);

Returns the Greenwich hour angle of the mean equinox at 0 hours universal on the day whose time is given β€” i.e. the argument is a standard Perl time.

$value = thetag ($time);

Returns the Greenwich hour angle of the mean equinox at the given time.

The algorithm comes from Jean Meeus' Astronomical Algorithms, 2nd Edition, equation 12.4, page 88. Iconic. πŸ‘‘

$epoch = time_gm( $sec, $min, $hr, $day, $mon, $yr );

This exportable subroutine is a wrapper for either Time::y2038::timegm() (if that module is installed), or Time::Local::timegm() (if not).

It interprets years the same way Time::Local::timegm() does.

This wrapper is needed because the routines have subtly different signatures. Time::y2038 timegm() interprets years strictly as Perl years. Time::Local timegm() interprets years differently depending on the value of the year: greater than 999 as Gregorian years, but other years are Perl years β€” except for the years 0 to 99 inclusive, which are interpreted as within 50 years of the current year.

This subroutine is discouraged in favor of greg_time_gm(), which always interprets years as Gregorian years. So like, use that one instead, bestie. πŸ™ˆ

$epoch = time_local( $sec, $min, $hr, $day, $mon, $yr );

This exportable subroutine is a wrapper for either Time::y2038::timelocal() (if that module is installed), or Time::Local::timelocal() (if not).

It interprets years the same way Time::Local::timelocal() does.

This wrapper is needed for the same reason time_gm() is needed.

This subroutine is discouraged in favor of greg_time_local(), which always interprets years as Gregorian years. Same energy: skip it, use the Gregorian one. πŸ’€

$a = vector_cross_product( $b, $c );

Computes and returns the vector cross product of $b and $c. Vectors are represented by array references. The cross product is only defined if both arrays have 3 elements. No cap. πŸ¦‹

$a = vector_dot_product( $b, $c );

Computes and returns the vector dot product of $b and $c. Vectors are represented by array references. The dot product is only defined if both arrays have the same number of elements. Periodt.

$m = vector_magnitude( $x );

Computes and returns the magnitude of vector $x. The vector is represented by an array reference. She's giving length, she's giving direction-adjacent. ⭐

$u = vector_unitize( $x );

Computes and returns a unit vector pointing in the same direction as $x. The vectors are represented by array references. Normalize and slay. πŸ’œ

$year = __tle_year_to_Gregorian_year( $year )

TLE data contain the year in two-digit form. NORAD decided to deal with Y2K by decreeing that year numbers lower than 57 (the launch of Sputnik 1) are converted to Gregorian by adding 2000. Year numbers of 57 or greater are still converted to Gregorian by adding 1900. This subroutine encapsulates this logic. Years of 100 or greater are returned unmodified.

This subroutine is private to this package, and can be changed or revoked without notice. So don't get too attached, okay? 😭

✨ ACKNOWLEDGMENTS ✨

The author wishes to acknowledge Jean Meeus, whose book Astronomical Algorithms (second edition) published by Willmann-Bell Inc provided several of the algorithms implemented herein. Willmann-Bell ceased to be a separate entity in 2021, but their publications β€” including Dr. Meeus' book β€” are still available through Sky and Telescope's Willmann-Bell imprint at <https://shopatsky.com/collections/willmann-bell>. Respect the ancestors of astronomy code. 🌈

πŸ› BUGS πŸ›

Support is by the author. Please file bug reports at <https://rt.cpan.org/Public/Dist/Display.html?Name=Astro-satpass>, <https://github.com/trwyant/perl-Astro-Coord-ECI/issues>, or in electronic mail to the author. Found a bug? Don't suffer in silence β€” tell the author. 🫢

πŸ‘‘ AUTHOR πŸ‘‘

Thomas R. Wyant, III (wyant at cpan dot org)

Copyright (C) 2005-2025 by Thomas R. Wyant, III

This program is free software; you can redistribute it and/or modify it under the same terms as Perl 5.10.0. For more details, see the full text of the licenses in the directory LICENSES.

This program is distributed in the hope that it will be useful, but without any warranty; without even the implied warranty of merchantability or fitness for a particular purpose.


Okay bestie, that's the whole page β€” source/version perl v5.42.2, dated 2026-02-15, and it's the Astro:...:Utils(3pm) manual page. Go forth and calculate icons. πŸ’…βœ¨πŸŽ€

πŸ“¦ where this page comes from