diff --git a/components/calendar/Cargo.toml b/components/calendar/Cargo.toml index 8aa62fd9e56..9967975f005 100644 --- a/components/calendar/Cargo.toml +++ b/components/calendar/Cargo.toml @@ -63,6 +63,10 @@ harness = false name = "datetime" harness = false +[[bench]] +name = "iso" +harness = false + [[example]] name = "iso_date_manipulations" diff --git a/components/calendar/benches/iso.rs b/components/calendar/benches/iso.rs new file mode 100644 index 00000000000..184ba0ce2f6 --- /dev/null +++ b/components/calendar/benches/iso.rs @@ -0,0 +1,20 @@ +// 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 criterion::{black_box, criterion_group, criterion_main, Criterion}; +use icu_calendar::{DateTime, Iso}; + +fn overview_bench(c: &mut Criterion) { + c.bench_function("iso/from_minutes_since_local_unix_epoch", |b| { + b.iter(|| { + for i in -250..250 { + let minutes = i * 10000; + DateTime::::from_minutes_since_local_unix_epoch(black_box(minutes)); + } + }); + }); +} + +criterion_group!(benches, overview_bench,); +criterion_main!(benches); diff --git a/components/calendar/src/helpers.rs b/components/calendar/src/helpers.rs new file mode 100644 index 00000000000..44e98d4a61a --- /dev/null +++ b/components/calendar/src/helpers.rs @@ -0,0 +1,69 @@ +// 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 ). + +/// Calculate `(n / d, n % d)` such that the remainder is always positive. +pub fn div_rem_euclid(n: i32, d: i32) -> (i32, i32) { + debug_assert!(d > 0); + let (a, b) = (n / d, n % d); + if n >= 0 || b == 0 { + (a, b) + } else { + (a - 1, d + b) + } +} + +#[test] +fn test_div_rem_euclid() { + assert_eq!(div_rem_euclid(i32::MIN, 1), (-2147483648, 0)); + assert_eq!(div_rem_euclid(i32::MIN, 2), (-1073741824, 0)); + assert_eq!(div_rem_euclid(i32::MIN, 3), (-715827883, 1)); + + assert_eq!(div_rem_euclid(-10, 1), (-10, 0)); + assert_eq!(div_rem_euclid(-10, 2), (-5, 0)); + assert_eq!(div_rem_euclid(-10, 3), (-4, 2)); + + assert_eq!(div_rem_euclid(-9, 1), (-9, 0)); + assert_eq!(div_rem_euclid(-9, 2), (-5, 1)); + assert_eq!(div_rem_euclid(-9, 3), (-3, 0)); + + assert_eq!(div_rem_euclid(-8, 1), (-8, 0)); + assert_eq!(div_rem_euclid(-8, 2), (-4, 0)); + assert_eq!(div_rem_euclid(-8, 3), (-3, 1)); + + assert_eq!(div_rem_euclid(-2, 1), (-2, 0)); + assert_eq!(div_rem_euclid(-2, 2), (-1, 0)); + assert_eq!(div_rem_euclid(-2, 3), (-1, 1)); + + assert_eq!(div_rem_euclid(-1, 1), (-1, 0)); + assert_eq!(div_rem_euclid(-1, 2), (-1, 1)); + assert_eq!(div_rem_euclid(-1, 3), (-1, 2)); + + assert_eq!(div_rem_euclid(0, 1), (0, 0)); + assert_eq!(div_rem_euclid(0, 2), (0, 0)); + assert_eq!(div_rem_euclid(0, 3), (0, 0)); + + assert_eq!(div_rem_euclid(1, 1), (1, 0)); + assert_eq!(div_rem_euclid(1, 2), (0, 1)); + assert_eq!(div_rem_euclid(1, 3), (0, 1)); + + assert_eq!(div_rem_euclid(2, 1), (2, 0)); + assert_eq!(div_rem_euclid(2, 2), (1, 0)); + assert_eq!(div_rem_euclid(2, 3), (0, 2)); + + assert_eq!(div_rem_euclid(8, 1), (8, 0)); + assert_eq!(div_rem_euclid(8, 2), (4, 0)); + assert_eq!(div_rem_euclid(8, 3), (2, 2)); + + assert_eq!(div_rem_euclid(9, 1), (9, 0)); + assert_eq!(div_rem_euclid(9, 2), (4, 1)); + assert_eq!(div_rem_euclid(9, 3), (3, 0)); + + assert_eq!(div_rem_euclid(10, 1), (10, 0)); + assert_eq!(div_rem_euclid(10, 2), (5, 0)); + assert_eq!(div_rem_euclid(10, 3), (3, 1)); + + assert_eq!(div_rem_euclid(i32::MAX, 1), (2147483647, 0)); + assert_eq!(div_rem_euclid(i32::MAX, 2), (1073741823, 1)); + assert_eq!(div_rem_euclid(i32::MAX, 3), (715827882, 1)); +} diff --git a/components/calendar/src/iso.rs b/components/calendar/src/iso.rs index 08198fe7d29..641fd25bc14 100644 --- a/components/calendar/src/iso.rs +++ b/components/calendar/src/iso.rs @@ -31,6 +31,7 @@ use crate::any_calendar::AnyCalendarKind; use crate::calendar_arithmetic::{ArithmeticDate, CalendarArithmetic}; +use crate::helpers; use crate::{types, Calendar, CalendarError, Date, DateDuration, DateDurationUnit, DateTime}; use tinystr::tinystr; @@ -313,9 +314,12 @@ impl DateTime { /// Convert minute count since 00:00:00 on Jan 1st, 1970 to ISO Date. /// + /// # Examples + /// /// ```rust /// use icu::calendar::DateTime; /// + /// // After Unix Epoch /// let today = DateTime::try_new_iso_datetime(2020, 2, 29, 0, 0, 0).unwrap(); /// /// assert_eq!(today.minutes_since_local_unix_epoch(), 26382240); @@ -324,10 +328,20 @@ impl DateTime { /// today /// ); /// + /// // Unix Epoch /// let today = DateTime::try_new_iso_datetime(1970, 1, 1, 0, 0, 0).unwrap(); /// /// assert_eq!(today.minutes_since_local_unix_epoch(), 0); /// assert_eq!(DateTime::from_minutes_since_local_unix_epoch(0), today); + /// + /// // Before Unix Epoch + /// let today = DateTime::try_new_iso_datetime(1967, 4, 6, 20, 40, 0).unwrap(); + /// + /// assert_eq!(today.minutes_since_local_unix_epoch(), -1440200); + /// assert_eq!( + /// DateTime::from_minutes_since_local_unix_epoch(-1440200), + /// today + /// ); /// ``` pub fn from_minutes_since_local_unix_epoch(minute: i32) -> DateTime { let (time, extra_days) = types::Time::from_minute_with_remainder_days(minute); @@ -415,8 +429,7 @@ impl Iso { // Lisp code reference: https://github.com/EdReingold/calendar-code2/blob/1ee51ecfaae6f856b0d7de3e36e9042100b4f424/calendar.l#L1191-L1217 fn iso_year_from_fixed(date: i32) -> i32 { // 400 year cycles have 146097 days - let n_400 = date / 146097; - let date = date % 146097; + let (n_400, date) = helpers::div_rem_euclid(date, 146097); // 100 year cycles have 36524 days let n_100 = date / 36524; @@ -655,4 +668,60 @@ mod test { let offset = today.added(DateDuration::new(0, 1, 0, 1)); assert_eq!(offset, today_plus_1_month_1_day); } + + #[test] + fn test_iso_to_from_fixed() { + // Reminder: ISO year 0 is Gregorian year 1 BCE. + // Year 0 is a leap year due to the 400-year rule. + fn check(fixed: i32, year: i32, month: u8, day: u8) { + assert_eq!(Iso::iso_year_from_fixed(fixed), year, "fixed: {}", fixed); + assert_eq!( + Iso::iso_from_fixed(fixed), + Date::try_new_iso_date(year, month, day).unwrap(), + "fixed: {}", + fixed + ); + assert_eq!( + Iso::fixed_from_iso_integers(year, month, day), + Some(fixed), + "fixed: {}", + fixed + ); + } + check(-1828, -5, 12, 31); + check(-1827, -4, 1, 1); // leap year + check(-1826, -4, 1, 2); + check(-1462, -4, 12, 31); + check(-1461, -3, 1, 1); + check(-1460, -3, 1, 2); + check(-732, -2, 12, 31); + check(-731, -1, 1, 1); + check(-367, -1, 12, 31); + check(-366, 0, 1, 1); // leap year + check(-365, 0, 1, 2); + check(-1, 0, 12, 31); + check(0, 1, 1, 1); + check(1, 1, 1, 2); + check(364, 1, 12, 31); + check(365, 2, 1, 1); + check(1459, 4, 12, 30); + check(1460, 4, 12, 31); // leap year + check(1461, 5, 1, 1); + } + + #[test] + fn test_from_minutes_since_local_unix_epoch() { + fn check(minutes: i32, year: i32, month: u8, day: u8, hour: u8, minute: u8) { + let today = DateTime::try_new_iso_datetime(year, month, day, hour, minute, 0).unwrap(); + assert_eq!(today.minutes_since_local_unix_epoch(), minutes); + assert_eq!( + DateTime::from_minutes_since_local_unix_epoch(minutes), + today + ); + } + + check(-1441, 1969, 12, 30, 23, 59); + check(-1440, 1969, 12, 31, 0, 0); + check(-1439, 1969, 12, 30, 0, 1); + } } diff --git a/components/calendar/src/lib.rs b/components/calendar/src/lib.rs index 0124c5b0540..cf21430e5b2 100644 --- a/components/calendar/src/lib.rs +++ b/components/calendar/src/lib.rs @@ -124,6 +124,7 @@ mod duration; mod error; pub mod ethiopian; pub mod gregorian; +mod helpers; pub mod indian; pub mod iso; pub mod japanese; diff --git a/components/calendar/src/types.rs b/components/calendar/src/types.rs index bb462b03827..a23a646cd01 100644 --- a/components/calendar/src/types.rs +++ b/components/calendar/src/types.rs @@ -5,6 +5,7 @@ //! This module contains various types used by `icu_calendar` and `icu_datetime` use crate::error::CalendarError; +use crate::helpers; use core::convert::TryFrom; use core::convert::TryInto; use core::fmt; @@ -479,23 +480,8 @@ impl Time { /// Takes a number of minutes, which could be positive or negative, and returns the Time /// and the day number, which could be positive or negative. pub(crate) fn from_minute_with_remainder_days(minute: i32) -> (Time, i32) { - let minutes_a_hour = 60; - let hours_a_day = 24; - let minutes_a_day = minutes_a_hour * hours_a_day; - let extra_days = if minute.is_negative() { - // TODO: migrate to div_floor - (minute + 1) / minutes_a_day - 1 - } else { - minute / minutes_a_day - }; - - let minutes = (minutes_a_hour + minute % minutes_a_hour) % minutes_a_hour; - let hours = if minute.is_negative() { - ((minutes_a_day - minutes - (minute % minutes_a_day).abs()) / minutes_a_hour) - % hours_a_day - } else { - (minute / minutes_a_hour) % hours_a_day - }; + let (extra_days, minute_in_day) = helpers::div_rem_euclid(minute, 1440); + let (hours, minutes) = (minute_in_day / 60, minute_in_day % 60); #[allow(clippy::unwrap_used)] // values are moduloed to be in range ( Self {