use crate::astronomy::{self, Astronomical, Location, MEAN_SYNODIC_MONTH, MEAN_TROPICAL_YEAR};
use crate::helpers::i64_to_i32;
use crate::rata_die::{Moment, RataDie};
use core::num::NonZeroU8;
#[allow(unused_imports)]
use core_maths::*;
const MAX_ITERS_FOR_MONTHS_OF_YEAR: u8 = 14;
pub trait ChineseBased {
fn location(fixed: RataDie) -> Location;
const EPOCH: RataDie;
}
const CHINESE_EPOCH: RataDie = RataDie::new(-963099);
const UTC_OFFSET_PRE_1929: f64 = (1397.0 / 180.0) / 24.0;
const UTC_OFFSET_POST_1929: f64 = 8.0 / 24.0;
const CHINESE_LOCATION_PRE_1929: Location =
Location::new_unchecked(39.0, 116.0, 43.5, UTC_OFFSET_PRE_1929);
const CHINESE_LOCATION_POST_1929: Location =
Location::new_unchecked(39.0, 116.0, 43.5, UTC_OFFSET_POST_1929);
const KOREAN_EPOCH: RataDie = RataDie::new(-852065);
const UTC_OFFSET_ORIGINAL: f64 = (3809.0 / 450.0) / 24.0;
const UTC_OFFSET_1908: f64 = 8.5 / 24.0;
const UTC_OFFSET_1912: f64 = 9.0 / 24.0;
const UTC_OFFSET_1954: f64 = 8.5 / 24.0;
const UTC_OFFSET_1961: f64 = 9.0 / 24.0;
const FIXED_1908: RataDie = RataDie::new(696608); const FIXED_1912: RataDie = RataDie::new(697978); const FIXED_1954: RataDie = RataDie::new(713398); const FIXED_1961: RataDie = RataDie::new(716097);
const KOREAN_LATITUDE: f64 = 37.0 + (34.0 / 60.0);
const KOREAN_LONGITUDE: f64 = 126.0 + (58.0 / 60.0);
const KOREAN_ELEVATION: f64 = 0.0;
const KOREAN_LOCATION_ORIGINAL: Location = Location::new_unchecked(
KOREAN_LATITUDE,
KOREAN_LONGITUDE,
KOREAN_ELEVATION,
UTC_OFFSET_ORIGINAL,
);
const KOREAN_LOCATION_1908: Location = Location::new_unchecked(
KOREAN_LATITUDE,
KOREAN_LONGITUDE,
KOREAN_ELEVATION,
UTC_OFFSET_1908,
);
const KOREAN_LOCATION_1912: Location = Location::new_unchecked(
KOREAN_LATITUDE,
KOREAN_LONGITUDE,
KOREAN_ELEVATION,
UTC_OFFSET_1912,
);
const KOREAN_LOCATION_1954: Location = Location::new_unchecked(
KOREAN_LATITUDE,
KOREAN_LONGITUDE,
KOREAN_ELEVATION,
UTC_OFFSET_1954,
);
const KOREAN_LOCATION_1961: Location = Location::new_unchecked(
KOREAN_LATITUDE,
KOREAN_LONGITUDE,
KOREAN_ELEVATION,
UTC_OFFSET_1961,
);
#[derive(Debug)]
#[allow(clippy::exhaustive_structs)] pub struct Chinese;
#[derive(Debug)]
#[allow(clippy::exhaustive_structs)] pub struct Dangi;
impl ChineseBased for Chinese {
fn location(fixed: RataDie) -> Location {
let year = crate::iso::iso_year_from_fixed(fixed);
if year < 1929 {
CHINESE_LOCATION_PRE_1929
} else {
CHINESE_LOCATION_POST_1929
}
}
const EPOCH: RataDie = CHINESE_EPOCH;
}
impl ChineseBased for Dangi {
fn location(fixed: RataDie) -> Location {
if fixed < FIXED_1908 {
KOREAN_LOCATION_ORIGINAL
} else if fixed < FIXED_1912 {
KOREAN_LOCATION_1908
} else if fixed < FIXED_1954 {
KOREAN_LOCATION_1912
} else if fixed < FIXED_1961 {
KOREAN_LOCATION_1954
} else {
KOREAN_LOCATION_1961
}
}
const EPOCH: RataDie = KOREAN_EPOCH;
}
#[derive(Debug, Copy, Clone)]
#[allow(clippy::exhaustive_structs)] pub struct YearBounds {
pub new_year: RataDie,
pub next_new_year: RataDie,
}
impl YearBounds {
#[inline]
pub fn compute<C: ChineseBased>(date: RataDie) -> Self {
let prev_solstice = winter_solstice_on_or_before::<C>(date);
let (new_year, next_solstice) = new_year_on_or_before_fixed_date::<C>(date, prev_solstice);
let next_new_year = new_year_on_or_before_fixed_date::<C>(new_year + 400, next_solstice).0;
Self {
new_year,
next_new_year,
}
}
pub fn count_days(self) -> u16 {
let result = self.next_new_year - self.new_year;
debug_assert!(
((u16::MIN as i64)..=(u16::MAX as i64)).contains(&result),
"Days in year should be in range of u16."
);
result as u16
}
pub fn is_leap(self) -> bool {
let difference = self.next_new_year - self.new_year;
difference > 365
}
}
pub(crate) fn major_solar_term_from_fixed<C: ChineseBased>(date: RataDie) -> u32 {
let moment: Moment = date.as_moment();
let location = C::location(date);
let universal: Moment = Location::universal_from_standard(moment, location);
let solar_longitude =
i64_to_i32(Astronomical::solar_longitude(Astronomical::julian_centuries(universal)) as i64);
debug_assert!(
solar_longitude.is_ok(),
"Solar longitude should be in range of i32"
);
let s = solar_longitude.unwrap_or_else(|e| e.saturate());
let result_signed = (2 + s.div_euclid(30) - 1).rem_euclid(12) + 1;
debug_assert!(result_signed >= 0);
result_signed as u32
}
pub(crate) fn no_major_solar_term<C: ChineseBased>(date: RataDie) -> bool {
major_solar_term_from_fixed::<C>(date)
== major_solar_term_from_fixed::<C>(new_moon_on_or_after::<C>((date + 1).as_moment()))
}
pub(crate) fn new_moon_on_or_after<C: ChineseBased>(moment: Moment) -> RataDie {
let new_moon_moment = Astronomical::new_moon_at_or_after(midnight::<C>(moment));
let location = C::location(new_moon_moment.as_rata_die());
Location::standard_from_universal(new_moon_moment, location).as_rata_die()
}
pub(crate) fn new_moon_before<C: ChineseBased>(moment: Moment) -> RataDie {
let new_moon_moment = Astronomical::new_moon_before(midnight::<C>(moment));
let location = C::location(new_moon_moment.as_rata_die());
Location::standard_from_universal(new_moon_moment, location).as_rata_die()
}
pub(crate) fn midnight<C: ChineseBased>(moment: Moment) -> Moment {
Location::universal_from_standard(moment, C::location(moment.as_rata_die()))
}
pub(crate) fn new_year_in_sui<C: ChineseBased>(prior_solstice: RataDie) -> (RataDie, RataDie) {
let following_solstice = winter_solstice_on_or_before::<C>(prior_solstice + 370); let month_after_eleventh = new_moon_on_or_after::<C>((prior_solstice + 1).as_moment()); let month_after_twelfth = new_moon_on_or_after::<C>((month_after_eleventh + 1).as_moment()); let next_eleventh_month = new_moon_before::<C>((following_solstice + 1).as_moment()); let lhs_argument =
((next_eleventh_month - month_after_eleventh) as f64 / MEAN_SYNODIC_MONTH).round() as i64;
if lhs_argument == 12
&& (no_major_solar_term::<C>(month_after_eleventh)
|| no_major_solar_term::<C>(month_after_twelfth))
{
(
new_moon_on_or_after::<C>((month_after_twelfth + 1).as_moment()),
following_solstice,
)
} else {
(month_after_twelfth, following_solstice)
}
}
pub(crate) fn winter_solstice_on_or_before<C: ChineseBased>(date: RataDie) -> RataDie {
let approx = Astronomical::estimate_prior_solar_longitude(
astronomy::WINTER,
midnight::<C>((date + 1).as_moment()),
);
let mut iters = 0;
let mut day = Moment::new((approx.inner() - 1.0).floor());
while iters < MAX_ITERS_FOR_MONTHS_OF_YEAR
&& astronomy::WINTER
>= Astronomical::solar_longitude(Astronomical::julian_centuries(midnight::<C>(
day + 1.0,
)))
{
iters += 1;
day += 1.0;
}
debug_assert!(
iters < MAX_ITERS_FOR_MONTHS_OF_YEAR,
"Number of iterations was higher than expected"
);
day.as_rata_die()
}
pub(crate) fn new_year_on_or_before_fixed_date<C: ChineseBased>(
date: RataDie,
prior_solstice: RataDie,
) -> (RataDie, RataDie) {
let new_year = new_year_in_sui::<C>(prior_solstice);
if date >= new_year.0 {
new_year
} else {
let date_in_last_sui = date - 180; let prior_solstice = winter_solstice_on_or_before::<C>(date_in_last_sui);
new_year_in_sui::<C>(prior_solstice)
}
}
pub fn fixed_mid_year_from_year<C: ChineseBased>(elapsed_years: i32) -> RataDie {
let cycle = (elapsed_years - 1).div_euclid(60) + 1;
let year = (elapsed_years - 1).rem_euclid(60) + 1;
C::EPOCH + ((((cycle - 1) * 60 + year - 1) as f64 + 0.5) * MEAN_TROPICAL_YEAR) as i64
}
pub fn is_leap_year<C: ChineseBased>(year: i32) -> bool {
let mid_year = fixed_mid_year_from_year::<C>(year);
YearBounds::compute::<C>(mid_year).is_leap()
}
pub fn last_month_day_in_year<C: ChineseBased>(year: i32) -> (u8, u8) {
let mid_year = fixed_mid_year_from_year::<C>(year);
let year_bounds = YearBounds::compute::<C>(mid_year);
let last_day = year_bounds.next_new_year - 1;
let month = if year_bounds.is_leap() { 13 } else { 12 };
let day = last_day - new_moon_before::<C>(last_day.as_moment()) + 1;
(month, day as u8)
}
pub fn days_in_provided_year<C: ChineseBased>(year: i32) -> u16 {
let mid_year = fixed_mid_year_from_year::<C>(year);
let bounds = YearBounds::compute::<C>(mid_year);
bounds.count_days()
}
#[derive(Debug)]
#[non_exhaustive]
pub struct ChineseFromFixedResult {
pub year: i32,
pub month: u8,
pub day: u8,
pub year_bounds: YearBounds,
pub leap_month: Option<NonZeroU8>,
}
pub fn chinese_based_date_from_fixed<C: ChineseBased>(date: RataDie) -> ChineseFromFixedResult {
let year_bounds = YearBounds::compute::<C>(date);
let first_day_of_year = year_bounds.new_year;
let year_float =
(1.5 - 1.0 / 12.0 + ((first_day_of_year - C::EPOCH) as f64) / MEAN_TROPICAL_YEAR).floor();
let year_int = i64_to_i32(year_float as i64);
debug_assert!(year_int.is_ok(), "Year should be in range of i32");
let year = year_int.unwrap_or_else(|e| e.saturate());
let new_moon = new_moon_before::<C>((date + 1).as_moment());
let month_i64 = ((new_moon - first_day_of_year) as f64 / MEAN_SYNODIC_MONTH).round() as i64 + 1;
debug_assert!(
((u8::MIN as i64)..=(u8::MAX as i64)).contains(&month_i64),
"Month should be in range of u8! Value {month_i64} failed for RD {date:?}"
);
let month = month_i64 as u8;
let day_i64 = date - new_moon + 1;
debug_assert!(
((u8::MIN as i64)..=(u8::MAX as i64)).contains(&month_i64),
"Day should be in range of u8! Value {month_i64} failed for RD {date:?}"
);
let day = day_i64 as u8;
let leap_month = if year_bounds.is_leap() {
NonZeroU8::new(get_leap_month_from_new_year::<C>(first_day_of_year))
} else {
None
};
ChineseFromFixedResult {
year,
month,
day,
year_bounds,
leap_month,
}
}
pub fn get_leap_month_from_new_year<C: ChineseBased>(new_year: RataDie) -> u8 {
let mut cur = new_year;
let mut result = 1;
while result < MAX_ITERS_FOR_MONTHS_OF_YEAR && !no_major_solar_term::<C>(cur) {
cur = new_moon_on_or_after::<C>((cur + 1).as_moment());
result += 1;
}
debug_assert!(result < MAX_ITERS_FOR_MONTHS_OF_YEAR, "The given year was not a leap year and an unexpected number of iterations occurred searching for a leap month.");
result
}
pub fn month_days<C: ChineseBased>(year: i32, month: u8) -> u8 {
let mid_year = fixed_mid_year_from_year::<C>(year);
let prev_solstice = winter_solstice_on_or_before::<C>(mid_year);
let new_year = new_year_on_or_before_fixed_date::<C>(mid_year, prev_solstice).0;
days_in_month::<C>(month, new_year, None).0
}
pub fn days_in_month<C: ChineseBased>(
month: u8,
new_year: RataDie,
prev_new_moon: Option<RataDie>,
) -> (u8, RataDie) {
let approx = new_year + ((month - 1) as i64 * 29);
let prev_new_moon = if let Some(prev_moon) = prev_new_moon {
prev_moon
} else {
new_moon_before::<C>((approx + 15).as_moment())
};
let next_new_moon = new_moon_on_or_after::<C>((approx + 15).as_moment());
let result = (next_new_moon - prev_new_moon) as u8;
debug_assert!(result == 29 || result == 30);
(result, next_new_moon)
}
pub fn days_until_month<C: ChineseBased>(new_year: RataDie, month: u8) -> u16 {
let month_approx = 28_u16.saturating_mul(u16::from(month) - 1);
let new_moon = new_moon_on_or_after::<C>(new_year.as_moment() + (month_approx as f64));
let result = new_moon - new_year;
debug_assert!(((u16::MIN as i64)..=(u16::MAX as i64)).contains(&result), "Result {result} from new moon: {new_moon:?} and new year: {new_year:?} should be in range of u16!");
result as u16
}
#[cfg(test)]
mod test {
use super::*;
use crate::rata_die::Moment;
#[test]
fn test_chinese_new_moon_directionality() {
for i in (-1000..1000).step_by(31) {
let moment = Moment::new(i as f64);
let before = new_moon_before::<Chinese>(moment);
let after = new_moon_on_or_after::<Chinese>(moment);
assert!(before < after, "Chinese new moon directionality failed for Moment: {moment:?}, with:\n\tBefore: {before:?}\n\tAfter: {after:?}");
}
}
#[test]
fn test_chinese_new_year_on_or_before() {
let fixed = crate::iso::fixed_from_iso(2023, 6, 22);
let prev_solstice = winter_solstice_on_or_before::<Chinese>(fixed);
let result_fixed = new_year_on_or_before_fixed_date::<Chinese>(fixed, prev_solstice).0;
let (y, m, d) = crate::iso::iso_from_fixed(result_fixed).unwrap();
assert_eq!(y, 2023);
assert_eq!(m, 1);
assert_eq!(d, 22);
}
fn seollal_on_or_before(fixed: RataDie) -> RataDie {
let prev_solstice = winter_solstice_on_or_before::<Dangi>(fixed);
new_year_on_or_before_fixed_date::<Dangi>(fixed, prev_solstice).0
}
#[test]
fn test_seollal() {
#[derive(Debug)]
struct TestCase {
iso_year: i32,
iso_month: u8,
iso_day: u8,
expected_year: i32,
expected_month: u8,
expected_day: u8,
}
let cases = [
TestCase {
iso_year: 2024,
iso_month: 6,
iso_day: 6,
expected_year: 2024,
expected_month: 2,
expected_day: 10,
},
TestCase {
iso_year: 2024,
iso_month: 2,
iso_day: 9,
expected_year: 2023,
expected_month: 1,
expected_day: 22,
},
TestCase {
iso_year: 2023,
iso_month: 1,
iso_day: 22,
expected_year: 2023,
expected_month: 1,
expected_day: 22,
},
TestCase {
iso_year: 2023,
iso_month: 1,
iso_day: 21,
expected_year: 2022,
expected_month: 2,
expected_day: 1,
},
TestCase {
iso_year: 2022,
iso_month: 6,
iso_day: 6,
expected_year: 2022,
expected_month: 2,
expected_day: 1,
},
TestCase {
iso_year: 2021,
iso_month: 6,
iso_day: 6,
expected_year: 2021,
expected_month: 2,
expected_day: 12,
},
TestCase {
iso_year: 2020,
iso_month: 6,
iso_day: 6,
expected_year: 2020,
expected_month: 1,
expected_day: 25,
},
TestCase {
iso_year: 2019,
iso_month: 6,
iso_day: 6,
expected_year: 2019,
expected_month: 2,
expected_day: 5,
},
TestCase {
iso_year: 2018,
iso_month: 6,
iso_day: 6,
expected_year: 2018,
expected_month: 2,
expected_day: 16,
},
TestCase {
iso_year: 2025,
iso_month: 6,
iso_day: 6,
expected_year: 2025,
expected_month: 1,
expected_day: 29,
},
TestCase {
iso_year: 2026,
iso_month: 8,
iso_day: 8,
expected_year: 2026,
expected_month: 2,
expected_day: 17,
},
TestCase {
iso_year: 2027,
iso_month: 4,
iso_day: 4,
expected_year: 2027,
expected_month: 2,
expected_day: 7,
},
TestCase {
iso_year: 2028,
iso_month: 9,
iso_day: 21,
expected_year: 2028,
expected_month: 1,
expected_day: 27,
},
];
for case in cases {
let fixed = crate::iso::fixed_from_iso(case.iso_year, case.iso_month, case.iso_day);
let seollal = seollal_on_or_before(fixed);
let (y, m, d) = crate::iso::iso_from_fixed(seollal).unwrap();
assert_eq!(
y, case.expected_year,
"Year check failed for case: {case:?}"
);
assert_eq!(
m, case.expected_month,
"Month check failed for case: {case:?}"
);
assert_eq!(d, case.expected_day, "Day check failed for case: {case:?}");
}
}
}