datetime_string/rfc3339/offset/
owned.rs

1//! RFC 3339 [`time-offset`] owned string type.
2//!
3//! [`time-offset`]: https://tools.ietf.org/html/rfc3339#section-5.6
4#![cfg(feature = "alloc")]
5
6use core::{convert::TryFrom, fmt, ops, str};
7
8use alloc::{borrow::ToOwned, string::String, vec::Vec};
9
10use crate::{
11    common::{TimeNumOffsetColonStr, TimeNumOffsetColonString, TimeOffsetSign},
12    error::{ConversionError, Error},
13};
14
15use super::{validate_bytes, TimeOffsetStr};
16
17/// Owned string for a time in RFC 3339 [`time-offset`] format, such as `+09:00`, `-00:00`, and `Z`.
18///
19/// Available when `alloc` feature is enabled.
20///
21/// # Examples
22///
23/// ```
24/// # use datetime_string::rfc3339::TimeOffsetString;
25/// use datetime_string::rfc3339::TimeOffsetStr;
26/// use std::convert::TryFrom;
27///
28/// let try_from = TimeOffsetString::try_from("-12:34")?;
29///
30/// let parse = "-12:34".parse::<TimeOffsetString>()?;
31/// let parse2: TimeOffsetString = "-12:34".parse()?;
32///
33/// let to_owned = TimeOffsetStr::from_str("-12:34")?.to_owned();
34/// let into: TimeOffsetString = TimeOffsetStr::from_str("-12:34")?.into();
35/// # Ok::<_, datetime_string::Error>(())
36/// ```
37///
38/// [`time-offset`]: https://tools.ietf.org/html/rfc3339#section-5.6
39#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
40#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
41#[repr(transparent)]
42// Note that `derive(Serialize)` cannot used here, because it encodes this as
43// `[u8; 8]` 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 TimeOffsetString(Vec<u8>);
51
52impl TimeOffsetString {
53    /// Creates a `TimeOffsetString` from the given bytes.
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    unsafe fn from_bytes_maybe_unchecked(s: Vec<u8>) -> Self {
63        debug_assert_ok!(validate_bytes(&s));
64        Self(s)
65    }
66
67    /// Returns `Z`.
68    ///
69    /// # Examples
70    ///
71    /// ```
72    /// # use datetime_string::rfc3339::TimeOffsetString;
73    /// let z = TimeOffsetString::z();
74    ///
75    /// assert_eq!(z.as_str(), "Z");
76    /// # Ok::<_, datetime_string::Error>(())
77    /// ```
78    #[inline]
79    #[must_use]
80    pub fn z() -> Self {
81        let z = *b"Z";
82        unsafe {
83            // This is safe because `Z` is valid `time-offset` string.
84            debug_assert_ok!(validate_bytes(&z));
85            Self::from_bytes_maybe_unchecked(z[..].to_owned())
86        }
87    }
88
89    /// Returns `-00:00`, which is defined in RFC 3339 to indicate "unknown local offset".
90    ///
91    /// # Examples
92    ///
93    /// ```
94    /// # use datetime_string::rfc3339::TimeOffsetString;
95    /// let unknown_local_offset = TimeOffsetString::unknown_local_offset();
96    /// assert_eq!(unknown_local_offset.as_str(), "-00:00");
97    /// # Ok::<_, datetime_string::Error>(())
98    /// ```
99    #[inline]
100    #[must_use]
101    pub fn unknown_local_offset() -> Self {
102        TimeNumOffsetColonString::unknown_local_offset().into()
103    }
104
105    /// Creates a new `TimeNumOffsetColonStr` from the given minutes.
106    ///
107    /// # Examples
108    ///
109    /// ```
110    /// # use datetime_string::rfc3339::TimeOffsetString;
111    /// let offset = TimeOffsetString::from_minutes(9 * 60)?;
112    /// assert_eq!(offset.as_str(), "+09:00");
113    ///
114    /// assert!(TimeOffsetString::from_minutes(-24 * 60).is_err(), "-24:00 is invaild time");
115    /// # Ok::<_, datetime_string::Error>(())
116    /// ```
117    pub fn from_minutes(minutes: i16) -> Result<Self, Error> {
118        TimeNumOffsetColonString::from_minutes(minutes).map(Into::into)
119    }
120
121    /// Creates a new `TimeNumOffsetColonStr` from the given minutes.
122    ///
123    /// # Examples
124    ///
125    /// ```
126    /// # use datetime_string::rfc3339::TimeOffsetString;
127    /// use datetime_string::common::TimeOffsetSign;
128    /// let offset = TimeOffsetString::from_sign_and_hm(TimeOffsetSign::Positive, 9, 30)?;
129    /// assert_eq!(offset.as_str(), "+09:30");
130    ///
131    /// let unknown_local_offset = TimeOffsetString::from_sign_and_hm(TimeOffsetSign::Negative, 0, 0)?;
132    /// assert_eq!(unknown_local_offset.as_str(), "-00:00");
133    ///
134    /// assert!(
135    ///     TimeOffsetString::from_sign_and_hm(TimeOffsetSign::Negative, 24, 0).is_err(),
136    ///     "-24:00 is invaild time"
137    /// );
138    /// # Ok::<_, datetime_string::Error>(())
139    /// ```
140    pub fn from_sign_and_hm(sign: TimeOffsetSign, hour_abs: u8, minute: u8) -> Result<Self, Error> {
141        TimeNumOffsetColonString::from_sign_and_hm(sign, hour_abs, minute).map(Into::into)
142    }
143
144    /// Creates a new `TimeNumOffsetColonStr` from the given minutes.
145    ///
146    /// # Examples
147    ///
148    /// ```
149    /// # use datetime_string::rfc3339::TimeOffsetString;
150    /// let offset = TimeOffsetString::from_hm_signed(-9, 0)?;
151    /// assert_eq!(offset.as_str(), "-09:00");
152    ///
153    /// let unknown_local_offset = TimeOffsetString::from_hm_signed(0, 0)?;
154    /// assert_eq!(unknown_local_offset.as_str(), "+00:00");
155    ///
156    /// assert!( TimeOffsetString::from_hm_signed(-24, 0).is_err(), "-24:00 is invaild time");
157    /// # Ok::<_, datetime_string::Error>(())
158    /// ```
159    pub fn from_hm_signed(hour: i8, minute: u8) -> Result<Self, Error> {
160        TimeNumOffsetColonString::from_hm_signed(hour, minute).map(Into::into)
161    }
162
163    /// Returns a `&TimeOffsetStr` for the string.
164    ///
165    /// # Examples
166    ///
167    /// ```
168    /// # use datetime_string::rfc3339::TimeOffsetString;
169    /// use datetime_string::rfc3339::TimeOffsetStr;
170    ///
171    /// let secfrac = "-12:34".parse::<TimeOffsetString>()?;
172    ///
173    /// // Usually you don't need to call `as_deref()` explicitly, because
174    /// // `Deref<Target = TimeOffsetStr>` trait is implemented.
175    /// let _: &TimeOffsetStr = secfrac.as_deref();
176    /// # Ok::<_, datetime_string::Error>(())
177    /// ```
178    #[inline]
179    #[must_use]
180    pub fn as_deref(&self) -> &TimeOffsetStr {
181        unsafe {
182            // This is safe because a valid `time-offset` string is also an ASCII string.
183            debug_assert_safe_version_ok!(TimeOffsetStr::from_bytes(&self.0));
184            TimeOffsetStr::from_bytes_maybe_unchecked(&self.0)
185        }
186    }
187
188    /// Returns a `&mut TimeOffsetStr` for the string.
189    ///
190    /// # Examples
191    ///
192    /// ```
193    /// # use datetime_string::rfc3339::TimeOffsetString;
194    /// use datetime_string::rfc3339::TimeOffsetStr;
195    ///
196    /// let mut secfrac = "-12:34".parse::<TimeOffsetString>()?;
197    ///
198    /// // Usually you don't need to call `as_deref_mut()` explicitly, because
199    /// // `DerefMut` trait is implemented.
200    /// let _: &mut TimeOffsetStr = secfrac.as_deref_mut();
201    /// # Ok::<_, datetime_string::Error>(())
202    /// ```
203    #[inline]
204    #[must_use]
205    pub fn as_deref_mut(&mut self) -> &mut TimeOffsetStr {
206        unsafe {
207            // This is safe because a valid `time-offset` string is also an
208            // ASCII string, and `TimeOffsetStr` ensures that the underlying
209            // bytes are ASCII string after modification.
210            debug_assert_ok!(TimeOffsetStr::from_bytes(&self.0));
211            TimeOffsetStr::from_bytes_maybe_unchecked_mut(&mut self.0)
212        }
213    }
214}
215
216impl core::borrow::Borrow<TimeOffsetStr> for TimeOffsetString {
217    #[inline]
218    fn borrow(&self) -> &TimeOffsetStr {
219        self.as_deref()
220    }
221}
222
223impl core::borrow::BorrowMut<TimeOffsetStr> for TimeOffsetString {
224    #[inline]
225    fn borrow_mut(&mut self) -> &mut TimeOffsetStr {
226        self.as_deref_mut()
227    }
228}
229
230impl alloc::borrow::ToOwned for TimeOffsetStr {
231    type Owned = TimeOffsetString;
232
233    #[inline]
234    fn to_owned(&self) -> Self::Owned {
235        self.into()
236    }
237}
238
239impl AsRef<[u8]> for TimeOffsetString {
240    #[inline]
241    fn as_ref(&self) -> &[u8] {
242        self.as_bytes()
243    }
244}
245
246impl AsRef<str> for TimeOffsetString {
247    #[inline]
248    fn as_ref(&self) -> &str {
249        self.as_str()
250    }
251}
252
253impl AsRef<TimeOffsetStr> for TimeOffsetString {
254    #[inline]
255    fn as_ref(&self) -> &TimeOffsetStr {
256        self
257    }
258}
259
260impl AsMut<TimeOffsetStr> for TimeOffsetString {
261    #[inline]
262    fn as_mut(&mut self) -> &mut TimeOffsetStr {
263        self
264    }
265}
266
267impl From<&TimeNumOffsetColonStr> for TimeOffsetString {
268    #[inline]
269    fn from(v: &TimeNumOffsetColonStr) -> TimeOffsetString {
270        unsafe {
271            // This is safe because `TimeNumOffsetColonStr` is subset of `TimeOffsetStr`.
272            debug_assert_ok!(validate_bytes(v.as_bytes()));
273            Self::from_bytes_maybe_unchecked(v.as_bytes().to_owned())
274        }
275    }
276}
277
278impl From<TimeNumOffsetColonString> for TimeOffsetString {
279    #[inline]
280    fn from(v: TimeNumOffsetColonString) -> TimeOffsetString {
281        unsafe {
282            // This is safe because `TimeNumOffsetColonStr` is subset of `TimeOffsetStr`.
283            debug_assert_ok!(validate_bytes(v.as_bytes()));
284            Self::from_bytes_maybe_unchecked(v.as_bytes().to_owned())
285        }
286    }
287}
288
289impl From<TimeOffsetString> for Vec<u8> {
290    #[inline]
291    fn from(v: TimeOffsetString) -> Vec<u8> {
292        v.0
293    }
294}
295
296impl From<TimeOffsetString> for String {
297    #[inline]
298    fn from(v: TimeOffsetString) -> String {
299        unsafe {
300            // This is safe because a valid `TimeOffsetString` is an ASCII string.
301            debug_assert_ok!(str::from_utf8(&v.0));
302            String::from_utf8_unchecked(v.0)
303        }
304    }
305}
306
307impl From<&TimeOffsetStr> for TimeOffsetString {
308    fn from(v: &TimeOffsetStr) -> Self {
309        unsafe {
310            // This is safe because the value is already validated.
311            debug_assert_ok!(validate_bytes(&v.0));
312            Self::from_bytes_maybe_unchecked(v.0.into())
313        }
314    }
315}
316
317impl TryFrom<&[u8]> for TimeOffsetString {
318    type Error = Error;
319
320    #[inline]
321    fn try_from(v: &[u8]) -> Result<Self, Self::Error> {
322        TimeOffsetStr::from_bytes(v).map(Into::into)
323    }
324}
325
326impl TryFrom<&str> for TimeOffsetString {
327    type Error = Error;
328
329    #[inline]
330    fn try_from(v: &str) -> Result<Self, Self::Error> {
331        TimeOffsetStr::from_str(v).map(Into::into)
332    }
333}
334
335impl TryFrom<Vec<u8>> for TimeOffsetString {
336    type Error = ConversionError<Vec<u8>>;
337
338    #[inline]
339    fn try_from(v: Vec<u8>) -> Result<Self, Self::Error> {
340        match validate_bytes(&v) {
341            Ok(_) => Ok(unsafe {
342                // This is safe because the value is successfully validated.
343                Self::from_bytes_maybe_unchecked(v)
344            }),
345            Err(e) => Err(ConversionError::new(v, e)),
346        }
347    }
348}
349
350impl TryFrom<String> for TimeOffsetString {
351    type Error = ConversionError<String>;
352
353    #[inline]
354    fn try_from(v: String) -> Result<Self, Self::Error> {
355        match validate_bytes(v.as_bytes()) {
356            Ok(_) => Ok(unsafe {
357                // This is safe because the value is successfully validated.
358                Self::from_bytes_maybe_unchecked(v.into_bytes())
359            }),
360            Err(e) => Err(ConversionError::new(v, e)),
361        }
362    }
363}
364
365impl fmt::Display for TimeOffsetString {
366    #[inline]
367    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
368        self.as_deref().fmt(f)
369    }
370}
371
372impl ops::Deref for TimeOffsetString {
373    type Target = TimeOffsetStr;
374
375    #[inline]
376    fn deref(&self) -> &Self::Target {
377        self.as_deref()
378    }
379}
380
381impl ops::DerefMut for TimeOffsetString {
382    #[inline]
383    fn deref_mut(&mut self) -> &mut Self::Target {
384        self.as_deref_mut()
385    }
386}
387
388impl str::FromStr for TimeOffsetString {
389    type Err = Error;
390
391    #[inline]
392    fn from_str(s: &str) -> Result<Self, Self::Err> {
393        Self::try_from(s)
394    }
395}
396
397impl_cmp_symmetric!(TimeOffsetStr, TimeOffsetString, &TimeOffsetString);
398impl_cmp_symmetric!(TimeOffsetStr, TimeOffsetString, TimeOffsetStr);
399impl_cmp_symmetric!(TimeOffsetStr, TimeOffsetString, &TimeOffsetStr);
400impl_cmp_symmetric!(str, TimeOffsetString, str);
401impl_cmp_symmetric!(str, TimeOffsetString, &str);
402impl_cmp_symmetric!(str, &TimeOffsetString, str);
403impl_cmp_symmetric!([u8], TimeOffsetString, [u8]);
404impl_cmp_symmetric!([u8], TimeOffsetString, &[u8]);
405impl_cmp_symmetric!([u8], &TimeOffsetString, [u8]);
406
407#[cfg(feature = "serde")]
408#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
409impl serde::Serialize for TimeOffsetString {
410    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
411    where
412        S: serde::Serializer,
413    {
414        serializer.serialize_str(self.as_str())
415    }
416}
417
418/// Items for serde support.
419#[cfg(feature = "serde")]
420mod serde_ {
421    use super::*;
422
423    use serde::de::{Deserialize, Deserializer, Visitor};
424
425    /// Visitor for `TimeOffsetString`.
426    struct StringVisitor;
427
428    impl<'de> Visitor<'de> for StringVisitor {
429        type Value = TimeOffsetString;
430
431        #[inline]
432        fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
433            f.write_str("RFC 3339 time-offset string")
434        }
435
436        #[inline]
437        fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
438        where
439            E: serde::de::Error,
440        {
441            Self::Value::try_from(v).map_err(E::custom)
442        }
443
444        #[inline]
445        fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
446        where
447            E: serde::de::Error,
448        {
449            Self::Value::try_from(v).map_err(E::custom)
450        }
451    }
452
453    impl<'de> Deserialize<'de> for TimeOffsetString {
454        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
455        where
456            D: Deserializer<'de>,
457        {
458            deserializer.deserialize_any(StringVisitor)
459        }
460    }
461}
462
463#[cfg(feature = "serde")]
464#[cfg(test)]
465mod tests {
466    use super::*;
467
468    use serde_test::{assert_de_tokens, assert_tokens, Token};
469
470    #[test]
471    fn ser_de_string() {
472        let raw: &'static str = "-12:34";
473        assert_tokens(
474            &TimeOffsetString::try_from(raw).unwrap(),
475            &[Token::Str(raw)],
476        );
477    }
478
479    #[test]
480    fn de_bytes() {
481        let raw: &'static [u8; 6] = b"-12:34";
482        assert_de_tokens(
483            &TimeOffsetString::try_from(&raw[..]).unwrap(),
484            &[Token::Bytes(raw)],
485        );
486    }
487}