datetime_string/common/
time_num_offset_colon.rs

1//! Time offset string in `+hh:mm` or `-hh:mm` format.
2//!
3//! This is also an RFC 3339 [`time-numoffset`].
4//!
5//! [`time-numoffset`]: https://tools.ietf.org/html/rfc3339#section-5.6
6
7use core::{
8    convert::TryFrom,
9    fmt,
10    ops::{self, Range, RangeTo},
11    str,
12};
13
14use crate::{parse::parse_digits2, str::write_digit2};
15
16#[cfg(feature = "alloc")]
17use alloc::{string::String, vec::Vec};
18
19use crate::common::TimeOffsetSign;
20
21use crate::error::{ComponentKind, Error, ErrorKind};
22
23/// Length of time-numoffset (e.g. "+12:34").
24const NUM_OFFSET_LEN: usize = 6;
25/// Range of the absolute hour in the string.
26const HOUR_ABS_RANGE: Range<usize> = 1..3;
27/// Range of the signed hour in the string.
28const HOUR_SIGNED_RANGE: RangeTo<usize> = ..3;
29/// Range of the minute in the string.
30const MINUTE_RANGE: Range<usize> = 4..6;
31/// Maximum value of the hour.
32const HOUR_MAX: u8 = 23;
33/// Maximum value of the minute.
34const MINUTE_MAX: u8 = 59;
35
36/// Validates the given string as an RFC 3339 [`time-numoffset`] string.
37///
38/// [`time-numoffset`]: https://tools.ietf.org/html/rfc3339#section-5.6
39fn validate_bytes(s: &[u8]) -> Result<(), Error> {
40    let s: &[u8; NUM_OFFSET_LEN] = TryFrom::try_from(s).map_err(|_| {
41        if s.len() < NUM_OFFSET_LEN {
42            ErrorKind::TooShort
43        } else {
44            ErrorKind::TooLong
45        }
46    })?;
47
48    if ((s[0] != b'+') && (s[0] != b'-')) || (s[3] != b':') {
49        return Err(ErrorKind::InvalidSeparator.into());
50    }
51
52    let hour_s: [u8; 2] = [s[1], s[2]];
53    let minute_s: [u8; 2] = [s[4], s[5]];
54
55    if !hour_s.iter().all(u8::is_ascii_digit) {
56        return Err(ErrorKind::InvalidComponentType(ComponentKind::OffsetHour).into());
57    }
58    if !minute_s.iter().all(u8::is_ascii_digit) {
59        return Err(ErrorKind::InvalidComponentType(ComponentKind::OffsetMinute).into());
60    }
61
62    let hour = parse_digits2(hour_s);
63    if hour > HOUR_MAX {
64        return Err(ErrorKind::ComponentOutOfRange(ComponentKind::OffsetHour).into());
65    }
66    let minute = parse_digits2(minute_s);
67    if minute > MINUTE_MAX {
68        return Err(ErrorKind::ComponentOutOfRange(ComponentKind::OffsetMinute).into());
69    }
70
71    Ok(())
72}
73
74/// String slice for a time offset in `+hh:mm` or `-hh:mm` format, such as `+09:00` and `-00:00`.
75///
76/// This is also an RFC 3339 [`time-numoffset`] string.
77///
78/// [`time-numoffset`]: https://tools.ietf.org/html/rfc3339#section-5.6
79#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
80#[repr(transparent)]
81// Note that `derive(Serialize)` cannot used here, because it encodes this as
82// `[u8]` rather than as a string.
83//
84// Comparisons implemented for the type are consistent (at least it is intended to be so).
85// See <https://github.com/rust-lang/rust-clippy/issues/2025>.
86// Note that `clippy::derive_ord_xor_partial_ord` would be introduced since Rust 1.47.0.
87#[allow(clippy::derive_hash_xor_eq)]
88#[allow(unknown_lints, clippy::derive_ord_xor_partial_ord)]
89pub struct TimeNumOffsetColonStr([u8]);
90
91impl TimeNumOffsetColonStr {
92    /// Creates a `&TimeNumOffsetColonStr` from the given byte slice.
93    ///
94    /// This performs assertion in debug build, but not in release build.
95    ///
96    /// # Safety
97    ///
98    /// `validate_bytes(s)` should return `Ok(())`.
99    #[inline]
100    #[must_use]
101    pub(crate) unsafe fn from_bytes_maybe_unchecked(s: &[u8]) -> &Self {
102        debug_assert_ok!(validate_bytes(s));
103        &*(s as *const [u8] as *const Self)
104    }
105
106    /// Creates a `&mut TimeNumOffsetColonStr` from the given mutable byte slice.
107    ///
108    /// This performs assertion in debug build, but not in release build.
109    ///
110    /// # Safety
111    ///
112    /// `validate_bytes(s)` should return `Ok(())`.
113    #[inline]
114    #[must_use]
115    pub(crate) unsafe fn from_bytes_maybe_unchecked_mut(s: &mut [u8]) -> &mut Self {
116        debug_assert_ok!(validate_bytes(s));
117        &mut *(s as *mut [u8] as *mut Self)
118    }
119
120    /// Creates a `&mut TimeNumOffsetColonStr` from the given mutable string slice.
121    ///
122    /// This performs assertion in debug build, but not in release build.
123    ///
124    /// # Safety
125    ///
126    /// `validate_bytes(s.as_bytes())` should return `Ok(())`.
127    #[inline]
128    #[must_use]
129    unsafe fn from_str_maybe_unchecked_mut(s: &mut str) -> &mut Self {
130        // This is safe because `TimeNumOffsetColonStr` ensures that the
131        // underlying bytes are ASCII string after modification.
132        Self::from_bytes_maybe_unchecked_mut(s.as_bytes_mut())
133    }
134
135    /// Creates a new `&TimeNumOffsetColonStr` from a string slice.
136    ///
137    /// # Examples
138    ///
139    /// ```
140    /// # use datetime_string::common::TimeNumOffsetColonStr;
141    /// let offset = TimeNumOffsetColonStr::from_str("-12:34")?;
142    /// assert_eq!(offset.as_str(), "-12:34");
143    ///
144    /// assert!(TimeNumOffsetColonStr::from_str("+00:00").is_ok());
145    /// assert!(TimeNumOffsetColonStr::from_str("+23:59").is_ok());
146    /// assert!(TimeNumOffsetColonStr::from_str("-00:00").is_ok());
147    /// assert!(TimeNumOffsetColonStr::from_str("-23:59").is_ok());
148    ///
149    /// assert!(TimeNumOffsetColonStr::from_str("Z").is_err(), "Not time-numoffset");
150    /// assert!(TimeNumOffsetColonStr::from_str("+24:00").is_err(), "Invalid hour");
151    /// assert!(TimeNumOffsetColonStr::from_str("+00:60").is_err(), "Invalid minute");
152    /// assert!(TimeNumOffsetColonStr::from_str("-24:00").is_err(), "Invalid hour");
153    /// assert!(TimeNumOffsetColonStr::from_str("-00:60").is_err(), "Invalid minute");
154    /// assert!(TimeNumOffsetColonStr::from_str("?00:00").is_err(), "Invalid sign");
155    /// assert!(TimeNumOffsetColonStr::from_str("00:00").is_err(), "Sign is missing");
156    /// # Ok::<_, datetime_string::Error>(())
157    /// ```
158    #[inline]
159    // `FromStr` trait cannot be implemented for a slice.
160    #[allow(clippy::should_implement_trait)]
161    pub fn from_str(s: &str) -> Result<&Self, Error> {
162        TryFrom::try_from(s)
163    }
164
165    /// Creates a new `&mut TimeNumOffsetColonStr` from a mutable string slice.
166    ///
167    /// # Examples
168    ///
169    /// ```
170    /// # use datetime_string::common::TimeNumOffsetColonStr;
171    /// use datetime_string::common::TimeOffsetSign;
172    /// let mut buf = "-12:34".to_owned();
173    /// let offset = TimeNumOffsetColonStr::from_mut_str(&mut buf)?;
174    /// assert_eq!(offset.as_str(), "-12:34");
175    ///
176    /// offset.set_sign(TimeOffsetSign::Positive);
177    /// assert_eq!(offset.as_str(), "+12:34");
178    /// # Ok::<_, datetime_string::Error>(())
179    /// ```
180    #[inline]
181    pub fn from_mut_str(s: &mut str) -> Result<&mut Self, Error> {
182        TryFrom::try_from(s)
183    }
184
185    /// Creates a new `&TimeNumOffsetColonStr` from a byte slice.
186    ///
187    /// # Examples
188    ///
189    /// ```
190    /// # use datetime_string::common::TimeNumOffsetColonStr;
191    /// let offset = TimeNumOffsetColonStr::from_bytes(b"-12:34")?;
192    /// assert_eq!(offset.as_str(), "-12:34");
193    ///
194    /// assert!(TimeNumOffsetColonStr::from_bytes(b"+00:00").is_ok());
195    /// assert!(TimeNumOffsetColonStr::from_bytes(b"+23:59").is_ok());
196    /// assert!(TimeNumOffsetColonStr::from_bytes(b"-00:00").is_ok());
197    /// assert!(TimeNumOffsetColonStr::from_bytes(b"-23:59").is_ok());
198    ///
199    /// assert!(TimeNumOffsetColonStr::from_bytes(b"Z").is_err(), "Not time-numoffset");
200    /// assert!(TimeNumOffsetColonStr::from_bytes(b"+24:00").is_err(), "Invalid hour");
201    /// assert!(TimeNumOffsetColonStr::from_bytes(b"+00:60").is_err(), "Invalid minute");
202    /// assert!(TimeNumOffsetColonStr::from_bytes(b"-24:00").is_err(), "Invalid hour");
203    /// assert!(TimeNumOffsetColonStr::from_bytes(b"-00:60").is_err(), "Invalid minute");
204    /// assert!(TimeNumOffsetColonStr::from_bytes(b"?00:00").is_err(), "Invalid sign");
205    /// assert!(TimeNumOffsetColonStr::from_bytes(b"00:00").is_err(), "Sign is missing");
206    /// # Ok::<_, datetime_string::Error>(())
207    /// ```
208    #[inline]
209    pub fn from_bytes(s: &[u8]) -> Result<&Self, Error> {
210        TryFrom::try_from(s)
211    }
212
213    /// Creates a new `&mut TimeNumOffsetColonStr` from a mutable byte slice.
214    ///
215    /// # Examples
216    ///
217    /// ```
218    /// # use datetime_string::common::TimeNumOffsetColonStr;
219    /// use datetime_string::common::TimeOffsetSign;
220    /// let mut buf: [u8; 6] = *b"-12:34";
221    /// let offset = TimeNumOffsetColonStr::from_bytes_mut(&mut buf)?;
222    /// assert_eq!(offset.as_str(), "-12:34");
223    ///
224    /// offset.set_sign(TimeOffsetSign::Positive);
225    /// assert_eq!(offset.as_str(), "+12:34");
226    /// # Ok::<_, datetime_string::Error>(())
227    /// ```
228    #[inline]
229    pub fn from_bytes_mut(s: &mut [u8]) -> Result<&mut Self, Error> {
230        TryFrom::try_from(s)
231    }
232
233    /// Assigns the given value.
234    ///
235    /// # Examples
236    ///
237    /// ```
238    /// # use datetime_string::common::TimeNumOffsetColonStr;
239    /// let mut buf: [u8; 6] = *b"+09:00";
240    /// let offset = TimeNumOffsetColonStr::from_bytes_mut(&mut buf[..])?;
241    /// assert_eq!(offset.as_str(), "+09:00");
242    ///
243    /// let newoffset = TimeNumOffsetColonStr::from_str("-00:00")?;
244    ///
245    /// offset.assign(newoffset);
246    /// assert_eq!(offset.as_str(), "-00:00");
247    /// assert_eq!(buf, *b"-00:00");
248    /// # Ok::<_, datetime_string::Error>(())
249    /// ```
250    #[inline]
251    pub fn assign(&mut self, v: &Self) {
252        debug_assert_eq!(self.0.len(), v.0.len());
253        self.0.copy_from_slice(&v.0);
254    }
255
256    /// Returns a string slice.
257    ///
258    /// # Examples
259    ///
260    /// ```
261    /// # use datetime_string::common::TimeNumOffsetColonStr;
262    /// let offset = TimeNumOffsetColonStr::from_str("-12:34")?;
263    ///
264    /// assert_eq!(offset.as_str(), "-12:34");
265    /// # Ok::<_, datetime_string::Error>(())
266    /// ```
267    #[inline]
268    #[must_use]
269    pub fn as_str(&self) -> &str {
270        unsafe {
271            // This is safe because `TimeNumOffsetColonStr` ensures that the
272            // underlying bytes are ASCII string.
273            debug_assert_safe_version_ok!(str::from_utf8(&self.0));
274            str::from_utf8_unchecked(&self.0)
275        }
276    }
277
278    /// Returns a byte slice.
279    ///
280    /// If you want to use indexed access, prefer [`as_bytes_fixed_len`].
281    ///
282    /// # Examples
283    ///
284    /// ```
285    /// # use datetime_string::common::TimeNumOffsetColonStr;
286    /// let offset = TimeNumOffsetColonStr::from_bytes(b"-12:34")?;
287    ///
288    /// assert_eq!(offset.as_bytes(), b"-12:34");
289    /// # Ok::<_, datetime_string::Error>(())
290    /// ```
291    ///
292    /// [`as_bytes_fixed_len`]: #method.as_bytes_fixed_len
293    #[inline]
294    #[must_use]
295    pub fn as_bytes(&self) -> &[u8] {
296        &self.0
297    }
298
299    /// Returns a fixed length byte slice.
300    ///
301    /// # Examples
302    ///
303    /// ```
304    /// # use datetime_string::common::TimeNumOffsetColonStr;
305    /// let offset = TimeNumOffsetColonStr::from_str("-12:34")?;
306    ///
307    /// let fixed_len: &[u8; 6] = offset.as_bytes_fixed_len();
308    /// assert_eq!(fixed_len, b"-12:34");
309    /// # Ok::<_, datetime_string::Error>(())
310    /// ```
311    #[inline]
312    #[must_use]
313    pub fn as_bytes_fixed_len(&self) -> &[u8; 6] {
314        debug_assert_eq!(
315            self.len(),
316            NUM_OFFSET_LEN,
317            "TimeNumOffsetColonStr must always be 6 bytes"
318        );
319
320        debug_assert_safe_version_ok!(<&[u8; NUM_OFFSET_LEN]>::try_from(&self.0));
321        let ptr = self.0.as_ptr() as *const [u8; NUM_OFFSET_LEN];
322        // This must be always safe because the length is already checked.
323        unsafe { &*ptr }
324    }
325
326    /// Returns the time offset sign.
327    ///
328    /// # Examples
329    ///
330    /// ```
331    /// # use datetime_string::common::TimeNumOffsetColonStr;
332    /// use datetime_string::common::TimeOffsetSign;
333    ///
334    /// let positive = TimeNumOffsetColonStr::from_str("+12:34")?;
335    /// assert_eq!(positive.sign(), TimeOffsetSign::Positive);
336    ///
337    /// let negative = TimeNumOffsetColonStr::from_str("-12:34")?;
338    /// assert_eq!(negative.sign(), TimeOffsetSign::Negative);
339    /// # Ok::<_, datetime_string::Error>(())
340    /// ```
341    ///
342    /// Note that signs are preserved for `+00:00` and `-00:00`.
343    ///
344    /// ```
345    /// # use datetime_string::common::TimeNumOffsetColonStr;
346    /// use datetime_string::common::TimeOffsetSign;
347    ///
348    /// let positive0 = TimeNumOffsetColonStr::from_str("+00:00")?;
349    /// assert_eq!(positive0.sign(), TimeOffsetSign::Positive);
350    ///
351    /// let negative0 = TimeNumOffsetColonStr::from_str("-00:00")?;
352    /// assert_eq!(negative0.sign(), TimeOffsetSign::Negative);
353    /// # Ok::<_, datetime_string::Error>(())
354    /// ```
355    #[inline]
356    #[must_use]
357    pub fn sign(&self) -> TimeOffsetSign {
358        if self.as_bytes()[0] == b'+' {
359            TimeOffsetSign::Positive
360        } else {
361            debug_assert_eq!(self.0[0], b'-');
362            TimeOffsetSign::Negative
363        }
364    }
365
366    /// Returns a mutable reference to the byte which contains the sign character.
367    ///
368    /// # Safety
369    ///
370    /// The returned byte should have `b'+'` or `b'-'`.
371    /// If other values are stored, it may lead to undefined behavior.
372    #[inline]
373    #[must_use]
374    unsafe fn sign_byte_mut(&mut self) -> &mut u8 {
375        debug_assert_safe_version_some!(self.0.get(0));
376        self.0.get_unchecked_mut(0)
377    }
378
379    /// Sets the given sign.
380    ///
381    /// ```
382    /// # use datetime_string::common::TimeNumOffsetColonStr;
383    /// use datetime_string::common::TimeOffsetSign;
384    ///
385    /// let mut buf = "-12:34".to_owned();
386    /// let offset = TimeNumOffsetColonStr::from_mut_str(&mut buf)?;
387    /// assert_eq!(offset.as_str(), "-12:34");
388    /// assert_eq!(offset.sign(), TimeOffsetSign::Negative);
389    ///
390    /// offset.set_sign(TimeOffsetSign::Positive);
391    /// assert_eq!(offset.as_str(), "+12:34");
392    /// assert_eq!(offset.sign(), TimeOffsetSign::Positive);
393    /// # Ok::<_, datetime_string::Error>(())
394    /// ```
395    #[inline]
396    pub fn set_sign(&mut self, sign: TimeOffsetSign) {
397        let byte = match sign {
398            TimeOffsetSign::Positive => b'+',
399            TimeOffsetSign::Negative => b'-',
400        };
401        unsafe {
402            // This is safe because a valid time-numoffset is also an ASCII string,
403            // and the substituted value (`byte`) is also an ASCII character.
404            *self.sign_byte_mut() = byte;
405        }
406        debug_assert_ok!(validate_bytes(&self.0));
407        debug_assert_eq!(self.sign(), sign);
408    }
409
410    /// Returns the absolute hour as a string slice.
411    ///
412    /// # Examples
413    ///
414    /// ```
415    /// # use datetime_string::common::TimeNumOffsetColonStr;
416    /// let offset = TimeNumOffsetColonStr::from_str("-12:34")?;
417    ///
418    /// assert_eq!(offset.hour_abs_str(), "12");
419    /// # Ok::<_, datetime_string::Error>(())
420    /// ```
421    #[inline]
422    #[must_use]
423    pub fn hour_abs_str(&self) -> &str {
424        unsafe {
425            // This is safe because the string is ASCII string and
426            // `HOUR_ABS_RANGE` is always inside the string.
427            debug_assert_safe_version_ok!(str::from_utf8(&self.0[HOUR_ABS_RANGE]));
428            str::from_utf8_unchecked(self.0.get_unchecked(HOUR_ABS_RANGE))
429        }
430    }
431
432    /// Returns the absolute hour as a fixed length byte slice.
433    ///
434    /// # Examples
435    ///
436    /// ```
437    /// # use datetime_string::common::TimeNumOffsetColonStr;
438    /// let offset = TimeNumOffsetColonStr::from_str("-12:34")?;
439    ///
440    /// let hour_fixed_len: &[u8; 2] = offset.hour_abs_bytes_fixed_len();
441    /// assert_eq!(hour_fixed_len, b"12");
442    /// # Ok::<_, datetime_string::Error>(())
443    /// ```
444    #[inline]
445    #[must_use]
446    pub fn hour_abs_bytes_fixed_len(&self) -> &[u8; 2] {
447        unsafe {
448            // This is safe because `HOUR_ABS_RANGE` fits inside the string.
449            debug_assert_safe_version_ok!(<&[u8; 2]>::try_from(&self.0[HOUR_ABS_RANGE]));
450            let ptr = self.0.as_ptr().add(HOUR_ABS_RANGE.start) as *const [u8; 2];
451            &*ptr
452        }
453    }
454
455    /// Returns the absolute hour as a fixed length mutable byte slice.
456    ///
457    /// # Safety
458    ///
459    /// The returned slice should have only ASCII digits.
460    /// If non-ASCII digits are stored, it may lead to undefined behavior.
461    #[inline]
462    #[must_use]
463    unsafe fn hour_abs_bytes_mut_fixed_len(&mut self) -> &mut [u8; 2] {
464        // This is safe because `HOUR_ABS_RANGE` fits inside the string.
465        debug_assert_ok!(<&[u8; 2]>::try_from(&self.0[HOUR_ABS_RANGE]));
466        let ptr = self.0.as_mut_ptr().add(HOUR_ABS_RANGE.start) as *mut [u8; 2];
467        &mut *ptr
468    }
469
470    /// Returns the absolute hour as an integer.
471    ///
472    /// # Examples
473    ///
474    /// ```
475    /// # use datetime_string::common::TimeNumOffsetColonStr;
476    /// let offset = TimeNumOffsetColonStr::from_str("-12:34")?;
477    ///
478    /// assert_eq!(offset.hour_abs(), 12);
479    /// # Ok::<_, datetime_string::Error>(())
480    /// ```
481    #[inline]
482    #[must_use]
483    pub fn hour_abs(&self) -> u8 {
484        parse_digits2(*self.hour_abs_bytes_fixed_len())
485    }
486
487    /// Sets the given absolute hour value to the string.
488    ///
489    /// # Failures
490    ///
491    /// * Fails if the time after modification is invalid.
492    ///
493    /// # Examples
494    ///
495    /// ```
496    /// # use datetime_string::common::TimeNumOffsetColonStr;
497    /// let mut buf: [u8; 6] = *b"-12:34";
498    /// let time = TimeNumOffsetColonStr::from_bytes_mut(&mut buf[..])?;
499    /// assert_eq!(time.as_str(), "-12:34");
500    ///
501    /// time.set_hour_abs(0)?;
502    /// assert_eq!(time.as_str(), "-00:34");
503    ///
504    /// assert!(time.set_hour_abs(24).is_err(), "-24:34 is invalid");
505    /// # Ok::<_, datetime_string::Error>(())
506    /// ```
507    #[inline]
508    pub fn set_hour_abs(&mut self, hour_abs: u8) -> Result<(), Error> {
509        if hour_abs > HOUR_MAX {
510            return Err(ErrorKind::ComponentOutOfRange(ComponentKind::OffsetHour).into());
511        }
512        unsafe {
513            // This is safe because `write_digit2()` fills the slice with ASCII digits.
514            write_digit2(self.hour_abs_bytes_mut_fixed_len(), hour_abs);
515        }
516        debug_assert_ok!(validate_bytes(&self.0));
517
518        Ok(())
519    }
520
521    /// Returns the signed hour as a string slice.
522    ///
523    /// # Examples
524    ///
525    /// ```
526    /// # use datetime_string::common::TimeNumOffsetColonStr;
527    /// let offset = TimeNumOffsetColonStr::from_str("-12:34")?;
528    ///
529    /// assert_eq!(offset.hour_signed_str(), "-12");
530    /// # Ok::<_, datetime_string::Error>(())
531    /// ```
532    #[inline]
533    #[must_use]
534    pub fn hour_signed_str(&self) -> &str {
535        unsafe {
536            // This is safe because the string is ASCII string and
537            // `HOUR_SIGNED_RANGE` is always inside the string.
538            debug_assert_safe_version_ok!(str::from_utf8(&self.0[HOUR_SIGNED_RANGE]));
539            str::from_utf8_unchecked(self.0.get_unchecked(HOUR_SIGNED_RANGE))
540        }
541    }
542
543    /// Returns the signed hour as a fixed length byte slice.
544    ///
545    /// # Examples
546    ///
547    /// ```
548    /// # use datetime_string::common::TimeNumOffsetColonStr;
549    /// let offset = TimeNumOffsetColonStr::from_str("-12:34")?;
550    ///
551    /// let hour_fixed_len: &[u8; 3] = offset.hour_signed_bytes_fixed_len();
552    /// assert_eq!(hour_fixed_len, b"-12");
553    /// # Ok::<_, datetime_string::Error>(())
554    /// ```
555    #[inline]
556    #[must_use]
557    pub fn hour_signed_bytes_fixed_len(&self) -> &[u8; 3] {
558        debug_assert_safe_version_ok!(<&[u8; 3]>::try_from(&self.0[HOUR_SIGNED_RANGE]));
559        let ptr = self.0[HOUR_SIGNED_RANGE].as_ptr() as *const [u8; 3];
560        // This must be always safe because the string is valid time-numoffset string.
561        unsafe { &*ptr }
562    }
563
564    /// Returns the signed hour as an integer.
565    ///
566    /// # Examples
567    ///
568    /// ```
569    /// # use datetime_string::common::TimeNumOffsetColonStr;
570    /// let offset = TimeNumOffsetColonStr::from_str("-12:34")?;
571    ///
572    /// assert_eq!(offset.hour_signed(), -12);
573    /// # Ok::<_, datetime_string::Error>(())
574    /// ```
575    ///
576    /// Note that both `+00` and `-00` are treaded as the same 0.
577    ///
578    /// ```
579    /// # use datetime_string::common::TimeNumOffsetColonStr;
580    /// let positive = TimeNumOffsetColonStr::from_str("+00:59")?;
581    /// assert_eq!(positive.hour_signed(), 0);
582    ///
583    /// let negative = TimeNumOffsetColonStr::from_str("-00:59")?;
584    /// assert_eq!(negative.hour_signed(), 0);
585    /// # Ok::<_, datetime_string::Error>(())
586    /// ```
587    #[inline]
588    #[must_use]
589    pub fn hour_signed(&self) -> i8 {
590        let abs = self.hour_abs() as i8;
591        match self.sign() {
592            TimeOffsetSign::Positive => abs,
593            TimeOffsetSign::Negative => -abs,
594        }
595    }
596
597    /// Sets the given absolute hour value and sign to the string.
598    ///
599    /// # Failures
600    ///
601    /// * Fails if the time after modification is invalid.
602    ///
603    /// # Examples
604    ///
605    /// ```
606    /// # use datetime_string::common::TimeNumOffsetColonStr;
607    /// use datetime_string::common::TimeOffsetSign;
608    ///
609    /// let mut buf: [u8; 6] = *b"-12:34";
610    /// let time = TimeNumOffsetColonStr::from_bytes_mut(&mut buf[..])?;
611    /// assert_eq!(time.as_str(), "-12:34");
612    ///
613    /// time.set_sign_and_hour_abs(TimeOffsetSign::Positive, 23)?;
614    /// assert_eq!(time.as_str(), "+23:34");
615    ///
616    /// assert!(time.set_sign_and_hour_abs(TimeOffsetSign::Negative, 24).is_err(), "-24:34 is invalid");
617    /// # Ok::<_, datetime_string::Error>(())
618    /// ```
619    #[inline]
620    pub fn set_sign_and_hour_abs(
621        &mut self,
622        sign: TimeOffsetSign,
623        hour_abs: u8,
624    ) -> Result<(), Error> {
625        self.set_hour_abs(hour_abs)?;
626        self.set_sign(sign);
627        debug_assert_ok!(validate_bytes(&self.0));
628
629        Ok(())
630    }
631
632    /// Sets the given signed hour value to the string.
633    ///
634    /// If `0` is passed, it is always considered as positive 0.
635    ///
636    /// # Failures
637    ///
638    /// * Fails if the time after modification is invalid.
639    ///
640    /// # Examples
641    ///
642    /// ```
643    /// # use datetime_string::common::TimeNumOffsetColonStr;
644    /// use datetime_string::common::TimeOffsetSign;
645    ///
646    /// let mut buf: [u8; 6] = *b"-12:34";
647    /// let time = TimeNumOffsetColonStr::from_bytes_mut(&mut buf[..])?;
648    /// assert_eq!(time.as_str(), "-12:34");
649    ///
650    /// time.set_hour_signed(0)?;
651    /// assert_eq!(time.as_str(), "+00:34");
652    ///
653    /// time.set_hour_signed(-1)?;
654    /// assert_eq!(time.as_str(), "-01:34");
655    ///
656    /// assert!(time.set_hour_signed(24).is_err(), "+24:34 is invalid");
657    /// # Ok::<_, datetime_string::Error>(())
658    /// ```
659    #[inline]
660    pub fn set_hour_signed(&mut self, hour: i8) -> Result<(), Error> {
661        let sign = if hour >= 0 {
662            TimeOffsetSign::Positive
663        } else {
664            TimeOffsetSign::Negative
665        };
666        let hour_abs = hour.wrapping_abs() as u8;
667
668        self.set_sign_and_hour_abs(sign, hour_abs)
669    }
670
671    /// Returns the minute as a string slice.
672    ///
673    /// # Examples
674    ///
675    /// ```
676    /// # use datetime_string::common::TimeNumOffsetColonStr;
677    /// let offset = TimeNumOffsetColonStr::from_str("-12:34")?;
678    ///
679    /// assert_eq!(offset.minute_str(), "34");
680    /// # Ok::<_, datetime_string::Error>(())
681    /// ```
682    #[inline]
683    #[must_use]
684    pub fn minute_str(&self) -> &str {
685        unsafe {
686            // This is safe because the string is ASCII string and
687            // `MINUTE_RANGE` is always inside the string.
688            debug_assert_safe_version_ok!(str::from_utf8(&self.0[MINUTE_RANGE]));
689            str::from_utf8_unchecked(self.0.get_unchecked(MINUTE_RANGE))
690        }
691    }
692
693    /// Returns the minute as a fixed length byte slice.
694    ///
695    /// # Examples
696    ///
697    /// ```
698    /// # use datetime_string::common::TimeNumOffsetColonStr;
699    /// let offset = TimeNumOffsetColonStr::from_str("-12:34")?;
700    ///
701    /// let minute_fixed_len: &[u8; 2] = offset.minute_bytes_fixed_len();
702    /// assert_eq!(minute_fixed_len, b"34");
703    /// # Ok::<_, datetime_string::Error>(())
704    /// ```
705    #[inline]
706    #[must_use]
707    pub fn minute_bytes_fixed_len(&self) -> &[u8; 2] {
708        unsafe {
709            // This is safe because `MINUTE_RANGE` fits inside the string.
710            debug_assert_safe_version_ok!(<&[u8; 2]>::try_from(&self.0[MINUTE_RANGE]));
711            let ptr = self.0.as_ptr().add(MINUTE_RANGE.start) as *const [u8; 2];
712            &*ptr
713        }
714    }
715
716    /// Returns the minute as a fixed length mutable byte slice.
717    ///
718    /// # Safety
719    ///
720    /// The returned slice should have only ASCII digits.
721    /// If non-ASCII digits are stored, it may lead to undefined behavior.
722    #[inline]
723    #[must_use]
724    unsafe fn minute_bytes_mut_fixed_len(&mut self) -> &mut [u8; 2] {
725        // This is safe because `MINUTE_RANGE` fits inside the string.
726        debug_assert_ok!(<&[u8; 2]>::try_from(&self.0[MINUTE_RANGE]));
727        let ptr = self.0.as_mut_ptr().add(MINUTE_RANGE.start) as *mut [u8; 2];
728        &mut *ptr
729    }
730
731    /// Returns the minute as an integer.
732    ///
733    /// # Examples
734    ///
735    /// ```
736    /// # use datetime_string::common::TimeNumOffsetColonStr;
737    /// let offset = TimeNumOffsetColonStr::from_str("-12:34")?;
738    ///
739    /// assert_eq!(offset.minute(), 34);
740    /// # Ok::<_, datetime_string::Error>(())
741    /// ```
742    #[inline]
743    #[must_use]
744    pub fn minute(&self) -> u8 {
745        parse_digits2(*self.minute_bytes_fixed_len())
746    }
747
748    /// Sets the given minute value to the string.
749    ///
750    /// # Failures
751    ///
752    /// * Fails if the time after modification is invalid.
753    ///
754    /// # Examples
755    ///
756    /// ```
757    /// # use datetime_string::common::TimeNumOffsetColonStr;
758    /// let mut buf: [u8; 6] = *b"-12:34";
759    /// let time = TimeNumOffsetColonStr::from_bytes_mut(&mut buf[..])?;
760    /// assert_eq!(time.as_str(), "-12:34");
761    ///
762    /// time.set_minute(01)?;
763    /// assert_eq!(time.as_str(), "-12:01");
764    ///
765    /// assert!(time.set_minute(60).is_err(), "-12:60 is invalid");
766    /// # Ok::<_, datetime_string::Error>(())
767    /// ```
768    #[inline]
769    pub fn set_minute(&mut self, minute: u8) -> Result<(), Error> {
770        if minute > MINUTE_MAX {
771            return Err(ErrorKind::ComponentOutOfRange(ComponentKind::OffsetMinute).into());
772        }
773        unsafe {
774            // This is safe because `write_digit2()` fills the slice with ASCII digits.
775            write_digit2(self.minute_bytes_mut_fixed_len(), minute);
776        }
777        debug_assert_ok!(validate_bytes(&self.0));
778
779        Ok(())
780    }
781
782    /// Sets the given signed hour and minute value to the string.
783    ///
784    /// If `0` is passed, it is always considered as positive 0.
785    ///
786    /// # Failures
787    ///
788    /// * Fails if the time after modification is invalid.
789    ///
790    /// # Examples
791    ///
792    /// ```
793    /// # use datetime_string::common::TimeNumOffsetColonStr;
794    /// use datetime_string::common::TimeOffsetSign;
795    ///
796    /// let mut buf: [u8; 6] = *b"-12:34";
797    /// let time = TimeNumOffsetColonStr::from_bytes_mut(&mut buf[..])?;
798    /// assert_eq!(time.as_str(), "-12:34");
799    ///
800    /// time.set_time_signed(0, 56)?;
801    /// assert_eq!(time.as_str(), "+00:56");
802    ///
803    /// time.set_time_signed(-1, 23)?;
804    /// assert_eq!(time.as_str(), "-01:23");
805    ///
806    /// assert!(time.set_time_signed(24, 00).is_err(), "+24:00 is invalid");
807    /// # Ok::<_, datetime_string::Error>(())
808    /// ```
809    #[inline]
810    pub fn set_time_signed(&mut self, hour: i8, minute: u8) -> Result<(), Error> {
811        let hour_abs = hour.wrapping_abs() as u8;
812        let sign = if hour >= 0 {
813            TimeOffsetSign::Positive
814        } else {
815            TimeOffsetSign::Negative
816        };
817
818        self.set_sign_and_time(sign, hour_abs, minute)
819    }
820
821    /// Sets the given sign, absolute hour, and minute value to the string.
822    ///
823    /// If `0` is passed, it is always considered as positive 0.
824    ///
825    /// # Failures
826    ///
827    /// * Fails if the time after modification is invalid.
828    ///
829    /// # Examples
830    ///
831    /// ```
832    /// # use datetime_string::common::TimeNumOffsetColonStr;
833    /// use datetime_string::common::TimeOffsetSign;
834    ///
835    /// let mut buf: [u8; 6] = *b"-12:34";
836    /// let time = TimeNumOffsetColonStr::from_bytes_mut(&mut buf[..])?;
837    /// assert_eq!(time.as_str(), "-12:34");
838    ///
839    /// time.set_sign_and_time(TimeOffsetSign::Negative, 0, 56)?;
840    /// assert_eq!(time.as_str(), "-00:56");
841    ///
842    /// time.set_sign_and_time(TimeOffsetSign::Positive, 1, 23)?;
843    /// assert_eq!(time.as_str(), "+01:23");
844    ///
845    /// assert!(time.set_sign_and_time(TimeOffsetSign::Positive, 24, 00).is_err(), "+24:00 is invalid");
846    /// # Ok::<_, datetime_string::Error>(())
847    /// ```
848    pub fn set_sign_and_time(
849        &mut self,
850        sign: TimeOffsetSign,
851        hour_abs: u8,
852        minute: u8,
853    ) -> Result<(), Error> {
854        if hour_abs > HOUR_MAX {
855            return Err(ErrorKind::ComponentOutOfRange(ComponentKind::OffsetHour).into());
856        }
857        if minute > MINUTE_MAX {
858            return Err(ErrorKind::ComponentOutOfRange(ComponentKind::OffsetMinute).into());
859        }
860
861        self.set_sign(sign);
862        unsafe {
863            // This is safe because `write_digit2()` fills the slice with ASCII digits.
864            write_digit2(self.hour_abs_bytes_mut_fixed_len(), hour_abs);
865            write_digit2(self.minute_bytes_mut_fixed_len(), minute);
866        }
867        debug_assert_ok!(validate_bytes(&self.0));
868
869        Ok(())
870    }
871
872    /// Returns the time offset in minutes.
873    ///
874    /// Note that both `+00:00` and `-00:00` is considered as 0 minutes offset.
875    /// RFC 3339 defines semantics of `-00:00` as "unknown local offset".
876    /// If your application should be aware of that semantics, use
877    /// [`is_unknown_local_offset`] method or [`sign`] method to distinguish them.
878    ///
879    /// # Examples
880    ///
881    /// ```
882    /// # use datetime_string::common::TimeNumOffsetColonStr;
883    /// let offset = TimeNumOffsetColonStr::from_str("-12:34")?;
884    ///
885    /// assert_eq!(offset.in_minutes(), -(12 * 60 + 34));
886    /// # Ok::<_, datetime_string::Error>(())
887    /// ```
888    ///
889    /// `0` is returned for both `+00:00` and `-00:00`.
890    ///
891    /// ```
892    /// # use datetime_string::common::TimeNumOffsetColonStr;
893    /// use datetime_string::common::TimeOffsetSign;
894    ///
895    /// let positive0 = TimeNumOffsetColonStr::from_str("+00:00")?;
896    /// assert_eq!(positive0.in_minutes(), 0);
897    /// assert_eq!(positive0.sign(), TimeOffsetSign::Positive);
898    /// assert!(!positive0.is_unknown_local_offset(), "0 minutes time offset");
899    ///
900    /// let negative0 = TimeNumOffsetColonStr::from_str("-00:00")?;
901    /// assert_eq!(negative0.in_minutes(), 0);
902    /// assert_eq!(negative0.sign(), TimeOffsetSign::Negative);
903    /// assert!(negative0.is_unknown_local_offset(), "unknown local offset");
904    /// # Ok::<_, datetime_string::Error>(())
905    /// ```
906    ///
907    /// [`is_unknown_local_offset`]: TimeNumOffsetColonStr::is_unknown_local_offset
908    /// [`sign`]: TimeNumOffsetColonStr::sign
909    #[must_use]
910    pub fn in_minutes(&self) -> i16 {
911        let abs_min = self.hour_abs() as i16 * 60 + self.minute() as i16;
912        match self.sign() {
913            TimeOffsetSign::Positive => abs_min,
914            TimeOffsetSign::Negative => -abs_min,
915        }
916    }
917
918    /// Sets the time offset for the given offset in minutes.
919    ///
920    /// # Failures
921    ///
922    /// * Fails if the value is invalid as time offset, i.e. it is not between
923    ///   -1440 and 1440.
924    ///
925    /// # Examples
926    ///
927    /// ```
928    /// # use datetime_string::common::TimeNumOffsetColonStr;
929    /// let mut buf: [u8; 6] = *b"-12:34";
930    /// let time = TimeNumOffsetColonStr::from_bytes_mut(&mut buf[..])?;
931    /// assert_eq!(time.as_str(), "-12:34");
932    ///
933    /// time.set_in_minutes(9 * 60)?;
934    /// assert_eq!(time.as_str(), "+09:00");
935    ///
936    /// time.set_in_minutes(0)?;
937    /// assert_eq!(time.as_str(), "+00:00");
938    ///
939    /// assert!(time.set_in_minutes(-24 * 60).is_err(), "-24:00 is invalid");
940    /// # Ok::<_, datetime_string::Error>(())
941    /// ```
942    pub fn set_in_minutes(&mut self, v: i16) -> Result<(), Error> {
943        /// Maximum allowed absolute value of minutes.
944        const IN_MINUTES_MAX: i16 = HOUR_MAX as i16 * 60 + MINUTE_MAX as i16;
945
946        if (v < -IN_MINUTES_MAX) || (v > IN_MINUTES_MAX) {
947            return Err(ErrorKind::ComponentOutOfRange(ComponentKind::Offset).into());
948        }
949
950        let sign = if v < 0 {
951            TimeOffsetSign::Negative
952        } else {
953            TimeOffsetSign::Positive
954        };
955        // The valid time offset does not overflow on `.abs()`.
956        let v = v.abs();
957        let hour_abs = (v / 60) as u8;
958        let minute = (v % 60) as u8;
959
960        self.set_sign_and_time(sign, hour_abs, minute)
961    }
962
963    /// Returns `true` if and only if the time offset means "unknown local offset" in RFC 3339.
964    ///
965    /// RFC 3339 defines `-00:00` as "unknown local offset".
966    ///
967    /// > If the time in UTC is known, but the offset to local time is unknown,
968    /// > this can be represented with an offset of "-00:00".
969    /// > This differs semantically from an offset of "Z" or "+00:00", which
970    /// > imply that UTC is the preferred reference point for the specified
971    /// > time.
972    /// >
973    /// > --- [RFC 3339, section 4.3. Unknown Local Offset Convention][rfc3339-4-3]
974    ///
975    /// This method returns `true` for `-00:00`.
976    ///
977    /// # Examples
978    ///
979    /// ```
980    /// # use datetime_string::common::TimeNumOffsetColonStr;
981    /// use datetime_string::common::TimeOffsetSign;
982    ///
983    /// let positive0 = TimeNumOffsetColonStr::from_str("+00:00")?;
984    /// assert!(!positive0.is_unknown_local_offset(), "0 minutes time offset");
985    ///
986    /// let negative0 = TimeNumOffsetColonStr::from_str("-00:00")?;
987    /// assert!(negative0.is_unknown_local_offset(), "unknown local offset");
988    /// # Ok::<_, datetime_string::Error>(())
989    /// ```
990    ///
991    /// [rfc3339-4-3]: https://tools.ietf.org/html/rfc3339#section-4.3
992    #[inline]
993    #[must_use]
994    pub fn is_unknown_local_offset(&self) -> bool {
995        &self.0 == b"-00:00"
996    }
997}
998
999#[cfg(feature = "alloc")]
1000#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
1001impl alloc::borrow::ToOwned for TimeNumOffsetColonStr {
1002    type Owned = TimeNumOffsetColonString;
1003
1004    #[inline]
1005    fn to_owned(&self) -> Self::Owned {
1006        self.into()
1007    }
1008}
1009
1010impl AsRef<[u8]> for TimeNumOffsetColonStr {
1011    #[inline]
1012    fn as_ref(&self) -> &[u8] {
1013        self.as_bytes()
1014    }
1015}
1016
1017impl AsRef<str> for TimeNumOffsetColonStr {
1018    #[inline]
1019    fn as_ref(&self) -> &str {
1020        self.as_str()
1021    }
1022}
1023
1024impl AsRef<TimeNumOffsetColonStr> for TimeNumOffsetColonStr {
1025    #[inline]
1026    fn as_ref(&self) -> &TimeNumOffsetColonStr {
1027        self
1028    }
1029}
1030
1031impl AsMut<TimeNumOffsetColonStr> for TimeNumOffsetColonStr {
1032    #[inline]
1033    fn as_mut(&mut self) -> &mut TimeNumOffsetColonStr {
1034        self
1035    }
1036}
1037
1038impl<'a> From<&'a TimeNumOffsetColonStr> for &'a str {
1039    #[inline]
1040    fn from(v: &'a TimeNumOffsetColonStr) -> Self {
1041        v.as_str()
1042    }
1043}
1044
1045#[cfg(feature = "chrono04")]
1046#[cfg_attr(docsrs, doc(cfg(feature = "chrono04")))]
1047impl From<&TimeNumOffsetColonStr> for chrono04::FixedOffset {
1048    #[inline]
1049    fn from(v: &TimeNumOffsetColonStr) -> Self {
1050        Self::east(i32::from(v.in_minutes()) * 60)
1051    }
1052}
1053
1054#[cfg(feature = "time03")]
1055#[cfg_attr(docsrs, doc(cfg(feature = "time03")))]
1056impl From<&TimeNumOffsetColonStr> for time03::UtcOffset {
1057    #[inline]
1058    fn from(v: &TimeNumOffsetColonStr) -> Self {
1059        // `TimeNumOffsetColonStr` can represent `-23:59` to `+23:59`.
1060        // `time03::UtcOffset` can represent `-23:59:59` to `+23:59:59`.
1061        // So this conversion should never fail.
1062        Self::from_whole_seconds(i32::from(v.in_minutes()) * 60)
1063            .expect("[validity] `time03::UtcOffset` can represent `-23:59:59` to `+23:59:59`")
1064    }
1065}
1066
1067impl<'a> TryFrom<&'a [u8]> for &'a TimeNumOffsetColonStr {
1068    type Error = Error;
1069
1070    #[inline]
1071    fn try_from(v: &'a [u8]) -> Result<Self, Self::Error> {
1072        validate_bytes(v)?;
1073        Ok(unsafe {
1074            // This is safe because a valid time-numoffset string is also an ASCII string.
1075            TimeNumOffsetColonStr::from_bytes_maybe_unchecked(v)
1076        })
1077    }
1078}
1079
1080impl<'a> TryFrom<&'a mut [u8]> for &'a mut TimeNumOffsetColonStr {
1081    type Error = Error;
1082
1083    #[inline]
1084    fn try_from(v: &'a mut [u8]) -> Result<Self, Self::Error> {
1085        validate_bytes(v)?;
1086        Ok(unsafe {
1087            // This is safe because a valid time-numoffset string is also an ASCII string.
1088            TimeNumOffsetColonStr::from_bytes_maybe_unchecked_mut(v)
1089        })
1090    }
1091}
1092
1093impl<'a> TryFrom<&'a str> for &'a TimeNumOffsetColonStr {
1094    type Error = Error;
1095
1096    #[inline]
1097    fn try_from(v: &'a str) -> Result<Self, Self::Error> {
1098        Self::try_from(v.as_bytes())
1099    }
1100}
1101
1102impl<'a> TryFrom<&'a mut str> for &'a mut TimeNumOffsetColonStr {
1103    type Error = Error;
1104
1105    #[inline]
1106    fn try_from(v: &'a mut str) -> Result<Self, Self::Error> {
1107        validate_bytes(v.as_bytes())?;
1108        Ok(unsafe {
1109            // This is safe because a valid time-numoffset string is also an ASCII string.
1110            TimeNumOffsetColonStr::from_str_maybe_unchecked_mut(v)
1111        })
1112    }
1113}
1114
1115impl fmt::Display for TimeNumOffsetColonStr {
1116    #[inline]
1117    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1118        self.as_str().fmt(f)
1119    }
1120}
1121
1122impl ops::Deref for TimeNumOffsetColonStr {
1123    type Target = str;
1124
1125    #[inline]
1126    fn deref(&self) -> &Self::Target {
1127        self.as_str()
1128    }
1129}
1130
1131impl_cmp_symmetric!(str, TimeNumOffsetColonStr, &TimeNumOffsetColonStr);
1132impl_cmp_symmetric!([u8], TimeNumOffsetColonStr, [u8]);
1133impl_cmp_symmetric!([u8], TimeNumOffsetColonStr, &[u8]);
1134impl_cmp_symmetric!([u8], &TimeNumOffsetColonStr, [u8]);
1135impl_cmp_symmetric!(str, TimeNumOffsetColonStr, str);
1136impl_cmp_symmetric!(str, TimeNumOffsetColonStr, &str);
1137impl_cmp_symmetric!(str, &TimeNumOffsetColonStr, str);
1138
1139#[cfg(feature = "serde")]
1140#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
1141impl serde::Serialize for TimeNumOffsetColonStr {
1142    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1143    where
1144        S: serde::Serializer,
1145    {
1146        serializer.serialize_str(self.as_str())
1147    }
1148}
1149
1150/// Owned string for a time offset in `+hh:mm` or `-hh:mm` format, such as `+09:00` and `-00:00`.
1151///
1152/// This is also an RFC 3339 [`time-numoffset`] string.
1153///
1154/// This is a fixed length string, and implements [`Copy`] trait.
1155///
1156/// To create a value of this type, use [`str::parse`] method or
1157/// [`std::convert::TryFrom`] trait, or convert from `&TimeNumOffsetColonStr`.
1158///
1159/// # Examples
1160///
1161/// ```
1162/// # use datetime_string::common::TimeNumOffsetColonString;
1163/// use datetime_string::common::TimeNumOffsetColonStr;
1164/// use std::convert::TryFrom;
1165///
1166/// let try_from = TimeNumOffsetColonString::try_from("-12:34")?;
1167///
1168/// let parse = "-12:34".parse::<TimeNumOffsetColonString>()?;
1169/// let parse2: TimeNumOffsetColonString = "-12:34".parse()?;
1170///
1171/// let to_owned = TimeNumOffsetColonStr::from_str("-12:34")?.to_owned();
1172/// let into: TimeNumOffsetColonString = TimeNumOffsetColonStr::from_str("-12:34")?.into();
1173/// # Ok::<_, datetime_string::Error>(())
1174/// ```
1175///
1176/// [`time-numoffset`]: https://tools.ietf.org/html/rfc3339#section-5.6
1177// Note that `derive(Serialize)` cannot used here, because it encodes this as
1178// `[u8; 6]` rather than as a string.
1179#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
1180#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
1181#[repr(transparent)]
1182// Comparisons implemented for the type are consistent (at least it is intended to be so).
1183// See <https://github.com/rust-lang/rust-clippy/issues/2025>.
1184// Note that `clippy::derive_ord_xor_partial_ord` would be introduced since Rust 1.47.0.
1185#[allow(clippy::derive_hash_xor_eq)]
1186#[allow(unknown_lints, clippy::derive_ord_xor_partial_ord)]
1187pub struct TimeNumOffsetColonString([u8; NUM_OFFSET_LEN]);
1188
1189impl TimeNumOffsetColonString {
1190    /// Creates a `TimeNumOffsetColonString` from the given bytes.
1191    ///
1192    /// # Safety
1193    ///
1194    /// `validate_bytes(&s)` should return `Ok(())`.
1195    #[inline]
1196    #[must_use]
1197    unsafe fn new_maybe_unchecked(s: [u8; 6]) -> Self {
1198        debug_assert_ok!(validate_bytes(&s));
1199        Self(s)
1200    }
1201
1202    /// Returns `+00:00`.
1203    ///
1204    /// # Examples
1205    ///
1206    /// ```
1207    /// # use datetime_string::common::TimeNumOffsetColonString;
1208    /// let utc = TimeNumOffsetColonString::utc();
1209    /// assert_eq!(utc.as_str(), "+00:00");
1210    /// # Ok::<_, datetime_string::Error>(())
1211    /// ```
1212    #[inline]
1213    #[must_use]
1214    pub fn utc() -> Self {
1215        unsafe {
1216            // This is safe because `+00:00` is valid.
1217            debug_assert_safe_version_ok!(Self::try_from(*b"+00:00"));
1218            Self::new_maybe_unchecked(*b"+00:00")
1219        }
1220    }
1221
1222    /// Returns `-00:00`, which is defined in RFC 3339 to indicate "unknown local offset".
1223    ///
1224    /// # Examples
1225    ///
1226    /// ```
1227    /// # use datetime_string::common::TimeNumOffsetColonString;
1228    /// let unknown_local_offset = TimeNumOffsetColonString::unknown_local_offset();
1229    /// assert_eq!(unknown_local_offset.as_str(), "-00:00");
1230    /// # Ok::<_, datetime_string::Error>(())
1231    /// ```
1232    #[inline]
1233    #[must_use]
1234    pub fn unknown_local_offset() -> Self {
1235        unsafe {
1236            // This is safe because `-00:00` is valid.
1237            debug_assert_safe_version_ok!(Self::try_from(*b"-00:00"));
1238            Self::new_maybe_unchecked(*b"-00:00")
1239        }
1240    }
1241
1242    /// Creates a new `TimeNumOffsetColonStr` from the given minutes.
1243    ///
1244    /// # Examples
1245    ///
1246    /// ```
1247    /// # use datetime_string::common::TimeNumOffsetColonString;
1248    /// let time = TimeNumOffsetColonString::from_minutes(9 * 60)?;
1249    /// assert_eq!(time.as_str(), "+09:00");
1250    ///
1251    /// assert!(TimeNumOffsetColonString::from_minutes(-24 * 60).is_err(), "-24:00 is invaild time");
1252    /// # Ok::<_, datetime_string::Error>(())
1253    /// ```
1254    pub fn from_minutes(minutes: i16) -> Result<Self, Error> {
1255        let mut v = Self::utc();
1256        v.set_in_minutes(minutes)?;
1257        Ok(v)
1258    }
1259
1260    /// Creates a new `TimeNumOffsetColonStr` from the given minutes.
1261    ///
1262    /// # Examples
1263    ///
1264    /// ```
1265    /// # use datetime_string::common::TimeNumOffsetColonString;
1266    /// use datetime_string::common::TimeOffsetSign;
1267    /// let time = TimeNumOffsetColonString::from_sign_and_hm(TimeOffsetSign::Positive, 9, 30)?;
1268    /// assert_eq!(time.as_str(), "+09:30");
1269    ///
1270    /// let unknown_local_offset = TimeNumOffsetColonString::from_sign_and_hm(TimeOffsetSign::Negative, 0, 0)?;
1271    /// assert_eq!(unknown_local_offset.as_str(), "-00:00");
1272    ///
1273    /// assert!(
1274    ///     TimeNumOffsetColonString::from_sign_and_hm(TimeOffsetSign::Negative, 24, 0).is_err(),
1275    ///     "-24:00 is invaild time"
1276    /// );
1277    /// # Ok::<_, datetime_string::Error>(())
1278    /// ```
1279    pub fn from_sign_and_hm(sign: TimeOffsetSign, hour_abs: u8, minute: u8) -> Result<Self, Error> {
1280        let mut v = Self::utc();
1281        v.set_sign_and_time(sign, hour_abs, minute)?;
1282        Ok(v)
1283    }
1284
1285    /// Creates a new `TimeNumOffsetColonStr` from the given minutes.
1286    ///
1287    /// # Examples
1288    ///
1289    /// ```
1290    /// # use datetime_string::common::TimeNumOffsetColonString;
1291    /// let offset = TimeNumOffsetColonString::from_hm_signed(-9, 0)?;
1292    /// assert_eq!(offset.as_str(), "-09:00");
1293    ///
1294    /// let unknown_local_offset = TimeNumOffsetColonString::from_hm_signed(0, 0)?;
1295    /// assert_eq!(unknown_local_offset.as_str(), "+00:00");
1296    ///
1297    /// assert!( TimeNumOffsetColonString::from_hm_signed(-24, 0).is_err(), "-24:00 is invaild time");
1298    /// # Ok::<_, datetime_string::Error>(())
1299    /// ```
1300    pub fn from_hm_signed(hour: i8, minute: u8) -> Result<Self, Error> {
1301        let mut v = Self::utc();
1302        v.set_time_signed(hour, minute)?;
1303        Ok(v)
1304    }
1305
1306    /// Returns a `&TimeNumOffsetColonStr` for the string.
1307    ///
1308    /// # Examples
1309    ///
1310    /// ```
1311    /// # use datetime_string::common::TimeNumOffsetColonString;
1312    /// use datetime_string::common::TimeNumOffsetColonStr;
1313    ///
1314    /// let offset = "-12:34".parse::<TimeNumOffsetColonString>()?;
1315    ///
1316    /// // Usually you don't need to call `as_deref()` explicitly, because
1317    /// // `Deref<Target = TimeNumOffsetColonStr>` trait is implemented.
1318    /// let _: &TimeNumOffsetColonStr = offset.as_deref();
1319    /// # Ok::<_, datetime_string::Error>(())
1320    /// ```
1321    #[inline]
1322    #[must_use]
1323    pub fn as_deref(&self) -> &TimeNumOffsetColonStr {
1324        unsafe {
1325            // This is safe because `self.0` is valid time-numoffset string.
1326            debug_assert_ok!(TimeNumOffsetColonStr::from_bytes(&self.0));
1327            TimeNumOffsetColonStr::from_bytes_maybe_unchecked(&self.0)
1328        }
1329    }
1330
1331    /// Returns a `&mut TimeNumOffsetColonStr` for the string.
1332    ///
1333    /// # Examples
1334    ///
1335    /// ```
1336    /// # use datetime_string::common::TimeNumOffsetColonString;
1337    /// use datetime_string::common::TimeNumOffsetColonStr;
1338    ///
1339    /// let mut offset = "-12:34".parse::<TimeNumOffsetColonString>()?;
1340    ///
1341    /// // Usually you don't need to call `as_deref_mut()` explicitly, because
1342    /// // `DerefMut` trait is implemented.
1343    /// let _: &mut TimeNumOffsetColonStr = offset.as_deref_mut();
1344    /// # Ok::<_, datetime_string::Error>(())
1345    /// ```
1346    #[inline]
1347    #[must_use]
1348    pub fn as_deref_mut(&mut self) -> &mut TimeNumOffsetColonStr {
1349        unsafe {
1350            // This is safe because `self.0` is valid time-numoffset string.
1351            debug_assert_ok!(TimeNumOffsetColonStr::from_bytes(&self.0));
1352            TimeNumOffsetColonStr::from_bytes_maybe_unchecked_mut(&mut self.0)
1353        }
1354    }
1355}
1356
1357impl core::borrow::Borrow<TimeNumOffsetColonStr> for TimeNumOffsetColonString {
1358    #[inline]
1359    fn borrow(&self) -> &TimeNumOffsetColonStr {
1360        self.as_deref()
1361    }
1362}
1363
1364impl core::borrow::BorrowMut<TimeNumOffsetColonStr> for TimeNumOffsetColonString {
1365    #[inline]
1366    fn borrow_mut(&mut self) -> &mut TimeNumOffsetColonStr {
1367        self.as_deref_mut()
1368    }
1369}
1370
1371impl AsRef<[u8]> for TimeNumOffsetColonString {
1372    #[inline]
1373    fn as_ref(&self) -> &[u8] {
1374        self.as_bytes()
1375    }
1376}
1377
1378impl AsRef<str> for TimeNumOffsetColonString {
1379    #[inline]
1380    fn as_ref(&self) -> &str {
1381        self.as_str()
1382    }
1383}
1384
1385impl AsRef<TimeNumOffsetColonStr> for TimeNumOffsetColonString {
1386    #[inline]
1387    fn as_ref(&self) -> &TimeNumOffsetColonStr {
1388        self
1389    }
1390}
1391
1392impl AsMut<TimeNumOffsetColonStr> for TimeNumOffsetColonString {
1393    #[inline]
1394    fn as_mut(&mut self) -> &mut TimeNumOffsetColonStr {
1395        self
1396    }
1397}
1398
1399#[cfg(feature = "alloc")]
1400#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
1401impl From<TimeNumOffsetColonString> for Vec<u8> {
1402    #[inline]
1403    fn from(v: TimeNumOffsetColonString) -> Vec<u8> {
1404        (*v.as_bytes_fixed_len()).into()
1405    }
1406}
1407
1408#[cfg(feature = "alloc")]
1409#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
1410impl From<TimeNumOffsetColonString> for String {
1411    #[inline]
1412    fn from(v: TimeNumOffsetColonString) -> String {
1413        let vec: Vec<u8> = (*v.as_bytes_fixed_len()).into();
1414        unsafe {
1415            // This is safe because a valid time-numoffset string is also an ASCII string.
1416            String::from_utf8_unchecked(vec)
1417        }
1418    }
1419}
1420
1421impl From<&TimeNumOffsetColonStr> for TimeNumOffsetColonString {
1422    fn from(v: &TimeNumOffsetColonStr) -> Self {
1423        unsafe {
1424            // This is safe because the value is already validated.
1425            Self::new_maybe_unchecked(*v.as_bytes_fixed_len())
1426        }
1427    }
1428}
1429
1430impl TryFrom<&[u8]> for TimeNumOffsetColonString {
1431    type Error = Error;
1432
1433    #[inline]
1434    fn try_from(v: &[u8]) -> Result<Self, Self::Error> {
1435        TimeNumOffsetColonStr::from_bytes(v).map(Into::into)
1436    }
1437}
1438
1439impl TryFrom<&str> for TimeNumOffsetColonString {
1440    type Error = Error;
1441
1442    #[inline]
1443    fn try_from(v: &str) -> Result<Self, Self::Error> {
1444        TimeNumOffsetColonStr::from_str(v).map(Into::into)
1445    }
1446}
1447
1448impl TryFrom<[u8; 6]> for TimeNumOffsetColonString {
1449    type Error = Error;
1450
1451    #[inline]
1452    fn try_from(v: [u8; 6]) -> Result<Self, Self::Error> {
1453        validate_bytes(&v)?;
1454        Ok(unsafe {
1455            // This is safe because the value is successfully validated.
1456            Self::new_maybe_unchecked(v)
1457        })
1458    }
1459}
1460
1461#[cfg(feature = "chrono04")]
1462#[cfg_attr(docsrs, doc(cfg(feature = "chrono04")))]
1463impl TryFrom<&chrono04::FixedOffset> for TimeNumOffsetColonString {
1464    type Error = Error;
1465
1466    /// Converts the given offset into `TimeNumOffsetColonString`.
1467    ///
1468    /// # Failures
1469    ///
1470    /// Fails when the second is not zero (for example, `+00:00:30`).
1471    fn try_from(v: &chrono04::FixedOffset) -> Result<Self, Self::Error> {
1472        let seconds = v.local_minus_utc();
1473
1474        if seconds % 60 != 0 {
1475            // `TimeNumOffsetColonString` can have only offsets with seconds part zero.
1476            return Err(ErrorKind::ComponentOutOfRange(ComponentKind::Offset).into());
1477        }
1478        // `chrono04::offset::FixedOffset` is guaranteed to have time offset
1479        // from UTC-23:59:59 to UTC+23:59:59.
1480        // This range can be representable by TimeNumOffsetColonString, if the
1481        // seconds component is zero.
1482        debug_assert!(seconds < i32::from(i16::max_value()));
1483        Ok(Self::from_minutes((seconds / 60) as i16)
1484            .expect("`chrono04::FixedOffset` must always have a valid time"))
1485    }
1486}
1487
1488impl fmt::Display for TimeNumOffsetColonString {
1489    #[inline]
1490    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1491        self.as_deref().fmt(f)
1492    }
1493}
1494
1495impl ops::Deref for TimeNumOffsetColonString {
1496    type Target = TimeNumOffsetColonStr;
1497
1498    #[inline]
1499    fn deref(&self) -> &Self::Target {
1500        self.as_deref()
1501    }
1502}
1503
1504impl ops::DerefMut for TimeNumOffsetColonString {
1505    #[inline]
1506    fn deref_mut(&mut self) -> &mut Self::Target {
1507        self.as_deref_mut()
1508    }
1509}
1510
1511impl str::FromStr for TimeNumOffsetColonString {
1512    type Err = Error;
1513
1514    #[inline]
1515    fn from_str(s: &str) -> Result<Self, Self::Err> {
1516        Self::try_from(s)
1517    }
1518}
1519
1520impl_cmp_symmetric!(
1521    TimeNumOffsetColonStr,
1522    TimeNumOffsetColonString,
1523    &TimeNumOffsetColonString
1524);
1525impl_cmp_symmetric!(
1526    TimeNumOffsetColonStr,
1527    TimeNumOffsetColonString,
1528    TimeNumOffsetColonStr
1529);
1530impl_cmp_symmetric!(
1531    TimeNumOffsetColonStr,
1532    TimeNumOffsetColonString,
1533    &TimeNumOffsetColonStr
1534);
1535impl_cmp_symmetric!(str, TimeNumOffsetColonString, str);
1536impl_cmp_symmetric!(str, TimeNumOffsetColonString, &str);
1537impl_cmp_symmetric!(str, &TimeNumOffsetColonString, str);
1538impl_cmp_symmetric!([u8], TimeNumOffsetColonString, [u8]);
1539impl_cmp_symmetric!([u8], TimeNumOffsetColonString, &[u8]);
1540impl_cmp_symmetric!([u8], &TimeNumOffsetColonString, [u8]);
1541
1542#[cfg(feature = "serde")]
1543#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
1544impl serde::Serialize for TimeNumOffsetColonString {
1545    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1546    where
1547        S: serde::Serializer,
1548    {
1549        serializer.serialize_str(self.as_str())
1550    }
1551}
1552
1553/// Items for serde support.
1554#[cfg(feature = "serde")]
1555mod serde_ {
1556    use super::*;
1557
1558    use serde::de::{Deserialize, Deserializer, Visitor};
1559
1560    /// Visitor for `&TimeNumOffsetColonStr`.
1561    struct StrVisitor;
1562
1563    impl<'de> Visitor<'de> for StrVisitor {
1564        type Value = &'de TimeNumOffsetColonStr;
1565
1566        #[inline]
1567        fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1568            f.write_str("RFC 3339 time-numoffset time string")
1569        }
1570
1571        #[inline]
1572        fn visit_borrowed_bytes<E>(self, v: &'de [u8]) -> Result<Self::Value, E>
1573        where
1574            E: serde::de::Error,
1575        {
1576            Self::Value::try_from(v).map_err(E::custom)
1577        }
1578
1579        #[inline]
1580        fn visit_borrowed_str<E>(self, v: &'de str) -> Result<Self::Value, E>
1581        where
1582            E: serde::de::Error,
1583        {
1584            Self::Value::try_from(v).map_err(E::custom)
1585        }
1586    }
1587
1588    impl<'de> Deserialize<'de> for &'de TimeNumOffsetColonStr {
1589        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1590        where
1591            D: Deserializer<'de>,
1592        {
1593            deserializer.deserialize_any(StrVisitor)
1594        }
1595    }
1596
1597    /// Visitor for `TimeNumOffsetColonString`.
1598    struct StringVisitor;
1599
1600    impl<'de> Visitor<'de> for StringVisitor {
1601        type Value = TimeNumOffsetColonString;
1602
1603        #[inline]
1604        fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1605            f.write_str("RFC 3339 time-numoffset time string")
1606        }
1607
1608        #[inline]
1609        fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
1610        where
1611            E: serde::de::Error,
1612        {
1613            Self::Value::try_from(v).map_err(E::custom)
1614        }
1615
1616        #[inline]
1617        fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
1618        where
1619            E: serde::de::Error,
1620        {
1621            Self::Value::try_from(v).map_err(E::custom)
1622        }
1623    }
1624
1625    impl<'de> Deserialize<'de> for TimeNumOffsetColonString {
1626        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1627        where
1628            D: Deserializer<'de>,
1629        {
1630            deserializer.deserialize_any(StringVisitor)
1631        }
1632    }
1633}
1634
1635#[cfg(test)]
1636mod tests {
1637    #[cfg(feature = "serde")]
1638    use super::*;
1639
1640    use super::validate_bytes as s_validate;
1641
1642    #[cfg(feature = "serde")]
1643    use serde_test::{assert_de_tokens, assert_tokens, Token};
1644
1645    #[test]
1646    fn validate_bytes() {
1647        assert!(s_validate(b"+00:00").is_ok());
1648        assert!(s_validate(b"+00:30").is_ok());
1649        assert!(s_validate(b"+00:59").is_ok());
1650        assert!(s_validate(b"+12:00").is_ok());
1651        assert!(s_validate(b"+12:30").is_ok());
1652        assert!(s_validate(b"+12:59").is_ok());
1653        assert!(s_validate(b"+23:00").is_ok());
1654        assert!(s_validate(b"+23:30").is_ok());
1655        assert!(s_validate(b"+23:59").is_ok());
1656        assert!(s_validate(b"-00:00").is_ok());
1657        assert!(s_validate(b"-00:30").is_ok());
1658        assert!(s_validate(b"-00:59").is_ok());
1659        assert!(s_validate(b"-12:00").is_ok());
1660        assert!(s_validate(b"-12:30").is_ok());
1661        assert!(s_validate(b"-12:59").is_ok());
1662        assert!(s_validate(b"-23:00").is_ok());
1663        assert!(s_validate(b"-23:30").is_ok());
1664        assert!(s_validate(b"-23:59").is_ok());
1665
1666        assert!(s_validate(b"Z").is_err());
1667        assert!(s_validate(b"+24:00").is_err());
1668        assert!(s_validate(b"-24:00").is_err());
1669        assert!(s_validate(b"+23:60").is_err());
1670        assert!(s_validate(b"-23:60").is_err());
1671        assert!(s_validate(b"00:00").is_err());
1672    }
1673
1674    #[cfg(feature = "serde")]
1675    #[test]
1676    fn ser_de_str() {
1677        let raw: &'static str = "-12:34";
1678        assert_tokens(
1679            &TimeNumOffsetColonStr::from_str(raw).unwrap(),
1680            &[Token::BorrowedStr(raw)],
1681        );
1682    }
1683
1684    #[cfg(feature = "serde")]
1685    #[test]
1686    fn ser_de_string() {
1687        let raw: &'static str = "-12:34";
1688        assert_tokens(
1689            &TimeNumOffsetColonString::try_from(raw).unwrap(),
1690            &[Token::Str(raw)],
1691        );
1692    }
1693
1694    #[cfg(feature = "serde")]
1695    #[test]
1696    fn de_bytes_slice() {
1697        let raw: &'static [u8; 6] = b"-12:34";
1698        assert_de_tokens(
1699            &TimeNumOffsetColonStr::from_bytes(raw).unwrap(),
1700            &[Token::BorrowedBytes(raw)],
1701        );
1702    }
1703
1704    #[cfg(feature = "serde")]
1705    #[test]
1706    fn de_bytes() {
1707        let raw: &'static [u8; 6] = b"-12:34";
1708        assert_de_tokens(
1709            &TimeNumOffsetColonString::try_from(&raw[..]).unwrap(),
1710            &[Token::Bytes(raw)],
1711        );
1712    }
1713}