astronoby 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/CHANGELOG.md +235 -0
  4. data/README.md +19 -1
  5. data/UPGRADING.md +270 -0
  6. data/docs/README.md +105 -16
  7. data/docs/altitude_crossings.md +92 -0
  8. data/docs/angles.md +1 -0
  9. data/docs/configuration.md +28 -17
  10. data/docs/coordinates.md +72 -12
  11. data/docs/deep_sky_bodies.md +46 -1
  12. data/docs/ephem.md +5 -2
  13. data/docs/equinoxes_solstices_times.md +4 -3
  14. data/docs/glossary.md +99 -1
  15. data/docs/iers.md +40 -0
  16. data/docs/instant.md +33 -21
  17. data/docs/lunar_eclipses.md +208 -0
  18. data/docs/lunar_observation.md +87 -0
  19. data/docs/moon_phases.md +4 -1
  20. data/docs/observer.md +20 -6
  21. data/docs/planetary_phenomena.md +78 -0
  22. data/docs/reference_frames.md +192 -34
  23. data/docs/rise_transit_set_times.md +6 -4
  24. data/docs/solar_system_bodies.md +26 -4
  25. data/docs/twilight_times.md +43 -24
  26. data/lib/astronoby/angle.rb +63 -2
  27. data/lib/astronoby/angles/dms.rb +18 -1
  28. data/lib/astronoby/angles/hms.rb +14 -1
  29. data/lib/astronoby/angular_velocity.rb +21 -0
  30. data/lib/astronoby/bodies/deep_sky_object.rb +6 -1
  31. data/lib/astronoby/bodies/deep_sky_object_position.rb +35 -38
  32. data/lib/astronoby/bodies/earth.rb +7 -44
  33. data/lib/astronoby/bodies/jupiter.rb +10 -0
  34. data/lib/astronoby/bodies/mars.rb +10 -0
  35. data/lib/astronoby/bodies/mercury.rb +10 -0
  36. data/lib/astronoby/bodies/moon.rb +158 -32
  37. data/lib/astronoby/bodies/neptune.rb +10 -0
  38. data/lib/astronoby/bodies/saturn.rb +10 -0
  39. data/lib/astronoby/bodies/solar_system_body.rb +240 -61
  40. data/lib/astronoby/bodies/sun.rb +79 -4
  41. data/lib/astronoby/bodies/uranus.rb +10 -0
  42. data/lib/astronoby/bodies/venus.rb +10 -0
  43. data/lib/astronoby/body.rb +6 -0
  44. data/lib/astronoby/cache.rb +13 -0
  45. data/lib/astronoby/center.rb +84 -0
  46. data/lib/astronoby/constants.rb +1 -2
  47. data/lib/astronoby/constellation.rb +9 -1
  48. data/lib/astronoby/constellations/data.rb +1 -1
  49. data/lib/astronoby/coordinates/ecliptic.rb +10 -1
  50. data/lib/astronoby/coordinates/equatorial.rb +79 -13
  51. data/lib/astronoby/coordinates/geodetic.rb +102 -0
  52. data/lib/astronoby/coordinates/horizontal.rb +13 -1
  53. data/lib/astronoby/delta_t.rb +44 -0
  54. data/lib/astronoby/distance.rb +35 -0
  55. data/lib/astronoby/duration.rb +116 -0
  56. data/lib/astronoby/earth_rotation.rb +70 -0
  57. data/lib/astronoby/equinox_solstice.rb +31 -8
  58. data/lib/astronoby/errors.rb +11 -0
  59. data/lib/astronoby/events/altitude_crossing.rb +29 -0
  60. data/lib/astronoby/events/altitude_crossing_calculator.rb +511 -0
  61. data/lib/astronoby/events/altitude_extreme.rb +31 -0
  62. data/lib/astronoby/events/conjunction.rb +51 -0
  63. data/lib/astronoby/events/conjunction_opposition_calculator.rb +84 -0
  64. data/lib/astronoby/events/extremum_calculator.rb +23 -176
  65. data/lib/astronoby/events/greatest_elongation.rb +58 -0
  66. data/lib/astronoby/events/greatest_elongation_calculator.rb +56 -0
  67. data/lib/astronoby/events/lunar_eclipse.rb +111 -0
  68. data/lib/astronoby/events/lunar_eclipse_calculator.rb +397 -0
  69. data/lib/astronoby/events/lunar_eclipse_geometry.rb +115 -0
  70. data/lib/astronoby/events/lunar_eclipse_phase.rb +45 -0
  71. data/lib/astronoby/events/lunar_eclipse_visibility.rb +296 -0
  72. data/lib/astronoby/events/opposition.rb +19 -0
  73. data/lib/astronoby/events/rise_transit_set_calculator.rb +9 -13
  74. data/lib/astronoby/events/rise_transit_set_event.rb +12 -1
  75. data/lib/astronoby/events/rise_transit_set_events.rb +12 -1
  76. data/lib/astronoby/events/twilight_calculator.rb +37 -147
  77. data/lib/astronoby/events/twilight_event.rb +24 -6
  78. data/lib/astronoby/events/twilight_events.rb +26 -6
  79. data/lib/astronoby/extremum_finder.rb +148 -0
  80. data/lib/astronoby/horizon.rb +40 -0
  81. data/lib/astronoby/horizontal_transformation.rb +112 -0
  82. data/lib/astronoby/instant.rb +56 -16
  83. data/lib/astronoby/libration.rb +25 -0
  84. data/lib/astronoby/mean_obliquity.rb +8 -0
  85. data/lib/astronoby/moon_orientation_ephemeris.rb +69 -0
  86. data/lib/astronoby/moon_physical_ephemeris.rb +263 -0
  87. data/lib/astronoby/nutation.rb +10 -20
  88. data/lib/astronoby/observer.rb +67 -49
  89. data/lib/astronoby/orientation.rb +107 -0
  90. data/lib/astronoby/position.rb +16 -0
  91. data/lib/astronoby/precession.rb +62 -61
  92. data/lib/astronoby/reference_frame.rb +80 -9
  93. data/lib/astronoby/reference_frames/apparent.rb +43 -12
  94. data/lib/astronoby/reference_frames/astrometric.rb +14 -1
  95. data/lib/astronoby/reference_frames/geometric.rb +7 -1
  96. data/lib/astronoby/reference_frames/mean_of_date.rb +19 -1
  97. data/lib/astronoby/reference_frames/teme.rb +159 -0
  98. data/lib/astronoby/reference_frames/topocentric.rb +36 -4
  99. data/lib/astronoby/refraction.rb +26 -5
  100. data/lib/astronoby/root_finder.rb +90 -0
  101. data/lib/astronoby/rotation.rb +49 -0
  102. data/lib/astronoby/time/greenwich_apparent_sidereal_time.rb +19 -8
  103. data/lib/astronoby/time/greenwich_mean_sidereal_time.rb +44 -5
  104. data/lib/astronoby/time/greenwich_sidereal_time.rb +21 -0
  105. data/lib/astronoby/time/local_apparent_sidereal_time.rb +21 -0
  106. data/lib/astronoby/time/local_mean_sidereal_time.rb +21 -0
  107. data/lib/astronoby/time/local_sidereal_time.rb +24 -0
  108. data/lib/astronoby/time/sidereal_time.rb +23 -1
  109. data/lib/astronoby/true_obliquity.rb +4 -0
  110. data/lib/astronoby/util/maths.rb +8 -0
  111. data/lib/astronoby/util/time.rb +0 -497
  112. data/lib/astronoby/vector.rb +10 -0
  113. data/lib/astronoby/velocity.rb +39 -0
  114. data/lib/astronoby/version.rb +1 -1
  115. data/lib/astronoby.rb +30 -0
  116. metadata +80 -5
