Skip to content

Commit

Permalink
Fix observation events times with local time dates
Browse files Browse the repository at this point in the history
Before, times from `Astronoby::Events::ObservationEvents` could produce
wrong results when the location was far away from the Greenwich
meridian.

The reason was a bug when converting times (hour+minute+second) into
`Time` objects. The date was correctly added by the time was in UTC.
When converting back the UTC time to local time, the date could change.

Now, the date is added depending on the time with concern of the user's
UTC offset.

The offset must be provided hen instantiating the `Astronoby::Observer`
object using a new attribute: `#utc_offset`.

```rb
utc_offset = "-05:00"
time = Time.new(2015, 2, 5, 0, 0, 0, utc_offset)
observer = Astronoby::Observer.new(
  latitude: Astronoby::Angle.from_degrees(38),
  longitude: Astronoby::Angle.from_degrees(-78),
  utc_offset: utc_offset
)
sun = Astronoby::Sun.new(time: time)
observation_events = sun.observation_events(observer: observer)

observation_events.rising_time.getlocal(utc_offset)
 # => 2015-02-05 07:12:59 -0500

observation_events.transit_time.getlocal(utc_offset)
 # => 2015-02-05 12:25:59 -0500

observation_events.setting_time.getlocal(utc_offset)
 # => 2015-02-05 17:39:27 -0500
```
  • Loading branch information
rhannequin committed Nov 8, 2024
1 parent acfae17 commit d8fcb65
Show file tree
Hide file tree
Showing 8 changed files with 156 additions and 41 deletions.
44 changes: 27 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,29 +140,34 @@ horizontal_coordinates.altitude.str(:dms)

#### Sunrise and sunset times and azimuths

Only date part of the time is relevant for the calculation. The offset must
be provided to the observer.

```rb
time = Time.new(2015, 2, 5)
utc_offset = "-05:00"
time = Time.new(2015, 2, 5, 0, 0, 0, utc_offset)
observer = Astronoby::Observer.new(
latitude: Astronoby::Angle.from_degrees(38),
longitude: Astronoby::Angle.from_degrees(-78)
longitude: Astronoby::Angle.from_degrees(-78),
utc_offset: utc_offset
)
sun = Astronoby::Sun.new(time: time)
observation_events = sun.observation_events(observer: observer)

observation_events.rising_time
# => 2015-02-05 12:12:59 UTC
observation_events.rising_time.getlocal(utc_offset)
# => 2015-02-05 07:12:59 -0500

observation_events.rising_azimuth.str(:dms)
# => "+109° 29′ 34.3674″"

observation_events.transit_time
# => 2015-02-05 17:25:59 UTC
observation_events.transit_time.getlocal(utc_offset)
# => 2015-02-05 12:25:59 -0500

observation_events.transit_altitude.str(:dms)
# => "+36° 8′ 15.8197″"

observation_events.setting_time
# => 2015-02-05 22:39:27 UTC
observation_events.setting_time.getlocal(utc_offset)
# => 2015-02-05 17:39:27 -0500

observation_events.setting_azimuth.str(:dms)
# => "+250° 40′ 42.8609″"
Expand Down Expand Up @@ -268,29 +273,34 @@ june_phases.each { puts "#{_1.phase}: #{_1.time}" }

#### Moonrise and moonset times and azimuths

Only date part of the time is relevant for the calculation. The offset must
be provided to the observer.

```rb
time = Time.utc(2024, 6, 1, 10, 0, 0)
utc_offset = "-10:00"
time = Time.new(2024, 9, 1, 0, 0, 0, utc_offset)
observer = Astronoby::Observer.new(
latitude: Astronoby::Angle.from_degrees(48.8566),
longitude: Astronoby::Angle.from_degrees(2.3522)
latitude: Astronoby::Angle.from_degrees(-17.5325),
longitude: Astronoby::Angle.from_degrees(-149.5677),
utc_offset: utc_offset
)
moon = Astronoby::Moon.new(time: time)
observation_events = moon.observation_events(observer: observer)

observation_events.rising_time
# => 2024-06-01 00:35:36 UTC
observation_events.rising_time.getlocal(utc_offset)
# => 2024-09-01 05:24:57 -1000

observation_events.rising_azimuth.str(:dms)
# => "+93° 7′ 43.2347″"

observation_events.transit_time
# => 2024-06-01 02:42:43 UTC
observation_events.transit_time.getlocal(utc_offset)
# => 2024-09-01 11:12:34 -1000

observation_events.transit_altitude.str(:dms)
# => "+26° 59′ 30.9915″"

observation_events.setting_time
# => 2024-06-01 16:02:26 UTC
observation_events.setting_time.getlocal(utc_offset)
# => 2024-09-01 16:12:10 -1000

observation_events.setting_azimuth.str(:dms)
# => "+273° 29′ 30.0954″"
Expand Down
24 changes: 20 additions & 4 deletions lib/astronoby/events/observation_events.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,11 @@ def initialize(

def compute
@initial_transit = initial_transit
@transit_time = Util::Time.decimal_hour_to_time(@date, @initial_transit)
@transit_time = Util::Time.decimal_hour_to_time(
@date,
@observer.utc_offset,
@initial_transit
)
@transit_altitude = local_horizontal_altitude_transit

return if h0.nil?
Expand All @@ -79,11 +83,23 @@ def compute
Constants::HOURS_PER_DAY * @final_setting
)

