scolapasta_int_parse/
radix.rs

1use core::num::NonZeroU32;
2
3use crate::error::{InvalidRadixError, InvalidRadixErrorKind};
4use crate::subject::IntegerString;
5use crate::whitespace;
6
7// Create a lookup table from each byte value (of which the ASCII range is
8// relevant) to the maximum minimum radix the character is valid for.
9const fn radix_table() -> [u32; 256] {
10    // `u32::MAX` is used as a sentinel such that no valid radix can use this
11    // byte.
12    let mut table = [u32::MAX; 256];
13    let mut byte = 0_u8;
14    loop {
15        let idx = byte as usize;
16        if byte >= b'0' && byte <= b'9' {
17            table[idx] = (byte - b'0' + 1) as u32;
18        } else if byte >= b'A' && byte <= b'Z' {
19            table[idx] = (byte - b'A' + 11) as u32;
20        } else if byte >= b'a' && byte <= b'z' {
21            table[idx] = (byte - b'a' + 11) as u32;
22        }
23        if byte.checked_add(1).is_none() {
24            return table;
25        }
26        byte += 1;
27    }
28}
29
30pub static RADIX_TABLE: [u32; 256] = radix_table();
31
32/// A checked container for the radix to use when converting a string to an
33/// integer.
34///
35/// This type enforces that its value is in the range 2 and 36 inclusive, which
36/// is required by [`i64::from_str_radix`].
37///
38/// This type is not part of the public API for [`parse`] but can be used on its
39/// own.
40///
41/// [`parse`]: crate::parse
42#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
43pub struct Radix(NonZeroU32);
44
45impl Default for Radix {
46    /// The default radix is `10`.
47    ///
48    /// # Examples
49    ///
50    /// ```
51    /// # use scolapasta_int_parse::Radix;
52    /// # fn example() -> Option<()> {
53    /// let default = Radix::default();
54    /// let base10 = Radix::new(10)?;
55    /// assert_eq!(default, base10);
56    /// assert_eq!(default.as_u32(), 10);
57    /// # Some(())
58    /// # }
59    /// # example().unwrap();
60    /// ```
61    fn default() -> Self {
62        // SAFETY: Constant `10` is non-zero and between 2 and 36.
63        unsafe { Self::new_unchecked(10) }
64    }
65}
66
67impl From<Radix> for u32 {
68    fn from(radix: Radix) -> Self {
69        radix.as_u32()
70    }
71}
72
73impl TryFrom<u32> for Radix {
74    type Error = InvalidRadixError;
75
76    /// Construct a new `Radix`.
77    ///
78    /// `radix` must be between 2 and 36 inclusive; otherwise [`None`] is
79    /// returned.
80    ///
81    /// # Examples
82    ///
83    /// ```
84    /// # use scolapasta_int_parse::Radix;
85    /// let zero_radix = Radix::try_from(0);
86    /// assert!(matches!(zero_radix, Err(_)));
87    ///
88    /// let radix = Radix::try_from(16);
89    /// assert!(matches!(radix, Ok(radix) if radix.as_u32() == 16));
90    ///
91    /// let invalid_radix = Radix::try_from(512);
92    /// assert!(matches!(invalid_radix, Err(_)));
93    /// ```
94    fn try_from(radix: u32) -> Result<Self, Self::Error> {
95        let radix = NonZeroU32::new(radix).ok_or_else(|| InvalidRadixErrorKind::Invalid(radix.into()))?;
96        radix.try_into()
97    }
98}
99
100impl TryFrom<NonZeroU32> for Radix {
101    type Error = InvalidRadixError;
102
103    /// Construct a new `Radix`.
104    ///
105    /// `radix` must be between 2 and 36 inclusive; otherwise [`None`] is
106    /// returned.
107    ///
108    /// # Examples
109    ///
110    /// ```
111    /// # use std::num::NonZeroU32;
112    /// # use scolapasta_int_parse::Radix;
113    /// let radix = Radix::try_from(NonZeroU32::new(16).unwrap());
114    /// assert!(matches!(radix, Ok(radix) if radix.as_u32() == 16));
115    ///
116    /// let invalid_radix = Radix::try_from(NonZeroU32::new(512).unwrap());
117    /// assert!(matches!(invalid_radix, Err(_)));
118    /// ```
119    fn try_from(radix: NonZeroU32) -> Result<Self, Self::Error> {
120        if (2..=36).contains(&radix.get()) {
121            Ok(Self(radix))
122        } else {
123            Err(InvalidRadixErrorKind::Invalid(radix.get().into()).into())
124        }
125    }
126}
127
128impl Radix {
129    /// Construct a new `Radix`.
130    ///
131    /// `radix` must be non-zero and between 2 and 36 inclusive; otherwise
132    /// [`None`] is returned.
133    ///
134    /// # Examples
135    ///
136    /// ```
137    /// # use scolapasta_int_parse::Radix;
138    /// let radix = Radix::new(16);
139    /// assert!(matches!(radix, Some(radix) if radix.as_u32() == 16));
140    ///
141    /// let invalid_radix = Radix::new(512);
142    /// assert_eq!(invalid_radix, None);
143    /// ```
144    #[must_use]
145    pub fn new(radix: u32) -> Option<Self> {
146        radix.try_into().ok()
147    }
148
149    /// Construct a new `Radix` without checking the value.
150    ///
151    /// # Safety
152    ///
153    /// The given radix must not be zero. The given radix must be between 2 and
154    /// 36 inclusive.
155    ///
156    /// # Examples
157    ///
158    /// ```
159    /// # use scolapasta_int_parse::Radix;
160    /// let radix = unsafe { Radix::new_unchecked(16) };
161    /// assert_eq!(radix.as_u32(), 16);
162    /// ```
163    #[must_use]
164    pub const unsafe fn new_unchecked(radix: u32) -> Self {
165        // SAFETY: The safety contract the caller must uphold guarantees `radix`
166        // is non-zero and between 2 and 36, which satisfies the safety contract
167        // of `NonZeroU32::new_unchecked`.
168        let radix = unsafe { NonZeroU32::new_unchecked(radix) };
169        Self(radix)
170    }
171
172    pub(crate) fn try_base_from_str_and_i64(
173        subject: IntegerString<'_>,
174        num: i64,
175    ) -> Result<Option<u32>, InvalidRadixError> {
176        match i32::try_from(num) {
177            // ```
178            // [3.1.2] > Integer "123", 0
179            // => 123
180            // [3.1.2] > Integer "0123", 0
181            // => 83
182            // [3.1.2] > Integer "0x123", 0
183            // => 291
184            // [3.1.2] > Integer "0x123", -1
185            // => 291
186            // [3.1.2] > Integer "111", -1
187            // => 111
188            // ```
189            Ok(0 | -1) => Ok(None),
190            // ```
191            // [3.1.2] > Integer "123", 1
192            // (irb):31:in `Integer': invalid radix 1 (ArgumentError)
193            //         from (irb):31:in `<main>'
194            //         from /usr/local/var/rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/irb-1.4.1/exe/irb:11:in `<top (required)>'
195            //         from /usr/local/var/rbenv/versions/3.1.2/bin/irb:25:in `load'
196            //         from /usr/local/var/rbenv/versions/3.1.2/bin/irb:25:in `<main>'
197            // [3.1.2] > Integer "0x123", 1
198            // (irb):32:in `Integer': invalid radix 1 (ArgumentError)
199            //         from (irb):32:in `<main>'
200            //         from /usr/local/var/rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/irb-1.4.1/exe/irb:11:in `<top (required)>'
201            //         from /usr/local/var/rbenv/versions/3.1.2/bin/irb:25:in `load'
202            //         from /usr/local/var/rbenv/versions/3.1.2/bin/irb:25:in `<main>'
203            // ```
204            Ok(1) => Err(InvalidRadixErrorKind::Invalid(num).into()),
205            // Octal and base literals ignore negative radixes, but only if they
206            // are in range of `i32`.
207            //
208            // ```
209            // [3.1.2] > Integer "0123", -1
210            // => 83
211            // [3.1.2] > Integer "0123", -2000
212            // => 83
213            // [3.1.2] > Integer "0x123", -1
214            // => 291
215            // [3.1.2] > Integer "0x123", -(2 ** 31)
216            // => 291
217            // [3.1.2] > Integer "0d123", -(2 ** 39)
218            // (irb):81:in `Integer': integer -549755813888 too small to convert to `int' (RangeError)
219            //         from (irb):81:in `<main>'
220            //         from /usr/local/var/rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/irb-1.4.1/exe/irb:11:in `<top (required)>'
221            //         from /usr/local/var/rbenv/versions/3.1.2/bin/irb:25:in `load'
222            //         from /usr/local/var/rbenv/versions/3.1.2/bin/irb:25:in `<main>'
223            // ```
224            Ok(_) if num < 0 && matches!(whitespace::trim_leading(subject.as_bytes()).first(), Some(&b'0')) => {
225                Ok(None)
226            }
227            // ```
228            // [3.1.2] > Integer "123", -(2 ** 31)
229            // (irb):63:in `Integer': invalid radix -2147483648 (ArgumentError)
230            //         from (irb):63:in `<main>'
231            //         from /usr/local/var/rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/irb-1.4.1/exe/irb:11:in `<top (required)>'
232            //         from /usr/local/var/rbenv/versions/3.1.2/bin/irb:25:in `load'
233            //         from /usr/local/var/rbenv/versions/3.1.2/bin/irb:25:in `<main>'
234            // ```
235            Ok(i32::MIN) => Err(InvalidRadixErrorKind::Invalid(num).into()),
236            Ok(radix) => {
237                // ```
238                // [3.1.2] > Integer "123", -(2**21)
239                // (irb):46:in `Integer': invalid radix 2097152 (ArgumentError)
240                //         from (irb):46:in `<main>'
241                //         from /usr/local/var/rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/irb-1.4.1/exe/irb:11:in `<top (required)>'
242                //         from /usr/local/var/rbenv/versions/3.1.2/bin/irb:25:in `load'
243                //         from /usr/local/var/rbenv/versions/3.1.2/bin/irb:25:in `<main>'
244                // ```
245                let radix = match u32::try_from(radix) {
246                    Ok(radix) => radix,
247                    // ```
248                    // [3.1.2] > Integer "111", -2
249                    // => 7
250                    // [3.1.2] > Integer "123", -36
251                    // => 1371
252                    // ```
253                    Err(_) if (-36..=-2).contains(&radix) => (-radix).try_into().expect("radix is in range for u32"),
254                    // ```
255                    // [3.1.2] > Integer "123", -(2 ** 21)
256                    // (irb):67:in `Integer': invalid radix 2097152 (ArgumentError)
257                    //         from (irb):67:in `<main>'
258                    //         from /usr/local/var/rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/irb-1.4.1/exe/irb:11:in `<top (required)>'
259                    //         from /usr/local/var/rbenv/versions/3.1.2/bin/irb:25:in `load'
260                    //         from /usr/local/var/rbenv/versions/3.1.2/bin/irb:25:in `<main>'
261                    // [3.1.2] > 2 ** 21
262                    // => 2097152
263                    // ```
264                    Err(_) => {
265                        // Unchecked negation is safe because we checked for
266                        // `i32::MIN` above.
267                        let num = -num;
268                        return Err(InvalidRadixErrorKind::Invalid(num).into());
269                    }
270                };
271                if let Some(radix) = Radix::new(radix) {
272                    Ok(Some(radix.as_u32()))
273                } else {
274                    // ```
275                    // [3.1.2] > Integer "123", 49
276                    // (irb):83:in `Integer': invalid radix 49 (ArgumentError)
277                    //         from (irb):83:in `<main>'
278                    //         from /usr/local/var/rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/irb-1.4.1/exe/irb:11:in `<top (required)>'
279                    //         from /usr/local/var/rbenv/versions/3.1.2/bin/irb:25:in `load'
280                    //         from /usr/local/var/rbenv/versions/3.1.2/bin/irb:25:in `<main>'
281                    // ```
282                    Err(InvalidRadixErrorKind::Invalid(num).into())
283                }
284            }
285            // ```
286            // [3.1.2] > Integer "123", (2 ** 32 + 1)
287            // (irb):34:in `Integer': integer 4294967297 too big to convert to `int' (RangeError)
288            //         from (irb):34:in `<main>'
289            //         from /usr/local/var/rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/irb-1.4.1/exe/irb:11:in `<top (required)>'
290            //         from /usr/local/var/rbenv/versions/3.1.2/bin/irb:25:in `load'
291            //         from /usr/local/var/rbenv/versions/3.1.2/bin/irb:25:in `<main>'
292            // ```
293            Err(_) if num > i32::MAX.into() => Err(InvalidRadixErrorKind::TooBig(num).into()),
294            // ```
295            // [3.1.2] > Integer "123", -(2 ** 32 + 1)
296            // (irb):33:in `Integer': integer -4294967297 too small to convert to `int' (RangeError)
297            //         from (irb):33:in `<main>'
298            //         from /usr/local/var/rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/irb-1.4.1/exe/irb:11:in `<top (required)>'
299            //         from /usr/local/var/rbenv/versions/3.1.2/bin/irb:25:in `load'
300            //         from /usr/local/var/rbenv/versions/3.1.2/bin/irb:25:in `<main>'
301            // ```
302            Err(_) if num < i32::MIN.into() => Err(InvalidRadixErrorKind::TooSmall(num).into()),
303            Err(_) => unreachable!("all cases covered"),
304        }
305    }
306
307    /// Extract the `Radix` as the underlying [`u32`].
308    ///
309    /// # Examples
310    ///
311    /// ```
312    /// # use scolapasta_int_parse::Radix;
313    /// # fn example() -> Option<()> {
314    /// for base in 2..=36 {
315    ///     let radix = Radix::new(base)?;
316    ///     assert_eq!(radix.as_u32(), base);
317    /// }
318    /// # Some(())
319    /// # }
320    /// # example().unwrap();
321    /// ```
322    #[inline]
323    #[must_use]
324    pub const fn as_u32(self) -> u32 {
325        self.0.get()
326    }
327}
328
329#[cfg(test)]
330#[expect(clippy::undocumented_unsafe_blocks, reason = "Testing unsafe functions")]
331mod tests {
332    use alloc::string::String;
333    use core::fmt::Write as _;
334
335    use super::{RADIX_TABLE, Radix};
336    use crate::error::InvalidRadixExceptionKind;
337
338    #[test]
339    fn default_is_radix_10() {
340        let default = Radix::default();
341        let base10 = Radix::new(10).unwrap();
342        assert_eq!(default, base10);
343        assert_eq!(default.as_u32(), 10);
344    }
345
346    #[test]
347    fn radix_new_validates_radix_is_nonzero() {
348        let radix = Radix::new(0);
349        assert_eq!(radix, None);
350    }
351
352    #[test]
353    fn radix_new_parses_valid_radixes() {
354        for r in 2..=36 {
355            let radix = Radix::new(r);
356            let radix = radix.unwrap();
357            assert_eq!(radix.as_u32(), r, "unexpected value for test case {r}");
358        }
359    }
360
361    #[test]
362    fn radix_new_rejects_too_large_radixes() {
363        for r in 37..=525 {
364            let radix = Radix::new(r);
365            assert_eq!(radix, None, "unexpected value for test case {r}");
366        }
367    }
368
369    #[test]
370    fn radix_new_unchecked_valid_radixes() {
371        for r in 2..=36 {
372            let radix = unsafe { Radix::new_unchecked(r) };
373            assert_eq!(radix.as_u32(), r, "unexpected value for test case {r}");
374        }
375    }
376
377    #[test]
378    fn radix_table_is_valid() {
379        let test_cases = [
380            (b'0', 1_u32),
381            (b'1', 2),
382            (b'2', 3),
383            (b'3', 4),
384            (b'4', 5),
385            (b'5', 6),
386            (b'6', 7),
387            (b'7', 8),
388            (b'8', 9),
389            (b'9', 10),
390            (b'A', 11),
391            (b'B', 12),
392            (b'C', 13),
393            (b'D', 14),
394            (b'E', 15),
395            (b'F', 16),
396            (b'G', 17),
397            (b'H', 18),
398            (b'I', 19),
399            (b'J', 20),
400            (b'K', 21),
401            (b'L', 22),
402            (b'M', 23),
403            (b'N', 24),
404            (b'O', 25),
405            (b'P', 26),
406            (b'Q', 27),
407            (b'R', 28),
408            (b'S', 29),
409            (b'T', 30),
410            (b'U', 31),
411            (b'V', 32),
412            (b'W', 33),
413            (b'X', 34),
414            (b'Y', 35),
415            (b'Z', 36),
416            (b'a', 11),
417            (b'b', 12),
418            (b'c', 13),
419            (b'd', 14),
420            (b'e', 15),
421            (b'f', 16),
422            (b'g', 17),
423            (b'h', 18),
424            (b'i', 19),
425            (b'j', 20),
426            (b'k', 21),
427            (b'l', 22),
428            (b'm', 23),
429            (b'n', 24),
430            (b'o', 25),
431            (b'p', 26),
432            (b'q', 27),
433            (b'r', 28),
434            (b's', 29),
435            (b't', 30),
436            (b'u', 31),
437            (b'v', 32),
438            (b'w', 33),
439            (b'x', 34),
440            (b'y', 35),
441            (b'z', 36),
442        ];
443        for (byte, radix) in test_cases {
444            assert_eq!(
445                RADIX_TABLE[usize::from(byte)],
446                radix,
447                "unexpected value for test case ({byte}, {radix})"
448            );
449        }
450    }
451
452    #[test]
453    fn non_ascii_alphanumeric_are_invalid() {
454        for byte in 0..=u8::MAX {
455            if byte.is_ascii_alphanumeric() {
456                continue;
457            }
458            // no valid radix can use this byte
459            assert_eq!(
460                RADIX_TABLE[usize::from(byte)],
461                u32::MAX,
462                "unexpected value for test case '{byte}'"
463            );
464        }
465    }
466
467    #[test]
468    fn from_base_zero() {
469        let subject = "123".try_into().unwrap();
470        let result = Radix::try_base_from_str_and_i64(subject, 0);
471        assert_eq!(result.unwrap(), None);
472
473        let subject = "0123".try_into().unwrap();
474        let result = Radix::try_base_from_str_and_i64(subject, 0);
475        assert_eq!(result.unwrap(), None);
476
477        let subject = "0x123".try_into().unwrap();
478        let result = Radix::try_base_from_str_and_i64(subject, 0);
479        assert_eq!(result.unwrap(), None);
480    }
481
482    #[test]
483    fn from_base_one_err() {
484        let subject = "123".try_into().unwrap();
485        let err = Radix::try_base_from_str_and_i64(subject, 1).unwrap_err();
486        assert_eq!(err.exception_kind(), InvalidRadixExceptionKind::ArgumentError);
487
488        let subject = "0123".try_into().unwrap();
489        let err = Radix::try_base_from_str_and_i64(subject, 1).unwrap_err();
490        assert_eq!(err.exception_kind(), InvalidRadixExceptionKind::ArgumentError);
491
492        let subject = "0x123".try_into().unwrap();
493        let err = Radix::try_base_from_str_and_i64(subject, 1).unwrap_err();
494        assert_eq!(err.exception_kind(), InvalidRadixExceptionKind::ArgumentError);
495    }
496
497    #[test]
498    fn from_base_i32_min_no_prefix_err() {
499        let subject = "123".try_into().unwrap();
500        let err = Radix::try_base_from_str_and_i64(subject, i32::MIN.into()).unwrap_err();
501        assert_eq!(err.exception_kind(), InvalidRadixExceptionKind::ArgumentError);
502    }
503
504    #[test]
505    fn from_base_i32_min_with_prefix_ignored() {
506        let subject = "0123".try_into().unwrap();
507        let result = Radix::try_base_from_str_and_i64(subject, i32::MIN.into());
508        assert_eq!(result.unwrap(), None);
509
510        let subject = "0x123".try_into().unwrap();
511        let result = Radix::try_base_from_str_and_i64(subject, i32::MIN.into());
512        assert_eq!(result.unwrap(), None);
513    }
514
515    #[test]
516    fn from_base_negative_out_of_i32_range_min_with_prefix_err() {
517        let subject = "0d123".try_into().unwrap();
518        let err = Radix::try_base_from_str_and_i64(subject, -(2_i64.pow(39))).unwrap_err();
519
520        let mut buf = String::new();
521        write!(&mut buf, "{err}").unwrap();
522        assert_eq!(&*buf, "integer -549755813888 too small to convert to `int'");
523        assert_eq!(err.exception_kind(), InvalidRadixExceptionKind::RangeError);
524    }
525
526    #[test]
527    fn from_base_negative_out_of_range_err() {
528        let subject = "123".try_into().unwrap();
529        let err = Radix::try_base_from_str_and_i64(subject, -(2_i64.pow(21))).unwrap_err();
530
531        let mut buf = String::new();
532        write!(&mut buf, "{err}").unwrap();
533        assert_eq!(&*buf, "invalid radix 2097152");
534        assert_eq!(err.exception_kind(), InvalidRadixExceptionKind::ArgumentError);
535
536        let subject = "123".try_into().unwrap();
537        let err = Radix::try_base_from_str_and_i64(subject, -(2_i64.pow(31))).unwrap_err();
538
539        let mut buf = String::new();
540        write!(&mut buf, "{err}").unwrap();
541        assert_eq!(&*buf, "invalid radix -2147483648");
542        assert_eq!(err.exception_kind(), InvalidRadixExceptionKind::ArgumentError);
543    }
544
545    #[test]
546    fn from_base_negative_abs_is_valid() {
547        let subject = "111".try_into().unwrap();
548        let result = Radix::try_base_from_str_and_i64(subject, -2);
549        assert_eq!(result.unwrap(), Some(2));
550
551        let subject = "111".try_into().unwrap();
552        let result = Radix::try_base_from_str_and_i64(subject, -10);
553        assert_eq!(result.unwrap(), Some(10));
554
555        let subject = "111".try_into().unwrap();
556        let result = Radix::try_base_from_str_and_i64(subject, -36);
557        assert_eq!(result.unwrap(), Some(36));
558    }
559
560    #[test]
561    fn from_base_negative_abs_is_invalid_err() {
562        let subject = "111".try_into().unwrap();
563        let err = Radix::try_base_from_str_and_i64(subject, -500).unwrap_err();
564
565        let mut buf = String::new();
566        write!(&mut buf, "{err}").unwrap();
567        assert_eq!(&*buf, "invalid radix 500");
568        assert_eq!(err.exception_kind(), InvalidRadixExceptionKind::ArgumentError);
569
570        let subject = "111".try_into().unwrap();
571        let err = Radix::try_base_from_str_and_i64(subject, -49).unwrap_err();
572
573        let mut buf = String::new();
574        write!(&mut buf, "{err}").unwrap();
575        assert_eq!(&*buf, "invalid radix 49");
576        assert_eq!(err.exception_kind(), InvalidRadixExceptionKind::ArgumentError);
577    }
578
579    #[test]
580    fn from_base_positive_is_valid() {
581        let subject = "111".try_into().unwrap();
582        let result = Radix::try_base_from_str_and_i64(subject, 2);
583        assert_eq!(result.unwrap(), Some(2));
584
585        let subject = "111".try_into().unwrap();
586        let result = Radix::try_base_from_str_and_i64(subject, 10);
587        assert_eq!(result.unwrap(), Some(10));
588
589        let subject = "111".try_into().unwrap();
590        let result = Radix::try_base_from_str_and_i64(subject, 36);
591        assert_eq!(result.unwrap(), Some(36));
592    }
593
594    #[test]
595    fn from_base_positive_is_invalid_err() {
596        let subject = "111".try_into().unwrap();
597        let err = Radix::try_base_from_str_and_i64(subject, 500).unwrap_err();
598
599        let mut buf = String::new();
600        write!(&mut buf, "{err}").unwrap();
601        assert_eq!(&*buf, "invalid radix 500");
602        assert_eq!(err.exception_kind(), InvalidRadixExceptionKind::ArgumentError);
603
604        let subject = "111".try_into().unwrap();
605        let err = Radix::try_base_from_str_and_i64(subject, 49).unwrap_err();
606
607        let mut buf = String::new();
608        write!(&mut buf, "{err}").unwrap();
609        assert_eq!(&*buf, "invalid radix 49");
610        assert_eq!(err.exception_kind(), InvalidRadixExceptionKind::ArgumentError);
611    }
612
613    #[test]
614    fn from_base_too_big_i32() {
615        let subject = "111".try_into().unwrap();
616        let err = Radix::try_base_from_str_and_i64(subject, i64::from(i32::MAX) + 1_i64).unwrap_err();
617
618        let mut buf = String::new();
619        write!(&mut buf, "{err}").unwrap();
620        assert_eq!(&*buf, "integer 2147483648 too big to convert to `int'");
621        assert_eq!(err.exception_kind(), InvalidRadixExceptionKind::RangeError);
622
623        let subject = "111".try_into().unwrap();
624        let err = Radix::try_base_from_str_and_i64(subject, 2_i64.pow(32) + 1_i64).unwrap_err();
625
626        let mut buf = String::new();
627        write!(&mut buf, "{err}").unwrap();
628        assert_eq!(&*buf, "integer 4294967297 too big to convert to `int'");
629        assert_eq!(err.exception_kind(), InvalidRadixExceptionKind::RangeError);
630    }
631
632    #[test]
633    fn from_base_too_small_i32() {
634        let subject = "111".try_into().unwrap();
635        let err = Radix::try_base_from_str_and_i64(subject, i64::from(i32::MIN) - 1_i64).unwrap_err();
636
637        let mut buf = String::new();
638        write!(&mut buf, "{err}").unwrap();
639        assert_eq!(&*buf, "integer -2147483649 too small to convert to `int'");
640        assert_eq!(err.exception_kind(), InvalidRadixExceptionKind::RangeError);
641
642        let subject = "111".try_into().unwrap();
643        let err = Radix::try_base_from_str_and_i64(subject, -(2_i64).pow(32) - 1_i64).unwrap_err();
644
645        let mut buf = String::new();
646        write!(&mut buf, "{err}").unwrap();
647        assert_eq!(&*buf, "integer -4294967297 too small to convert to `int'");
648        assert_eq!(err.exception_kind(), InvalidRadixExceptionKind::RangeError);
649    }
650
651    #[test]
652    fn negative_radix_with_inline_base_and_leading_spaces_ignores() {
653        // ```console
654        // [3.1.2] > Integer "                  0123", -6
655        // => 83
656        // [3.1.2] > Integer "                  0x123", -6
657        // => 291
658        // ```
659        let subject = "                  0123".try_into().unwrap();
660        let radix = Radix::try_base_from_str_and_i64(subject, -6).unwrap();
661        assert_eq!(radix, None);
662
663        let subject = "                  0x123".try_into().unwrap();
664        let radix = Radix::try_base_from_str_and_i64(subject, -6).unwrap();
665        assert_eq!(radix, None);
666    }
667}