@@ -36,6 +36,7 @@ module Astronoby
36
36
 
37
37
  # WGS84 Earth Constants
38
38
  WGS84_EARTH_EQUATORIAL_RADIUS_IN_METERS = 6378137
39
+ IAU_MOON_RADIUS_IN_METERS = 0.2725076 * WGS84_EARTH_EQUATORIAL_RADIUS_IN_METERS
39
40
  WGS84_INVERSE_FLATTENING = 298.257223563
40
41
  WGS84_FLATTENING = 1.0 / WGS84_INVERSE_FLATTENING
41
42
  WGS84_ECCENTICITY_SQUARED = 2.0 * WGS84_FLATTENING - WGS84_FLATTENING * WGS84_FLATTENING
@@ -45,7 +46,5 @@ module Astronoby
45
46
  EARTH_FLATTENING_CORRECTION = 0.996647
46
47
 
47
48
  EARTH_ANGULAR_VELOCITY_RAD_PER_S = 7.2921159e-5
48
-
49
- TAI_TT_OFFSET = 32.184
50
49
  end
51
50
  end
@@ -1,9 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Astronoby
4
+ # Represents an IAU constellation with its full name and standard
5
+ # three-letter abbreviation.
4
6
  class Constellation
5
- attr_reader :name, :abbreviation
7
+ # @return [String] the constellation name (e.g., "Orion")
8
+ attr_reader :name
6
9
 