@rising_time = Util::Time.decimal_hour_to_time(@date, rationalized_corrected_rising)
@rising_time = Util::Time.decimal_hour_to_time(
@date,
@observer.utc_offset,
rationalized_corrected_rising
)
@rising_azimuth = local_horizontal_azimuth_rising
@transit_time = Util::Time.decimal_hour_to_time(@date, rationalized_corrected_transit)
@transit_time = Util::Time.decimal_hour_to_time(
@date,
@observer.utc_offset,
rationalized_corrected_transit
)
@transit_altitude = local_horizontal_altitude_transit
@setting_time = Util::Time.decimal_hour_to_time(@date, rationalized_corrected_setting)
@setting_time = Util::Time.decimal_hour_to_time(
@date,
@observer.utc_offset,
rationalized_corrected_setting
)
@setting_azimuth = local_horizontal_azimuth_setting
end

Expand Down
12 changes: 11 additions & 1 deletion lib/astronoby/observer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,18 @@ class Observer
MOLAR_MASS_OF_AIR = 0.0289644
UNIVERSAL_GAS_CONSTANT = 8.31432

attr_reader :latitude, :longitude, :elevation, :temperature, :pressure
attr_reader :latitude,
:longitude,
:elevation,
:utc_offset,
:temperature,
:pressure

# @param latitude [Angle] geographic latitude of the observer
# @param longitude [Angle] geographic longitude of the observer
# @param elevation [Astronoby::Distance] geographic elevation (or altitude)
# of the observer above sea level
# @param utc_offset [Numeric, String] offset from Coordinated Universal Time
# @param temperature [Numeric] temperature at the observer's location in
# kelvins
# @param pressure [Numeric] atmospheric pressure at the observer's
Expand All @@ -24,12 +30,14 @@ def initialize(
latitude:,
longitude:,
elevation: DEFAULT_ELEVATION,
utc_offset: 0,
temperature: DEFAULT_TEMPERATURE,
pressure: nil
)
@latitude = latitude
@longitude = longitude
@elevation = elevation
@utc_offset = utc_offset
@temperature = temperature
@pressure = pressure || compute_pressure
end
Expand All @@ -40,6 +48,7 @@ def ==(other)
@latitude == other.latitude &&
@longitude == other.longitude &&
@elevation == other.elevation &&
@utc_offset == other.utc_offset &&
@temperature == other.temperature &&
@pressure == other.pressure
end
Expand All @@ -51,6 +60,7 @@ def hash
@latitude,
@longitude,
@elevation,
@utc_offset,
@temperature,
@pressure
].hash
Expand Down
2 changes: 1 addition & 1 deletion lib/astronoby/time/greenwich_sidereal_time.rb
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ def to_utc

utc = SIDEREAL_MINUTE_IN_UT_MINUTE * a

Util::Time.decimal_hour_to_time(date, utc)
Util::Time.decimal_hour_to_time(date, 0, utc)
end

def to_lst(longitude:)
Expand Down
13 changes: 12 additions & 1 deletion lib/astronoby/util/time.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ module Time
# @param date [Date]
# @param decimal [Numeric] Hour of the day, in decimal hours
# @return [::Time] Date and time
def self.decimal_hour_to_time(date, decimal)
def self.decimal_hour_to_time(date, utc_offset, decimal)
absolute_hour = decimal.abs
hour = absolute_hour.floor

Expand All @@ -25,6 +25,17 @@ def self.decimal_hour_to_time(date, decimal)
second = Constants::SECONDS_PER_MINUTE *
(absolute_decimal_minute - absolute_decimal_minute.floor)

date_in_local_time = ::Time
.utc(date.year, date.month, date.day, hour, minute, second)
.getlocal(utc_offset)
.to_date

if date_in_local_time < date
date = date.next_day
elsif date_in_local_time > date
date = date.prev_day
end

::Time.utc(date.year, date.month, date.day, hour, minute, second).round
end

Expand Down
58 changes: 46 additions & 12 deletions spec/astronoby/bodies/sun_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -427,19 +427,36 @@
end

context "when the given time includes a time zone far from the Greenwich meridian" do
it "returns the sunrise time for the right date" do
time = Time.new(1991, 3, 14, 0, 0, 0, "-10:00")
it "returns the sunrise time for the right date when the offset is positive" do
time = Time.utc(1991, 3, 14)
observer = Astronoby::Observer.new(
latitude: Astronoby::Angle.from_degrees(-17.6509),
longitude: Astronoby::Angle.from_degrees(-149.4260)
longitude: Astronoby::Angle.from_degrees(-149.4260),
utc_offset: "+12:00"
)
sun = described_class.new(time: time)
observation_events = sun.observation_events(observer: observer)

rising_time = observation_events.rising_time

