Skip to content

Commit 3a60ceb

Browse files
committed
Fix ordering of UtcOffset
1 parent 7d05f5e commit 3a60ceb

File tree

3 files changed

+59
-27
lines changed

3 files changed

+59
-27
lines changed

time/src/offset_date_time.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ impl OffsetDateTime {
222222
/// ```
223223
#[inline]
224224
pub const fn checked_to_offset(self, offset: UtcOffset) -> Option<Self> {
225-
if self.offset.as_u32() == offset.as_u32() {
225+
if self.offset.as_u32_for_equality() == offset.as_u32_for_equality() {
226226
return Some(self);
227227
}
228228

@@ -359,7 +359,7 @@ impl OffsetDateTime {
359359
let to = offset;
360360

361361
// Fast path for when no conversion is necessary.
362-
if from.as_u32() == to.as_u32() {
362+
if from.as_u32_for_equality() == to.as_u32_for_equality() {
363363
return (self.year(), self.ordinal(), self.time());
364364
}
365365

time/src/utc_offset.rs

Lines changed: 40 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -76,14 +76,14 @@ pub struct UtcOffset {
7676
impl Hash for UtcOffset {
7777
#[inline]
7878
fn hash<H: Hasher>(&self, state: &mut H) {
79-
state.write_u32(self.as_u32());
79+
state.write_u32(self.as_u32_for_equality());
8080
}
8181
}
8282

8383
impl PartialEq for UtcOffset {
8484
#[inline]
8585
fn eq(&self, other: &Self) -> bool {
86-
self.as_u32().eq(&other.as_u32())
86+
self.as_u32_for_equality().eq(&other.as_u32_for_equality())
8787
}
8888
}
8989

@@ -97,30 +97,45 @@ impl PartialOrd for UtcOffset {
9797
impl Ord for UtcOffset {
9898
#[inline]
9999
fn cmp(&self, other: &Self) -> Ordering {
100-
self.as_u32().cmp(&other.as_u32())
100+
self.as_i32_for_comparison()
101+
.cmp(&other.as_i32_for_comparison())
101102
}
102103
}
103104

104105
impl UtcOffset {
105-
/// Provide a representation of the `UtcOffset` as a `u32`. This value can be used for equality,
106-
/// hashing, and ordering.
107-
#[inline]
108-
pub(crate) const fn as_u32(self) -> u32 {
109-
#[cfg(target_endian = "big")]
110-
return u32::from_be_bytes([
111-
self.seconds.get().cast_unsigned(),
112-
self.minutes.get().cast_unsigned(),
113-
self.hours.get().cast_unsigned(),
114-
0,
115-
]);
116-
117-
#[cfg(target_endian = "little")]
118-
return u32::from_le_bytes([
119-
self.seconds.get().cast_unsigned(),
120-
self.minutes.get().cast_unsigned(),
121-
self.hours.get().cast_unsigned(),
122-
0,
123-
]);
106+
/// Provide a representation of the `UtcOffset` as a `i32`. This value can be used for equality,
107+
/// and hashing. This value is not suitable for ordering; use `as_i32_for_comparison` instead.
108+
#[inline]
109+
pub(crate) const fn as_u32_for_equality(self) -> u32 {
110+
// Safety: Size and alignment are handled by the compiler. Both the source and destination
111+
// types are plain old data (POD) types.
112+
unsafe {
113+
if const { cfg!(target_endian = "little") } {
114+
core::mem::transmute::<[i8; 4], u32>([
115+
self.seconds.get(),
116+
self.minutes.get(),
117+
self.hours.get(),
118+
0,
119+
])
120+
} else {
121+
core::mem::transmute::<[i8; 4], u32>([
122+
self.hours.get(),
123+
self.minutes.get(),
124+
self.seconds.get(),
125+
0,
126+
])
127+
}
128+
}
129+
}
130+
131+
/// Provide a representation of the `UtcOffset` as a `i32`. This value can be used for ordering.
132+
/// While it is suitable for equality, `as_u32_for_equality` is preferred for performance
133+
/// reasons.
134+
#[inline]
135+
const fn as_i32_for_comparison(self) -> i32 {
136+
(self.hours.get() as i32) << 16
137+
| (self.minutes.get() as i32) << 8
138+
| (self.seconds.get() as i32)
124139
}
125140

126141
/// A `UtcOffset` that is UTC.
@@ -381,7 +396,7 @@ impl UtcOffset {
381396
/// ```
382397
#[inline]
383398
pub const fn is_utc(self) -> bool {
384-
self.hours.get() == 0 && self.minutes.get() == 0 && self.seconds.get() == 0
399+
self.as_u32_for_equality() == Self::UTC.as_u32_for_equality()
385400
}
386401

387402
/// Check if the offset is positive, or east of UTC.
@@ -394,7 +409,7 @@ impl UtcOffset {
394409
/// ```
395410
#[inline]
396411
pub const fn is_positive(self) -> bool {
397-
self.hours.get() > 0 || self.minutes.get() > 0 || self.seconds.get() > 0
412+
self.as_i32_for_comparison() > Self::UTC.as_i32_for_comparison()
398413
}
399414

400415
/// Check if the offset is negative, or west of UTC.
@@ -407,7 +422,7 @@ impl UtcOffset {
407422
/// ```
408423
#[inline]
409424
pub const fn is_negative(self) -> bool {
410-
self.hours.get() < 0 || self.minutes.get() < 0 || self.seconds.get() < 0
425+
self.as_i32_for_comparison() < Self::UTC.as_i32_for_comparison()
411426
}
412427

413428
/// Attempt to obtain the system's UTC offset at a known moment in time. If the offset cannot be

time/tests/integration/utc_offset.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::cmp::Ordering;
2+
13
use rstest::rstest;
24
use time::macros::offset;
35
use time::{OffsetDateTime, UtcOffset};
@@ -141,6 +143,21 @@ fn is_negative(#[case] offset: UtcOffset, #[case] expected: bool) {
141143
assert_eq!(offset.is_negative(), expected);
142144
}
143145

146+
#[rstest]
147+
#[case(offset!(UTC), offset!(UTC), Ordering::Equal)]
148+
#[case(offset!(+1), offset!(+1), Ordering::Equal)]
149+
#[case(offset!(-1), offset!(-1), Ordering::Equal)]
150+
#[case(offset!(+1), offset!(UTC), Ordering::Greater)]
151+
#[case(offset!(UTC), offset!(-1), Ordering::Greater)]
152+
#[case(offset!(-1), offset!(+1), Ordering::Less)]
153+
#[case(offset!(+23:59), offset!(+23:58), Ordering::Greater)]
154+
#[case(offset!(-23:59), offset!(-23:58), Ordering::Less)]
155+
#[case(offset!(+23:59:59), offset!(+23:59:58), Ordering::Greater)]
156+
#[case(offset!(-23:59:59), offset!(-23:59:58), Ordering::Less)]
157+
fn ordering(#[case] a: UtcOffset, #[case] b: UtcOffset, #[case] expected: Ordering) {
158+
assert_eq!(a.cmp(&b), expected);
159+
}
160+
144161
#[rstest]
145162
#[case(offset!(UTC), offset!(UTC))]
146163
#[case(offset!(+0:00:01), offset!(-0:00:01))]

0 commit comments

Comments
 (0)