Skip to main content

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}