10
+ # @return [String] the IAU three-letter abbreviation (e.g., "Ori")
11
+ attr_reader :abbreviation
12
+
13
+ # @param name [String] the constellation name
14
+ # @param abbreviation [String] the IAU abbreviation
7
15
  def initialize(name, abbreviation)
8
16
  @name = name
9
17
  @abbreviation = abbreviation
@@ -31,7 +31,7 @@ module Astronoby
31
31
  end
32
32
 
33
33
  def self.read_file(filename)
34
- File.readlines(data_path(filename)).map(&:strip)
34
+ File.readlines(data_path(filename), encoding: "UTF-8").map(&:strip)
35
35
  end
36
36
 
37
37
  def self.data_path(filename)
@@ -2,14 +2,23 @@
2
2
 
3
3
  module Astronoby
4
4
  module Coordinates
5
+ # Ecliptic coordinate system (latitude and longitude relative to the
6
+ # ecliptic plane).
5
7
  class Ecliptic
6
- attr_reader :latitude, :longitude
8
+ # @return [Astronoby::Angle] ecliptic latitude
9
+ attr_reader :latitude
7
10
 
11
+ # @return [Astronoby::Angle] ecliptic longitude
12
+ attr_reader :longitude
13
+
14
+ # @param latitude [Astronoby::Angle] ecliptic latitude
15
+ # @param longitude [Astronoby::Angle] ecliptic longitude
8
16
  def initialize(latitude:, longitude:)
9
17
  @latitude = latitude
10
18
  @longitude = longitude
11
19
  end
12
20
 
21
+ # @return [Astronoby::Coordinates::Ecliptic] zero coordinates
13
22
  def self.zero
14
23
  new(latitude: Angle.zero, longitude: Angle.zero)
15
24
  end
@@ -2,9 +2,24 @@
2
2
 
3
3
  module Astronoby
4
4
  module Coordinates
5
+ # Equatorial coordinate system (right ascension and declination).
5
6
  class Equatorial
6
- attr_reader :declination, :right_ascension, :hour_angle, :epoch
7
+ # @return [Astronoby::Angle] declination
8
+ attr_reader :declination
7
9
 
10
+ # @return [Astronoby::Angle] right ascension
11
+ attr_reader :right_ascension
12
+
13
+ # @return [Astronoby::Angle, nil] hour angle, if set
14
+ attr_reader :hour_angle
15
+
16
+ # @return [Numeric] the Julian Date epoch
17
+ attr_reader :epoch
18
+
19
+ # @param declination [Astronoby::Angle] declination
20
+ # @param right_ascension [Astronoby::Angle] right ascension
21
+ # @param hour_angle [Astronoby::Angle, nil] hour angle
22
+ # @param epoch [Numeric] Julian Date epoch (default: J2000.0 = 2451545.0)
8
23
  def initialize(
9
24
  declination:,
10
25
  right_ascension:,
@@ -17,12 +32,23 @@ module Astronoby
17
32
  @epoch = epoch
18
33
  end
19
34
 
20
- def self.zero
21
- new(declination: Angle.zero, right_ascension: Angle.zero)
35
+ # @param epoch [Numeric] Julian Date epoch the coordinates are referred to
36
+ # @return [Astronoby::Coordinates::Equatorial] zero coordinates
37
+ def self.zero(epoch: JulianDate::DEFAULT_EPOCH)
38
+ new(
39
+ declination: Angle.zero,
40
+ right_ascension: Angle.zero,
41
+ epoch: epoch
42
+ )
22
43
  end
23
44
 
24
- def self.from_position_vector(position)
25
- return zero if position.zero?
45
+ # Derives equatorial coordinates from a position vector.
46
+ #
47
+ # @param position [Astronoby::Vector<Astronoby::Distance>] position vector
48
+ # @param epoch [Numeric] Julian Date epoch the vector is referred to
49
+ # @return [Astronoby::Coordinates::Equatorial] equatorial coordinates
50
+ def self.from_position_vector(position, epoch: JulianDate::DEFAULT_EPOCH)
51
+ return zero(epoch: epoch) if position.zero?
26
52
 
27
53
  term1 = position.z.m
28
54
  term2 = position.magnitude.m
@@ -38,9 +64,18 @@ module Astronoby
38
64
  angle
39
65
  )
