scolapasta_int_parse/
lib.rs

1#![warn(clippy::all, clippy::pedantic, clippy::undocumented_unsafe_blocks)]
2#![allow(
3    clippy::let_underscore_untyped,
4    reason = "https://github.com/rust-lang/rust-clippy/pull/10442#issuecomment-1516570154"
5)]
6#![allow(
7    clippy::question_mark,
8    reason = "https://github.com/rust-lang/rust-clippy/issues/8281"
9)]
10#![allow(clippy::manual_let_else, reason = "manual_let_else was very buggy on release")]
11#![allow(clippy::missing_errors_doc, reason = "A lot of existing code fails this lint")]
12#![allow(
13    clippy::module_name_repetitions,
14    reason = "incompatible with how code is organized in private modules"
15)]
16#![allow(
17    clippy::unnecessary_lazy_evaluations,
18    reason = "https://github.com/rust-lang/rust-clippy/issues/8109"
19)]
20#![cfg_attr(
21    test,
22    allow(clippy::non_ascii_literal, reason = "tests sometimes require UTF-8 string content")
23)]
24#![allow(unknown_lints)]
25#![warn(
26    missing_copy_implementations,
27    missing_debug_implementations,
28    missing_docs,
29    rust_2024_compatibility,
30    trivial_casts,
31    trivial_numeric_casts,
32    unused_qualifications,
33    variant_size_differences
34)]
35// Enable feature callouts in generated documentation:
36// https://doc.rust-lang.org/beta/unstable-book/language-features/doc-cfg.html
37//
38// This approach is borrowed from tokio.
39#![cfg_attr(docsrs, feature(doc_cfg))]
40#![cfg_attr(docsrs, feature(doc_alias))]
41
42//! Parse a given byte string and optional radix into an [`i64`].
43//!
44//! [`parse`] wraps [`i64::from_str_radix`] by normalizing the input byte string:
45//!
46//! - Assert the byte string is ASCII and does not contain NUL bytes.
47//! - Parse the radix to ensure it is in range and valid for the given input
48//!   byte string.
49//! - Trim leading whitespace.
50//! - Accept a single, optional `+` or `-` sign byte.
51//! - Parse a literal radix out of the string from one of `0b`, `0B`, `0o`,
52//!   `0O`, `0d`, `0D`, `0x`, or `0X`. If the given radix is `Some(_)`, the
53//!   radix must match the embedded radix literal. A `0` prefix of arbitrary
54//!   length is interpreted as an octal literal.
55//! - Remove ("squeeze") leading zeros.
56//! - Collect ASCII alphanumeric bytes and filter out underscores.
57//!
58//! The functions and types in this crate can be used to implement
59//! [`Kernel#Integer`] in Ruby.
60//!
61//! [`Kernel#Integer`]: https://ruby-doc.org/core-3.1.2/Kernel.html#method-i-Integer
62
63#![no_std]
64
65// Ensure code blocks in `README.md` compile
66#[cfg(doctest)]
67#[doc = include_str!("../README.md")]
68mod readme {}
69
70extern crate alloc;
71#[cfg(feature = "std")]
72extern crate std;
73
74mod error;
75mod parser;
76mod radix;
77mod subject;
78mod whitespace;
79
80pub use error::{ArgumentError, Error, InvalidRadixError, InvalidRadixExceptionKind};
81use parser::{Sign, State as ParseState};
82use radix::RADIX_TABLE;
83pub use radix::Radix;
84use subject::IntegerString;
85
86/// Parse a given byte string and optional [`Radix`] into an [`i64`].
87///
88/// This function wraps [`i64::from_str_radix`] by normalizing the input byte
89/// string:
90///
91/// - Assert the byte string is ASCII and does not contain NUL bytes.
92/// - Parse the radix to ensure it is in range and valid for the given input
93///   byte string.
94/// - Trim leading and trailing whitespace.
95/// - Accept a single, optional `+` or `-` sign byte.
96/// - Parse a literal radix out of the string from one of `0b`, `0B`, `0o`,
97///   `0O`, `0d`, `0D`, `0x`, or `0X`. If the given radix is `Some(_)`, the
98///   radix must match the embedded radix literal. A `0` prefix of arbitrary
99///   length is interpreted as an octal literal.
100/// - Remove ("squeeze") leading zeros.
101/// - Collect ASCII alphanumeric bytes and filter out underscores.
102/// - Pass the collected ASCII alphanumeric bytes to [`i64::from_str_radix`].
103///
104/// If the given radix argument is [`None`] the input byte string is either
105/// parsed with the radix embedded within it (e.g. `0x...` is base 16) or
106/// defaults to base 10.
107///
108/// # Errors
109///
110/// This function can return an error in the following circumstances:
111///
112/// - The input byte string has non-ASCII bytes.
113/// - The input byte string contains a NUL byte.
114/// - The input byte string is the empty byte slice.
115/// - The input byte string only contains +/- signs.
116/// - The given radix does not match a `0x`-style prefix.
117/// - Invalid or duplicate +/- signs are in the input.
118/// - Consecutive underscores are present in the input.
119/// - Leading or trailing underscores are present in the input.
120/// - The input contains ASCII alphanumeric bytes that are invalid for the
121///   computed radix.
122/// - The input radix is out of range of [`i32`].
123/// - The input radix is negative (if the input byte string does not have an
124///   `0x`-style prefix) and out of range `-36..=-2`.
125/// - The input radix is out of range of `2..=36`.
126///
127/// See [`ArgumentError`] and [`InvalidRadixError`] for more details.
128///
129/// # Examples
130///
131/// ```
132/// # use scolapasta_int_parse::{Error, parse};
133/// # fn example() -> Result<(), Error<'static>> {
134/// let int_max = parse("9_223_372_036_854_775_807", None)?;
135/// assert_eq!(int_max, i64::MAX);
136///
137/// let deadbeef = parse("                       0x000000000deadbeef", None)?;
138/// assert_eq!(deadbeef, 3_735_928_559);
139///
140/// let octal = parse("000123", None)?;
141/// assert_eq!(octal, 83);
142///
143/// let negative = parse("-199", None)?;
144/// assert_eq!(negative, -199);
145///
146/// let positive = parse("+199", None)?;
147/// assert_eq!(positive, 199);
148/// # Ok(())
149/// # }
150/// # example().unwrap();
151/// ```
152///
153/// If a `Some(_)` radix is given, that radix is used:
154///
155/// ```
156/// # use scolapasta_int_parse::{Error, parse};
157/// # fn example() -> Result<(), Error<'static>> {
158/// let num = parse("32xyz", Some(36))?;
159/// assert_eq!(num, 5_176_187);
160///
161/// let binary = parse("1100_0011", Some(2))?;
162/// assert_eq!(binary, 195);
163/// # Ok(())
164/// # }
165/// # example().unwrap();
166/// ```
167///
168/// If a `Some(_)` radix is given and it does not match the embedded radix, an
169/// error is returned:
170///
171/// ```
172/// # use scolapasta_int_parse::parse;
173/// let result = parse("0b1100_0011", Some(12));
174/// assert!(result.is_err());
175/// ```
176pub fn parse<T>(subject: &T, radix: Option<i64>) -> Result<i64, Error<'_>>
177where
178    T: AsRef<[u8]> + ?Sized,
179{
180    let subject = subject.as_ref();
181    parse_inner(subject, radix)
182}
183
184fn parse_inner(subject: &[u8], radix: Option<i64>) -> Result<i64, Error<'_>> {
185    // Phase 1: Ensure ASCII, ensure no NUL bytes.
186    let subject = IntegerString::try_from(subject)?;
187    // Phase 2: Parse radix
188    let radix = if let Some(radix) = radix {
189        Radix::try_base_from_str_and_i64(subject, radix)?
190    } else {
191        None
192    };
193    let mut state = ParseState::new(subject);
194
195    // Phase 3: Trim leading and trailing whitespace.
196    let mut chars = whitespace::trim(subject.as_bytes()).iter().copied().peekable();
197
198    // Phase 4: Set sign.
199    match chars.peek() {
200        Some(b'+') => {
201            state = state.set_sign(Sign::Positive)?;
202            chars.next();
203        }
204        Some(b'-') => {
205            state = state.set_sign(Sign::Negative)?;
206            chars.next();
207        }
208        Some(_) => {}
209        None => return Err(subject.into()),
210    }
211
212    // Phase 5: Determine radix.
213    let radix = match chars.peek() {
214        // https://github.com/ruby/ruby/blob/v3_1_2/bignum.c#L4094-L4115
215        Some(b'0') => {
216            chars.next();
217            match (chars.peek(), radix) {
218                (Some(b'b' | b'B'), None | Some(2)) => {
219                    chars.next();
220                    2
221                }
222                (Some(b'o' | b'O'), None | Some(8)) => {
223                    chars.next();
224                    8
225                }
226                (Some(b'd' | b'D'), None | Some(10)) => {
227                    chars.next();
228                    10
229                }
230                (Some(b'x' | b'X'), None | Some(16)) => {
231                    chars.next();
232                    16
233                }
234                (Some(b'b' | b'B' | b'o' | b'O' | b'd' | b'D' | b'x' | b'X'), Some(_)) => return Err(subject.into()),
235                (None, _) => return Ok(0),
236                (Some(_), None) => 8,
237                (Some(_), Some(radix)) => radix,
238            }
239        }
240        Some(_) => radix.unwrap_or(10),
241        None => return Err(subject.into()),
242    };
243
244    // Phase 6: Squeeze leading zeros, reject invalid underscore sequences.
245    loop {
246        if chars.next_if_eq(&b'0').is_some() {
247            if chars.next_if_eq(&b'_').is_some() {
248                match chars.peek() {
249                    None | Some(b'_') => return Err(subject.into()),
250                    Some(_) => {}
251                }
252            }
253        } else if let Some(b'_') = chars.peek() {
254            return Err(subject.into());
255        } else {
256            break;
257        }
258    }
259
260    // Phase 7: Collect ASCII alphanumeric digits, reject invalid underscore
261    // sequences.
262    loop {
263        match chars.next() {
264            Some(b'_') => match chars.peek() {
265                None | Some(b'_') => return Err(subject.into()),
266                Some(_) => {}
267            },
268            Some(b) if RADIX_TABLE[usize::from(b)] <= radix => {
269                state = state.collect_digit(b);
270            }
271            Some(_) => return Err(subject.into()),
272            None => break,
273        }
274    }
275
276    // Phase 8: Parse (signed) ASCII alphanumeric string to an `i64`.
277    let src = state.into_numeric_string()?;
278    i64::from_str_radix(&src, radix).map_err(|_| subject.into())
279}
280
281#[cfg(test)]
282mod tests {
283    use crate::parse;
284
285    #[test]
286    fn parse_int_max() {
287        let result = parse("9_223_372_036_854_775_807", None);
288        assert_eq!(result.unwrap(), i64::MAX);
289        let result = parse("+9_223_372_036_854_775_807", None);
290        assert_eq!(result.unwrap(), i64::MAX);
291    }
292
293    #[test]
294    fn parse_int_min() {
295        let result = parse("-9_223_372_036_854_775_808", None);
296        assert_eq!(result.unwrap(), i64::MIN);
297    }
298
299    #[test]
300    fn leading_zero_does_not_imply_octal_when_given_radix() {
301        // ```
302        // [3.1.2] > parse('017', 12)
303        // => 19
304        // [3.1.2] > parse('-017', 12)
305        // => -19
306        // ```
307        let result = parse("017", Some(12));
308        assert_eq!(result.unwrap(), 19);
309        let result = parse("-017", Some(12));
310        assert_eq!(result.unwrap(), -19);
311    }
312
313    #[test]
314    fn squeeze_leading_zeros() {
315        let result = parse("0x0000000000000011", Some(16));
316        assert_eq!(result.unwrap(), 17);
317        let result = parse("-0x0000000000000011", Some(16));
318        assert_eq!(result.unwrap(), -17);
319
320        let result = parse("0x00_00000000000011", Some(16));
321        assert_eq!(result.unwrap(), 17);
322        let result = parse("-0x00_00000000000011", Some(16));
323        assert_eq!(result.unwrap(), -17);
324
325        let result = parse("0x0_0_0_11", Some(16));
326        assert_eq!(result.unwrap(), 17);
327        let result = parse("-0x0_0_0_11", Some(16));
328        assert_eq!(result.unwrap(), -17);
329
330        let result = parse("-0x00000_15", Some(16));
331        assert_eq!(result.unwrap(), -21);
332    }
333
334    #[test]
335    fn squeeze_leading_zeros_is_octal_when_octal_digits() {
336        let result = parse("000000000000000000000000000000000000000123", None);
337        assert_eq!(result.unwrap(), 83);
338    }
339
340    #[test]
341    fn squeeze_leading_is_invalid_when_non_octal_digits() {
342        parse("000000000000000000000000000000000000000987", None).unwrap_err();
343    }
344
345    #[test]
346    fn squeeze_leading_zeros_enforces_no_double_underscore() {
347        parse("0x___11", Some(16)).unwrap_err();
348        parse("-0x___11", Some(16)).unwrap_err();
349        parse("0x0___11", Some(16)).unwrap_err();
350        parse("-0x0___11", Some(16)).unwrap_err();
351        parse("0x_0__11", Some(16)).unwrap_err();
352        parse("-0x_0__11", Some(16)).unwrap_err();
353        parse("0x_00__11", Some(16)).unwrap_err();
354        parse("-0x_00__11", Some(16)).unwrap_err();
355    }
356
357    #[test]
358    fn no_digits_with_base_prefix() {
359        parse("0x", None).unwrap_err();
360        parse("0b", None).unwrap_err();
361        parse("0o", None).unwrap_err();
362        parse("o", None).unwrap_err();
363        parse("0d", None).unwrap_err();
364        parse("0X", None).unwrap_err();
365        parse("0B", None).unwrap_err();
366        parse("0O", None).unwrap_err();
367        parse("O", None).unwrap_err();
368        parse("0D", None).unwrap_err();
369    }
370
371    #[test]
372    fn no_digits_with_base_prefix_neg() {
373        parse("-0x", None).unwrap_err();
374        parse("-0b", None).unwrap_err();
375        parse("-0o", None).unwrap_err();
376        parse("-o", None).unwrap_err();
377        parse("-0d", None).unwrap_err();
378        parse("-0X", None).unwrap_err();
379        parse("-0B", None).unwrap_err();
380        parse("-0O", None).unwrap_err();
381        parse("-O", None).unwrap_err();
382        parse("-0D", None).unwrap_err();
383    }
384
385    #[test]
386    fn no_digits_with_invalid_base_prefix() {
387        parse("0z", None).unwrap_err();
388        parse("0z", Some(12)).unwrap_err();
389    }
390
391    #[test]
392    fn no_digits_with_invalid_base_prefix_neg() {
393        parse("-0z", None).unwrap_err();
394        parse("-0z", Some(12)).unwrap_err();
395    }
396
397    #[test]
398    fn binary_alpha_requires_zero_prefix() {
399        parse("B1", None).unwrap_err();
400        parse("b1", None).unwrap_err();
401    }
402
403    #[test]
404    fn binary_parses() {
405        let result = parse("0B1111", None);
406        assert_eq!(result.unwrap(), 15);
407        let result = parse("0b1111", None);
408        assert_eq!(result.unwrap(), 15);
409        let result = parse("-0B1111", None);
410        assert_eq!(result.unwrap(), -15);
411        let result = parse("-0b1111", None);
412        assert_eq!(result.unwrap(), -15);
413    }
414
415    #[test]
416    fn binary_with_given_2_radix_parses() {
417        let result = parse("0B1111", Some(2));
418        assert_eq!(result.unwrap(), 15);
419        let result = parse("0b1111", Some(2));
420        assert_eq!(result.unwrap(), 15);
421        let result = parse("-0B1111", Some(2));
422        assert_eq!(result.unwrap(), -15);
423        let result = parse("-0b1111", Some(2));
424        assert_eq!(result.unwrap(), -15);
425    }
426
427    #[test]
428    fn binary_with_mismatched_radix_is_err() {
429        parse("0B1111", Some(24)).unwrap_err();
430        parse("0b1111", Some(24)).unwrap_err();
431        parse("-0B1111", Some(24)).unwrap_err();
432        parse("-0b1111", Some(24)).unwrap_err();
433    }
434
435    #[test]
436    fn binary_with_digits_out_of_radix_is_err() {
437        parse("0B1111AH", None).unwrap_err();
438        parse("0b1111ah", None).unwrap_err();
439    }
440
441    #[test]
442    fn octal_alpha_requires_zero_prefix() {
443        parse("O7", None).unwrap_err();
444        parse("o7", None).unwrap_err();
445    }
446
447    #[test]
448    fn octal_parses() {
449        let result = parse("0O17", None);
450        assert_eq!(result.unwrap(), 15);
451        let result = parse("0o17", None);
452        assert_eq!(result.unwrap(), 15);
453        let result = parse("-0O17", None);
454        assert_eq!(result.unwrap(), -15);
455        let result = parse("-0o17", None);
456        assert_eq!(result.unwrap(), -15);
457    }
458
459    #[test]
460    fn octal_with_given_8_radix_parses() {
461        let result = parse("0O17", Some(8));
462        assert_eq!(result.unwrap(), 15);
463        let result = parse("0o17", Some(8));
464        assert_eq!(result.unwrap(), 15);
465        let result = parse("-0O17", Some(8));
466        assert_eq!(result.unwrap(), -15);
467        let result = parse("-0o17", Some(8));
468        assert_eq!(result.unwrap(), -15);
469    }
470
471    #[test]
472    fn octal_no_alpha_parses() {
473        let result = parse("017", None);
474        assert_eq!(result.unwrap(), 15);
475        let result = parse("-017", None);
476        assert_eq!(result.unwrap(), -15);
477    }
478
479    #[test]
480    fn octal_no_alpha_with_given_8_radix_parses() {
481        let result = parse("017", Some(8));
482        assert_eq!(result.unwrap(), 15);
483        let result = parse("-017", Some(8));
484        assert_eq!(result.unwrap(), -15);
485    }
486
487    #[test]
488    fn octal_with_mismatched_radix_is_err() {
489        parse("0O17", Some(24)).unwrap_err();
490        parse("0o17", Some(24)).unwrap_err();
491        parse("-0O17", Some(24)).unwrap_err();
492        parse("-0o17", Some(24)).unwrap_err();
493    }
494
495    #[test]
496    fn octal_with_digits_out_of_radix_is_err() {
497        parse("0O17AH", None).unwrap_err();
498        parse("0o17ah", None).unwrap_err();
499    }
500
501    #[test]
502    fn decimal_alpha_requires_zero_prefix() {
503        parse("D9", None).unwrap_err();
504        parse("d9", None).unwrap_err();
505    }
506
507    #[test]
508    fn decimal_parses() {
509        let result = parse("0D15", None);
510        assert_eq!(result.unwrap(), 15);
511        let result = parse("0d15", None);
512        assert_eq!(result.unwrap(), 15);
513        let result = parse("-0D15", None);
514        assert_eq!(result.unwrap(), -15);
515        let result = parse("-0d15", None);
516        assert_eq!(result.unwrap(), -15);
517    }
518
519    #[test]
520    fn decimal_with_given_10_radix_parses() {
521        let result = parse("0D15", Some(10));
522        assert_eq!(result.unwrap(), 15);
523        let result = parse("0d15", Some(10));
524        assert_eq!(result.unwrap(), 15);
525        let result = parse("-0D15", Some(10));
526        assert_eq!(result.unwrap(), -15);
527        let result = parse("-0d15", Some(10));
528        assert_eq!(result.unwrap(), -15);
529    }
530
531    #[test]
532    fn decimal_with_mismatched_radix_is_err() {
533        parse("0D15", Some(24)).unwrap_err();
534        parse("0d15", Some(24)).unwrap_err();
535        parse("-0D15", Some(24)).unwrap_err();
536        parse("-0d15", Some(24)).unwrap_err();
537    }
538
539    #[test]
540    fn decimal_with_digits_out_of_radix_is_err() {
541        parse("0D15AH", None).unwrap_err();
542        parse("0d15ah", None).unwrap_err();
543    }
544
545    #[test]
546    fn hex_alpha_requires_zero_prefix() {
547        parse("XF", None).unwrap_err();
548        parse("xF", None).unwrap_err();
549        parse("Xf", None).unwrap_err();
550        parse("xf", None).unwrap_err();
551    }
552
553    #[test]
554    fn hex_parses() {
555        let result = parse("0XF", None);
556        assert_eq!(result.unwrap(), 15);
557        let result = parse("0xF", None);
558        assert_eq!(result.unwrap(), 15);
559        let result = parse("-0XF", None);
560        assert_eq!(result.unwrap(), -15);
561        let result = parse("-0xF", None);
562        assert_eq!(result.unwrap(), -15);
563        let result = parse("0Xf", None);
564        assert_eq!(result.unwrap(), 15);
565        let result = parse("0xf", None);
566        assert_eq!(result.unwrap(), 15);
567        let result = parse("-0Xf", None);
568        assert_eq!(result.unwrap(), -15);
569        let result = parse("-0xf", None);
570        assert_eq!(result.unwrap(), -15);
571    }
572
573    #[test]
574    fn hex_with_given_16_radix_parses() {
575        let result = parse("0XF", Some(16));
576        assert_eq!(result.unwrap(), 15);
577        let result = parse("0xF", Some(16));
578        assert_eq!(result.unwrap(), 15);
579        let result = parse("-0XF", Some(16));
580        assert_eq!(result.unwrap(), -15);
581        let result = parse("-0xF", Some(16));
582        assert_eq!(result.unwrap(), -15);
583        let result = parse("0Xf", Some(16));
584        assert_eq!(result.unwrap(), 15);
585        let result = parse("0xf", Some(16));
586        assert_eq!(result.unwrap(), 15);
587        let result = parse("-0Xf", Some(16));
588        assert_eq!(result.unwrap(), -15);
589        let result = parse("-0xf", Some(16));
590        assert_eq!(result.unwrap(), -15);
591    }
592
593    #[test]
594    fn hex_with_mismatched_radix_is_err() {
595        parse("0XF", Some(24)).unwrap_err();
596        parse("0xF", Some(24)).unwrap_err();
597        parse("0Xf", Some(24)).unwrap_err();
598        parse("0xf", Some(24)).unwrap_err();
599        parse("-0XF", Some(24)).unwrap_err();
600        parse("-0xF", Some(24)).unwrap_err();
601        parse("-0Xf", Some(24)).unwrap_err();
602        parse("-0xf", Some(24)).unwrap_err();
603    }
604
605    #[test]
606    fn hex_with_digits_out_of_radix_is_err() {
607        parse("0XFAH", None).unwrap_err();
608        parse("0xFah", None).unwrap_err();
609        parse("0XfAH", None).unwrap_err();
610        parse("0xfah", None).unwrap_err();
611    }
612
613    #[test]
614    fn digits_out_of_radix_is_err() {
615        parse("17AH", Some(12)).unwrap_err();
616        parse("17ah", Some(12)).unwrap_err();
617        parse("17AH", None).unwrap_err();
618        parse("17ah", None).unwrap_err();
619    }
620
621    #[test]
622    fn parsing_is_case_insensitive() {
623        // ```
624        // [3.1.2] > parse('abcdefgxyz', 36)
625        // => 1047601316316923
626        // [3.1.2] > parse('abcdefgxyz'.upcase, 36)
627        // => 1047601316316923
628        // ```
629        let result = parse("abcdefgxyz", Some(36));
630        assert_eq!(result.unwrap(), 1_047_601_316_316_923);
631        let result = parse("ABCDEFGXYZ", Some(36));
632        assert_eq!(result.unwrap(), 1_047_601_316_316_923);
633    }
634
635    #[test]
636    fn leading_underscore_is_err() {
637        parse("0x_0000001234567", None).unwrap_err();
638        parse("0_x0000001234567", None).unwrap_err();
639        parse("___0x0000001234567", None).unwrap_err();
640    }
641
642    #[test]
643    fn double_underscore_is_err() {
644        parse("0x111__11", None).unwrap_err();
645    }
646
647    #[test]
648    fn trailing_underscore_is_err() {
649        parse("0x111_11_", None).unwrap_err();
650        parse("0x00000_", None).unwrap_err();
651    }
652
653    #[test]
654    fn all_spaces_is_err() {
655        parse("    ", None).unwrap_err();
656    }
657
658    #[test]
659    fn empty_is_err() {
660        parse("", None).unwrap_err();
661    }
662
663    #[test]
664    fn more_than_one_sign_is_err() {
665        parse("++12", None).unwrap_err();
666        parse("+-12", None).unwrap_err();
667        parse("-+12", None).unwrap_err();
668        parse("--12", None).unwrap_err();
669    }
670
671    #[test]
672    fn zero_radix_is_default() {
673        // ```
674        // [3.1.2] > Integer "0x111", 0
675        // => 273
676        // [3.1.2] > Integer "111", 0
677        // => 111
678        // ```
679        let result = parse("0x111", Some(0));
680        assert_eq!(result.unwrap(), 273);
681        let result = parse("111", Some(0));
682        assert_eq!(result.unwrap(), 111);
683    }
684
685    #[test]
686    fn negative_one_radix_is_default() {
687        // ```
688        // [3.1.2] > Integer('0x123f'.upcase, -1)
689        // => 4671
690        // [3.1.2] > Integer('0x123f'.upcase, 16)
691        // => 4671
692        // [3.1.2] > Integer "111", -1
693        // => 111
694        // ```
695        let result = parse("0x123f", Some(-1));
696        assert_eq!(result.unwrap(), 4671);
697        let result = parse("111", Some(-1));
698        assert_eq!(result.unwrap(), 111);
699    }
700
701    #[test]
702    fn one_radix_is_err() {
703        parse("0x123f", Some(1)).unwrap_err();
704        parse("111", Some(1)).unwrap_err();
705    }
706
707    #[test]
708    fn out_of_range_radix_is_err() {
709        parse("0x123f", Some(1200)).unwrap_err();
710        parse("123", Some(1200)).unwrap_err();
711        parse("123", Some(-1200)).unwrap_err();
712    }
713
714    #[test]
715    fn literals_with_negative_out_of_range_radix_ignore_radix() {
716        let result = parse("0x123f", Some(-1200));
717        assert_eq!(result.unwrap(), 4671);
718    }
719
720    #[test]
721    fn negative_radix_in_valid_range_is_parsed() {
722        // ```
723        // [3.1.2] > Integer "111", -2
724        // => 7
725        // [3.1.2] > Integer "111", -10
726        // => 111
727        // [3.1.2] > Integer "111", -36
728        // => 1333
729        // ```
730        let result = parse("111", Some(-2));
731        assert_eq!(result.unwrap(), 7);
732        let result = parse("111", Some(-10));
733        assert_eq!(result.unwrap(), 111);
734        let result = parse("111", Some(-36));
735        assert_eq!(result.unwrap(), 1333);
736    }
737
738    #[test]
739    fn all_valid_radixes() {
740        // ```
741        // (2..36).each {|r| puts "(\"111\", #{r}, #{Integer "111", r}),"; nil }.to_a.uniq
742        // (2..36).each {|r| puts "(\"111\", #{-r}, #{Integer "111", -r}),"; nil }.to_a.uniq
743        // ```
744        let test_cases = [
745            ("111", 2, 7),
746            ("111", 3, 13),
747            ("111", 4, 21),
748            ("111", 5, 31),
749            ("111", 6, 43),
750            ("111", 7, 57),
751            ("111", 8, 73),
752            ("111", 9, 91),
753            ("111", 10, 111),
754            ("111", 11, 133),
755            ("111", 12, 157),
756            ("111", 13, 183),
757            ("111", 14, 211),
758            ("111", 15, 241),
759            ("111", 16, 273),
760            ("111", 17, 307),
761            ("111", 18, 343),
762            ("111", 19, 381),
763            ("111", 20, 421),
764            ("111", 21, 463),
765            ("111", 22, 507),
766            ("111", 23, 553),
767            ("111", 24, 601),
768            ("111", 25, 651),
769            ("111", 26, 703),
770            ("111", 27, 757),
771            ("111", 28, 813),
772            ("111", 29, 871),
773            ("111", 30, 931),
774            ("111", 31, 993),
775            ("111", 32, 1057),
776            ("111", 33, 1123),
777            ("111", 34, 1191),
778            ("111", 35, 1261),
779            ("111", 36, 1333),
780            ("111", -2, 7),
781            ("111", -3, 13),
782            ("111", -4, 21),
783            ("111", -5, 31),
784            ("111", -6, 43),
785            ("111", -7, 57),
786            ("111", -8, 73),
787            ("111", -9, 91),
788            ("111", -10, 111),
789            ("111", -11, 133),
790            ("111", -12, 157),
791            ("111", -13, 183),
792            ("111", -14, 211),
793            ("111", -15, 241),
794            ("111", -16, 273),
795            ("111", -17, 307),
796            ("111", -18, 343),
797            ("111", -19, 381),
798            ("111", -20, 421),
799            ("111", -21, 463),
800            ("111", -22, 507),
801            ("111", -23, 553),
802            ("111", -24, 601),
803            ("111", -25, 651),
804            ("111", -26, 703),
805            ("111", -27, 757),
806            ("111", -28, 813),
807            ("111", -29, 871),
808            ("111", -30, 931),
809            ("111", -31, 993),
810            ("111", -32, 1057),
811            ("111", -33, 1123),
812            ("111", -34, 1191),
813            ("111", -35, 1261),
814            ("111", -36, 1333),
815        ];
816        for (subject, radix, output) in test_cases {
817            let result = parse(subject, Some(radix));
818            assert_eq!(
819                result.unwrap(),
820                output,
821                "Mismatched output for test case ({subject}, {radix}, {output})"
822            );
823        }
824    }
825
826    #[test]
827    fn int_max_radix_does_not_panic() {
828        parse("111", Some(i64::MAX)).unwrap_err();
829    }
830
831    #[test]
832    fn int_min_radix_does_not_panic() {
833        parse("111", Some(i64::MIN)).unwrap_err();
834    }
835
836    #[test]
837    fn decimal_zero() {
838        let result = parse("0", None);
839        assert_eq!(result.unwrap(), 0);
840        let result = parse("0", Some(2));
841        assert_eq!(result.unwrap(), 0);
842        let result = parse("0", Some(8));
843        assert_eq!(result.unwrap(), 0);
844        let result = parse("0", Some(10));
845        assert_eq!(result.unwrap(), 0);
846        let result = parse("0", Some(16));
847        assert_eq!(result.unwrap(), 0);
848        let result = parse("0", Some(36));
849        assert_eq!(result.unwrap(), 0);
850    }
851
852    #[test]
853    fn decimal_zero_whitespace() {
854        let result = parse("0 ", None);
855        assert_eq!(result.unwrap(), 0);
856        let result = parse(" 0", None);
857        assert_eq!(result.unwrap(), 0);
858        let result = parse(" 0 ", None);
859        assert_eq!(result.unwrap(), 0);
860    }
861
862    #[test]
863    fn trailing_whitespace() {
864        let result = parse("1 ", None);
865        assert_eq!(result.unwrap(), 1);
866    }
867
868    #[test]
869    fn all_ascii_whitespace_is_trimmed_from_end() {
870        // ```
871        // [3.1.2] > Integer "93 "
872        // => 93
873        // [3.1.2] > Integer "93\n"
874        // => 93
875        // [3.1.2] > Integer "93\t"
876        // => 93
877        // [3.1.2] > Integer "93\u000A"
878        // => 93
879        // [3.1.2] > Integer "93\u000C"
880        // => 93
881        // [3.1.2] > Integer "93\u000D"
882        // => 93
883        // ```
884        let result = parse("93 ", None);
885        assert_eq!(result.unwrap(), 93);
886        let result = parse("93\n", None);
887        assert_eq!(result.unwrap(), 93);
888        let result = parse("93\t", None);
889        assert_eq!(result.unwrap(), 93);
890        let result = parse("93\u{000A}", None);
891        assert_eq!(result.unwrap(), 93);
892        let result = parse("93\u{000C}", None);
893        assert_eq!(result.unwrap(), 93);
894        let result = parse("93\u{000D}", None);
895        assert_eq!(result.unwrap(), 93);
896    }
897
898    #[test]
899    fn all_ascii_whitespace_is_trimmed_from_start() {
900        // ```
901        // [3.1.2] > Integer " 93"
902        // => 93
903        // [3.1.2] > Integer "\n93"
904        // => 93
905        // [3.1.2] > Integer "\t93"
906        // => 93
907        // [3.1.2] > Integer "\u000A93"
908        // => 93
909        // [3.1.2] > Integer "\u000C93"
910        // => 93
911        // [3.1.2] > Integer "\u000D93"
912        // => 93
913        // ```
914        let result = parse(" 93", None);
915        assert_eq!(result.unwrap(), 93);
916        let result = parse("\n93", None);
917        assert_eq!(result.unwrap(), 93);
918        let result = parse("\t93", None);
919        assert_eq!(result.unwrap(), 93);
920        let result = parse("\u{000A}93", None);
921        assert_eq!(result.unwrap(), 93);
922        let result = parse("\u{000C}93", None);
923        assert_eq!(result.unwrap(), 93);
924        let result = parse("\u{000D}93", None);
925        assert_eq!(result.unwrap(), 93);
926    }
927
928    #[test]
929    fn inputs_with_both_leading_and_trailing_whitespace_are_parsed() {
930        let result = parse("  93  ", None);
931        assert_eq!(result.unwrap(), 93);
932        let result = parse("\n 93 \n", None);
933        assert_eq!(result.unwrap(), 93);
934        let result = parse("\t 93 \t", None);
935        assert_eq!(result.unwrap(), 93);
936        let result = parse("\u{000A} 93 \u{000A}", None);
937        assert_eq!(result.unwrap(), 93);
938        let result = parse("\u{000C} 93 \u{000C}", None);
939        assert_eq!(result.unwrap(), 93);
940        let result = parse("\u{000D} 93 \u{000D}", None);
941        assert_eq!(result.unwrap(), 93);
942    }
943
944    #[test]
945    fn negative_radix_leading_whitespace() {
946        // ```
947        // [3.1.2] > Integer "                  0123", -6
948        // => 83
949        // [3.1.2] > Integer "                  0x123", -6
950        // => 291
951        // ```
952        let result = parse("                  0123", Some(-6));
953        assert_eq!(result.unwrap(), 83);
954        let result = parse("                  0x123", Some(-6));
955        assert_eq!(result.unwrap(), 291);
956    }
957
958    #[test]
959    fn trim_vertical_tab() {
960        // ```
961        // [3.1.2] > Integer "    \x0B 27"
962        // => 27
963        // ```
964        let result = parse(b"    \x0B 27", None);
965        assert_eq!(result.unwrap(), 27);
966    }
967}