jiff/tz/offset.rs
1use core::{
2 ops::{Add, AddAssign, Neg, Sub, SubAssign},
3 time::Duration as UnsignedDuration,
4};
5
6use jcore::{constants as c, tz::Offset as JOffset};
7
8use crate::{
9 civil,
10 duration::{Duration, SDuration},
11 error::{tz::offset::Error as E, Error, ErrorContext},
12 span::Span,
13 timestamp::Timestamp,
14 tz::{AmbiguousOffset, AmbiguousTimestamp, AmbiguousZoned, TimeZone},
15 util::{b, constant, round::Increment},
16 RoundMode, SignedDuration, Unit,
17};
18
19/// An enum indicating whether a particular datetime is in DST or not.
20///
21/// DST stands for "daylight saving time." It is a label used to apply to
22/// points in time as a way to contrast it with "standard time." DST is
23/// usually, but not always, one hour ahead of standard time. When DST takes
24/// effect is usually determined by governments, and the rules can vary
25/// depending on the location. DST is typically used as a means to maximize
26/// "sunlight" time during typical working hours, and as a cost cutting measure
27/// by reducing energy consumption. (The effectiveness of DST and whether it
28/// is overall worth it is a separate question entirely.)
29///
30/// In general, most users should never need to deal with this type. But it can
31/// be occasionally useful in circumstances where callers need to know whether
32/// DST is active or not for a particular point in time.
33///
34/// This type has a `From<bool>` trait implementation, where the bool is
35/// interpreted as being `true` when DST is active.
36#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
37#[cfg_attr(feature = "defmt", derive(defmt::Format))]
38pub enum Dst {
39 /// DST is not in effect. In other words, standard time is in effect.
40 No,
41 /// DST is in effect.
42 Yes,
43}
44
45impl Dst {
46 /// Returns true when this value is equal to `Dst::Yes`.
47 pub fn is_dst(self) -> bool {
48 matches!(self, Dst::Yes)
49 }
50
51 /// Returns true when this value is equal to `Dst::No`.
52 ///
53 /// `std` in this context refers to "standard time." That is, it is the
54 /// offset from UTC used when DST is not in effect.
55 pub fn is_std(self) -> bool {
56 matches!(self, Dst::No)
57 }
58
59 pub(crate) fn from_jcore(dst: jcore::tz::Dst) -> Dst {
60 match dst {
61 jcore::tz::Dst::Yes => Dst::Yes,
62 jcore::tz::Dst::No => Dst::No,
63 }
64 }
65}
66
67impl From<bool> for Dst {
68 fn from(is_dst: bool) -> Dst {
69 if is_dst {
70 Dst::Yes
71 } else {
72 Dst::No
73 }
74 }
75}
76
77/// Represents a fixed time zone offset.
78///
79/// Negative offsets correspond to time zones west of the prime meridian, while
80/// positive offsets correspond to time zones east of the prime meridian.
81/// Equivalently, in all cases, `civil-time - offset = UTC`.
82///
83/// # Display format
84///
85/// This type implements the `std::fmt::Display` trait. It
86/// will convert the offset to a string format in the form
87/// `{sign}{hours}[:{minutes}[:{seconds}]]`, where `minutes` and `seconds` are
88/// only present when non-zero. For example:
89///
90/// ```
91/// use jiff::tz;
92///
93/// let o = tz::offset(-5);
94/// assert_eq!(o.to_string(), "-05");
95/// let o = tz::Offset::from_seconds(-18_000).unwrap();
96/// assert_eq!(o.to_string(), "-05");
97/// let o = tz::Offset::from_seconds(-18_060).unwrap();
98/// assert_eq!(o.to_string(), "-05:01");
99/// let o = tz::Offset::from_seconds(-18_062).unwrap();
100/// assert_eq!(o.to_string(), "-05:01:02");
101///
102/// // The min value.
103/// let o = tz::Offset::from_seconds(-93_599).unwrap();
104/// assert_eq!(o.to_string(), "-25:59:59");
105/// // The max value.
106/// let o = tz::Offset::from_seconds(93_599).unwrap();
107/// assert_eq!(o.to_string(), "+25:59:59");
108/// // No offset.
109/// let o = tz::offset(0);
110/// assert_eq!(o.to_string(), "+00");
111/// ```
112///
113/// # Example
114///
115/// This shows how to create a zoned datetime with a time zone using a fixed
116/// offset:
117///
118/// ```
119/// use jiff::{civil::date, tz, Zoned};
120///
121/// let offset = tz::offset(-4).to_time_zone();
122/// let zdt = date(2024, 7, 8).at(15, 20, 0, 0).to_zoned(offset)?;
123/// assert_eq!(zdt.to_string(), "2024-07-08T15:20:00-04:00[-04:00]");
124///
125/// # Ok::<(), Box<dyn std::error::Error>>(())
126/// ```
127///
128/// Notice that the zoned datetime still includes a time zone annotation. But
129/// since there is no time zone identifier, the offset instead is repeated as
130/// an additional assertion that a fixed offset datetime was intended.
131#[derive(Clone, Copy, Eq, Hash, PartialEq, PartialOrd, Ord)]
132#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
133pub struct Offset {
134 inner: JOffset,
135}
136
137impl Offset {
138 /// The minimum possible time zone offset.
139 ///
140 /// This corresponds to the offset `-25:59:59`.
141 pub const MIN: Offset = Offset { inner: JOffset::MIN };
142
143 /// The maximum possible time zone offset.
144 ///
145 /// This corresponds to the offset `25:59:59`.
146 pub const MAX: Offset = Offset { inner: JOffset::MAX };
147
148 /// The offset corresponding to UTC. That is, no offset at all.
149 ///
150 /// This is defined to always be equivalent to `Offset::ZERO`, but it is
151 /// semantically distinct. This ought to be used when UTC is desired
152 /// specifically, while `Offset::ZERO` ought to be used when one wants to
153 /// express "no offset." For example, when adding offsets, `Offset::ZERO`
154 /// corresponds to the identity.
155 pub const UTC: Offset = Offset { inner: JOffset::UTC };
156
157 /// The offset corresponding to no offset at all.
158 ///
159 /// This is defined to always be equivalent to `Offset::UTC`, but it is
160 /// semantically distinct. This ought to be used when a zero offset is
161 /// desired specifically, while `Offset::UTC` ought to be used when one
162 /// wants to express UTC. For example, when adding offsets, `Offset::ZERO`
163 /// corresponds to the identity.
164 pub const ZERO: Offset = Offset { inner: JOffset::UTC };
165
166 /// Creates a new time zone offset in a `const` context from a given number
167 /// of hours.
168 ///
169 /// Negative offsets correspond to time zones west of the prime meridian,
170 /// while positive offsets correspond to time zones east of the prime
171 /// meridian. Equivalently, in all cases, `civil-time - offset = UTC`.
172 ///
173 /// The fallible non-const version of this constructor is
174 /// [`Offset::from_hours`].
175 ///
176 /// # Panics
177 ///
178 /// This routine panics when the given number of hours is out of range.
179 /// Namely, `hours` must be in the range `-25..=25`.
180 ///
181 /// # Example
182 ///
183 /// ```
184 /// use jiff::tz::Offset;
185 ///
186 /// let o = Offset::constant(-5);
187 /// assert_eq!(o.seconds(), -18_000);
188 /// let o = Offset::constant(5);
189 /// assert_eq!(o.seconds(), 18_000);
190 /// ```
191 ///
192 /// Alternatively, one can use the terser `jiff::tz::offset` free function:
193 ///
194 /// ```
195 /// use jiff::tz;
196 ///
197 /// let o = tz::offset(-5);
198 /// assert_eq!(o.seconds(), -18_000);
199 /// let o = tz::offset(5);
200 /// assert_eq!(o.seconds(), 18_000);
201 /// ```
202 #[inline]
203 pub const fn constant(hours: i8) -> Offset {
204 let hours = constant::unwrapr!(
205 b::OffsetHours::checkc(hours as i64),
206 "invalid time zone offset hours",
207 );
208 Offset::constant_seconds((hours as i32) * 60 * 60)
209 }
210
211 /// Creates a new time zone offset in a `const` context from a given number
212 /// of seconds.
213 ///
214 /// Negative offsets correspond to time zones west of the prime meridian,
215 /// while positive offsets correspond to time zones east of the prime
216 /// meridian. Equivalently, in all cases, `civil-time - offset = UTC`.
217 ///
218 /// The fallible non-const version of this constructor is
219 /// [`Offset::from_seconds`].
220 ///
221 /// # Panics
222 ///
223 /// This routine panics when the given number of seconds is out of range.
224 /// The range corresponds to the offsets `-25:59:59..=25:59:59`. In units
225 /// of seconds, that corresponds to `-93,599..=93,599`.
226 ///
227 /// # Example
228 ///
229 /// ```ignore
230 /// use jiff::tz::Offset;
231 ///
232 /// let o = Offset::constant_seconds(-18_000);
233 /// assert_eq!(o.seconds(), -18_000);
234 /// let o = Offset::constant_seconds(18_000);
235 /// assert_eq!(o.seconds(), 18_000);
236 /// ```
237 // This is currently unexported because I find the name too long and
238 // very off-putting. I don't think non-hour offsets are used enough to
239 // warrant its existence. And I think I'd rather `Offset::hms` be const and
240 // exported instead of this monstrosity.
241 #[inline]
242 pub(crate) const fn constant_seconds(seconds: i32) -> Offset {
243 let inner = constant::unwrapr!(
244 JOffset::from_seconds(seconds),
245 "invalid time zone offset seconds",
246 );
247 Offset { inner }
248 }
249
250 /// Creates a new time zone offset from a given number of hours.
251 ///
252 /// Negative offsets correspond to time zones west of the prime meridian,
253 /// while positive offsets correspond to time zones east of the prime
254 /// meridian. Equivalently, in all cases, `civil-time - offset = UTC`.
255 ///
256 /// # Errors
257 ///
258 /// This routine returns an error when the given number of hours is out of
259 /// range. Namely, `hours` must be in the range `-25..=25`.
260 ///
261 /// # Example
262 ///
263 /// ```
264 /// use jiff::tz::Offset;
265 ///
266 /// let o = Offset::from_hours(-5)?;
267 /// assert_eq!(o.seconds(), -18_000);
268 /// let o = Offset::from_hours(5)?;
269 /// assert_eq!(o.seconds(), 18_000);
270 ///
271 /// # Ok::<(), Box<dyn std::error::Error>>(())
272 /// ```
273 #[inline]
274 pub fn from_hours(hours: i8) -> Result<Offset, Error> {
275 let inner = JOffset::from_hours(hours).map_err(Error::jcore_range)?;
276 Ok(Offset { inner })
277 }
278
279 /// Creates a new time zone offset in a `const` context from a given number
280 /// of seconds.
281 ///
282 /// Negative offsets correspond to time zones west of the prime meridian,
283 /// while positive offsets correspond to time zones east of the prime
284 /// meridian. Equivalently, in all cases, `civil-time - offset = UTC`.
285 ///
286 /// # Errors
287 ///
288 /// This routine returns an error when the given number of seconds is out
289 /// of range. The range corresponds to the offsets `-25:59:59..=25:59:59`.
290 /// In units of seconds, that corresponds to `-93,599..=93,599`.
291 ///
292 /// # Example
293 ///
294 /// ```
295 /// use jiff::tz::Offset;
296 ///
297 /// let o = Offset::from_seconds(-18_000)?;
298 /// assert_eq!(o.seconds(), -18_000);
299 /// let o = Offset::from_seconds(18_000)?;
300 /// assert_eq!(o.seconds(), 18_000);
301 ///
302 /// # Ok::<(), Box<dyn std::error::Error>>(())
303 /// ```
304 #[inline]
305 pub fn from_seconds(seconds: i32) -> Result<Offset, Error> {
306 let inner =
307 JOffset::from_seconds(seconds).map_err(Error::jcore_range)?;
308 Ok(Offset { inner })
309 }
310
311 /// Returns the total number of seconds in this offset.
312 ///
313 /// The value returned is guaranteed to represent an offset in the range
314 /// `-25:59:59..=25:59:59`. Or more precisely, the value will be in units
315 /// of seconds in the range `-93,599..=93,599`.
316 ///
317 /// Negative offsets correspond to time zones west of the prime meridian,
318 /// while positive offsets correspond to time zones east of the prime
319 /// meridian. Equivalently, in all cases, `civil-time - offset = UTC`.
320 ///
321 /// # Example
322 ///
323 /// ```
324 /// use jiff::tz;
325 ///
326 /// let o = tz::offset(-5);
327 /// assert_eq!(o.seconds(), -18_000);
328 /// let o = tz::offset(5);
329 /// assert_eq!(o.seconds(), 18_000);
330 /// ```
331 #[inline]
332 pub const fn seconds(self) -> i32 {
333 self.inner.seconds()
334 }
335
336 /// Returns the negation of this offset.
337 ///
338 /// A negative offset will become positive and vice versa. This is a no-op
339 /// if the offset is zero.
340 ///
341 /// This never panics.
342 ///
343 /// # Example
344 ///
345 /// ```
346 /// use jiff::tz;
347 ///
348 /// assert_eq!(tz::offset(-5).negate(), tz::offset(5));
349 /// // It's also available via the `-` operator:
350 /// assert_eq!(-tz::offset(-5), tz::offset(5));
351 /// ```
352 pub fn negate(self) -> Offset {
353 let inner = self.inner.negate();
354 Offset { inner }
355 }
356
357 /// Returns the "sign number" or "signum" of this offset.
358 ///
359 /// The number returned is `-1` when this offset is negative,
360 /// `0` when this offset is zero and `1` when this span is positive.
361 ///
362 /// # Example
363 ///
364 /// ```
365 /// use jiff::tz;
366 ///
367 /// assert_eq!(tz::offset(5).signum(), 1);
368 /// assert_eq!(tz::offset(0).signum(), 0);
369 /// assert_eq!(tz::offset(-5).signum(), -1);
370 /// ```
371 #[inline]
372 pub fn signum(self) -> i8 {
373 self.inner.signum()
374 }
375
376 /// Returns true if and only if this offset is positive.
377 ///
378 /// This returns false when the offset is zero or negative.
379 ///
380 /// # Example
381 ///
382 /// ```
383 /// use jiff::tz;
384 ///
385 /// assert!(tz::offset(5).is_positive());
386 /// assert!(!tz::offset(0).is_positive());
387 /// assert!(!tz::offset(-5).is_positive());
388 /// ```
389 pub fn is_positive(self) -> bool {
390 self.inner.is_positive()
391 }
392
393 /// Returns true if and only if this offset is less than zero.
394 ///
395 /// # Example
396 ///
397 /// ```
398 /// use jiff::tz;
399 ///
400 /// assert!(!tz::offset(5).is_negative());
401 /// assert!(!tz::offset(0).is_negative());
402 /// assert!(tz::offset(-5).is_negative());
403 /// ```
404 pub fn is_negative(self) -> bool {
405 self.inner.is_negative()
406 }
407
408 /// Returns true if and only if this offset is zero.
409 ///
410 /// Or equivalently, when this offset corresponds to [`Offset::UTC`].
411 ///
412 /// # Example
413 ///
414 /// ```
415 /// use jiff::tz;
416 ///
417 /// assert!(!tz::offset(5).is_zero());
418 /// assert!(tz::offset(0).is_zero());
419 /// assert!(!tz::offset(-5).is_zero());
420 /// ```
421 pub fn is_zero(self) -> bool {
422 self.inner.is_zero()
423 }
424
425 /// Converts this offset into a [`TimeZone`].
426 ///
427 /// This is a convenience function for calling [`TimeZone::fixed`] with
428 /// this offset.
429 ///
430 /// # Example
431 ///
432 /// ```
433 /// use jiff::tz::offset;
434 ///
435 /// let tz = offset(-4).to_time_zone();
436 /// assert_eq!(
437 /// tz.to_datetime(jiff::Timestamp::UNIX_EPOCH).to_string(),
438 /// "1969-12-31T20:00:00",
439 /// );
440 /// ```
441 pub fn to_time_zone(self) -> TimeZone {
442 TimeZone::fixed(self)
443 }
444
445 /// Converts the given timestamp to a civil datetime using this offset.
446 ///
447 /// # Example
448 ///
449 /// ```
450 /// use jiff::{civil::date, tz, Timestamp};
451 ///
452 /// assert_eq!(
453 /// tz::offset(-8).to_datetime(Timestamp::UNIX_EPOCH),
454 /// date(1969, 12, 31).at(16, 0, 0, 0),
455 /// );
456 /// ```
457 #[inline]
458 pub fn to_datetime(self, timestamp: Timestamp) -> civil::DateTime {
459 civil::DateTime::from_jcore(
460 self.inner.to_datetime(timestamp.to_jcore()),
461 )
462 }
463
464 /// Converts the given civil datetime to a timestamp using this offset.
465 ///
466 /// # Errors
467 ///
468 /// This returns an error if this would have returned a timestamp outside
469 /// of its minimum and maximum values.
470 ///
471 /// # Example
472 ///
473 /// This example shows how to find the timestamp corresponding to
474 /// `1969-12-31T16:00:00-08`.
475 ///
476 /// ```
477 /// use jiff::{civil::date, tz, Timestamp};
478 ///
479 /// assert_eq!(
480 /// tz::offset(-8).to_timestamp(date(1969, 12, 31).at(16, 0, 0, 0))?,
481 /// Timestamp::UNIX_EPOCH,
482 /// );
483 /// # Ok::<(), Box<dyn std::error::Error>>(())
484 /// ```
485 ///
486 /// This example shows some maximum boundary conditions where this routine
487 /// will fail:
488 ///
489 /// ```
490 /// use jiff::{civil::date, tz, Timestamp, ToSpan};
491 ///
492 /// let dt = date(9999, 12, 31).at(23, 0, 0, 0);
493 /// assert!(tz::offset(-8).to_timestamp(dt).is_err());
494 ///
495 /// // If the offset is big enough, then converting it to a UTC
496 /// // timestamp will fit, even when using the maximum civil datetime.
497 /// let dt = date(9999, 12, 31).at(23, 59, 59, 999_999_999);
498 /// assert_eq!(tz::Offset::MAX.to_timestamp(dt).unwrap(), Timestamp::MAX);
499 /// // But adjust the offset down 1 second is enough to go out-of-bounds.
500 /// assert!((tz::Offset::MAX - 1.seconds()).to_timestamp(dt).is_err());
501 /// ```
502 ///
503 /// Same as above, but for minimum values:
504 ///
505 /// ```
506 /// use jiff::{civil::date, tz, Timestamp, ToSpan};
507 ///
508 /// let dt = date(-9999, 1, 1).at(1, 0, 0, 0);
509 /// assert!(tz::offset(8).to_timestamp(dt).is_err());
510 ///
511 /// // If the offset is small enough, then converting it to a UTC
512 /// // timestamp will fit, even when using the minimum civil datetime.
513 /// let dt = date(-9999, 1, 1).at(0, 0, 0, 0);
514 /// assert_eq!(tz::Offset::MIN.to_timestamp(dt).unwrap(), Timestamp::MIN);
515 /// // But adjust the offset up 1 second is enough to go out-of-bounds.
516 /// assert!((tz::Offset::MIN + 1.seconds()).to_timestamp(dt).is_err());
517 /// ```
518 #[inline]
519 pub fn to_timestamp(
520 self,
521 dt: civil::DateTime,
522 ) -> Result<Timestamp, Error> {
523 Ok(Timestamp::from_jcore(
524 self.inner
525 .to_timestamp(dt.to_jcore())
526 .context(E::ConvertDateTimeToTimestamp { offset: self })?,
527 ))
528 }
529
530 /// Adds the given span of time to this offset.
531 ///
532 /// Since time zone offsets have second resolution, any fractional seconds
533 /// in the duration given are ignored.
534 ///
535 /// This operation accepts three different duration types: [`Span`],
536 /// [`SignedDuration`] or [`std::time::Duration`]. This is achieved via
537 /// `From` trait implementations for the [`OffsetArithmetic`] type.
538 ///
539 /// # Errors
540 ///
541 /// This returns an error if the result of adding the given span would
542 /// exceed the minimum or maximum allowed `Offset` value.
543 ///
544 /// This also returns an error if the span given contains any non-zero
545 /// units bigger than hours.
546 ///
547 /// # Example
548 ///
549 /// This example shows how to add one hour to an offset (if the offset
550 /// corresponds to standard time, then adding an hour will usually give
551 /// you DST time):
552 ///
553 /// ```
554 /// use jiff::{tz, ToSpan};
555 ///
556 /// let off = tz::offset(-5);
557 /// assert_eq!(off.checked_add(1.hours()).unwrap(), tz::offset(-4));
558 /// ```
559 ///
560 /// And note that while fractional seconds are ignored, units less than
561 /// seconds aren't ignored if they sum up to a duration at least as big
562 /// as one second:
563 ///
564 /// ```
565 /// use jiff::{tz, ToSpan};
566 ///
567 /// let off = tz::offset(5);
568 /// let span = 900.milliseconds()
569 /// .microseconds(50_000)
570 /// .nanoseconds(50_000_000);
571 /// assert_eq!(
572 /// off.checked_add(span).unwrap(),
573 /// tz::Offset::from_seconds((5 * 60 * 60) + 1).unwrap(),
574 /// );
575 /// // Any leftover fractional part is ignored.
576 /// let span = 901.milliseconds()
577 /// .microseconds(50_001)
578 /// .nanoseconds(50_000_001);
579 /// assert_eq!(
580 /// off.checked_add(span).unwrap(),
581 /// tz::Offset::from_seconds((5 * 60 * 60) + 1).unwrap(),
582 /// );
583 /// ```
584 ///
585 /// This example shows some cases where checked addition will fail.
586 ///
587 /// ```
588 /// use jiff::{tz::Offset, ToSpan};
589 ///
590 /// // Adding units above 'hour' always results in an error.
591 /// assert!(Offset::UTC.checked_add(1.day()).is_err());
592 /// assert!(Offset::UTC.checked_add(1.week()).is_err());
593 /// assert!(Offset::UTC.checked_add(1.month()).is_err());
594 /// assert!(Offset::UTC.checked_add(1.year()).is_err());
595 ///
596 /// // Adding even 1 second to the max, or subtracting 1 from the min,
597 /// // will result in overflow and thus an error will be returned.
598 /// assert!(Offset::MIN.checked_add(-1.seconds()).is_err());
599 /// assert!(Offset::MAX.checked_add(1.seconds()).is_err());
600 /// ```
601 ///
602 /// # Example: adding absolute durations
603 ///
604 /// This shows how to add signed and unsigned absolute durations to an
605 /// `Offset`. Like with `Span`s, any fractional seconds are ignored.
606 ///
607 /// ```
608 /// use std::time::Duration;
609 ///
610 /// use jiff::{tz::offset, SignedDuration};
611 ///
612 /// let off = offset(-10);
613 ///
614 /// let dur = SignedDuration::from_hours(11);
615 /// assert_eq!(off.checked_add(dur)?, offset(1));
616 /// assert_eq!(off.checked_add(-dur)?, offset(-21));
617 ///
618 /// // Any leftover time is truncated. That is, only
619 /// // whole seconds from the duration are considered.
620 /// let dur = Duration::new(3 * 60 * 60, 999_999_999);
621 /// assert_eq!(off.checked_add(dur)?, offset(-7));
622 ///
623 /// # Ok::<(), Box<dyn std::error::Error>>(())
624 /// ```
625 #[inline]
626 pub fn checked_add<A: Into<OffsetArithmetic>>(
627 self,
628 duration: A,
629 ) -> Result<Offset, Error> {
630 let duration: OffsetArithmetic = duration.into();
631 duration.checked_add(self)
632 }
633
634 #[inline]
635 fn checked_add_span(self, span: &Span) -> Result<Offset, Error> {
636 if let Some(err) = span.smallest_non_time_non_zero_unit_error() {
637 return Err(err);
638 }
639
640 let span = b::OffsetTotalSeconds::check(
641 span.to_invariant_duration().as_secs(),
642 )?;
643 // No overflow is possible here because even `Offset::MIN +
644 // Offset::MIN` fits into an `i32`. And note that the number of seconds
645 // in the span is limited to the range supported by `Offset`.
646 Offset::from_seconds(span + self.seconds())
647 }
648
649 #[inline]
650 fn checked_add_duration(
651 self,
652 duration: SignedDuration,
653 ) -> Result<Offset, Error> {
654 let duration = b::OffsetTotalSeconds::check(duration.as_secs())
655 .context(E::OverflowAddSignedDuration)?;
656 Offset::from_seconds(duration + self.seconds())
657 }
658
659 /// This routine is identical to [`Offset::checked_add`] with the duration
660 /// negated.
661 ///
662 /// # Errors
663 ///
664 /// This has the same error conditions as [`Offset::checked_add`].
665 ///
666 /// # Example
667 ///
668 /// ```
669 /// use std::time::Duration;
670 ///
671 /// use jiff::{tz, SignedDuration, ToSpan};
672 ///
673 /// let off = tz::offset(-4);
674 /// assert_eq!(
675 /// off.checked_sub(1.hours())?,
676 /// tz::offset(-5),
677 /// );
678 /// assert_eq!(
679 /// off.checked_sub(SignedDuration::from_hours(1))?,
680 /// tz::offset(-5),
681 /// );
682 /// assert_eq!(
683 /// off.checked_sub(Duration::from_secs(60 * 60))?,
684 /// tz::offset(-5),
685 /// );
686 ///
687 /// # Ok::<(), Box<dyn std::error::Error>>(())
688 /// ```
689 #[inline]
690 pub fn checked_sub<A: Into<OffsetArithmetic>>(
691 self,
692 duration: A,
693 ) -> Result<Offset, Error> {
694 let duration: OffsetArithmetic = duration.into();
695 duration.checked_neg().and_then(|oa| oa.checked_add(self))
696 }
697
698 /// This routine is identical to [`Offset::checked_add`], except the
699 /// result saturates on overflow. That is, instead of overflow, either
700 /// [`Offset::MIN`] or [`Offset::MAX`] is returned.
701 ///
702 /// # Example
703 ///
704 /// This example shows some cases where saturation will occur.
705 ///
706 /// ```
707 /// use jiff::{tz::Offset, SignedDuration, ToSpan};
708 ///
709 /// // Adding units above 'day' always results in saturation.
710 /// assert_eq!(Offset::UTC.saturating_add(1.weeks()), Offset::MAX);
711 /// assert_eq!(Offset::UTC.saturating_add(1.months()), Offset::MAX);
712 /// assert_eq!(Offset::UTC.saturating_add(1.years()), Offset::MAX);
713 ///
714 /// // Adding even 1 second to the max, or subtracting 1 from the min,
715 /// // will result in saturationg.
716 /// assert_eq!(Offset::MIN.saturating_add(-1.seconds()), Offset::MIN);
717 /// assert_eq!(Offset::MAX.saturating_add(1.seconds()), Offset::MAX);
718 ///
719 /// // Adding absolute durations also saturates as expected.
720 /// assert_eq!(Offset::UTC.saturating_add(SignedDuration::MAX), Offset::MAX);
721 /// assert_eq!(Offset::UTC.saturating_add(SignedDuration::MIN), Offset::MIN);
722 /// assert_eq!(Offset::UTC.saturating_add(std::time::Duration::MAX), Offset::MAX);
723 /// ```
724 #[inline]
725 pub fn saturating_add<A: Into<OffsetArithmetic>>(
726 self,
727 duration: A,
728 ) -> Offset {
729 let duration: OffsetArithmetic = duration.into();
730 self.checked_add(duration).unwrap_or_else(|_| {
731 if duration.is_negative() {
732 Offset::MIN
733 } else {
734 Offset::MAX
735 }
736 })
737 }
738
739 /// This routine is identical to [`Offset::saturating_add`] with the span
740 /// parameter negated.
741 ///
742 /// # Example
743 ///
744 /// This example shows some cases where saturation will occur.
745 ///
746 /// ```
747 /// use jiff::{tz::Offset, SignedDuration, ToSpan};
748 ///
749 /// // Adding units above 'day' always results in saturation.
750 /// assert_eq!(Offset::UTC.saturating_sub(1.weeks()), Offset::MIN);
751 /// assert_eq!(Offset::UTC.saturating_sub(1.months()), Offset::MIN);
752 /// assert_eq!(Offset::UTC.saturating_sub(1.years()), Offset::MIN);
753 ///
754 /// // Adding even 1 second to the max, or subtracting 1 from the min,
755 /// // will result in saturationg.
756 /// assert_eq!(Offset::MIN.saturating_sub(1.seconds()), Offset::MIN);
757 /// assert_eq!(Offset::MAX.saturating_sub(-1.seconds()), Offset::MAX);
758 ///
759 /// // Adding absolute durations also saturates as expected.
760 /// assert_eq!(Offset::UTC.saturating_sub(SignedDuration::MAX), Offset::MIN);
761 /// assert_eq!(Offset::UTC.saturating_sub(SignedDuration::MIN), Offset::MAX);
762 /// assert_eq!(Offset::UTC.saturating_sub(std::time::Duration::MAX), Offset::MIN);
763 /// ```
764 #[inline]
765 pub fn saturating_sub<A: Into<OffsetArithmetic>>(
766 self,
767 duration: A,
768 ) -> Offset {
769 let duration: OffsetArithmetic = duration.into();
770 let Ok(duration) = duration.checked_neg() else { return Offset::MIN };
771 self.saturating_add(duration)
772 }
773
774 /// Returns the span of time from this offset until the other given.
775 ///
776 /// When the `other` offset is more west (i.e., more negative) of the prime
777 /// meridian than this offset, then the span returned will be negative.
778 ///
779 /// # Properties
780 ///
781 /// Adding the span returned to this offset will always equal the `other`
782 /// offset given.
783 ///
784 /// # Examples
785 ///
786 /// ```
787 /// use jiff::{tz, ToSpan};
788 ///
789 /// assert_eq!(
790 /// tz::offset(-5).until(tz::Offset::UTC),
791 /// (5 * 60 * 60).seconds().fieldwise(),
792 /// );
793 /// // Flipping the operands in this case results in a negative span.
794 /// assert_eq!(
795 /// tz::Offset::UTC.until(tz::offset(-5)),
796 /// -(5 * 60 * 60).seconds().fieldwise(),
797 /// );
798 /// // The maximum span you can get:
799 /// assert_eq!(
800 /// tz::Offset::MIN.until(tz::Offset::MAX),
801 /// 187_198.seconds().fieldwise(),
802 /// );
803 /// ```
804 #[inline]
805 pub fn until(self, other: Offset) -> Span {
806 // OK because `Offset::MIN - Offset::MAX` will
807 // never overflow `i32`.
808 let diff = other.seconds() - self.seconds();
809 Span::new().seconds(diff)
810 }
811
812 /// Returns the span of time since the other offset given from this offset.
813 ///
814 /// When the `other` is more east (i.e., more positive) of the prime
815 /// meridian than this offset, then the span returned will be negative.
816 ///
817 /// # Properties
818 ///
819 /// Adding the span returned to the `other` offset will always equal this
820 /// offset.
821 ///
822 /// # Examples
823 ///
824 /// ```
825 /// use jiff::{tz, ToSpan};
826 ///
827 /// assert_eq!(
828 /// tz::Offset::UTC.since(tz::offset(-5)),
829 /// (5 * 60 * 60).seconds().fieldwise(),
830 /// );
831 /// // Flipping the operands in this case results in a negative span.
832 /// assert_eq!(
833 /// tz::offset(-5).since(tz::Offset::UTC),
834 /// -(5 * 60 * 60).seconds().fieldwise(),
835 /// );
836 /// ```
837 #[inline]
838 pub fn since(self, other: Offset) -> Span {
839 self.until(other).negate()
840 }
841
842 /// Returns an absolute duration representing the difference in time from
843 /// this offset until the given `other` offset.
844 ///
845 /// When the `other` offset is more west (i.e., more negative) of the prime
846 /// meridian than this offset, then the duration returned will be negative.
847 ///
848 /// Unlike [`Offset::until`], this returns a duration corresponding to a
849 /// 96-bit integer of nanoseconds between two offsets.
850 ///
851 /// # When should I use this versus [`Offset::until`]?
852 ///
853 /// See the type documentation for [`SignedDuration`] for the section on
854 /// when one should use [`Span`] and when one should use `SignedDuration`.
855 /// In short, use `Span` (and therefore `Offset::until`) unless you have a
856 /// specific reason to do otherwise.
857 ///
858 /// # Examples
859 ///
860 /// ```
861 /// use jiff::{tz, SignedDuration};
862 ///
863 /// assert_eq!(
864 /// tz::offset(-5).duration_until(tz::Offset::UTC),
865 /// SignedDuration::from_hours(5),
866 /// );
867 /// // Flipping the operands in this case results in a negative span.
868 /// assert_eq!(
869 /// tz::Offset::UTC.duration_until(tz::offset(-5)),
870 /// SignedDuration::from_hours(-5),
871 /// );
872 /// ```
873 #[inline]
874 pub fn duration_until(self, other: Offset) -> SignedDuration {
875 SignedDuration::offset_until(self, other)
876 }
877
878 /// This routine is identical to [`Offset::duration_until`], but the order
879 /// of the parameters is flipped.
880 ///
881 /// # Examples
882 ///
883 /// ```
884 /// use jiff::{tz, SignedDuration};
885 ///
886 /// assert_eq!(
887 /// tz::Offset::UTC.duration_since(tz::offset(-5)),
888 /// SignedDuration::from_hours(5),
889 /// );
890 /// assert_eq!(
891 /// tz::offset(-5).duration_since(tz::Offset::UTC),
892 /// SignedDuration::from_hours(-5),
893 /// );
894 /// ```
895 #[inline]
896 pub fn duration_since(self, other: Offset) -> SignedDuration {
897 SignedDuration::offset_until(other, self)
898 }
899
900 /// Returns a new offset that is rounded according to the given
901 /// configuration.
902 ///
903 /// Rounding an offset has a number of parameters, all of which are
904 /// optional. When no parameters are given, then no rounding is done, and
905 /// the offset as given is returned. That is, it's a no-op.
906 ///
907 /// As is consistent with `Offset` itself, rounding only supports units of
908 /// hours, minutes or seconds. If any other unit is provided, then an error
909 /// is returned.
910 ///
911 /// The parameters are, in brief:
912 ///
913 /// * [`OffsetRound::smallest`] sets the smallest [`Unit`] that is allowed
914 /// to be non-zero in the offset returned. By default, it is set to
915 /// [`Unit::Second`], i.e., no rounding occurs. When the smallest unit is
916 /// set to something bigger than seconds, then the non-zero units in the
917 /// offset smaller than the smallest unit are used to determine how the
918 /// offset should be rounded. For example, rounding `+01:59` to the nearest
919 /// hour using the default rounding mode would produce `+02:00`.
920 /// * [`OffsetRound::mode`] determines how to handle the remainder
921 /// when rounding. The default is [`RoundMode::HalfExpand`], which
922 /// corresponds to how you were likely taught to round in school.
923 /// Alternative modes, like [`RoundMode::Trunc`], exist too. For example,
924 /// a truncating rounding of `+01:59` to the nearest hour would
925 /// produce `+01:00`.
926 /// * [`OffsetRound::increment`] sets the rounding granularity to
927 /// use for the configured smallest unit. For example, if the smallest unit
928 /// is minutes and the increment is `15`, then the offset returned will
929 /// always have its minute component set to a multiple of `15`.
930 ///
931 /// # Errors
932 ///
933 /// In general, there are two main ways for rounding to fail: an improper
934 /// configuration like trying to round an offset to the nearest unit other
935 /// than hours/minutes/seconds, or when overflow occurs. Overflow can occur
936 /// when the offset would exceed the minimum or maximum `Offset` values.
937 /// Typically, this can only realistically happen if the offset before
938 /// rounding is already close to its minimum or maximum value.
939 ///
940 /// # Example: rounding to the nearest multiple of 15 minutes
941 ///
942 /// Most time zone offsets fall on an hour boundary, but some fall on the
943 /// half-hour or even 15 minute boundary:
944 ///
945 /// ```
946 /// use jiff::{tz::Offset, Unit};
947 ///
948 /// let offset = Offset::from_seconds(-(44 * 60 + 30)).unwrap();
949 /// let rounded = offset.round((Unit::Minute, 15))?;
950 /// assert_eq!(rounded, Offset::from_seconds(-45 * 60).unwrap());
951 ///
952 /// # Ok::<(), Box<dyn std::error::Error>>(())
953 /// ```
954 ///
955 /// # Example: rounding can fail via overflow
956 ///
957 /// ```
958 /// use jiff::{tz::Offset, Unit};
959 ///
960 /// assert_eq!(Offset::MAX.to_string(), "+25:59:59");
961 /// assert_eq!(
962 /// Offset::MAX.round(Unit::Minute).unwrap_err().to_string(),
963 /// "rounding time zone offset resulted in a duration that overflows: \
964 /// parameter 'time zone offset total seconds' is not \
965 /// in the required range of -93599..=93599",
966 /// );
967 /// ```
968 #[inline]
969 pub fn round<R: Into<OffsetRound>>(
970 self,
971 options: R,
972 ) -> Result<Offset, Error> {
973 let options: OffsetRound = options.into();
974 options.round(self)
975 }
976}
977
978impl Offset {
979 /// This creates an `Offset` via hours/minutes/seconds components.
980 ///
981 /// Currently, it exists because it's convenient for use in tests.
982 ///
983 /// I originally wanted to expose this in the public API, but I couldn't
984 /// decide on how I wanted to treat signedness. There are a variety of
985 /// choices:
986 ///
987 /// * Require all values to be positive, and ask the caller to use
988 /// `-offset` to negate it.
989 /// * Require all values to have the same sign. If any differs, either
990 /// panic or return an error.
991 /// * If any have a negative sign, then behave as if all have a negative
992 /// sign.
993 /// * Permit any combination of sign and combine them correctly.
994 /// Similar to how `std::time::Duration::new(-1s, 1ns)` is turned into
995 /// `-999,999,999ns`.
996 ///
997 /// I think the last option is probably the right behavior, but also the
998 /// most annoying to implement. But if someone wants to take a crack at it,
999 /// a PR is welcome.
1000 #[cfg(test)]
1001 #[inline]
1002 pub(crate) const fn hms(hours: i8, minutes: i8, seconds: i8) -> Offset {
1003 let hours = constant::unwrapr!(
1004 b::OffsetHours::checkc(hours as i64),
1005 "invalid time zone offset hours",
1006 );
1007 let minutes = constant::unwrapr!(
1008 b::OffsetMinutes::checkc(minutes as i64),
1009 "invalid time zone offset minutes",
1010 );
1011 let seconds = constant::unwrapr!(
1012 b::OffsetSeconds::checkc(seconds as i64),
1013 "invalid time zone offset seconds",
1014 );
1015 let seconds = (hours as i32 * c::SECS_PER_HOUR_32)
1016 + (minutes as i32 * c::SECS_PER_MIN_32)
1017 + (seconds as i32);
1018 let inner =
1019 constant::unwrapr!(JOffset::from_seconds(seconds), "valid offset");
1020 Offset { inner }
1021 }
1022
1023 #[inline]
1024 pub(crate) fn part_hours(self) -> i8 {
1025 (self.seconds() / c::SECS_PER_HOUR_32) as i8
1026 }
1027
1028 #[inline]
1029 pub(crate) fn part_minutes(self) -> i8 {
1030 ((self.seconds() / c::SECS_PER_MIN_32) % c::MINS_PER_HOUR_32) as i8
1031 }
1032
1033 #[inline]
1034 pub(crate) fn part_seconds(self) -> i8 {
1035 (self.seconds() % c::SECS_PER_MIN_32) as i8
1036 }
1037
1038 #[inline]
1039 pub(crate) const fn from_jcore(offset: JOffset) -> Offset {
1040 Offset { inner: offset }
1041 }
1042
1043 #[inline]
1044 pub(crate) const fn from_seconds_unchecked(seconds: i32) -> Offset {
1045 // TODO: Benchmark whether the check here is hurting us. If it is,
1046 // then we'll need a safety boundary in jiff-core to support this
1047 // operation.
1048 let inner =
1049 constant::unwrapr!(JOffset::from_seconds(seconds), "valid offset");
1050 Offset { inner }
1051 }
1052
1053 #[inline]
1054 pub(crate) fn to_abbreviation(&self) -> jcore::tz::Abbreviation {
1055 use core::fmt::Write;
1056
1057 let mut dst = jcore::util::ArrayStr::<9>::new("").unwrap();
1058 // OK because the string representation of an offset
1059 // can never exceed 9 bytes. The longest possible, e.g.,
1060 // is `-25:59:59`.
1061 write!(&mut dst, "{}", self).unwrap();
1062 // The correctness argument here is unfortunately convuleted. In
1063 // environments with `alloc`, this will always succeed because the
1064 // heap is used as a fallback. But in core-only environments, the
1065 // abbreviation capacity is specifically set to `9` in jiff-core to
1066 // accommodate this use case. Thus, this can never fail.
1067 jcore::tz::Abbreviation::new(dst.as_str())
1068 .expect("`Abbreviation` capacity is big enough")
1069 }
1070
1071 /// Round this offset to the nearest minute and returns the hour/minute
1072 /// components as unsigned integers.
1073 ///
1074 /// Generally speaking, the second component on an offset is always zero.
1075 /// There are _some_ cases in the tzdb where this isn't true (like
1076 /// `Africa/Monrovia` before `1972-01-07`), but virtually all time zones
1077 /// use offsets with whole hours. Some go to whole minutes. The only other
1078 /// way to get non-zero seconds is to explicitly use a fixed offset.
1079 ///
1080 /// A pathological case is the minimum or maximum offset. In this case,
1081 /// truncation is used instead of rounding to the nearest whole minute.
1082 #[inline]
1083 pub(crate) fn round_to_nearest_minute(self) -> (u8, u8) {
1084 #[inline(never)]
1085 #[cold]
1086 fn round(mut hours: u8, mut minutes: u8) -> (u8, u8) {
1087 const MAX_HOURS: u8 = b::OffsetHours::MAX.unsigned_abs();
1088 const MAX_MINS: u8 = b::OffsetMinutes::MAX.unsigned_abs();
1089
1090 if minutes == 59 {
1091 hours += 1;
1092 minutes = 0;
1093 // An edge case: if rounding results in an offset beyond
1094 // Jiff's boundaries, then we truncate to the max (or min)
1095 // offset supported.
1096 if hours > MAX_HOURS {
1097 hours = MAX_HOURS;
1098 minutes = MAX_MINS;
1099 }
1100 } else {
1101 minutes += 1;
1102 }
1103 (hours, minutes)
1104 }
1105
1106 let total_seconds = self.seconds().unsigned_abs();
1107 let hours = (total_seconds / (60 * 60)) as u8;
1108 let minutes = ((total_seconds / 60) % 60) as u8;
1109 let seconds = (total_seconds % 60) as u8;
1110
1111 // RFCs 2822, 3339 and 9557 require that time zone offsets are an
1112 // integral number of minutes. While rounding based on seconds doesn't
1113 // seem clearly indicated, the `1937-01-01T12:00:27.87+00:20` example
1114 // in RFC 3339 seems to suggest that the number of minutes should be
1115 // "as close as possible" to the actual offset. So we just do basic
1116 // rounding here.
1117 if seconds >= 30 {
1118 return round(hours, minutes);
1119 }
1120 (hours, minutes)
1121 }
1122}
1123
1124impl core::fmt::Debug for Offset {
1125 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
1126 let sign = if self.is_negative() { "-" } else { "" };
1127 write!(
1128 f,
1129 "{sign}{:02}:{:02}:{:02}",
1130 self.part_hours().unsigned_abs(),
1131 self.part_minutes().unsigned_abs(),
1132 self.part_seconds().unsigned_abs(),
1133 )
1134 }
1135}
1136
1137impl core::fmt::Display for Offset {
1138 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
1139 let sign = if self.is_negative() { "-" } else { "+" };
1140 let hours = self.part_hours().unsigned_abs();
1141 let minutes = self.part_minutes().unsigned_abs();
1142 let seconds = self.part_seconds().unsigned_abs();
1143 if hours == 0 && minutes == 0 && seconds == 0 {
1144 f.write_str("+00")
1145 } else if hours != 0 && minutes == 0 && seconds == 0 {
1146 write!(f, "{sign}{hours:02}")
1147 } else if minutes != 0 && seconds == 0 {
1148 write!(f, "{sign}{hours:02}:{minutes:02}")
1149 } else {
1150 write!(f, "{sign}{hours:02}:{minutes:02}:{seconds:02}")
1151 }
1152 }
1153}
1154
1155/// Adds a span of time to an offset. This panics on overflow.
1156///
1157/// For checked arithmetic, see [`Offset::checked_add`].
1158impl Add<Span> for Offset {
1159 type Output = Offset;
1160
1161 #[inline]
1162 fn add(self, rhs: Span) -> Offset {
1163 self.checked_add(rhs)
1164 .expect("adding span to offset should not overflow")
1165 }
1166}
1167
1168/// Adds a span of time to an offset in place. This panics on overflow.
1169///
1170/// For checked arithmetic, see [`Offset::checked_add`].
1171impl AddAssign<Span> for Offset {
1172 #[inline]
1173 fn add_assign(&mut self, rhs: Span) {
1174 *self = self.add(rhs);
1175 }
1176}
1177
1178/// Subtracts a span of time from an offset. This panics on overflow.
1179///
1180/// For checked arithmetic, see [`Offset::checked_sub`].
1181impl Sub<Span> for Offset {
1182 type Output = Offset;
1183
1184 #[inline]
1185 fn sub(self, rhs: Span) -> Offset {
1186 self.checked_sub(rhs)
1187 .expect("subtracting span from offsetsshould not overflow")
1188 }
1189}
1190
1191/// Subtracts a span of time from an offset in place. This panics on overflow.
1192///
1193/// For checked arithmetic, see [`Offset::checked_sub`].
1194impl SubAssign<Span> for Offset {
1195 #[inline]
1196 fn sub_assign(&mut self, rhs: Span) {
1197 *self = self.sub(rhs);
1198 }
1199}
1200
1201/// Computes the span of time between two offsets.
1202///
1203/// This will return a negative span when the offset being subtracted is
1204/// greater (i.e., more east with respect to the prime meridian).
1205impl Sub for Offset {
1206 type Output = Span;
1207
1208 #[inline]
1209 fn sub(self, rhs: Offset) -> Span {
1210 self.since(rhs)
1211 }
1212}
1213
1214/// Adds a signed duration of time to an offset. This panics on overflow.
1215///
1216/// For checked arithmetic, see [`Offset::checked_add`].
1217impl Add<SignedDuration> for Offset {
1218 type Output = Offset;
1219
1220 #[inline]
1221 fn add(self, rhs: SignedDuration) -> Offset {
1222 self.checked_add(rhs)
1223 .expect("adding signed duration to offset should not overflow")
1224 }
1225}
1226
1227/// Adds a signed duration of time to an offset in place. This panics on
1228/// overflow.
1229///
1230/// For checked arithmetic, see [`Offset::checked_add`].
1231impl AddAssign<SignedDuration> for Offset {
1232 #[inline]
1233 fn add_assign(&mut self, rhs: SignedDuration) {
1234 *self = self.add(rhs);
1235 }
1236}
1237
1238/// Subtracts a signed duration of time from an offset. This panics on
1239/// overflow.
1240///
1241/// For checked arithmetic, see [`Offset::checked_sub`].
1242impl Sub<SignedDuration> for Offset {
1243 type Output = Offset;
1244
1245 #[inline]
1246 fn sub(self, rhs: SignedDuration) -> Offset {
1247 self.checked_sub(rhs).expect(
1248 "subtracting signed duration from offsetsshould not overflow",
1249 )
1250 }
1251}
1252
1253/// Subtracts a signed duration of time from an offset in place. This panics on
1254/// overflow.
1255///
1256/// For checked arithmetic, see [`Offset::checked_sub`].
1257impl SubAssign<SignedDuration> for Offset {
1258 #[inline]
1259 fn sub_assign(&mut self, rhs: SignedDuration) {
1260 *self = self.sub(rhs);
1261 }
1262}
1263
1264/// Adds an unsigned duration of time to an offset. This panics on overflow.
1265///
1266/// For checked arithmetic, see [`Offset::checked_add`].
1267impl Add<UnsignedDuration> for Offset {
1268 type Output = Offset;
1269
1270 #[inline]
1271 fn add(self, rhs: UnsignedDuration) -> Offset {
1272 self.checked_add(rhs)
1273 .expect("adding unsigned duration to offset should not overflow")
1274 }
1275}
1276
1277/// Adds an unsigned duration of time to an offset in place. This panics on
1278/// overflow.
1279///
1280/// For checked arithmetic, see [`Offset::checked_add`].
1281impl AddAssign<UnsignedDuration> for Offset {
1282 #[inline]
1283 fn add_assign(&mut self, rhs: UnsignedDuration) {
1284 *self = self.add(rhs);
1285 }
1286}
1287
1288/// Subtracts an unsigned duration of time from an offset. This panics on
1289/// overflow.
1290///
1291/// For checked arithmetic, see [`Offset::checked_sub`].
1292impl Sub<UnsignedDuration> for Offset {
1293 type Output = Offset;
1294
1295 #[inline]
1296 fn sub(self, rhs: UnsignedDuration) -> Offset {
1297 self.checked_sub(rhs).expect(
1298 "subtracting unsigned duration from offsetsshould not overflow",
1299 )
1300 }
1301}
1302
1303/// Subtracts an unsigned duration of time from an offset in place. This panics
1304/// on overflow.
1305///
1306/// For checked arithmetic, see [`Offset::checked_sub`].
1307impl SubAssign<UnsignedDuration> for Offset {
1308 #[inline]
1309 fn sub_assign(&mut self, rhs: UnsignedDuration) {
1310 *self = self.sub(rhs);
1311 }
1312}
1313
1314/// Negate this offset.
1315///
1316/// A positive offset becomes negative and vice versa. This is a no-op for the
1317/// zero offset.
1318///
1319/// This never panics.
1320impl Neg for Offset {
1321 type Output = Offset;
1322
1323 #[inline]
1324 fn neg(self) -> Offset {
1325 self.negate()
1326 }
1327}
1328
1329/// Converts a `SignedDuration` to a time zone offset.
1330///
1331/// If the signed duration has fractional seconds, then it is automatically
1332/// rounded to the nearest second. (Because an `Offset` has only second
1333/// precision.)
1334///
1335/// # Errors
1336///
1337/// This returns an error if the duration overflows the limits of an `Offset`.
1338///
1339/// # Example
1340///
1341/// ```
1342/// use jiff::{tz::{self, Offset}, SignedDuration};
1343///
1344/// let sdur = SignedDuration::from_secs(-5 * 60 * 60);
1345/// let offset = Offset::try_from(sdur)?;
1346/// assert_eq!(offset, tz::offset(-5));
1347///
1348/// // Sub-seconds results in rounded.
1349/// let sdur = SignedDuration::new(-5 * 60 * 60, -500_000_000);
1350/// let offset = Offset::try_from(sdur)?;
1351/// assert_eq!(offset, tz::Offset::from_seconds(-(5 * 60 * 60 + 1)).unwrap());
1352///
1353/// # Ok::<(), Box<dyn std::error::Error>>(())
1354/// ```
1355impl TryFrom<SignedDuration> for Offset {
1356 type Error = Error;
1357
1358 fn try_from(sdur: SignedDuration) -> Result<Offset, Error> {
1359 let mut seconds = sdur.as_secs();
1360 let subsec = sdur.subsec_nanos();
1361 if subsec >= 500_000_000 {
1362 seconds = seconds.saturating_add(1);
1363 } else if subsec <= -500_000_000 {
1364 seconds = seconds.saturating_sub(1);
1365 }
1366 let seconds =
1367 i32::try_from(seconds).map_err(|_| E::OverflowSignedDuration)?;
1368 Offset::from_seconds(seconds)
1369 .map_err(|_| Error::from(E::OverflowSignedDuration))
1370 }
1371}
1372
1373#[cfg(feature = "defmt")]
1374impl defmt::Format for Offset {
1375 fn format(&self, f: defmt::Formatter) {
1376 let sign = if self.is_negative() { "-" } else { "" };
1377 defmt::write!(
1378 f,
1379 "{=str}{=u8:02}:{=u8:02}:{=u8:02}",
1380 sign,
1381 self.part_hours().unsigned_abs(),
1382 self.part_minutes().unsigned_abs(),
1383 self.part_seconds().unsigned_abs(),
1384 )
1385 }
1386}
1387
1388/// Options for [`Offset::checked_add`] and [`Offset::checked_sub`].
1389///
1390/// This type provides a way to ergonomically add one of a few different
1391/// duration types to a [`Offset`].
1392///
1393/// The main way to construct values of this type is with its `From` trait
1394/// implementations:
1395///
1396/// * `From<Span> for OffsetArithmetic` adds (or subtracts) the given span to
1397/// the receiver offset.
1398/// * `From<SignedDuration> for OffsetArithmetic` adds (or subtracts)
1399/// the given signed duration to the receiver offset.
1400/// * `From<std::time::Duration> for OffsetArithmetic` adds (or subtracts)
1401/// the given unsigned duration to the receiver offset.
1402///
1403/// # Example
1404///
1405/// ```
1406/// use std::time::Duration;
1407///
1408/// use jiff::{tz::offset, SignedDuration, ToSpan};
1409///
1410/// let off = offset(-10);
1411/// assert_eq!(off.checked_add(11.hours())?, offset(1));
1412/// assert_eq!(off.checked_add(SignedDuration::from_hours(11))?, offset(1));
1413/// assert_eq!(off.checked_add(Duration::from_secs(11 * 60 * 60))?, offset(1));
1414///
1415/// # Ok::<(), Box<dyn std::error::Error>>(())
1416/// ```
1417#[derive(Clone, Copy, Debug)]
1418pub struct OffsetArithmetic {
1419 duration: Duration,
1420}
1421
1422impl OffsetArithmetic {
1423 #[inline]
1424 fn checked_add(self, offset: Offset) -> Result<Offset, Error> {
1425 match self.duration.to_signed()? {
1426 SDuration::Span(span) => offset.checked_add_span(span),
1427 SDuration::Absolute(sdur) => offset.checked_add_duration(sdur),
1428 }
1429 }
1430
1431 #[inline]
1432 fn checked_neg(self) -> Result<OffsetArithmetic, Error> {
1433 let duration = self.duration.checked_neg()?;
1434 Ok(OffsetArithmetic { duration })
1435 }
1436
1437 #[inline]
1438 fn is_negative(&self) -> bool {
1439 self.duration.is_negative()
1440 }
1441}
1442
1443impl From<Span> for OffsetArithmetic {
1444 fn from(span: Span) -> OffsetArithmetic {
1445 let duration = Duration::from(span);
1446 OffsetArithmetic { duration }
1447 }
1448}
1449
1450impl From<SignedDuration> for OffsetArithmetic {
1451 fn from(sdur: SignedDuration) -> OffsetArithmetic {
1452 let duration = Duration::from(sdur);
1453 OffsetArithmetic { duration }
1454 }
1455}
1456
1457impl From<UnsignedDuration> for OffsetArithmetic {
1458 fn from(udur: UnsignedDuration) -> OffsetArithmetic {
1459 let duration = Duration::from(udur);
1460 OffsetArithmetic { duration }
1461 }
1462}
1463
1464impl<'a> From<&'a Span> for OffsetArithmetic {
1465 fn from(span: &'a Span) -> OffsetArithmetic {
1466 OffsetArithmetic::from(*span)
1467 }
1468}
1469
1470impl<'a> From<&'a SignedDuration> for OffsetArithmetic {
1471 fn from(sdur: &'a SignedDuration) -> OffsetArithmetic {
1472 OffsetArithmetic::from(*sdur)
1473 }
1474}
1475
1476impl<'a> From<&'a UnsignedDuration> for OffsetArithmetic {
1477 fn from(udur: &'a UnsignedDuration) -> OffsetArithmetic {
1478 OffsetArithmetic::from(*udur)
1479 }
1480}
1481
1482/// Options for [`Offset::round`].
1483///
1484/// This type provides a way to configure the rounding of an offset. This
1485/// includes setting the smallest unit (i.e., the unit to round), the rounding
1486/// increment and the rounding mode (e.g., "ceil" or "truncate").
1487///
1488/// [`Offset::round`] accepts anything that implements
1489/// `Into<OffsetRound>`. There are a few key trait implementations that
1490/// make this convenient:
1491///
1492/// * `From<Unit> for OffsetRound` will construct a rounding
1493/// configuration where the smallest unit is set to the one given.
1494/// * `From<(Unit, i64)> for OffsetRound` will construct a rounding
1495/// configuration where the smallest unit and the rounding increment are set to
1496/// the ones given.
1497///
1498/// In order to set other options (like the rounding mode), one must explicitly
1499/// create a `OffsetRound` and pass it to `Offset::round`.
1500///
1501/// # Example
1502///
1503/// This example shows how to always round up to the nearest half-hour:
1504///
1505/// ```
1506/// use jiff::{tz::{Offset, OffsetRound}, RoundMode, Unit};
1507///
1508/// let offset = Offset::from_seconds(4 * 60 * 60 + 17 * 60).unwrap();
1509/// let rounded = offset.round(
1510/// OffsetRound::new()
1511/// .smallest(Unit::Minute)
1512/// .increment(30)
1513/// .mode(RoundMode::Expand),
1514/// )?;
1515/// assert_eq!(rounded, Offset::from_seconds(4 * 60 * 60 + 30 * 60).unwrap());
1516///
1517/// # Ok::<(), Box<dyn std::error::Error>>(())
1518/// ```
1519#[derive(Clone, Copy, Debug)]
1520pub struct OffsetRound {
1521 smallest: Unit,
1522 mode: RoundMode,
1523 increment: i64,
1524}
1525
1526impl OffsetRound {
1527 /// Create a new default configuration for rounding a time zone offset via
1528 /// [`Offset::round`].
1529 ///
1530 /// The default configuration does no rounding.
1531 #[inline]
1532 pub fn new() -> OffsetRound {
1533 OffsetRound {
1534 smallest: Unit::Second,
1535 mode: RoundMode::HalfExpand,
1536 increment: 1,
1537 }
1538 }
1539
1540 /// Set the smallest units allowed in the offset returned. These are the
1541 /// units that the offset is rounded to.
1542 ///
1543 /// # Errors
1544 ///
1545 /// The unit must be [`Unit::Hour`], [`Unit::Minute`] or [`Unit::Second`].
1546 ///
1547 /// # Example
1548 ///
1549 /// A basic example that rounds to the nearest minute:
1550 ///
1551 /// ```
1552 /// use jiff::{tz::Offset, Unit};
1553 ///
1554 /// let offset = Offset::from_seconds(-(5 * 60 * 60 + 30)).unwrap();
1555 /// assert_eq!(offset.round(Unit::Hour)?, Offset::from_hours(-5).unwrap());
1556 ///
1557 /// # Ok::<(), Box<dyn std::error::Error>>(())
1558 /// ```
1559 #[inline]
1560 pub fn smallest(self, unit: Unit) -> OffsetRound {
1561 OffsetRound { smallest: unit, ..self }
1562 }
1563
1564 /// Set the rounding mode.
1565 ///
1566 /// This defaults to [`RoundMode::HalfExpand`], which makes rounding work
1567 /// like how you were taught in school.
1568 ///
1569 /// # Example
1570 ///
1571 /// A basic example that rounds to the nearest hour, but changing its
1572 /// rounding mode to truncation:
1573 ///
1574 /// ```
1575 /// use jiff::{tz::{Offset, OffsetRound}, RoundMode, Unit};
1576 ///
1577 /// let offset = Offset::from_seconds(-(5 * 60 * 60 + 30 * 60)).unwrap();
1578 /// assert_eq!(
1579 /// offset.round(OffsetRound::new()
1580 /// .smallest(Unit::Hour)
1581 /// .mode(RoundMode::Trunc),
1582 /// )?,
1583 /// // The default round mode does rounding like
1584 /// // how you probably learned in school, and would
1585 /// // result in rounding to -6 hours. But we
1586 /// // change it to truncation here, which makes it
1587 /// // round -5.
1588 /// Offset::from_hours(-5).unwrap(),
1589 /// );
1590 ///
1591 /// # Ok::<(), Box<dyn std::error::Error>>(())
1592 /// ```
1593 #[inline]
1594 pub fn mode(self, mode: RoundMode) -> OffsetRound {
1595 OffsetRound { mode, ..self }
1596 }
1597
1598 /// Set the rounding increment for the smallest unit.
1599 ///
1600 /// The default value is `1`. Other values permit rounding the smallest
1601 /// unit to the nearest integer increment specified. For example, if the
1602 /// smallest unit is set to [`Unit::Minute`], then a rounding increment of
1603 /// `30` would result in rounding in increments of a half hour. That is,
1604 /// the only minute value that could result would be `0` or `30`.
1605 ///
1606 /// # Errors
1607 ///
1608 /// Unlike rounding a [`Span`](crate::Span), the increment does not need to
1609 /// divide evenly into the next largest unit. Callers can round an offset
1610 /// to any increment value so long as it is greater than zero and less than
1611 /// or equal to `1_000_000_000`.
1612 ///
1613 /// # Example
1614 ///
1615 /// This shows how to round an offset to the nearest 30 minute increment:
1616 ///
1617 /// ```
1618 /// use jiff::{tz::Offset, Unit};
1619 ///
1620 /// let offset = Offset::from_seconds(4 * 60 * 60 + 15 * 60).unwrap();
1621 /// assert_eq!(
1622 /// offset.round((Unit::Minute, 30))?,
1623 /// Offset::from_seconds(4 * 60 * 60 + 30 * 60).unwrap(),
1624 /// );
1625 ///
1626 /// # Ok::<(), Box<dyn std::error::Error>>(())
1627 /// ```
1628 #[inline]
1629 pub fn increment(self, increment: i64) -> OffsetRound {
1630 OffsetRound { increment, ..self }
1631 }
1632
1633 /// Does the actual offset rounding.
1634 fn round(&self, offset: Offset) -> Result<Offset, Error> {
1635 let increment = Increment::for_offset(self.smallest, self.increment)?;
1636 // let rounded_sdur = SignedDuration::from(offset).round(self.0)?;
1637 let rounded = increment
1638 .round(self.mode, SignedDuration::from(offset))
1639 .context(E::RoundOverflow)?;
1640 Offset::try_from(rounded)
1641 .map_err(|_| b::OffsetTotalSeconds::error())
1642 .context(E::RoundOverflow)
1643 }
1644}
1645
1646impl Default for OffsetRound {
1647 fn default() -> OffsetRound {
1648 OffsetRound::new()
1649 }
1650}
1651
1652impl From<Unit> for OffsetRound {
1653 fn from(unit: Unit) -> OffsetRound {
1654 OffsetRound::default().smallest(unit)
1655 }
1656}
1657
1658impl From<(Unit, i64)> for OffsetRound {
1659 fn from((unit, increment): (Unit, i64)) -> OffsetRound {
1660 OffsetRound::default().smallest(unit).increment(increment)
1661 }
1662}
1663
1664/// Configuration for resolving disparities between an offset and a time zone.
1665///
1666/// A conflict between an offset and a time zone most commonly appears in a
1667/// datetime string. For example, `2024-06-14T17:30-05[America/New_York]`
1668/// has a definitive inconsistency between the reported offset (`-05`) and
1669/// the time zone (`America/New_York`), because at this time in New York,
1670/// daylight saving time (DST) was in effect. In New York in the year 2024,
1671/// DST corresponded to the UTC offset `-04`.
1672///
1673/// Other conflict variations exist. For example, in 2019, Brazil abolished
1674/// DST completely. But if one were to create a datetime for 2020 in 2018, that
1675/// datetime in 2020 would reflect the DST rules as they exist in 2018. That
1676/// could in turn result in a datetime with an offset that is incorrect with
1677/// respect to the rules in 2019.
1678///
1679/// For this reason, this crate exposes a few ways of resolving these
1680/// conflicts. It is most commonly used as configuration for parsing
1681/// [`Zoned`](crate::Zoned) values via
1682/// [`fmt::temporal::DateTimeParser::offset_conflict`](crate::fmt::temporal::DateTimeParser::offset_conflict). But this configuration can also be used directly via
1683/// [`OffsetConflict::resolve`].
1684///
1685/// The default value is `OffsetConflict::Reject`, which results in an
1686/// error being returned if the offset and a time zone are not in agreement.
1687/// This is the default so that Jiff does not automatically make silent choices
1688/// about whether to prefer the time zone or the offset. The
1689/// [`fmt::temporal::DateTimeParser::parse_zoned_with`](crate::fmt::temporal::DateTimeParser::parse_zoned_with)
1690/// documentation shows an example demonstrating its utility in the face
1691/// of changes in the law, such as the abolition of daylight saving time.
1692/// By rejecting such things, one can ensure that the original timestamp is
1693/// preserved or else an error occurs.
1694///
1695/// This enum is non-exhaustive so that other forms of offset conflicts may be
1696/// added in semver compatible releases.
1697///
1698/// # Example
1699///
1700/// This example shows how to always use the time zone even if the offset is
1701/// wrong.
1702///
1703/// ```
1704/// use jiff::{civil::date, tz};
1705///
1706/// let dt = date(2024, 6, 14).at(17, 30, 0, 0);
1707/// let offset = tz::offset(-5); // wrong! should be -4
1708/// let newyork = tz::db().get("America/New_York")?;
1709///
1710/// // The default conflict resolution, 'Reject', will error.
1711/// let result = tz::OffsetConflict::Reject
1712/// .resolve(dt, offset, newyork.clone());
1713/// assert!(result.is_err());
1714///
1715/// // But we can change it to always prefer the time zone.
1716/// let zdt = tz::OffsetConflict::AlwaysTimeZone
1717/// .resolve(dt, offset, newyork.clone())?
1718/// .unambiguous()?;
1719/// assert_eq!(zdt.datetime(), date(2024, 6, 14).at(17, 30, 0, 0));
1720/// // The offset has been corrected automatically.
1721/// assert_eq!(zdt.offset(), tz::offset(-4));
1722///
1723/// # Ok::<(), Box<dyn std::error::Error>>(())
1724/// ```
1725///
1726/// # Example: parsing
1727///
1728/// This example shows how to set the offset conflict resolution configuration
1729/// while parsing a [`Zoned`](crate::Zoned) datetime. In this example, we
1730/// always prefer the offset, even if it conflicts with the time zone.
1731///
1732/// ```
1733/// use jiff::{civil::date, fmt::temporal::DateTimeParser, tz};
1734///
1735/// static PARSER: DateTimeParser = DateTimeParser::new()
1736/// .offset_conflict(tz::OffsetConflict::AlwaysOffset);
1737///
1738/// let zdt = PARSER.parse_zoned("2024-06-14T17:30-05[America/New_York]")?;
1739/// // The time *and* offset have been corrected. The offset given was invalid,
1740/// // so it cannot be kept, but the timestamp returned is equivalent to
1741/// // `2024-06-14T17:30-05`. It is just adjusted automatically to be correct
1742/// // in the `America/New_York` time zone.
1743/// assert_eq!(zdt.datetime(), date(2024, 6, 14).at(18, 30, 0, 0));
1744/// assert_eq!(zdt.offset(), tz::offset(-4));
1745///
1746/// # Ok::<(), Box<dyn std::error::Error>>(())
1747/// ```
1748#[derive(Clone, Copy, Debug, Default)]
1749#[non_exhaustive]
1750pub enum OffsetConflict {
1751 /// When the offset and time zone are in conflict, this will always use
1752 /// the offset to interpret the date time.
1753 ///
1754 /// When resolving to a [`AmbiguousZoned`], the time zone attached
1755 /// to the timestamp will still be the same as the time zone given. The
1756 /// difference here is that the offset will be adjusted such that it is
1757 /// correct for the given time zone. However, the timestamp itself will
1758 /// always match the datetime and offset given (and which is always
1759 /// unambiguous).
1760 ///
1761 /// Basically, you should use this option when you want to keep the exact
1762 /// time unchanged (as indicated by the datetime and offset), even if it
1763 /// means a change to civil time.
1764 AlwaysOffset,
1765 /// When the offset and time zone are in conflict, this will always use
1766 /// the time zone to interpret the date time.
1767 ///
1768 /// When resolving to an [`AmbiguousZoned`], the offset attached to the
1769 /// timestamp will always be determined by only looking at the time zone.
1770 /// This in turn implies that the timestamp returned could be ambiguous,
1771 /// since this conflict resolution strategy specifically ignores the
1772 /// offset. (And, we're only at this point because the offset is not
1773 /// possible for the given time zone, so it can't be used in concert with
1774 /// the time zone anyway.) This is unlike the `AlwaysOffset` strategy where
1775 /// the timestamp returned is guaranteed to be unambiguous.
1776 ///
1777 /// You should use this option when you want to keep the civil time
1778 /// unchanged even if it means a change to the exact time.
1779 AlwaysTimeZone,
1780 /// Always attempt to use the offset to resolve a datetime to a timestamp,
1781 /// unless the offset is invalid for the provided time zone. In that case,
1782 /// use the time zone. When the time zone is used, it's possible for an
1783 /// ambiguous datetime to be returned.
1784 ///
1785 /// See [`ZonedWith::offset_conflict`](crate::ZonedWith::offset_conflict)
1786 /// for an example of when this strategy is useful.
1787 PreferOffset,
1788 /// When the offset and time zone are in conflict, this strategy always
1789 /// results in conflict resolution returning an error.
1790 ///
1791 /// This is the default since a conflict between the offset and the time
1792 /// zone usually implies an invalid datetime in some way.
1793 #[default]
1794 Reject,
1795}
1796
1797impl OffsetConflict {
1798 /// Resolve a potential conflict between an [`Offset`] and a [`TimeZone`].
1799 ///
1800 /// # Errors
1801 ///
1802 /// This returns an error if this would have returned a timestamp outside
1803 /// of its minimum and maximum values.
1804 ///
1805 /// This can also return an error when using the [`OffsetConflict::Reject`]
1806 /// strategy. Namely, when using the `Reject` strategy, any offset that is
1807 /// not compatible with the given datetime and time zone will always result
1808 /// in an error.
1809 ///
1810 /// # Example
1811 ///
1812 /// This example shows how each of the different conflict resolution
1813 /// strategies are applied.
1814 ///
1815 /// ```
1816 /// use jiff::{civil::date, tz};
1817 ///
1818 /// let dt = date(2024, 6, 14).at(17, 30, 0, 0);
1819 /// let offset = tz::offset(-5); // wrong! should be -4
1820 /// let newyork = tz::db().get("America/New_York")?;
1821 ///
1822 /// // Here, we use the offset and ignore the time zone.
1823 /// let zdt = tz::OffsetConflict::AlwaysOffset
1824 /// .resolve(dt, offset, newyork.clone())?
1825 /// .unambiguous()?;
1826 /// // The datetime (and offset) have been corrected automatically
1827 /// // and the resulting Zoned instant corresponds precisely to
1828 /// // `2024-06-14T17:30-05[UTC]`.
1829 /// assert_eq!(zdt.to_string(), "2024-06-14T18:30:00-04:00[America/New_York]");
1830 ///
1831 /// // Here, we use the time zone and ignore the offset.
1832 /// let zdt = tz::OffsetConflict::AlwaysTimeZone
1833 /// .resolve(dt, offset, newyork.clone())?
1834 /// .unambiguous()?;
1835 /// // The offset has been corrected automatically and the resulting
1836 /// // Zoned instant corresponds precisely to `2024-06-14T17:30-04[UTC]`.
1837 /// // Notice how the civil time remains the same, but the exact instant
1838 /// // has changed!
1839 /// assert_eq!(zdt.to_string(), "2024-06-14T17:30:00-04:00[America/New_York]");
1840 ///
1841 /// // Here, we prefer the offset, but fall back to the time zone.
1842 /// // In this example, it has the same behavior as `AlwaysTimeZone`.
1843 /// let zdt = tz::OffsetConflict::PreferOffset
1844 /// .resolve(dt, offset, newyork.clone())?
1845 /// .unambiguous()?;
1846 /// assert_eq!(zdt.to_string(), "2024-06-14T17:30:00-04:00[America/New_York]");
1847 ///
1848 /// // The default conflict resolution, 'Reject', will error.
1849 /// let result = tz::OffsetConflict::Reject
1850 /// .resolve(dt, offset, newyork.clone());
1851 /// assert!(result.is_err());
1852 ///
1853 /// # Ok::<(), Box<dyn std::error::Error>>(())
1854 /// ```
1855 pub fn resolve(
1856 self,
1857 dt: civil::DateTime,
1858 offset: Offset,
1859 tz: TimeZone,
1860 ) -> Result<AmbiguousZoned, Error> {
1861 self.resolve_with(dt, offset, tz, |off1, off2| off1 == off2)
1862 }
1863
1864 /// Resolve a potential conflict between an [`Offset`] and a [`TimeZone`]
1865 /// using the given definition of equality for an `Offset`.
1866 ///
1867 /// The equality predicate is always given a pair of offsets where the
1868 /// first is the offset given to `resolve_with` and the second is the
1869 /// offset found in the `TimeZone`.
1870 ///
1871 /// # Errors
1872 ///
1873 /// This returns an error if this would have returned a timestamp outside
1874 /// of its minimum and maximum values.
1875 ///
1876 /// This can also return an error when using the [`OffsetConflict::Reject`]
1877 /// strategy. Namely, when using the `Reject` strategy, any offset that is
1878 /// not compatible with the given datetime and time zone will always result
1879 /// in an error.
1880 ///
1881 /// # Example
1882 ///
1883 /// Unlike [`OffsetConflict::resolve`], this routine permits overriding
1884 /// the definition of equality used for comparing offsets. In
1885 /// `OffsetConflict::resolve`, exact equality is used. This can be
1886 /// troublesome in some cases when a time zone has an offset with
1887 /// fractional minutes, such as `Africa/Monrovia` before 1972.
1888 ///
1889 /// Because RFC 3339 and RFC 9557 do not support time zone offsets
1890 /// with fractional minutes, Jiff will serialize offsets with
1891 /// fractional minutes by rounding to the nearest minute. This
1892 /// will result in a different offset than what is actually
1893 /// used in the time zone. Parsing this _should_ succeed, but
1894 /// if exact offset equality is used, it won't. This is why a
1895 /// [`fmt::temporal::DateTimeParser`](crate::fmt::temporal::DateTimeParser)
1896 /// uses this routine with offset equality that rounds offsets to the
1897 /// nearest minute before comparison.
1898 ///
1899 /// ```
1900 /// use jiff::{civil::date, tz::{Offset, OffsetConflict, TimeZone}, Unit};
1901 ///
1902 /// let dt = date(1968, 2, 1).at(23, 15, 0, 0);
1903 /// let offset = Offset::from_seconds(-(44 * 60 + 30)).unwrap();
1904 /// let zdt = dt.in_tz("Africa/Monrovia")?;
1905 /// assert_eq!(zdt.offset(), offset);
1906 /// // Notice that the offset has been rounded!
1907 /// assert_eq!(zdt.to_string(), "1968-02-01T23:15:00-00:45[Africa/Monrovia]");
1908 ///
1909 /// // Now imagine parsing extracts the civil datetime, the offset and
1910 /// // the time zone, and then naively does exact offset comparison:
1911 /// let tz = TimeZone::get("Africa/Monrovia")?;
1912 /// // This is the parsed offset, which won't precisely match the actual
1913 /// // offset used by `Africa/Monrovia` at this time.
1914 /// let offset = Offset::from_seconds(-45 * 60).unwrap();
1915 /// let result = OffsetConflict::Reject.resolve(dt, offset, tz.clone());
1916 /// assert_eq!(
1917 /// result.unwrap_err().to_string(),
1918 /// "datetime could not resolve to a timestamp since `reject` \
1919 /// conflict resolution was chosen, and because datetime has offset \
1920 /// `-00:45`, but the time zone `Africa/Monrovia` for the given \
1921 /// datetime unambiguously has offset `-00:44:30`",
1922 /// );
1923 /// let is_equal = |parsed: Offset, candidate: Offset| {
1924 /// parsed == candidate || candidate.round(Unit::Minute).map_or(
1925 /// parsed == candidate,
1926 /// |candidate| parsed == candidate,
1927 /// )
1928 /// };
1929 /// let zdt = OffsetConflict::Reject.resolve_with(
1930 /// dt,
1931 /// offset,
1932 /// tz.clone(),
1933 /// is_equal,
1934 /// )?.unambiguous()?;
1935 /// // Notice that the offset is the actual offset from the time zone:
1936 /// assert_eq!(zdt.offset(), Offset::from_seconds(-(44 * 60 + 30)).unwrap());
1937 /// // But when we serialize, the offset gets rounded. If we didn't
1938 /// // do this, we'd risk the datetime not being parsable by other
1939 /// // implementations since RFC 3339 and RFC 9557 don't support fractional
1940 /// // minutes in the offset.
1941 /// assert_eq!(zdt.to_string(), "1968-02-01T23:15:00-00:45[Africa/Monrovia]");
1942 ///
1943 /// # Ok::<(), Box<dyn std::error::Error>>(())
1944 /// ```
1945 ///
1946 /// And indeed, notice that parsing uses this same kind of offset equality
1947 /// to permit zoned datetimes whose offsets would be equivalent after
1948 /// rounding:
1949 ///
1950 /// ```
1951 /// use jiff::{tz::Offset, Zoned};
1952 ///
1953 /// let zdt: Zoned = "1968-02-01T23:15:00-00:45[Africa/Monrovia]".parse()?;
1954 /// // As above, notice that even though we parsed `-00:45` as the
1955 /// // offset, the actual offset of our zoned datetime is the correct
1956 /// // one from the time zone.
1957 /// assert_eq!(zdt.offset(), Offset::from_seconds(-(44 * 60 + 30)).unwrap());
1958 /// // And similarly, re-serializing it results in rounding the offset
1959 /// // again for compatibility with RFC 3339 and RFC 9557.
1960 /// assert_eq!(zdt.to_string(), "1968-02-01T23:15:00-00:45[Africa/Monrovia]");
1961 ///
1962 /// // And we also support parsing the actual fractional minute offset
1963 /// // as well:
1964 /// let zdt: Zoned = "1968-02-01T23:15:00-00:44:30[Africa/Monrovia]".parse()?;
1965 /// assert_eq!(zdt.offset(), Offset::from_seconds(-(44 * 60 + 30)).unwrap());
1966 /// assert_eq!(zdt.to_string(), "1968-02-01T23:15:00-00:45[Africa/Monrovia]");
1967 ///
1968 /// # Ok::<(), Box<dyn std::error::Error>>(())
1969 /// ```
1970 ///
1971 /// Rounding does not occur when the parsed offset itself contains
1972 /// sub-minute precision. In that case, exact equality is used:
1973 ///
1974 /// ```
1975 /// use jiff::Zoned;
1976 ///
1977 /// let result = "1970-06-01T00-00:45:00[Africa/Monrovia]".parse::<Zoned>();
1978 /// assert_eq!(
1979 /// result.unwrap_err().to_string(),
1980 /// "datetime could not resolve to a timestamp since `reject` \
1981 /// conflict resolution was chosen, and because datetime has offset \
1982 /// `-00:45`, but the time zone `Africa/Monrovia` for the given \
1983 /// datetime unambiguously has offset `-00:44:30`",
1984 /// );
1985 /// ```
1986 pub fn resolve_with<F>(
1987 self,
1988 dt: civil::DateTime,
1989 offset: Offset,
1990 tz: TimeZone,
1991 is_equal: F,
1992 ) -> Result<AmbiguousZoned, Error>
1993 where
1994 F: FnMut(Offset, Offset) -> bool,
1995 {
1996 match self {
1997 // In this case, we ignore any TZ annotation (although still
1998 // require that it exists) and always use the provided offset.
1999 OffsetConflict::AlwaysOffset => {
2000 let kind = AmbiguousOffset::Unambiguous { offset };
2001 Ok(AmbiguousTimestamp::new(dt, kind).into_ambiguous_zoned(tz))
2002 }
2003 // In this case, we ignore any provided offset and always use the
2004 // time zone annotation.
2005 OffsetConflict::AlwaysTimeZone => Ok(tz.into_ambiguous_zoned(dt)),
2006 // In this case, we use the offset if it's correct, but otherwise
2007 // fall back to the time zone annotation if it's not.
2008 OffsetConflict::PreferOffset => Ok(
2009 OffsetConflict::resolve_via_prefer(dt, offset, tz, is_equal),
2010 ),
2011 // In this case, if the offset isn't possible for the provided time
2012 // zone annotation, then we return an error.
2013 OffsetConflict::Reject => {
2014 OffsetConflict::resolve_via_reject(dt, offset, tz, is_equal)
2015 }
2016 }
2017 }
2018
2019 /// Given a parsed datetime, a parsed offset and a parsed time zone, this
2020 /// attempts to resolve the datetime to a particular instant based on the
2021 /// 'prefer' strategy.
2022 ///
2023 /// In the 'prefer' strategy, we prefer to use the parsed offset to resolve
2024 /// any ambiguity in the parsed datetime and time zone, but only if the
2025 /// parsed offset is valid for the parsed datetime and time zone. If the
2026 /// parsed offset isn't valid, then it is ignored. In the case where it is
2027 /// ignored, it is possible for an ambiguous instant to be returned.
2028 fn resolve_via_prefer(
2029 dt: civil::DateTime,
2030 given: Offset,
2031 tz: TimeZone,
2032 mut is_equal: impl FnMut(Offset, Offset) -> bool,
2033 ) -> AmbiguousZoned {
2034 use crate::tz::AmbiguousOffset::*;
2035
2036 let amb = tz.to_ambiguous_timestamp(dt);
2037 match amb.offset() {
2038 // We only look for folds because we consider all offsets for gaps
2039 // to be invalid. Which is consistent with how they're treated as
2040 // `OffsetConflict::Reject`. Thus, like any other invalid offset,
2041 // we fallback to disambiguation (which is handled by the caller).
2042 Fold { before, after }
2043 if is_equal(given, before) || is_equal(given, after) =>
2044 {
2045 let kind = Unambiguous { offset: given };
2046 AmbiguousTimestamp::new(dt, kind)
2047 }
2048 _ => amb,
2049 }
2050 .into_ambiguous_zoned(tz)
2051 }
2052
2053 /// Given a parsed datetime, a parsed offset and a parsed time zone, this
2054 /// attempts to resolve the datetime to a particular instant based on the
2055 /// 'reject' strategy.
2056 ///
2057 /// That is, if the offset is not possibly valid for the given datetime and
2058 /// time zone, then this returns an error.
2059 ///
2060 /// This guarantees that on success, an unambiguous timestamp is returned.
2061 /// This occurs because if the datetime is ambiguous for the given time
2062 /// zone, then the parsed offset either matches one of the possible offsets
2063 /// (and thus provides an unambiguous choice), or it doesn't and an error
2064 /// is returned.
2065 fn resolve_via_reject(
2066 dt: civil::DateTime,
2067 given: Offset,
2068 tz: TimeZone,
2069 mut is_equal: impl FnMut(Offset, Offset) -> bool,
2070 ) -> Result<AmbiguousZoned, Error> {
2071 use crate::tz::AmbiguousOffset::*;
2072
2073 let amb = tz.to_ambiguous_timestamp(dt);
2074 match amb.offset() {
2075 Unambiguous { offset } if !is_equal(given, offset) => {
2076 Err(Error::from(E::ResolveRejectUnambiguous {
2077 given,
2078 offset,
2079 tz,
2080 }))
2081 }
2082 Unambiguous { .. } => Ok(amb.into_ambiguous_zoned(tz)),
2083 Gap { before, after } => {
2084 // In `jiff 0.1`, we reported an error when we found a gap
2085 // where neither offset matched what was given. But now we
2086 // report an error whenever we find a gap, as we consider
2087 // all offsets to be invalid for the gap. This now matches
2088 // Temporal's behavior which I think is more consistent. And in
2089 // particular, this makes it more consistent with the behavior
2090 // of `PreferOffset` when a gap is found (which was also
2091 // changed to treat all offsets in a gap as invalid).
2092 //
2093 // Ref: https://github.com/tc39/proposal-temporal/issues/2892
2094 Err(Error::from(E::ResolveRejectGap {
2095 given,
2096 before,
2097 after,
2098 tz,
2099 }))
2100 }
2101 Fold { before, after }
2102 if !is_equal(given, before) && !is_equal(given, after) =>
2103 {
2104 Err(Error::from(E::ResolveRejectFold {
2105 given,
2106 before,
2107 after,
2108 tz,
2109 }))
2110 }
2111 Fold { .. } => {
2112 let kind = Unambiguous { offset: given };
2113 Ok(AmbiguousTimestamp::new(dt, kind).into_ambiguous_zoned(tz))
2114 }
2115 }
2116 }
2117}