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}