#[cfg(feature = "alloc")]
mod owned;
use core::{cmp::Ordering, convert::TryFrom, fmt, ops, str};
use crate::{
common::TimeOffsetSign,
error::{ComponentKind, Error, ErrorKind},
};
use super::TimeNumOffsetStr;
#[cfg(feature = "alloc")]
pub use self::owned::TimeOffsetString;
fn validate_bytes(s: &[u8]) -> Result<(), Error> {
match s.len().cmp(&1) {
Ordering::Less => Err(ErrorKind::TooShort.into()),
Ordering::Equal => {
if s[0] == b'Z' {
Ok(())
} else {
Err(ErrorKind::InvalidComponentType(ComponentKind::Offset).into())
}
}
Ordering::Greater => TimeNumOffsetStr::from_bytes(s).map(|_| ()),
}
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(transparent)]
#[allow(clippy::derive_hash_xor_eq)]
#[allow(unknown_lints, clippy::derive_ord_xor_partial_ord)]
pub struct TimeOffsetStr([u8]);
impl TimeOffsetStr {
#[inline]
#[must_use]
pub(crate) unsafe fn from_bytes_maybe_unchecked(s: &[u8]) -> &Self {
debug_assert_ok!(validate_bytes(s));
&*(s as *const [u8] as *const Self)
}
#[inline]
#[must_use]
pub(crate) unsafe fn from_bytes_maybe_unchecked_mut(s: &mut [u8]) -> &mut Self {
debug_assert_ok!(validate_bytes(s));
&mut *(s as *mut [u8] as *mut Self)
}
#[inline]
#[must_use]
unsafe fn from_str_maybe_unchecked_mut(s: &mut str) -> &mut Self {
Self::from_bytes_maybe_unchecked_mut(s.as_bytes_mut())
}
#[inline]
#[allow(clippy::should_implement_trait)]
pub fn from_str(s: &str) -> Result<&Self, Error> {
TryFrom::try_from(s)
}
#[inline]
pub fn from_mut_str(s: &mut str) -> Result<&mut Self, Error> {
TryFrom::try_from(s)
}
#[inline]
pub fn from_bytes(s: &[u8]) -> Result<&Self, Error> {
TryFrom::try_from(s)
}
#[inline]
pub fn from_bytes_mut(s: &mut [u8]) -> Result<&mut Self, Error> {
TryFrom::try_from(s)
}
#[inline]
#[must_use]
pub fn as_str(&self) -> &str {
unsafe {
debug_assert_safe_version_ok!(str::from_utf8(&self.0));
str::from_utf8_unchecked(&self.0)
}
}
#[inline]
#[must_use]
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
#[inline]
pub fn sign(&self) -> Option<TimeOffsetSign> {
match self.0[0] {
b'Z' => None,
b'+' => Some(TimeOffsetSign::Positive),
v => {
debug_assert_eq!(v, b'-');
Some(TimeOffsetSign::Negative)
}
}
}
#[inline]
pub fn to_numoffset(&self) -> Option<&TimeNumOffsetStr> {
if self.len() == 1 {
return None;
}
Some(unsafe {
debug_assert_safe_version_ok!(TimeNumOffsetStr::from_bytes(&self.0));
TimeNumOffsetStr::from_bytes_maybe_unchecked(&self.0)
})
}
#[inline]
#[allow(clippy::wrong_self_convention)]
pub fn to_numoffset_mut(&mut self) -> Option<&mut TimeNumOffsetStr> {
if self.len() == 1 {
return None;
}
Some(unsafe {
debug_assert_ok!(TimeNumOffsetStr::from_bytes(&self.0));
TimeNumOffsetStr::from_bytes_maybe_unchecked_mut(&mut self.0)
})
}
#[inline]
#[must_use]
pub fn hour_abs(&self) -> u8 {
self.to_numoffset().map_or(0, |v| v.hour_abs())
}
#[inline]
#[must_use]
pub fn hour_signed(&self) -> i8 {
self.to_numoffset().map_or(0, |v| v.hour_signed())
}
#[inline]
#[must_use]
pub fn minute(&self) -> u8 {
self.to_numoffset().map_or(0, |v| v.minute())
}
#[inline]
#[must_use]
pub fn in_minutes(&self) -> i16 {
self.to_numoffset().map_or(0, |v| v.in_minutes())
}
#[inline]
#[must_use]
pub fn is_unknown_local_offset(&self) -> bool {
&self.0 == b"-00:00"
}
}
impl AsRef<[u8]> for TimeOffsetStr {
#[inline]
fn as_ref(&self) -> &[u8] {
&self.0
}
}
impl AsRef<str> for TimeOffsetStr {
#[inline]
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl AsRef<TimeOffsetStr> for TimeOffsetStr {
#[inline]
fn as_ref(&self) -> &TimeOffsetStr {
self
}
}
impl AsMut<TimeOffsetStr> for TimeOffsetStr {
#[inline]
fn as_mut(&mut self) -> &mut TimeOffsetStr {
self
}
}
impl<'a> From<&'a TimeOffsetStr> for &'a str {
#[inline]
fn from(v: &'a TimeOffsetStr) -> Self {
v.as_str()
}
}
#[cfg(feature = "chrono04")]
#[cfg_attr(docsrs, doc(cfg(feature = "chrono04")))]
impl From<&TimeOffsetStr> for chrono04::FixedOffset {
#[inline]
fn from(v: &TimeOffsetStr) -> Self {
Self::east(i32::from(v.in_minutes()) * 60)
}
}
#[cfg(feature = "time03")]
#[cfg_attr(docsrs, doc(cfg(feature = "time03")))]
impl From<&TimeOffsetStr> for time03::UtcOffset {
#[inline]
fn from(v: &TimeOffsetStr) -> Self {
Self::from_whole_seconds(i32::from(v.in_minutes()) * 60)
.expect("[validity] the minutes-precision offset must be valid and representable")
}
}
impl<'a> TryFrom<&'a [u8]> for &'a TimeOffsetStr {
type Error = Error;
#[inline]
fn try_from(v: &'a [u8]) -> Result<Self, Self::Error> {
validate_bytes(v)?;
Ok(unsafe {
TimeOffsetStr::from_bytes_maybe_unchecked(v)
})
}
}
impl<'a> TryFrom<&'a mut [u8]> for &'a mut TimeOffsetStr {
type Error = Error;
#[inline]
fn try_from(v: &'a mut [u8]) -> Result<Self, Self::Error> {
validate_bytes(v)?;
Ok(unsafe {
TimeOffsetStr::from_bytes_maybe_unchecked_mut(v)
})
}
}
impl<'a> TryFrom<&'a str> for &'a TimeOffsetStr {
type Error = Error;
#[inline]
fn try_from(v: &'a str) -> Result<Self, Self::Error> {
Self::try_from(v.as_bytes())
}
}
impl<'a> TryFrom<&'a mut str> for &'a mut TimeOffsetStr {
type Error = Error;
#[inline]
fn try_from(v: &'a mut str) -> Result<Self, Self::Error> {
validate_bytes(v.as_bytes())?;
Ok(unsafe {
TimeOffsetStr::from_str_maybe_unchecked_mut(v)
})
}
}
impl fmt::Display for TimeOffsetStr {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.as_str().fmt(f)
}
}
impl ops::Deref for TimeOffsetStr {
type Target = str;
#[inline]
fn deref(&self) -> &Self::Target {
self.as_str()
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for TimeOffsetStr {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(self.as_str())
}
}
impl_cmp_symmetric!(str, TimeOffsetStr, &TimeOffsetStr);
impl_cmp_symmetric!([u8], TimeOffsetStr, [u8]);
impl_cmp_symmetric!([u8], TimeOffsetStr, &[u8]);
impl_cmp_symmetric!([u8], &TimeOffsetStr, [u8]);
impl_cmp_symmetric!(str, TimeOffsetStr, str);
impl_cmp_symmetric!(str, TimeOffsetStr, &str);
impl_cmp_symmetric!(str, &TimeOffsetStr, str);
#[cfg(feature = "serde")]
mod serde_ {
use super::*;
use serde::de::{Deserialize, Deserializer, Visitor};
struct StrVisitor;
impl<'de> Visitor<'de> for StrVisitor {
type Value = &'de TimeOffsetStr;
#[inline]
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("RFC 3339 time-offset string")
}
#[inline]
fn visit_borrowed_bytes<E>(self, v: &'de [u8]) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Self::Value::try_from(v).map_err(E::custom)
}
#[inline]
fn visit_borrowed_str<E>(self, v: &'de str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Self::Value::try_from(v).map_err(E::custom)
}
}
impl<'de> Deserialize<'de> for &'de TimeOffsetStr {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(StrVisitor)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use super::validate_bytes as s_validate;
#[cfg(feature = "serde")]
use serde_test::{assert_de_tokens, assert_tokens, Token};
#[test]
fn validate_bytes() {
assert!(s_validate(b"Z").is_ok());
assert!(s_validate(b"-00:00").is_ok());
assert!(s_validate(b"-12:30").is_ok());
assert!(s_validate(b"-23:59").is_ok());
assert!(s_validate(b"+00:00").is_ok());
assert!(s_validate(b"+12:30").is_ok());
assert!(s_validate(b"+23:59").is_ok());
assert!(TimeOffsetStr::from_str("z").is_err());
assert!(TimeOffsetStr::from_str("a").is_err());
assert!(TimeOffsetStr::from_str("+24:00").is_err());
assert!(TimeOffsetStr::from_str("+00:60").is_err());
assert!(TimeOffsetStr::from_str("-24:00").is_err());
assert!(TimeOffsetStr::from_str("-00:60").is_err());
assert!(TimeOffsetStr::from_str("?00:00").is_err());
assert!(TimeOffsetStr::from_str("00:00").is_err());
}
#[cfg(feature = "serde")]
#[test]
fn ser_de_str() {
let raw: &'static str = "-12:34";
assert_tokens(
&TimeOffsetStr::from_str(raw).unwrap(),
&[Token::BorrowedStr(raw)],
);
}
#[cfg(feature = "serde")]
#[test]
fn de_bytes_slice() {
let raw: &'static [u8; 6] = b"-12:34";
assert_de_tokens(
&TimeOffsetStr::from_bytes(raw).unwrap(),
&[Token::BorrowedBytes(raw)],
);
}
}