40
66
 
41
- new(declination: declination, right_ascension: right_ascension)
67
+ new(
68
+ declination: declination,
69
+ right_ascension: right_ascension,
70
+ epoch: epoch
71
+ )
42
72
  end
43
73
 
74
+ # Computes the hour angle for a given time and observer longitude.
75
+ #
76
+ # @param time [Time] the UTC time
77
+ # @param longitude [Astronoby::Angle] the observer's longitude
78
+ # @return [Astronoby::Angle] the hour angle
44
79
  def compute_hour_angle(time:, longitude:)
45
80
  last = LocalApparentSiderealTime.from_utc(time.utc, longitude: longitude)
46
81
  ha = (last.time - @right_ascension.hours)
@@ -49,6 +84,11 @@ module Astronoby
49
84
  Angle.from_hours(ha)
50
85
  end
51
86
 
87
+ # Converts to horizontal coordinates for a given observer and time.
88
+ #
89
+ # @param time [Time] the UTC time
90
+ # @param observer [Astronoby::Observer] the observer
91
+ # @return [Astronoby::Coordinates::Horizontal] horizontal coordinates
52
92
  def to_horizontal(time:, observer:)
53
93
  latitude = observer.latitude
54
94
  longitude = observer.longitude
@@ -74,25 +114,32 @@ module Astronoby
74
114
  )
75
115
  end
76
116
 
117
+ # Converts to ecliptic coordinates.
118
+ #
77
119
  # Source:
78
120
  # Title: Celestial Calculations
79
121
  # Author: J. L. Lawrence
80
122
  # Edition: MIT Press
81
123
  # Chapter: 4 - Orbits and Coordinate Systems
82
- def to_ecliptic(instant:)
83
- mean_obliquity = MeanObliquity.at(instant)
84
-
124
+ #
125
+ # @param instant [Astronoby::Instant] the time instant for the obliquity
126
+ # @param obliquity [Astronoby::Angle] the obliquity of the ecliptic to
127
+ # rotate by. Defaults to the mean obliquity of date; pass the true
128
+ # obliquity when the equatorial coordinates already include nutation
129
+ # (true-of-date apparent and topocentric places).
130
+ # @return [Astronoby::Coordinates::Ecliptic] ecliptic coordinates
131
+ def to_ecliptic(instant:, obliquity: MeanObliquity.at(instant))
85
132
  y = Angle.from_radians(
86
- @right_ascension.sin * mean_obliquity.cos +
87
- @declination.tan * mean_obliquity.sin
133
+ @right_ascension.sin * obliquity.cos +
134
+ @declination.tan * obliquity.sin
88
135
  )
89
136
  x = Angle.from_radians(@right_ascension.cos)
90
137
  r = Angle.atan(y.radians / x.radians)
91
138
  longitude = Util::Trigonometry.adjustement_for_arctangent(y, x, r)
92
139
 
93
140
  latitude = Angle.asin(
94
- @declination.sin * mean_obliquity.cos -
95
- @declination.cos * mean_obliquity.sin * @right_ascension.sin
141
+ @declination.sin * obliquity.cos -
142
+ @declination.cos * obliquity.sin * @right_ascension.sin
96
143
  )
97
144
 
98
145
  Ecliptic.new(
@@ -100,6 +147,25 @@ module Astronoby
100
147
  longitude: longitude
101
148
  )
