1use core::num::NonZeroU32;
2
3use crate::error::{InvalidRadixError, InvalidRadixErrorKind};
4use crate::subject::IntegerString;
5use crate::whitespace;
6
7const fn radix_table() -> [u32; 256] {
10 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#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
43pub struct Radix(NonZeroU32);
44
45impl Default for Radix {
46 fn default() -> Self {
62 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 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 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 #[must_use]
145 pub fn new(radix: u32) -> Option<Self> {
146 radix.try_into().ok()
147 }
148
149 #[must_use]
164 pub const unsafe fn new_unchecked(radix: u32) -> Self {
165 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 Ok(0 | -1) => Ok(None),
190 Ok(1) => Err(InvalidRadixErrorKind::Invalid(num).into()),
205 Ok(_) if num < 0 && matches!(whitespace::trim_leading(subject.as_bytes()).first(), Some(&b'0')) => {
225 Ok(None)
226 }
227 Ok(i32::MIN) => Err(InvalidRadixErrorKind::Invalid(num).into()),
236 Ok(radix) => {
237 let radix = match u32::try_from(radix) {
246 Ok(radix) => radix,
247 Err(_) if (-36..=-2).contains(&radix) => (-radix).try_into().expect("radix is in range for u32"),
254 Err(_) => {
265 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 Err(InvalidRadixErrorKind::Invalid(num).into())
283 }
284 }
285 Err(_) if num > i32::MAX.into() => Err(InvalidRadixErrorKind::TooBig(num).into()),
294 Err(_) if num < i32::MIN.into() => Err(InvalidRadixErrorKind::TooSmall(num).into()),
303 Err(_) => unreachable!("all cases covered"),
304 }
305 }
306
307 #[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 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 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}