datetime_string/rfc3339/
offset.rs

1//! RFC 3339 [`time-offset`] string types.
2//!
3//! [`time-offset`]: https://tools.ietf.org/html/rfc3339#section-5.6
4
5#[cfg(feature = "alloc")]
6mod owned;
7
8use core::{cmp::Ordering, convert::TryFrom, fmt, ops, str};
9
10use crate::{
11    common::TimeOffsetSign,
12    error::{ComponentKind, Error, ErrorKind},
13};
14
15use super::TimeNumOffsetStr;
16
17#[cfg(feature = "alloc")]
18pub use self::owned::TimeOffsetString;
19
20/// Validates the given string as an RFC 3339 [`time-offset`].
21///
22/// [`time-offset`]: https://tools.ietf.org/html/rfc3339#section-5.6
23fn validate_bytes(s: &[u8]) -> Result<(), Error> {
24    match s.len().cmp(&1) {
25        Ordering::Less => Err(ErrorKind::TooShort.into()),
26        Ordering::Equal => {
27            if s[0] == b'Z' {
28                Ok(())
29            } else {
30                Err(ErrorKind::InvalidComponentType(ComponentKind::Offset).into())
31            }
32        }
33        Ordering::Greater => TimeNumOffsetStr::from_bytes(s).map(|_| ()),
34    }
35}
36
37/// String slice for a time in RFC 3339 [`time-offset`] format, such as `+09:00`, `-00:00`, and `Z`.
38///
39/// [`time-offset`]: https://tools.ietf.org/html/rfc3339#section-5.6
40#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
41#[repr(transparent)]
42// Note that `derive(Serialize)` cannot used here, because it encodes this as
43// `[u8]` rather than as a string.
44//
45// Comparisons implemented for the type are consistent (at least it is intended to be so).
46// See <https://github.com/rust-lang/rust-clippy/issues/2025>.
47// Note that `clippy::derive_ord_xor_partial_ord` would be introduced since Rust 1.47.0.
48#[allow(clippy::derive_hash_xor_eq)]
49#[allow(unknown_lints, clippy::derive_ord_xor_partial_ord)]
50pub struct TimeOffsetStr([u8]);
51
52impl TimeOffsetStr {
53    /// Creates a `&TimeOffsetStr` from the given byte slice.
54    ///
55    /// This performs assertion in debug build, but not in release build.
56    ///
57    /// # Safety
58    ///
59    /// `validate_bytes(s)` should return `Ok(())`.
60    #[inline]
61    #[must_use]
62    pub(crate) unsafe fn from_bytes_maybe_unchecked(s: &[u8]) -> &Self {
63        debug_assert_ok!(validate_bytes(s));
64        &*(s as *const [u8] as *const Self)
65    }
66
67    /// Creates a `&mut TimeOffsetStr` from the given mutable byte slice.
68    ///
69    /// This performs assertion in debug build, but not in release build.
70    ///
71    /// # Safety
72    ///
73    /// `validate_bytes(s)` should return `Ok(())`.
74    #[inline]
75    #[must_use]
76    pub(crate) unsafe fn from_bytes_maybe_unchecked_mut(s: &mut [u8]) -> &mut Self {
77        debug_assert_ok!(validate_bytes(s));
78        &mut *(s as *mut [u8] as *mut Self)
79    }
80
81    /// Creates a `&mut TimeOffsetStr` from the given mutable string slice.
82    ///
83    /// This performs assertion in debug build, but not in release build.
84    ///
85    /// # Safety
86    ///
87    /// `validate_bytes(s.as_bytes())` should return `Ok(())`.
88    #[inline]
89    #[must_use]
90    unsafe fn from_str_maybe_unchecked_mut(s: &mut str) -> &mut Self {
91        // This is safe because ``TimeOffsetStr` ensures that the underlying
92        // bytes are ASCII string after modification.
93        Self::from_bytes_maybe_unchecked_mut(s.as_bytes_mut())
94    }
95
96    /// Creates a new `&TimeOffsetStr` from a string slice.
97    ///
98    /// # Examples
99    ///
100    /// ```
101    /// # use datetime_string::rfc3339::TimeOffsetStr;
102    /// let time = TimeOffsetStr::from_str("-12:34")?;
103    /// assert_eq!(time.as_str(), "-12:34");
104    ///
105    /// assert!(TimeOffsetStr::from_str("Z").is_ok());
106    /// assert!(TimeOffsetStr::from_str("+00:00").is_ok());
107    /// assert!(TimeOffsetStr::from_str("+23:59").is_ok());
108    /// assert!(TimeOffsetStr::from_str("-00:00").is_ok());
109    /// assert!(TimeOffsetStr::from_str("-23:59").is_ok());
110    ///
111    /// assert!(TimeOffsetStr::from_str("z").is_err(), "lowercase Z is not allowed");
112    /// assert!(TimeOffsetStr::from_str("a").is_err(), "Invalid name");
113    /// assert!(TimeOffsetStr::from_str("+24:00").is_err(), "Invalid hour");
114    /// assert!(TimeOffsetStr::from_str("+00:60").is_err(), "Invalid minute");
115    /// assert!(TimeOffsetStr::from_str("-24:00").is_err(), "Invalid hour");
116    /// assert!(TimeOffsetStr::from_str("-00:60").is_err(), "Invalid minute");
117    /// assert!(TimeOffsetStr::from_str("?00:00").is_err(), "Invalid sign");
118    /// assert!(TimeOffsetStr::from_str("00:00").is_err(), "Sign is missing");
119    /// # Ok::<_, datetime_string::Error>(())
120    /// ```
121    #[inline]
122    // `FromStr` trait cannot be implemented for a slice.
123    #[allow(clippy::should_implement_trait)]
124    pub fn from_str(s: &str) -> Result<&Self, Error> {
125        TryFrom::try_from(s)
126    }
127
128    /// Creates a new `&mut TimeOffsetStr` from a mutable string slice.
129    ///
130    /// # Examples
131    ///
132    /// ```
133    /// # use datetime_string::rfc3339::TimeOffsetStr;
134    /// use datetime_string::common::TimeOffsetSign;
135    ///
136    /// let mut buf = "-12:34".to_owned();
137    /// let offset = TimeOffsetStr::from_mut_str(&mut buf)?;
138    /// assert_eq!(offset.as_str(), "-12:34");
139    ///
140    /// offset.to_numoffset_mut().unwrap().set_sign(TimeOffsetSign::Positive);
141    /// assert_eq!(offset.as_str(), "+12:34");
142    /// # Ok::<_, datetime_string::Error>(())
143    /// ```
144    #[inline]
145    pub fn from_mut_str(s: &mut str) -> Result<&mut Self, Error> {
146        TryFrom::try_from(s)
147    }
148
149    /// Creates a new `&TimeOffsetStr` from a byte slice.
150    ///
151    /// # Examples
152    ///
153    /// ```
154    /// # use datetime_string::rfc3339::TimeOffsetStr;
155    /// let time = TimeOffsetStr::from_bytes(b"-12:34")?;
156    /// assert_eq!(time.as_str(), "-12:34");
157    ///
158    /// assert!(TimeOffsetStr::from_bytes(b"Z").is_ok());
159    /// assert!(TimeOffsetStr::from_bytes(b"+00:00").is_ok());
160    /// assert!(TimeOffsetStr::from_bytes(b"+23:59").is_ok());
161    /// assert!(TimeOffsetStr::from_bytes(b"-00:00").is_ok());
162    /// assert!(TimeOffsetStr::from_bytes(b"-23:59").is_ok());
163    ///
164    /// assert!(TimeOffsetStr::from_bytes(b"z").is_err(), "lowercase Z is not allowed");
165    /// assert!(TimeOffsetStr::from_bytes(b"a").is_err(), "Invalid name");
166    /// assert!(TimeOffsetStr::from_bytes(b"+24:00").is_err(), "Invalid hour");
167    /// assert!(TimeOffsetStr::from_bytes(b"+00:60").is_err(), "Invalid minute");
168    /// assert!(TimeOffsetStr::from_bytes(b"-24:00").is_err(), "Invalid hour");
169    /// assert!(TimeOffsetStr::from_bytes(b"-00:60").is_err(), "Invalid minute");
170    /// assert!(TimeOffsetStr::from_bytes(b"?00:00").is_err(), "Invalid sign");
171    /// assert!(TimeOffsetStr::from_bytes(b"00:00").is_err(), "Sign is missing");
172    /// # Ok::<_, datetime_string::Error>(())
173    /// ```
174    #[inline]
175    pub fn from_bytes(s: &[u8]) -> Result<&Self, Error> {
176        TryFrom::try_from(s)
177    }
178
179    /// Creates a new `&mut TimeOffsetStr` from a mutable byte slice.
180    ///
181    /// # Examples
182    ///
183    /// ```
184    /// # use datetime_string::rfc3339::TimeOffsetStr;
185    /// use datetime_string::common::TimeOffsetSign;
186    ///
187    /// let mut buf: [u8; 6] = *b"-12:34";
188    /// let offset = TimeOffsetStr::from_bytes_mut(&mut buf)?;
189    /// assert_eq!(offset.as_str(), "-12:34");
190    ///
191    /// offset.to_numoffset_mut().unwrap().set_sign(TimeOffsetSign::Positive);
192    /// assert_eq!(offset.as_str(), "+12:34");
193    /// # Ok::<_, datetime_string::Error>(())
194    /// ```
195    #[inline]
196    pub fn from_bytes_mut(s: &mut [u8]) -> Result<&mut Self, Error> {
197        TryFrom::try_from(s)
198    }
199
200    /// Returns a string slice.
201    ///
202    /// # Examples
203    ///
204    /// ```
205    /// # use datetime_string::rfc3339::TimeOffsetStr;
206    /// let time = TimeOffsetStr::from_str("-12:34")?;
207    ///
208    /// assert_eq!(time.as_str(), "-12:34");
209    /// # Ok::<_, datetime_string::Error>(())
210    /// ```
211    #[inline]
212    #[must_use]
213    pub fn as_str(&self) -> &str {
214        unsafe {
215            // This is safe because the `TimeOffsetStr` ensures that the
216            // underlying bytes are ASCII string.
217            debug_assert_safe_version_ok!(str::from_utf8(&self.0));
218            str::from_utf8_unchecked(&self.0)
219        }
220    }
221
222    /// Returns a byte slice.
223    ///
224    /// If you want to use indexed access, prefer [`as_bytes_fixed_len`].
225    ///
226    /// # Examples
227    ///
228    /// ```
229    /// # use datetime_string::rfc3339::TimeOffsetStr;
230    /// let time = TimeOffsetStr::from_str("-12:34")?;
231    ///
232    /// assert_eq!(time.as_bytes(), b"-12:34");
233    /// # Ok::<_, datetime_string::Error>(())
234    /// ```
235    ///
236    /// [`as_bytes_fixed_len`]: #method.as_bytes_fixed_len
237    #[inline]
238    #[must_use]
239    pub fn as_bytes(&self) -> &[u8] {
240        &self.0
241    }
242
243    /// Returns a sign if available.
244    ///
245    /// # Examples
246    ///
247    /// ```
248    /// # use datetime_string::rfc3339::TimeOffsetStr;
249    /// use datetime_string::common::TimeOffsetSign;
250    ///
251    /// let positive = TimeOffsetStr::from_str("+12:34")?;
252    /// assert_eq!(positive.sign(), Some(TimeOffsetSign::Positive));
253    ///
254    /// let negative = TimeOffsetStr::from_str("-00:00")?;
255    /// assert_eq!(negative.sign(), Some(TimeOffsetSign::Negative));
256    ///
257    /// let zulu = TimeOffsetStr::from_str("Z")?;
258    /// assert_eq!(zulu.sign(), None);
259    /// # Ok::<_, datetime_string::Error>(())
260    /// ```
261    #[inline]
262    pub fn sign(&self) -> Option<TimeOffsetSign> {
263        match self.0[0] {
264            b'Z' => None,
265            b'+' => Some(TimeOffsetSign::Positive),
266            v => {
267                debug_assert_eq!(v, b'-');
268                Some(TimeOffsetSign::Negative)
269            }
270        }
271    }
272
273    /// Returns a `&TimeNumOffsetStr` if the time offset is not `Z`.
274    ///
275    /// # Examples
276    ///
277    /// ```
278    /// # use datetime_string::rfc3339::TimeOffsetStr;
279    /// let numoffset = TimeOffsetStr::from_str("+12:34")?;
280    /// assert_eq!(numoffset.to_numoffset().unwrap().hour_abs(), 12);
281    ///
282    /// let zulu = TimeOffsetStr::from_str("Z")?;
283    /// assert_eq!(zulu.to_numoffset(), None);
284    /// # Ok::<_, datetime_string::Error>(())
285    /// ```
286    #[inline]
287    pub fn to_numoffset(&self) -> Option<&TimeNumOffsetStr> {
288        if self.len() == 1 {
289            return None;
290        }
291        Some(unsafe {
292            // This is safe because `time-offset` is "Z" or `time-numoffset`,
293            // and the string is already checked that not being "Z".
294            debug_assert_safe_version_ok!(TimeNumOffsetStr::from_bytes(&self.0));
295            TimeNumOffsetStr::from_bytes_maybe_unchecked(&self.0)
296        })
297    }
298
299    /// Returns a `&mut TimeNumOffsetStr` if the time offset is not `Z`.
300    ///
301    /// # Examples
302    ///
303    /// ```
304    /// # use datetime_string::rfc3339::TimeOffsetStr;
305    /// let mut buf_num = "+12:34".to_owned();
306    /// let numoffset = TimeOffsetStr::from_mut_str(&mut buf_num)?;
307    /// numoffset.to_numoffset_mut().unwrap().set_hour_abs(23);
308    /// assert_eq!(numoffset.as_str(), "+23:34");
309    /// assert_eq!(buf_num, "+23:34");
310    ///
311    /// let mut buf_zulu = "Z".to_owned();
312    /// let zulu = TimeOffsetStr::from_mut_str(&mut buf_zulu)?;
313    /// assert_eq!(zulu.to_numoffset_mut(), None);
314    /// # Ok::<_, datetime_string::Error>(())
315    /// ```
316    #[inline]
317    // This mimics API of `std::path::Path::to_str(&self) -> Option<&str>`, and
318    // `to_*` seems more appropriate than `as_*` (because this method does not
319    // return a reference directly).
320    #[allow(clippy::wrong_self_convention)]
321    pub fn to_numoffset_mut(&mut self) -> Option<&mut TimeNumOffsetStr> {
322        if self.len() == 1 {
323            return None;
324        }
325        Some(unsafe {
326            // This is safe because `time-offset` is "Z" or `time-numoffset`,
327            // the string is already checked that not being "Z", and
328            // `TimeNumOffsetStr` ensures that the underlying bytes are ASCII
329            // string after modification.
330            debug_assert_ok!(TimeNumOffsetStr::from_bytes(&self.0));
331            TimeNumOffsetStr::from_bytes_maybe_unchecked_mut(&mut self.0)
332        })
333    }
334
335    /// Returns the absolute hour as an integer.
336    ///
337    /// # Examples
338    ///
339    /// ```
340    /// # use datetime_string::rfc3339::TimeOffsetStr;
341    /// let offset = TimeOffsetStr::from_str("-12:34")?;
342    /// assert_eq!(offset.hour_abs(), 12);
343    ///
344    /// let zulu = TimeOffsetStr::from_str("Z")?;
345    /// assert_eq!(zulu.hour_abs(), 0);
346    ///
347    /// let negative0 = TimeOffsetStr::from_str("-00:00")?;
348    /// assert_eq!(negative0.hour_abs(), 0);
349    /// # Ok::<_, datetime_string::Error>(())
350    /// ```
351    #[inline]
352    #[must_use]
353    pub fn hour_abs(&self) -> u8 {
354        self.to_numoffset().map_or(0, |v| v.hour_abs())
355    }
356
357    /// Returns the signed hour as an integer.
358    ///
359    /// # Examples
360    ///
361    /// ```
362    /// # use datetime_string::rfc3339::TimeOffsetStr;
363    /// let offset = TimeOffsetStr::from_str("-12:34")?;
364    /// assert_eq!(offset.hour_signed(), -12);
365    ///
366    /// let zulu = TimeOffsetStr::from_str("Z")?;
367    /// assert_eq!(zulu.hour_signed(), 0);
368    /// # Ok::<_, datetime_string::Error>(())
369    /// ```
370    ///
371    /// Note that both `+00` and `-00` are treaded as the same 0.
372    ///
373    /// ```
374    /// # use datetime_string::rfc3339::TimeOffsetStr;
375    /// let positive = TimeOffsetStr::from_str("+00:59")?;
376    /// assert_eq!(positive.hour_signed(), 0);
377    ///
378    /// let negative = TimeOffsetStr::from_str("-00:59")?;
379    /// assert_eq!(negative.hour_signed(), 0);
380    /// # Ok::<_, datetime_string::Error>(())
381    /// ```
382    #[inline]
383    #[must_use]
384    pub fn hour_signed(&self) -> i8 {
385        self.to_numoffset().map_or(0, |v| v.hour_signed())
386    }
387
388    /// Returns the minute as an integer.
389    ///
390    /// # Examples
391    ///
392    /// ```
393    /// # use datetime_string::rfc3339::TimeOffsetStr;
394    /// let offset = TimeOffsetStr::from_str("-12:34")?;
395    /// assert_eq!(offset.minute(), 34);
396    ///
397    /// let zulu = TimeOffsetStr::from_str("Z")?;
398    /// assert_eq!(zulu.minute(), 0);
399    /// # Ok::<_, datetime_string::Error>(())
400    /// ```
401    #[inline]
402    #[must_use]
403    pub fn minute(&self) -> u8 {
404        self.to_numoffset().map_or(0, |v| v.minute())
405    }
406
407    /// Returns the time offset in minutes.
408    ///
409    /// Note that both `+00:00` and `-00:00` is considered as 0 minutes offset.
410    /// RFC 3339 defines semantics of `-00:00` as "unknown local offset".
411    /// If your application should be aware of that semantics, use
412    /// [`is_unknown_local_offset`] method or [`sign`] method to distinguish them.
413    ///
414    /// # Examples
415    ///
416    /// ```
417    /// # use datetime_string::rfc3339::TimeOffsetStr;
418    /// let offset = TimeOffsetStr::from_str("-12:34")?;
419    /// assert_eq!(offset.in_minutes(), -(12 * 60 + 34));
420    ///
421    /// let zulu = TimeOffsetStr::from_str("Z")?;
422    /// assert_eq!(zulu.in_minutes(), 0);
423    /// # Ok::<_, datetime_string::Error>(())
424    /// ```
425    ///
426    /// `0` is returned for both `+00:00` and `-00:00`.
427    ///
428    /// ```
429    /// # use datetime_string::rfc3339::TimeOffsetStr;
430    /// use datetime_string::common::TimeOffsetSign;
431    ///
432    /// let positive0 = TimeOffsetStr::from_str("+00:00")?;
433    /// assert_eq!(positive0.in_minutes(), 0);
434    /// assert_eq!(positive0.sign(), Some(TimeOffsetSign::Positive));
435    /// assert!(!positive0.is_unknown_local_offset(), "0 minutes time offset");
436    ///
437    /// let negative0 = TimeOffsetStr::from_str("-00:00")?;
438    /// assert_eq!(negative0.in_minutes(), 0);
439    /// assert_eq!(negative0.sign(), Some(TimeOffsetSign::Negative));
440    /// assert!(negative0.is_unknown_local_offset(), "unknown local offset");
441    /// # Ok::<_, datetime_string::Error>(())
442    /// ```
443    ///
444    /// [`is_unknown_local_offset`]: TimeOffsetStr::is_unknown_local_offset
445    /// [`sign`]: TimeOffsetStr::sign
446    #[inline]
447    #[must_use]
448    pub fn in_minutes(&self) -> i16 {
449        self.to_numoffset().map_or(0, |v| v.in_minutes())
450    }
451
452    /// Returns `true` if and only if the time offset means "unknown local offset" in RFC 3339.
453    ///
454    /// RFC 3339 defines `-00:00` as "unknown local offset".
455    ///
456    /// > If the time in UTC is known, but the offset to local time is unknown,
457    /// > this can be represented with an offset of "-00:00".
458    /// > This differs semantically from an offset of "Z" or "+00:00", which
459    /// > imply that UTC is the preferred reference point for the specified
460    /// > time.
461    /// >
462    /// > --- [RFC 3339, section 4.3. Unknown Local Offset Convention][rfc3339-4-3]
463    ///
464    /// This method returns `true` for `-00:00`.
465    ///
466    /// # Examples
467    ///
468    /// ```
469    /// # use datetime_string::rfc3339::TimeOffsetStr;
470    /// use datetime_string::common::TimeOffsetSign;
471    ///
472    /// let positive0 = TimeOffsetStr::from_str("+00:00")?;
473    /// assert!(!positive0.is_unknown_local_offset(), "0 minutes time offset");
474    ///
475    /// let zulu = TimeOffsetStr::from_str("Z")?;
476    /// assert!(!zulu.is_unknown_local_offset(), "UTC");
477    ///
478    /// let negative0 = TimeOffsetStr::from_str("-00:00")?;
479    /// assert!(negative0.is_unknown_local_offset(), "unknown local offset");
480    /// # Ok::<_, datetime_string::Error>(())
481    /// ```
482    ///
483    /// [rfc3339-4-3]: https://tools.ietf.org/html/rfc3339#section-4.3
484    #[inline]
485    #[must_use]
486    pub fn is_unknown_local_offset(&self) -> bool {
487        &self.0 == b"-00:00"
488    }
489}
490
491impl AsRef<[u8]> for TimeOffsetStr {
492    #[inline]
493    fn as_ref(&self) -> &[u8] {
494        &self.0
495    }
496}
497
498impl AsRef<str> for TimeOffsetStr {
499    #[inline]
500    fn as_ref(&self) -> &str {
501        self.as_str()
502    }
503}
504
505impl AsRef<TimeOffsetStr> for TimeOffsetStr {
506    #[inline]
507    fn as_ref(&self) -> &TimeOffsetStr {
508        self
509    }
510}
511
512impl AsMut<TimeOffsetStr> for TimeOffsetStr {
513    #[inline]
514    fn as_mut(&mut self) -> &mut TimeOffsetStr {
515        self
516    }
517}
518
519impl<'a> From<&'a TimeOffsetStr> for &'a str {
520    #[inline]
521    fn from(v: &'a TimeOffsetStr) -> Self {
522        v.as_str()
523    }
524}
525
526#[cfg(feature = "chrono04")]
527#[cfg_attr(docsrs, doc(cfg(feature = "chrono04")))]
528impl From<&TimeOffsetStr> for chrono04::FixedOffset {
529    #[inline]
530    fn from(v: &TimeOffsetStr) -> Self {
531        Self::east(i32::from(v.in_minutes()) * 60)
532    }
533}
534
535#[cfg(feature = "time03")]
536#[cfg_attr(docsrs, doc(cfg(feature = "time03")))]
537impl From<&TimeOffsetStr> for time03::UtcOffset {
538    #[inline]
539    fn from(v: &TimeOffsetStr) -> Self {
540        Self::from_whole_seconds(i32::from(v.in_minutes()) * 60)
541            .expect("[validity] the minutes-precision offset must be valid and representable")
542    }
543}
544
545impl<'a> TryFrom<&'a [u8]> for &'a TimeOffsetStr {
546    type Error = Error;
547
548    #[inline]
549    fn try_from(v: &'a [u8]) -> Result<Self, Self::Error> {
550        validate_bytes(v)?;
551        Ok(unsafe {
552            // This is safe because a valid `time-offset` string is also an ASCII string.
553            TimeOffsetStr::from_bytes_maybe_unchecked(v)
554        })
555    }
556}
557
558impl<'a> TryFrom<&'a mut [u8]> for &'a mut TimeOffsetStr {
559    type Error = Error;
560
561    #[inline]
562    fn try_from(v: &'a mut [u8]) -> Result<Self, Self::Error> {
563        validate_bytes(v)?;
564        Ok(unsafe {
565            // This is safe because a valid `time-offset` string is also an ASCII string.
566            TimeOffsetStr::from_bytes_maybe_unchecked_mut(v)
567        })
568    }
569}
570
571impl<'a> TryFrom<&'a str> for &'a TimeOffsetStr {
572    type Error = Error;
573
574    #[inline]
575    fn try_from(v: &'a str) -> Result<Self, Self::Error> {
576        Self::try_from(v.as_bytes())
577    }
578}
579
580impl<'a> TryFrom<&'a mut str> for &'a mut TimeOffsetStr {
581    type Error = Error;
582
583    #[inline]
584    fn try_from(v: &'a mut str) -> Result<Self, Self::Error> {
585        validate_bytes(v.as_bytes())?;
586        Ok(unsafe {
587            // This is safe because a valid `time-offset` string is also an ASCII string.
588            TimeOffsetStr::from_str_maybe_unchecked_mut(v)
589        })
590    }
591}
592
593impl fmt::Display for TimeOffsetStr {
594    #[inline]
595    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
596        self.as_str().fmt(f)
597    }
598}
599
600impl ops::Deref for TimeOffsetStr {
601    type Target = str;
602
603    #[inline]
604    fn deref(&self) -> &Self::Target {
605        self.as_str()
606    }
607}
608
609#[cfg(feature = "serde")]
610impl serde::Serialize for TimeOffsetStr {
611    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
612    where
613        S: serde::Serializer,
614    {
615        serializer.serialize_str(self.as_str())
616    }
617}
618
619impl_cmp_symmetric!(str, TimeOffsetStr, &TimeOffsetStr);
620impl_cmp_symmetric!([u8], TimeOffsetStr, [u8]);
621impl_cmp_symmetric!([u8], TimeOffsetStr, &[u8]);
622impl_cmp_symmetric!([u8], &TimeOffsetStr, [u8]);
623impl_cmp_symmetric!(str, TimeOffsetStr, str);
624impl_cmp_symmetric!(str, TimeOffsetStr, &str);
625impl_cmp_symmetric!(str, &TimeOffsetStr, str);
626
627/// Items for serde support.
628#[cfg(feature = "serde")]
629mod serde_ {
630    use super::*;
631
632    use serde::de::{Deserialize, Deserializer, Visitor};
633
634    /// Visitor for `&TimeOffsetStr`.
635    struct StrVisitor;
636
637    impl<'de> Visitor<'de> for StrVisitor {
638        type Value = &'de TimeOffsetStr;
639
640        #[inline]
641        fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
642            f.write_str("RFC 3339 time-offset string")
643        }
644
645        #[inline]
646        fn visit_borrowed_bytes<E>(self, v: &'de [u8]) -> Result<Self::Value, E>
647        where
648            E: serde::de::Error,
649        {
650            Self::Value::try_from(v).map_err(E::custom)
651        }
652
653        #[inline]
654        fn visit_borrowed_str<E>(self, v: &'de str) -> Result<Self::Value, E>
655        where
656            E: serde::de::Error,
657        {
658            Self::Value::try_from(v).map_err(E::custom)
659        }
660    }
661
662    impl<'de> Deserialize<'de> for &'de TimeOffsetStr {
663        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
664        where
665            D: Deserializer<'de>,
666        {
667            deserializer.deserialize_any(StrVisitor)
668        }
669    }
670}
671
672#[cfg(test)]
673mod tests {
674    use super::*;
675
676    use super::validate_bytes as s_validate;
677
678    #[cfg(feature = "serde")]
679    use serde_test::{assert_de_tokens, assert_tokens, Token};
680
681    #[test]
682    fn validate_bytes() {
683        assert!(s_validate(b"Z").is_ok());
684        assert!(s_validate(b"-00:00").is_ok());
685        assert!(s_validate(b"-12:30").is_ok());
686        assert!(s_validate(b"-23:59").is_ok());
687        assert!(s_validate(b"+00:00").is_ok());
688        assert!(s_validate(b"+12:30").is_ok());
689        assert!(s_validate(b"+23:59").is_ok());
690
691        assert!(TimeOffsetStr::from_str("z").is_err());
692        assert!(TimeOffsetStr::from_str("a").is_err());
693        assert!(TimeOffsetStr::from_str("+24:00").is_err());
694        assert!(TimeOffsetStr::from_str("+00:60").is_err());
695        assert!(TimeOffsetStr::from_str("-24:00").is_err());
696        assert!(TimeOffsetStr::from_str("-00:60").is_err());
697        assert!(TimeOffsetStr::from_str("?00:00").is_err());
698        assert!(TimeOffsetStr::from_str("00:00").is_err());
699    }
700
701    #[cfg(feature = "serde")]
702    #[test]
703    fn ser_de_str() {
704        let raw: &'static str = "-12:34";
705        assert_tokens(
706            &TimeOffsetStr::from_str(raw).unwrap(),
707            &[Token::BorrowedStr(raw)],
708        );
709    }
710
711    #[cfg(feature = "serde")]
712    #[test]
713    fn de_bytes_slice() {
714        let raw: &'static [u8; 6] = b"-12:34";
715        assert_de_tokens(
716            &TimeOffsetStr::from_bytes(raw).unwrap(),
717            &[Token::BorrowedBytes(raw)],
718        );
719    }
720}