#![allow(clippy::missing_safety_doc)]
#[derive(Clone, Hash, PartialEq, Eq)]
#[repr(transparent)]
pub struct Timestamp(String);
impl Timestamp {
#[inline]
pub fn new(raw: String) -> Result<Self, TimestampParseError> {
TimestampRef::validate(raw.as_ref())?;
Ok(Self(raw))
}
#[allow(unsafe_code)]
#[inline]
pub const unsafe fn new_unchecked(raw: String) -> Self { Self(raw) }
#[inline]
#[track_caller]
pub fn from_static(raw: &'static str) -> Self {
::std::borrow::ToOwned::to_owned(TimestampRef::from_static(raw))
}
#[allow(unsafe_code)]
#[inline]
pub fn into_boxed_ref(self) -> Box<TimestampRef> {
fn _ptr_safety_comment() {}
let box_str = self.0.into_boxed_str();
unsafe { Box::from_raw(Box::into_raw(box_str) as *mut TimestampRef) }
}
#[inline]
pub fn take(self) -> String { self.0 }
}
impl ::std::convert::From<&'_ TimestampRef> for Timestamp {
#[inline]
fn from(s: &TimestampRef) -> Self { ::std::borrow::ToOwned::to_owned(s) }
}
impl ::std::convert::From<Timestamp> for ::std::string::String {
#[inline]
fn from(s: Timestamp) -> Self { s.0 }
}
impl ::std::borrow::Borrow<TimestampRef> for Timestamp {
#[inline]
fn borrow(&self) -> &TimestampRef { ::std::ops::Deref::deref(self) }
}
impl ::std::convert::AsRef<TimestampRef> for Timestamp {
#[inline]
fn as_ref(&self) -> &TimestampRef { ::std::ops::Deref::deref(self) }
}
impl ::std::convert::AsRef<str> for Timestamp {
#[inline]
fn as_ref(&self) -> &str { self.as_str() }
}
impl ::std::convert::From<Timestamp> for Box<TimestampRef> {
#[inline]
fn from(r: Timestamp) -> Self { r.into_boxed_ref() }
}
impl ::std::convert::From<Box<TimestampRef>> for Timestamp {
#[inline]
fn from(r: Box<TimestampRef>) -> Self { r.into_owned() }
}
impl<'a> ::std::convert::From<::std::borrow::Cow<'a, TimestampRef>> for Timestamp {
#[inline]
fn from(r: ::std::borrow::Cow<'a, TimestampRef>) -> Self {
match r {
::std::borrow::Cow::Borrowed(b) => ::std::borrow::ToOwned::to_owned(b),
::std::borrow::Cow::Owned(o) => o,
}
}
}
impl ::std::convert::From<Timestamp> for ::std::borrow::Cow<'_, TimestampRef> {
#[inline]
fn from(owned: Timestamp) -> Self { ::std::borrow::Cow::Owned(owned) }
}
impl ::std::convert::TryFrom<::std::string::String> for Timestamp {
type Error = TimestampParseError;
#[inline]
fn try_from(s: ::std::string::String) -> Result<Self, Self::Error> {
const fn ensure_try_from_string_error_converts_to_validator_error<
T: From<<String as ::std::convert::TryFrom<::std::string::String>>::Error>,
>() {
}
ensure_try_from_string_error_converts_to_validator_error::<Self::Error>();
Self::new(s)
}
}
impl ::std::convert::TryFrom<&'_ str> for Timestamp {
type Error = TimestampParseError;
#[inline]
fn try_from(s: &str) -> Result<Self, Self::Error> {
let ref_ty = TimestampRef::from_str(s)?;
Ok(::std::borrow::ToOwned::to_owned(ref_ty))
}
}
impl ::std::str::FromStr for Timestamp {
type Err = TimestampParseError;
#[inline]
fn from_str(s: &str) -> Result<Self, Self::Err> {
let ref_ty = TimestampRef::from_str(s)?;
Ok(::std::borrow::ToOwned::to_owned(ref_ty))
}
}
impl ::std::borrow::Borrow<str> for Timestamp {
#[inline]
fn borrow(&self) -> &str { self.as_str() }
}
impl ::std::ops::Deref for Timestamp {
type Target = TimestampRef;
#[allow(unsafe_code)]
#[inline]
fn deref(&self) -> &Self::Target {
fn _unchecked_safety_comment() {}
unsafe { TimestampRef::from_str_unchecked(::std::convert::AsRef::as_ref(&self.0)) }
}
}
impl ::std::fmt::Debug for Timestamp {
#[inline]
fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
<TimestampRef as ::std::fmt::Debug>::fmt(::std::ops::Deref::deref(self), f)
}
}
impl ::std::fmt::Display for Timestamp {
#[inline]
fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
<TimestampRef as ::std::fmt::Display>::fmt(::std::ops::Deref::deref(self), f)
}
}
#[cfg(feature = "serde")]
impl ::serde::Serialize for Timestamp {
fn serialize<S: ::serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
<String as ::serde::Serialize>::serialize(&self.0, serializer)
}
}
#[cfg(feature = "serde")]
impl<'de> ::serde::Deserialize<'de> for Timestamp {
fn deserialize<D: ::serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let raw = <String as ::serde::Deserialize<'de>>::deserialize(deserializer)?;
Self::new(raw).map_err(<D::Error as ::serde::de::Error>::custom)
}
}
#[repr(transparent)]
#[derive(Hash, PartialEq, Eq)]
pub struct TimestampRef(str);
impl TimestampRef {
#[allow(unsafe_code, clippy::should_implement_trait)]
#[inline]
pub fn from_str(raw: &str) -> Result<&Self, TimestampParseError> {
Self::validate(raw)?;
fn _unchecked_safety_comment() {}
Ok(unsafe { Self::from_str_unchecked(raw) })
}
#[allow(unsafe_code)]
#[inline]
pub const unsafe fn from_str_unchecked(raw: &str) -> &Self {
fn _ptr_safety_comment() {}
&*(raw as *const str as *const Self)
}
#[inline]
#[track_caller]
pub fn from_static(raw: &'static str) -> &'static Self {
Self::from_str(raw).expect(concat!("invalid ", stringify!(TimestampRef)))
}
#[allow(unsafe_code)]
#[inline]
pub fn into_owned(self: Box<TimestampRef>) -> Timestamp {
fn _ptr_safety_comment() {}
let raw = Box::into_raw(self);
let boxed = unsafe { Box::from_raw(raw as *mut str) };
let s = ::std::convert::From::from(boxed);
fn _unchecked_safety_comment() {}
unsafe { Timestamp::new_unchecked(s) }
}
#[inline]
pub const fn as_str(&self) -> &str { &self.0 }
}
impl ::std::borrow::ToOwned for TimestampRef {
type Owned = Timestamp;
#[inline]
fn to_owned(&self) -> Self::Owned { Timestamp(self.0.into()) }
}
impl ::std::cmp::PartialEq<TimestampRef> for Timestamp {
#[inline]
fn eq(&self, other: &TimestampRef) -> bool { self.as_str() == other.as_str() }
}
impl ::std::cmp::PartialEq<Timestamp> for TimestampRef {
#[inline]
fn eq(&self, other: &Timestamp) -> bool { self.as_str() == other.as_str() }
}
impl ::std::cmp::PartialEq<&'_ TimestampRef> for Timestamp {
#[inline]
fn eq(&self, other: &&TimestampRef) -> bool { self.as_str() == other.as_str() }
}
impl ::std::cmp::PartialEq<Timestamp> for &'_ TimestampRef {
#[inline]
fn eq(&self, other: &Timestamp) -> bool { self.as_str() == other.as_str() }
}
impl<'a> ::std::convert::TryFrom<&'a str> for &'a TimestampRef {
type Error = TimestampParseError;
#[inline]
fn try_from(s: &'a str) -> Result<&'a TimestampRef, Self::Error> { TimestampRef::from_str(s) }
}
impl ::std::borrow::Borrow<str> for TimestampRef {
#[inline]
fn borrow(&self) -> &str { &self.0 }
}
impl ::std::convert::AsRef<str> for TimestampRef {
#[inline]
fn as_ref(&self) -> &str { &self.0 }
}
impl<'a> ::std::convert::From<&'a TimestampRef> for ::std::borrow::Cow<'a, TimestampRef> {
#[inline]
fn from(r: &'a TimestampRef) -> Self { ::std::borrow::Cow::Borrowed(r) }
}
impl<'a, 'b: 'a> ::std::convert::From<&'a ::std::borrow::Cow<'b, TimestampRef>>
for &'a TimestampRef
{
#[inline]
fn from(r: &'a ::std::borrow::Cow<'b, TimestampRef>) -> &'a TimestampRef {
::std::borrow::Borrow::borrow(r)
}
}
impl ::std::convert::From<&'_ TimestampRef> for ::std::rc::Rc<TimestampRef> {
#[allow(unsafe_code)]
#[inline]
fn from(r: &'_ TimestampRef) -> Self {
fn _ptr_safety_comment() {}
let rc = ::std::rc::Rc::<str>::from(r.as_str());
unsafe { ::std::rc::Rc::from_raw(::std::rc::Rc::into_raw(rc) as *const TimestampRef) }
}
}
impl ::std::convert::From<&'_ TimestampRef> for ::std::sync::Arc<TimestampRef> {
#[allow(unsafe_code)]
#[inline]
fn from(r: &'_ TimestampRef) -> Self {
fn _ptr_safety_comment() {}
let arc = ::std::sync::Arc::<str>::from(r.as_str());
unsafe {
::std::sync::Arc::from_raw(::std::sync::Arc::into_raw(arc) as *const TimestampRef)
}
}
}
impl ::std::fmt::Debug for TimestampRef {
#[inline]
fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
<str as ::std::fmt::Debug>::fmt(&self.0, f)
}
}
impl ::std::fmt::Display for TimestampRef {
#[inline]
fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
<str as ::std::fmt::Display>::fmt(&self.0, f)
}
}
#[cfg(feature = "serde")]
impl ::serde::Serialize for TimestampRef {
fn serialize<S: ::serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
<str as ::serde::Serialize>::serialize(self.as_str(), serializer)
}
}
#[cfg(feature = "serde")]
impl<'de: 'a, 'a> ::serde::Deserialize<'de> for &'a TimestampRef {
fn deserialize<D: ::serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let raw = <&str as ::serde::Deserialize<'de>>::deserialize(deserializer)?;
TimestampRef::from_str(raw).map_err(<D::Error as ::serde::de::Error>::custom)
}
}
#[cfg(feature = "serde")]
impl<'de> ::serde::Deserialize<'de> for Box<TimestampRef> {
fn deserialize<D: ::serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let owned = <Timestamp as ::serde::Deserialize<'de>>::deserialize(deserializer)?;
Ok(owned.into_boxed_ref())
}
}
impl_extra!(validated, Timestamp, TimestampRef, TimestampParseError);
#[cfg(feature = "arbitrary")]
impl<'a> arbitrary::Arbitrary<'a> for Timestamp {
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
let year = u.int_in_range(0..=9999)?;
let month = u.int_in_range(1..=12)?;
const M_D: [u8; 12] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
let day = u.int_in_range(1..=M_D[month as usize - 1])?;
let hour = u.int_in_range(0..=23)?;
let minute = u.int_in_range(0..=59)?;
let second = u.int_in_range(0..=59)?;
let millis = if bool::arbitrary(u)? {
let millis = u.int_in_range(0..=999)?;
format!(".{millis:03}")
} else {
"".to_owned()
};
format!("{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}{millis}Z")
.parse()
.map_err(|_| arbitrary::Error::IncorrectFormat)
}
}
impl TimestampRef {
fn validate(s: &str) -> Result<(), TimestampParseError> {
#[cfg(feature = "time")]
{
let _ = time::OffsetDateTime::parse(s, &time::format_description::well_known::Rfc3339)?;
Ok(())
}
#[cfg(not(feature = "time"))]
{
if !s.chars().all(|c| {
c.is_numeric()
|| c == 'T'
|| c == 'Z'
|| c == '+'
|| c == '.'
|| c == '-'
|| c == ':'
}) {
return Err(TimestampParseError::invalid());
}
if let Some(i) = s.find('T') {
if i < 1 {
return Err(TimestampParseError::invalid());
};
let (full_date, full_time) = s.split_at(i);
if full_date.len() != "1900-00-00".len() {
return Err(TimestampParseError::invalid_s(full_date));
}
if !full_date.chars().all(|c| c.is_numeric() || c == '-') {
return Err(TimestampParseError::invalid_s(full_date));
}
let partial_time = if let Some(stripped) = full_time.strip_suffix('Z') {
stripped
} else {
return Err(TimestampParseError::Other("unsupported non-UTC timestamp, enable the `time` feature in `twitch_types` to enable parsing these"));
};
if 2 != partial_time
.chars()
.into_iter()
.filter(|&b| b == ':')
.count()
{
return Err(TimestampParseError::invalid_s(partial_time));
};
if !partial_time.contains('.') && partial_time.len() != "T00:00:00".len() {
return Err(TimestampParseError::invalid_s(partial_time));
} else if partial_time.contains('.') {
let mut i = partial_time.split('.');
if !i
.next()
.map(|s| s.len() == "T00:00:00".len())
.unwrap_or_default()
{
return Err(TimestampParseError::invalid_s(partial_time));
}
}
} else {
return Err(TimestampParseError::invalid());
}
Ok(())
}
}
}
impl From<std::convert::Infallible> for TimestampParseError {
fn from(value: std::convert::Infallible) -> Self { match value {} }
}
#[derive(Debug)]
#[non_exhaustive]
pub enum TimestampParseError {
#[cfg(feature = "time")]
TimeError(time::error::Parse),
#[cfg(feature = "time")]
TimeFormatError(time::error::Format),
Other(&'static str),
InvalidFormat {
location: &'static std::panic::Location<'static>,
s: Option<String>,
},
}
impl core::fmt::Display for TimestampParseError {
fn fmt(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result {
#[allow(unused_variables)]
match self {
#[cfg(feature = "time")]
TimestampParseError::TimeError(parse_error) => {
write!(formatter, "Could not parse the timestamp using `time`")
}
#[cfg(feature = "time")]
TimestampParseError::TimeFormatError(_) => {
write!(formatter, "Could not format the timestamp using `time`")
}
TimestampParseError::Other(other) => {
write!(formatter, "{other}")
}
TimestampParseError::InvalidFormat { location, s } => {
write!(
formatter,
"timestamp has an invalid format. {s:?} - {location}"
)
}
}
}
}
impl std::error::Error for TimestampParseError {
fn source(&self) -> std::option::Option<&(dyn std::error::Error + 'static)> {
match self {
#[cfg(feature = "time")]
TimestampParseError::TimeError { 0: source, .. } => {
std::option::Option::Some(source as _)
}
#[cfg(feature = "time")]
TimestampParseError::TimeFormatError { 0: source, .. } => {
std::option::Option::Some(source as _)
}
TimestampParseError::Other { .. } => std::option::Option::None,
TimestampParseError::InvalidFormat { .. } => std::option::Option::None,
}
}
}
#[cfg(feature = "time")]
impl std::convert::From<time::error::Parse> for TimestampParseError {
fn from(source: time::error::Parse) -> Self { TimestampParseError::TimeError(source) }
}
#[cfg(feature = "time")]
impl std::convert::From<time::error::Format> for TimestampParseError {
fn from(source: time::error::Format) -> Self { TimestampParseError::TimeFormatError(source) }
}
impl TimestampParseError {
#[cfg(not(feature = "time"))]
#[track_caller]
fn invalid() -> Self {
Self::InvalidFormat {
location: std::panic::Location::caller(),
s: None,
}
}
#[cfg(not(feature = "time"))]
#[track_caller]
fn invalid_s(s: &str) -> Self {
Self::InvalidFormat {
location: std::panic::Location::caller(),
s: Some(s.to_string()),
}
}
}
impl Timestamp {
fn set_time(&mut self, hours: u8, minutes: u8, seconds: u8) {
#[cfg(feature = "time")]
{
let _ = std::mem::replace(
self,
self.to_fixed_offset()
.replace_time(
time::Time::from_hms(hours, minutes, seconds)
.expect("could not create time"),
)
.try_into()
.expect("could not make timestamp"),
);
}
#[cfg(not(feature = "time"))]
{
const ERROR_MSG: &str = "malformed timestamp";
assert!(hours < 24);
assert!(minutes < 60);
assert!(seconds < 60);
#[inline]
fn replace_len2(s: &mut str, replace: &str) {
assert!(replace.as_bytes().len() == 2);
assert!(s.as_bytes().len() == 2);
let replace = replace.as_bytes();
let b = unsafe { s.as_bytes_mut() };
b[0] = replace[0];
b[1] = replace[1];
}
let t = self.0.find('T').expect(ERROR_MSG);
let partial_time: &mut str = &mut self.0[t + 1..];
let mut matches = partial_time.match_indices(':');
let (h, m, s) = (
0,
matches.next().expect(ERROR_MSG).0 + 1,
matches.next().expect(ERROR_MSG).0 + 1,
);
assert!(matches.next().is_none());
partial_time
.get_mut(h..h + 2)
.map(|s| replace_len2(s, &format!("{:02}", hours)))
.expect(ERROR_MSG);
partial_time
.get_mut(m..m + 2)
.map(|s| replace_len2(s, &format!("{:02}", minutes)))
.expect(ERROR_MSG);
partial_time
.get_mut(s..s + 2)
.map(|s| replace_len2(s, &format!("{:02}", seconds)))
.expect(ERROR_MSG);
}
}
}
#[cfg(feature = "time")]
#[cfg_attr(nightly, doc(cfg(feature = "time")))]
impl Timestamp {
pub fn now() -> Timestamp {
time::OffsetDateTime::now_utc()
.try_into()
.expect("could not make timestamp")
}
pub fn today() -> Timestamp {
time::OffsetDateTime::now_utc()
.replace_time(time::Time::MIDNIGHT)
.try_into()
.expect("could not make timestamp")
}
}
impl TimestampRef {
#[allow(unreachable_code)]
pub fn normalize(&'_ self) -> Result<std::borrow::Cow<'_, TimestampRef>, TimestampParseError> {
let s = self.as_str();
if s.ends_with('Z') {
Ok(self.into())
} else {
#[cfg(feature = "time")]
{
let utc = self.to_utc();
return Ok(std::borrow::Cow::Owned(utc.try_into()?));
}
panic!("non `Z` timestamps are not possible to use without the `time` feature enabled for `twitch_types`")
}
}
pub fn is_before<T: ?Sized>(&self, other: &T) -> bool
where Self: PartialOrd<T> {
self < other
}
pub fn to_day(&self) -> Timestamp {
let mut c = self.to_owned();
c.set_time(0, 0, 0);
c
}
pub fn year(&self) -> &str { &self.0[0..4] }
pub fn month(&self) -> &str { &self.0[5..7] }
pub fn day(&self) -> &str { &self.0[8..10] }
pub fn hour(&self) -> &str { &self.0[11..13] }
pub fn minute(&self) -> &str { &self.0[14..16] }
pub fn second(&self) -> &str { &self.0[17..19] }
pub fn millis(&self) -> Option<&str> {
if self.0[19..].contains('.') {
let sub = &self.0[20..];
Some(&sub[..sub.find(|c: char| !c.is_ascii_digit()).unwrap_or(sub.len())])
} else {
None
}
}
}
#[cfg(feature = "time")]
#[cfg_attr(nightly, doc(cfg(feature = "time")))]
impl TimestampRef {
pub fn to_utc(&self) -> time::OffsetDateTime {
self.to_fixed_offset().to_offset(time::UtcOffset::UTC)
}
pub fn to_fixed_offset(&self) -> time::OffsetDateTime {
time::OffsetDateTime::parse(&self.0, &time::format_description::well_known::Rfc3339)
.expect("this should never fail")
}
}
impl PartialOrd for Timestamp {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
let this: &TimestampRef = self.as_ref();
let other: &TimestampRef = other.as_ref();
this.partial_cmp(other)
}
}
impl PartialOrd<Timestamp> for TimestampRef {
fn partial_cmp(&self, other: &Timestamp) -> Option<std::cmp::Ordering> {
let other: &TimestampRef = other.as_ref();
self.partial_cmp(other)
}
}
impl PartialOrd for TimestampRef {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
let this = self
.normalize()
.expect("normalization failed, this is a bug");
let other = other
.normalize()
.expect("normalization of other failed, this is a bug");
#[allow(clippy::if_same_then_else)]
if this.as_ref().as_str().contains('.') ^ other.as_ref().as_str().contains('.') {
#[cfg(feature = "tracing")]
tracing::warn!("comparing two `Timestamps` with differing punctuation");
return None;
} else if this.0.len() != other.0.len() {
#[cfg(feature = "tracing")]
tracing::warn!("comparing two `Timestamps` with differing length");
return None;
}
this.as_str().partial_cmp(other.as_str())
}
}
#[cfg(feature = "time")]
#[cfg_attr(nightly, doc(cfg(feature = "time")))]
impl PartialEq<time::OffsetDateTime> for Timestamp {
fn eq(&self, other: &time::OffsetDateTime) -> bool {
let this: &TimestampRef = self.as_ref();
this.eq(other)
}
}
#[cfg(feature = "time")]
#[cfg_attr(nightly, doc(cfg(feature = "time")))]
impl PartialOrd<time::OffsetDateTime> for Timestamp {
fn partial_cmp(&self, other: &time::OffsetDateTime) -> Option<std::cmp::Ordering> {
let this: &TimestampRef = self.as_ref();
this.partial_cmp(other)
}
}
#[cfg(feature = "time")]
#[cfg_attr(nightly, doc(cfg(feature = "time")))]
impl PartialEq<time::OffsetDateTime> for TimestampRef {
fn eq(&self, other: &time::OffsetDateTime) -> bool { &self.to_utc() == other }
}
#[cfg(feature = "time")]
#[cfg_attr(nightly, doc(cfg(feature = "time")))]
impl PartialOrd<time::OffsetDateTime> for TimestampRef {
fn partial_cmp(&self, other: &time::OffsetDateTime) -> Option<std::cmp::Ordering> {
self.to_utc().partial_cmp(other)
}
}
#[cfg(feature = "time")]
#[cfg_attr(nightly, doc(cfg(feature = "time")))]
impl std::convert::TryFrom<time::OffsetDateTime> for Timestamp {
type Error = time::error::Format;
fn try_from(value: time::OffsetDateTime) -> Result<Self, Self::Error> {
Ok(Timestamp(
value.format(&time::format_description::well_known::Rfc3339)?,
))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
pub fn time_test() {
let mut time1 = Timestamp::try_from("2021-11-11T10:00:00Z").unwrap();
time1.set_time(10, 0, 32);
let time2 = Timestamp::try_from("2021-11-10T10:00:00Z").unwrap();
assert!(time2.is_before(&time1));
dbg!(time1.normalize().unwrap());
#[cfg(feature = "time")]
let time = Timestamp::try_from("2021-11-11T13:37:00-01:00").unwrap();
#[cfg(feature = "time")]
dbg!(time.normalize().unwrap());
}
}