use chrono::{DateTime, Utc};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum GpstError {
#[error("Invalid date-time for GPST, is earlier than GPS Epoch: {0}")]
BeforeGPSEpoch(String),
#[error("Could not convert date-time to nanosecond timestamp: {0}")]
TimestampNano(String),
}
const GPS_EPOCH: i64 = 315964800 * TO_NANO_INT;
const TO_NANO_INT: i64 = 1000000000;
const TO_NANO_FLOAT: f64 = 1e9;
const SECONDS_PER_WEEK: f64 = 604800.0;
const NANOSECONDS_PER_WEEK: f64 = SECONDS_PER_WEEK * TO_NANO_FLOAT;
#[derive(Debug, PartialEq)]
pub struct Gpst {
pub seconds: f64,
pub week: i64,
pub week_seconds: f64,
}
pub trait GpstLike {
fn gpst(&self, leap_seconds: bool) -> Result<Gpst, GpstError>;
}
impl GpstLike for DateTime<Utc> {
fn gpst(&self, leap_seconds: bool) -> Result<Gpst, GpstError> {
let timestamp_nanos = self
.timestamp_nanos_opt()
.ok_or(GpstError::TimestampNano(self.to_rfc3339()))?;
let mut nanoseconds = timestamp_nanos - GPS_EPOCH;
if leap_seconds {
nanoseconds += num_leaps(nanoseconds);
}
if nanoseconds < 0 {
GpstError::BeforeGPSEpoch(self.to_rfc3339());
}
let week = nanoseconds as f64 / NANOSECONDS_PER_WEEK;
let week_start = from_gpst(week as i64, 0.0, leap_seconds)?;
let week_start_timestamp_nanos =
week_start
.timestamp_nanos_opt()
.ok_or(GpstError::TimestampNano(format!(
"Week Start: {}",
week_start.to_rfc3339()
)))?;
Ok(Gpst {
seconds: (nanoseconds as f64 / TO_NANO_FLOAT),
week: week as i64,
week_seconds: (timestamp_nanos - week_start_timestamp_nanos) as f64 / TO_NANO_FLOAT,
})
}
}
pub fn from_gpst_seconds(seconds: f64, leap_seconds: bool) -> Result<DateTime<Utc>, GpstError> {
let mut nanoseconds = (seconds * TO_NANO_FLOAT) as i64;
if leap_seconds {
nanoseconds -= num_leaps(nanoseconds);
}
let date_time = DateTime::from_timestamp_nanos(nanoseconds + GPS_EPOCH);
Ok(date_time)
}
pub fn from_gpst(
week: i64,
week_seconds: f64,
leap_seconds: bool,
) -> Result<DateTime<Utc>, GpstError> {
let gps_seconds = (week as f64 * SECONDS_PER_WEEK) + week_seconds;
from_gpst_seconds(gps_seconds, leap_seconds)
}
const LEAP_SECONDS: [i64; 18] = [
46828800, 78364801, 109900802, 173059203, 252028804, 315187205, 346723206, 393984007,
425520008, 457056009, 504489610, 551750411, 599184012, 820108813, 914803214, 1025136015,
1119744016, 1167264017,
];
fn num_leaps(gps_nanoseconds: i64) -> i64 {
let mut count = 0;
for leap_second in LEAP_SECONDS {
let leap_nanoseconds = leap_second * TO_NANO_INT;
if leap_nanoseconds < gps_nanoseconds {
count += TO_NANO_INT;
}
}
count
}
mod tests {
use crate::{from_gpst, Gpst, GpstLike, GPS_EPOCH, LEAP_SECONDS};
use chrono::{DateTime, NaiveDate};
#[test]
fn to() {
let date_time = NaiveDate::from_ymd_opt(2005, 1, 28)
.unwrap()
.and_hms_nano_opt(13, 30, 0, 0)
.unwrap()
.and_utc();
assert_eq!(
date_time.gpst(true).unwrap(),
Gpst {
seconds: 790954213.0,
week: 1307,
week_seconds: 480613.0
}
);
}
#[test]
fn from() {
let date_time = NaiveDate::from_ymd_opt(2005, 1, 28)
.unwrap()
.and_hms_nano_opt(13, 30, 0, 0)
.unwrap()
.and_utc();
assert_eq!(from_gpst(1307, 480613.0, true).unwrap(), date_time)
}
#[test]
fn print_leap_seconds() {
for leap_second in LEAP_SECONDS {
let date_time = DateTime::from_timestamp_nanos(leap_second + GPS_EPOCH);
println!("{}", date_time.to_rfc3339());
}
}
}