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