102
149
  end
150
+
151
+ # Position angle of another point as seen from this one, measured
152
+ # eastward (counterclockwise) from the direction of the north celestial
153
+ # pole. Both points must be expressed in the same frame.
154
+ #
155
+ # @param other [Astronoby::Coordinates::Equatorial] the target point
156
+ # @return [Astronoby::Angle] position angle, between -180 and 180 degrees
157
+ def position_angle_to(other)
158
+ delta_right_ascension = other.right_ascension - @right_ascension
159
+
160
+ Angle.from_radians(
161
+ Math.atan2(
162
+ other.declination.cos * delta_right_ascension.sin,
163
+ other.declination.sin * @declination.cos -
164
+ other.declination.cos * @declination.sin *
165
+ delta_right_ascension.cos
166
+ )
167
+ )
168
+ end
103
169
  end
104
170
  end
105
171
  end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Astronoby
4
+ module Coordinates
5
+ # Geodetic coordinate system (WGS-84 latitude, longitude, and elevation).
6
+ #
7
+ # Geodetic coordinates describe a position on or above the Earth's surface
8
+ # using the WGS-84 reference ellipsoid, the same system used by GPS.
9
+ #
10
+ # The reverse conversion from ECEF (Earth-Centered Earth-Fixed) Cartesian
11
+ # coordinates uses Bowring's iterative method, which converges in 2-3
12
+ # iterations for typical satellite altitudes.
13
+ class Geodetic
14
+ SEMI_MINOR_AXIS =
15
+ Constants::WGS84_EARTH_EQUATORIAL_RADIUS_IN_METERS *
16
+ (1 - Constants::WGS84_FLATTENING)
17
+ SECOND_ECCENTRICITY_SQUARED =
18
+ (Constants::WGS84_EARTH_EQUATORIAL_RADIUS_IN_METERS**2 -
19
+ SEMI_MINOR_AXIS**2) /
20
+ SEMI_MINOR_AXIS**2
21
+ MAX_ITERATIONS = 10
22
+ CONVERGENCE_THRESHOLD = 1e-12
23
+
24
+ # @return [Astronoby::Angle] geodetic latitude
25
+ attr_reader :latitude
26
+
27
+ # @return [Astronoby::Angle] geodetic longitude
28
+ attr_reader :longitude
29
+
30
+ # @return [Astronoby::Distance] elevation above the WGS-84 ellipsoid
31
+ attr_reader :elevation
32
+
33
+ # @param latitude [Astronoby::Angle] geodetic latitude
34
+ # @param longitude [Astronoby::Angle] geodetic longitude
35
+ # @param elevation [Astronoby::Distance] elevation above the WGS-84
36
+ # ellipsoid
37
+ def initialize(latitude:, longitude:, elevation:)
38
+ @latitude = latitude
39
+ @longitude = longitude
40
+ @elevation = elevation
41
+ end
42
+
43
+ # Converts ECEF Cartesian coordinates to geodetic coordinates using
44
+ # Bowring's iterative method.
45
+ #
46
+ # Source:
47
+ # Title: Transformation from Spatial to Geographical Coordinates
48
+ # Author: B. R. Bowring
49
+ # Edition: Survey Review, Vol. 23, No. 181, 1976
50
+ #
51
+ # @param position [Astronoby::Vector<Astronoby::Distance>] ECEF position
52
+ # @return [Astronoby::Coordinates::Geodetic] geodetic coordinates
53
+ def self.from_ecef(position)
54
+ x = position.x.m
55
+ y = position.y.m
56
+ z = position.z.m
57
+
58
+ a = Constants::WGS84_EARTH_EQUATORIAL_RADIUS_IN_METERS
59
+ e2 = Constants::WGS84_ECCENTICITY_SQUARED
60
+
61
+ p = Math.sqrt(x * x + y * y)
62
+ longitude = Math.atan2(y, x)
63
+
64
+ theta = Math.atan2(z * a, p * SEMI_MINOR_AXIS)
65
+ latitude = Math.atan2(
66
+ z + SECOND_ECCENTRICITY_SQUARED * SEMI_MINOR_AXIS *
67
+ Math.sin(theta)**3,
68
+ p - e2 * a * Math.cos(theta)**3
69
+ )
70
+
71
+ MAX_ITERATIONS.times do
72
+ prev_latitude = latitude
73
+ theta = Math.atan2(
74
+ (1 - Constants::WGS84_FLATTENING) * Math.sin(latitude),
75
+ Math.cos(latitude)
76
+ )
77
+ latitude = Math.atan2(
78
+ z + SECOND_ECCENTRICITY_SQUARED * SEMI_MINOR_AXIS *
79
+ Math.sin(theta)**3,
80
+ p - e2 * a * Math.cos(theta)**3
81
+ )
82
+ break if (latitude - prev_latitude).abs < CONVERGENCE_THRESHOLD
83
+ end
84
+
85
+ sin_lat = Math.sin(latitude)
86
+ n = a / Math.sqrt(1 - e2 * sin_lat * sin_lat)
87
+
88
+ elevation = if Math.cos(latitude).abs > 1e-10
89
+ p / Math.cos(latitude) - n
90
+ else
91
+ z / sin_lat - n * (1 - e2)
92
+ end
93
+
94
+ new(
95
+ latitude: Angle.from_radians(latitude),
96
+ longitude: Angle.from_radians(longitude),
97
+ elevation: Distance.from_meters(elevation)
98
+ )
99
+ end
100
+ end
101
+ end
102
+ end
@@ -2,9 +2,21 @@
2
2
 
