Skip to main content

nil_num/
roman.rs

1// Copyright (C) Call of Nil contributors
2// SPDX-License-Identifier: AGPL-3.0-only
3
4use 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}