Skip to content

Commit

Permalink
Remove icu::calendar unix epoch APIs (#5748)
Browse files Browse the repository at this point in the history
The Unix epoch is a physical time concept. It does not work on a civil
time API. https://errorprone.info/docs/time
  • Loading branch information
robertbastian authored Oct 30, 2024
1 parent 67a3904 commit 10d734c
Show file tree
Hide file tree
Showing 30 changed files with 91 additions and 502 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 0 additions & 4 deletions components/calendar/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,6 @@ harness = false
name = "datetime"
harness = false

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

[[bench]]
name = "convert"
harness = false
Expand Down
20 changes: 0 additions & 20 deletions components/calendar/benches/iso.rs

This file was deleted.

22 changes: 20 additions & 2 deletions components/calendar/src/date.rs
Original file line number Diff line number Diff line change
Expand Up @@ -320,12 +320,30 @@ impl<A: AsCalendar> Date<A> {
&self.calendar
}

#[cfg(test)]
pub(crate) fn to_fixed(&self) -> calendrical_calculations::rata_die::RataDie {
#[doc(hidden)]
pub fn to_fixed(&self) -> calendrical_calculations::rata_die::RataDie {
Iso::fixed_from_iso(self.to_iso().inner)
}
}

impl Date<Iso> {
/// Calculates the number of days between two dates.
///
/// ```rust
/// use icu::calendar::Date;
/// use icu::calendar::DateDurationUnit;
///
/// let a = Date::try_new_iso(1994, 12, 10).unwrap();
/// let b = Date::try_new_iso(2024, 10, 30).unwrap();
///
/// assert_eq!(b.days_since(a), 10_917);
/// ```
#[doc(hidden)] // unstable
pub fn days_since(&self, other: Date<Iso>) -> i32 {
(Iso::fixed_from_iso(*self.inner()) - Iso::fixed_from_iso(other.inner)) as i32
}
}

impl<C: IntoAnyCalendar, A: AsCalendar<Calendar = C>> Date<A> {
/// Type-erase the date, converting it to a date for [`AnyCalendar`]
pub fn to_any(&self) -> Date<AnyCalendar> {
Expand Down
101 changes: 1 addition & 100 deletions components/calendar/src/iso.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
use crate::calendar_arithmetic::{ArithmeticDate, CalendarArithmetic};
use crate::error::DateError;
use crate::{types, Calendar, Date, DateDuration, DateDurationUnit, DateTime, RangeError, Time};
use calendrical_calculations::helpers::{i64_to_saturated_i32, I32CastError};
use calendrical_calculations::helpers::I32CastError;
use calendrical_calculations::rata_die::RataDie;
use tinystr::tinystr;

Expand Down Expand Up @@ -252,11 +252,6 @@ impl Date<Iso> {
.map(IsoDateInner)
.map(|inner| Date::from_raw(inner, Iso))
}

/// Constructs an ISO date representing the UNIX epoch on January 1, 1970.
pub fn unix_epoch() -> Self {
Date::from_raw(IsoDateInner(ArithmeticDate::new_unchecked(1970, 1, 1)), Iso)
}
}

impl DateTime<Iso> {
Expand Down Expand Up @@ -288,83 +283,6 @@ impl DateTime<Iso> {
time: Time::try_new(hour, minute, second, 0)?,
})
}

/// Constructs an ISO datetime representing the UNIX epoch on January 1, 1970
/// at midnight.
pub fn local_unix_epoch() -> Self {
DateTime {
date: Date::unix_epoch(),
time: Time::midnight(),
}
}

/// Minute count representation of calendars starting from 00:00:00 on Jan 1st, 1970.
///
/// ```rust
/// use icu::calendar::DateTime;
///
/// let today = DateTime::try_new_iso(2020, 2, 29, 0, 0, 0).unwrap();
///
/// assert_eq!(today.minutes_since_local_unix_epoch(), 26382240);
/// assert_eq!(
/// DateTime::from_minutes_since_local_unix_epoch(26382240),
/// today
/// );
///
/// let today = DateTime::try_new_iso(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);
/// ```
pub fn minutes_since_local_unix_epoch(&self) -> i32 {
let minutes_a_hour = 60;
let hours_a_day = 24;
let minutes_a_day = minutes_a_hour * hours_a_day;
let unix_epoch = Iso::fixed_from_iso(Date::unix_epoch().inner);
let result = (Iso::fixed_from_iso(*self.date.inner()) - unix_epoch) * minutes_a_day
+ i64::from(self.time.hour.number()) * minutes_a_hour
+ i64::from(self.time.minute.number());
i64_to_saturated_i32(result)
}

/// 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(2020, 2, 29, 0, 0, 0).unwrap();
///
/// assert_eq!(today.minutes_since_local_unix_epoch(), 26382240);
/// assert_eq!(
/// DateTime::from_minutes_since_local_unix_epoch(26382240),
/// today
/// );
///
/// // Unix Epoch
/// let today = DateTime::try_new_iso(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(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) = Time::from_minute_with_remainder_days(minute);
let unix_epoch = Date::unix_epoch();
let unix_epoch_days = Iso::fixed_from_iso(unix_epoch.inner);
let date = Iso::iso_from_fixed(unix_epoch_days + extra_days as i64);
DateTime { date, time }
}
}

