ako 0.0.3

Ako is a Rust crate that offers a practical and human-friendly approach to creating, manipulating, formatting and converting dates, times and timestamps.
Documentation
use crate::calendar::Iso;
use crate::{AsDate, AsTime, Moment, PlainDateTime};

/// The Julian date corresponding to January 1.5 in the year 2000
const J2000: f64 = 2451545.0;

/// Number of Julian days in a century
const CENTURY_DAYS: f64 = 36525.0;

/// Returns the number of centuries since J2000
pub fn j2000_century(date: f64) -> f64 {
    (date - J2000) / CENTURY_DAYS
}

pub fn to_julian_year(moment: Moment) -> f64 {
    let dt = moment.as_date(Iso);
    let (year, day) = Iso.date_to_ordinal(dt.as_days_since_ako_epoch());

    // start with the year
    let mut y = year.number() as f64;

    // add the fraction of the completed year by day
    y += day as f64 / Iso.year_days(year.number()) as f64;

    // TODO: add the fraction of the completed day by seconds

    y
}

pub fn to_julian_day(moment: Moment) -> f64 {
    // Chapter 7, "Julian Day", Page 61

    let dt = PlainDateTime::from_moment(Iso, moment);
    let (year, month, day) = Iso.date_components(dt.as_date().as_days_since_ako_epoch());
    let mut year = year.number();
    let mut month = month.number();

    if month < 3 {
        year -= 1;
        month += 12;
    }

    let a = year / 100;
    let b = (2 - a + a / 4) as f64;

    let y1 = ((365.25 * (year as f64 + 4716.0)) as i64) as f64;
    let m1 = ((30.6001 * (month as f64 + 1.0)) as i64) as f64;

    // the fraction of the completed day by seconds
    let f = (dt.as_time().as_nanoseconds_since_midnight() as f64) / 86_400.0 / 1_000_000_000.0;

    y1 + m1 + day as f64 + b - 1524.5 + f
}

#[allow(clippy::inconsistent_digit_grouping)]
pub fn from_julian_day(mut date: f64) -> crate::Result<Moment> {
    date += 0.5;

    // let `z` by the integer _days_, and `f` the fractional _nanoseconds_
    let z = date.trunc() as i64;
    let f = date.fract();

    // calculate `a` as follows
    // 100 has been added as a factor to remove decimal points
    // double underscores have been used
    // to indicate where a decimal was in the original formula
    let alpha = (z * 1__00 - 1_867_216__25) / 36_524__25;
    let a = z + 1 + alpha - (alpha / 4);

    // then calculate the following
    // remember the double underscore indicates where a decimal point was in the original formula
    let b = a + 1_524;
    let c = (b * 1__00 - 122__10) / 365__25;
    let d = (365__25 * c) / 1__00;
    let e = ((b - d) * 1__0000) / 30__6001;

    // now we can determine the date on the gregorian calendar
    let day = (b - d) - ((30__6001 * e) / 1__0000);
    let month = if e >= 14 { e - 13 } else { e - 1 };
    let year = c - if month > 2 { 4716 } else { 4715 };

    // now take this calendar date and convert it to a Moment
    match Iso.date(year as i32, month as u8, day as u8) {
        Ok(date) => {
            let m = Moment::from_date(date);
            let f = (f * 86_400.0 * 1_000_000_000.0) as i128;

            // finally, add the time-of-day in nanoseconds
            Ok(Moment::from_unix_nanoseconds(m.to_unix_nanoseconds() + f))
        }

        Err(error) => Err(error),
    }
}

#[cfg(test)]
mod tests {
    use float_cmp::assert_approx_eq;
    use test_case::test_case;

    use super::{Moment, from_julian_day, j2000_century, to_julian_day, to_julian_year};

    #[test_case(2460665.885255802, 0.24971622876938965)]
    fn expect_j2000_century(date: f64, expected: f64) {
        assert_approx_eq!(f64, j2000_century(date), expected);
    }

    #[test_case(1711843200, 2024.2459016393443)]
    fn expect_julian_year(timestamp: i64, year: f64) {
        assert_eq!(to_julian_year(Moment::from_unix_seconds(timestamp)), year);
    }

    #[test_case(2460665.8852546294, 1734772485999982059)]
    #[test_case(2460576.031099537, 1727009086999990046)]
    #[test_case(2460665.8900694447, 1734772902000018954)]
    #[test_case(2506596.5231712963, 5703179602000004053)]
    fn expect_julian_day(date: f64, expected: i128) -> crate::Result<()> {
        let moment = from_julian_day(date)?;

        assert_eq!(moment.to_unix_nanoseconds(), expected);
        assert_eq!(to_julian_day(Moment::from_unix_nanoseconds(expected)), date);
        assert_eq!(to_julian_day(moment), date);

        Ok(())
    }
}