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 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}