impl Iso {
Expand Down Expand Up @@ -823,21 +741,4 @@ mod test {
check(1461, 4, 12, 31); // leap year
check(1462, 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(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, 31, 0, 1);
check(-2879, 1969, 12, 30, 0, 1);
}
}
147 changes: 0 additions & 147 deletions components/calendar/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -679,153 +679,6 @@ impl Time {
nanosecond: nanosecond.try_into()?,
})
}

/// 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 (extra_days, minute_in_day) = (minute.div_euclid(1440), minute.rem_euclid(1440));
let (hours, minutes) = (minute_in_day / 60, minute_in_day % 60);
#[allow(clippy::unwrap_used)] // values are moduloed to be in range
(
Self {
hour: (hours as u8).try_into().unwrap(),
minute: (minutes as u8).try_into().unwrap(),
second: IsoSecond::zero(),
nanosecond: NanoSecond::zero(),
},
extra_days,
)
}
}

#[test]
fn test_from_minute_with_remainder_days() {
#[derive(Debug)]
struct TestCase {
minute: i32,
expected_time: Time,
expected_remainder: i32,
}
let zero_time = Time::new(
IsoHour::zero(),
IsoMinute::zero(),
IsoSecond::zero(),
NanoSecond::zero(),
);
let first_minute_in_day = Time::new(
IsoHour::zero(),
IsoMinute::try_from(1u8).unwrap(),
IsoSecond::zero(),
NanoSecond::zero(),
);
let last_minute_in_day = Time::new(
IsoHour::try_from(23u8).unwrap(),
IsoMinute::try_from(59u8).unwrap(),
IsoSecond::zero(),
NanoSecond::zero(),
);
let cases = [
TestCase {
minute: 0,
expected_time: zero_time,
expected_remainder: 0,
},
TestCase {
minute: 30,
expected_time: Time::new(
IsoHour::zero(),
IsoMinute::try_from(30u8).unwrap(),
IsoSecond::zero(),
NanoSecond::zero(),
),
expected_remainder: 0,
},
TestCase {
minute: 60,
expected_time: Time::new(
IsoHour::try_from(1u8).unwrap(),
IsoMinute::zero(),
IsoSecond::zero(),
NanoSecond::zero(),
),
expected_remainder: 0,
},
TestCase {
minute: 90,
expected_time: Time::new(
IsoHour::try_from(1u8).unwrap(),
IsoMinute::try_from(30u8).unwrap(),
IsoSecond::zero(),
NanoSecond::zero(),
),
expected_remainder: 0,
},
TestCase {
minute: 1439,
expected_time: last_minute_in_day,
expected_remainder: 0,
},
TestCase {
minute: 1440,
expected_time: Time::new(
IsoHour::zero(),
IsoMinute::zero(),
IsoSecond::zero(),
NanoSecond::zero(),
),
expected_remainder: 1,
},
TestCase {
minute: 1441,
expected_time: first_minute_in_day,
expected_remainder: 1,
},
TestCase {
minute: i32::MAX,
expected_time: Time::new(
IsoHour::try_from(2u8).unwrap(),
IsoMinute::try_from(7u8).unwrap(),
IsoSecond::zero(),
NanoSecond::zero(),
),
expected_remainder: 1491308,
},
TestCase {
minute: -1,
expected_time: last_minute_in_day,
expected_remainder: -1,
},
TestCase {
minute: -1439,
expected_time: first_minute_in_day,
expected_remainder: -1,
},
TestCase {
minute: -1440,
expected_time: zero_time,
expected_remainder: -1,
},
TestCase {
minute: -1441,
expected_time: last_minute_in_day,
expected_remainder: -2,
},
TestCase {
minute: i32::MIN,
expected_time: Time::new(
IsoHour::try_from(21u8).unwrap(),
IsoMinute::try_from(52u8).unwrap(),
IsoSecond::zero(),
NanoSecond::zero(),
),
expected_remainder: -1491309,
},
];
for cas in cases {
let (actual_time, actual_remainder) = Time::from_minute_with_remainder_days(cas.minute);
assert_eq!(actual_time, cas.expected_time, "{cas:?}");
assert_eq!(actual_remainder, cas.expected_remainder, "{cas:?}");
}
}

