Skip to main content

tuwunel_core/matrix/pdu/
count.rs

1#![expect(
2	clippy::cast_possible_wrap,
3	clippy::cast_sign_loss,
4	clippy::as_conversions
5)]
6
7use std::{cmp::Ordering, fmt, fmt::Display, str::FromStr};
8
9use ruma::api::Direction;
10
11use crate::{Error, Result, err};
12
13/// Sequence number locating a PDU in a room timeline.
14///
15/// Valid normal counts range from zero through `i64::MAX`, while valid
16/// backfilled counts range from `i64::MIN` through zero. Ordering compares
17/// their signed representations, with zero shared by both variants.
18#[derive(Hash, PartialEq, Eq, Clone, Copy, Debug)]
19pub enum Count {
20	/// Sequence assigned to an event in the normal timeline.
21	///
22	/// Normal counts advance forward as new local or federated events are
23	/// appended. Valid values do not exceed `i64::MAX`.
24	Normal(u64),
25
26	/// Sequence assigned to an event in the backfilled timeline.
27	///
28	/// Valid backfilled counts occupy the nonpositive signed ordering range.
29	Backfilled(i64),
30}
31
32impl Count {
33	/// Encodes the count's integer bits in big-endian order.
34	///
35	/// Normal values retain their unsigned representation. Backfilled values
36	/// are reinterpreted as unsigned two's-complement bits before encoding.
37	///
38	/// # Panics
39	///
40	/// Panics in debug builds if a positive `Backfilled` value was constructed
41	/// directly.
42	#[inline]
43	#[must_use]
44	pub fn to_be_bytes(self) -> [u8; size_of::<u64>()] { self.into_unsigned().to_be_bytes() }
45
46	/// Interprets unsigned integer bits as a signed timeline count.
47	///
48	/// Values with the high bit set become negative backfilled counts. Other
49	/// positive values become normal counts, while zero becomes backfilled
50	/// zero.
51	#[inline]
52	#[must_use]
53	pub fn from_unsigned(unsigned: u64) -> Self { Self::from_signed(unsigned as i64) }
54
55	/// Classifies a signed integer as a normal or backfilled count.
56	///
57	/// Positive values become normal counts. Zero and negative values become
58	/// backfilled counts.
59	#[inline]
60	#[must_use]
61	pub fn from_signed(signed: i64) -> Self {
62		match signed {
63			| i64::MIN..=0 => Self::Backfilled(signed),
64			| _ => Self::Normal(signed as u64),
65		}
66	}
67
68	/// Converts the count to its unsigned integer bit representation.
69	///
70	/// Backfilled values are reinterpreted using two's-complement bits. The
71	/// variant information is not retained in the returned integer.
72	///
73	/// # Panics
74	///
75	/// Panics in debug builds if a positive `Backfilled` value was constructed
76	/// directly.
77	#[inline]
78	#[must_use]
79	pub fn into_unsigned(self) -> u64 {
80		self.debug_assert_valid();
81		match self {
82			| Self::Normal(i) => i,
83			| Self::Backfilled(i) => i as u64,
84		}
85	}
86
87	/// Converts the count to the signed representation used for ordering.
88	///
89	/// Valid normal values cast into the nonnegative signed domain, while valid
90	/// backfilled values retain their signed count. Direct construction outside
91	/// the documented ranges violates the ordering invariant.
92	///
93	/// # Panics
94	///
95	/// Panics in debug builds if a positive `Backfilled` value was constructed
96	/// directly.
97	#[inline]
98	#[must_use]
99	pub fn into_signed(self) -> i64 {
100		self.debug_assert_valid();
101		match self {
102			| Self::Normal(i) => i as i64,
103			| Self::Backfilled(i) => i,
104		}
105	}
106
107	/// Converts a count to a normal-timeline position.
108	///
109	/// Existing normal counts are preserved. Any backfilled count maps to the
110	/// beginning of the normal timeline at zero.
111	///
112	/// # Panics
113	///
114	/// Panics in debug builds if a positive `Backfilled` value was constructed
115	/// directly.
116	#[inline]
117	#[must_use]
118	pub fn into_normal(self) -> Self {
119		self.debug_assert_valid();
120		match self {
121			| Self::Normal(i) => Self::Normal(i),
122			| Self::Backfilled(_) => Self::Normal(0),
123		}
124	}
125
126	/// Advances or retreats the count by one without integer overflow.
127	///
128	/// Forward movement adds one and backward movement subtracts one. The
129	/// original variant is preserved, so callers must avoid crossing that
130	/// variant's valid timeline range.
131	#[inline]
132	pub fn checked_inc(self, dir: Direction) -> Result<Self, Error> {
133		match dir {
134			| Direction::Forward => self.checked_add(1),
135			| Direction::Backward => self.checked_sub(1),
136		}
137	}
138
139	/// Adds an unsigned offset without integer overflow.
140	///
141	/// Backfilled offsets are cast to `i64` and must not exceed `i64::MAX`.
142	/// Callers must keep the resulting variant within its documented range;
143	/// integer overflow returns an arithmetic error.
144	#[inline]
145	pub fn checked_add(self, add: u64) -> Result<Self, Error> {
146		Ok(match self {
147			| Self::Normal(i) => Self::Normal(
148				i.checked_add(add)
149					.ok_or_else(|| err!(Arithmetic("Count::Normal overflow")))?,
150			),
151			| Self::Backfilled(i) => Self::Backfilled(
152				i.checked_add(add as i64)
153					.ok_or_else(|| err!(Arithmetic("Count::Backfilled overflow")))?,
154			),
155		})
156	}
157
158	/// Subtracts an unsigned offset without integer underflow.
159	///
160	/// Backfilled offsets are cast to `i64` and must not exceed `i64::MAX`.
161	/// Callers must keep the resulting variant within its documented range;
162	/// integer underflow returns an arithmetic error.
163	#[inline]
164	pub fn checked_sub(self, sub: u64) -> Result<Self, Error> {
165		Ok(match self {
166			| Self::Normal(i) => Self::Normal(
167				i.checked_sub(sub)
168					.ok_or_else(|| err!(Arithmetic("Count::Normal underflow")))?,
169			),
170			| Self::Backfilled(i) => Self::Backfilled(
171				i.checked_sub(sub as i64)
172					.ok_or_else(|| err!(Arithmetic("Count::Backfilled underflow")))?,
173			),
174		})
175	}
176
177	/// Advances or retreats the count by one with saturation.
178	///
179	/// Forward movement adds one and backward movement subtracts one.
180	/// Saturation uses the integer boundary of the existing variant and does
181	/// not prevent a backfilled value from crossing zero.
182	#[inline]
183	#[must_use]
184	pub fn saturating_inc(self, dir: Direction) -> Self {
185		match dir {
186			| Direction::Forward => self.saturating_add(1),
187			| Direction::Backward => self.saturating_sub(1),
188		}
189	}
190
191	/// Adds an unsigned offset with saturation at the integer boundary.
192	///
193	/// Backfilled offsets are cast to `i64` and must not exceed `i64::MAX`.
194	/// Saturation uses the underlying integer boundary and does not repair a
195	/// result outside the variant's documented range.
196	#[inline]
197	#[must_use]
198	pub fn saturating_add(self, add: u64) -> Self {
199		match self {
200			| Self::Normal(i) => Self::Normal(i.saturating_add(add)),
201			| Self::Backfilled(i) => Self::Backfilled(i.saturating_add(add as i64)),
202		}
203	}
204
205	/// Subtracts an unsigned offset with saturation at the integer boundary.
206	///
207	/// Backfilled offsets are cast to `i64` and must not exceed `i64::MAX`.
208	/// Saturation uses the underlying integer boundary and does not repair a
209	/// result outside the variant's documented range.
210	#[inline]
211	#[must_use]
212	pub fn saturating_sub(self, sub: u64) -> Self {
213		match self {
214			| Self::Normal(i) => Self::Normal(i.saturating_sub(sub)),
215			| Self::Backfilled(i) => Self::Backfilled(i.saturating_sub(sub as i64)),
216		}
217	}
218
219	/// Returns the earliest valid timeline count.
220	///
221	/// The minimum is a backfilled count at `i64::MIN`. It sorts before every
222	/// other valid count.
223	#[inline]
224	#[must_use]
225	pub const fn min() -> Self { Self::Backfilled(i64::MIN) }
226
227	/// Returns the latest valid timeline count.
228	///
229	/// The maximum is a normal count at `i64::MAX`. This keeps the value within
230	/// the signed domain used by ordering.
231	#[inline]
232	#[must_use]
233	pub const fn max() -> Self { Self::Normal(i64::MAX as u64) }
234
235	#[inline]
236	pub(crate) fn debug_assert_valid(&self) {
237		if let Self::Backfilled(i) = self {
238			debug_assert!(*i <= 0, "Backfilled sequence must be negative");
239		}
240	}
241}
242
243impl Display for Count {
244	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
245		self.debug_assert_valid();
246		match self {
247			| Self::Normal(i) => write!(f, "{i}"),
248			| Self::Backfilled(i) => write!(f, "{i}"),
249		}
250	}
251}
252
253impl From<i64> for Count {
254	#[inline]
255	fn from(signed: i64) -> Self { Self::from_signed(signed) }
256}
257
258impl From<u64> for Count {
259	#[inline]
260	fn from(unsigned: u64) -> Self { Self::from_unsigned(unsigned) }
261}
262
263impl FromStr for Count {
264	type Err = Error;
265
266	fn from_str(token: &str) -> Result<Self, Self::Err> { Ok(Self::from_signed(token.parse()?)) }
267}
268
269impl PartialOrd for Count {
270	fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other)) }
271}
272
273impl Ord for Count {
274	fn cmp(&self, other: &Self) -> Ordering { self.into_signed().cmp(&other.into_signed()) }
275}
276
277impl Default for Count {
278	fn default() -> Self { Self::Normal(0) }
279}