1use core::str::FromStr;
4
5use derive_more::{Debug, Display};
6#[cfg(feature = "zerocopy-08")]
7use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout};
8
9use crate::spelling::{Count, read_count};
10use crate::{ParseTickdeltaError, TickRate, Timedelta};
11
12#[repr(transparent)]
31#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
32#[display("{}", Count(*_0, " ticks"))]
33#[debug("{self}")]
34#[cfg_attr(feature = "zerocopy-08", derive(FromBytes, IntoBytes, Immutable, KnownLayout))]
35pub struct Tickstamp(pub(crate) i64);
36
37impl Tickstamp {
38 #[inline]
40 #[must_use]
41 pub const fn from_ticks(ticks: i64) -> Self {
42 Self(ticks)
43 }
44
45 #[inline]
47 #[must_use]
48 pub const fn as_ticks(self) -> i64 {
49 self.0
50 }
51}
52
53#[repr(transparent)]
68#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
69#[display("{}", Count(*_0, " ticks"))]
70#[debug("{self}")]
71#[cfg_attr(feature = "zerocopy-08", derive(FromBytes, IntoBytes, Immutable, KnownLayout))]
72pub struct Tickdelta(pub(crate) i64);
73
74impl Tickdelta {
75 #[inline]
77 #[must_use]
78 pub const fn from_ticks(ticks: i64) -> Self {
79 Self(ticks)
80 }
81
82 #[inline]
84 #[must_use]
85 pub const fn as_ticks(self) -> i64 {
86 self.0
87 }
88
89 #[inline]
91 #[must_use]
92 pub const fn to_timedelta(self, rate: TickRate) -> Timedelta {
93 Timedelta(rate.ticks_to_nanos(self.0))
94 }
95}
96
97macro_rules! read_as_ticks {
99 ($count:ident) => {
100 impl FromStr for $count {
102 type Err = ParseTickdeltaError;
103
104 fn from_str(text: &str) -> Result<Self, Self::Err> {
105 let count = text.strip_suffix(" ticks").and_then(read_count);
106 count.map(Self).ok_or(ParseTickdeltaError)
107 }
108 }
109 };
110}
111
112read_as_ticks!(Tickstamp);
113read_as_ticks!(Tickdelta);
114
115#[cfg(test)]
116mod tests {
117 use alloc::format;
118 use alloc::string::ToString as _;
119
120 use rstest::rstest;
121
122 use crate::{ParseTickdeltaError, Tickdelta, Tickstamp};
123
124 #[test]
125 fn a_count_of_ticks_reads_back_from_its_spelling() {
126 let span = Tickdelta::from_ticks(-24);
127 assert_eq!(span.to_string(), "-24 ticks", "written as its count");
128 assert_eq!("-24 ticks".parse(), Ok(span), "and read back");
129 assert_eq!(
130 "1000 ticks".parse(),
131 Ok(Tickstamp::from_ticks(1_000)),
132 "a reading the same way"
133 );
134 }
135
136 #[test]
137 fn a_width_pads_the_count_with_its_unit() {
138 assert_eq!(format!("[{:>10}]", Tickdelta::from_ticks(24)), "[ 24 ticks]", "a span");
139 assert_eq!(format!("[{:<10}]", Tickstamp::from_ticks(24)), "[24 ticks ]", "and a reading");
140 }
141
142 #[rstest]
143 #[case::no_unit("24")]
144 #[case::a_sign_display_never_writes("+24 ticks")]
145 #[case::one_tick("1 tick")]
146 #[case::no_space("24ticks")]
147 #[case::past_the_range("9223372036854775808 ticks")]
148 fn a_malformed_count_of_ticks_is_refused(#[case] text: &str) {
149 assert_eq!(text.parse::<Tickdelta>(), Err(ParseTickdeltaError), "{text:?} is refused");
150 }
151}