#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LeapSecondTable {
pub source: &'static str,
pub first_mjd: i32,
pub last_mjd: i32,
pub entries: usize,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Ut1Provenance {
pub source: &'static str,
pub first_mjd: i32,
pub last_mjd: i32,
pub first_jd_tt: f64,
pub last_jd_tt: f64,
pub entries: usize,
}
impl Ut1Provenance {
pub fn covers_jd_tt(&self, jd_tt: f64) -> bool {
jd_tt.is_finite() && jd_tt >= self.first_jd_tt && jd_tt <= self.last_jd_tt
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ValidityMode {
Strict,
#[default]
Permissive,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DegradeReason {
BeforeCoverage,
AfterCoverage,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TimeScaleInputErrorKind {
Missing,
NonFinite,
NotPositive,
Negative,
OutOfRange,
FloatParse,
IntParse,
InvalidCivilDate,
InvalidCivilTime,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Validated<T> {
pub value: T,
pub degraded: Option<DegradeReason>,
}
impl<T> Validated<T> {
pub fn ok(value: T) -> Self {
Self {
value,
degraded: None,
}
}
pub fn degraded(value: T, reason: DegradeReason) -> Self {
Self {
value,
degraded: Some(reason),
}
}
pub fn is_valid(&self) -> bool {
self.degraded.is_none()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CoverageError {
InvalidInput {
field: &'static str,
kind: TimeScaleInputErrorKind,
},
OutsideCoverage(DegradeReason),
}
impl core::fmt::Display for CoverageError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
CoverageError::InvalidInput { field, kind } => {
write!(f, "invalid time-scale input {field}: {kind:?}")
}
CoverageError::OutsideCoverage(DegradeReason::BeforeCoverage) => {
write!(f, "instant precedes EOP/UT1 table coverage")
}
CoverageError::OutsideCoverage(DegradeReason::AfterCoverage) => {
write!(f, "instant follows EOP/UT1 table coverage")
}
}
}
}
impl std::error::Error for CoverageError {}
pub fn check_ut1_coverage(
prov: &Ut1Provenance,
jd_tt: f64,
mode: ValidityMode,
) -> Result<Option<DegradeReason>, CoverageError> {
if !jd_tt.is_finite() {
return Err(CoverageError::InvalidInput {
field: "jd_tt",
kind: TimeScaleInputErrorKind::NonFinite,
});
}
let reason = if jd_tt < prov.first_jd_tt {
Some(DegradeReason::BeforeCoverage)
} else if jd_tt > prov.last_jd_tt {
Some(DegradeReason::AfterCoverage)
} else {
None
};
match (mode, reason) {
(ValidityMode::Strict, Some(r)) => Err(CoverageError::OutsideCoverage(r)),
(_, r) => Ok(r),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn provenance() -> Ut1Provenance {
Ut1Provenance {
source: "test",
first_mjd: 0,
last_mjd: 1,
first_jd_tt: 2_451_545.0,
last_jd_tt: 2_451_546.0,
entries: 2,
}
}
#[test]
fn ut1_coverage_rejects_nonfinite_query() {
let prov = provenance();
let expected = Err(CoverageError::InvalidInput {
field: "jd_tt",
kind: TimeScaleInputErrorKind::NonFinite,
});
assert_eq!(
check_ut1_coverage(&prov, f64::NAN, ValidityMode::Strict),
expected
);
assert_eq!(
check_ut1_coverage(&prov, f64::INFINITY, ValidityMode::Permissive),
expected
);
assert!(!prov.covers_jd_tt(f64::NAN));
}
#[test]
fn ut1_coverage_valid_query_is_unchanged() {
let prov = provenance();
assert!(prov.covers_jd_tt(2_451_545.5));
assert_eq!(
check_ut1_coverage(&prov, 2_451_545.5, ValidityMode::Strict),
Ok(None)
);
}
}