Skip to content
727 changes: 40 additions & 687 deletions components/calendar/src/cal/chinese.rs

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions components/calendar/src/cal/chinese/china_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,9 @@ pub const DATA: ChineseBasedCache = ChineseBasedCache {
PackedChineseBasedYearInfo::new(2098, [l, l, s, l, s, s, s, l, s, l, s, l, s], None, gregorian(2098, 2, 1)),
PackedChineseBasedYearInfo::new(2099, [l, l, s, l, l, s, s, l, s, s, l, s, l], Some(3), gregorian(2099, 1, 21)),
PackedChineseBasedYearInfo::new(2100, [l, l, s, l, s, l, s, l, s, s, l, s, s], None, gregorian(2100, 2, 9)),
// Extra two years of correct data because the simple calculation lines up at the beginning of 2103
PackedChineseBasedYearInfo::new(2101, [l, l, s, l, l, s, l, s, l, s, s, l, s], Some(8), gregorian(2101, 1, 29)),
PackedChineseBasedYearInfo::new(2102, [l, s, l, l, s, l, s, l, l, s, l, s, s], None, gregorian(2102, 2, 17)),
]},
};

Expand Down
3 changes: 3 additions & 0 deletions components/calendar/src/cal/chinese/korea_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,9 @@ pub const DATA: ChineseBasedCache = ChineseBasedCache {
PackedChineseBasedYearInfo::new(2098, [l, l, s, l, s, s, l, s, s, l, l, s, s], None, gregorian(2098, 2, 1)),
PackedChineseBasedYearInfo::new(2099, [l, l, l, s, l, s, s, l, s, s, l, s, l], Some(4), gregorian(2099, 1, 21)),
PackedChineseBasedYearInfo::new(2100, [l, l, s, l, s, l, s, l, s, s, l, s, s], None, gregorian(2100, 2, 9)),
// Extra two years of correct data because the simple calculation lines up at the beginning of 2103
PackedChineseBasedYearInfo::new(2101, [l, l, s, l, l, s, l, s, l, s, l, s, s], Some(8), gregorian(2101, 1, 29)),
PackedChineseBasedYearInfo::new(2102, [l, s, l, l, s, l, s, l, l, s, l, s, s], None, gregorian(2102, 2, 17)),
]},
};

Expand Down
162 changes: 162 additions & 0 deletions components/calendar/src/cal/chinese/simple.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// This file is part of ICU4X. For terms of use, please see the file
// called LICENSE at the top level of the ICU4X source tree
// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).

use super::LunarChineseYearData;
use calendrical_calculations::{gregorian::DAYS_IN_400_YEAR_CYCLE, rata_die::RataDie};

macro_rules! day_fraction_to_ms {
($n:tt $(/ $d:tt)+) => {{
Milliseconds((MILLISECONDS_IN_EPHEMERIS_DAY as i128 * $n as i128 $( / $d as i128)+) as i64)
}};
($n:tt $(/ $d:tt)+, exact) => {{
let d = day_fraction_to_ms!($n $(/ $d)+);
assert!((d.0 as i128 $(* $d as i128)+) % MILLISECONDS_IN_EPHEMERIS_DAY as i128 == 0, "inexact");
d
}};
}

pub(super) const UTC_PLUS_8: Milliseconds = day_fraction_to_ms!(8 / 24);
pub(super) const UTC_PLUS_9: Milliseconds = day_fraction_to_ms!(9 / 24);
// Reference time was UTC+(1397/180)
pub(super) const BEIJING_UTC_OFFSET: Milliseconds = day_fraction_to_ms!(1397 / 180 / 24);

/// The mean year length according to the Gregorian solar cycle.
const MEAN_GREGORIAN_YEAR_LENGTH: Milliseconds =
day_fraction_to_ms!(DAYS_IN_400_YEAR_CYCLE / 400, exact);

/// The mean solar term length according to the Gregorian solar cycle
const MEAN_GREGORIAN_SOLAR_TERM_LENGTH: Milliseconds =
day_fraction_to_ms!(DAYS_IN_400_YEAR_CYCLE / 400 / 12, exact);

/// The mean synodic length on Jan 1 2000 according to the [Astronomical Almanac (1992)].
///
/// [Astronomical Almanac (1992)]: https://archive.org/details/131123ExplanatorySupplementAstronomicalAlmanac/page/n302/mode/1up
const MEAN_SYNODIC_MONTH_LENGTH: Milliseconds = day_fraction_to_ms!(295305888531 / 10000000000i64);