3
3
  module Astronoby
4
4
  module Coordinates
5
+ # Horizontal coordinate system (azimuth and altitude) for a specific
6
+ # observer.
5
7
  class Horizontal
6
- attr_reader :azimuth, :altitude, :observer
8
+ # @return [Astronoby::Angle] azimuth (measured from north, clockwise)
9
+ attr_reader :azimuth
7
10
 
11
+ # @return [Astronoby::Angle] altitude above the horizon
12
+ attr_reader :altitude
13
+
14
+ # @return [Astronoby::Observer] the observer
15
+ attr_reader :observer
16
+
17
+ # @param azimuth [Astronoby::Angle] azimuth
18
+ # @param altitude [Astronoby::Angle] altitude
19
+ # @param observer [Astronoby::Observer] the observer
8
20
  def initialize(
9
21
  azimuth:,
10
22
  altitude:,
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "iers"
4
+
5
+ module Astronoby
6
+ module DeltaT
7
+ class << self
8
+ # @param instant [Numeric, Time, Date, DateTime] a Julian Date, or a date
9
+ # and time. Any time scale will do.
10
+ # @return [Numeric] ΔT in seconds
11
+ # @raise [Astronoby::IncompatibleArgumentsError] if the instant is not a
12
+ # Numeric, Time, Date or DateTime
13
+ def at(instant)
14
+ case instant
15
+ when Numeric
16
+ IERS::DeltaT.at(jd: instant).delta_t
17
+ when ::Time, ::Date, ::DateTime
18
+ IERS::DeltaT.at(instant).delta_t
19
+ else
20
+ raise IncompatibleArgumentsError,
21
+ "Expected a Numeric, Time, Date or DateTime object, got #{instant.class}"
22
+ end
23
+ rescue IERS::OutOfRangeError => error
24
+ outside_available_range(error)
25
+ end
26
+
27
+ private
28
+
29
+ # IERS covers 1800 up to the end of its EOP series, and refuses anything
30
+ # outside. Rather than propagate that, we answer with the closest value
31
+ # we have: 0 before 1800, where ΔT is small next to the uncertainty on
32
+ # it, and the last measured value after the series, which drifts slowly
33
+ # enough to beat any extrapolation we could invent.
34
+ def outside_available_range(error)
35
+ last_measured = IERS::Data.finals_entries.last
36
+ return 0 if last_measured.nil?
37
+ return 0 if error.requested_mjd.nil?
38
+ return 0 if error.requested_mjd < last_measured.mjd
39
+
40
+ IERS::DeltaT.at(mjd: last_measured.mjd).delta_t
41
+ end
42
+ end
43
+ end
44
+ end
@@ -1,93 +1,128 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Astronoby
4
+ # Represents a distance with meters as its internal representation.
5
+ # Provides conversions between meters, kilometers, astronomical units,
6
+ # and parsecs.
7
+ #
8
+ # @example Create a distance from astronomical units
9
+ # distance = Astronoby::Distance.from_au(1.0)
10
+ # distance.km # => 149597870.7
11
+ #
4
12
  class Distance
5
13
  include Comparable
6
14
 
7
15
  class << self
16
+ # @return [Astronoby::Distance] a zero distance
8
17
  def zero
9
18
  new(0)
10
19
  end
11
20
 
21
+ # @param meters [Numeric] the distance in meters
22
+ # @return [Astronoby::Distance] a new Distance
12
23
  def from_meters(meters)
13
24
  new(meters)
14
25
  end
15
26
  alias_method :from_m, :from_meters
16
27
 
28
+ # @param kilometers [Numeric] the distance in kilometers
29
+ # @return [Astronoby::Distance] a new Distance
17
30
  def from_kilometers(kilometers)
18
31
  meters = kilometers * Constants::KILOMETER_IN_METERS
19
32
  from_meters(meters)
20
33
  end
21
34
  alias_method :from_km, :from_kilometers
22
35
 
36
+ # @param astronomical_units [Numeric] the distance in AU
37
+ # @return [Astronoby::Distance] a new Distance
23
38
  def from_astronomical_units(astronomical_units)
24
39
  meters = astronomical_units * Constants::ASTRONOMICAL_UNIT_IN_METERS
25
40
  from_meters(meters)
26
41
  end
27
42
  alias_method :from_au, :from_astronomical_units
28
43
 
44
+ # @param parsecs [Numeric] the distance in parsecs
45
+ # @return [Astronoby::Distance] a new Distance
29
46
  def from_parsecs(parsecs)
30
47
  meters = parsecs * Constants::PARSEC_IN_METERS
31
48
  from_meters(meters)
32
49
  end
33
50
  alias_method :from_pc, :from_parsecs
34
51
 
52
+ # @param array [Array<Numeric>] array of meter values
53
+ # @return [Astronoby::Vector<Astronoby::Distance>] a vector of Distances
35
54
  def vector_from_meters(array)
36
55
  Vector.elements(array.map { from_meters(_1) })
37
56
  end
38
57
  alias_method :vector_from_m, :vector_from_meters
39
58
  end
40
59
 
60
+ # @return [Numeric] the distance in meters
41
61
  attr_reader :meters
42
62
  alias_method :m, :meters
43
63
 
64
+ # @param meters [Numeric] the distance in meters
44
65
  def initialize(meters)
45
66
  @meters = meters
46
67
  freeze
47
68
  end
48
69
 
70
+ # @return [Float] the distance in kilometers
49
71
  def kilometers
50
72
  @meters / Constants::KILOMETER_IN_METERS.to_f
51
73
  end
52
74
  alias_method :km, :kilometers
53
75
 
76
+ # @return [Float] the distance in astronomical units
54
77
  def astronomical_units
55
78
  @meters / Constants::ASTRONOMICAL_UNIT_IN_METERS.to_f
56
79
  end
57
80
  alias_method :au, :astronomical_units
58
81
 
82
+ # @param other [Astronoby::Distance] distance to add
83
+ # @return [Astronoby::Distance] the sum
59
84
  def +(other)
60
85
  self.class.from_meters(meters + other.meters)
61
86
  end
62
87
 
88
+ # @param other [Astronoby::Distance] distance to subtract
89
+ # @return [Astronoby::Distance] the difference
63
90
  def -(other)
64
91
  self.class.from_meters(@meters - other.meters)
65
92
  end
66
93
 
94
+ # @return [Astronoby::Distance] the negated distance
67
95
  def -@
68
96
  self.class.from_meters(-@meters)
69
97
  end
70
98
 
99
+ # @return [Boolean] true if the distance is positive
71
100
  def positive?
72
101
  meters > 0
73
102
  end
74
103
 
104
+ # @return [Boolean] true if the distance is negative
75
105
  def negative?
76
106
  meters < 0
77
107
  end
78
108
 
109
+ # @return [Boolean] true if the distance is zero
79
110
  def zero?
80
111
  meters.zero?
81
112
  end
82
113
 
114
+ # @return [Numeric] the square of the distance in meters
83
115
  def abs2
84
116
  meters**2
85
117
  end
86
118
 
119
+ # @return [Integer] hash value
87
120
  def hash
88
121
  [meters, self.class].hash
89
122
  end
90
123
 
124
+ # @param other [Astronoby::Distance] distance to compare with
125
+ # @return [Integer, nil] -1, 0, or 1; nil if not comparable
91
126
  def <=>(other)
92
127
  return unless other.is_a?(self.class)
93
128
 
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Astronoby
4
+ class Duration
5
+ include Comparable
6
+
7
+ class << self
8
+ # @return [Astronoby::Duration] a zero duration
9
+ def zero
10
+ new(0)
11
+ end
12
+
13
+ # @param seconds [Numeric] the duration in seconds
14
+ # @return [Astronoby::Duration] a new Duration
15
+ def from_seconds(seconds)
16
+ new(seconds)
17
+ end
18
+
19
+ # @param minutes [Numeric] the duration in minutes
20
+ # @return [Astronoby::Duration] a new Duration
21
+ def from_minutes(minutes)
22
+ seconds = minutes * Constants::SECONDS_PER_MINUTE
23
+ from_seconds(seconds)
24
+ end
25
+
26
+ # @param hours [Numeric] the duration in hours
27
+ # @return [Astronoby::Duration] a new Duration
28
+ def from_hours(hours)
29
+ seconds = hours * Constants::SECONDS_PER_HOUR
30
+ from_seconds(seconds)
31
+ end
32
+
33
+ # @param days [Numeric] the duration in days
34
+ # @return [Astronoby::Duration] a new Duration
35
+ def from_days(days)
36
+ seconds = days * Constants::SECONDS_PER_DAY
37
+ from_seconds(seconds)
38
+ end
39
+ end
40
+
41
+ # @return [Numeric] the duration in seconds
42
+ attr_reader :seconds
43
+
44
+ # @param seconds [Numeric] the duration in seconds
45
+ def initialize(seconds)
46
+ @seconds = seconds
47
+ freeze
48
+ end
49
+
50
+ # @return [Float] the duration in minutes
51
+ def minutes
52
+ @seconds / Constants::SECONDS_PER_MINUTE
53
+ end
54
+
55
+ # @return [Float] the duration in hours
56
+ def hours
57
+ @seconds / Constants::SECONDS_PER_HOUR
58
+ end
59
+
60
+ # @return [Float] the duration in days
61
+ def days
62
+ @seconds / Constants::SECONDS_PER_DAY
63
+ end
64
+
65
+ # @param other [Astronoby::Duration] duration to add
66
+ # @return [Astronoby::Duration] the sum
67
+ def +(other)
68
+ self.class.from_seconds(@seconds + other.seconds)
69
+ end
70
+
71
+ # @param other [Astronoby::Duration] duration to subtract
72
+ # @return [Astronoby::Duration] the difference
73
+ def -(other)
74
+ self.class.from_seconds(@seconds - other.seconds)
75
+ end
76
+
77
+ # @return [Astronoby::Duration] the negated duration
78
+ def -@
79
+ self.class.from_seconds(-@seconds)
80
+ end
81
+
82
+ # @return [Astronoby::Duration] the absolute duration
83
+ def abs
84
+ self.class.from_seconds(@seconds.abs)
85
+ end
86
+
87
+ # @return [Boolean] true if the duration is positive
88
+ def positive?
89
+ @seconds > 0
90
+ end
91
+
92
+ # @return [Boolean] true if the duration is negative
93
+ def negative?
94
+ @seconds < 0
95
+ end
96
+
97
+ # @return [Boolean] true if the duration is zero
98
+ def zero?
99
+ @seconds.zero?
100
+ end
101
+
102
+ # @return [Integer] hash value
103
+ def hash
104
+ [@seconds, self.class].hash
105
+ end
106
+
107
+ # @param other [Astronoby::Duration] duration to compare with
108
+ # @return [Integer, nil] -1, 0, or 1; nil if not comparable
109
+ def <=>(other)
110
+ return unless other.is_a?(self.class)
111
+
112
+ seconds <=> other.seconds
113
+ end
114
+ alias_method :eql?, :==
115
+ end
116
+ end