expect(rising_time).to eq Time.utc(1991, 3, 14, 16, 0, 16)
# Time from IMCCE: 1991-03-14T16:01:12
expect(rising_time.getlocal(observer.utc_offset).to_date)
.to eq time.to_date
end

it "returns the sunrise time for the right date when the offset is negative" do
time = Time.utc(1991, 3, 14)
observer = Astronoby::Observer.new(
latitude: Astronoby::Angle.from_degrees(-17.6509),
longitude: Astronoby::Angle.from_degrees(-149.4260),
utc_offset: "-12:00"
)
sun = described_class.new(time: time)
observation_events = sun.observation_events(observer: observer)

rising_time = observation_events.rising_time

expect(rising_time.getlocal(observer.utc_offset).to_date)
.to eq time.to_date
end
end
end
Expand Down Expand Up @@ -563,19 +580,36 @@
end

context "when the given time includes a time zone far from the Greenwich meridian" do
it "returns the sunset time for the right date" do
time = Time.new(1991, 3, 14, 6, 0, 0, "+12:00")
it "returns the sunset time for the right date when the offset is positive" do
time = Time.utc(2024, 9, 11)
observer = Astronoby::Observer.new(
latitude: Astronoby::Angle.from_degrees(-36.8509),
longitude: Astronoby::Angle.from_degrees(174.7645)
latitude: Astronoby::Angle.from_degrees(41.87),
longitude: Astronoby::Angle.from_degrees(-87.62),
utc_offset: "+12:00"
)
sun = described_class.new(time: time)
sun = Astronoby::Sun.new(time: time)
observation_events = sun.observation_events(observer: observer)

setting_time = observation_events.setting_time

expect(setting_time.getlocal(observer.utc_offset).to_date)
.to eq time.to_date
end

it "returns the sunset time for the right date when the offset is negative" do
time = Time.utc(2024, 9, 11)
observer = Astronoby::Observer.new(
latitude: Astronoby::Angle.from_degrees(41.87),
longitude: Astronoby::Angle.from_degrees(-87.62),
utc_offset: "-12:00"
)
sun = Astronoby::Sun.new(time: time)
observation_events = sun.observation_events(observer: observer)

setting_time = observation_events.setting_time

expect(setting_time).to eq Time.utc(1991, 3, 14, 6, 42, 40)
# Time from IMCCE: 1991-03-14T06:41:30
expect(setting_time.getlocal(observer.utc_offset).to_date)
.to eq time.to_date
end
end
end
Expand Down
12 changes: 10 additions & 2 deletions spec/astronoby/observer_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@
latitude: Astronoby::Angle.from_degrees(45),
longitude: Astronoby::Angle.from_degrees(90),
elevation: Astronoby::Distance.from_meters(100),
utc_offset: "+01:00",
temperature: 280,
pressure: 10
)
observer2 = described_class.new(
latitude: Astronoby::Angle.from_degrees(45),
longitude: Astronoby::Angle.from_degrees(90),
elevation: Astronoby::Distance.from_meters(100),
utc_offset: "+01:00",
temperature: 280,
pressure: 10
)
Expand All @@ -28,13 +30,15 @@
latitude: Astronoby::Angle.from_degrees(45),
longitude: Astronoby::Angle.from_degrees(90),
elevation: Astronoby::Distance.from_meters(100),
utc_offset: "+01:00",
temperature: 280,
pressure: 10
)
observer2 = described_class.new(
latitude: Astronoby::Angle.from_degrees(45),
longitude: Astronoby::Angle.from_degrees(90),
elevation: Astronoby::Distance.from_meters(100),
utc_offset: "+01:00",
temperature: 280,
pressure: 15
)
Expand All @@ -44,18 +48,20 @@
end

describe "hash equality" do
it "makes an angle foundable as a Hash key" do
it "makes an observer foundable as a Hash key" do
observer1 = described_class.new(
latitude: Astronoby::Angle.from_degrees(45),
longitude: Astronoby::Angle.from_degrees(90),
elevation: Astronoby::Distance.from_meters(100),
utc_offset: "+01:00",
temperature: 280,
pressure: 10
)
observer2 = described_class.new(
latitude: Astronoby::Angle.from_degrees(45),
longitude: Astronoby::Angle.from_degrees(90),
elevation: Astronoby::Distance.from_meters(100),
utc_offset: "+01:00",
temperature: 280,
pressure: 10
)
Expand All @@ -64,18 +70,20 @@
expect(map[observer2]).to eq :observer
end

it "makes an angle foundable in a Set" do
it "makes an observer foundable in a Set" do
observer1 = described_class.new(
latitude: Astronoby::Angle.from_degrees(45),
longitude: Astronoby::Angle.from_degrees(90),
elevation: Astronoby::Distance.from_meters(100),
utc_offset: "+01:00",
temperature: 280,
pressure: 10
)
observer2 = described_class.new(
latitude: Astronoby::Angle.from_degrees(45),
longitude: Astronoby::Angle.from_degrees(90),
elevation: Astronoby::Distance.from_meters(100),
utc_offset: "+01:00",
temperature: 280,
pressure: 10
)
Expand Down
Loading

0 comments on commit d8fcb65

Please sign in to comment.