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