#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Calendar {
Gregorian,
Julian,
}
pub const EARLIEST_VALID_GREGORIAN_YEAR: i32 = 1583;
pub fn julian_day(year: i32, month: i32, day: f64, calendar: Calendar) -> f64 {
assert!(year >= -4712);
if year == -4712 {
assert!(day >= 1.5);
}
assert!(month >= 1 && month <= 12);
assert!(day >= 0.0 && day <= 31.0);
let (y, m) = if month > 2 {
(year, month)
} else {
(year - 1, month + 12)
};
let b = match calendar {
Calendar::Julian => 0.0,
Calendar::Gregorian => {
let a: i32 = y / 100;
f64::from(2 - a + a / 4)
}
};
f64::from((365.25 * f64::from(y + 4716)) as i32 + (30.6001 * f64::from(m + 1)) as i32) + day + b
- 1524.5
}
pub fn modified_julian_day(year: i32, month: i32, day: f64, calendar: Calendar) -> f64 {
julian_day(year, month, day, calendar) - 2_400_000.5
}
pub fn jd_to_mjd(julian_day: f64) -> f64 {
julian_day - 2_400_000.5
}
pub fn mjd_to_jd(modified_julian_day: f64) -> f64 {
modified_julian_day + 2_400_000.5
}
pub fn is_leap_year(year: i32, calendar: Calendar) -> bool {
match calendar {
Calendar::Julian => year % 4 == 0,
Calendar::Gregorian => (year % 4 == 0 && year % 100 != 0) || year % 400 == 0,
}
}
pub fn julian_day_to_calendar_date(julian_day: f64) -> (i32, i32, f64) {
assert!(julian_day >= 0.0);
let jd = julian_day + 0.5;
let integer_part = jd.floor();
let fractional_part = jd - integer_part;
let b = 1524.0
+ if integer_part < 2_299_161.0 {
integer_part
} else {
let alpha = ((integer_part - 1867216.25) / 36524.25).floor();
integer_part + 1.0 + alpha - (alpha / 4.0).floor()
};
let c = ((b - 122.1) / 365.25).floor();
let d = (365.25 * c).floor();
let e = ((b - d) / 30.6001).floor();
let day_of_month = b - d - (30.6001 * e).floor() + fractional_part;
let month = if e < 14.0 {
e as i32 - 1
} else {
e as i32 - 13
};
let year = if month > 2 {
c as i32 - 4716
} else {
c as i32 - 4715
};
(year, month, day_of_month)
}
pub fn interval_in_days(date1: (i32, i32, f64), date2: (i32, i32, f64), calendar: Calendar) -> f64 {
let jd1 = julian_day(date1.0, date1.1, date1.2, calendar);
let jd2 = julian_day(date2.0, date2.1, date2.2, calendar);
jd2 - jd1
}
#[derive(PartialEq, Eq, Clone, Copy, Debug, Hash)]
pub enum DayOfWeek {
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
Sunday,
}
pub fn day_of_week(julian_day: f64) -> DayOfWeek {
assert!(julian_day > 0.0);
let w = (julian_day + 1.5) as i32 % 7;
match w {
0 => DayOfWeek::Sunday,
1 => DayOfWeek::Monday,
2 => DayOfWeek::Tuesday,
3 => DayOfWeek::Wednesday,
4 => DayOfWeek::Thursday,
5 => DayOfWeek::Friday,
6 => DayOfWeek::Saturday,
_ => panic!(
"failed to compute day of week for Julian Day {}",
julian_day
),
}
}
pub fn day_of_year(year: i32, month: i32, day: f64, calendar: Calendar) -> f64 {
let k = if is_leap_year(year, calendar) {
1.0
} else {
2.0
};
let month = f64::from(month);
(275.0 * month / 9.0).floor() - k * ((month + 9.0) / 12.0).floor() + day - 30.0
}
pub fn date_from_day_of_year(year: i32, day_of_year: f64, calendar: Calendar) -> (i32, f64) {
let k = if is_leap_year(year, calendar) {
1.0
} else {
2.0
};
let month = if day_of_year < 32.0 {
1.0
} else {
(9.0 * (k + day_of_year) / 275.0 + 0.98).floor()
};
let day =
day_of_year - (275.0 * month / 9.0).floor() + k * ((month + 9.0) / 12.0).floor() + 30.0;
(month as i32, day)
}
pub fn gregorian_easter(year: i32) -> (i32, i32) {
assert!(year >= EARLIEST_VALID_GREGORIAN_YEAR);
let a = year % 19;
let (b, c) = (year / 100, year % 100);
let (d, e) = (b / 4, b % 4);
let f = (b + 8) / 25;
let g = (b - f + 1) / 3;
let h = (19 * a + b - d - g + 15) % 30;
let (i, k) = (c / 4, c % 4);
let l = (32 + 2 * e + 2 * i - h - k) % 7;
let m = (a + 11 * h + 22 * l) / 451;
let n = h + l - 7 * m + 114;
let month = n / 31;
let day_of_easter_sunday = n % 31 + 1;
(month, day_of_easter_sunday)
}
pub fn julian_easter(year: i32) -> (i32, i32) {
let a = year % 4;
let b = year % 7;
let c = year % 19;
let d = (19 * c + 15) % 30;
let e = (2 * a + 4 * b - d + 34) % 7;
let f = d + e + 114;
let month = f / 31;
let day_of_easter_sunday = f % 31 + 1;
(month, day_of_easter_sunday)
}
pub fn passover_date(year: i32, calendar: Calendar) -> (i32, i32) {
if calendar == Calendar::Gregorian {
assert!(year >= EARLIEST_VALID_GREGORIAN_YEAR);
}
let c = year / 100;
let s = match calendar {
Calendar::Gregorian => (3 * c - 5) / 4,
Calendar::Julian => 0,
};
let a = (12 * year + 12) % 19;
let b = year % 4;
let q = -1.904412361576 + 1.554241796621 * f64::from(a) + 0.25 * f64::from(b)
- 0.003177794022 * f64::from(year)
+ f64::from(s);
let j = (q.floor() as i32 + 3 * year + 5 * b + 2 - s) % 7;
let r = q - q.floor();
let q_int = q.floor() as i32;
let d = if matches!(j, 2 | 4 | 6) {
q_int + 23
} else if j == 1 && a > 6 && r >= 0.632870370 {
q_int + 24
} else if j == 0 && a > 11 && r >= 0.897723765 {
q_int + 23
} else {
q_int + 22
};
if d <= 31 {
(3, d)
} else {
(4, d - 31)
}
}
pub fn hebrew_year(julian_or_gregorian_year: i32) -> i32 {
julian_or_gregorian_year + 3760
}
pub fn number_of_months_in_hebrew_year(hebrew_year: i32) -> i32 {
assert!(hebrew_year >= 0);
match hebrew_year % 19 {
0 | 3 | 6 | 8 | 11 | 14 | 17 => 13,
_ => 12,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::assert_approx_equal;
#[test]
fn jd_test() {
assert_approx_equal(julian_day(1957, 10, 4.81, Calendar::Gregorian), 2436116.31);
}
#[test]
fn more_jd_tests() {
assert_approx_equal(julian_day(2000, 1, 1.5, Calendar::Gregorian), 2451545.0);
assert_approx_equal(julian_day(1999, 1, 1.0, Calendar::Gregorian), 2451179.5);
assert_approx_equal(julian_day(1987, 1, 27.0, Calendar::Gregorian), 2446822.5);
assert_approx_equal(julian_day(1987, 6, 19.5, Calendar::Gregorian), 2446966.0);
assert_approx_equal(julian_day(1988, 1, 27.0, Calendar::Gregorian), 2447187.5);
assert_approx_equal(julian_day(1988, 6, 19.5, Calendar::Gregorian), 2447332.0);
assert_approx_equal(julian_day(1900, 1, 1.0, Calendar::Gregorian), 2415020.5);
assert_approx_equal(julian_day(1600, 1, 1.0, Calendar::Gregorian), 2305447.5);
assert_approx_equal(julian_day(1600, 12, 31.0, Calendar::Gregorian), 2305812.5);
assert_approx_equal(julian_day(837, 4, 10.3, Calendar::Julian), 2026871.8);
assert_approx_equal(julian_day(-123, 12, 31.0, Calendar::Julian), 1676496.5);
assert_approx_equal(julian_day(-122, 1, 1.0, Calendar::Julian), 1676497.5);
assert_approx_equal(julian_day(-1000, 7, 12.5, Calendar::Julian), 1356001.0);
assert_approx_equal(julian_day(-1000, 2, 29.0, Calendar::Julian), 1355866.5);
assert_approx_equal(julian_day(-1001, 8, 17.9, Calendar::Julian), 1355671.4);
assert_approx_equal(julian_day(-4712, 1, 1.5, Calendar::Julian), 0.0);
}
#[test]
fn rev_jd() {
let jd = 2436116.31;
let (year, month, day) = julian_day_to_calendar_date(jd);
assert_eq!(year, 1957);
assert_eq!(month, 10);
assert_approx_equal(day, 4.81);
}
#[test]
fn rev_jd_1842713() {
let jd = 1842713.0;
let (year, month, day) = julian_day_to_calendar_date(jd);
assert_eq!(year, 333);
assert_eq!(month, 1);
assert_approx_equal(day, 27.5);
}
#[test]
fn rev_jd_1507900() {
let jd = 1507900.13;
let (year, month, day) = julian_day_to_calendar_date(jd);
assert_eq!(year, -584);
assert_eq!(month, 5);
assert_approx_equal(day, 28.63);
}
#[test]
fn comet_halley_interval() {
let interval = interval_in_days((1910, 4, 20.0), (1986, 2, 9.0), Calendar::Gregorian);
assert_eq!(interval, 27689.0);
}
#[test]
fn day_of_week_tests() {
assert_eq!(
day_of_week(julian_day(2021, 5, 8.0, Calendar::Gregorian)),
DayOfWeek::Saturday
);
assert_eq!(
day_of_week(julian_day(2021, 5, 8.999999, Calendar::Gregorian)),
DayOfWeek::Saturday
);
assert_eq!(
day_of_week(julian_day(1954, 6, 30.0, Calendar::Gregorian)),
DayOfWeek::Wednesday
);
assert_eq!(
day_of_week(julian_day(1582, 10, 4.0, Calendar::Julian)),
DayOfWeek::Thursday
);
assert_eq!(
day_of_week(julian_day(1582, 10, 15.0, Calendar::Gregorian)),
DayOfWeek::Friday
);
}
#[test]
fn day_of_year_tests() {
assert_eq!(day_of_year(1978, 11, 14.0, Calendar::Gregorian), 318.0);
assert_eq!(day_of_year(1978, 11, 14.9, Calendar::Gregorian), 318.9);
assert_eq!(day_of_year(1988, 4, 22.0, Calendar::Gregorian), 113.0);
}
#[test]
fn date_from_day_of_year_tests() {
assert_eq!(
date_from_day_of_year(1978, 318.0, Calendar::Gregorian),
(11, 14.0)
);
assert_eq!(
date_from_day_of_year(1978, 318.75, Calendar::Gregorian),
(11, 14.75)
);
assert_eq!(
date_from_day_of_year(1988, 113.0, Calendar::Gregorian),
(4, 22.0)
);
}
#[test]
fn easter() {
assert_eq!(gregorian_easter(2021), (4, 4));
assert_eq!(gregorian_easter(2015), (4, 5));
assert_eq!(gregorian_easter(1991), (3, 31));
assert_eq!(gregorian_easter(1992), (4, 19));
assert_eq!(gregorian_easter(1993), (4, 11));
assert_eq!(gregorian_easter(1954), (4, 18));
assert_eq!(gregorian_easter(2000), (4, 23));
assert_eq!(gregorian_easter(1818), (3, 22));
assert_eq!(gregorian_easter(2285), (3, 22));
assert_eq!(gregorian_easter(1886), (4, 25));
assert_eq!(gregorian_easter(1943), (4, 25));
assert_eq!(gregorian_easter(2038), (4, 25));
assert_eq!(julian_easter(179), (4, 12));
assert_eq!(julian_easter(711), (4, 12));
assert_eq!(julian_easter(1243), (4, 12));
}
#[test]
fn passover() {
assert_eq!(passover_date(1990, Calendar::Gregorian), (4, 10));
assert_eq!(hebrew_year(1990), 5750);
assert_eq!(number_of_months_in_hebrew_year(5751), 12);
}
}