/// Number of milliseconds in a day.
const MILLISECONDS_IN_EPHEMERIS_DAY: i64 = 24 * 60 * 60 * 1000;

// 1999-12-22T07:44, https://aa.usno.navy.mil/calculated/seasons?year=2024&tz=0.00&tz_sign=-1&tz_label=false&dst=false
const UTC_SOLSTICE: LocalMoment = LocalMoment {
rata_die: calendrical_calculations::gregorian::fixed_from_gregorian(1999, 12, 22),
local_milliseconds: ((7 * 60) + 44) * 60 * 1000,
};

// 2000-01-06T18:14 https://aa.usno.navy.mil/calculated/moon/phases?date=2000-01-01&nump=1&format=t
const UTC_NEW_MOON: LocalMoment = LocalMoment {
rata_die: calendrical_calculations::gregorian::fixed_from_gregorian(2000, 1, 6),
local_milliseconds: ((18 * 60) + 14) * 60 * 1000,
};

#[derive(Debug, Copy, Clone, Default)]
pub(super) struct Milliseconds(i64);

impl core::ops::Mul<i64> for Milliseconds {
type Output = Self;

fn mul(self, rhs: i64) -> Self::Output {
Self(self.0 * rhs)
}
}

#[derive(Debug, Copy, Clone)]
struct LocalMoment {
rata_die: RataDie,
local_milliseconds: u32,
}

impl core::ops::Add<Milliseconds> for LocalMoment {
type Output = Self;

fn add(self, Milliseconds(duration): Milliseconds) -> Self::Output {
let temp = self.local_milliseconds as i64 + duration;
Self {
rata_die: self.rata_die + temp.div_euclid(MILLISECONDS_IN_EPHEMERIS_DAY),
local_milliseconds: temp.rem_euclid(MILLISECONDS_IN_EPHEMERIS_DAY) as u32,
}
}
}

