diff --git a/components/calendar/src/cal/chinese.rs b/components/calendar/src/cal/chinese.rs index 81a57e97b69..93cb0c92cdd 100644 --- a/components/calendar/src/cal/chinese.rs +++ b/components/calendar/src/cal/chinese.rs @@ -25,6 +25,8 @@ mod china_data; mod korea_data; #[path = "chinese/qing_data.rs"] mod qing_data; +#[path = "chinese/simple.rs"] +mod simple; /// The [Chinese Calendar](https://en.wikipedia.org/wiki/Chinese_calendar) /// @@ -131,14 +133,15 @@ pub trait Rules: Clone + core::fmt::Debug + crate::cal::scaffold::UnstableSealed /// /// For years since 1912, this uses the [GB/T 33661-2017] rules. /// As accurate computation is computationally expensive, years until -/// 2100 are precomputed. If performance beyond 2100 is an issue, clients +/// 2100 are precomputed, and after that this type regresses to a simplified +/// calculation. If accuracy beyond 2100 is required, clients /// can implement their own [`Rules`] type containing more precomputed data. /// We note that the calendar is inherently uncertain for some future dates. /// /// Before 1912 [different rules](https://ytliu.epizy.com/Shixian/Shixian_summary.html) /// were used. This type produces correct data for the years 1900-1912, and -/// falls back to [GB/T 33661-2017] proleptically before 1900. If accuracy -/// is required before 1900, clients can implement their own [`Rules`] type +/// falls back to a simplified calculation before 1900. If accuracy is +/// required before 1900, clients can implement their own [`Rules`] type /// using data such as from the excellent compilation by [Yuk Tung Liu]. /// /// [Purple Mountain Observatory for the years 1900-2025]: http://www.pmo.cas.cn/xwdt2019/kpdt2019/202203/P020250414456381274062.pdf @@ -178,11 +181,11 @@ impl Rules for China { if let Some(data) = china_data::DATA.get(related_iso) { data } else if related_iso > china_data::DATA.first_related_iso_year { - Self::gb_t_33661_2017(related_iso) + LunarChineseYearData::simple(simple::UTC_PLUS_8, related_iso) } else { - qing_data::DATA - .get(related_iso) - .unwrap_or_else(|| Self::gb_t_33661_2017(related_iso)) + qing_data::DATA.get(related_iso).unwrap_or_else(|| { + LunarChineseYearData::simple(simple::BEIJING_UTC_OFFSET, related_iso) + }) } } @@ -194,12 +197,12 @@ impl Rules for China { Ok(match (number, is_leap, day > 29) { (1, false, false) => 1972, (1, false, true) => 1970, - (1, true, false) => 1651, - (1, true, true) => 1461, + (1, true, false) => 1898, + (1, true, true) => 1898, (2, false, false) => 1972, (2, false, true) => 1972, (2, true, false) => 1947, - (2, true, true) => 1765, + (2, true, true) => 1830, (3, false, false) => 1972, (3, false, true) => 1966, (3, true, false) => 1966, @@ -223,26 +226,26 @@ impl Rules for China { (8, false, false) => 1972, (8, false, true) => 1971, (8, true, false) => 1957, - (8, true, true) => 1718, + (8, true, true) => 1691, (9, false, false) => 1972, (9, false, true) => 1972, (9, true, false) => 2014, - (9, true, true) => -5738, + (9, true, true) => 1843, (10, false, false) => 1972, (10, false, true) => 1972, (10, true, false) => 1984, - (10, true, true) => -4098, + (10, true, true) => 1737, // Dec 31, 1972 is 1972-M11-26, dates after that // are in the next year (11, false, false) if day > 26 => 1971, (11, false, false) => 1972, (11, false, true) => 1969, (11, true, false) => 2033, - (11, true, true) => -2173, + (11, true, true) => 1889, (12, false, false) => 1971, (12, false, true) => 1971, - (12, true, false) => 1403, - (12, true, true) => -180, + (12, true, false) => 1878, + (12, true, true) => 1783, _ => return Err(DateError::UnknownMonthCode(month_code)), }) } @@ -264,13 +267,14 @@ impl Rules for China { /// For years since 1912, this uses [adapted GB/T 33661-2017] rules, /// using Korea time instead of Beijing Time. /// As accurate computation is computationally expensive, years until -/// 2100 are precomputed. If performance beyond 2100 is an issue, clients +/// 2100 are precomputed, and after that this type regresses to a simplified +/// calculation. If accuracy beyond 2100 is required, clients /// can implement their own [`Rules`] type containing more precomputed data. /// We note that the calendar is inherently uncertain for some future dates. /// /// Before 1912 [different rules](https://ytliu.epizy.com/Shixian/Shixian_summary.html) /// were used (those of Qing-dynasty China). This type produces correct data -/// for the years 1900-1912, and falls back to [GB/T 33661-2017] proleptically +/// for the years 1900-1912, and falls back to a simplified calculation /// before 1900. If accuracy is required before 1900, clients can implement /// their own [`Rules`] type using data such as from the excellent compilation /// by [Yuk Tung Liu]. @@ -354,13 +358,13 @@ impl Rules for Korea { if let Some(data) = korea_data::DATA.get(related_iso) { data } else if related_iso > korea_data::DATA.first_related_iso_year { - Self::adapted_gb_t_33661_2017(related_iso) + LunarChineseYearData::simple(simple::UTC_PLUS_9, related_iso) } else { // Korea used Qing-dynasty rules before 1912 // https://github.com/unicode-org/icu4x/issues/6455#issuecomment-3282175550 - qing_data::DATA - .get(related_iso) - .unwrap_or_else(|| China::gb_t_33661_2017(related_iso)) + qing_data::DATA.get(related_iso).unwrap_or_else(|| { + LunarChineseYearData::simple(simple::BEIJING_UTC_OFFSET, related_iso) + }) } } @@ -372,12 +376,12 @@ impl Rules for Korea { Ok(match (number, is_leap, day > 29) { (1, false, false) => 1972, (1, false, true) => 1970, - (1, true, false) => 1651, - (1, true, true) => 1461, + (1, true, false) => 1898, + (1, true, true) => 1898, (2, false, false) => 1972, (2, false, true) => 1972, (2, true, false) => 1947, - (2, true, true) => 1765, + (2, true, true) => 1830, (3, false, false) => 1972, (3, false, true) => 1968, (3, true, false) => 1966, @@ -401,26 +405,26 @@ impl Rules for Korea { (8, false, false) => 1972, (8, false, true) => 1971, (8, true, false) => 1957, - (8, true, true) => 1718, + (8, true, true) => 1691, (9, false, false) => 1972, (9, false, true) => 1972, (9, true, false) => 2014, - (9, true, true) => -5738, + (9, true, true) => 1843, (10, false, false) => 1972, (10, false, true) => 1972, (10, true, false) => 1984, - (10, true, true) => -4098, + (10, true, true) => 1737, // Dec 31, 1972 is 1972-M11-26, dates after that // are in the next year (11, false, false) if day > 26 => 1971, (11, false, false) => 1972, (11, false, true) => 1969, (11, true, false) => 2033, - (11, true, true) => -2173, + (11, true, true) => 1889, (12, false, false) => 1971, (12, false, true) => 1971, - (12, true, false) => 1403, - (12, true, true) => -180, + (12, true, false) => 1878, + (12, true, true) => 1783, _ => return Err(DateError::UnknownMonthCode(month_code)), }) } @@ -1164,14 +1168,14 @@ mod test { TestCase { rd: 0, expected_year: 0, - expected_month: 12, - expected_day: 20, + expected_month: 11, + expected_day: 19, }, TestCase { rd: -1, expected_year: 0, - expected_month: 12, - expected_day: 19, + expected_month: 11, + expected_day: 18, }, TestCase { rd: -365, @@ -1308,7 +1312,7 @@ mod test { iso_day: 15, expected_year: -2637, expected_month: 12, - expected_day: 30, + expected_day: 29, }, ]; @@ -1577,124 +1581,6 @@ mod test { } } - #[test] - fn test_consistent_with_icu() { - #[derive(Debug)] - struct TestCase { - iso_year: i32, - iso_month: u8, - iso_day: u8, - expected_rel_iso: i32, - expected_cyclic: u8, - expected_month: u8, - expected_day: u8, - } - - let cases = [ - TestCase { - iso_year: -2332, - iso_month: 3, - iso_day: 1, - expected_rel_iso: -2332, - expected_cyclic: 5, - expected_month: 1, - expected_day: 16, - }, - TestCase { - iso_year: -2332, - iso_month: 2, - iso_day: 15, - expected_rel_iso: -2332, - expected_cyclic: 5, - expected_month: 1, - expected_day: 1, - }, - TestCase { - // This test case fails to match ICU - iso_year: -2332, - iso_month: 2, - iso_day: 14, - expected_rel_iso: -2333, - expected_cyclic: 4, - expected_month: 13, - expected_day: 30, - }, - TestCase { - // This test case fails to match ICU - iso_year: -2332, - iso_month: 1, - iso_day: 17, - expected_rel_iso: -2333, - expected_cyclic: 4, - expected_month: 13, - expected_day: 2, - }, - TestCase { - // This test case fails to match ICU - iso_year: -2332, - iso_month: 1, - iso_day: 16, - expected_rel_iso: -2333, - expected_cyclic: 4, - expected_month: 13, - expected_day: 1, - }, - TestCase { - iso_year: -2332, - iso_month: 1, - iso_day: 15, - expected_rel_iso: -2333, - expected_cyclic: 4, - expected_month: 12, - expected_day: 29, - }, - TestCase { - iso_year: -2332, - iso_month: 1, - iso_day: 1, - expected_rel_iso: -2333, - expected_cyclic: 4, - expected_month: 12, - expected_day: 15, - }, - TestCase { - iso_year: -2333, - iso_month: 1, - iso_day: 16, - expected_rel_iso: -2334, - expected_cyclic: 3, - expected_month: 12, - expected_day: 19, - }, - ]; - - for case in cases { - let iso = Date::try_new_iso(case.iso_year, case.iso_month, case.iso_day).unwrap(); - let chinese = iso.to_calendar(LunarChinese::new_china()); - let chinese_rel_iso = chinese.cyclic_year().related_iso; - let chinese_cyclic = chinese.cyclic_year().year; - let chinese_month = chinese.month().ordinal; - let chinese_day = chinese.day_of_month().0; - - assert_eq!( - chinese_rel_iso, case.expected_rel_iso, - "Related ISO failed for test case: {case:?}" - ); - assert_eq!( - chinese_cyclic, case.expected_cyclic, - "Cyclic year failed for test case: {case:?}" - ); - assert_eq!( - chinese_month, case.expected_month, - "Month failed for test case: {case:?}" - ); - assert_eq!( - chinese_day, case.expected_day, - "Day failed for test case: {case:?}" - ); - } - } - fn check_cyclic_and_rel_iso(year: i32) { let iso = Date::try_new_iso(year, 6, 6).unwrap(); let chinese = iso.to_calendar(LunarChinese::new_china()); @@ -1785,539 +1671,6 @@ mod test { ); } - #[test] - fn test_korean_consistent_with_icu() { - // Test cases for this test are derived from existing ICU Intl.DateTimeFormat. If there is a bug in ICU, - // these test cases may be affected, and this calendar's output may not be entirely valid. - - // There are a number of test cases which do not match ICU for dates very far in the past or future, - // see #3709. - - #[derive(Debug)] - struct TestCase { - iso_year: i32, - iso_month: u8, - iso_day: u8, - expected_rel_iso: i32, - expected_cyclic: u8, - expected_month: u8, - expected_day: u8, - } - - let cases = [ - TestCase { - // #3709: This test case fails to match ICU - iso_year: 4321, - iso_month: 1, - iso_day: 23, - expected_rel_iso: 4320, - expected_cyclic: 57, - expected_month: 13, - expected_day: 12, - }, - TestCase { - iso_year: 3649, - iso_month: 9, - iso_day: 20, - expected_rel_iso: 3649, - expected_cyclic: 46, - expected_month: 9, - expected_day: 1, - }, - TestCase { - iso_year: 3333, - iso_month: 3, - iso_day: 3, - expected_rel_iso: 3333, - expected_cyclic: 30, - expected_month: 1, - expected_day: 25, - }, - TestCase { - iso_year: 3000, - iso_month: 3, - iso_day: 30, - expected_rel_iso: 3000, - expected_cyclic: 57, - expected_month: 3, - expected_day: 3, - }, - TestCase { - iso_year: 2772, - iso_month: 7, - iso_day: 27, - expected_rel_iso: 2772, - expected_cyclic: 9, - expected_month: 7, - expected_day: 5, - }, - TestCase { - iso_year: 2525, - iso_month: 2, - iso_day: 25, - expected_rel_iso: 2525, - expected_cyclic: 2, - expected_month: 2, - expected_day: 3, - }, - TestCase { - iso_year: 2345, - iso_month: 3, - iso_day: 21, - expected_rel_iso: 2345, - expected_cyclic: 2, - expected_month: 2, - expected_day: 17, - }, - TestCase { - iso_year: 2222, - iso_month: 2, - iso_day: 22, - expected_rel_iso: 2222, - expected_cyclic: 59, - expected_month: 1, - expected_day: 11, - }, - TestCase { - iso_year: 2167, - iso_month: 6, - iso_day: 22, - expected_rel_iso: 2167, - expected_cyclic: 4, - expected_month: 5, - expected_day: 6, - }, - TestCase { - iso_year: 2121, - iso_month: 2, - iso_day: 12, - expected_rel_iso: 2120, - expected_cyclic: 17, - expected_month: 13, - expected_day: 25, - }, - TestCase { - iso_year: 2080, - iso_month: 12, - iso_day: 31, - expected_rel_iso: 2080, - expected_cyclic: 37, - expected_month: 12, - expected_day: 21, - }, - TestCase { - iso_year: 2030, - iso_month: 3, - iso_day: 20, - expected_rel_iso: 2030, - expected_cyclic: 47, - expected_month: 2, - expected_day: 17, - }, - TestCase { - iso_year: 2027, - iso_month: 2, - iso_day: 7, - expected_rel_iso: 2027, - expected_cyclic: 44, - expected_month: 1, - expected_day: 1, - }, - TestCase { - iso_year: 2023, - iso_month: 7, - iso_day: 1, - expected_rel_iso: 2023, - expected_cyclic: 40, - expected_month: 6, - expected_day: 14, - }, - TestCase { - iso_year: 2022, - iso_month: 3, - iso_day: 1, - expected_rel_iso: 2022, - expected_cyclic: 39, - expected_month: 1, - expected_day: 29, - }, - TestCase { - iso_year: 2021, - iso_month: 2, - iso_day: 1, - expected_rel_iso: 2020, - expected_cyclic: 37, - expected_month: 13, - expected_day: 20, - }, - TestCase { - iso_year: 2016, - iso_month: 3, - iso_day: 30, - expected_rel_iso: 2016, - expected_cyclic: 33, - expected_month: 2, - expected_day: 22, - }, - TestCase { - iso_year: 2016, - iso_month: 7, - iso_day: 30, - expected_rel_iso: 2016, - expected_cyclic: 33, - expected_month: 6, - expected_day: 27, - }, - TestCase { - iso_year: 2015, - iso_month: 9, - iso_day: 22, - expected_rel_iso: 2015, - expected_cyclic: 32, - expected_month: 8, - expected_day: 10, - }, - TestCase { - iso_year: 2013, - iso_month: 10, - iso_day: 1, - expected_rel_iso: 2013, - expected_cyclic: 30, - expected_month: 8, - expected_day: 27, - }, - TestCase { - iso_year: 2010, - iso_month: 2, - iso_day: 1, - expected_rel_iso: 2009, - expected_cyclic: 26, - expected_month: 13, - expected_day: 18, - }, - TestCase { - iso_year: 2000, - iso_month: 8, - iso_day: 30, - expected_rel_iso: 2000, - expected_cyclic: 17, - expected_month: 8, - expected_day: 2, - }, - TestCase { - iso_year: 1990, - iso_month: 11, - iso_day: 11, - expected_rel_iso: 1990, - expected_cyclic: 7, - expected_month: 10, - expected_day: 24, - }, - TestCase { - iso_year: 1970, - iso_month: 6, - iso_day: 10, - expected_rel_iso: 1970, - expected_cyclic: 47, - expected_month: 5, - expected_day: 7, - }, - TestCase { - iso_year: 1970, - iso_month: 1, - iso_day: 1, - expected_rel_iso: 1969, - expected_cyclic: 46, - expected_month: 11, - expected_day: 24, - }, - TestCase { - iso_year: 1941, - iso_month: 12, - iso_day: 7, - expected_rel_iso: 1941, - expected_cyclic: 18, - expected_month: 11, - expected_day: 19, - }, - TestCase { - iso_year: 1812, - iso_month: 5, - iso_day: 4, - expected_rel_iso: 1812, - expected_cyclic: 9, - expected_month: 3, - expected_day: 24, - }, - TestCase { - iso_year: 1655, - iso_month: 6, - iso_day: 15, - expected_rel_iso: 1655, - expected_cyclic: 32, - expected_month: 5, - expected_day: 12, - }, - TestCase { - iso_year: 1333, - iso_month: 3, - iso_day: 10, - expected_rel_iso: 1333, - expected_cyclic: 10, - expected_month: 2, - expected_day: 16, - }, - TestCase { - iso_year: 1000, - iso_month: 10, - iso_day: 10, - expected_rel_iso: 1000, - expected_cyclic: 37, - expected_month: 9, - expected_day: 5, - }, - TestCase { - iso_year: 842, - iso_month: 2, - iso_day: 15, - expected_rel_iso: 841, - expected_cyclic: 58, - expected_month: 13, - expected_day: 28, - }, - TestCase { - iso_year: 101, - iso_month: 1, - iso_day: 10, - expected_rel_iso: 100, - expected_cyclic: 37, - expected_month: 12, - expected_day: 24, - }, - TestCase { - iso_year: -1, - iso_month: 3, - iso_day: 28, - expected_rel_iso: -1, - expected_cyclic: 56, - expected_month: 2, - expected_day: 25, - }, - TestCase { - iso_year: -3, - iso_month: 2, - iso_day: 28, - expected_rel_iso: -3, - expected_cyclic: 54, - expected_month: 2, - expected_day: 5, - }, - TestCase { - iso_year: -365, - iso_month: 7, - iso_day: 24, - expected_rel_iso: -365, - expected_cyclic: 52, - expected_month: 6, - expected_day: 24, - }, - TestCase { - iso_year: -999, - iso_month: 9, - iso_day: 9, - expected_rel_iso: -999, - expected_cyclic: 18, - expected_month: 7, - expected_day: 27, - }, - TestCase { - iso_year: -1500, - iso_month: 1, - iso_day: 5, - expected_rel_iso: -1501, - expected_cyclic: 56, - expected_month: 12, - expected_day: 2, - }, - TestCase { - iso_year: -2332, - iso_month: 3, - iso_day: 1, - expected_rel_iso: -2332, - expected_cyclic: 5, - expected_month: 1, - expected_day: 16, - }, - TestCase { - iso_year: -2332, - iso_month: 2, - iso_day: 15, - expected_rel_iso: -2332, - expected_cyclic: 5, - expected_month: 1, - expected_day: 1, - }, - TestCase { - // #3709: This test case fails to match ICU - iso_year: -2332, - iso_month: 2, - iso_day: 14, - expected_rel_iso: -2333, - expected_cyclic: 4, - expected_month: 13, - expected_day: 30, - }, - TestCase { - // #3709: This test case fails to match ICU - iso_year: -2332, - iso_month: 1, - iso_day: 17, - expected_rel_iso: -2333, - expected_cyclic: 4, - expected_month: 13, - expected_day: 2, - }, - TestCase { - // #3709: This test case fails to match ICU - iso_year: -2332, - iso_month: 1, - iso_day: 16, - expected_rel_iso: -2333, - expected_cyclic: 4, - expected_month: 13, - expected_day: 1, - }, - TestCase { - iso_year: -2332, - iso_month: 1, - iso_day: 15, - expected_rel_iso: -2333, - expected_cyclic: 4, - expected_month: 12, - expected_day: 29, - }, - TestCase { - iso_year: -2332, - iso_month: 1, - iso_day: 1, - expected_rel_iso: -2333, - expected_cyclic: 4, - expected_month: 12, - expected_day: 15, - }, - TestCase { - iso_year: -2333, - iso_month: 1, - iso_day: 16, - expected_rel_iso: -2334, - expected_cyclic: 3, - expected_month: 12, - expected_day: 19, - }, - TestCase { - iso_year: -2333, - iso_month: 1, - iso_day: 27, - expected_rel_iso: -2333, - expected_cyclic: 4, - expected_month: 1, - expected_day: 1, - }, - TestCase { - iso_year: -2333, - iso_month: 1, - iso_day: 26, - expected_rel_iso: -2334, - expected_cyclic: 3, - expected_month: 12, - expected_day: 29, - }, - TestCase { - iso_year: -2600, - iso_month: 9, - iso_day: 16, - expected_rel_iso: -2600, - expected_cyclic: 37, - expected_month: 8, - expected_day: 16, - }, - TestCase { - iso_year: -2855, - iso_month: 2, - iso_day: 3, - expected_rel_iso: -2856, - expected_cyclic: 21, - expected_month: 12, - expected_day: 30, - }, - TestCase { - // #3709: This test case fails to match ICU - iso_year: -3000, - iso_month: 5, - iso_day: 15, - expected_rel_iso: -3000, - expected_cyclic: 57, - expected_month: 4, - expected_day: 1, - }, - TestCase { - // #3709: This test case fails to match ICU - iso_year: -3649, - iso_month: 9, - iso_day: 20, - expected_rel_iso: -3649, - expected_cyclic: 8, - expected_month: 8, - expected_day: 10, - }, - TestCase { - // #3709: This test case fails to match ICU - iso_year: -3649, - iso_month: 3, - iso_day: 30, - expected_rel_iso: -3649, - expected_cyclic: 8, - expected_month: 2, - expected_day: 14, - }, - TestCase { - // #3709: This test case fails to match ICU - iso_year: -3650, - iso_month: 3, - iso_day: 30, - expected_rel_iso: -3650, - expected_cyclic: 7, - expected_month: 3, - expected_day: 3, - }, - ]; - - for case in cases { - let iso = Date::try_new_iso(case.iso_year, case.iso_month, case.iso_day).unwrap(); - let korean = iso.to_calendar(LunarChinese::new_korea()); - let korean_cyclic = korean.cyclic_year(); - let korean_month = korean.month().ordinal; - let korean_day = korean.day_of_month().0; - - assert_eq!( - korean_cyclic.related_iso, case.expected_rel_iso, - "Related ISO failed for test case: {case:?}" - ); - assert_eq!( - korean_cyclic.year, case.expected_cyclic, - "Cyclic year failed for test case: {case:?}" - ); - assert_eq!( - korean_month, case.expected_month, - "Month failed for test case: {case:?}" - ); - assert_eq!( - korean_day, case.expected_day, - "Day failed for test case: {case:?}" - ); - } - } - #[test] #[ignore] // slow, network fn test_against_hong_kong_observatory_data() { diff --git a/components/calendar/src/cal/chinese/china_data.rs b/components/calendar/src/cal/chinese/china_data.rs index b1aacdd1579..0a9115101db 100644 --- a/components/calendar/src/cal/chinese/china_data.rs +++ b/components/calendar/src/cal/chinese/china_data.rs @@ -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)), ]}, }; diff --git a/components/calendar/src/cal/chinese/korea_data.rs b/components/calendar/src/cal/chinese/korea_data.rs index 4d5fd0c8c76..90cdb4854f5 100644 --- a/components/calendar/src/cal/chinese/korea_data.rs +++ b/components/calendar/src/cal/chinese/korea_data.rs @@ -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)), ]}, }; diff --git a/components/calendar/src/cal/chinese/simple.rs b/components/calendar/src/cal/chinese/simple.rs new file mode 100644 index 00000000000..d500daa935d --- /dev/null +++ b/components/calendar/src/cal/chinese/simple.rs @@ -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 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 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) + } +} diff --git a/components/calendar/src/provider/chinese_based.rs b/components/calendar/src/provider/chinese_based.rs index 10f5b99142a..cc6f4544a8b 100644 --- a/components/calendar/src/provider/chinese_based.rs +++ b/components/calendar/src/provider/chinese_based.rs @@ -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 { @@ -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" ); diff --git a/components/calendar/src/tests/continuity_test.rs b/components/calendar/src/tests/continuity_test.rs index ce97a549faf..0971bdded89 100644 --- a/components/calendar/src/tests/continuity_test.rs +++ b/components/calendar/src/tests/continuity_test.rs @@ -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] @@ -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] diff --git a/provider/source/src/calendar/eras.rs b/provider/source/src/calendar/eras.rs index d8d5bdbce02..8f8d342ca22 100644 --- a/provider/source/src/calendar/eras.rs +++ b/provider/source/src/calendar/eras.rs @@ -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()); } } } diff --git a/utils/calendrical_calculations/src/gregorian.rs b/utils/calendrical_calculations/src/gregorian.rs index cefed614f3d..6da3d1bce84 100644 --- a/utils/calendrical_calculations/src/gregorian.rs +++ b/utils/calendrical_calculations/src/gregorian.rs @@ -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 @@ -56,31 +68,27 @@ pub const fn year_from_fixed(date: RataDie) -> Result { // 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.