1use num_traits::ToPrimitive;
5use serde::{Deserialize, Serialize};
6use std::fmt;
7use std::iter::repeat_n;
8use strum::{Display, EnumIter, IntoEnumIterator};
9
10#[derive(Clone, Debug, Deserialize, Serialize)]
11pub struct Roman(Box<[Numeral]>);
12
13impl Roman {
14 const MIN: usize = 1;
15 const MAX: usize = 3999;
16
17 pub fn parse(value: impl ToRoman) -> Option<Self> {
18 value.to_roman()
19 }
20}
21
22impl Default for Roman {
23 fn default() -> Self {
24 Self(Box::from([Numeral::I]))
25 }
26}
27
28impl fmt::Display for Roman {
29 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30 for numeral in &self.0 {
31 write!(f, "{numeral}")?;
32 }
33
34 Ok(())
35 }
36}
37
38impl From<&Roman> for u16 {
39 fn from(roman: &Roman) -> Self {
40 let mut value = 0u16;
41 for numeral in &roman.0 {
42 let numeral = u16::from(*numeral);
43 value = value.saturating_add(numeral);
44 }
45
46 value
47 }
48}
49
50#[derive(Clone, Copy, Debug, Display, Deserialize, Serialize, EnumIter)]
51#[derive_const(PartialEq, Eq)]
52#[serde(rename_all = "UPPERCASE")]
53#[strum(serialize_all = "UPPERCASE")]
54pub enum Numeral {
55 I,
56 IV,
57 V,
58 IX,
59 X,
60 XL,
61 L,
62 XC,
63 C,
64 CD,
65 D,
66 CM,
67 M,
68}
69
70impl const From<Numeral> for u16 {
71 fn from(numeral: Numeral) -> Self {
72 match numeral {
73 Numeral::I => 1,
74 Numeral::IV => 4,
75 Numeral::V => 5,
76 Numeral::IX => 9,
77 Numeral::X => 10,
78 Numeral::XL => 40,
79 Numeral::L => 50,
80 Numeral::XC => 90,
81 Numeral::C => 100,
82 Numeral::CD => 400,
83 Numeral::D => 500,
84 Numeral::CM => 900,
85 Numeral::M => 1000,
86 }
87 }
88}
89
90macro_rules! impl_from_numeral {
91 ($($target:ident),+ $(,)?) => {
92 $(
93 impl const From<Numeral> for $target {
94 fn from(numeral: Numeral) -> Self {
95 u16::from(numeral).into()
96 }
97 }
98 )+
99 };
100}
101
102impl_from_numeral!(i32, i64, u32, u64, usize);
103
104pub trait ToRoman {
105 fn to_roman(self) -> Option<Roman>;
106}
107
108impl ToRoman for usize {
109 fn to_roman(mut self) -> Option<Roman> {
110 if (Roman::MIN..=Roman::MAX).contains(&self) {
111 let mut roman = Vec::new();
112 for numeral in Numeral::iter().rev() {
113 if self == 0 {
114 break;
115 }
116
117 let value = usize::from(numeral);
118 let count = self.saturating_div(value);
119 roman.extend(repeat_n(numeral, count));
120 self = self.saturating_sub(count * value);
121 }
122
123 Some(Roman(roman.into_boxed_slice()))
124 } else {
125 None
126 }
127 }
128}
129
130macro_rules! impl_to_roman {
131 ($($num:ident),+ $(,)?) => {
132 $(
133 impl ToRoman for $num {
134 fn to_roman(self) -> Option<Roman> {
135 self.to_usize().and_then(ToRoman::to_roman)
136 }
137 }
138 )+
139 };
140}
141
142impl_to_roman!(i8, i16, i32, i64, u8, u16, u32, u64, f32, f64);
143
144#[cfg(test)]
145mod tests {
146 use super::{Roman, ToRoman};
147
148 macro_rules! to_str {
149 ($number:expr) => {
150 $number
151 .to_roman()
152 .unwrap()
153 .to_string()
154 .as_str()
155 };
156 }
157
158 #[test]
159 fn to_roman() {
160 assert_eq!(to_str!(1), "I");
161 assert_eq!(to_str!(4), "IV");
162 assert_eq!(to_str!(5), "V");
163 assert_eq!(to_str!(9), "IX");
164 assert_eq!(to_str!(10), "X");
165 assert_eq!(to_str!(30), "XXX");
166 assert_eq!(to_str!(40), "XL");
167 assert_eq!(to_str!(50), "L");
168 assert_eq!(to_str!(100), "C");
169 assert_eq!(to_str!(300), "CCC");
170 assert_eq!(to_str!(400), "CD");
171 assert_eq!(to_str!(500), "D");
172 assert_eq!(to_str!(900), "CM");
173 assert_eq!(to_str!(1000), "M");
174 assert_eq!(to_str!(2350), "MMCCCL");
175 assert_eq!(to_str!(3000), "MMM");
176 assert_eq!(to_str!(3999), "MMMCMXCIX");
177 }
178
179 #[test]
180 fn min_max() {
181 assert!(Roman::parse(0u16).is_none());
182 assert!(Roman::parse(2000u16).is_some());
183 assert!(Roman::parse(4000u16).is_none());
184 }
185}