impl super::LunarChineseYearData {
/// A fast approximation for the Chinese calendar, inspired by the _píngqì_ (平氣) rule
/// used in the Ming dynasty.
///
/// Stays anchored in the Gregorian calendar, even as the Gregorian calendar drifts
/// from the seasons in the distant future and distant past.
pub(super) fn simple(utc_offset: Milliseconds, related_iso: i32) -> LunarChineseYearData {
fn periodic_duration_on_or_before(
rata_die: RataDie,
base_moment: LocalMoment,
duration: Milliseconds,
) -> LocalMoment {
let num_periods = ((rata_die - base_moment.rata_die + 1)
* MILLISECONDS_IN_EPHEMERIS_DAY
- base_moment.local_milliseconds as i64
- 1)
.div_euclid(duration.0);
base_moment + duration * num_periods
}

let mut major_solar_term = periodic_duration_on_or_before(
calendrical_calculations::iso::day_before_year(related_iso),
UTC_SOLSTICE + utc_offset,
MEAN_GREGORIAN_YEAR_LENGTH,
);

let mut new_moon = periodic_duration_on_or_before(
major_solar_term.rata_die,
UTC_NEW_MOON + utc_offset,
MEAN_SYNODIC_MONTH_LENGTH,
);

let mut next_new_moon = new_moon + MEAN_SYNODIC_MONTH_LENGTH;

// The solstice is in the month of the 11th solar term of the previous year
let mut solar_term = -2;
let mut had_leap_in_sui = false;

// Skip the months before the year (M11, maybe M11L, M12, maybe M12L)
while solar_term < 0
|| (next_new_moon.rata_die <= major_solar_term.rata_die && !had_leap_in_sui)
{
if next_new_moon.rata_die <= major_solar_term.rata_die && !had_leap_in_sui {
had_leap_in_sui = true;
} else {
solar_term += 1;
major_solar_term = major_solar_term + MEAN_GREGORIAN_SOLAR_TERM_LENGTH;
}

(new_moon, next_new_moon) = (next_new_moon, next_new_moon + MEAN_SYNODIC_MONTH_LENGTH);
}

debug_assert_eq!(solar_term, 0);

let start_day = new_moon.rata_die;
let mut month_lengths = [false; 13];
let mut leap_month = None;

// Iterate over the 12 solar terms, producing potentially 13 months
while solar_term < 12
|| (next_new_moon.rata_die <= major_solar_term.rata_die && !had_leap_in_sui)
{
*month_lengths
.get_mut(solar_term as usize + leap_month.is_some() as usize)
.unwrap_or(&mut false) = next_new_moon.rata_die - new_moon.rata_die == 30;

if next_new_moon.rata_die <= major_solar_term.rata_die && !had_leap_in_sui {
had_leap_in_sui = true;
leap_month = Some(solar_term as u8 + 1);
} else {
solar_term += 1;
major_solar_term = major_solar_term + MEAN_GREGORIAN_SOLAR_TERM_LENGTH;
}

(new_moon, next_new_moon) = (next_new_moon, next_new_moon + MEAN_SYNODIC_MONTH_LENGTH);
}

debug_assert_eq!(solar_term, 12);

LunarChineseYearData::new(related_iso, start_day, month_lengths, leap_month)
}
}
6 changes: 4 additions & 2 deletions components/calendar/src/provider/chinese_based.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ impl PackedChineseBasedYearInfo {
///
/// According to Reingold & Dershowitz, ch 19.6, Chinese New Year occurs on Jan 21 - Feb 21 inclusive.
///
/// Our simple approximation sometimes returns Feb 22.
///
/// We allow it to occur as early as January 19 which is the earliest the second new moon
/// could occur after the Winter Solstice if the solstice is pinned to December 20.
const fn earliest_ny(related_iso: i32) -> RataDie {
Expand Down Expand Up @@ -108,10 +110,10 @@ impl PackedChineseBasedYearInfo {
ny_offset >= 0 || out_of_valid_astronomical_range,
"Year offset too small to store"
);
// The maximum new-year's offset we have found is 33
// The maximum new-year's offset we have found is 34
#[cfg(debug_assertions)]
debug_assert!(
ny_offset < 34 || out_of_valid_astronomical_range,
ny_offset < 35 || out_of_valid_astronomical_range,
"Year offset too big to store"
);

Expand Down
12 changes: 12 additions & 0 deletions components/calendar/src/tests/continuity_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,12 @@ fn test_chinese_continuity() {
check_every_250_days(date.unwrap());
let date = Date::try_new_chinese_with_calendar(-10000, 1, 1, cal);
check_every_250_days(date.unwrap());

let date = Date::try_new_chinese_with_calendar(1899, 1, 1, cal);
check_continuity(date.unwrap());

let date = Date::try_new_chinese_with_calendar(2099, 1, 1, cal);
check_continuity(date.unwrap());
}

#[test]
Expand All @@ -86,6 +92,12 @@ fn test_korean_continuity() {
check_continuity(date.unwrap());
let date = Date::try_new_chinese_with_calendar(-300, 1, 1, cal);
check_every_250_days(date.unwrap());

let date = Date::try_new_chinese_with_calendar(1900, 1, 1, cal);
check_continuity(date.unwrap());

let date = Date::try_new_chinese_with_calendar(2100, 1, 1, cal);
check_continuity(date.unwrap());
}

#[test]
Expand Down
40 changes: 17 additions & 23 deletions provider/source/src/calendar/eras.rs
Original file line number Diff line number Diff line change
Expand Up @@ -413,40 +413,34 @@ fn test_calendar_eras() {
);
}

if era.start.is_some() && calendar != "japanese" {
assert_eq!(in_era.day_of_year().0, 1, "{calendar:?}");
}

match in_era.year() {
icu::calendar::types::YearInfo::Era(era_year) => {
// Unless this is the first era and it's not an inverse era, check that the
// not_in_era date is in a different era
if idx != 0 || era.end.is_some() {
assert_ne!(not_in_era.year().era().unwrap().era, era_year.era);
}
let icu::calendar::types::YearInfo::Era(era_year) = in_era.year() else {
continue;
};

// Check that the correct era code is returned
if let Some(code) = era.code.as_deref() {
assert_eq!(era_year.era, code);
}
// Unless this is the first era and it's not an inverse era, check that the
// not_in_era date is in a different era
if idx != 0 || era.end.is_some() {
assert_ne!(not_in_era.year().era().unwrap().era, era_year.era);
}

// Check that the start/end date uses year 1, and minimal/maximal month/day
assert_eq!(era_year.year, 1, "Didn't get correct year for {in_era:?}");
}
// Cyclic calendars use related_iso for their extended years, which won't
// work with the CLDR "default" eras. Skip testing them.
icu::calendar::types::YearInfo::Cyclic(_) => (),
_ => unreachable!(),
// Check that the correct era code is returned
if let Some(code) = era.code.as_deref() {
assert_eq!(era_year.era, code);
}

// Check that the start/end date uses year 1, and minimal/maximal month/day
assert_eq!(era_year.year, 1, "Didn't get correct year for {in_era:?}");

if calendar == "japanese" {
// Japanese is the only calendar that doesn't have its own months
// Japanese is the only calendar that doesn't start eras on a new year
} else if era.start.is_some() {
assert_eq!(in_era.month().ordinal, 1);
assert_eq!(in_era.day_of_month().0, 1);
assert_eq!(in_era.day_of_year().0, 1);
} else {
assert_eq!(in_era.month().ordinal, in_era.months_in_year());
assert_eq!(in_era.day_of_month().0, in_era.days_in_month());
assert_eq!(in_era.day_of_year().0, in_era.days_in_year());
}
}
}
Expand Down
36 changes: 22 additions & 14 deletions utils/calendrical_calculations/src/gregorian.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,18 @@ use crate::rata_die::RataDie;
// The Gregorian epoch is equivalent to first day in fixed day measurement
const EPOCH: RataDie = RataDie::new(1);

