roman_numerals_rs/
lib.rs

1//! # roman-numerals
2//!
3//! A library for manipulating well-formed Roman numerals.
4//!
5//! Integers between 1 and 3,999 (inclusive) are supported.
6//! Numbers beyond this range will return an ``OutOfRangeError``.
7//!
8//! The classical system of roman numerals requires that
9//! the same character may not appear more than thrice consecutively,
10//! meaning that 'MMMCMXCIX' (3,999) is the largest well-formed Roman numeral.
11//! The smallest is 'I' (1), as there is no symbol for zero in Roman numerals.
12//!
13//! Both upper- and lower-case formatting of roman numerals are supported,
14//! and likewise for parsing strings,
15//! although the entire string must be of the same case.
16//! Numerals that do not adhere to the classical form are rejected
17//! with an ``InvalidRomanNumeralError``.
18//!
19//! ## Example usage
20//!
21//! ### Create a roman numeral
22//!
23//! ```rust
24//! use roman_numerals_rs::RomanNumeral;
25//!
26//! let num = RomanNumeral::new(16).unwrap();
27//! assert_eq!(num.to_string(), "XVI");
28//!
29//! let num: RomanNumeral = "XVI".parse().unwrap();
30//! assert_eq!(num.as_u16(), 16);
31//!
32//! let num: RomanNumeral = 3_999.try_into().unwrap();
33//! println!("{num}");  // MMMCMXCIX
34//! ```
35//!
36//! ### Convert a roman numeral to a string
37//!
38//! ```rust
39//! use roman_numerals_rs::RomanNumeral;
40//!
41//! let num = RomanNumeral::new(16).unwrap();
42//! assert_eq!(num.to_string(), "XVI");
43//! assert_eq!(num.to_uppercase(), "XVI");
44//! assert_eq!(num.to_lowercase(), "xvi");
45//! assert_eq!(format!("{num:X}"), "XVI");
46//! assert_eq!(format!("{num:x}"), "xvi");
47//! ```
48//!
49//! ### Extract the decimal value of a roman numeral
50//!
51//! ```rust
52//! use roman_numerals_rs::RomanNumeral;
53//!
54//! let num = RomanNumeral::new(42).unwrap();
55//! assert_eq!(num.as_u16(), 42);
56//! ```
57//!
58//! ### Invalid input
59//!
60//! ```rust
61//! use core::str::FromStr;
62//! use roman_numerals_rs::{RomanNumeral, InvalidRomanNumeralError, OutOfRangeError};
63//!
64//! let res = RomanNumeral::from_str("Spam!");
65//! assert!(matches!(res.unwrap_err(), InvalidRomanNumeralError));
66//!
67//! let res = "CLL".parse::<RomanNumeral>();
68//! assert!(matches!(res.unwrap_err(), InvalidRomanNumeralError));
69//!
70//! let res = RomanNumeral::new(0);
71//! assert!(matches!(res.unwrap_err(), OutOfRangeError));
72//!
73//! let res = RomanNumeral::new(4_000);
74//! assert!(matches!(res.unwrap_err(), OutOfRangeError));
75//! ```
76//!
77//! ## Licence
78//!
79//! This project is licenced under the terms of either
80//! the Zero-Clause BSD licence or the CC0 1.0 Universal licence.
81
82#![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
96/// The value of the smallest well-formed roman numeral.
97pub const MIN: u16 = 1;
98/// The value of the largest well-formed roman numeral.
99pub const MAX: u16 = 3_999;
100
101/// Returned as an error if a numeral is constructed with an invalid input.
102#[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/// Returned as an error if a parsed string is not a roman numeral.
115#[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/// A Roman numeral.
128///
129/// Only values between 1 and 3,999 are valid.
130/// Stores the value internally as a ``NonZero<u16>``.
131#[non_exhaustive]
132#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
133pub struct RomanNumeral(NonZero<u16>);
134
135impl RomanNumeral {
136    /// The smallest well-formed Roman numeral: I (1).
137    pub const MIN: Self = Self(
138        // TODO: Use NonZero::new(MIN).unwrap() when MSRV >= 1.83.
139        // SAFETY: crate::MIN is a non-zero constant value.
140        unsafe { NonZero::new_unchecked(MIN) },
141    );
142
143    /// The largest well-formed Roman numeral: MMMCMXCIX (3,999).
144    pub const MAX: Self = Self(
145        // TODO: Use NonZero::new(MAX).unwrap() when MSRV >= 1.83.
146        // SAFETY: crate::MAX is a non-zero constant value.
147        unsafe { NonZero::new_unchecked(MAX) },
148    );
149
150    /// Creates a ``RomanNumeral`` for any value that implements.
151    /// Requires ``value`` to be greater than 0 and less than 4,000.
152    ///
153    /// Example
154    /// -------
155    ///
156    /// .. code-block:: rust
157    ///
158    //     let answer: RomanNumeral = RomanNumeral::new(42).unwrap();
159    //     assert_eq!("XLII", answer.to_uppercase());
160    ///
161    #[inline]
162    pub const fn new(value: u16) -> Result<Self, OutOfRangeError> {
163        if 0 != value && value < 4_000 {
164            // SAFETY: 0 < value <= 3,999
165            Ok(Self(unsafe { NonZero::new_unchecked(value) }))
166        } else {
167            Err(OutOfRangeError)
168        }
169    }
170
171    /// Return the value of this ``RomanNumeral`` as an ``u16``.
172    ///
173    /// Example
174    /// -------
175    ///
176    /// .. code-block:: rust
177    ///
178    ///    let answer: RomanNumeral = RomanNumeral::new(42)?;
179    ///    assert_eq!(answer.as_u16(), 42_u16);
180    ///
181    #[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    /// Converts a RomanNumeral into a u16.
190    fn from(value: RomanNumeral) -> Self {
191        value.as_u16()
192    }
193}
194
195impl From<RomanNumeral> for u32 {
196    /// Converts a RomanNumeral into a u32.
197    fn from(value: RomanNumeral) -> Self {
198        Self::from(value.as_u16())
199    }
200}
201
202impl From<RomanNumeral> for u64 {
203    /// Converts a RomanNumeral into a u64.
204    fn from(value: RomanNumeral) -> Self {
205        Self::from(value.as_u16())
206    }
207}
208
209impl From<RomanNumeral> for u128 {
210    /// Converts a RomanNumeral into a u128.
211    fn from(value: RomanNumeral) -> Self {
212        Self::from(value.as_u16())
213    }
214}
215
216impl From<RomanNumeral> for usize {
217    /// Converts a RomanNumeral into a usize.
218    fn from(value: RomanNumeral) -> Self {
219        value.as_u16() as Self
220    }
221}
222
223impl From<RomanNumeral> for i16 {
224    /// Converts a RomanNumeral into an i16.
225    fn from(value: RomanNumeral) -> Self {
226        // i16::MAX is 32,767 (2^15 − 1)
227        // Largest Roman numeral is 3,999
228        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    /// Converts a RomanNumeral into an i32.
235    fn from(value: RomanNumeral) -> Self {
236        Self::from(value.as_u16())
237    }
238}
239
240impl From<RomanNumeral> for i64 {
241    /// Converts a RomanNumeral into an i64.
242    fn from(value: RomanNumeral) -> Self {
243        Self::from(value.as_u16())
244    }
245}
246
247impl From<RomanNumeral> for i128 {
248    /// Converts a RomanNumeral into an i128.
249    fn from(value: RomanNumeral) -> Self {
250        Self::from(value.as_u16())
251    }
252}
253
254impl From<RomanNumeral> for isize {
255    /// Converts a RomanNumeral into an isize.
256    fn from(value: RomanNumeral) -> Self {
257        // isize::MAX is 32,767 (2^15 − 1) for 16-bit targets
258        // Largest Roman numeral is 3,999
259        Self::try_from(value.as_u16())
260            .unwrap_or_else(|_| unreachable!("RomanNumeral::MAX fits in 12 bits."))
261    }
262}
263
264impl RomanNumeral {
265    /// Converts a ``RomanNumeral`` to an uppercase string.
266    ///
267    /// Example
268    /// -------
269    ///
270    /// .. code-block:: rust
271    ///
272    ///    let answer: RomanNumeral = RomanNumeral::new(42)?;
273    ///    assert_eq!("XLII", answer.to_uppercase());
274    ///
275    #[must_use]
276    #[cfg(feature = "std")]
277    pub fn to_uppercase(self) -> String {
278        format!("{self:X}")
279    }
280
281    /// Converts a ``RomanNumeral`` to a lowercase string.
282    ///
283    /// Example
284    /// -------
285    ///
286    /// .. code-block:: rust
287    ///
288    ///    let answer: RomanNumeral = RomanNumeral::new(42)?;
289    ///    assert_eq!("xlii", answer.to_lowercase());
290    ///
291    #[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]; // longest numeral is MMMDCCCLXXXVIII.
299        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        // idx must be equal to the length of the string written to buf.
310        debug_assert_ne!(idx, 0);
311        debug_assert_eq!(
312            buf.iter().take_while(|el| el.is_ascii_alphabetic()).count(),
313            idx
314        );
315        // SAFETY: buf only consists of valid ASCII characters;
316        //         idx is the length of the string.
317        let out = unsafe { core::str::from_utf8_unchecked(&buf[..idx]) };
318        f.write_str(out)
319    }
320}
321
322impl fmt::Display for RomanNumeral {
323    /// Converts a ``RomanNumeral`` to an uppercase string.
324    ///
325    /// Example
326    /// -------
327    ///
328    /// .. code-block:: rust
329    ///
330    ///    let answer: RomanNumeral = RomanNumeral::new(42)?;
331    ///    assert_eq!("XLII", answer.to_string());
332    ///
333    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
334        self.fmt_str(f, true)
335    }
336}
337
338impl fmt::UpperHex for RomanNumeral {
339    /// Converts a ``RomanNumeral`` to an uppercase string.
340    ///
341    /// Example
342    /// -------
343    ///
344    /// .. code-block:: rust
345    ///
346    ///    let answer: RomanNumeral = RomanNumeral::new(42)?;
347    ///    println!("{answer:X}");  // XLII
348    ///
349    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
350        self.fmt_str(f, true)
351    }
352}
353
354impl fmt::LowerHex for RomanNumeral {
355    /// Converts a ``RomanNumeral`` to a lowercase string.
356    ///
357    /// Example
358    /// -------
359    ///
360    /// .. code-block:: rust
361    ///
362    ///    let answer: RomanNumeral = RomanNumeral::new(42)?;
363    ///    println!("{answer:x}");  // xlii
364    ///
365    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    /// Creates a ``RomanNumeral`` from a well-formed string
376    /// representation of the roman numeral.
377    ///
378    /// Returns ``RomanNumeral`` or ``InvalidRomanNumeralError``.
379    ///
380    /// Example
381    /// -------
382    ///
383    /// .. code-block:: rust
384    ///
385    ///    let answer: RomanNumeral = "XLII".parse()?;
386    ///    assert_eq!(42, answer.0);
387    ///
388    #[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        // ASCII-only uppercase string.
395        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            // Either Non-ASCII or mixed-case ASCII.
401            return Err(InvalidRomanNumeralError);
402        };
403
404        // ASCII-only uppercase string only containing I, V, X, L, C, D, M.
405        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        // Thousands: between 0 and 4 "M" characters at the start
413        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            // SAFETY: idx is only incremented after adding to result,
426            //         and chars is not empty, hence ``idx > 1``.
427            return Ok(Self(unsafe { NonZero::new_unchecked(result) }));
428        }
429
430        // Hundreds: 900 ("CM"), 400 ("CD"), 0-300 (0 to 3 "C" characters),
431        // or 500-800 ("D", followed by 0 to 3 "C" characters)
432        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            // SAFETY: idx is only incremented after adding to result,
457            //         and chars is not empty, hence ``idx > 1``.
458            return Ok(Self(unsafe { NonZero::new_unchecked(result) }));
459        }
460
461        // Tens: 90 ("XC"), 40 ("XL"), 0-30 (0 to 3 "X" characters),
462        // or 50-80 ("L", followed by 0 to 3 "X" characters)
463        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            // SAFETY: idx is only incremented after adding to result,
488            //         and chars is not empty, hence ``idx > 1``.
489            return Ok(Self(unsafe { NonZero::new_unchecked(result) }));
490        }
491
492        // Ones: 9 ("IX"), 4 ("IV"), 0-3 (0 to 3 "I" characters),
493        // or 5-8 ("V", followed by 0 to 3 "I" characters)
494        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            // SAFETY: idx is only incremented after adding to result,
519            //         and chars is not empty, hence ``idx > 1``.
520            Ok(Self(unsafe { NonZero::new_unchecked(result) }))
521        } else {
522            Err(InvalidRomanNumeralError)
523        }
524    }
525}
526
527/// Numeral value, uppercase character, and lowercase character.
528const 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    /// Creates a ``RomanNumeral`` from an ``u8``.
548    ///
549    /// Returns ``RomanNumeral`` or ``OutOfRangeError``.
550    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    /// Creates a ``RomanNumeral`` from an ``u16``.
559    ///
560    /// Returns ``RomanNumeral`` or ``OutOfRangeError``.
561    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    /// Creates a ``RomanNumeral`` from an ``u32``.
570    ///
571    /// Returns ``RomanNumeral`` or ``OutOfRangeError``.
572    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    /// Creates a ``RomanNumeral`` from an ``u64``.
581    ///
582    /// Returns ``RomanNumeral`` or ``OutOfRangeError``.
583    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    /// Creates a ``RomanNumeral`` from an ``u128``.
592    ///
593    /// Returns ``RomanNumeral`` or ``OutOfRangeError``.
594    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    /// Creates a ``RomanNumeral`` from an ``usize``.
603    ///
604    /// Returns ``RomanNumeral`` or ``OutOfRangeError``.
605    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    /// Creates a ``RomanNumeral`` from an ``i8``.
614    ///
615    /// Returns ``RomanNumeral`` or ``OutOfRangeError``.
616    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    /// Creates a ``RomanNumeral`` from an ``i16``.
625    ///
626    /// Returns ``RomanNumeral`` or ``OutOfRangeError``.
627    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    /// Creates a ``RomanNumeral`` from an ``i32``.
636    ///
637    /// Returns ``RomanNumeral`` or ``OutOfRangeError``.
638    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    /// Creates a ``RomanNumeral`` from an ``i64``.
647    ///
648    /// Returns ``RomanNumeral`` or ``OutOfRangeError``.
649    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    /// Creates a ``RomanNumeral`` from an ``i128``.
658    ///
659    /// Returns ``RomanNumeral`` or ``OutOfRangeError``.
660    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}