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