twitch_types/
time.rs

1#![allow(clippy::missing_safety_doc)]
2
3#[derive(Clone, Hash, PartialEq, Eq)]
4#[repr(transparent)]
5/// RFC3339 timestamp
6pub struct Timestamp(String);
7
8impl Timestamp {
9    ///Constructs a new Timestamp if it conforms to [`Timestamp`]
10    #[inline]
11    pub fn new(raw: String) -> Result<Self, TimestampParseError> {
12        TimestampRef::validate(raw.as_ref())?;
13        Ok(Self(raw))
14    }
15
16    ///Constructs a new Timestamp without validation
17    ///
18    ///# Safety
19    ///
20    ///Consumers of this function must ensure that values conform to [`Timestamp`]. Failure to maintain this invariant may lead to undefined behavior.
21    #[allow(unsafe_code)]
22    #[inline]
23    pub const unsafe fn new_unchecked(raw: String) -> Self { Self(raw) }
24
25    #[inline]
26    ///Constructs a new Timestamp from a static reference if it conforms to [`Timestamp`]
27    ///
28    ///# Panics
29    ///
30    ///This function will panic if the provided raw string is not valid.
31    #[track_caller]
32    pub fn from_static(raw: &'static str) -> Self {
33        ::std::borrow::ToOwned::to_owned(TimestampRef::from_static(raw))
34    }
35
36    ///Converts this `Timestamp` into a [`Box<TimestampRef>`]
37    ///
38    ///This will drop any excess capacity.
39    #[allow(unsafe_code)]
40    #[inline]
41    pub fn into_boxed_ref(self) -> Box<TimestampRef> {
42        ///SAFETY: `TimestampRef` is `#[repr(transparent)]` around a single `str` field, so a `*mut str` can be safely reinterpreted as a `*mut TimestampRef`
43        fn _ptr_safety_comment() {}
44
45        let box_str = self.0.into_boxed_str();
46        unsafe { Box::from_raw(Box::into_raw(box_str) as *mut TimestampRef) }
47    }
48
49    ///Unwraps the underlying [`String`] value
50    #[inline]
51    pub fn take(self) -> String { self.0 }
52}
53
54impl ::std::convert::From<&'_ TimestampRef> for Timestamp {
55    #[inline]
56    fn from(s: &TimestampRef) -> Self { ::std::borrow::ToOwned::to_owned(s) }
57}
58
59impl ::std::convert::From<Timestamp> for ::std::string::String {
60    #[inline]
61    fn from(s: Timestamp) -> Self { s.0 }
62}
63
64impl ::std::borrow::Borrow<TimestampRef> for Timestamp {
65    #[inline]
66    fn borrow(&self) -> &TimestampRef { ::std::ops::Deref::deref(self) }
67}
68
69impl ::std::convert::AsRef<TimestampRef> for Timestamp {
70    #[inline]
71    fn as_ref(&self) -> &TimestampRef { ::std::ops::Deref::deref(self) }
72}
73
74impl ::std::convert::AsRef<str> for Timestamp {
75    #[inline]
76    fn as_ref(&self) -> &str { self.as_str() }
77}
78
79impl ::std::convert::From<Timestamp> for Box<TimestampRef> {
80    #[inline]
81    fn from(r: Timestamp) -> Self { r.into_boxed_ref() }
82}
83
84impl ::std::convert::From<Box<TimestampRef>> for Timestamp {
85    #[inline]
86    fn from(r: Box<TimestampRef>) -> Self { r.into_owned() }
87}
88
89impl<'a> ::std::convert::From<::std::borrow::Cow<'a, TimestampRef>> for Timestamp {
90    #[inline]
91    fn from(r: ::std::borrow::Cow<'a, TimestampRef>) -> Self {
92        match r {
93            ::std::borrow::Cow::Borrowed(b) => ::std::borrow::ToOwned::to_owned(b),
94            ::std::borrow::Cow::Owned(o) => o,
95        }
96    }
97}
98
99impl ::std::convert::From<Timestamp> for ::std::borrow::Cow<'_, TimestampRef> {
100    #[inline]
101    fn from(owned: Timestamp) -> Self { ::std::borrow::Cow::Owned(owned) }
102}
103
104impl ::std::convert::TryFrom<::std::string::String> for Timestamp {
105    type Error = TimestampParseError;
106
107    #[inline]
108    fn try_from(s: ::std::string::String) -> Result<Self, Self::Error> {
109        const fn ensure_try_from_string_error_converts_to_validator_error<
110            T: From<<String as ::std::convert::TryFrom<::std::string::String>>::Error>,
111        >() {
112        }
113
114        ensure_try_from_string_error_converts_to_validator_error::<Self::Error>();
115        Self::new(s)
116    }
117}
118
119impl ::std::convert::TryFrom<&'_ str> for Timestamp {
120    type Error = TimestampParseError;
121
122    #[inline]
123    fn try_from(s: &str) -> Result<Self, Self::Error> {
124        let ref_ty = TimestampRef::from_str(s)?;
125        Ok(::std::borrow::ToOwned::to_owned(ref_ty))
126    }
127}
128
129impl ::std::str::FromStr for Timestamp {
130    type Err = TimestampParseError;
131
132    #[inline]
133    fn from_str(s: &str) -> Result<Self, Self::Err> {
134        let ref_ty = TimestampRef::from_str(s)?;
135        Ok(::std::borrow::ToOwned::to_owned(ref_ty))
136    }
137}
138
139impl ::std::borrow::Borrow<str> for Timestamp {
140    #[inline]
141    fn borrow(&self) -> &str { self.as_str() }
142}
143
144impl ::std::ops::Deref for Timestamp {
145    type Target = TimestampRef;
146
147    #[allow(unsafe_code)]
148    #[inline]
149    fn deref(&self) -> &Self::Target {
150        ///SAFETY: The value was satisfies the type's invariant and conforms to the required implicit contracts of the validator.
151        fn _unchecked_safety_comment() {}
152
153        unsafe { TimestampRef::from_str_unchecked(::std::convert::AsRef::as_ref(&self.0)) }
154    }
155}
156
157impl ::std::fmt::Debug for Timestamp {
158    #[inline]
159    fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
160        <TimestampRef as ::std::fmt::Debug>::fmt(::std::ops::Deref::deref(self), f)
161    }
162}
163
164impl ::std::fmt::Display for Timestamp {
165    #[inline]
166    fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
167        <TimestampRef as ::std::fmt::Display>::fmt(::std::ops::Deref::deref(self), f)
168    }
169}
170
171#[cfg(feature = "serde")]
172impl ::serde::Serialize for Timestamp {
173    fn serialize<S: ::serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
174        <String as ::serde::Serialize>::serialize(&self.0, serializer)
175    }
176}
177
178#[cfg(feature = "serde")]
179impl<'de> ::serde::Deserialize<'de> for Timestamp {
180    fn deserialize<D: ::serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
181        let raw = <String as ::serde::Deserialize<'de>>::deserialize(deserializer)?;
182        Self::new(raw).map_err(<D::Error as ::serde::de::Error>::custom)
183    }
184}
185#[repr(transparent)]
186#[derive(Hash, PartialEq, Eq)]
187/// RFC3339 timestamp
188pub struct TimestampRef(str);
189
190impl TimestampRef {
191    #[allow(unsafe_code, clippy::should_implement_trait)]
192    #[inline]
193    ///Transparently reinterprets the string slice as a strongly-typed TimestampRef if it conforms to [`Timestamp`]
194    pub fn from_str(raw: &str) -> Result<&Self, TimestampParseError> {
195        Self::validate(raw)?;
196        ///SAFETY: The value was just checked and found to already conform to the required implicit contracts of the validator.
197        fn _unchecked_safety_comment() {}
198
199        Ok(unsafe { Self::from_str_unchecked(raw) })
200    }
201
202    #[allow(unsafe_code)]
203    #[inline]
204    ///Transparently reinterprets the string slice as a strongly-typed TimestampRef without validating
205    pub const unsafe fn from_str_unchecked(raw: &str) -> &Self {
206        ///SAFETY: `TimestampRef` is `#[repr(transparent)]` around a single `str` field, so a `*const str` can be safely reinterpreted as a `*const TimestampRef`
207        fn _ptr_safety_comment() {}
208
209        &*(raw as *const str as *const Self)
210    }
211
212    #[inline]
213    ///Transparently reinterprets the static string slice as a strongly-typed TimestampRef if it conforms to [`Timestamp`]
214    ///
215    ///# Panics
216    ///
217    ///This function will panic if the provided raw string is not valid.
218    #[track_caller]
219    pub fn from_static(raw: &'static str) -> &'static Self {
220        Self::from_str(raw).expect(concat!("invalid ", stringify!(TimestampRef)))
221    }
222
223    #[allow(unsafe_code)]
224    #[inline]
225    ///Converts a [`Box<TimestampRef>`] into a [`Timestamp`] without copying or allocating
226    pub fn into_owned(self: Box<TimestampRef>) -> Timestamp {
227        ///SAFETY: `TimestampRef` is `#[repr(transparent)]` around a single `str` field, so a `*mut str` can be safely reinterpreted as a `*mut TimestampRef`
228        fn _ptr_safety_comment() {}
229
230        let raw = Box::into_raw(self);
231        let boxed = unsafe { Box::from_raw(raw as *mut str) };
232        let s = ::std::convert::From::from(boxed);
233        ///SAFETY: The value was just checked and found to already conform to the required implicit contracts of the validator.
234        fn _unchecked_safety_comment() {}
235
236        unsafe { Timestamp::new_unchecked(s) }
237    }
238
239    /// Provides access to the underlying value as a string slice.
240    #[inline]
241    pub const fn as_str(&self) -> &str { &self.0 }
242}
243
244impl ::std::borrow::ToOwned for TimestampRef {
245    type Owned = Timestamp;
246
247    #[inline]
248    fn to_owned(&self) -> Self::Owned { Timestamp(self.0.into()) }
249}
250
251impl ::std::cmp::PartialEq<TimestampRef> for Timestamp {
252    #[inline]
253    fn eq(&self, other: &TimestampRef) -> bool { self.as_str() == other.as_str() }
254}
255
256impl ::std::cmp::PartialEq<Timestamp> for TimestampRef {
257    #[inline]
258    fn eq(&self, other: &Timestamp) -> bool { self.as_str() == other.as_str() }
259}
260
261impl ::std::cmp::PartialEq<&'_ TimestampRef> for Timestamp {
262    #[inline]
263    fn eq(&self, other: &&TimestampRef) -> bool { self.as_str() == other.as_str() }
264}
265
266impl ::std::cmp::PartialEq<Timestamp> for &'_ TimestampRef {
267    #[inline]
268    fn eq(&self, other: &Timestamp) -> bool { self.as_str() == other.as_str() }
269}
270
271impl<'a> ::std::convert::TryFrom<&'a str> for &'a TimestampRef {
272    type Error = TimestampParseError;
273
274    #[inline]
275    fn try_from(s: &'a str) -> Result<&'a TimestampRef, Self::Error> { TimestampRef::from_str(s) }
276}
277
278impl ::std::borrow::Borrow<str> for TimestampRef {
279    #[inline]
280    fn borrow(&self) -> &str { &self.0 }
281}
282
283impl ::std::convert::AsRef<str> for TimestampRef {
284    #[inline]
285    fn as_ref(&self) -> &str { &self.0 }
286}
287
288impl<'a> ::std::convert::From<&'a TimestampRef> for ::std::borrow::Cow<'a, TimestampRef> {
289    #[inline]
290    fn from(r: &'a TimestampRef) -> Self { ::std::borrow::Cow::Borrowed(r) }
291}
292
293impl<'a, 'b: 'a> ::std::convert::From<&'a ::std::borrow::Cow<'b, TimestampRef>>
294    for &'a TimestampRef
295{
296    #[inline]
297    fn from(r: &'a ::std::borrow::Cow<'b, TimestampRef>) -> &'a TimestampRef {
298        ::std::borrow::Borrow::borrow(r)
299    }
300}
301
302impl ::std::convert::From<&'_ TimestampRef> for ::std::rc::Rc<TimestampRef> {
303    #[allow(unsafe_code)]
304    #[inline]
305    fn from(r: &'_ TimestampRef) -> Self {
306        ///SAFETY: `TimestampRef` is `#[repr(transparent)]` around a single `str` field, so a `*const str` can be safely reinterpreted as a `*const TimestampRef`
307        fn _ptr_safety_comment() {}
308
309        let rc = ::std::rc::Rc::<str>::from(r.as_str());
310        unsafe { ::std::rc::Rc::from_raw(::std::rc::Rc::into_raw(rc) as *const TimestampRef) }
311    }
312}
313
314impl ::std::convert::From<&'_ TimestampRef> for ::std::sync::Arc<TimestampRef> {
315    #[allow(unsafe_code)]
316    #[inline]
317    fn from(r: &'_ TimestampRef) -> Self {
318        ///SAFETY: `TimestampRef` is `#[repr(transparent)]` around a single `str` field, so a `*const str` can be safely reinterpreted as a `*const TimestampRef`
319        fn _ptr_safety_comment() {}
320
321        let arc = ::std::sync::Arc::<str>::from(r.as_str());
322        unsafe {
323            ::std::sync::Arc::from_raw(::std::sync::Arc::into_raw(arc) as *const TimestampRef)
324        }
325    }
326}
327
328impl ::std::fmt::Debug for TimestampRef {
329    #[inline]
330    fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
331        <str as ::std::fmt::Debug>::fmt(&self.0, f)
332    }
333}
334
335impl ::std::fmt::Display for TimestampRef {
336    #[inline]
337    fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
338        <str as ::std::fmt::Display>::fmt(&self.0, f)
339    }
340}
341
342#[cfg(feature = "serde")]
343impl ::serde::Serialize for TimestampRef {
344    fn serialize<S: ::serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
345        <str as ::serde::Serialize>::serialize(self.as_str(), serializer)
346    }
347}
348
349#[cfg(feature = "serde")]
350impl<'de: 'a, 'a> ::serde::Deserialize<'de> for &'a TimestampRef {
351    fn deserialize<D: ::serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
352        let raw = <&str as ::serde::Deserialize<'de>>::deserialize(deserializer)?;
353        TimestampRef::from_str(raw).map_err(<D::Error as ::serde::de::Error>::custom)
354    }
355}
356
357#[cfg(feature = "serde")]
358impl<'de> ::serde::Deserialize<'de> for Box<TimestampRef> {
359    fn deserialize<D: ::serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
360        let owned = <Timestamp as ::serde::Deserialize<'de>>::deserialize(deserializer)?;
361        Ok(owned.into_boxed_ref())
362    }
363}
364
365impl_extra!(validated, Timestamp, TimestampRef, TimestampParseError);
366
367#[cfg(feature = "arbitrary")]
368impl<'a> arbitrary::Arbitrary<'a> for Timestamp {
369    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
370        let year = u.int_in_range(0..=9999)?;
371        let month = u.int_in_range(1..=12)?;
372        const M_D: [u8; 12] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
373        let day = u.int_in_range(1..=M_D[month as usize - 1])?;
374        let hour = u.int_in_range(0..=23)?;
375        let minute = u.int_in_range(0..=59)?;
376        let second = u.int_in_range(0..=59)?;
377        let millis = if bool::arbitrary(u)? {
378            let millis = u.int_in_range(0..=999)?;
379            format!(".{millis:03}")
380        } else {
381            "".to_owned()
382        };
383        format!("{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}{millis}Z")
384            .parse()
385            .map_err(|_| arbitrary::Error::IncorrectFormat)
386    }
387}
388
389impl TimestampRef {
390    fn validate(s: &str) -> Result<(), TimestampParseError> {
391        #[cfg(feature = "time")]
392        {
393            let _ = time::OffsetDateTime::parse(s, &time::format_description::well_known::Rfc3339)?;
394            Ok(())
395        }
396        #[cfg(not(feature = "time"))]
397        {
398            // This validator is lacking some features for now
399            if !s.chars().all(|c| {
400                c.is_numeric()
401                    || c == 'T'
402                    || c == 'Z'
403                    || c == '+'
404                    || c == '.'
405                    || c == '-'
406                    || c == ':'
407            }) {
408                return Err(TimestampParseError::invalid());
409            }
410            // PSA: Don't do time stuff... it sucks
411            if let Some(i) = s.find('T') {
412                // if no `T`, then it's not a valid timestamp
413                if i < 1 {
414                    return Err(TimestampParseError::invalid());
415                };
416                let (full_date, full_time) = s.split_at(i);
417                if full_date.len() != "1900-00-00".len() {
418                    return Err(TimestampParseError::invalid_s(full_date));
419                }
420                if !full_date.chars().all(|c| c.is_numeric() || c == '-') {
421                    return Err(TimestampParseError::invalid_s(full_date));
422                }
423                let partial_time = if let Some(stripped) = full_time.strip_suffix('Z') {
424                    stripped
425                } else {
426                    return Err(TimestampParseError::Other("unsupported non-UTC timestamp, enable the `time` feature in `twitch_types` to enable parsing these"));
427                };
428                if 2 != partial_time
429                    .chars()
430                    .into_iter()
431                    .filter(|&b| b == ':')
432                    .count()
433                {
434                    return Err(TimestampParseError::invalid_s(partial_time));
435                };
436                if !partial_time.contains('.') && partial_time.len() != "T00:00:00".len() {
437                    return Err(TimestampParseError::invalid_s(partial_time));
438                } else if partial_time.contains('.') {
439                    let mut i = partial_time.split('.');
440                    // if len not correct or next is none
441                    if !i
442                        .next()
443                        .map(|s| s.len() == "T00:00:00".len())
444                        .unwrap_or_default()
445                    {
446                        return Err(TimestampParseError::invalid_s(partial_time));
447                    }
448                }
449            } else {
450                return Err(TimestampParseError::invalid());
451            }
452            Ok(())
453        }
454    }
455}
456
457impl From<std::convert::Infallible> for TimestampParseError {
458    fn from(value: std::convert::Infallible) -> Self { match value {} }
459}
460
461/// Errors that can occur when parsing a timestamp.
462#[derive(Debug)]
463#[non_exhaustive]
464pub enum TimestampParseError {
465    /// Could not parse the timestamp using `time`
466    #[cfg(feature = "time")]
467    TimeError(time::error::Parse),
468    /// Could not format the timestamp using `time`
469    #[cfg(feature = "time")]
470    TimeFormatError(time::error::Format),
471    /// Other error
472    Other(&'static str),
473    /// Timestamp has an invalid format.
474    InvalidFormat {
475        /// location of error
476        location: &'static std::panic::Location<'static>,
477        /// Thing that failed
478        s: Option<String>,
479    },
480}
481
482impl core::fmt::Display for TimestampParseError {
483    fn fmt(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result {
484        #[allow(unused_variables)]
485        match self {
486            #[cfg(feature = "time")]
487            TimestampParseError::TimeError(parse_error) => {
488                write!(formatter, "Could not parse the timestamp using `time`")
489            }
490            #[cfg(feature = "time")]
491            TimestampParseError::TimeFormatError(_) => {
492                write!(formatter, "Could not format the timestamp using `time`")
493            }
494            TimestampParseError::Other(other) => {
495                write!(formatter, "{other}")
496            }
497            TimestampParseError::InvalidFormat { location, s } => {
498                write!(
499                    formatter,
500                    "timestamp has an invalid format. {s:?} - {location}"
501                )
502            }
503        }
504    }
505}
506
507impl std::error::Error for TimestampParseError {
508    fn source(&self) -> std::option::Option<&(dyn std::error::Error + 'static)> {
509        match self {
510            #[cfg(feature = "time")]
511            TimestampParseError::TimeError { 0: source, .. } => {
512                std::option::Option::Some(source as _)
513            }
514            #[cfg(feature = "time")]
515            TimestampParseError::TimeFormatError { 0: source, .. } => {
516                std::option::Option::Some(source as _)
517            }
518            TimestampParseError::Other { .. } => std::option::Option::None,
519            TimestampParseError::InvalidFormat { .. } => std::option::Option::None,
520        }
521    }
522}
523#[cfg(feature = "time")]
524impl std::convert::From<time::error::Parse> for TimestampParseError {
525    fn from(source: time::error::Parse) -> Self { TimestampParseError::TimeError(source) }
526}
527#[cfg(feature = "time")]
528impl std::convert::From<time::error::Format> for TimestampParseError {
529    fn from(source: time::error::Format) -> Self { TimestampParseError::TimeFormatError(source) }
530}
531
532impl TimestampParseError {
533    #[cfg(not(feature = "time"))]
534    #[track_caller]
535    fn invalid() -> Self {
536        Self::InvalidFormat {
537            location: std::panic::Location::caller(),
538            s: None,
539        }
540    }
541
542    #[cfg(not(feature = "time"))]
543    #[track_caller]
544    fn invalid_s(s: &str) -> Self {
545        Self::InvalidFormat {
546            location: std::panic::Location::caller(),
547            s: Some(s.to_string()),
548        }
549    }
550}
551
552impl Timestamp {
553    /// Set the partial-time component of the timestamp.
554    ///
555    /// # Panics
556    ///
557    /// Internally, without the `time` feature, this uses `unsafe` to deal with the raw string bytes. To ensure safety, the method will panic on invalid input and source.
558    fn set_time(&mut self, hours: u8, minutes: u8, seconds: u8) {
559        #[cfg(feature = "time")]
560        {
561            let _ = std::mem::replace(
562                self,
563                self.to_fixed_offset()
564                    .replace_time(
565                        time::Time::from_hms(hours, minutes, seconds)
566                            .expect("could not create time"),
567                    )
568                    .try_into()
569                    .expect("could not make timestamp"),
570            );
571        }
572        #[cfg(not(feature = "time"))]
573        {
574            const ERROR_MSG: &str = "malformed timestamp";
575            assert!(hours < 24);
576            assert!(minutes < 60);
577            assert!(seconds < 60);
578
579            #[inline]
580            fn replace_len2(s: &mut str, replace: &str) {
581                assert!(replace.as_bytes().len() == 2);
582                assert!(s.as_bytes().len() == 2);
583
584                let replace = replace.as_bytes();
585                // Safety:
586                // There are two things to make sure the replacement is valid.
587                // 1. The length of the two slices are equal to two.
588                // 2. `replace` slice does not contain any invalid characters.
589                //    As a property of being a `&str` of len 2, start and end of the str slice are valid boundaries, start is index 0, end is index 1 == `replace.len()` => 2 iff 1.)
590                let b = unsafe { s.as_bytes_mut() };
591                b[0] = replace[0];
592                b[1] = replace[1];
593            }
594            let t = self.0.find('T').expect(ERROR_MSG);
595            let partial_time: &mut str = &mut self.0[t + 1..];
596            // find the hours, minutes and seconds
597            let mut matches = partial_time.match_indices(':');
598            let (h, m, s) = (
599                0,
600                matches.next().expect(ERROR_MSG).0 + 1,
601                matches.next().expect(ERROR_MSG).0 + 1,
602            );
603            assert!(matches.next().is_none());
604            // RFC3339 requires partial-time components to be 2DIGIT
605            partial_time
606                .get_mut(h..h + 2)
607                .map(|s| replace_len2(s, &format!("{:02}", hours)))
608                .expect(ERROR_MSG);
609            partial_time
610                .get_mut(m..m + 2)
611                .map(|s| replace_len2(s, &format!("{:02}", minutes)))
612                .expect(ERROR_MSG);
613            partial_time
614                .get_mut(s..s + 2)
615                .map(|s| replace_len2(s, &format!("{:02}", seconds)))
616                .expect(ERROR_MSG);
617        }
618    }
619}
620
621#[cfg(feature = "time")]
622#[cfg_attr(nightly, doc(cfg(feature = "time")))]
623impl Timestamp {
624    /// Create a timestamp corresponding to current time
625    pub fn now() -> Timestamp {
626        time::OffsetDateTime::now_utc()
627            .try_into()
628            .expect("could not make timestamp")
629    }
630
631    /// Create a timestamp corresponding to the start of the current day. Timezone will always be UTC.
632    pub fn today() -> Timestamp {
633        time::OffsetDateTime::now_utc()
634            .replace_time(time::Time::MIDNIGHT)
635            .try_into()
636            .expect("could not make timestamp")
637    }
638}
639
640impl TimestampRef {
641    /// Normalize the timestamp into UTC time.
642    ///
643    /// # Examples
644    ///
645    /// ```rust
646    /// use twitch_types::Timestamp;
647    ///
648    /// let time = Timestamp::try_from("2021-07-01T13:37:00Z")?;
649    /// assert_eq!(time.normalize()?.as_ref(), &time);
650    /// let time2 = Timestamp::try_from("2021-07-01T13:37:00-01:00")?;
651    /// assert_ne!(time2.normalize()?.as_ref(), &time2);
652    /// # Ok::<(), std::boxed::Box<dyn std::error::Error + 'static>>(())
653    /// ```
654    #[allow(unreachable_code)]
655    pub fn normalize(&'_ self) -> Result<std::borrow::Cow<'_, TimestampRef>, TimestampParseError> {
656        let s = self.as_str();
657        if s.ends_with('Z') {
658            Ok(self.into())
659        } else {
660            #[cfg(feature = "time")]
661            {
662                let utc = self.to_utc();
663                return Ok(std::borrow::Cow::Owned(utc.try_into()?));
664            }
665            panic!("non `Z` timestamps are not possible to use without the `time` feature enabled for `twitch_types`")
666        }
667    }
668
669    /// Compare another time and return `self < other`.
670    ///
671    /// # Examples
672    ///
673    /// ```rust
674    /// use twitch_types::Timestamp;
675    ///
676    /// let time2021 = Timestamp::try_from("2021-07-01T13:37:00Z").unwrap();
677    /// let time2020 = Timestamp::try_from("2020-07-01T13:37:00Z").unwrap();
678    /// assert!(time2020.is_before(&time2021));
679    /// ```
680    pub fn is_before<T: ?Sized>(&self, other: &T) -> bool
681    where Self: PartialOrd<T> {
682        self < other
683    }
684
685    /// Make a timestamp with the time component set to 00:00:00.
686    ///
687    /// # Examples
688    ///
689    /// ```rust
690    /// use twitch_types::Timestamp;
691    ///
692    /// let time = Timestamp::try_from("2021-07-01T13:37:00Z").unwrap();
693    /// assert_eq!(time.to_day().as_str(), "2021-07-01T00:00:00Z");
694    /// ```
695    pub fn to_day(&self) -> Timestamp {
696        let mut c = self.to_owned();
697        c.set_time(0, 0, 0);
698        c
699    }
700
701    /// Get the year
702    ///
703    /// # Examples
704    ///
705    /// ```rust
706    /// use twitch_types::Timestamp;
707    ///
708    /// let time = Timestamp::try_from("2021-07-01T13:37:00Z").unwrap();
709    /// assert_eq!(time.year(), "2021");
710    /// ```
711    pub fn year(&self) -> &str { &self.0[0..4] }
712
713    /// Get the month
714    ///
715    /// # Examples
716    ///
717    /// ```rust
718    /// use twitch_types::Timestamp;
719    ///
720    /// let time = Timestamp::try_from("2021-07-01T13:37:00Z").unwrap();
721    /// assert_eq!(time.month(), "07");
722    /// ```
723    pub fn month(&self) -> &str { &self.0[5..7] }
724
725    /// Get the day
726    ///
727    /// # Examples
728    ///
729    /// ```rust
730    /// use twitch_types::Timestamp;
731    ///
732    /// let time = Timestamp::try_from("2021-07-01T13:37:00Z").unwrap();
733    /// assert_eq!(time.day(), "01");
734    /// ```
735    pub fn day(&self) -> &str { &self.0[8..10] }
736
737    /// Get the hour
738    ///
739    /// # Examples
740    ///
741    /// ```rust
742    /// use twitch_types::Timestamp;
743    ///
744    /// let time = Timestamp::try_from("2021-07-01T13:37:00Z").unwrap();
745    /// assert_eq!(time.hour(), "13");
746    /// ```
747    pub fn hour(&self) -> &str { &self.0[11..13] }
748
749    /// Get the minute
750    ///
751    /// # Examples
752    ///
753    /// ```rust
754    /// use twitch_types::Timestamp;
755    ///
756    /// let time = Timestamp::try_from("2021-07-01T13:37:00Z").unwrap();
757    /// assert_eq!(time.minute(), "37");
758    /// ```
759    pub fn minute(&self) -> &str { &self.0[14..16] }
760
761    /// Get the second
762    ///
763    /// # Examples
764    ///
765    /// ```rust
766    /// use twitch_types::Timestamp;
767    ///
768    /// let time = Timestamp::try_from("2021-07-01T13:37:00Z").unwrap();
769    /// assert_eq!(time.second(), "00");
770    /// ```
771    pub fn second(&self) -> &str { &self.0[17..19] }
772
773    /// Get the millis
774    ///
775    /// # Examples
776    ///
777    /// ```rust
778    /// use twitch_types::Timestamp;
779    ///
780    /// let time = Timestamp::try_from("2021-07-01T13:37:00.123Z").unwrap();
781    /// assert_eq!(time.millis(), Some("123"));
782    /// let time = Timestamp::try_from("2021-07-01T13:37:00Z").unwrap();
783    /// assert_eq!(time.millis(), None);
784    /// ```
785    pub fn millis(&self) -> Option<&str> {
786        if self.0[19..].contains('.') {
787            let sub = &self.0[20..];
788            Some(&sub[..sub.find(|c: char| !c.is_ascii_digit()).unwrap_or(sub.len())])
789        } else {
790            None
791        }
792    }
793}
794
795#[cfg(feature = "time")]
796#[cfg_attr(nightly, doc(cfg(feature = "time")))]
797impl TimestampRef {
798    /// Construct into a [`OffsetDateTime`](time::OffsetDateTime) time with a guaranteed UTC offset.
799    ///
800    /// # Panics
801    ///
802    /// This method assumes the timestamp is a valid rfc3339 timestamp, and panics if not.
803    pub fn to_utc(&self) -> time::OffsetDateTime {
804        self.to_fixed_offset().to_offset(time::UtcOffset::UTC)
805    }
806
807    /// Construct into a [`OffsetDateTime`](time::OffsetDateTime) time.
808    ///
809    /// # Panics
810    ///
811    /// This method assumes the timestamp is a valid rfc3339 timestamp, and panics if not.
812    pub fn to_fixed_offset(&self) -> time::OffsetDateTime {
813        time::OffsetDateTime::parse(&self.0, &time::format_description::well_known::Rfc3339)
814            .expect("this should never fail")
815    }
816}
817
818impl PartialOrd for Timestamp {
819    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
820        // Defer to TimestampRef impl
821        let this: &TimestampRef = self.as_ref();
822        let other: &TimestampRef = other.as_ref();
823        this.partial_cmp(other)
824    }
825}
826
827impl PartialOrd<Timestamp> for TimestampRef {
828    fn partial_cmp(&self, other: &Timestamp) -> Option<std::cmp::Ordering> {
829        // Defer to TimestampRef impl
830        let other: &TimestampRef = other.as_ref();
831        self.partial_cmp(other)
832    }
833}
834
835impl PartialOrd for TimestampRef {
836    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
837        // to check ordering, we normalize offset, then do a lexicographic comparison if possible,
838        // We can do this because the timestamp should always be RFC3339 with time-offset = 'Z' with normalize.
839        // However, we need to make sure punctuation and length is correct. Without the `time` feature, it's impossible to get a non-UTC timestamp, so normalize will do nothing.
840        let this = self
841            .normalize()
842            .expect("normalization failed, this is a bug");
843        let other = other
844            .normalize()
845            .expect("normalization of other failed, this is a bug");
846        // If a punctuation exists in only one, we can't order.
847        #[allow(clippy::if_same_then_else)]
848        if this.as_ref().as_str().contains('.') ^ other.as_ref().as_str().contains('.') {
849            #[cfg(feature = "tracing")]
850            tracing::warn!("comparing two `Timestamps` with differing punctuation");
851            return None;
852        } else if this.0.len() != other.0.len() {
853            #[cfg(feature = "tracing")]
854            tracing::warn!("comparing two `Timestamps` with differing length");
855            return None;
856        }
857        this.as_str().partial_cmp(other.as_str())
858    }
859}
860
861#[cfg(feature = "time")]
862#[cfg_attr(nightly, doc(cfg(feature = "time")))]
863impl PartialEq<time::OffsetDateTime> for Timestamp {
864    fn eq(&self, other: &time::OffsetDateTime) -> bool {
865        // Defer to TimestampRef impl
866        let this: &TimestampRef = self.as_ref();
867        this.eq(other)
868    }
869}
870
871#[cfg(feature = "time")]
872#[cfg_attr(nightly, doc(cfg(feature = "time")))]
873impl PartialOrd<time::OffsetDateTime> for Timestamp {
874    fn partial_cmp(&self, other: &time::OffsetDateTime) -> Option<std::cmp::Ordering> {
875        // Defer to TimestampRef impl
876        let this: &TimestampRef = self.as_ref();
877        this.partial_cmp(other)
878    }
879}
880
881#[cfg(feature = "time")]
882#[cfg_attr(nightly, doc(cfg(feature = "time")))]
883impl PartialEq<time::OffsetDateTime> for TimestampRef {
884    fn eq(&self, other: &time::OffsetDateTime) -> bool { &self.to_utc() == other }
885}
886
887#[cfg(feature = "time")]
888#[cfg_attr(nightly, doc(cfg(feature = "time")))]
889impl PartialOrd<time::OffsetDateTime> for TimestampRef {
890    fn partial_cmp(&self, other: &time::OffsetDateTime) -> Option<std::cmp::Ordering> {
891        self.to_utc().partial_cmp(other)
892    }
893}
894
895#[cfg(feature = "time")]
896#[cfg_attr(nightly, doc(cfg(feature = "time")))]
897impl std::convert::TryFrom<time::OffsetDateTime> for Timestamp {
898    type Error = time::error::Format;
899
900    fn try_from(value: time::OffsetDateTime) -> Result<Self, Self::Error> {
901        Ok(Timestamp(
902            value.format(&time::format_description::well_known::Rfc3339)?,
903        ))
904    }
905}
906
907#[cfg(test)]
908mod tests {
909    use super::*;
910
911    #[test]
912    pub fn time_test() {
913        let mut time1 = Timestamp::try_from("2021-11-11T10:00:00Z").unwrap();
914        time1.set_time(10, 0, 32);
915        let time2 = Timestamp::try_from("2021-11-10T10:00:00Z").unwrap();
916        assert!(time2.is_before(&time1));
917        dbg!(time1.normalize().unwrap());
918        #[cfg(feature = "time")]
919        let time = Timestamp::try_from("2021-11-11T13:37:00-01:00").unwrap();
920        #[cfg(feature = "time")]
921        dbg!(time.normalize().unwrap());
922    }
923}