1#![cfg_attr(not(feature = "std"), no_std)]
83#![warn(missing_docs)]
84#![warn(clippy::std_instead_of_core)]
85#![warn(clippy::print_stderr)]
86#![warn(clippy::print_stdout)]
87
88#[cfg(not(feature = "std"))]
89extern crate alloc;
90
91use core::error::Error;
92use core::fmt;
93use core::num::NonZero;
94use core::str::FromStr;
95
96pub const MIN: u16 = 1;
98pub const MAX: u16 = 3_999;
100
101#[derive(Debug, Clone, Copy, Eq, PartialEq)]
103#[non_exhaustive]
104pub struct OutOfRangeError;
105
106impl fmt::Display for OutOfRangeError {
107 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
108 write!(f, "Number out of range (must be between 1 and 3,999).")
109 }
110}
111
112impl Error for OutOfRangeError {}
113
114#[derive(Debug, Clone, Copy, Eq, PartialEq)]
116#[non_exhaustive]
117pub struct InvalidRomanNumeralError;
118
119impl fmt::Display for InvalidRomanNumeralError {
120 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
121 write!(f, "Invalid Roman numeral.")
122 }
123}
124
125impl Error for InvalidRomanNumeralError {}
126
127#[non_exhaustive]
132#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
133pub struct RomanNumeral(NonZero<u16>);
134
135impl RomanNumeral {
136 pub const MIN: Self = Self(
138 unsafe { NonZero::new_unchecked(MIN) },
141 );
142
143 pub const MAX: Self = Self(
145 unsafe { NonZero::new_unchecked(MAX) },
148 );
149
150 #[inline]
162 pub const fn new(value: u16) -> Result<Self, OutOfRangeError> {
163 if 0 != value && value < 4_000 {
164 Ok(Self(unsafe { NonZero::new_unchecked(value) }))
166 } else {
167 Err(OutOfRangeError)
168 }
169 }
170
171 #[must_use]
182 #[inline]
183 pub const fn as_u16(self) -> u16 {
184 self.0.get()
185 }
186}
187
188impl From<RomanNumeral> for u16 {
189 fn from(value: RomanNumeral) -> Self {
191 value.as_u16()
192 }
193}
194
195impl From<RomanNumeral> for u32 {
196 fn from(value: RomanNumeral) -> Self {
198 Self::from(value.as_u16())
199 }
200}
201
202impl From<RomanNumeral> for u64 {
203 fn from(value: RomanNumeral) -> Self {
205 Self::from(value.as_u16())
206 }
207}
208
209impl From<RomanNumeral> for u128 {
210 fn from(value: RomanNumeral) -> Self {
212 Self::from(value.as_u16())
213 }
214}
215
216impl From<RomanNumeral> for usize {
217 fn from(value: RomanNumeral) -> Self {
219 value.as_u16() as Self
220 }
221}
222
223impl From<RomanNumeral> for i16 {
224 fn from(value: RomanNumeral) -> Self {
226 Self::try_from(value.as_u16())
229 .unwrap_or_else(|_| unreachable!("RomanNumeral::MAX fits in 12 bits."))
230 }
231}
232
233impl From<RomanNumeral> for i32 {
234 fn from(value: RomanNumeral) -> Self {
236 Self::from(value.as_u16())
237 }
238}
239
240impl From<RomanNumeral> for i64 {
241 fn from(value: RomanNumeral) -> Self {
243 Self::from(value.as_u16())
244 }
245}
246
247impl From<RomanNumeral> for i128 {
248 fn from(value: RomanNumeral) -> Self {
250 Self::from(value.as_u16())
251 }
252}
253
254impl From<RomanNumeral> for isize {
255 fn from(value: RomanNumeral) -> Self {
257 Self::try_from(value.as_u16())
260 .unwrap_or_else(|_| unreachable!("RomanNumeral::MAX fits in 12 bits."))
261 }
262}
263
264impl RomanNumeral {
265 #[must_use]
276 #[cfg(feature = "std")]
277 pub fn to_uppercase(self) -> String {
278 format!("{self:X}")
279 }
280
281 #[must_use]
292 #[cfg(feature = "std")]
293 pub fn to_lowercase(self) -> String {
294 format!("{self:x}")
295 }
296
297 fn fmt_str(self, f: &mut fmt::Formatter, uppercase: bool) -> fmt::Result {
298 let mut buf = [0_u8; 15]; let mut n = self.0.get();
300 let mut idx = 0;
301 for &(value, part_upper, part_lower) in ROMAN_NUMERAL_PREFIXES {
302 while n >= value {
303 n -= value;
304 let part = if uppercase { part_upper } else { part_lower };
305 buf[idx..idx + part.len()].copy_from_slice(part);
306 idx += part.len();
307 }
308 }
309 debug_assert_ne!(idx, 0);
311 debug_assert_eq!(
312 buf.iter().take_while(|el| el.is_ascii_alphabetic()).count(),
313 idx
314 );
315 let out = unsafe { core::str::from_utf8_unchecked(&buf[..idx]) };
318 f.write_str(out)
319 }
320}
321
322impl fmt::Display for RomanNumeral {
323 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
334 self.fmt_str(f, true)
335 }
336}
337
338impl fmt::UpperHex for RomanNumeral {
339 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
350 self.fmt_str(f, true)
351 }
352}
353
354impl fmt::LowerHex for RomanNumeral {
355 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
366 self.fmt_str(f, false)
367 }
368}
369
370const PREFIXES_BYTES: [u8; 7] = [b'I', b'V', b'X', b'L', b'C', b'D', b'M'];
371
372impl FromStr for RomanNumeral {
373 type Err = InvalidRomanNumeralError;
374
375 #[allow(clippy::too_many_lines)]
389 fn from_str(s: &str) -> Result<Self, InvalidRomanNumeralError> {
390 if s.is_empty() {
391 return Err(InvalidRomanNumeralError);
392 }
393
394 let chars = if s.chars().all(|c| c.is_ascii_uppercase()) {
396 s.as_bytes()
397 } else if s.chars().all(|c| c.is_ascii_lowercase()) {
398 &s.as_bytes().to_ascii_uppercase()
399 } else {
400 return Err(InvalidRomanNumeralError);
402 };
403
404 if chars.iter().any(|c| !PREFIXES_BYTES.contains(c)) {
406 return Err(InvalidRomanNumeralError);
407 }
408
409 let mut result: u16 = 0;
410 let mut idx: usize = 0;
411
412 for _ in 0..4 {
414 let Some(x) = chars.get(idx..=idx) else {
415 break;
416 };
417 if x == b"M" {
418 result += 1_000;
419 idx += 1;
420 } else {
421 break;
422 }
423 }
424 if chars.len() == idx {
425 return Ok(Self(unsafe { NonZero::new_unchecked(result) }));
428 }
429
430 if chars[idx..].starts_with(b"CM") {
433 result += 900;
434 idx += 2;
435 } else if chars[idx..].starts_with(b"CD") {
436 result += 400;
437 idx += 2;
438 } else {
439 if chars.get(idx..=idx).unwrap_or_default() == b"D" {
440 result += 500;
441 idx += 1;
442 }
443 for _ in 0..3 {
444 let Some(x) = chars.get(idx..=idx) else {
445 break;
446 };
447 if x == b"C" {
448 result += 100;
449 idx += 1;
450 } else {
451 break;
452 }
453 }
454 }
455 if chars.len() == idx {
456 return Ok(Self(unsafe { NonZero::new_unchecked(result) }));
459 }
460
461 if chars[idx..].starts_with(b"XC") {
464 result += 90;
465 idx += 2;
466 } else if chars[idx..].starts_with(b"XL") {
467 result += 40;
468 idx += 2;
469 } else {
470 if chars.get(idx..=idx).unwrap_or_default() == b"L" {
471 result += 50;
472 idx += 1;
473 }
474 for _ in 0..3 {
475 let Some(x) = chars.get(idx..=idx) else {
476 break;
477 };
478 if x == b"X" {
479 result += 10;
480 idx += 1;
481 } else {
482 break;
483 }
484 }
485 }
486 if chars.len() == idx {
487 return Ok(Self(unsafe { NonZero::new_unchecked(result) }));
490 }
491
492 if chars[idx..].starts_with(b"IX") {
495 result += 9;
496 idx += 2;
497 } else if chars[idx..].starts_with(b"IV") {
498 result += 4;
499 idx += 2;
500 } else {
501 if chars.get(idx..=idx).unwrap_or_default() == b"V" {
502 result += 5;
503 idx += 1;
504 }
505 for _ in 0..3 {
506 let Some(x) = chars.get(idx..=idx) else {
507 break;
508 };
509 if x == b"I" {
510 result += 1;
511 idx += 1;
512 } else {
513 break;
514 }
515 }
516 }
517 if chars.len() == idx {
518 Ok(Self(unsafe { NonZero::new_unchecked(result) }))
521 } else {
522 Err(InvalidRomanNumeralError)
523 }
524 }
525}
526
527const ROMAN_NUMERAL_PREFIXES: &[(u16, &[u8], &[u8])] = &[
529 (1000, b"M", b"m"),
530 (900, b"CM", b"cm"),
531 (500, b"D", b"d"),
532 (400, b"CD", b"cd"),
533 (100, b"C", b"c"),
534 (90, b"XC", b"xc"),
535 (50, b"L", b"l"),
536 (40, b"XL", b"xl"),
537 (10, b"X", b"x"),
538 (9, b"IX", b"ix"),
539 (5, b"V", b"v"),
540 (4, b"IV", b"iv"),
541 (1, b"I", b"i"),
542];
543
544impl TryFrom<u8> for RomanNumeral {
545 type Error = OutOfRangeError;
546
547 fn try_from(value: u8) -> Result<Self, OutOfRangeError> {
551 Self::new(u16::from(value))
552 }
553}
554
555impl TryFrom<u16> for RomanNumeral {
556 type Error = OutOfRangeError;
557
558 fn try_from(value: u16) -> Result<Self, OutOfRangeError> {
562 Self::new(value)
563 }
564}
565
566impl TryFrom<u32> for RomanNumeral {
567 type Error = OutOfRangeError;
568
569 fn try_from(value: u32) -> Result<Self, OutOfRangeError> {
573 u16::try_from(value).map_or(Err(OutOfRangeError), Self::new)
574 }
575}
576
577impl TryFrom<u64> for RomanNumeral {
578 type Error = OutOfRangeError;
579
580 fn try_from(value: u64) -> Result<Self, OutOfRangeError> {
584 u16::try_from(value).map_or(Err(OutOfRangeError), Self::new)
585 }
586}
587
588impl TryFrom<u128> for RomanNumeral {
589 type Error = OutOfRangeError;
590
591 fn try_from(value: u128) -> Result<Self, OutOfRangeError> {
595 u16::try_from(value).map_or(Err(OutOfRangeError), Self::new)
596 }
597}
598
599impl TryFrom<usize> for RomanNumeral {
600 type Error = OutOfRangeError;
601
602 fn try_from(value: usize) -> Result<Self, OutOfRangeError> {
606 u16::try_from(value).map_or(Err(OutOfRangeError), Self::new)
607 }
608}
609
610impl TryFrom<i8> for RomanNumeral {
611 type Error = OutOfRangeError;
612
613 fn try_from(value: i8) -> Result<Self, OutOfRangeError> {
617 u16::try_from(value).map_or(Err(OutOfRangeError), Self::new)
618 }
619}
620
621impl TryFrom<i16> for RomanNumeral {
622 type Error = OutOfRangeError;
623
624 fn try_from(value: i16) -> Result<Self, OutOfRangeError> {
628 u16::try_from(value).map_or(Err(OutOfRangeError), Self::new)
629 }
630}
631
632impl TryFrom<i32> for RomanNumeral {
633 type Error = OutOfRangeError;
634
635 fn try_from(value: i32) -> Result<Self, OutOfRangeError> {
639 u16::try_from(value).map_or(Err(OutOfRangeError), Self::new)
640 }
641}
642
643impl TryFrom<i64> for RomanNumeral {
644 type Error = OutOfRangeError;
645
646 fn try_from(value: i64) -> Result<Self, OutOfRangeError> {
650 u16::try_from(value).map_or(Err(OutOfRangeError), Self::new)
651 }
652}
653
654impl TryFrom<i128> for RomanNumeral {
655 type Error = OutOfRangeError;
656
657 fn try_from(value: i128) -> Result<Self, OutOfRangeError> {
661 u16::try_from(value).map_or(Err(OutOfRangeError), Self::new)
662 }
663}
664
665#[cfg(test)]
666mod test {
667 #[cfg(not(feature = "std"))]
668 use alloc::string::ToString;
669
670 use super::*;
671
672 #[test]
673 fn test_roman_numeral_associated_constants() {
674 assert_eq!(RomanNumeral::MIN.as_u16(), 1_u16);
675 assert_eq!(RomanNumeral::MAX.as_u16(), 3_999_u16);
676 }
677
678 #[test]
679 fn test_roman_numeral_new() {
680 let rn_42: RomanNumeral = RomanNumeral(NonZero::new(42_u16).unwrap());
681
682 assert_eq!(RomanNumeral::new(0), Err(OutOfRangeError));
683 assert_eq!(RomanNumeral::new(1), Ok(RomanNumeral::MIN));
684 assert_eq!(RomanNumeral::new(1_u8.into()), Ok(RomanNumeral::MIN));
685 assert_eq!(RomanNumeral::new(1_u16), Ok(RomanNumeral::MIN));
686 assert_eq!(RomanNumeral::new(42), Ok(rn_42));
687 assert_eq!(RomanNumeral::new(3_999), Ok(RomanNumeral::MAX));
688 assert_eq!(RomanNumeral::new(MAX), Ok(RomanNumeral::MAX));
689 assert!(matches!(RomanNumeral::new(4_000), Err(OutOfRangeError)));
690 assert!(matches!(RomanNumeral::new(u16::MAX), Err(OutOfRangeError)));
691 }
692
693 #[test]
694 fn test_from_one() {
695 assert_eq!(u16::from(RomanNumeral::MIN), 1);
696 assert_eq!(u32::from(RomanNumeral::MIN), 1);
697 assert_eq!(u64::from(RomanNumeral::MIN), 1);
698 assert_eq!(u128::from(RomanNumeral::MIN), 1);
699 assert_eq!(usize::from(RomanNumeral::MIN), 1);
700 assert_eq!(i16::from(RomanNumeral::MIN), 1);
701 assert_eq!(i32::from(RomanNumeral::MIN), 1);
702 assert_eq!(i64::from(RomanNumeral::MIN), 1);
703 assert_eq!(i128::from(RomanNumeral::MIN), 1);
704 assert_eq!(isize::from(RomanNumeral::MIN), 1);
705 }
706
707 #[test]
708 fn test_roman_numeral_to_string() {
709 let test_numerals = [
710 "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X", "XI", "XII", "XIII",
711 "XIV", "XV", "XVI", "XVII", "XVIII", "XIX", "XX", "XXI", "XXII", "XXIII", "XXIV",
712 ];
713 for (i, roman_str) in test_numerals.iter().enumerate() {
714 let n: u16 = (i + 1).try_into().unwrap();
715 let expected: RomanNumeral = RomanNumeral::new(n).unwrap();
716 assert_eq!(expected.to_string(), *roman_str);
717 }
718 let rn_1984: RomanNumeral = RomanNumeral::new(1984).unwrap();
719 assert_eq!(rn_1984.to_string(), "MCMLXXXIV");
720 }
721
722 #[test]
723 fn test_roman_numeral_parse_string() {
724 let test_numerals = [
725 "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X", "XI", "XII", "XIII",
726 "XIV", "XV", "XVI", "XVII", "XVIII", "XIX", "XX", "XXI", "XXII", "XXIII", "XXIV",
727 ];
728 for (i, roman_str) in test_numerals.iter().enumerate() {
729 let n: u16 = (i + 1).try_into().unwrap();
730 let expected: RomanNumeral = RomanNumeral::new(n).unwrap();
731 let parsed: RomanNumeral = roman_str.parse().expect("parsing failed!");
732 assert_eq!(parsed, expected);
733 }
734
735 let rn_16: RomanNumeral = RomanNumeral::new(16).unwrap();
736 let parsed: RomanNumeral = "xvi".parse().unwrap();
737 assert_eq!(parsed, rn_16);
738
739 let rn_1583: RomanNumeral = RomanNumeral::new(1583).unwrap();
740 let parsed: RomanNumeral = "MDLXXXIII".parse().unwrap();
741 assert_eq!(parsed, rn_1583);
742
743 let rn_1984: RomanNumeral = RomanNumeral::new(1984).unwrap();
744 let parsed: RomanNumeral = "MCMLXXXIV".parse().unwrap();
745 assert_eq!(parsed, rn_1984);
746
747 let rn_2000: RomanNumeral = RomanNumeral::new(2000).unwrap();
748 let parsed: RomanNumeral = "MM".parse().unwrap();
749 assert_eq!(parsed, rn_2000);
750
751 let parsed: RomanNumeral = "MMMCMXCIX".parse().unwrap();
752 assert_eq!(parsed, RomanNumeral::MAX);
753 }
754
755 #[test]
756 fn test_try_from_one() {
757 assert_eq!(RomanNumeral::try_from(1_u8), Ok(RomanNumeral::MIN));
758 assert_eq!(RomanNumeral::try_from(1_u16), Ok(RomanNumeral::MIN));
759 assert_eq!(RomanNumeral::try_from(1_u32), Ok(RomanNumeral::MIN));
760 assert_eq!(RomanNumeral::try_from(1_u64), Ok(RomanNumeral::MIN));
761 assert_eq!(RomanNumeral::try_from(1_u128), Ok(RomanNumeral::MIN));
762 assert_eq!(RomanNumeral::try_from(1_usize), Ok(RomanNumeral::MIN));
763 assert_eq!(RomanNumeral::try_from(1_i8), Ok(RomanNumeral::MIN));
764 assert_eq!(RomanNumeral::try_from(1_i16), Ok(RomanNumeral::MIN));
765 assert_eq!(RomanNumeral::try_from(1_i32), Ok(RomanNumeral::MIN));
766 assert_eq!(RomanNumeral::try_from(1_i64), Ok(RomanNumeral::MIN));
767 assert_eq!(RomanNumeral::try_from(1_i128), Ok(RomanNumeral::MIN));
768 }
769
770 #[test]
771 fn test_roman_numeral_round_trip() {
772 for i in 1..=3_999 {
773 let r = RomanNumeral::new(i).unwrap().to_string();
774 let parsed: RomanNumeral = r.parse().unwrap();
775 let val: u16 = parsed.as_u16();
776 assert_eq!(val, i);
777 }
778 }
779}