/// A weekday in a 7-day week, according to ISO-8601.
Expand Down
5 changes: 4 additions & 1 deletion components/datetime/benches/datetime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ fn datetime_benches(c: &mut Criterion) {
time,
// zone is unused but we need to make the types match
zone: TimeZoneInfo::utc()
.at_time((Date::unix_epoch(), Time::midnight()))
.at_time((
Date::try_new_iso(2024, 1, 1).unwrap(),
Time::midnight(),
))
.with_zone_variant(ZoneVariant::standard()),
}
}
Expand Down
5 changes: 2 additions & 3 deletions components/datetime/src/neo_pattern.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ size_test!(DateTimePattern, date_time_pattern_size, 32);
/// then check the resolved components:
///
/// ```
/// use icu::calendar::DateTime;
/// use icu::calendar::Date;
/// use icu::calendar::Gregorian;
/// use icu::datetime::fieldset::YMD;
/// use icu::datetime::neo_pattern::DateTimePattern;
Expand All @@ -60,8 +60,7 @@ size_test!(DateTimePattern, date_time_pattern_size, 32);
/// )
/// .unwrap()
/// // The pattern can depend on the datetime being formatted.
/// // For this example, we'll choose the local Unix epoch.
/// .format(&DateTime::local_unix_epoch().to_calendar(Gregorian))
/// .format(&Date::try_new_iso(2024, 1, 1).unwrap().to_calendar(Gregorian))
/// .pattern();
/// assert_writeable_eq!(data_pattern, pattern_str);
/// assert_eq!(custom_pattern, data_pattern);
Expand Down
2 changes: 1 addition & 1 deletion components/datetime/src/provider/time_zones.rs
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ impl<'a> zerovec::maps::ZeroMapKV<'a> for MetazoneId {
#[yoke(prove_covariance_manually)]
pub struct MetazonePeriodV1<'data>(
/// The default mapping between period and metazone id. The second level key is a wall-clock time represented as
/// the number of minutes since the local unix epoch. It represents when the metazone started to be used.
/// the number of minutes since the local [`EPOCH`](icu_timezone::provider::EPOCH). It represents when the metazone started to be used.
#[cfg_attr(feature = "serde", serde(borrow))]
pub ZeroMap2d<'data, TimeZoneBcp47Id, IsoMinutesSinceEpoch, Option<MetazoneId>>,
);
Loading

0 comments on commit 10d734c

Please sign in to comment.