Skip to main content

arithmetic_nonmax/
lib.rs

1//! `NonMax` provides integer types that cannot be the maximum value of their underlying primitive type.
2//!
3//! # Memory Optimization
4//!
5//! The main benefit of `NonMax<T>` is that `Option<NonMax<T>>` has the same size as `T`.
6//! This is achieved through Rust's "niche optimization", where the bit pattern of the
7//! maximum value is used to represent `None`.
8//!
9//! | Primitive | `size_of::<T>()` | `size_of::<Option<T>>()` | `size_of::<Option<NonMax<T>>>()` |
10//! |-----------|------------------|--------------------------|---------------------------------|
11//! | `u32`     | 4                | 8                        | **4**                           |
12//! | `i32`     | 4                | 8                        | **4**                           |
13//! | `u8`      | 1                | 2                        | **1**                           |
14
15#![no_std]
16
17use core::convert::TryFrom;
18use core::fmt::{self, Binary, Display, LowerHex, Octal, UpperHex};
19use core::hash::Hash;
20use core::marker::PhantomData;
21use core::num::NonZero;
22use core::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Rem, RemAssign, Sub, SubAssign};
23
24/// Error type returned when a value is the maximum for its type.
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub struct MaxValueError;
27
28impl Display for MaxValueError {
29    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30        write!(f, "provided value is the maximum value for this type")
31    }
32}
33
34impl core::error::Error for MaxValueError {}
35
36/// A wrapper type for an integer that cannot be its maximum value.
37///
38/// This type leverages Rust's `NonZero` optimization by mapping the maximum value to zero internally.
39/// As a result, `Option<NonMax<T>>` has the same size as the underlying primitive type `T`.
40///
41/// # Examples
42/// ```
43/// # use arithmetic_nonmax::NonMaxU32;
44/// # use core::mem::size_of;
45/// assert_eq!(size_of::<NonMaxU32>(), 4);
46/// assert_eq!(size_of::<Option<NonMaxU32>>(), 4);
47/// ```
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
49pub struct NonMax<T: NonMaxItem>(T::NonZero);
50
51impl<T: NonMaxItem + Copy> NonMax<T> {
52    /// Creates a new `NonMax` if the given value is not the maximum value.
53    ///
54    /// # Examples
55    /// ```
56    /// # use arithmetic_nonmax::NonMaxU8;
57    /// assert!(NonMaxU8::new(254).is_some());
58    /// assert!(NonMaxU8::new(255).is_none());
59    /// ```
60    pub fn new(value: T) -> Option<Self> {
61        Value::new(value).to_inner_repr().to_nonmax()
62    }
63
64    /// Creates a new `NonMax` without checking the value.
65    ///
66    /// # Safety
67    /// The value must not be the maximum value of the underlying type.
68    pub unsafe fn new_unchecked(value: T) -> Self {
69        unsafe { Value::new(value).to_inner_repr().to_nonmax_unchecked() }
70    }
71
72    fn to_real_repr(self) -> Value<T, Real> {
73        T::from_nonzero(self.0).to_real_repr()
74    }
75
76    /// Returns the underlying primitive value.
77    ///
78    /// # Examples
79    /// ```
80    /// # use arithmetic_nonmax::NonMaxU32;
81    /// let x = NonMaxU32::new(123).unwrap();
82    /// assert_eq!(x.get(), 123);
83    /// ```
84    pub fn get(&self) -> T {
85        self.to_real_repr().value()
86    }
87
88    /// Checked integer addition. Computes `self + rhs`, returning `None` if overflow occurred
89    /// or if the result is the maximum value.
90    pub fn checked_add(self, rhs: Self) -> Option<Self> {
91        self.get().checked_add(rhs.get()).and_then(Self::new)
92    }
93
94    /// Checked integer subtraction. Computes `self - rhs`, returning `None` if overflow occurred
95    /// or if the result is the maximum value.
96    pub fn checked_sub(self, rhs: Self) -> Option<Self> {
97        self.get().checked_sub(rhs.get()).and_then(Self::new)
98    }
99
100    /// Checked integer multiplication. Computes `self * rhs`, returning `None` if overflow occurred
101    /// or if the result is the maximum value.
102    pub fn checked_mul(self, rhs: Self) -> Option<Self> {
103        self.get().checked_mul(rhs.get()).and_then(Self::new)
104    }
105
106    /// Checked integer division. Computes `self / rhs`, returning `None` if the divisor is zero
107    /// or if the result is the maximum value.
108    pub fn checked_div(self, rhs: Self) -> Option<Self> {
109        self.get().checked_div(rhs.get()).and_then(Self::new)
110    }
111
112    /// Checked integer remainder. Computes `self % rhs`, returning `None` if the divisor is zero
113    /// or if the result is the maximum value.
114    pub fn checked_rem(self, rhs: Self) -> Option<Self> {
115        self.get().checked_rem(rhs.get()).and_then(Self::new)
116    }
117
118    /// Checked addition with a primitive value.
119    ///
120    /// # Examples
121    /// ```
122    /// # use arithmetic_nonmax::NonMaxU8;
123    /// let x = NonMaxU8::new(100).unwrap();
124    /// assert_eq!(x.checked_add_val(50).unwrap().get(), 150);
125    /// assert!(x.checked_add_val(155).is_none()); // 255 is MAX
126    /// ```
127    pub fn checked_add_val(self, rhs: T) -> Option<Self> {
128        self.get().checked_add(rhs).and_then(Self::new)
129    }
130
131    /// Checked subtraction with a primitive value.
132    pub fn checked_sub_val(self, rhs: T) -> Option<Self> {
133        self.get().checked_sub(rhs).and_then(Self::new)
134    }
135
136    /// Checked multiplication with a primitive value.
137    pub fn checked_mul_val(self, rhs: T) -> Option<Self> {
138        self.get().checked_mul(rhs).and_then(Self::new)
139    }
140
141    /// Checked division with a primitive value.
142    pub fn checked_div_val(self, rhs: T) -> Option<Self> {
143        self.get().checked_div(rhs).and_then(Self::new)
144    }
145
146    /// Checked remainder with a primitive value.
147    pub fn checked_rem_val(self, rhs: T) -> Option<Self> {
148        self.get().checked_rem(rhs).and_then(Self::new)
149    }
150}
151
152impl<T: NonMaxItem + Copy + Add<Output = T>> Add for NonMax<T> {
153    type Output = Self;
154    fn add(self, rhs: Self) -> Self::Output {
155        self.checked_add(rhs)
156            .expect("attempt to add with overflow or to maximum value")
157    }
158}
159
160impl<T: NonMaxItem + Copy + Add<Output = T>> Add<T> for NonMax<T> {
161    type Output = Self;
162    fn add(self, rhs: T) -> Self::Output {
163        self.checked_add_val(rhs)
164            .expect("attempt to add with overflow or to maximum value")
165    }
166}
167
168impl<T: NonMaxItem + Copy + Add<Output = T>> AddAssign for NonMax<T> {
169    fn add_assign(&mut self, rhs: Self) {
170        *self = *self + rhs;
171    }
172}
173
174impl<T: NonMaxItem + Copy + Add<Output = T>> AddAssign<T> for NonMax<T> {
175    fn add_assign(&mut self, rhs: T) {
176        *self = *self + rhs;
177    }
178}
179
180impl<T: NonMaxItem + Copy + Sub<Output = T>> Sub for NonMax<T> {
181    type Output = Self;
182    fn sub(self, rhs: Self) -> Self::Output {
183        self.checked_sub(rhs)
184            .expect("attempt to subtract with overflow or to maximum value")
185    }
186}
187
188impl<T: NonMaxItem + Copy + Sub<Output = T>> Sub<T> for NonMax<T> {
189    type Output = Self;
190    fn sub(self, rhs: T) -> Self::Output {
191        self.checked_sub_val(rhs)
192            .expect("attempt to subtract with overflow or to maximum value")
193    }
194}
195
196impl<T: NonMaxItem + Copy + Sub<Output = T>> SubAssign for NonMax<T> {
197    fn sub_assign(&mut self, rhs: Self) {
198        *self = *self - rhs;
199    }
200}
201
202impl<T: NonMaxItem + Copy + Sub<Output = T>> SubAssign<T> for NonMax<T> {
203    fn sub_assign(&mut self, rhs: T) {
204        *self = *self - rhs;
205    }
206}
207
208impl<T: NonMaxItem + Copy + Mul<Output = T>> Mul for NonMax<T> {
209    type Output = Self;
210    fn mul(self, rhs: Self) -> Self::Output {
211        self.checked_mul(rhs)
212            .expect("attempt to multiply with overflow or to maximum value")
213    }
214}
215
216impl<T: NonMaxItem + Copy + Mul<Output = T>> Mul<T> for NonMax<T> {
217    type Output = Self;
218    fn mul(self, rhs: T) -> Self::Output {
219        self.checked_mul_val(rhs)
220            .expect("attempt to multiply with overflow or to maximum value")
221    }
222}
223
224impl<T: NonMaxItem + Copy + Mul<Output = T>> MulAssign for NonMax<T> {
225    fn mul_assign(&mut self, rhs: Self) {
226        *self = *self * rhs;
227    }
228}
229
230impl<T: NonMaxItem + Copy + Mul<Output = T>> MulAssign<T> for NonMax<T> {
231    fn mul_assign(&mut self, rhs: T) {
232        *self = *self * rhs;
233    }
234}
235
236impl<T: NonMaxItem + Copy + Div<Output = T>> Div for NonMax<T> {
237    type Output = Self;
238    fn div(self, rhs: Self) -> Self::Output {
239        self.checked_div(rhs)
240            .expect("attempt to divide by zero or to maximum value")
241    }
242}
243
244impl<T: NonMaxItem + Copy + Div<Output = T>> Div<T> for NonMax<T> {
245    type Output = Self;
246    fn div(self, rhs: T) -> Self::Output {
247        self.checked_div_val(rhs)
248            .expect("attempt to divide by zero or to maximum value")
249    }
250}
251
252impl<T: NonMaxItem + Copy + Div<Output = T>> DivAssign for NonMax<T> {
253    fn div_assign(&mut self, rhs: Self) {
254        *self = *self / rhs;
255    }
256}
257
258impl<T: NonMaxItem + Copy + Div<Output = T>> DivAssign<T> for NonMax<T> {
259    fn div_assign(&mut self, rhs: T) {
260        *self = *self / rhs;
261    }
262}
263
264impl<T: NonMaxItem + Copy + Rem<Output = T>> Rem for NonMax<T> {
265    type Output = Self;
266    fn rem(self, rhs: Self) -> Self::Output {
267        self.checked_rem(rhs)
268            .expect("attempt to calculate remainder by zero or to maximum value")
269    }
270}
271
272impl<T: NonMaxItem + Copy + Rem<Output = T>> Rem<T> for NonMax<T> {
273    type Output = Self;
274    fn rem(self, rhs: T) -> Self::Output {
275        self.checked_rem_val(rhs)
276            .expect("attempt to calculate remainder by zero or to maximum value")
277    }
278}
279
280impl<T: NonMaxItem + Copy + Rem<Output = T>> RemAssign for NonMax<T> {
281    fn rem_assign(&mut self, rhs: Self) {
282        *self = *self % rhs;
283    }
284}
285
286impl<T: NonMaxItem + Copy + Rem<Output = T>> RemAssign<T> for NonMax<T> {
287    fn rem_assign(&mut self, rhs: T) {
288        *self = *self % rhs;
289    }
290}
291
292impl<T: NonMaxItem + Copy + PartialOrd> PartialOrd for NonMax<T> {
293    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
294        self.to_real_repr().partial_cmp(&other.to_real_repr())
295    }
296}
297
298impl<T: NonMaxItem + Copy + Ord> Ord for NonMax<T> {
299    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
300        self.to_real_repr().cmp(&other.to_real_repr())
301    }
302}
303
304impl<T: NonMaxItem + Copy + Display> Display for NonMax<T> {
305    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
306        Display::fmt(&self.get(), f)
307    }
308}
309
310impl<T: NonMaxItem + Copy + Binary> Binary for NonMax<T> {
311    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
312        Binary::fmt(&self.get(), f)
313    }
314}
315
316impl<T: NonMaxItem + Copy + Octal> Octal for NonMax<T> {
317    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
318        Octal::fmt(&self.get(), f)
319    }
320}
321
322impl<T: NonMaxItem + Copy + LowerHex> LowerHex for NonMax<T> {
323    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
324        LowerHex::fmt(&self.get(), f)
325    }
326}
327
328impl<T: NonMaxItem + Copy + UpperHex> UpperHex for NonMax<T> {
329    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
330        UpperHex::fmt(&self.get(), f)
331    }
332}
333
334impl<T: NonMaxItem + Copy> Default for NonMax<T> {
335    fn default() -> Self {
336        Self::new(T::ZERO).unwrap()
337    }
338}
339
340#[doc(hidden)]
341pub trait NonMaxItem: Sized {
342    type NonZero: Copy + PartialEq + Eq + PartialOrd + Ord + Hash;
343    const ZERO: Self;
344    fn transform(self) -> Self;
345    fn to_nonzero(value: Value<Self, Inner>) -> Option<Self::NonZero>;
346    unsafe fn to_nonzero_unchecked(value: Value<Self, Inner>) -> Self::NonZero;
347    fn from_nonzero(value: Self::NonZero) -> Value<Self, Inner>;
348
349    fn checked_add(self, rhs: Self) -> Option<Self>;
350    fn checked_sub(self, rhs: Self) -> Option<Self>;
351    fn checked_mul(self, rhs: Self) -> Option<Self>;
352    fn checked_div(self, rhs: Self) -> Option<Self>;
353    fn checked_rem(self, rhs: Self) -> Option<Self>;
354}
355
356macro_rules! impl_non_max_item {
357    ($($t:ty, $name:ident, $doc:expr),*) => {
358        $(
359            impl NonMaxItem for $t {
360                type NonZero = NonZero<$t>;
361                const ZERO: Self = 0;
362                fn transform(self) -> Self {
363                    self ^ <$t>::MAX
364                }
365                fn to_nonzero(value: Value<Self, Inner>) -> Option<Self::NonZero> {
366                    Self::NonZero::new(value.value())
367                }
368                unsafe fn to_nonzero_unchecked(value: Value<Self, Inner>) -> Self::NonZero {
369                    unsafe { Self::NonZero::new_unchecked(value.value()) }
370                }
371                fn from_nonzero(value: Self::NonZero) -> Value<Self, Inner> {
372                    Value::new(value.get())
373                }
374
375                fn checked_add(self, rhs: Self) -> Option<Self> { self.checked_add(rhs) }
376                fn checked_sub(self, rhs: Self) -> Option<Self> { self.checked_sub(rhs) }
377                fn checked_mul(self, rhs: Self) -> Option<Self> { self.checked_mul(rhs) }
378                fn checked_div(self, rhs: Self) -> Option<Self> { self.checked_div(rhs) }
379                fn checked_rem(self, rhs: Self) -> Option<Self> { self.checked_rem(rhs) }
380            }
381
382            impl From<NonMax<$t>> for $t {
383                fn from(value: NonMax<$t>) -> Self {
384                    value.get()
385                }
386            }
387
388            impl TryFrom<$t> for NonMax<$t> {
389                type Error = MaxValueError;
390
391                fn try_from(value: $t) -> Result<Self, Self::Error> {
392                    Self::new(value).ok_or(MaxValueError)
393                }
394            }
395
396            #[doc = $doc]
397            pub type $name = NonMax<$t>;
398        )*
399    };
400}
401
402impl_non_max_item!(
403    u8,
404    NonMaxU8,
405    "An unsigned 8-bit integer that cannot be `u8::MAX`.",
406    u16,
407    NonMaxU16,
408    "An unsigned 16-bit integer that cannot be `u16::MAX`.",
409    u32,
410    NonMaxU32,
411    "An unsigned 32-bit integer that cannot be `u32::MAX`.",
412    u64,
413    NonMaxU64,
414    "An unsigned 64-bit integer that cannot be `u64::MAX`.",
415    u128,
416    NonMaxU128,
417    "An unsigned 128-bit integer that cannot be `u128::MAX`.",
418    usize,
419    NonMaxUsize,
420    "An unsigned pointer-sized integer that cannot be `usize::MAX`.",
421    i8,
422    NonMaxI8,
423    "A signed 8-bit integer that cannot be `i8::MAX`.",
424    i16,
425    NonMaxI16,
426    "A signed 16-bit integer that cannot be `i16::MAX`.",
427    i32,
428    NonMaxI32,
429    "A signed 32-bit integer that cannot be `i32::MAX`.",
430    i64,
431    NonMaxI64,
432    "A signed 64-bit integer that cannot be `i64::MAX`.",
433    i128,
434    NonMaxI128,
435    "A signed 128-bit integer that cannot be `i128::MAX`.",
436    isize,
437    NonMaxIsize,
438    "A signed pointer-sized integer that cannot be `isize::MAX`."
439);
440
441#[doc(hidden)]
442#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
443pub struct Real;
444#[doc(hidden)]
445#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
446pub struct Inner;
447
448#[doc(hidden)]
449#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
450pub struct Value<T, M> {
451    value: T,
452    _marker: PhantomData<M>,
453}
454
455impl<T: NonMaxItem + Copy, M> Value<T, M> {
456    fn new(value: T) -> Self {
457        Self {
458            value,
459            _marker: PhantomData,
460        }
461    }
462
463    fn value(&self) -> T {
464        self.value
465    }
466}
467
468impl<T: NonMaxItem + Copy> Value<T, Real> {
469    fn to_inner_repr(self) -> Value<T, Inner> {
470        Value::new(T::transform(self.value))
471    }
472}
473
474impl<T: NonMaxItem + Copy + Add<Output = T>> Add for Value<T, Real> {
475    type Output = Self;
476    fn add(self, rhs: Self) -> Self::Output {
477        Self::new(self.value() + rhs.value())
478    }
479}
480
481impl<T: NonMaxItem + Copy + Sub<Output = T>> Sub for Value<T, Real> {
482    type Output = Self;
483    fn sub(self, rhs: Self) -> Self::Output {
484        Self::new(self.value() - rhs.value())
485    }
486}
487
488impl<T: NonMaxItem + Copy + Mul<Output = T>> Mul for Value<T, Real> {
489    type Output = Self;
490    fn mul(self, rhs: Self) -> Self::Output {
491        Self::new(self.value() * rhs.value())
492    }
493}
494
495impl<T: NonMaxItem + Copy + Div<Output = T>> Div for Value<T, Real> {
496    type Output = Self;
497    fn div(self, rhs: Self) -> Self::Output {
498        Self::new(self.value() / rhs.value())
499    }
500}
501
502impl<T: NonMaxItem + Copy + Rem<Output = T>> Rem for Value<T, Real> {
503    type Output = Self;
504    fn rem(self, rhs: Self) -> Self::Output {
505        Self::new(self.value() % rhs.value())
506    }
507}
508
509impl<T: NonMaxItem + Copy> Value<T, Inner> {
510    fn to_real_repr(self) -> Value<T, Real> {
511        Value::new(T::transform(self.value))
512    }
513
514    fn to_nonmax(self) -> Option<NonMax<T>> {
515        T::to_nonzero(self).map(NonMax)
516    }
517
518    unsafe fn to_nonmax_unchecked(self) -> NonMax<T> {
519        NonMax(unsafe { T::to_nonzero_unchecked(self) })
520    }
521}
522
523impl<T: NonMaxItem + Copy + PartialOrd> PartialOrd for Value<T, Real> {
524    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
525        self.value().partial_cmp(&other.value())
526    }
527}
528
529impl<T: NonMaxItem + Copy + Ord> Ord for Value<T, Real> {
530    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
531        self.value().cmp(&other.value())
532    }
533}
534
535impl<T: NonMaxItem + Copy + Display> Display for Value<T, Real> {
536    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
537        write!(f, "{}", self.value())
538    }
539}
540
541#[cfg(test)]
542mod tests {
543    extern crate std;
544    use super::*;
545    use core::mem::size_of;
546    use std::collections::HashSet;
547
548    #[test]
549    fn test_hash() {
550        let mut set = HashSet::new();
551        set.insert(NonMaxU32::new(1).unwrap());
552        set.insert(NonMaxU32::new(2).unwrap());
553        set.insert(NonMaxU32::new(1).unwrap());
554
555        assert_eq!(set.len(), 2);
556        assert!(set.contains(&NonMaxU32::new(1).unwrap()));
557    }
558
559    #[test]
560    fn test_sizes() {
561        assert_eq!(size_of::<NonMaxU32>(), 4);
562        assert_eq!(size_of::<Option<NonMaxU32>>(), 4);
563
564        assert_eq!(size_of::<NonMaxI32>(), 4);
565        assert_eq!(size_of::<Option<NonMaxI32>>(), 4);
566
567        assert_eq!(size_of::<NonMaxU8>(), 1);
568        assert_eq!(size_of::<Option<NonMaxU8>>(), 1);
569    }
570
571    #[test]
572    fn test_conversions() {
573        let x = NonMaxU8::try_from(100).unwrap();
574        assert_eq!(u8::from(x), 100);
575
576        let max_val = u8::MAX;
577        assert!(NonMaxU8::try_from(max_val).is_err());
578    }
579
580    #[test]
581    fn test_arithmetic_with_val() {
582        let x = NonMaxU8::new(100).unwrap();
583        let y = x + 50;
584        assert_eq!(u8::from(y), 150);
585
586        let mut z = NonMaxU8::new(10).unwrap();
587        z += 20;
588        assert_eq!(u8::from(z), 30);
589
590        let a = NonMaxU8::new(10).unwrap();
591        let b = a * 5;
592        assert_eq!(u8::from(b), 50);
593
594        let c = NonMaxU8::new(100).unwrap();
595        let d = c / 3;
596        assert_eq!(u8::from(d), 33);
597    }
598
599    #[test]
600    fn test_add_overflow() {
601        let x = NonMaxU8::try_from(250).unwrap();
602        // Now it should return None instead of panicking
603        assert!(x.checked_add_val(10).is_none());
604    }
605
606    #[test]
607    fn test_add_to_max() {
608        let x = NonMaxU8::try_from(250).unwrap();
609        // Result is 255 (MAX), so it should return None
610        assert!(x.checked_add_val(5).is_none());
611    }
612
613    #[test]
614    fn test_signed_integer() {
615        // i8: -128 to 127. MAX is 127.
616        let x = NonMaxI8::try_from(100).unwrap();
617        let y = x + 20;
618        assert_eq!(i8::from(y), 120);
619
620        let z = NonMaxI8::try_from(-50).unwrap();
621        let w = z + 10;
622        assert_eq!(i8::from(w), -40);
623
624        // MIN (-128) is allowed
625        let min_val = NonMaxI8::try_from(i8::MIN).unwrap();
626        assert_eq!(i8::from(min_val), -128);
627    }
628
629    #[test]
630    fn test_signed_overflow() {
631        let x = NonMaxI8::try_from(120).unwrap();
632        // Overflow detected
633        assert!(x.checked_add_val(10).is_none());
634    }
635
636    #[test]
637    fn test_signed_to_max() {
638        let x = NonMaxI8::try_from(120).unwrap();
639        // Result is 127 (MAX), so None
640        assert!(x.checked_add_val(7).is_none());
641    }
642
643    #[test]
644    fn test_formatting() {
645        let x = NonMaxU8::new(254).unwrap();
646        assert_eq!(std::format!("{}", x), "254");
647        assert_eq!(std::format!("{:b}", x), "11111110");
648        assert_eq!(std::format!("{:o}", x), "376");
649        assert_eq!(std::format!("{:x}", x), "fe");
650        assert_eq!(std::format!("{:X}", x), "FE");
651    }
652}