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}