Skip to content

Commit ec327a2

Browse files
committed
Optimize Julian day calculations
1 parent 0e1eddd commit ec327a2

File tree

6 files changed

+97
-68
lines changed

6 files changed

+97
-68
lines changed

benchmarks/date.rs

+6-5
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,12 @@ setup_benchmark! {
5959
}
6060

6161
fn from_julian_day(ben: &mut Bencher<'_>) {
62-
ben.iter(|| Date::from_julian_day(-34_803_190));
63-
ben.iter(|| Date::from_julian_day(0));
64-
ben.iter(|| Date::from_julian_day(2_459_753));
62+
let dates = DATES.map(|date| date.to_julian_day());
63+
ben.iter(|| {
64+
for date in dates {
65+
let _ = black_box(Date::from_julian_day(date));
66+
}
67+
});
6568
}
6669
// endregion constructors
6770

@@ -171,8 +174,6 @@ setup_benchmark! {
171174
ben.iter(|| {
172175
for date in DATES {
173176
black_box(date.to_julian_day());
174-
175-
176177
}
177178
});
178179
}

time/src/date.rs

+64-48
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use crate::ext::DigitCount;
1919
#[cfg(feature = "formatting")]
2020
use crate::formatting::Formattable;
2121
use crate::internal_macros::{
22-
cascade, const_try, const_try_opt, div_floor, ensure_ranged, expect_opt, impl_add_assign,
22+
const_try, const_try_opt, div_floor, ensure_ranged, expect_opt, impl_add_assign,
2323
impl_sub_assign,
2424
};
2525
#[cfg(feature = "parsing")]
@@ -74,28 +74,41 @@ impl Date {
7474
unsafe { Self::__from_ordinal_date_unchecked(MAX_YEAR, days_in_year(MAX_YEAR)) };
7575

7676
// region: constructors
77-
/// Construct a `Date` from the year and ordinal values, the validity of which must be
77+
/// Construct a `Date` from its internal representation, the validity of which must be
7878
/// guaranteed by the caller.
7979
///
8080
/// # Safety
8181
///
82-
/// `ordinal` must be non-zero and at most the number of days in `year`. `year` should be in the
83-
/// range `MIN_YEAR..=MAX_YEAR`, but this is not a safety invariant.
84-
#[doc(hidden)]
85-
pub const unsafe fn __from_ordinal_date_unchecked(year: i32, ordinal: u16) -> Self {
82+
/// - `ordinal` must be non-zero and at most the number of days in `year`
83+
/// - `is_leap_year` must be `true` if and only if `year` is a leap year
84+
const unsafe fn from_parts(year: i32, is_leap_year: bool, ordinal: u16) -> Self {
8685
debug_assert!(year >= MIN_YEAR);
8786
debug_assert!(year <= MAX_YEAR);
8887
debug_assert!(ordinal != 0);
8988
debug_assert!(ordinal <= days_in_year(year));
90-
91-
let is_leap = is_leap_year(year) as i32;
89+
debug_assert!(crate::util::is_leap_year(year) == is_leap_year);
9290

9391
Self {
94-
// Safety: The caller must guarantee that `ordinal` is not zero.
95-
value: unsafe { NonZeroI32::new_unchecked(year << 10 | is_leap << 9 | ordinal as i32) },
92+
// Safety: `ordinal` is not zero.
93+
value: unsafe {
94+
NonZeroI32::new_unchecked(year << 10 | (is_leap_year as i32) << 9 | ordinal as i32)
95+
},
9696
}
9797
}
9898

99+
/// Construct a `Date` from the year and ordinal values, the validity of which must be
100+
/// guaranteed by the caller.
101+
///
102+
/// # Safety
103+
///
104+
/// `ordinal` must be non-zero and at most the number of days in `year`. `year` should be in the
105+
/// range `MIN_YEAR..=MAX_YEAR`, but this is not a safety invariant.
106+
#[doc(hidden)]
107+
pub const unsafe fn __from_ordinal_date_unchecked(year: i32, ordinal: u16) -> Self {
108+
// Safety: The caller must guarantee that `ordinal` is not zero.
109+
unsafe { Self::from_parts(year, is_leap_year(year), ordinal) }
110+
}
111+
99112
/// Attempt to create a `Date` from the year, month, and day.
100113
///
101114
/// ```rust
@@ -259,47 +272,50 @@ impl Date {
259272
pub const fn from_julian_day(julian_day: i32) -> Result<Self, error::ComponentRange> {
260273
type JulianDay = RangedI32<{ Date::MIN.to_julian_day() }, { Date::MAX.to_julian_day() }>;
261274
ensure_ranged!(JulianDay: julian_day);
262-
Ok(Self::from_julian_day_unchecked(julian_day))
275+
// Safety: The Julian day number is in range.
276+
Ok(unsafe { Self::from_julian_day_unchecked(julian_day) })
263277
}
264278

265279
/// Create a `Date` from the Julian day.
266280
///
267-
/// This does not check the validity of the provided Julian day, and as such may result in an
268-
/// internally invalid value.
269-
#[doc(alias = "from_julian_date_unchecked")]
270-
pub(crate) const fn from_julian_day_unchecked(julian_day: i32) -> Self {
281+
/// # Safety
282+
///
283+
/// The provided Julian day number must be between `Date::MIN.to_julian_day()` and
284+
/// `Date::MAX.to_julian_day()` inclusive.
285+
pub(crate) const unsafe fn from_julian_day_unchecked(julian_day: i32) -> Self {
271286
debug_assert!(julian_day >= Self::MIN.to_julian_day());
272287
debug_assert!(julian_day <= Self::MAX.to_julian_day());
273288

274-
// To avoid a potential overflow, the value may need to be widened for some arithmetic.
289+
const S: i32 = 2_500;
290+
const K: i32 = 719_468 + 146_097 * S;
291+
const L: i32 = 400 * S;
275292

276-
let z = julian_day - 1_721_119;
277-
let (mut year, mut ordinal) = if julian_day < -19_752_948 || julian_day > 23_195_514 {
278-
let g = 100 * z as i64 - 25;
279-
let a = (g / 3_652_425) as i32;
280-
let b = a - a / 4;
281-
let year = div_floor!(100 * b as i64 + g, 36525) as i32;
282-
let ordinal = (b + z - div_floor!(36525 * year as i64, 100) as i32) as _;
283-
(year, ordinal)
284-
} else {
285-
let g = 100 * z - 25;
286-
let a = g / 3_652_425;
287-
let b = a - a / 4;
288-
let year = div_floor!(100 * b + g, 36525);
289-
let ordinal = (b + z - div_floor!(36525 * year, 100)) as _;
290-
(year, ordinal)
291-
};
293+
let julian_day = julian_day - 2_440_588;
294+
let n = (julian_day + K) as u32;
295+
296+
let n_1 = 4 * n + 3;
297+
let c = n_1 / 146_097;
298+
let n_c = n_1 % 146_097 / 4;
299+
300+
let n_2 = 4 * n_c + 3;
301+
let p_2 = 2_939_745 * n_2 as u64;
302+
let z = (p_2 >> 32) as u32;
303+
let n_y = p_2 as u32 / 2_939_745 / 4;
304+
let y = 100 * c + z;
305+
306+
let j = n_y >= 306;
307+
let y_g = y as i32 - L + j as i32;
292308

293-
if is_leap_year(year) {
294-
ordinal += 60;
295-
cascade!(ordinal in 1..367 => year);
309+
let is_leap_year = is_leap_year(y_g);
310+
let ordinal = if j {
311+
n_y - 305
296312
} else {
297-
ordinal += 59;
298-
cascade!(ordinal in 1..366 => year);
299-
}
313+
n_y + 60 + is_leap_year as u32
314+
};
300315

301-
// Safety: `ordinal` is not zero.
302-
unsafe { Self::__from_ordinal_date_unchecked(year, ordinal) }
316+
// Safety: `ordinal` is not zero and `is_leap_year` is correct, so long as the Julian day
317+
// number is in range.
318+
unsafe { Self::from_parts(y_g, is_leap_year, ordinal as _) }
303319
}
304320
// endregion constructors
305321

@@ -708,9 +724,6 @@ impl Date {
708724

709725
/// Get the Julian day for the date.
710726
///
711-
/// The algorithm to perform this conversion is derived from one provided by Peter Baum; it is
712-
/// freely available [here](https://www.researchgate.net/publication/316558298_Date_Algorithms).
713-
///
714727
/// ```rust
715728
/// # use time_macros::date;
716729
/// assert_eq!(date!(-4713 - 11 - 24).to_julian_day(), 0);
@@ -719,12 +732,15 @@ impl Date {
719732
/// assert_eq!(date!(2019-12-31).to_julian_day(), 2_458_849);
720733
/// ```
721734
pub const fn to_julian_day(self) -> i32 {
722-
let year = self.year() - 1;
723-
let ordinal = self.ordinal() as i32;
735+
let (year, ordinal) = self.to_ordinal_date();
736+
737+
// The algorithm requires a non-negative year. Add the lowest value to make it so. This is
738+
// adjusted for at the end with the final subtraction.
739+
let adj_year = year + 999_999;
740+
let century = adj_year / 100;
724741

725-
ordinal + 365 * year + div_floor!(year, 4) - div_floor!(year, 100)
726-
+ div_floor!(year, 400)
727-
+ 1_721_425
742+
let days_before_year = (1461 * adj_year as i64 / 4) as i32 - century + century / 4;
743+
days_before_year + ordinal as i32 - 363_521_075
728744
}
729745
// endregion getters
730746

time/src/offset_date_time.rs

+6-3
Original file line numberDiff line numberDiff line change
@@ -372,9 +372,12 @@ impl OffsetDateTime {
372372
ensure_ranged!(Timestamp: timestamp);
373373

374374
// Use the unchecked method here, as the input validity has already been verified.
375-
let date = Date::from_julian_day_unchecked(
376-
UNIX_EPOCH_JULIAN_DAY + div_floor!(timestamp, Second::per(Day) as i64) as i32,
377-
);
375+
// Safety: The Julian day number is in range.
376+
let date = unsafe {
377+
Date::from_julian_day_unchecked(
378+
UNIX_EPOCH_JULIAN_DAY + div_floor!(timestamp, Second::per(Day) as i64) as i32,
379+
)
380+
};
378381

379382
let seconds_within_day = timestamp.rem_euclid(Second::per(Day) as _);
380383
// Safety: All values are in range.

time/src/quickcheck.rs

+9-6
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,15 @@ macro_rules! arbitrary_between {
5454

5555
impl Arbitrary for Date {
5656
fn arbitrary(g: &mut Gen) -> Self {
57-
Self::from_julian_day_unchecked(arbitrary_between!(
58-
i32;
59-
g,
60-
Self::MIN.to_julian_day(),
61-
Self::MAX.to_julian_day()
62-
))
57+
// Safety: The Julian day number is in range.
58+
unsafe {
59+
Self::from_julian_day_unchecked(arbitrary_between!(
60+
i32;
61+
g,
62+
Self::MIN.to_julian_day(),
63+
Self::MAX.to_julian_day()
64+
))
65+
}
6366
}
6467

6568
fn shrink(&self) -> Box<dyn Iterator<Item = Self>> {

time/src/rand.rs

+6-3
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,12 @@ impl Distribution<Time> for Standard {
1313

1414
impl Distribution<Date> for Standard {
1515
fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> Date {
16-
Date::from_julian_day_unchecked(
17-
rng.gen_range(Date::MIN.to_julian_day()..=Date::MAX.to_julian_day()),
18-
)
16+
// Safety: The Julian day number is in range.
17+
unsafe {
18+
Date::from_julian_day_unchecked(
19+
rng.gen_range(Date::MIN.to_julian_day()..=Date::MAX.to_julian_day()),
20+
)
21+
}
1922
}
2023
}
2124

time/src/utc_date_time.rs

+6-3
Original file line numberDiff line numberDiff line change
@@ -193,9 +193,12 @@ impl UtcDateTime {
193193
ensure_ranged!(Timestamp: timestamp);
194194

195195
// Use the unchecked method here, as the input validity has already been verified.
196-
let date = Date::from_julian_day_unchecked(
197-
UNIX_EPOCH_JULIAN_DAY + div_floor!(timestamp, Second::per(Day) as i64) as i32,
198-
);
196+
// Safety: The Julian day number is in range.
197+
let date = unsafe {
198+
Date::from_julian_day_unchecked(
199+
UNIX_EPOCH_JULIAN_DAY + div_floor!(timestamp, Second::per(Day) as i64) as i32,
200+
)
201+
};
199202

200203
let seconds_within_day = timestamp.rem_euclid(Second::per(Day) as _);
201204
// Safety: All values are in range.

0 commit comments

Comments
 (0)