From 8898a75df8d1c3557a2902a6ae7bc970025a2c2f Mon Sep 17 00:00:00 2001 From: Niklas Vousten Date: Sat, 21 Oct 2023 00:46:05 +0200 Subject: [PATCH] Added Datetime parsing to SI. Run cargo fmt --- src/lib.rs | 2 +- src/si/client.rs | 6 +- src/si/ephemeris.rs | 168 ++++++++++++++++++++++++++++++-------- src/si/mod.rs | 4 +- tests/real_horizons_si.rs | 110 +++++++++++++------------ 5 files changed, 195 insertions(+), 95 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index f0f4931..8830781 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,7 +8,7 @@ mod utilities; #[cfg(feature = "si")] /// Ephemeris information based on SI-units. -/// +/// /// SI-units from the crate *uom*: pub mod si; diff --git a/src/si/client.rs b/src/si/client.rs index ab46d28..4dca4ac 100644 --- a/src/si/client.rs +++ b/src/si/client.rs @@ -3,9 +3,9 @@ use serde::{Deserialize, Serialize}; use thiserror::Error; use crate::si::ephemeris::{ - EphemerisOrbitalElementsItem, EphemerisOrbitalElementsParser, EphemerisVectorItem, - EphemerisVectorParser, - }; + EphemerisOrbitalElementsItem, EphemerisOrbitalElementsParser, EphemerisVectorItem, + EphemerisVectorParser, +}; /// Generic Horizons response. Their API just gives some JSON with two field, /// some statuses and `result` field which is just human-readable string diff --git a/src/si/ephemeris.rs b/src/si/ephemeris.rs index f4c9554..2990512 100644 --- a/src/si/ephemeris.rs +++ b/src/si/ephemeris.rs @@ -1,7 +1,8 @@ -use crate::utilities::{take_expecting, take_or_empty}; +use chrono::{DateTime, NaiveDateTime, Utc}; +use uom::si::f32::{Angle, AngularVelocity, Length, Time, Velocity}; +use uom::si::{angle, angular_velocity, length, time, velocity}; -use uom::si::f32::{Length, Velocity, Angle, AngularVelocity, Time}; -use uom::si::{length, velocity, angle, angular_velocity, time}; +use crate::utilities::{take_expecting, take_or_empty}; /// Position and velocity of a body. Units are SI-based /// @@ -18,6 +19,9 @@ use uom::si::{length, velocity, angle, angular_velocity, time}; /// | RR | Range-rate; radial velocity wrt coord. center | #[derive(Debug, PartialEq)] pub struct EphemerisVectorItem { + /// Timestamp of the entry in UTC + pub time: DateTime, + /// Position of the moving body relative to the Sun /// /// [x, y, z] @@ -49,6 +53,9 @@ pub struct EphemerisVectorItem { /// For a detailed explenation of keplarian orbital elements, visit [Wikipedia](https://en.wikipedia.org/wiki/Orbital_elements) #[derive(Debug, PartialEq)] pub struct EphemerisOrbitalElementsItem { + /// Timestamp of the entry in UTC + pub time: DateTime, + /// Describes the "roundness" of the orbit. /// /// Value of 0 means a circle, everything until 1 is an eliptic orbit. @@ -112,11 +119,13 @@ pub struct EphemerisOrbitalElementsItem { enum EphemerisVectorParserState { WaitingForSoe, WaitingForDate, - WaitingForPosition, + Date(DateTime), Position { + time: DateTime, position: [Length; 3], }, Complete { + time: DateTime, position: [Length; 3], velocity: [Velocity; 3], }, @@ -126,13 +135,15 @@ enum EphemerisVectorParserState { enum EphemerisOrbitalElementsParserState { WaitingForSoe, WaitingForDate, - WaitingForFirstRow, + Date(DateTime), FirstRow { + time: DateTime, eccentricity: f32, periapsis_distance: Length, inclination: Angle, }, SecondRow { + time: DateTime, eccentricity: f32, periapsis_distance: Length, inclination: Angle, @@ -142,6 +153,7 @@ enum EphemerisOrbitalElementsParserState { time_of_periapsis: Time, }, ThirdRow { + time: DateTime, eccentricity: f32, periapsis_distance: Length, inclination: Angle, @@ -201,10 +213,12 @@ impl<'a, Input: Iterator> Iterator for EphemerisVectorParser<'a, if line == "$$EOE" { self.state = EphemerisVectorParserState::End; } else { - self.state = EphemerisVectorParserState::WaitingForPosition; + let time = parse_date_time(line); + + self.state = EphemerisVectorParserState::Date(time); } } - EphemerisVectorParserState::WaitingForPosition => { + EphemerisVectorParserState::Date(time) => { // TODO: Don't panic. let line = take_expecting(line, " X =").unwrap(); let (x, line) = take_or_empty(line, 22); @@ -216,6 +230,7 @@ impl<'a, Input: Iterator> Iterator for EphemerisVectorParser<'a, let (z, _) = take_or_empty(line, 22); self.state = EphemerisVectorParserState::Position { + time, position: [ Length::new::(x.trim().parse::().unwrap()), Length::new::(y.trim().parse::().unwrap()), @@ -223,7 +238,7 @@ impl<'a, Input: Iterator> Iterator for EphemerisVectorParser<'a, ], }; } - EphemerisVectorParserState::Position { position } => { + EphemerisVectorParserState::Position { time, position } => { // TODO: Don't panic. let line = take_expecting(line, " VX=").unwrap(); let (vx, line) = take_or_empty(line, 22); @@ -235,18 +250,33 @@ impl<'a, Input: Iterator> Iterator for EphemerisVectorParser<'a, let (vz, _) = take_or_empty(line, 22); self.state = EphemerisVectorParserState::Complete { + time, position, velocity: [ - Velocity::new::(vx.trim().parse::().unwrap()), - Velocity::new::(vy.trim().parse::().unwrap()), - Velocity::new::(vz.trim().parse::().unwrap()), + Velocity::new::( + vx.trim().parse::().unwrap(), + ), + Velocity::new::( + vy.trim().parse::().unwrap(), + ), + Velocity::new::( + vz.trim().parse::().unwrap(), + ), ], }; } // Would parse third line and then return Item => ignores third line and returns directly - EphemerisVectorParserState::Complete { position, velocity } => { + EphemerisVectorParserState::Complete { + time, + position, + velocity, + } => { self.state = EphemerisVectorParserState::WaitingForDate; - return Some(EphemerisVectorItem { position, velocity }); + return Some(EphemerisVectorItem { + time, + position, + velocity, + }); } EphemerisVectorParserState::End => { // Should we drain input iterator? @@ -277,10 +307,12 @@ impl<'a, Input: Iterator> Iterator for EphemerisOrbitalElementsP if line == "$$EOE" { self.state = EphemerisOrbitalElementsParserState::End; } else { - self.state = EphemerisOrbitalElementsParserState::WaitingForFirstRow; + let time = parse_date_time(line); + + self.state = EphemerisOrbitalElementsParserState::Date(time); } } - EphemerisOrbitalElementsParserState::WaitingForFirstRow => { + EphemerisOrbitalElementsParserState::Date(time) => { let line = take_expecting(line, " EC=").unwrap(); let (eccentricity, line) = take_or_empty(line, 22); @@ -291,12 +323,18 @@ impl<'a, Input: Iterator> Iterator for EphemerisOrbitalElementsP let (inclination, _) = take_or_empty(line, 22); self.state = EphemerisOrbitalElementsParserState::FirstRow { + time, eccentricity: eccentricity.trim().parse::().unwrap(), - periapsis_distance: Length::new::(periapsis_distance.trim().parse::().unwrap()), - inclination: Angle::new::(inclination.trim().parse::().unwrap()), + periapsis_distance: Length::new::( + periapsis_distance.trim().parse::().unwrap(), + ), + inclination: Angle::new::( + inclination.trim().parse::().unwrap(), + ), }; } EphemerisOrbitalElementsParserState::FirstRow { + time, eccentricity, periapsis_distance, inclination, @@ -311,21 +349,23 @@ impl<'a, Input: Iterator> Iterator for EphemerisOrbitalElementsP let (time_of_periapsis, _) = take_or_empty(line, 22); self.state = EphemerisOrbitalElementsParserState::SecondRow { + time, eccentricity, periapsis_distance, inclination, - longitude_of_ascending_node: Angle::new::(longitude_of_ascending_node - .trim() - .parse::() - .unwrap()), - argument_of_perifocus: Angle::new::(argument_of_perifocus - .trim() - .parse::() - .unwrap()), - time_of_periapsis: Time::new::(time_of_periapsis.trim().parse::().unwrap()), + longitude_of_ascending_node: Angle::new::( + longitude_of_ascending_node.trim().parse::().unwrap(), + ), + argument_of_perifocus: Angle::new::( + argument_of_perifocus.trim().parse::().unwrap(), + ), + time_of_periapsis: Time::new::( + time_of_periapsis.trim().parse::().unwrap(), + ), }; } EphemerisOrbitalElementsParserState::SecondRow { + time, eccentricity, periapsis_distance, inclination, @@ -343,19 +383,27 @@ impl<'a, Input: Iterator> Iterator for EphemerisOrbitalElementsP let (true_anomaly, _) = take_or_empty(line, 22); self.state = EphemerisOrbitalElementsParserState::ThirdRow { + time, eccentricity, periapsis_distance, inclination, longitude_of_ascending_node, argument_of_perifocus, time_of_periapsis, - mean_motion: AngularVelocity::new::(mean_motion.trim().parse::().unwrap()), - mean_anomaly: Angle::new::(mean_anomaly.trim().parse::().unwrap()), - true_anomaly: Angle::new::(true_anomaly.trim().parse::().unwrap()), + mean_motion: AngularVelocity::new::( + mean_motion.trim().parse::().unwrap(), + ), + mean_anomaly: Angle::new::( + mean_anomaly.trim().parse::().unwrap(), + ), + true_anomaly: Angle::new::( + true_anomaly.trim().parse::().unwrap(), + ), }; } // Parses last line and return Item EphemerisOrbitalElementsParserState::ThirdRow { + time, eccentricity, periapsis_distance, inclination, @@ -377,6 +425,7 @@ impl<'a, Input: Iterator> Iterator for EphemerisOrbitalElementsP self.state = EphemerisOrbitalElementsParserState::WaitingForDate; return Some(EphemerisOrbitalElementsItem { + time, eccentricity, periapsis_distance, inclination, @@ -386,12 +435,15 @@ impl<'a, Input: Iterator> Iterator for EphemerisOrbitalElementsP mean_motion, mean_anomaly, true_anomaly, - semi_major_axis: Length::new::(semi_major_axis.trim().parse::().unwrap()), - apoapsis_distance: Length::new::(apoapsis_distance.trim().parse::().unwrap()), - siderral_orbit_period: Time::new::(siderral_orbit_period - .trim() - .parse::() - .unwrap()), + semi_major_axis: Length::new::( + semi_major_axis.trim().parse::().unwrap(), + ), + apoapsis_distance: Length::new::( + apoapsis_distance.trim().parse::().unwrap(), + ), + siderral_orbit_period: Time::new::( + siderral_orbit_period.trim().parse::().unwrap(), + ), }); } EphemerisOrbitalElementsParserState::End => { @@ -407,8 +459,24 @@ impl<'a, Input: Iterator> Iterator for EphemerisOrbitalElementsP } } +fn parse_date_time(line: &str) -> DateTime { + let date_time_str: &str = line.split_terminator('=').collect::>()[1].trim(); + + let date_time_str = take_expecting(date_time_str, "A.D. ").unwrap(); + + //let line = line.trim_end_matches("TDB").trim(); + //let line = line.trim_end_matches(".0000"); + let (time, _) = take_or_empty(date_time_str, 20); //Somehow the formatter does not like %.4f + + NaiveDateTime::parse_from_str(time, "%Y-%b-%d %H:%M:%S") + .unwrap() + .and_utc() +} + #[cfg(test)] mod tests { + use chrono::TimeZone; + use super::*; #[test] @@ -419,6 +487,7 @@ mod tests { // TODO: This will probably fail intermittently due to float comparison. assert_eq!( EphemerisVectorItem { + time: Utc.with_ymd_and_hms(2022, 8, 13, 19, 55, 56).unwrap(), // A.D. 2022-Aug-13 19:55:56.0000 TDB position: [ Length::new::(1.870010427985840E+02), Length::new::(2.484687803242536E+03), @@ -443,6 +512,8 @@ mod tests { // TODO: This will probably fail intermittently due to float comparison. assert_eq!( EphemerisOrbitalElementsItem { + time: Utc.with_ymd_and_hms(2022, 6, 19, 18, 0, 0).unwrap(), // A.D. 2022-Jun-19 18:00:00.0000 TDB + eccentricity: 1.711794334680415E-02, periapsis_distance: Length::new::(1.469885520304013E+08), inclination: Angle::new::(3.134746902320420E-03), @@ -451,7 +522,9 @@ mod tests { argument_of_perifocus: Angle::new::(3.006492364709574E+02), time_of_periapsis: Time::new::(2459584.392523936927), - mean_motion: AngularVelocity::new::(1.141316101270797E-05), + mean_motion: AngularVelocity::new::( + 1.141316101270797E-05 + ), mean_anomaly: Angle::new::(1.635515780663357E+02), true_anomaly: Angle::new::(1.640958153023696E+02), @@ -462,4 +535,27 @@ mod tests { ephem[0] ); } + + #[test] + fn test_parsing_date_time() { + let lines: [&str; 4] = [ + "2459750.250000000 = A.D. 2022-Jun-19 18:00:00.0000 TDB ", // orbital_elements.txt + "2459750.375000000 = A.D. 2022-Jun-19 21:00:00.0000 TDB ", + "2459805.372175926 = A.D. 2022-Aug-13 20:55:56.0000 TDB ", // vector.txt + "2459805.455509259 = A.D. 2022-Aug-13 22:55:56.0000 TDB ", + ]; + + let expected: [DateTime; 4] = [ + Utc.with_ymd_and_hms(2022, 6, 19, 18, 0, 0).unwrap(), + Utc.with_ymd_and_hms(2022, 6, 19, 21, 0, 0).unwrap(), + Utc.with_ymd_and_hms(2022, 8, 13, 20, 55, 56).unwrap(), + Utc.with_ymd_and_hms(2022, 8, 13, 22, 55, 56).unwrap(), + ]; + + for (i, line) in lines.into_iter().enumerate() { + let time = parse_date_time(line); + + assert_eq!(time, expected[i]); + } + } } diff --git a/src/si/mod.rs b/src/si/mod.rs index f229478..0c98933 100644 --- a/src/si/mod.rs +++ b/src/si/mod.rs @@ -1,5 +1,5 @@ -mod ephemeris; mod client; +mod ephemeris; pub use client::{ephemeris_orbital_elements, ephemeris_vector}; -pub use ephemeris::{EphemerisOrbitalElementsItem, EphemerisVectorItem}; \ No newline at end of file +pub use ephemeris::{EphemerisOrbitalElementsItem, EphemerisVectorItem}; diff --git a/tests/real_horizons_si.rs b/tests/real_horizons_si.rs index 3a59a58..a88670c 100644 --- a/tests/real_horizons_si.rs +++ b/tests/real_horizons_si.rs @@ -1,57 +1,61 @@ - #[cfg(feature = "si")] mod si { -/// Tests in this module connect to the real Horizons system. As such, they -/// require Internet access and might start failing if Horizon's API changes. -use chrono::{TimeZone, Utc}; -use rhorizons::si::*; - -use uom::si::f32::Length; -use uom::si::length; - -fn init() { - let _ = env_logger::builder().is_test(true).try_init(); -} - -#[tokio::test] -#[cfg_attr(not(feature = "si"), ignore)] -async fn getting_earths_ephemeris() { - init(); - - // 2457677.000000000 = A.D. 2016-Oct-15 12:00:00.0000 TDB - // X = 1.379561021896053E+08 Y = 5.667156012930278E+07 Z =-2.601196352168918E+03 - // VX=-1.180102398133564E+01 VY= 2.743089439727051E+01 VZ= 3.309367894566151E-05 - // LT= 4.974865749957088E+02 RG= 1.491427231399648E+08 RR=-4.926267109444211E-01 - let vectors = ephemeris_vector( - 399, - Utc.with_ymd_and_hms(2016, 10, 15, 12, 0, 0).unwrap(), - Utc.with_ymd_and_hms(2016, 10, 15, 13, 0, 0).unwrap(), - ) - .await; - - assert_eq!(Length::new::(1.379561021896053E+08), vectors[0].position[0]); -} - -#[tokio::test] -#[cfg_attr(not(feature = "si"), ignore)] -async fn getting_jupiter_ephemeris() { - init(); - - // Target body name: Jupiter (599) {source: jup365_merged} - // Center body name: Sun (10) {source: jup365_merged} - // 2457677.000000000 = A.D. 2016-Oct-15 12:00:00.0000 TDB - // X =-8.125930353044792E+08 Y =-6.890018021386522E+07 Z = 1.846888215010012E+07 - // VX= 9.479984730623543E-01 VY=-1.241342015681963E+01 VZ= 3.033885124560420E-02 - // LT= 2.720942202383012E+03 RG= 8.157179509283365E+08 RR= 1.048282114626244E-01 - let vectors = ephemeris_vector( - 599, - Utc.with_ymd_and_hms(2016, 10, 15, 12, 0, 0).unwrap(), - Utc.with_ymd_and_hms(2016, 10, 15, 13, 0, 0).unwrap(), - ) - .await; - - assert_eq!(Length::new::(-8.125930353044792E+08), vectors[0].position[0]); + /// Tests in this module connect to the real Horizons system. As such, they + /// require Internet access and might start failing if Horizon's API changes. + use chrono::{TimeZone, Utc}; + use rhorizons::si::*; + + use uom::si::f32::Length; + use uom::si::length; + + fn init() { + let _ = env_logger::builder().is_test(true).try_init(); + } + + #[tokio::test] + #[cfg_attr(not(feature = "si"), ignore)] + async fn getting_earths_ephemeris() { + init(); + + // 2457677.000000000 = A.D. 2016-Oct-15 12:00:00.0000 TDB + // X = 1.379561021896053E+08 Y = 5.667156012930278E+07 Z =-2.601196352168918E+03 + // VX=-1.180102398133564E+01 VY= 2.743089439727051E+01 VZ= 3.309367894566151E-05 + // LT= 4.974865749957088E+02 RG= 1.491427231399648E+08 RR=-4.926267109444211E-01 + let vectors = ephemeris_vector( + 399, + Utc.with_ymd_and_hms(2016, 10, 15, 12, 0, 0).unwrap(), + Utc.with_ymd_and_hms(2016, 10, 15, 13, 0, 0).unwrap(), + ) + .await; + + assert_eq!( + Length::new::(1.379561021896053E+08), + vectors[0].position[0] + ); + } + + #[tokio::test] + #[cfg_attr(not(feature = "si"), ignore)] + async fn getting_jupiter_ephemeris() { + init(); + + // Target body name: Jupiter (599) {source: jup365_merged} + // Center body name: Sun (10) {source: jup365_merged} + // 2457677.000000000 = A.D. 2016-Oct-15 12:00:00.0000 TDB + // X =-8.125930353044792E+08 Y =-6.890018021386522E+07 Z = 1.846888215010012E+07 + // VX= 9.479984730623543E-01 VY=-1.241342015681963E+01 VZ= 3.033885124560420E-02 + // LT= 2.720942202383012E+03 RG= 8.157179509283365E+08 RR= 1.048282114626244E-01 + let vectors = ephemeris_vector( + 599, + Utc.with_ymd_and_hms(2016, 10, 15, 12, 0, 0).unwrap(), + Utc.with_ymd_and_hms(2016, 10, 15, 13, 0, 0).unwrap(), + ) + .await; + + assert_eq!( + Length::new::(-8.125930353044792E+08), + vectors[0].position[0] + ); + } } - -} \ No newline at end of file