Skip to main content

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