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