celestial_time/scales/
gps.rs1use crate::constants::UNIX_EPOCH_JD;
42use crate::julian::JulianDate;
43use crate::parsing::parse_iso8601;
44use crate::{TimeError, TimeResult};
45use celestial_core::constants::SECONDS_PER_DAY_F64;
46use std::fmt;
47use std::str::FromStr;
48
49#[derive(Debug, Clone, Copy, PartialEq)]
54#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
55pub struct GPS(JulianDate);
56
57impl GPS {
58 pub fn new(seconds: i64, nanos: u32) -> Self {
63 let total_seconds =
64 seconds as f64 + nanos as f64 / celestial_core::constants::NANOSECONDS_PER_SECOND_F64;
65 let jd = JulianDate::from_f64(UNIX_EPOCH_JD + total_seconds / SECONDS_PER_DAY_F64);
66 Self(jd)
67 }
68
69 pub fn from_julian_date(jd: JulianDate) -> Self {
71 Self(jd)
72 }
73
74 pub fn from_julian_date_raw(jd1: f64, jd2: f64) -> Self {
79 Self(JulianDate::new(jd1, jd2))
80 }
81
82 pub fn j2000() -> Self {
84 Self(JulianDate::j2000())
85 }
86
87 pub fn to_julian_date(&self) -> JulianDate {
89 self.0
90 }
91
92 pub fn add_seconds(&self, seconds: f64) -> Self {
96 Self(self.0.add_seconds(seconds))
97 }
98
99 pub fn add_days(&self, days: f64) -> Self {
103 Self(self.0.add_days(days))
104 }
105}
106
107impl fmt::Display for GPS {
109 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110 write!(f, "GPS {}", self.0)
111 }
112}
113
114impl From<JulianDate> for GPS {
116 fn from(jd: JulianDate) -> Self {
117 Self::from_julian_date(jd)
118 }
119}
120
121impl FromStr for GPS {
125 type Err = TimeError;
126
127 fn from_str(s: &str) -> TimeResult<Self> {
128 let parsed = parse_iso8601(s)?;
129 Ok(Self::from_julian_date(parsed.to_julian_date()))
130 }
131}
132
133pub fn gps_from_calendar(year: i32, month: u8, day: u8, hour: u8, minute: u8, second: f64) -> GPS {
139 let jd = JulianDate::from_calendar(year, month, day, hour, minute, second);
140 GPS::from_julian_date(jd)
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146 use crate::constants::UNIX_EPOCH_JD;
147 use celestial_core::constants::J2000_JD;
148
149 #[test]
150 fn test_gps_constructors() {
151 assert_eq!(GPS::new(0, 0).to_julian_date().to_f64(), UNIX_EPOCH_JD);
152 assert_eq!(GPS::j2000().to_julian_date().to_f64(), J2000_JD);
153 assert_eq!(
154 gps_from_calendar(2000, 1, 1, 12, 0, 0.0)
155 .to_julian_date()
156 .to_f64(),
157 J2000_JD
158 );
159
160 let jd = JulianDate::new(J2000_JD, 0.123456789);
161 let gps_direct = GPS::from_julian_date(jd);
162 let gps_from_trait: GPS = jd.into();
163 assert_eq!(
164 gps_direct.to_julian_date().jd1(),
165 gps_from_trait.to_julian_date().jd1()
166 );
167 assert_eq!(
168 gps_direct.to_julian_date().jd2(),
169 gps_from_trait.to_julian_date().jd2()
170 );
171 }
172
173 #[test]
174 fn test_gps_arithmetic() {
175 let gps = GPS::j2000();
176 assert_eq!(gps.add_days(1.0).to_julian_date().to_f64(), J2000_JD + 1.0);
177 assert_eq!(
178 gps.add_seconds(3600.0).to_julian_date().to_f64(),
179 J2000_JD + 1.0 / 24.0
180 );
181 }
182
183 #[test]
184 fn test_gps_display() {
185 let display_str = format!("{}", GPS::from_julian_date(JulianDate::new(J2000_JD, 0.5)));
186 assert!(display_str.starts_with("GPS"));
187 assert!(display_str.contains("2451545"));
188 }
189
190 #[test]
191 fn test_gps_string_parsing() {
192 assert_eq!(
193 GPS::from_str("2000-01-01T12:00:00")
194 .unwrap()
195 .to_julian_date()
196 .to_f64(),
197 GPS::j2000().to_julian_date().to_f64()
198 );
199
200 let result = GPS::from_str("2000-01-01T12:00:00.123").unwrap();
201 let expected_jd = J2000_JD + 0.123 / SECONDS_PER_DAY_F64;
202 let diff = (result.to_julian_date().to_f64() - expected_jd).abs();
203 assert!(diff < 1e-14, "fractional seconds diff: {:.2e}", diff);
204
205 assert!(GPS::from_str("invalid-date").is_err());
206 }
207
208 #[cfg(feature = "serde")]
209 #[test]
210 fn test_gps_serde_round_trip() {
211 let test_cases = [
212 GPS::j2000(),
213 GPS::new(0, 0),
214 gps_from_calendar(2024, 6, 15, 14, 30, 45.123),
215 gps_from_calendar(1990, 12, 31, 23, 59, 59.999999999),
216 ];
217
218 for original in test_cases {
219 let json = serde_json::to_string(&original).unwrap();
220 let deserialized: GPS = serde_json::from_str(&json).unwrap();
221
222 let total_diff =
223 (original.to_julian_date().to_f64() - deserialized.to_julian_date().to_f64()).abs();
224 assert!(
225 total_diff < 1e-14,
226 "serde precision loss: {:.2e}",
227 total_diff
228 );
229 }
230 }
231}