use core::ops::{Add, Sub};
use crate::{
ConvertUnit, Date, Days, Duration, Month, TryIntoExact,
errors::{InvalidGregorianDate, InvalidHistoricDate, InvalidJulianDate},
units::SecondsPerDay,
};
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ModifiedJulianDate<Representation, Period: ?Sized = SecondsPerDay> {
time_since_epoch: Duration<Representation, Period>,
}
const MODIFIED_JULIAN_DATE_UNIX_EPOCH: Days<i32> = Days::new(40587);
impl<Representation> ModifiedJulianDate<Representation, SecondsPerDay> {
pub const fn new(mjd: Representation) -> Self {
Self::from_time_since_epoch(Days::new(mjd))
}
}
impl<Representation, Period: ?Sized> ModifiedJulianDate<Representation, Period> {
pub const fn from_time_since_epoch(time_since_epoch: Duration<Representation, Period>) -> Self {
Self { time_since_epoch }
}
pub const fn time_since_epoch(&self) -> Duration<Representation, Period>
where
Representation: Copy,
{
self.time_since_epoch
}
pub fn from_date(date: Date<Representation>) -> Self
where
Representation: Copy
+ From<i32>
+ Add<Representation, Output = Representation>
+ ConvertUnit<SecondsPerDay, Period>,
{
Self {
time_since_epoch: date.time_since_epoch().into_unit()
+ MODIFIED_JULIAN_DATE_UNIX_EPOCH.cast().into_unit(),
}
}
pub fn into_date(&self) -> Date<Representation>
where
Representation: Copy
+ From<i32>
+ Sub<Representation, Output = Representation>
+ ConvertUnit<Period, SecondsPerDay>,
{
Date::from_time_since_epoch(
self.time_since_epoch.into_unit() - MODIFIED_JULIAN_DATE_UNIX_EPOCH.cast(),
)
}
pub fn cast<Target>(self) -> ModifiedJulianDate<Target, Period>
where
Representation: Into<Target>,
{
ModifiedJulianDate::from_time_since_epoch(self.time_since_epoch.cast())
}
pub fn try_cast<Target>(
self,
) -> Result<ModifiedJulianDate<Target, Period>, <Representation as TryIntoExact<Target>>::Error>
where
Representation: TryIntoExact<Target>,
{
Ok(ModifiedJulianDate::from_time_since_epoch(
self.time_since_epoch.try_cast()?,
))
}
}
impl ModifiedJulianDate<i32> {
pub fn from_historic_date(
year: i32,
month: Month,
day: u8,
) -> Result<Self, InvalidHistoricDate> {
match Date::from_historic_date(year, month, day) {
Ok(date) => Ok(Self::from_date(date)),
Err(error) => Err(error),
}
}
pub fn from_gregorian_date(
year: i32,
month: Month,
day: u8,
) -> Result<Self, InvalidGregorianDate> {
match Date::from_gregorian_date(year, month, day) {
Ok(date) => Ok(Self::from_date(date)),
Err(error) => Err(error),
}
}
pub fn from_julian_date(year: i32, month: Month, day: u8) -> Result<Self, InvalidJulianDate> {
match Date::from_julian_date(year, month, day) {
Ok(date) => Ok(Self::from_date(date)),
Err(error) => Err(error),
}
}
}
impl<Representation, Period: ?Sized> From<Date<Representation>>
for ModifiedJulianDate<Representation, Period>
where
Representation: Copy
+ From<i32>
+ Add<Representation, Output = Representation>
+ ConvertUnit<SecondsPerDay, Period>,
{
fn from(value: Date<Representation>) -> Self {
Self::from_date(value)
}
}
impl<Representation, Period: ?Sized> From<ModifiedJulianDate<Representation, Period>>
for Date<Representation>
where
Representation: Copy
+ From<i32>
+ Sub<Representation, Output = Representation>
+ ConvertUnit<Period, SecondsPerDay>,
{
fn from(value: ModifiedJulianDate<Representation, Period>) -> Self {
value.into_date()
}
}
#[cfg(test)]
fn check_historic_modified_julian_date(
year: i32,
month: Month,
day: u8,
time_since_epoch: Days<i32>,
) {
assert_eq!(
ModifiedJulianDate::from_historic_date(year, month, day)
.unwrap()
.time_since_epoch(),
time_since_epoch,
);
}
#[test]
fn historic_dates_from_meeus() {
use crate::Month::*;
check_historic_modified_julian_date(2000, January, 1, Days::new(51544));
check_historic_modified_julian_date(1999, January, 1, Days::new(51179));
check_historic_modified_julian_date(1987, January, 27, Days::new(46822));
check_historic_modified_julian_date(1987, June, 19, Days::new(46965));
check_historic_modified_julian_date(1988, January, 27, Days::new(47187));
check_historic_modified_julian_date(1988, June, 19, Days::new(47331));
check_historic_modified_julian_date(1900, January, 1, Days::new(15020));
check_historic_modified_julian_date(1600, January, 1, Days::new(-94553));
check_historic_modified_julian_date(1600, December, 31, Days::new(-94188));
check_historic_modified_julian_date(837, April, 10, Days::new(-373129));
check_historic_modified_julian_date(-123, December, 31, Days::new(-723504));
check_historic_modified_julian_date(-122, January, 1, Days::new(-723503));
check_historic_modified_julian_date(-1000, July, 12, Days::new(-1044000));
check_historic_modified_julian_date(-1000, February, 29, Days::new(-1044134));
check_historic_modified_julian_date(-1001, August, 17, Days::new(-1044330));
check_historic_modified_julian_date(-4712, January, 1, Days::new(-2400001));
}
#[test]
fn float_mjd_from_date() {
let mjd = ModifiedJulianDate::from_historic_date(1997, Month::April, 20)
.unwrap()
.cast();
let time_since_epoch = mjd.time_since_epoch();
assert_eq!(time_since_epoch, Days::new(50558.0f64));
}
#[cfg(kani)]
mod proof_harness {
use super::*;
#[kani::proof]
fn from_date_never_panics() {
let date: Date<i32> = kani::any();
kani::assume(
date.time_since_epoch().count() <= i32::MAX - MODIFIED_JULIAN_DATE_UNIX_EPOCH.count(),
);
let _ = ModifiedJulianDate::<i32>::from_date(date);
}
}