const_num_traits/ops/convert.rs
1//! Integer-to-integer conversions: sign reinterpretation, absolute
2//! difference, value-preserving widening, lossy truncation, and the generic
3//! cast family, mirroring `cast_signed` / `cast_unsigned` / `unsigned_abs` /
4//! `abs_diff` / `widen` / `truncate` /
5//! `{checked,wrapping,saturating,strict}_cast` / `clamp_magnitude` on the
6//! primitive integer types.
7//!
8//! The checked/saturating/strict *sign-cast* and *truncate* variants found
9//! in std are NOT mirrored as methods here: they are exactly
10//! the generic cast traits ([`CheckedCast`], [`SaturatingCast`],
11//! [`StrictCast`]) applied to the counterpart type, so the sign-cast and
12//! truncate traits stay one-method (the Tier-A bit-pattern operations) and
13//! the generic casts are the single home for value-checked conversion.
14//!
15//! Stability in std (as of nightly 2026): `unsigned_abs` (1.51) and
16//! `abs_diff` (1.60) are delegated; `cast_signed`/`cast_unsigned` (1.87),
17//! `widen`/`truncate` (`integer_widen_truncate`), the generic `*_cast`
18//! family (`integer_casts`) and `clamp_magnitude` are newer than the
19//! crate's MSRV or still nightly-only, and are hand-rolled with the same
20//! semantics.
21//!
22//! **CT tiers**: [`CastSigned`]/[`CastUnsigned`], [`Truncate`], [`Widen`],
23//! [`WrappingCast`], [`AbsDiff`] and [`UnsignedAbs`] are Tier A
24//! (bit-pattern / borrow-free operations); [`CheckedCast`],
25//! [`SaturatingCast`] and [`ClampMagnitude`] are Tier B; [`StrictCast`] is
26//! Tier C (data-dependent panic).
27
28c0nst::c0nst! {
29/// Computes the absolute difference of two values.
30pub c0nst trait AbsDiff: Sized {
31 /// The result type: `Self` for unsigned types, the unsigned counterpart
32 /// for signed types (so the difference always fits).
33 type Output;
34
35 /// Computes the absolute difference between `self` and `other` without
36 /// the possibility of overflow.
37 ///
38 /// ```
39 /// use const_num_traits::AbsDiff;
40 ///
41 /// assert_eq!(AbsDiff::abs_diff(100u8, 120), 20);
42 /// assert_eq!(AbsDiff::abs_diff(i8::MIN, i8::MAX), 255u8);
43 /// ```
44 fn abs_diff(self, other: Self) -> Self::Output;
45}
46}
47
48macro_rules! abs_diff_impl {
49 ($($t:ty => $out:ty;)*) => {$(
50 c0nst::c0nst! {
51 c0nst impl AbsDiff for $t {
52 type Output = $out;
53
54 #[inline]
55 fn abs_diff(self, other: Self) -> $out {
56 <$t>::abs_diff(self, other)
57 }
58 }
59 }
60 )*};
61}
62
63abs_diff_impl! {
64 u8 => u8; u16 => u16; u32 => u32; u64 => u64; usize => usize; u128 => u128;
65 i8 => u8; i16 => u16; i32 => u32; i64 => u64; isize => usize; i128 => u128;
66}
67
68c0nst::c0nst! {
69/// Computes the absolute value as the unsigned counterpart type, which
70/// cannot overflow (unlike `abs`, which overflows on `MIN`).
71pub c0nst trait UnsignedAbs: Sized {
72 /// The unsigned counterpart type (e.g. `u32` for `i32`).
73 type Unsigned;
74
75 /// Computes the absolute value of `self` without any wrapping or
76 /// panicking.
77 ///
78 /// ```
79 /// use const_num_traits::UnsignedAbs;
80 ///
81 /// assert_eq!(UnsignedAbs::unsigned_abs(-100i8), 100u8);
82 /// assert_eq!(UnsignedAbs::unsigned_abs(i8::MIN), 128u8);
83 /// ```
84 fn unsigned_abs(self) -> Self::Unsigned;
85}
86}
87
88c0nst::c0nst! {
89/// Clamps the magnitude (absolute value) of a signed integer.
90pub c0nst trait ClampMagnitude: Sized {
91 /// The unsigned counterpart type used for the limit.
92 type Unsigned;
93 /// The (owned) result type.
94 type Output;
95
96 /// Clamps `self` to the symmetric range `[-limit, limit]`. Limits that
97 /// don't fit in `Self` leave the value unchanged.
98 ///
99 /// ```
100 /// use const_num_traits::ClampMagnitude;
101 ///
102 /// assert_eq!(ClampMagnitude::clamp_magnitude(120i8, 100), 100);
103 /// assert_eq!(ClampMagnitude::clamp_magnitude(-120i8, 100), -100);
104 /// assert_eq!(ClampMagnitude::clamp_magnitude(-120i8, 200), -120);
105 /// ```
106 fn clamp_magnitude(self, limit: Self::Unsigned) -> Self::Output;
107}
108}
109
110macro_rules! unsigned_abs_impl {
111 ($($t:ty => $u:ty;)*) => {$(
112 c0nst::c0nst! {
113 c0nst impl UnsignedAbs for $t {
114 type Unsigned = $u;
115
116 #[inline]
117 fn unsigned_abs(self) -> $u {
118 <$t>::unsigned_abs(self)
119 }
120 }
121 }
122
123 c0nst::c0nst! {
124 c0nst impl ClampMagnitude for $t {
125 type Unsigned = $u;
126 type Output = $t;
127
128 #[inline]
129 fn clamp_magnitude(self, limit: $u) -> $t {
130 if limit <= <$t>::MAX as $u {
131 let lim = limit as $t;
132 if self < -lim {
133 -lim
134 } else if self > lim {
135 lim
136 } else {
137 self
138 }
139 } else {
140 self
141 }
142 }
143 }
144 }
145 )*};
146}
147
148unsigned_abs_impl! {
149 i8 => u8; i16 => u16; i32 => u32; i64 => u64; isize => usize; i128 => u128;
150}
151
152c0nst::c0nst! {
153/// Reinterprets an unsigned integer's bits as its signed counterpart.
154///
155/// For value-checked conversion to the signed counterpart, use
156/// [`CheckedCast`] / [`SaturatingCast`] / [`StrictCast`] with the signed
157/// target type.
158pub c0nst trait CastSigned: Sized {
159 /// The signed counterpart type (e.g. `i32` for `u32`).
160 type Signed;
161
162 /// Returns the bit pattern of `self` reinterpreted as its signed
163 /// counterpart (two's complement: values above `Signed::MAX` become
164 /// negative).
165 ///
166 /// ```
167 /// use const_num_traits::CastSigned;
168 ///
169 /// assert_eq!(CastSigned::cast_signed(255u8), -1i8);
170 /// assert_eq!(CastSigned::cast_signed(127u8), 127i8);
171 /// ```
172 fn cast_signed(self) -> Self::Signed;
173}
174}
175
176macro_rules! cast_signed_impl {
177 ($($t:ty => $s:ty;)*) => {$(
178 c0nst::c0nst! {
179 c0nst impl CastSigned for $t {
180 type Signed = $s;
181
182 #[inline]
183 fn cast_signed(self) -> $s {
184 self as $s
185 }
186 }
187 }
188 )*};
189}
190
191cast_signed_impl! {
192 u8 => i8; u16 => i16; u32 => i32; u64 => i64; usize => isize; u128 => i128;
193}
194
195c0nst::c0nst! {
196/// Reinterprets a signed integer's bits as its unsigned counterpart.
197///
198/// For value-checked conversion to the unsigned counterpart, use
199/// [`CheckedCast`] / [`SaturatingCast`] / [`StrictCast`] with the unsigned
200/// target type.
201pub c0nst trait CastUnsigned: Sized {
202 /// The unsigned counterpart type (e.g. `u32` for `i32`).
203 type Unsigned;
204
205 /// Returns the bit pattern of `self` reinterpreted as its unsigned
206 /// counterpart (two's complement: negative values become large).
207 ///
208 /// ```
209 /// use const_num_traits::CastUnsigned;
210 ///
211 /// assert_eq!(CastUnsigned::cast_unsigned(-1i8), 255u8);
212 /// assert_eq!(CastUnsigned::cast_unsigned(127i8), 127u8);
213 /// ```
214 fn cast_unsigned(self) -> Self::Unsigned;
215}
216}
217
218macro_rules! cast_unsigned_impl {
219 ($($t:ty => $u:ty;)*) => {$(
220 c0nst::c0nst! {
221 c0nst impl CastUnsigned for $t {
222 type Unsigned = $u;
223
224 #[inline]
225 fn cast_unsigned(self) -> $u {
226 self as $u
227 }
228 }
229 }
230 )*};
231}
232
233cast_unsigned_impl! {
234 i8 => u8; i16 => u16; i32 => u32; i64 => u64; isize => usize; i128 => u128;
235}
236
237c0nst::c0nst! {
238/// Widens to an integer of the same signedness and the same size or larger,
239/// always preserving the value.
240pub c0nst trait Widen<T>: Sized {
241 /// Widens `self` to the target type, preserving its value. Implemented
242 /// for the same pairs as std's `widen` (including the identity
243 /// conversion; `usize`/`isize` only participate where the conversion is
244 /// valid on every platform).
245 ///
246 /// ```
247 /// use const_num_traits::Widen;
248 ///
249 /// let x: u32 = Widen::widen(200u8);
250 /// assert_eq!(x, 200);
251 /// let y: i128 = Widen::widen(-5i64);
252 /// assert_eq!(y, -5);
253 /// ```
254 fn widen(self) -> T;
255}
256}
257
258macro_rules! widen_impl {
259 ($($from:ty => $($to:ty),+;)*) => {$($(
260 c0nst::c0nst! {
261 c0nst impl Widen<$to> for $from {
262 #[inline]
263 fn widen(self) -> $to {
264 self as $to
265 }
266 }
267 }
268 )+)*};
269}
270
271// same pairs as core's `impl_widen!`
272widen_impl! {
273 u8 => u8, u16, u32, u64, u128, usize;
274 u16 => u16, u32, u64, u128, usize;
275 u32 => u32, u64, u128;
276 u64 => u64, u128;
277 u128 => u128;
278 usize => usize;
279
280 i8 => i8, i16, i32, i64, i128, isize;
281 i16 => i16, i32, i64, i128, isize;
282 i32 => i32, i64, i128;
283 i64 => i64, i128;
284 i128 => i128;
285 isize => isize;
286}
287
288c0nst::c0nst! {
289/// Truncates to an integer of the same signedness and the same size or
290/// smaller, discarding high-order bits.
291///
292/// For bounds-checked or saturating narrowing, use [`CheckedCast`] /
293/// [`SaturatingCast`] / [`StrictCast`] — on same-signedness pairs they have
294/// exactly the semantics of std's `checked_truncate` /
295/// `saturating_truncate`.
296pub c0nst trait Truncate<T>: Sized {
297 /// Truncates `self` to the target type, discarding high-order bits.
298 /// Implemented for the same pairs as std's `truncate` (including the
299 /// identity conversion).
300 ///
301 /// ```
302 /// use const_num_traits::Truncate;
303 ///
304 /// let x: u8 = Truncate::truncate(0x1234u16);
305 /// assert_eq!(x, 0x34);
306 /// ```
307 fn truncate(self) -> T;
308}
309}
310
311macro_rules! truncate_impl {
312 ($($from:ty => $($to:ty),+;)*) => {$($(
313 c0nst::c0nst! {
314 c0nst impl Truncate<$to> for $from {
315 #[inline]
316 fn truncate(self) -> $to {
317 self as $to
318 }
319 }
320 }
321 )+)*};
322}
323
324// same pairs as core's `impl_truncate!`
325truncate_impl! {
326 u8 => u8;
327 u16 => u16, u8;
328 u32 => u32, u16, u8;
329 u64 => u64, u32, u16, u8;
330 u128 => u128, u64, u32, u16, u8;
331 usize => usize, u16, u8;
332
333 i8 => i8;
334 i16 => i16, i8;
335 i32 => i32, i16, i8;
336 i64 => i64, i32, i16, i8;
337 i128 => i128, i64, i32, i16, i8;
338 isize => isize, i16, i8;
339}
340
341c0nst::c0nst! {
342/// Fallible value-preserving conversion between any two integer types
343/// (the trait counterpart of std's generic `checked_cast`).
344pub c0nst trait CheckedCast<T>: Sized {
345 /// Converts `self` to the target type, returning `None` if the value
346 /// doesn't fit.
347 ///
348 /// ```
349 /// use const_num_traits::CheckedCast;
350 ///
351 /// let ok: Option<i8> = CheckedCast::checked_cast(100u16);
352 /// let no: Option<i8> = CheckedCast::checked_cast(300u16);
353 /// assert_eq!(ok, Some(100));
354 /// assert_eq!(no, None);
355 /// ```
356 fn checked_cast(self) -> Option<T>;
357}
358}
359
360c0nst::c0nst! {
361/// Value-preserving conversion between any two integer types that panics on
362/// overflow, even in release builds (std's generic `strict_cast`).
363pub c0nst trait StrictCast<T>: Sized {
364 /// Converts `self` to the target type, panicking if the value doesn't
365 /// fit.
366 fn strict_cast(self) -> T;
367}
368}
369
370c0nst::c0nst! {
371/// Wrapping conversion between any two integer types
372/// (std's generic `wrapping_cast` — the semantics of `as`).
373pub c0nst trait WrappingCast<T>: Sized {
374 /// Converts `self` to the target type, wrapping around at the boundary
375 /// of the type.
376 ///
377 /// ```
378 /// use const_num_traits::WrappingCast;
379 ///
380 /// let x: u8 = WrappingCast::wrapping_cast(300u16);
381 /// assert_eq!(x, 44);
382 /// ```
383 fn wrapping_cast(self) -> T;
384}
385}
386
387c0nst::c0nst! {
388/// Saturating conversion between any two integer types
389/// (std's generic `saturating_cast`).
390pub c0nst trait SaturatingCast<T>: Sized {
391 /// Converts `self` to the target type, saturating at the target's
392 /// numeric bounds instead of wrapping.
393 ///
394 /// ```
395 /// use const_num_traits::SaturatingCast;
396 ///
397 /// let x: u8 = SaturatingCast::saturating_cast(300u16);
398 /// let y: u8 = SaturatingCast::saturating_cast(-5i32);
399 /// assert_eq!(x, 255);
400 /// assert_eq!(y, 0);
401 /// ```
402 fn saturating_cast(self) -> T;
403}
404}
405
406macro_rules! cast_pair_impl {
407 ($from:ty => $($to:ty),*) => {$(
408 c0nst::c0nst! {
409 c0nst impl CheckedCast<$to> for $from {
410 // The round-trip check catches truncation; the sign comparison
411 // catches same-width reinterpretation (e.g. 200u8 -> -56i8).
412 #[inline]
413 #[allow(unused_comparisons)]
414 fn checked_cast(self) -> Option<$to> {
415 let r = self as $to;
416 if (r as $from) == self && ((r < 0) == (self < 0)) {
417 Some(r)
418 } else {
419 None
420 }
421 }
422 }
423 }
424
425 c0nst::c0nst! {
426 c0nst impl StrictCast<$to> for $from {
427 #[inline]
428 #[track_caller]
429 fn strict_cast(self) -> $to {
430 match CheckedCast::<$to>::checked_cast(self) {
431 Some(v) => v,
432 None => panic!("attempt to cast integer with overflow"),
433 }
434 }
435 }
436 }
437
438 c0nst::c0nst! {
439 c0nst impl WrappingCast<$to> for $from {
440 #[inline]
441 fn wrapping_cast(self) -> $to {
442 self as $to
443 }
444 }
445 }
446
447 c0nst::c0nst! {
448 c0nst impl SaturatingCast<$to> for $from {
449 #[inline]
450 #[allow(unused_comparisons)]
451 fn saturating_cast(self) -> $to {
452 match CheckedCast::<$to>::checked_cast(self) {
453 Some(v) => v,
454 None => {
455 if self < 0 {
456 <$to>::MIN
457 } else {
458 <$to>::MAX
459 }
460 }
461 }
462 }
463 }
464 }
465 )*};
466}
467
468macro_rules! cast_all_impl {
469 ($($from:ty),*) => {$(
470 cast_pair_impl!($from => u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize);
471 )*};
472}
473
474cast_all_impl!(
475 u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize
476);
477
478#[cfg(test)]
479mod tests {
480 use super::*;
481
482 #[test]
483 fn abs_diff_unsigned_abs() {
484 assert_eq!(AbsDiff::abs_diff(100u8, 120), 20);
485 assert_eq!(AbsDiff::abs_diff(120u8, 100), 20);
486 assert_eq!(AbsDiff::abs_diff(i8::MIN, i8::MAX), 255u8);
487 assert_eq!(AbsDiff::abs_diff(-10i32, 10), 20u32);
488 assert_eq!(UnsignedAbs::unsigned_abs(i128::MIN), 1u128 << 127);
489 assert_eq!(UnsignedAbs::unsigned_abs(-5isize), 5usize);
490 }
491
492 #[test]
493 fn clamp_magnitude() {
494 assert_eq!(ClampMagnitude::clamp_magnitude(120i8, 100), 100);
495 assert_eq!(ClampMagnitude::clamp_magnitude(-120i8, 100), -100);
496 assert_eq!(ClampMagnitude::clamp_magnitude(50i8, 100), 50);
497 // limit doesn't fit in i8: unchanged
498 assert_eq!(ClampMagnitude::clamp_magnitude(-120i8, 200), -120);
499 assert_eq!(ClampMagnitude::clamp_magnitude(i8::MIN, 127), -127);
500 }
501
502 #[test]
503 fn sign_casts() {
504 assert_eq!(CastSigned::cast_signed(255u8), -1i8);
505 assert_eq!(CastSigned::cast_signed(127u8), 127i8);
506 assert_eq!(CastUnsigned::cast_unsigned(-1i8), 255u8);
507 assert_eq!(CastUnsigned::cast_unsigned(5i8), 5u8);
508 // the value-checked variants live in the generic casts:
509 let c: Option<i8> = CheckedCast::checked_cast(255u8);
510 assert_eq!(c, None);
511 let s: i8 = SaturatingCast::saturating_cast(255u8);
512 assert_eq!(s, 127);
513 let c: Option<u8> = CheckedCast::checked_cast(-1i8);
514 assert_eq!(c, None);
515 let s: u8 = SaturatingCast::saturating_cast(-1i8);
516 assert_eq!(s, 0);
517 }
518
519 #[test]
520 #[should_panic(expected = "attempt to cast integer with overflow")]
521 fn strict_cast_panics() {
522 let _: u8 = StrictCast::strict_cast(-1i8);
523 }
524
525 #[test]
526 fn widen_truncate() {
527 let w: u128 = Widen::widen(u8::MAX);
528 assert_eq!(w, 255);
529 let w: i64 = Widen::widen(i32::MIN);
530 assert_eq!(w, i32::MIN as i64);
531 let t: u8 = Truncate::truncate(0xABCDu16);
532 assert_eq!(t, 0xCD);
533 // checked/saturating narrowing via the generic casts
534 let t: Option<i8> = CheckedCast::checked_cast(-1000i32);
535 assert_eq!(t, None);
536 let t: Option<i8> = CheckedCast::checked_cast(-100i32);
537 assert_eq!(t, Some(-100));
538 let t: i8 = SaturatingCast::saturating_cast(376i32);
539 assert_eq!(t, 127);
540 let t: i8 = SaturatingCast::saturating_cast(-1000i32);
541 assert_eq!(t, -128);
542 }
543
544 #[test]
545 fn generic_casts() {
546 // same-width sign reinterpretations are rejected by checked_cast
547 let r: Option<i8> = CheckedCast::checked_cast(200u8);
548 assert_eq!(r, None);
549 let r: Option<u8> = CheckedCast::checked_cast(-1i8);
550 assert_eq!(r, None);
551 // narrowing
552 let r: Option<u8> = CheckedCast::checked_cast(300u16);
553 assert_eq!(r, None);
554 let r: Option<u8> = CheckedCast::checked_cast(255u16);
555 assert_eq!(r, Some(255));
556 // widening with sign change
557 let r: Option<u128> = CheckedCast::checked_cast(-1i8);
558 assert_eq!(r, None);
559 let r: Option<i128> = CheckedCast::checked_cast(u64::MAX);
560 assert_eq!(r, Some(u64::MAX as i128));
561 // wrapping/saturating
562 let r: u8 = WrappingCast::wrapping_cast(-1i32);
563 assert_eq!(r, 255);
564 let r: u8 = SaturatingCast::saturating_cast(-1i32);
565 assert_eq!(r, 0);
566 let r: i8 = SaturatingCast::saturating_cast(u128::MAX);
567 assert_eq!(r, 127);
568 let r: i8 = SaturatingCast::saturating_cast(i128::MIN);
569 assert_eq!(r, -128);
570 // identity
571 let r: Option<usize> = CheckedCast::checked_cast(5usize);
572 assert_eq!(r, Some(5));
573 }
574
575 #[test]
576 fn generic_cast_exhaustive_u16_i8() {
577 // brute-force agreement with TryInto for one representative pair
578 for x in 0u16..=u16::MAX {
579 let checked: Option<i8> = CheckedCast::checked_cast(x);
580 let expected: Option<i8> = TryInto::try_into(x).ok();
581 assert_eq!(checked, expected, "{x}");
582 }
583 for x in i16::MIN..=i16::MAX {
584 let checked: Option<u8> = CheckedCast::checked_cast(x);
585 let expected: Option<u8> = TryInto::try_into(x).ok();
586 assert_eq!(checked, expected, "{x}");
587 }
588 }
589}