Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ISO calendar with negative years #2706

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions components/calendar/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ harness = false
name = "datetime"
harness = false

[[bench]]
name = "iso"
harness = false

[[example]]
name = "iso_date_manipulations"

Expand Down
20 changes: 20 additions & 0 deletions components/calendar/benches/iso.rs
Original file line number Diff line number Diff line change
@@ -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::<Iso>::from_minutes_since_local_unix_epoch(black_box(minutes));
}
});
});
}

criterion_group!(benches, overview_bench,);
criterion_main!(benches);
69 changes: 69 additions & 0 deletions components/calendar/src/helpers.rs
Original file line number Diff line number Diff line change
@@ -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));
}
73 changes: 71 additions & 2 deletions components/calendar/src/iso.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -313,9 +314,12 @@ impl DateTime<Iso> {

/// 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);
Expand All @@ -324,10 +328,20 @@ impl DateTime<Iso> {
/// 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<Iso> {
let (time, extra_days) = types::Time::from_minute_with_remainder_days(minute);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
}
1 change: 1 addition & 0 deletions components/calendar/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
20 changes: 3 additions & 17 deletions components/calendar/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down