const DAYS_IN_YEAR: i64 = 365;

// One leap year every 4 years
const DAYS_IN_4_YEAR_CYCLE: i64 = DAYS_IN_YEAR * 4 + 1;

// No leap year every 100 years
const DAYS_IN_100_YEAR_CYCLE: i64 = 25 * DAYS_IN_4_YEAR_CYCLE - 1;

// One extra leap year every 400 years
/// The number of days in the 400 year cycle.
pub const DAYS_IN_400_YEAR_CYCLE: i64 = 4 * DAYS_IN_100_YEAR_CYCLE + 1;

/// Whether or not `year` is a leap year
///
/// Inspired by Neri-Schneider <https://www.youtube.com/watch?v=J9KijLyP-yg&t=1239s>
Expand Down Expand Up @@ -56,31 +68,27 @@ pub const fn year_from_fixed(date: RataDie) -> Result<i32, I32CastError> {
// Shouldn't overflow because it's not possbile to construct extreme values of RataDie
let date = date.since(EPOCH);

// 400 year cycles have 146097 days
let (n_400, date) = (date.div_euclid(146097), date.rem_euclid(146097));
let (n_400, date) = (
date.div_euclid(DAYS_IN_400_YEAR_CYCLE),
date.rem_euclid(DAYS_IN_400_YEAR_CYCLE),
);

// 100 year cycles have 36524 days
let (n_100, date) = (date / 36524, date % 36524);
let (n_100, date) = (date / DAYS_IN_100_YEAR_CYCLE, date % DAYS_IN_100_YEAR_CYCLE);

// 4 year cycles have 1461 days
let (n_4, date) = (date / 1461, date % 1461);
let (n_4, date) = (date / DAYS_IN_4_YEAR_CYCLE, date % DAYS_IN_4_YEAR_CYCLE);

let n_1 = date / 365;
let n_1 = date / DAYS_IN_YEAR;

let year = 400 * n_400 + 100 * n_100 + 4 * n_4 + n_1;
let year = 400 * n_400 + 100 * n_100 + 4 * n_4 + n_1 + (n_100 != 4 && n_1 != 4) as i64;

if n_100 == 4 || n_1 == 4 {
i64_to_i32(year)
} else {
i64_to_i32(year + 1)
}
i64_to_i32(year)
}

/// Calculates the day before Jan 1 of `year`.
pub const fn day_before_year(year: i32) -> RataDie {
let prev_year = (year as i64) - 1;
// Calculate days per year
let mut fixed: i64 = 365 * prev_year;
let mut fixed: i64 = DAYS_IN_YEAR * prev_year;
// Adjust for leap year logic. We can avoid the branch of div_euclid by making prev_year positive:
// YEAR_SHIFT is larger (in magnitude) than any prev_year, and, being divisible by 400,
// distributes correctly over the calculation on the next line.
Expand Down