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::{err, 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).with_context(|| {
530 err!(
531 "converting {dt} with offset {offset} to timestamp overflowed",
532 offset = self,
533 )
534 })
535 }
536
537 /// Adds the given span of time to this offset.
538 ///
539 /// Since time zone offsets have second resolution, any fractional seconds
540 /// in the duration given are ignored.
541 ///
542 /// This operation accepts three different duration types: [`Span`],
543 /// [`SignedDuration`] or [`std::time::Duration`]. This is achieved via
544 /// `From` trait implementations for the [`OffsetArithmetic`] type.
545 ///
546 /// # Errors
547 ///
548 /// This returns an error if the result of adding the given span would
549 /// exceed the minimum or maximum allowed `Offset` value.
550 ///
551 /// This also returns an error if the span given contains any non-zero
552 /// units bigger than hours.
553 ///
554 /// # Example
555 ///
556 /// This example shows how to add one hour to an offset (if the offset
557 /// corresponds to standard time, then adding an hour will usually give
558 /// you DST time):
559 ///
560 /// ```
561 /// use jiff::{tz, ToSpan};
562 ///
563 /// let off = tz::offset(-5);
564 /// assert_eq!(off.checked_add(1.hours()).unwrap(), tz::offset(-4));
565 /// ```
566 ///
567 /// And note that while fractional seconds are ignored, units less than
568 /// seconds aren't ignored if they sum up to a duration at least as big
569 /// as one second:
570 ///
571 /// ```
572 /// use jiff::{tz, ToSpan};
573 ///
574 /// let off = tz::offset(5);
575 /// let span = 900.milliseconds()
576 /// .microseconds(50_000)
577 /// .nanoseconds(50_000_000);
578 /// assert_eq!(
579 /// off.checked_add(span).unwrap(),
580 /// tz::Offset::from_seconds((5 * 60 * 60) + 1).unwrap(),
581 /// );
582 /// // Any leftover fractional part is ignored.
583 /// let span = 901.milliseconds()
584 /// .microseconds(50_001)
585 /// .nanoseconds(50_000_001);
586 /// assert_eq!(
587 /// off.checked_add(span).unwrap(),
588 /// tz::Offset::from_seconds((5 * 60 * 60) + 1).unwrap(),
589 /// );
590 /// ```
591 ///
592 /// This example shows some cases where checked addition will fail.
593 ///
594 /// ```
595 /// use jiff::{tz::Offset, ToSpan};
596 ///
597 /// // Adding units above 'hour' always results in an error.
598 /// assert!(Offset::UTC.checked_add(1.day()).is_err());
599 /// assert!(Offset::UTC.checked_add(1.week()).is_err());
600 /// assert!(Offset::UTC.checked_add(1.month()).is_err());
601 /// assert!(Offset::UTC.checked_add(1.year()).is_err());
602 ///
603 /// // Adding even 1 second to the max, or subtracting 1 from the min,
604 /// // will result in overflow and thus an error will be returned.
605 /// assert!(Offset::MIN.checked_add(-1.seconds()).is_err());
606 /// assert!(Offset::MAX.checked_add(1.seconds()).is_err());
607 /// ```
608 ///
609 /// # Example: adding absolute durations
610 ///
611 /// This shows how to add signed and unsigned absolute durations to an
612 /// `Offset`. Like with `Span`s, any fractional seconds are ignored.
613 ///
614 /// ```
615 /// use std::time::Duration;
616 ///
617 /// use jiff::{tz::offset, SignedDuration};
618 ///
619 /// let off = offset(-10);
620 ///
621 /// let dur = SignedDuration::from_hours(11);
622 /// assert_eq!(off.checked_add(dur)?, offset(1));
623 /// assert_eq!(off.checked_add(-dur)?, offset(-21));
624 ///
625 /// // Any leftover time is truncated. That is, only
626 /// // whole seconds from the duration are considered.
627 /// let dur = Duration::new(3 * 60 * 60, 999_999_999);
628 /// assert_eq!(off.checked_add(dur)?, offset(-7));
629 ///
630 /// # Ok::<(), Box<dyn std::error::Error>>(())
631 /// ```
632 #[inline]
633 pub fn checked_add<A: Into<OffsetArithmetic>>(
634 self,
635 duration: A,
636 ) -> Result<Offset, Error> {
637 let duration: OffsetArithmetic = duration.into();
638 duration.checked_add(self)
639 }
640
641 #[inline]
642 fn checked_add_span(self, span: Span) -> Result<Offset, Error> {
643 if let Some(err) = span.smallest_non_time_non_zero_unit_error() {
644 return Err(err);
645 }
646 let span_seconds = t::SpanZoneOffset::try_rfrom(
647 "span-seconds",
648 span.to_invariant_nanoseconds().div_ceil(t::NANOS_PER_SECOND),
649 )?;
650 let offset_seconds = self.seconds_ranged();
651 let seconds =
652 offset_seconds.try_checked_add("offset-seconds", span_seconds)?;
653 Ok(Offset::from_seconds_ranged(seconds))
654 }
655
656 #[inline]
657 fn checked_add_duration(
658 self,
659 duration: SignedDuration,
660 ) -> Result<Offset, Error> {
661 let duration =
662 t::SpanZoneOffset::try_new("duration-seconds", duration.as_secs())
663 .with_context(|| {
664 err!(
665 "adding signed duration {duration:?} \
666 to offset {self} overflowed maximum offset seconds"
667 )
668 })?;
669 let offset_seconds = self.seconds_ranged();
670 let seconds = offset_seconds
671 .try_checked_add("offset-seconds", duration)
672 .with_context(|| {
673 err!(
674 "adding signed duration {duration:?} \
675 to offset {self} overflowed"
676 )
677 })?;
678 Ok(Offset::from_seconds_ranged(seconds))
679 }
680
681 /// This routine is identical to [`Offset::checked_add`] with the duration
682 /// negated.
683 ///
684 /// # Errors
685 ///
686 /// This has the same error conditions as [`Offset::checked_add`].
687 ///
688 /// # Example
689 ///
690 /// ```
691 /// use std::time::Duration;
692 ///
693 /// use jiff::{tz, SignedDuration, ToSpan};
694 ///
695 /// let off = tz::offset(-4);
696 /// assert_eq!(
697 /// off.checked_sub(1.hours())?,
698 /// tz::offset(-5),
699 /// );
700 /// assert_eq!(
701 /// off.checked_sub(SignedDuration::from_hours(1))?,
702 /// tz::offset(-5),
703 /// );
704 /// assert_eq!(
705 /// off.checked_sub(Duration::from_secs(60 * 60))?,
706 /// tz::offset(-5),
707 /// );
708 ///
709 /// # Ok::<(), Box<dyn std::error::Error>>(())
710 /// ```
711 #[inline]
712 pub fn checked_sub<A: Into<OffsetArithmetic>>(
713 self,
714 duration: A,
715 ) -> Result<Offset, Error> {
716 let duration: OffsetArithmetic = duration.into();
717 duration.checked_neg().and_then(|oa| oa.checked_add(self))
718 }
719
720 /// This routine is identical to [`Offset::checked_add`], except the
721 /// result saturates on overflow. That is, instead of overflow, either
722 /// [`Offset::MIN`] or [`Offset::MAX`] is returned.
723 ///
724 /// # Example
725 ///
726 /// This example shows some cases where saturation will occur.
727 ///
728 /// ```
729 /// use jiff::{tz::Offset, SignedDuration, ToSpan};
730 ///
731 /// // Adding units above 'day' always results in saturation.
732 /// assert_eq!(Offset::UTC.saturating_add(1.weeks()), Offset::MAX);
733 /// assert_eq!(Offset::UTC.saturating_add(1.months()), Offset::MAX);
734 /// assert_eq!(Offset::UTC.saturating_add(1.years()), Offset::MAX);
735 ///
736 /// // Adding even 1 second to the max, or subtracting 1 from the min,
737 /// // will result in saturationg.
738 /// assert_eq!(Offset::MIN.saturating_add(-1.seconds()), Offset::MIN);
739 /// assert_eq!(Offset::MAX.saturating_add(1.seconds()), Offset::MAX);
740 ///
741 /// // Adding absolute durations also saturates as expected.
742 /// assert_eq!(Offset::UTC.saturating_add(SignedDuration::MAX), Offset::MAX);
743 /// assert_eq!(Offset::UTC.saturating_add(SignedDuration::MIN), Offset::MIN);
744 /// assert_eq!(Offset::UTC.saturating_add(std::time::Duration::MAX), Offset::MAX);
745 /// ```
746 #[inline]
747 pub fn saturating_add<A: Into<OffsetArithmetic>>(
748 self,
749 duration: A,
750 ) -> Offset {
751 let duration: OffsetArithmetic = duration.into();
752 self.checked_add(duration).unwrap_or_else(|_| {
753 if duration.is_negative() {
754 Offset::MIN
755 } else {
756 Offset::MAX
757 }
758 })
759 }
760
761 /// This routine is identical to [`Offset::saturating_add`] with the span
762 /// parameter negated.
763 ///
764 /// # Example
765 ///
766 /// This example shows some cases where saturation will occur.
767 ///
768 /// ```
769 /// use jiff::{tz::Offset, SignedDuration, ToSpan};
770 ///
771 /// // Adding units above 'day' always results in saturation.
772 /// assert_eq!(Offset::UTC.saturating_sub(1.weeks()), Offset::MIN);
773 /// assert_eq!(Offset::UTC.saturating_sub(1.months()), Offset::MIN);
774 /// assert_eq!(Offset::UTC.saturating_sub(1.years()), Offset::MIN);
775 ///
776 /// // Adding even 1 second to the max, or subtracting 1 from the min,
777 /// // will result in saturationg.
778 /// assert_eq!(Offset::MIN.saturating_sub(1.seconds()), Offset::MIN);
779 /// assert_eq!(Offset::MAX.saturating_sub(-1.seconds()), Offset::MAX);
780 ///
781 /// // Adding absolute durations also saturates as expected.
782 /// assert_eq!(Offset::UTC.saturating_sub(SignedDuration::MAX), Offset::MIN);
783 /// assert_eq!(Offset::UTC.saturating_sub(SignedDuration::MIN), Offset::MAX);
784 /// assert_eq!(Offset::UTC.saturating_sub(std::time::Duration::MAX), Offset::MIN);
785 /// ```
786 #[inline]
787 pub fn saturating_sub<A: Into<OffsetArithmetic>>(
788 self,
789 duration: A,
790 ) -> Offset {
791 let duration: OffsetArithmetic = duration.into();
792 let Ok(duration) = duration.checked_neg() else { return Offset::MIN };
793 self.saturating_add(duration)
794 }
795
796 /// Returns the span of time from this offset until the other given.
797 ///
798 /// When the `other` offset is more west (i.e., more negative) of the prime
799 /// meridian than this offset, then the span returned will be negative.
800 ///
801 /// # Properties
802 ///
803 /// Adding the span returned to this offset will always equal the `other`
804 /// offset given.
805 ///
806 /// # Examples
807 ///
808 /// ```
809 /// use jiff::{tz, ToSpan};
810 ///
811 /// assert_eq!(
812 /// tz::offset(-5).until(tz::Offset::UTC),
813 /// (5 * 60 * 60).seconds().fieldwise(),
814 /// );
815 /// // Flipping the operands in this case results in a negative span.
816 /// assert_eq!(
817 /// tz::Offset::UTC.until(tz::offset(-5)),
818 /// -(5 * 60 * 60).seconds().fieldwise(),
819 /// );
820 /// ```
821 #[inline]
822 pub fn until(self, other: Offset) -> Span {
823 let diff = other.seconds_ranged() - self.seconds_ranged();
824 Span::new().seconds_ranged(diff.rinto())
825 }
826
827 /// Returns the span of time since the other offset given from this offset.
828 ///
829 /// When the `other` is more east (i.e., more positive) of the prime
830 /// meridian than this offset, then the span returned will be negative.
831 ///
832 /// # Properties
833 ///
834 /// Adding the span returned to the `other` offset will always equal this
835 /// offset.
836 ///
837 /// # Examples
838 ///
839 /// ```
840 /// use jiff::{tz, ToSpan};
841 ///
842 /// assert_eq!(
843 /// tz::Offset::UTC.since(tz::offset(-5)),
844 /// (5 * 60 * 60).seconds().fieldwise(),
845 /// );
846 /// // Flipping the operands in this case results in a negative span.
847 /// assert_eq!(
848 /// tz::offset(-5).since(tz::Offset::UTC),
849 /// -(5 * 60 * 60).seconds().fieldwise(),
850 /// );
851 /// ```
852 #[inline]
853 pub fn since(self, other: Offset) -> Span {
854 self.until(other).negate()
855 }
856
857 /// Returns an absolute duration representing the difference in time from
858 /// this offset until the given `other` offset.
859 ///
860 /// When the `other` offset is more west (i.e., more negative) of the prime
861 /// meridian than this offset, then the duration returned will be negative.
862 ///
863 /// Unlike [`Offset::until`], this returns a duration corresponding to a
864 /// 96-bit integer of nanoseconds between two offsets.
865 ///
866 /// # When should I use this versus [`Offset::until`]?
867 ///
868 /// See the type documentation for [`SignedDuration`] for the section on
869 /// when one should use [`Span`] and when one should use `SignedDuration`.
870 /// In short, use `Span` (and therefore `Offset::until`) unless you have a
871 /// specific reason to do otherwise.
872 ///
873 /// # Examples
874 ///
875 /// ```
876 /// use jiff::{tz, SignedDuration};
877 ///
878 /// assert_eq!(
879 /// tz::offset(-5).duration_until(tz::Offset::UTC),
880 /// SignedDuration::from_hours(5),
881 /// );
882 /// // Flipping the operands in this case results in a negative span.
883 /// assert_eq!(
884 /// tz::Offset::UTC.duration_until(tz::offset(-5)),
885 /// SignedDuration::from_hours(-5),
886 /// );
887 /// ```
888 #[inline]
889 pub fn duration_until(self, other: Offset) -> SignedDuration {
890 SignedDuration::offset_until(self, other)
891 }
892
893 /// This routine is identical to [`Offset::duration_until`], but the order
894 /// of the parameters is flipped.
895 ///
896 /// # Examples
897 ///
898 /// ```
899 /// use jiff::{tz, SignedDuration};
900 ///
901 /// assert_eq!(
902 /// tz::Offset::UTC.duration_since(tz::offset(-5)),
903 /// SignedDuration::from_hours(5),
904 /// );
905 /// assert_eq!(
906 /// tz::offset(-5).duration_since(tz::Offset::UTC),
907 /// SignedDuration::from_hours(-5),
908 /// );
909 /// ```
910 #[inline]
911 pub fn duration_since(self, other: Offset) -> SignedDuration {
912 SignedDuration::offset_until(other, self)
913 }
914
915 /// Returns a new offset that is rounded according to the given
916 /// configuration.
917 ///
918 /// Rounding an offset has a number of parameters, all of which are
919 /// optional. When no parameters are given, then no rounding is done, and
920 /// the offset as given is returned. That is, it's a no-op.
921 ///
922 /// As is consistent with `Offset` itself, rounding only supports units of
923 /// hours, minutes or seconds. If any other unit is provided, then an error
924 /// is returned.
925 ///
926 /// The parameters are, in brief:
927 ///
928 /// * [`OffsetRound::smallest`] sets the smallest [`Unit`] that is allowed
929 /// to be non-zero in the offset returned. By default, it is set to
930 /// [`Unit::Second`], i.e., no rounding occurs. When the smallest unit is
931 /// set to something bigger than seconds, then the non-zero units in the
932 /// offset smaller than the smallest unit are used to determine how the
933 /// offset should be rounded. For example, rounding `+01:59` to the nearest
934 /// hour using the default rounding mode would produce `+02:00`.
935 /// * [`OffsetRound::mode`] determines how to handle the remainder
936 /// when rounding. The default is [`RoundMode::HalfExpand`], which
937 /// corresponds to how you were likely taught to round in school.
938 /// Alternative modes, like [`RoundMode::Trunc`], exist too. For example,
939 /// a truncating rounding of `+01:59` to the nearest hour would
940 /// produce `+01:00`.
941 /// * [`OffsetRound::increment`] sets the rounding granularity to
942 /// use for the configured smallest unit. For example, if the smallest unit
943 /// is minutes and the increment is `15`, then the offset returned will
944 /// always have its minute component set to a multiple of `15`.
945 ///
946 /// # Errors
947 ///
948 /// In general, there are two main ways for rounding to fail: an improper
949 /// configuration like trying to round an offset to the nearest unit other
950 /// than hours/minutes/seconds, or when overflow occurs. Overflow can occur
951 /// when the offset would exceed the minimum or maximum `Offset` values.
952 /// Typically, this can only realistically happen if the offset before
953 /// rounding is already close to its minimum or maximum value.
954 ///
955 /// # Example: rounding to the nearest multiple of 15 minutes
956 ///
957 /// Most time zone offsets fall on an hour boundary, but some fall on the
958 /// half-hour or even 15 minute boundary:
959 ///
960 /// ```
961 /// use jiff::{tz::Offset, Unit};
962 ///
963 /// let offset = Offset::from_seconds(-(44 * 60 + 30)).unwrap();
964 /// let rounded = offset.round((Unit::Minute, 15))?;
965 /// assert_eq!(rounded, Offset::from_seconds(-45 * 60).unwrap());
966 ///
967 /// # Ok::<(), Box<dyn std::error::Error>>(())
968 /// ```
969 ///
970 /// # Example: rounding can fail via overflow
971 ///
972 /// ```
973 /// use jiff::{tz::Offset, Unit};
974 ///
975 /// assert_eq!(Offset::MAX.to_string(), "+25:59:59");
976 /// assert_eq!(
977 /// Offset::MAX.round(Unit::Minute).unwrap_err().to_string(),
978 /// "rounding offset `+25:59:59` resulted in a duration of 26h, \
979 /// which overflows `Offset`",
980 /// );
981 /// ```
982 #[inline]
983 pub fn round<R: Into<OffsetRound>>(
984 self,
985 options: R,
986 ) -> Result<Offset, Error> {
987 let options: OffsetRound = options.into();
988 options.round(self)
989 }
990}
991
992impl Offset {
993 /// This creates an `Offset` via hours/minutes/seconds components.
994 ///
995 /// Currently, it exists because it's convenient for use in tests.
996 ///
997 /// I originally wanted to expose this in the public API, but I couldn't
998 /// decide on how I wanted to treat signedness. There are a variety of
999 /// choices:
1000 ///
1001 /// * Require all values to be positive, and ask the caller to use
1002 /// `-offset` to negate it.
1003 /// * Require all values to have the same sign. If any differs, either
1004 /// panic or return an error.
1005 /// * If any have a negative sign, then behave as if all have a negative
1006 /// sign.
1007 /// * Permit any combination of sign and combine them correctly.
1008 /// Similar to how `std::time::Duration::new(-1s, 1ns)` is turned into
1009 /// `-999,999,999ns`.
1010 ///
1011 /// I think the last option is probably the right behavior, but also the
1012 /// most annoying to implement. But if someone wants to take a crack at it,
1013 /// a PR is welcome.
1014 #[cfg(test)]
1015 #[inline]
1016 pub(crate) const fn hms(hours: i8, minutes: i8, seconds: i8) -> Offset {
1017 let total = (hours as i32 * 60 * 60)
1018 + (minutes as i32 * 60)
1019 + (seconds as i32);
1020 Offset { span: t::SpanZoneOffset::new_unchecked(total) }
1021 }
1022
1023 #[inline]
1024 pub(crate) fn from_hours_ranged(
1025 hours: impl RInto<t::SpanZoneOffsetHours>,
1026 ) -> Offset {
1027 let hours: t::SpanZoneOffset = hours.rinto().rinto();
1028 Offset::from_seconds_ranged(hours * t::SECONDS_PER_HOUR)
1029 }
1030
1031 #[inline]
1032 pub(crate) fn from_seconds_ranged(
1033 seconds: impl RInto<t::SpanZoneOffset>,
1034 ) -> Offset {
1035 Offset { span: seconds.rinto() }
1036 }
1037
1038 /*
1039 #[inline]
1040 pub(crate) fn from_ioffset(ioff: Composite<IOffset>) -> Offset {
1041 let span = rangeint::uncomposite!(ioff, c => (c.second));
1042 Offset { span: span.to_rint() }
1043 }
1044 */
1045
1046 #[inline]
1047 pub(crate) fn to_ioffset(self) -> Composite<IOffset> {
1048 rangeint::composite! {
1049 (second = self.span) => {
1050 IOffset { second }
1051 }
1052 }
1053 }
1054
1055 #[inline]
1056 pub(crate) const fn from_ioffset_const(ioff: IOffset) -> Offset {
1057 Offset::from_seconds_unchecked(ioff.second)
1058 }
1059
1060 #[inline]
1061 pub(crate) const fn from_seconds_unchecked(second: i32) -> Offset {
1062 Offset { span: t::SpanZoneOffset::new_unchecked(second) }
1063 }
1064
1065 /*
1066 #[inline]
1067 pub(crate) const fn to_ioffset_const(self) -> IOffset {
1068 IOffset { second: self.span.get_unchecked() }
1069 }
1070 */
1071
1072 #[inline]
1073 pub(crate) const fn seconds_ranged(self) -> t::SpanZoneOffset {
1074 self.span
1075 }
1076
1077 #[inline]
1078 pub(crate) fn part_hours_ranged(self) -> t::SpanZoneOffsetHours {
1079 self.span.div_ceil(t::SECONDS_PER_HOUR).rinto()
1080 }
1081
1082 #[inline]
1083 pub(crate) fn part_minutes_ranged(self) -> t::SpanZoneOffsetMinutes {
1084 self.span
1085 .div_ceil(t::SECONDS_PER_MINUTE)
1086 .rem_ceil(t::MINUTES_PER_HOUR)
1087 .rinto()
1088 }
1089
1090 #[inline]
1091 pub(crate) fn part_seconds_ranged(self) -> t::SpanZoneOffsetSeconds {
1092 self.span.rem_ceil(t::SECONDS_PER_MINUTE).rinto()
1093 }
1094
1095 #[inline]
1096 pub(crate) fn to_array_str(&self) -> ArrayStr<9> {
1097 use core::fmt::Write;
1098
1099 let mut dst = ArrayStr::new("").unwrap();
1100 // OK because the string representation of an offset
1101 // can never exceed 9 bytes. The longest possible, e.g.,
1102 // is `-25:59:59`.
1103 write!(&mut dst, "{}", self).unwrap();
1104 dst
1105 }
1106}
1107
1108impl core::fmt::Debug for Offset {
1109 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
1110 let sign = if self.seconds_ranged() < C(0) { "-" } else { "" };
1111 write!(
1112 f,
1113 "{sign}{:02}:{:02}:{:02}",
1114 self.part_hours_ranged().abs(),
1115 self.part_minutes_ranged().abs(),
1116 self.part_seconds_ranged().abs(),
1117 )
1118 }
1119}
1120
1121impl core::fmt::Display for Offset {
1122 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
1123 let sign = if self.span < C(0) { "-" } else { "+" };
1124 let hours = self.part_hours_ranged().abs().get();
1125 let minutes = self.part_minutes_ranged().abs().get();
1126 let seconds = self.part_seconds_ranged().abs().get();
1127 if hours == 0 && minutes == 0 && seconds == 0 {
1128 write!(f, "+00")
1129 } else if hours != 0 && minutes == 0 && seconds == 0 {
1130 write!(f, "{sign}{hours:02}")
1131 } else if minutes != 0 && seconds == 0 {
1132 write!(f, "{sign}{hours:02}:{minutes:02}")
1133 } else {
1134 write!(f, "{sign}{hours:02}:{minutes:02}:{seconds:02}")
1135 }
1136 }
1137}
1138
1139/// Adds a span of time to an offset. This panics on overflow.
1140///
1141/// For checked arithmetic, see [`Offset::checked_add`].
1142impl Add<Span> for Offset {
1143 type Output = Offset;
1144
1145 #[inline]
1146 fn add(self, rhs: Span) -> Offset {
1147 self.checked_add(rhs)
1148 .expect("adding span to offset should not overflow")
1149 }
1150}
1151
1152/// Adds a span of time to an offset in place. This panics on overflow.
1153///
1154/// For checked arithmetic, see [`Offset::checked_add`].
1155impl AddAssign<Span> for Offset {
1156 #[inline]
1157 fn add_assign(&mut self, rhs: Span) {
1158 *self = self.add(rhs);
1159 }
1160}
1161
1162/// Subtracts a span of time from an offset. This panics on overflow.
1163///
1164/// For checked arithmetic, see [`Offset::checked_sub`].
1165impl Sub<Span> for Offset {
1166 type Output = Offset;
1167
1168 #[inline]
1169 fn sub(self, rhs: Span) -> Offset {
1170 self.checked_sub(rhs)
1171 .expect("subtracting span from offsetsshould not overflow")
1172 }
1173}
1174
1175/// Subtracts a span of time from an offset in place. This panics on overflow.
1176///
1177/// For checked arithmetic, see [`Offset::checked_sub`].
1178impl SubAssign<Span> for Offset {
1179 #[inline]
1180 fn sub_assign(&mut self, rhs: Span) {
1181 *self = self.sub(rhs);
1182 }
1183}
1184
1185/// Computes the span of time between two offsets.
1186///
1187/// This will return a negative span when the offset being subtracted is
1188/// greater (i.e., more east with respect to the prime meridian).
1189impl Sub for Offset {
1190 type Output = Span;
1191
1192 #[inline]
1193 fn sub(self, rhs: Offset) -> Span {
1194 self.since(rhs)
1195 }
1196}
1197
1198/// Adds a signed duration of time to an offset. This panics on overflow.
1199///
1200/// For checked arithmetic, see [`Offset::checked_add`].
1201impl Add<SignedDuration> for Offset {
1202 type Output = Offset;
1203
1204 #[inline]
1205 fn add(self, rhs: SignedDuration) -> Offset {
1206 self.checked_add(rhs)
1207 .expect("adding signed duration to offset should not overflow")
1208 }
1209}
1210
1211/// Adds a signed duration of time to an offset in place. This panics on
1212/// overflow.
1213///
1214/// For checked arithmetic, see [`Offset::checked_add`].
1215impl AddAssign<SignedDuration> for Offset {
1216 #[inline]
1217 fn add_assign(&mut self, rhs: SignedDuration) {
1218 *self = self.add(rhs);
1219 }
1220}
1221
1222/// Subtracts a signed duration of time from an offset. This panics on
1223/// overflow.
1224///
1225/// For checked arithmetic, see [`Offset::checked_sub`].
1226impl Sub<SignedDuration> for Offset {
1227 type Output = Offset;
1228
1229 #[inline]
1230 fn sub(self, rhs: SignedDuration) -> Offset {
1231 self.checked_sub(rhs).expect(
1232 "subtracting signed duration from offsetsshould not overflow",
1233 )
1234 }
1235}
1236
1237/// Subtracts a signed duration of time from an offset in place. This panics on
1238/// overflow.
1239///
1240/// For checked arithmetic, see [`Offset::checked_sub`].
1241impl SubAssign<SignedDuration> for Offset {
1242 #[inline]
1243 fn sub_assign(&mut self, rhs: SignedDuration) {
1244 *self = self.sub(rhs);
1245 }
1246}
1247
1248/// Adds an unsigned duration of time to an offset. This panics on overflow.
1249///
1250/// For checked arithmetic, see [`Offset::checked_add`].
1251impl Add<UnsignedDuration> for Offset {
1252 type Output = Offset;
1253
1254 #[inline]
1255 fn add(self, rhs: UnsignedDuration) -> Offset {
1256 self.checked_add(rhs)
1257 .expect("adding unsigned duration to offset should not overflow")
1258 }
1259}
1260
1261/// Adds an unsigned duration of time to an offset in place. This panics on
1262/// overflow.
1263///
1264/// For checked arithmetic, see [`Offset::checked_add`].
1265impl AddAssign<UnsignedDuration> for Offset {
1266 #[inline]
1267 fn add_assign(&mut self, rhs: UnsignedDuration) {
1268 *self = self.add(rhs);
1269 }
1270}
1271
1272/// Subtracts an unsigned duration of time from an offset. This panics on
1273/// overflow.
1274///
1275/// For checked arithmetic, see [`Offset::checked_sub`].
1276impl Sub<UnsignedDuration> for Offset {
1277 type Output = Offset;
1278
1279 #[inline]
1280 fn sub(self, rhs: UnsignedDuration) -> Offset {
1281 self.checked_sub(rhs).expect(
1282 "subtracting unsigned duration from offsetsshould not overflow",
1283 )
1284 }
1285}
1286
1287/// Subtracts an unsigned duration of time from an offset in place. This panics
1288/// on overflow.
1289///
1290/// For checked arithmetic, see [`Offset::checked_sub`].
1291impl SubAssign<UnsignedDuration> for Offset {
1292 #[inline]
1293 fn sub_assign(&mut self, rhs: UnsignedDuration) {
1294 *self = self.sub(rhs);
1295 }
1296}
1297
1298/// Negate this offset.
1299///
1300/// A positive offset becomes negative and vice versa. This is a no-op for the
1301/// zero offset.
1302///
1303/// This never panics.
1304impl Neg for Offset {
1305 type Output = Offset;
1306
1307 #[inline]
1308 fn neg(self) -> Offset {
1309 self.negate()
1310 }
1311}
1312
1313/// Converts a `SignedDuration` to a time zone offset.
1314///
1315/// If the signed duration has fractional seconds, then it is automatically
1316/// rounded to the nearest second. (Because an `Offset` has only second
1317/// precision.)
1318///
1319/// # Errors
1320///
1321/// This returns an error if the duration overflows the limits of an `Offset`.
1322///
1323/// # Example
1324///
1325/// ```
1326/// use jiff::{tz::{self, Offset}, SignedDuration};
1327///
1328/// let sdur = SignedDuration::from_secs(-5 * 60 * 60);
1329/// let offset = Offset::try_from(sdur)?;
1330/// assert_eq!(offset, tz::offset(-5));
1331///
1332/// // Sub-seconds results in rounded.
1333/// let sdur = SignedDuration::new(-5 * 60 * 60, -500_000_000);
1334/// let offset = Offset::try_from(sdur)?;
1335/// assert_eq!(offset, tz::Offset::from_seconds(-(5 * 60 * 60 + 1)).unwrap());
1336///
1337/// # Ok::<(), Box<dyn std::error::Error>>(())
1338/// ```
1339impl TryFrom<SignedDuration> for Offset {
1340 type Error = Error;
1341
1342 fn try_from(sdur: SignedDuration) -> Result<Offset, Error> {
1343 let mut seconds = sdur.as_secs();
1344 let subsec = sdur.subsec_nanos();
1345 if subsec >= 500_000_000 {
1346 seconds = seconds.saturating_add(1);
1347 } else if subsec <= -500_000_000 {
1348 seconds = seconds.saturating_sub(1);
1349 }
1350 let seconds = i32::try_from(seconds).map_err(|_| {
1351 err!("`SignedDuration` of {sdur} overflows `Offset`")
1352 })?;
1353 Offset::from_seconds(seconds)
1354 .map_err(|_| err!("`SignedDuration` of {sdur} overflows `Offset`"))
1355 }
1356}
1357
1358/// Options for [`Offset::checked_add`] and [`Offset::checked_sub`].
1359///
1360/// This type provides a way to ergonomically add one of a few different
1361/// duration types to a [`Offset`].
1362///
1363/// The main way to construct values of this type is with its `From` trait
1364/// implementations:
1365///
1366/// * `From<Span> for OffsetArithmetic` adds (or subtracts) the given span to
1367/// the receiver offset.
1368/// * `From<SignedDuration> for OffsetArithmetic` adds (or subtracts)
1369/// the given signed duration to the receiver offset.
1370/// * `From<std::time::Duration> for OffsetArithmetic` adds (or subtracts)
1371/// the given unsigned duration to the receiver offset.
1372///
1373/// # Example
1374///
1375/// ```
1376/// use std::time::Duration;
1377///
1378/// use jiff::{tz::offset, SignedDuration, ToSpan};
1379///
1380/// let off = offset(-10);
1381/// assert_eq!(off.checked_add(11.hours())?, offset(1));
1382/// assert_eq!(off.checked_add(SignedDuration::from_hours(11))?, offset(1));
1383/// assert_eq!(off.checked_add(Duration::from_secs(11 * 60 * 60))?, offset(1));
1384///
1385/// # Ok::<(), Box<dyn std::error::Error>>(())
1386/// ```
1387#[derive(Clone, Copy, Debug)]
1388pub struct OffsetArithmetic {
1389 duration: Duration,
1390}
1391
1392impl OffsetArithmetic {
1393 #[inline]
1394 fn checked_add(self, offset: Offset) -> Result<Offset, Error> {
1395 match self.duration.to_signed()? {
1396 SDuration::Span(span) => offset.checked_add_span(span),
1397 SDuration::Absolute(sdur) => offset.checked_add_duration(sdur),
1398 }
1399 }
1400
1401 #[inline]
1402 fn checked_neg(self) -> Result<OffsetArithmetic, Error> {
1403 let duration = self.duration.checked_neg()?;
1404 Ok(OffsetArithmetic { duration })
1405 }
1406
1407 #[inline]
1408 fn is_negative(&self) -> bool {
1409 self.duration.is_negative()
1410 }
1411}
1412
1413impl From<Span> for OffsetArithmetic {
1414 fn from(span: Span) -> OffsetArithmetic {
1415 let duration = Duration::from(span);
1416 OffsetArithmetic { duration }
1417 }
1418}
1419
1420impl From<SignedDuration> for OffsetArithmetic {
1421 fn from(sdur: SignedDuration) -> OffsetArithmetic {
1422 let duration = Duration::from(sdur);
1423 OffsetArithmetic { duration }
1424 }
1425}
1426
1427impl From<UnsignedDuration> for OffsetArithmetic {
1428 fn from(udur: UnsignedDuration) -> OffsetArithmetic {
1429 let duration = Duration::from(udur);
1430 OffsetArithmetic { duration }
1431 }
1432}
1433
1434impl<'a> From<&'a Span> for OffsetArithmetic {
1435 fn from(span: &'a Span) -> OffsetArithmetic {
1436 OffsetArithmetic::from(*span)
1437 }
1438}
1439
1440impl<'a> From<&'a SignedDuration> for OffsetArithmetic {
1441 fn from(sdur: &'a SignedDuration) -> OffsetArithmetic {
1442 OffsetArithmetic::from(*sdur)
1443 }
1444}
1445
1446impl<'a> From<&'a UnsignedDuration> for OffsetArithmetic {
1447 fn from(udur: &'a UnsignedDuration) -> OffsetArithmetic {
1448 OffsetArithmetic::from(*udur)
1449 }
1450}
1451
1452/// Options for [`Offset::round`].
1453///
1454/// This type provides a way to configure the rounding of an offset. This
1455/// includes setting the smallest unit (i.e., the unit to round), the rounding
1456/// increment and the rounding mode (e.g., "ceil" or "truncate").
1457///
1458/// [`Offset::round`] accepts anything that implements
1459/// `Into<OffsetRound>`. There are a few key trait implementations that
1460/// make this convenient:
1461///
1462/// * `From<Unit> for OffsetRound` will construct a rounding
1463/// configuration where the smallest unit is set to the one given.
1464/// * `From<(Unit, i64)> for OffsetRound` will construct a rounding
1465/// configuration where the smallest unit and the rounding increment are set to
1466/// the ones given.
1467///
1468/// In order to set other options (like the rounding mode), one must explicitly
1469/// create a `OffsetRound` and pass it to `Offset::round`.
1470///
1471/// # Example
1472///
1473/// This example shows how to always round up to the nearest half-hour:
1474///
1475/// ```
1476/// use jiff::{tz::{Offset, OffsetRound}, RoundMode, Unit};
1477///
1478/// let offset = Offset::from_seconds(4 * 60 * 60 + 17 * 60).unwrap();
1479/// let rounded = offset.round(
1480/// OffsetRound::new()
1481/// .smallest(Unit::Minute)
1482/// .increment(30)
1483/// .mode(RoundMode::Expand),
1484/// )?;
1485/// assert_eq!(rounded, Offset::from_seconds(4 * 60 * 60 + 30 * 60).unwrap());
1486///
1487/// # Ok::<(), Box<dyn std::error::Error>>(())
1488/// ```
1489#[derive(Clone, Copy, Debug)]
1490pub struct OffsetRound(SignedDurationRound);
1491
1492impl OffsetRound {
1493 /// Create a new default configuration for rounding a time zone offset via
1494 /// [`Offset::round`].
1495 ///
1496 /// The default configuration does no rounding.
1497 #[inline]
1498 pub fn new() -> OffsetRound {
1499 OffsetRound(SignedDurationRound::new().smallest(Unit::Second))
1500 }
1501
1502 /// Set the smallest units allowed in the offset returned. These are the
1503 /// units that the offset is rounded to.
1504 ///
1505 /// # Errors
1506 ///
1507 /// The unit must be [`Unit::Hour`], [`Unit::Minute`] or [`Unit::Second`].
1508 ///
1509 /// # Example
1510 ///
1511 /// A basic example that rounds to the nearest minute:
1512 ///
1513 /// ```
1514 /// use jiff::{tz::Offset, Unit};
1515 ///
1516 /// let offset = Offset::from_seconds(-(5 * 60 * 60 + 30)).unwrap();
1517 /// assert_eq!(offset.round(Unit::Hour)?, Offset::from_hours(-5).unwrap());
1518 ///
1519 /// # Ok::<(), Box<dyn std::error::Error>>(())
1520 /// ```
1521 #[inline]
1522 pub fn smallest(self, unit: Unit) -> OffsetRound {
1523 OffsetRound(self.0.smallest(unit))
1524 }
1525
1526 /// Set the rounding mode.
1527 ///
1528 /// This defaults to [`RoundMode::HalfExpand`], which makes rounding work
1529 /// like how you were taught in school.
1530 ///
1531 /// # Example
1532 ///
1533 /// A basic example that rounds to the nearest hour, but changing its
1534 /// rounding mode to truncation:
1535 ///
1536 /// ```
1537 /// use jiff::{tz::{Offset, OffsetRound}, RoundMode, Unit};
1538 ///
1539 /// let offset = Offset::from_seconds(-(5 * 60 * 60 + 30 * 60)).unwrap();
1540 /// assert_eq!(
1541 /// offset.round(OffsetRound::new()
1542 /// .smallest(Unit::Hour)
1543 /// .mode(RoundMode::Trunc),
1544 /// )?,
1545 /// // The default round mode does rounding like
1546 /// // how you probably learned in school, and would
1547 /// // result in rounding to -6 hours. But we
1548 /// // change it to truncation here, which makes it
1549 /// // round -5.
1550 /// Offset::from_hours(-5).unwrap(),
1551 /// );
1552 ///
1553 /// # Ok::<(), Box<dyn std::error::Error>>(())
1554 /// ```
1555 #[inline]
1556 pub fn mode(self, mode: RoundMode) -> OffsetRound {
1557 OffsetRound(self.0.mode(mode))
1558 }
1559
1560 /// Set the rounding increment for the smallest unit.
1561 ///
1562 /// The default value is `1`. Other values permit rounding the smallest
1563 /// unit to the nearest integer increment specified. For example, if the
1564 /// smallest unit is set to [`Unit::Minute`], then a rounding increment of
1565 /// `30` would result in rounding in increments of a half hour. That is,
1566 /// the only minute value that could result would be `0` or `30`.
1567 ///
1568 /// # Errors
1569 ///
1570 /// The rounding increment must divide evenly into the next highest unit
1571 /// after the smallest unit configured (and must not be equivalent to
1572 /// it). For example, if the smallest unit is [`Unit::Second`], then
1573 /// *some* of the valid values for the rounding increment are `1`, `2`,
1574 /// `4`, `5`, `15` and `30`. Namely, any integer that divides evenly into
1575 /// `60` seconds since there are `60` seconds in the next highest unit
1576 /// (minutes).
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(self.0.increment(increment))
1596 }
1597
1598 /// Does the actual offset rounding.
1599 fn round(&self, offset: Offset) -> Result<Offset, Error> {
1600 let smallest = self.0.get_smallest();
1601 if !(Unit::Second <= smallest && smallest <= Unit::Hour) {
1602 return Err(err!(
1603 "rounding `Offset` failed because \
1604 a unit of {plural} was provided, but offset rounding \
1605 can only use hours, minutes or seconds",
1606 plural = smallest.plural(),
1607 ));
1608 }
1609 let rounded_sdur = SignedDuration::from(offset).round(self.0)?;
1610 Offset::try_from(rounded_sdur).map_err(|_| {
1611 err!(
1612 "rounding offset `{offset}` resulted in a duration \
1613 of {rounded_sdur:?}, which overflows `Offset`",
1614 )
1615 })
1616 }
1617}
1618
1619impl Default for OffsetRound {
1620 fn default() -> OffsetRound {
1621 OffsetRound::new()
1622 }
1623}
1624
1625impl From<Unit> for OffsetRound {
1626 fn from(unit: Unit) -> OffsetRound {
1627 OffsetRound::default().smallest(unit)
1628 }
1629}
1630
1631impl From<(Unit, i64)> for OffsetRound {
1632 fn from((unit, increment): (Unit, i64)) -> OffsetRound {
1633 OffsetRound::default().smallest(unit).increment(increment)
1634 }
1635}
1636
1637/// Configuration for resolving disparities between an offset and a time zone.
1638///
1639/// A conflict between an offset and a time zone most commonly appears in a
1640/// datetime string. For example, `2024-06-14T17:30-05[America/New_York]`
1641/// has a definitive inconsistency between the reported offset (`-05`) and
1642/// the time zone (`America/New_York`), because at this time in New York,
1643/// daylight saving time (DST) was in effect. In New York in the year 2024,
1644/// DST corresponded to the UTC offset `-04`.
1645///
1646/// Other conflict variations exist. For example, in 2019, Brazil abolished
1647/// DST completely. But if one were to create a datetime for 2020 in 2018, that
1648/// datetime in 2020 would reflect the DST rules as they exist in 2018. That
1649/// could in turn result in a datetime with an offset that is incorrect with
1650/// respect to the rules in 2019.
1651///
1652/// For this reason, this crate exposes a few ways of resolving these
1653/// conflicts. It is most commonly used as configuration for parsing
1654/// [`Zoned`](crate::Zoned) values via
1655/// [`fmt::temporal::DateTimeParser::offset_conflict`](crate::fmt::temporal::DateTimeParser::offset_conflict). But this configuration can also be used directly via
1656/// [`OffsetConflict::resolve`].
1657///
1658/// The default value is `OffsetConflict::Reject`, which results in an
1659/// error being returned if the offset and a time zone are not in agreement.
1660/// This is the default so that Jiff does not automatically make silent choices
1661/// about whether to prefer the time zone or the offset. The
1662/// [`fmt::temporal::DateTimeParser::parse_zoned_with`](crate::fmt::temporal::DateTimeParser::parse_zoned_with)
1663/// documentation shows an example demonstrating its utility in the face
1664/// of changes in the law, such as the abolition of daylight saving time.
1665/// By rejecting such things, one can ensure that the original timestamp is
1666/// preserved or else an error occurs.
1667///
1668/// This enum is non-exhaustive so that other forms of offset conflicts may be
1669/// added in semver compatible releases.
1670///
1671/// # Example
1672///
1673/// This example shows how to always use the time zone even if the offset is
1674/// wrong.
1675///
1676/// ```
1677/// use jiff::{civil::date, tz};
1678///
1679/// let dt = date(2024, 6, 14).at(17, 30, 0, 0);
1680/// let offset = tz::offset(-5); // wrong! should be -4
1681/// let newyork = tz::db().get("America/New_York")?;
1682///
1683/// // The default conflict resolution, 'Reject', will error.
1684/// let result = tz::OffsetConflict::Reject
1685/// .resolve(dt, offset, newyork.clone());
1686/// assert!(result.is_err());
1687///
1688/// // But we can change it to always prefer the time zone.
1689/// let zdt = tz::OffsetConflict::AlwaysTimeZone
1690/// .resolve(dt, offset, newyork.clone())?
1691/// .unambiguous()?;
1692/// assert_eq!(zdt.datetime(), date(2024, 6, 14).at(17, 30, 0, 0));
1693/// // The offset has been corrected automatically.
1694/// assert_eq!(zdt.offset(), tz::offset(-4));
1695///
1696/// # Ok::<(), Box<dyn std::error::Error>>(())
1697/// ```
1698///
1699/// # Example: parsing
1700///
1701/// This example shows how to set the offset conflict resolution configuration
1702/// while parsing a [`Zoned`](crate::Zoned) datetime. In this example, we
1703/// always prefer the offset, even if it conflicts with the time zone.
1704///
1705/// ```
1706/// use jiff::{civil::date, fmt::temporal::DateTimeParser, tz};
1707///
1708/// static PARSER: DateTimeParser = DateTimeParser::new()
1709/// .offset_conflict(tz::OffsetConflict::AlwaysOffset);
1710///
1711/// let zdt = PARSER.parse_zoned("2024-06-14T17:30-05[America/New_York]")?;
1712/// // The time *and* offset have been corrected. The offset given was invalid,
1713/// // so it cannot be kept, but the timestamp returned is equivalent to
1714/// // `2024-06-14T17:30-05`. It is just adjusted automatically to be correct
1715/// // in the `America/New_York` time zone.
1716/// assert_eq!(zdt.datetime(), date(2024, 6, 14).at(18, 30, 0, 0));
1717/// assert_eq!(zdt.offset(), tz::offset(-4));
1718///
1719/// # Ok::<(), Box<dyn std::error::Error>>(())
1720/// ```
1721#[derive(Clone, Copy, Debug, Default)]
1722#[non_exhaustive]
1723pub enum OffsetConflict {
1724 /// When the offset and time zone are in conflict, this will always use
1725 /// the offset to interpret the date time.
1726 ///
1727 /// When resolving to a [`AmbiguousZoned`], the time zone attached
1728 /// to the timestamp will still be the same as the time zone given. The
1729 /// difference here is that the offset will be adjusted such that it is
1730 /// correct for the given time zone. However, the timestamp itself will
1731 /// always match the datetime and offset given (and which is always
1732 /// unambiguous).
1733 ///
1734 /// Basically, you should use this option when you want to keep the exact
1735 /// time unchanged (as indicated by the datetime and offset), even if it
1736 /// means a change to civil time.
1737 AlwaysOffset,
1738 /// When the offset and time zone are in conflict, this will always use
1739 /// the time zone to interpret the date time.
1740 ///
1741 /// When resolving to an [`AmbiguousZoned`], the offset attached to the
1742 /// timestamp will always be determined by only looking at the time zone.
1743 /// This in turn implies that the timestamp returned could be ambiguous,
1744 /// since this conflict resolution strategy specifically ignores the
1745 /// offset. (And, we're only at this point because the offset is not
1746 /// possible for the given time zone, so it can't be used in concert with
1747 /// the time zone anyway.) This is unlike the `AlwaysOffset` strategy where
1748 /// the timestamp returned is guaranteed to be unambiguous.
1749 ///
1750 /// You should use this option when you want to keep the civil time
1751 /// unchanged even if it means a change to the exact time.
1752 AlwaysTimeZone,
1753 /// Always attempt to use the offset to resolve a datetime to a timestamp,
1754 /// unless the offset is invalid for the provided time zone. In that case,
1755 /// use the time zone. When the time zone is used, it's possible for an
1756 /// ambiguous datetime to be returned.
1757 ///
1758 /// See [`ZonedWith::offset_conflict`](crate::ZonedWith::offset_conflict)
1759 /// for an example of when this strategy is useful.
1760 PreferOffset,
1761 /// When the offset and time zone are in conflict, this strategy always
1762 /// results in conflict resolution returning an error.
1763 ///
1764 /// This is the default since a conflict between the offset and the time
1765 /// zone usually implies an invalid datetime in some way.
1766 #[default]
1767 Reject,
1768}
1769
1770impl OffsetConflict {
1771 /// Resolve a potential conflict between an [`Offset`] and a [`TimeZone`].
1772 ///
1773 /// # Errors
1774 ///
1775 /// This returns an error if this would have returned a timestamp outside
1776 /// of its minimum and maximum values.
1777 ///
1778 /// This can also return an error when using the [`OffsetConflict::Reject`]
1779 /// strategy. Namely, when using the `Reject` strategy, any offset that is
1780 /// not compatible with the given datetime and time zone will always result
1781 /// in an error.
1782 ///
1783 /// # Example
1784 ///
1785 /// This example shows how each of the different conflict resolution
1786 /// strategies are applied.
1787 ///
1788 /// ```
1789 /// use jiff::{civil::date, tz};
1790 ///
1791 /// let dt = date(2024, 6, 14).at(17, 30, 0, 0);
1792 /// let offset = tz::offset(-5); // wrong! should be -4
1793 /// let newyork = tz::db().get("America/New_York")?;
1794 ///
1795 /// // Here, we use the offset and ignore the time zone.
1796 /// let zdt = tz::OffsetConflict::AlwaysOffset
1797 /// .resolve(dt, offset, newyork.clone())?
1798 /// .unambiguous()?;
1799 /// // The datetime (and offset) have been corrected automatically
1800 /// // and the resulting Zoned instant corresponds precisely to
1801 /// // `2024-06-14T17:30-05[UTC]`.
1802 /// assert_eq!(zdt.to_string(), "2024-06-14T18:30:00-04:00[America/New_York]");
1803 ///
1804 /// // Here, we use the time zone and ignore the offset.
1805 /// let zdt = tz::OffsetConflict::AlwaysTimeZone
1806 /// .resolve(dt, offset, newyork.clone())?
1807 /// .unambiguous()?;
1808 /// // The offset has been corrected automatically and the resulting
1809 /// // Zoned instant corresponds precisely to `2024-06-14T17:30-04[UTC]`.
1810 /// // Notice how the civil time remains the same, but the exact instant
1811 /// // has changed!
1812 /// assert_eq!(zdt.to_string(), "2024-06-14T17:30:00-04:00[America/New_York]");
1813 ///
1814 /// // Here, we prefer the offset, but fall back to the time zone.
1815 /// // In this example, it has the same behavior as `AlwaysTimeZone`.
1816 /// let zdt = tz::OffsetConflict::PreferOffset
1817 /// .resolve(dt, offset, newyork.clone())?
1818 /// .unambiguous()?;
1819 /// assert_eq!(zdt.to_string(), "2024-06-14T17:30:00-04:00[America/New_York]");
1820 ///
1821 /// // The default conflict resolution, 'Reject', will error.
1822 /// let result = tz::OffsetConflict::Reject
1823 /// .resolve(dt, offset, newyork.clone());
1824 /// assert!(result.is_err());
1825 ///
1826 /// # Ok::<(), Box<dyn std::error::Error>>(())
1827 /// ```
1828 pub fn resolve(
1829 self,
1830 dt: civil::DateTime,
1831 offset: Offset,
1832 tz: TimeZone,
1833 ) -> Result<AmbiguousZoned, Error> {
1834 self.resolve_with(dt, offset, tz, |off1, off2| off1 == off2)
1835 }
1836
1837 /// Resolve a potential conflict between an [`Offset`] and a [`TimeZone`]
1838 /// using the given definition of equality for an `Offset`.
1839 ///
1840 /// The equality predicate is always given a pair of offsets where the
1841 /// first is the offset given to `resolve_with` and the second is the
1842 /// offset found in the `TimeZone`.
1843 ///
1844 /// # Errors
1845 ///
1846 /// This returns an error if this would have returned a timestamp outside
1847 /// of its minimum and maximum values.
1848 ///
1849 /// This can also return an error when using the [`OffsetConflict::Reject`]
1850 /// strategy. Namely, when using the `Reject` strategy, any offset that is
1851 /// not compatible with the given datetime and time zone will always result
1852 /// in an error.
1853 ///
1854 /// # Example
1855 ///
1856 /// Unlike [`OffsetConflict::resolve`], this routine permits overriding
1857 /// the definition of equality used for comparing offsets. In
1858 /// `OffsetConflict::resolve`, exact equality is used. This can be
1859 /// troublesome in some cases when a time zone has an offset with
1860 /// fractional minutes, such as `Africa/Monrovia` before 1972.
1861 ///
1862 /// Because RFC 3339 and RFC 9557 do not support time zone offsets
1863 /// with fractional minutes, Jiff will serialize offsets with
1864 /// fractional minutes by rounding to the nearest minute. This
1865 /// will result in a different offset than what is actually
1866 /// used in the time zone. Parsing this _should_ succeed, but
1867 /// if exact offset equality is used, it won't. This is why a
1868 /// [`fmt::temporal::DateTimeParser`](crate::fmt::temporal::DateTimeParser)
1869 /// uses this routine with offset equality that rounds offsets to the
1870 /// nearest minute before comparison.
1871 ///
1872 /// ```
1873 /// use jiff::{civil::date, tz::{Offset, OffsetConflict, TimeZone}, Unit};
1874 ///
1875 /// let dt = date(1968, 2, 1).at(23, 15, 0, 0);
1876 /// let offset = Offset::from_seconds(-(44 * 60 + 30)).unwrap();
1877 /// let zdt = dt.in_tz("Africa/Monrovia")?;
1878 /// assert_eq!(zdt.offset(), offset);
1879 /// // Notice that the offset has been rounded!
1880 /// assert_eq!(zdt.to_string(), "1968-02-01T23:15:00-00:45[Africa/Monrovia]");
1881 ///
1882 /// // Now imagine parsing extracts the civil datetime, the offset and
1883 /// // the time zone, and then naively does exact offset comparison:
1884 /// let tz = TimeZone::get("Africa/Monrovia")?;
1885 /// // This is the parsed offset, which won't precisely match the actual
1886 /// // offset used by `Africa/Monrovia` at this time.
1887 /// let offset = Offset::from_seconds(-45 * 60).unwrap();
1888 /// let result = OffsetConflict::Reject.resolve(dt, offset, tz.clone());
1889 /// assert_eq!(
1890 /// result.unwrap_err().to_string(),
1891 /// "datetime 1968-02-01T23:15:00 could not resolve to a timestamp \
1892 /// since 'reject' conflict resolution was chosen, and because \
1893 /// datetime has offset -00:45, but the time zone Africa/Monrovia \
1894 /// for the given datetime unambiguously has offset -00:44:30",
1895 /// );
1896 /// let is_equal = |parsed: Offset, candidate: Offset| {
1897 /// parsed == candidate || candidate.round(Unit::Minute).map_or(
1898 /// parsed == candidate,
1899 /// |candidate| parsed == candidate,
1900 /// )
1901 /// };
1902 /// let zdt = OffsetConflict::Reject.resolve_with(
1903 /// dt,
1904 /// offset,
1905 /// tz.clone(),
1906 /// is_equal,
1907 /// )?.unambiguous()?;
1908 /// // Notice that the offset is the actual offset from the time zone:
1909 /// assert_eq!(zdt.offset(), Offset::from_seconds(-(44 * 60 + 30)).unwrap());
1910 /// // But when we serialize, the offset gets rounded. If we didn't
1911 /// // do this, we'd risk the datetime not being parsable by other
1912 /// // implementations since RFC 3339 and RFC 9557 don't support fractional
1913 /// // minutes in the offset.
1914 /// assert_eq!(zdt.to_string(), "1968-02-01T23:15:00-00:45[Africa/Monrovia]");
1915 ///
1916 /// # Ok::<(), Box<dyn std::error::Error>>(())
1917 /// ```
1918 ///
1919 /// And indeed, notice that parsing uses this same kind of offset equality
1920 /// to permit zoned datetimes whose offsets would be equivalent after
1921 /// rounding:
1922 ///
1923 /// ```
1924 /// use jiff::{tz::Offset, Zoned};
1925 ///
1926 /// let zdt: Zoned = "1968-02-01T23:15:00-00:45[Africa/Monrovia]".parse()?;
1927 /// // As above, notice that even though we parsed `-00:45` as the
1928 /// // offset, the actual offset of our zoned datetime is the correct
1929 /// // one from the time zone.
1930 /// assert_eq!(zdt.offset(), Offset::from_seconds(-(44 * 60 + 30)).unwrap());
1931 /// // And similarly, re-serializing it results in rounding the offset
1932 /// // again for compatibility with RFC 3339 and RFC 9557.
1933 /// assert_eq!(zdt.to_string(), "1968-02-01T23:15:00-00:45[Africa/Monrovia]");
1934 ///
1935 /// // And we also support parsing the actual fractional minute offset
1936 /// // as well:
1937 /// let zdt: Zoned = "1968-02-01T23:15:00-00:44:30[Africa/Monrovia]".parse()?;
1938 /// assert_eq!(zdt.offset(), Offset::from_seconds(-(44 * 60 + 30)).unwrap());
1939 /// assert_eq!(zdt.to_string(), "1968-02-01T23:15:00-00:45[Africa/Monrovia]");
1940 ///
1941 /// # Ok::<(), Box<dyn std::error::Error>>(())
1942 /// ```
1943 pub fn resolve_with<F>(
1944 self,
1945 dt: civil::DateTime,
1946 offset: Offset,
1947 tz: TimeZone,
1948 is_equal: F,
1949 ) -> Result<AmbiguousZoned, Error>
1950 where
1951 F: FnMut(Offset, Offset) -> bool,
1952 {
1953 match self {
1954 // In this case, we ignore any TZ annotation (although still
1955 // require that it exists) and always use the provided offset.
1956 OffsetConflict::AlwaysOffset => {
1957 let kind = AmbiguousOffset::Unambiguous { offset };
1958 Ok(AmbiguousTimestamp::new(dt, kind).into_ambiguous_zoned(tz))
1959 }
1960 // In this case, we ignore any provided offset and always use the
1961 // time zone annotation.
1962 OffsetConflict::AlwaysTimeZone => Ok(tz.into_ambiguous_zoned(dt)),
1963 // In this case, we use the offset if it's correct, but otherwise
1964 // fall back to the time zone annotation if it's not.
1965 OffsetConflict::PreferOffset => Ok(
1966 OffsetConflict::resolve_via_prefer(dt, offset, tz, is_equal),
1967 ),
1968 // In this case, if the offset isn't possible for the provided time
1969 // zone annotation, then we return an error.
1970 OffsetConflict::Reject => {
1971 OffsetConflict::resolve_via_reject(dt, offset, tz, is_equal)
1972 }
1973 }
1974 }
1975
1976 /// Given a parsed datetime, a parsed offset and a parsed time zone, this
1977 /// attempts to resolve the datetime to a particular instant based on the
1978 /// 'prefer' strategy.
1979 ///
1980 /// In the 'prefer' strategy, we prefer to use the parsed offset to resolve
1981 /// any ambiguity in the parsed datetime and time zone, but only if the
1982 /// parsed offset is valid for the parsed datetime and time zone. If the
1983 /// parsed offset isn't valid, then it is ignored. In the case where it is
1984 /// ignored, it is possible for an ambiguous instant to be returned.
1985 fn resolve_via_prefer(
1986 dt: civil::DateTime,
1987 given: Offset,
1988 tz: TimeZone,
1989 mut is_equal: impl FnMut(Offset, Offset) -> bool,
1990 ) -> AmbiguousZoned {
1991 use crate::tz::AmbiguousOffset::*;
1992
1993 let amb = tz.to_ambiguous_timestamp(dt);
1994 match amb.offset() {
1995 // We only look for folds because we consider all offsets for gaps
1996 // to be invalid. Which is consistent with how they're treated as
1997 // `OffsetConflict::Reject`. Thus, like any other invalid offset,
1998 // we fallback to disambiguation (which is handled by the caller).
1999 Fold { before, after }
2000 if is_equal(given, before) || is_equal(given, after) =>
2001 {
2002 let kind = Unambiguous { offset: given };
2003 AmbiguousTimestamp::new(dt, kind)
2004 }
2005 _ => amb,
2006 }
2007 .into_ambiguous_zoned(tz)
2008 }
2009
2010 /// Given a parsed datetime, a parsed offset and a parsed time zone, this
2011 /// attempts to resolve the datetime to a particular instant based on the
2012 /// 'reject' strategy.
2013 ///
2014 /// That is, if the offset is not possibly valid for the given datetime and
2015 /// time zone, then this returns an error.
2016 ///
2017 /// This guarantees that on success, an unambiguous timestamp is returned.
2018 /// This occurs because if the datetime is ambiguous for the given time
2019 /// zone, then the parsed offset either matches one of the possible offsets
2020 /// (and thus provides an unambiguous choice), or it doesn't and an error
2021 /// is returned.
2022 fn resolve_via_reject(
2023 dt: civil::DateTime,
2024 given: Offset,
2025 tz: TimeZone,
2026 mut is_equal: impl FnMut(Offset, Offset) -> bool,
2027 ) -> Result<AmbiguousZoned, Error> {
2028 use crate::tz::AmbiguousOffset::*;
2029
2030 let amb = tz.to_ambiguous_timestamp(dt);
2031 match amb.offset() {
2032 Unambiguous { offset } if !is_equal(given, offset) => Err(err!(
2033 "datetime {dt} could not resolve to a timestamp since \
2034 'reject' conflict resolution was chosen, and because \
2035 datetime has offset {given}, but the time zone {tzname} for \
2036 the given datetime unambiguously has offset {offset}",
2037 tzname = tz.diagnostic_name(),
2038 )),
2039 Unambiguous { .. } => Ok(amb.into_ambiguous_zoned(tz)),
2040 Gap { before, after } => {
2041 // In `jiff 0.1`, we reported an error when we found a gap
2042 // where neither offset matched what was given. But now we
2043 // report an error whenever we find a gap, as we consider
2044 // all offsets to be invalid for the gap. This now matches
2045 // Temporal's behavior which I think is more consistent. And in
2046 // particular, this makes it more consistent with the behavior
2047 // of `PreferOffset` when a gap is found (which was also
2048 // changed to treat all offsets in a gap as invalid).
2049 //
2050 // Ref: https://github.com/tc39/proposal-temporal/issues/2892
2051 Err(err!(
2052 "datetime {dt} could not resolve to timestamp \
2053 since 'reject' conflict resolution was chosen, and \
2054 because datetime has offset {given}, but the time \
2055 zone {tzname} for the given datetime falls in a gap \
2056 (between offsets {before} and {after}), and all \
2057 offsets for a gap are regarded as invalid",
2058 tzname = tz.diagnostic_name(),
2059 ))
2060 }
2061 Fold { before, after }
2062 if !is_equal(given, before) && !is_equal(given, after) =>
2063 {
2064 Err(err!(
2065 "datetime {dt} could not resolve to timestamp \
2066 since 'reject' conflict resolution was chosen, and \
2067 because datetime has offset {given}, but the time \
2068 zone {tzname} for the given datetime falls in a fold \
2069 between offsets {before} and {after}, neither of which \
2070 match the offset",
2071 tzname = tz.diagnostic_name(),
2072 ))
2073 }
2074 Fold { .. } => {
2075 let kind = Unambiguous { offset: given };
2076 Ok(AmbiguousTimestamp::new(dt, kind).into_ambiguous_zoned(tz))
2077 }
2078 }
2079 }
2080}