Skip to content

Commit fb3ecc1

Browse files
committed
Format roman numerals
Resolves #20
1 parent e6ec065 commit fb3ecc1

File tree

4 files changed

+209
-6
lines changed

4 files changed

+209
-6
lines changed

readme.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,18 @@ assert_eq!("x₁", format!("x{}", Subscript(1)));
2929
assert_eq!("", format!("n{}", Superscript(2)));
3030
```
3131

32+
### Roman Numerals
33+
Formats unsigned integers as Roman numerals.
34+
35+
```rust
36+
use fmtastic::Roman;
37+
38+
assert_eq!("ⅾⅽⅽⅼⅹⅹⅹⅰⅹ", format!("{:#}", Roman::new(789_u16).unwrap())); // lowercase
39+
assert_eq!("ⅯⅯⅩⅩⅠⅤ", format!("{}", Roman::new(2024_u16).unwrap()));
40+
assert_eq!("MMXXIV", format!("{}", Roman::new(2024_u16).unwrap().ascii())); // ascii
41+
assert_eq!("ⅠⅠⅠ", format!("{}", Roman::from(3_u8))); // u8's can always be formatted as Roman numeral
42+
```
43+
3244
### Seven-Segment Digits
3345
Formats an unsigned integer using seven-segment digits
3446
from the [Legacy Computing] block.

src/integer.rs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
use core::fmt;
2-
use core::ops::Mul;
32
use core::ops::{Div, Rem, Sub};
3+
use core::ops::{Mul, SubAssign};
44

55
pub(crate) trait IntegerImpl
66
where
77
Self: Copy,
88
Self: Div<Self, Output = Self>,
99
Self: Rem<Self, Output = Self>,
1010
Self: TryInto<u8>,
11+
Self: TryFrom<u16>,
1112
Self: PartialOrd<Self>,
1213
Self: Sub<Self, Output = Self>,
14+
Self: SubAssign<Self>,
1315
{
1416
const ZERO: Self;
1517
const ONE: Self;
@@ -38,6 +40,9 @@ where
3840
fn into_public(self) -> Self::Public;
3941
}
4042

43+
#[allow(dead_code)] // This is clearly used dear compiler
44+
pub(crate) trait UnsignedIntegerImpl: IntegerImpl + crate::roman::RomanInteger {}
45+
4146
pub(crate) enum Sign {
4247
Negative,
4348
PositiveOrZero,
@@ -131,11 +136,17 @@ macro_rules! impl_unsigned_integer {
131136
impl crate::ToIntegerImpl for $ty {
132137
type Impl = $ty;
133138

134-
fn to_impl(&self) -> $ty {
135-
*self
139+
fn into_impl(self) -> $ty {
140+
self
136141
}
137142
}
138143

144+
impl crate::ToUnsignedIntegerImpl for $ty {
145+
type UnsignedImpl = $ty;
146+
}
147+
148+
impl UnsignedIntegerImpl for $ty {}
149+
139150
impl IntegerImpl for $ty {
140151
common_integer_items!($ty);
141152

src/lib.rs

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
//!
44
//! # [Vulgar Fractions]
55
//! Creates beautiful unicode fractions like ¼ or ¹⁰⁄₃.
6-
//! ```rust
6+
//! ```
77
//! # use fmtastic::VulgarFraction;
88
//! assert_eq!("¹⁰⁄₃", format!("{}", VulgarFraction::new(10, 3)));
99
//! assert_eq!("¼", format!("{}", VulgarFraction::new(1, 4)));
@@ -12,12 +12,23 @@
1212
//! # Sub- and superscript
1313
//! Formats integers as sub- or superscript.
1414
//!
15-
//! ```rust
15+
//! ```
1616
//! # use fmtastic::{Subscript, Superscript};
1717
//! assert_eq!("x₁", format!("x{}", Subscript(1)));
1818
//! assert_eq!("n²", format!("n{}", Superscript(2)));
1919
//! ```
2020
//!
21+
//! # Roman Numerals
22+
//! Formats unsigned integers as Roman numerals.
23+
//!
24+
//! ```
25+
//! # use fmtastic::Roman;
26+
//! assert_eq!("ⅾⅽⅽⅼⅹⅹⅹⅰⅹ", format!("{:#}", Roman::new(789_u16).unwrap())); // lowercase
27+
//! assert_eq!("ⅯⅯⅩⅩⅠⅤ", format!("{}", Roman::new(2024_u16).unwrap()));
28+
//! assert_eq!("MMXXIV", format!("{}", Roman::new(2024_u16).unwrap().ascii())); // ascii
29+
//! assert_eq!("ⅠⅠⅠ", format!("{}", Roman::from(3_u8))); // u8's can always be formatted as Roman numeral
30+
//! ```
31+
//!
2132
//! [Vulgar Fractions]: https://en.wikipedia.org/wiki/Fraction_(mathematics)#Simple,_common,_or_vulgar_fractions
2233
//!
2334
//! # Seven-Segment Digits
@@ -77,14 +88,19 @@ pub trait SignedInteger: Integer {}
7788

7889
/// Abstraction over unsigned integer types.
7990
/// Unsigned integers can be formatted as [`Segmented`] or [`TallyMarks`].
80-
pub trait UnsignedInteger: Integer {}
91+
#[allow(private_bounds)]
92+
pub trait UnsignedInteger: Integer + ToUnsignedIntegerImpl {}
8193

8294
pub(crate) trait ToIntegerImpl {
8395
type Impl: crate::integer::IntegerImpl<Public = Self>;
8496

8597
fn into_impl(self) -> Self::Impl;
8698
}
8799

100+
pub(crate) trait ToUnsignedIntegerImpl: ToIntegerImpl<Impl = Self::UnsignedImpl> {
101+
type UnsignedImpl: integer::UnsignedIntegerImpl<Public = Self>;
102+
}
103+
88104
mod sub_superscript;
89105
pub use sub_superscript::*;
90106
mod fraction;
@@ -96,6 +112,8 @@ mod seven_segment;
96112
pub use seven_segment::*;
97113
mod ballot_box;
98114
pub use ballot_box::*;
115+
mod roman;
116+
pub use roman::*;
99117

100118
mod digits;
101119

src/roman.rs

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
// Adapted from Yann Villessuzanne's roman.rs under the
2+
// Unlicense, at https://github.com/linfir/roman.rs/
3+
4+
use crate::integer::IntegerImpl;
5+
use crate::UnsignedInteger;
6+
use core::fmt;
7+
8+
/// Formats unsigned integers as Roman numerals.
9+
///
10+
/// By default, the dedicated unicode symbols for Roman numerals are used.
11+
/// You can use [`Roman::ascii`] to use ASCII symbols instead.
12+
///
13+
/// ```
14+
/// # use fmtastic::Roman;
15+
/// assert_eq!("ⅾⅽⅽⅼⅹⅹⅹⅰⅹ", format!("{:#}", Roman::new(789_u16).unwrap())); // lowercase
16+
/// assert_eq!("ⅯⅯⅩⅩⅠⅤ", format!("{}", Roman::new(2024_u16).unwrap()));
17+
/// assert_eq!("MMXXIV", format!("{}", Roman::new(2024_u16).unwrap().ascii())); // ascii
18+
/// assert_eq!("ⅠⅠⅠ", format!("{}", Roman::from(3_u8))); // u8's can always be formatted as Roman numeral
19+
/// ```
20+
///
21+
/// ## Formatting Flags
22+
/// ### Alternate `#`
23+
/// By default uppercase numerals are used.
24+
/// The alternate flag `#` can be used to switch to lowercase numerals.
25+
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
26+
pub struct Roman<T>(T, SymbolRepertoire);
27+
28+
impl<T> Roman<T> {
29+
/// Uses ASCII symbols instead of the dedicated unciode
30+
/// symbols for Roman numerals.
31+
pub fn ascii(mut self) -> Self {
32+
self.1 = SymbolRepertoire::Ascii;
33+
self
34+
}
35+
}
36+
37+
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
38+
#[non_exhaustive]
39+
enum SymbolRepertoire {
40+
Unicode,
41+
Ascii,
42+
}
43+
44+
impl From<u8> for Roman<u8> {
45+
fn from(value: u8) -> Self {
46+
Roman(value, SymbolRepertoire::Unicode)
47+
}
48+
}
49+
50+
impl<T> Roman<T>
51+
where
52+
T: UnsignedInteger,
53+
{
54+
/// Creates a new [`Roman`] numeral.
55+
/// Returns `None` if the value is not between 1 and 3999.
56+
pub fn new(value: T) -> Option<Roman<T>> {
57+
if T::Impl::ZERO < value.into_impl() && value.into_impl() <= T::UnsignedImpl::ROMAN_MAX {
58+
Some(Roman(value, SymbolRepertoire::Unicode))
59+
} else {
60+
None
61+
}
62+
}
63+
}
64+
65+
impl<T> fmt::Display for Roman<T>
66+
where
67+
T: UnsignedInteger,
68+
{
69+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70+
let mut n = self.0.into_impl();
71+
for (symbol, value) in roman_pairs::<T>(self.1, f.alternate()) {
72+
let value = value.into_impl();
73+
while n >= value {
74+
n -= value;
75+
write!(f, "{symbol}")?;
76+
}
77+
}
78+
debug_assert!(n == T::Impl::ZERO);
79+
Ok(())
80+
}
81+
}
82+
83+
fn roman_pairs<T>(
84+
repertoire: SymbolRepertoire,
85+
lowercase: bool,
86+
) -> impl Iterator<Item = (&'static str, T)>
87+
where
88+
T: UnsignedInteger,
89+
{
90+
ROMAN_PAIRS.iter().copied().filter_map(
91+
move |(upper_unicode, lower_unicode, upper_ascii, lower_ascii, value)| {
92+
let symbol = match (repertoire, lowercase) {
93+
(SymbolRepertoire::Unicode, false) => upper_unicode,
94+
(SymbolRepertoire::Unicode, true) => lower_unicode,
95+
(SymbolRepertoire::Ascii, false) => upper_ascii,
96+
(SymbolRepertoire::Ascii, true) => lower_ascii,
97+
};
98+
Some((symbol, T::Impl::try_from(value).ok()?.into_public()))
99+
},
100+
)
101+
}
102+
103+
static ROMAN_PAIRS: &[(&str, &str, &str, &str, u16)] = &[
104+
("Ⅿ", "ⅿ", "M", "m", 1000),
105+
("ⅭⅯ", "ⅽⅿ", "CM", "cm", 900),
106+
("Ⅾ", "ⅾ", "D", "d", 500),
107+
("ⅭⅮ", "ⅽⅾ", "CD", "cd", 400),
108+
("Ⅽ", "ⅽ", "C", "c", 100),
109+
("ⅩⅭ", "ⅹⅽ", "XC", "xc", 90),
110+
("Ⅼ", "ⅼ", "L", "l", 50),
111+
("ⅩⅬ", "ⅹⅼ", "XL", "xl", 40),
112+
("Ⅹ", "ⅹ", "X", "x", 10),
113+
("ⅠⅩ", "ⅰⅹ", "IX", "ix", 9),
114+
("Ⅴ", "ⅴ", "V", "v", 5),
115+
("ⅠⅤ", "ⅰⅴ", "IV", "iv", 4),
116+
("Ⅰ", "ⅰ", "I", "i", 1),
117+
];
118+
119+
pub(crate) trait RomanInteger {
120+
const ROMAN_MAX: Self;
121+
}
122+
123+
impl RomanInteger for u8 {
124+
const ROMAN_MAX: Self = u8::MAX;
125+
}
126+
127+
macro_rules! impl_roman_integer {
128+
($($ty:ty),*) => {
129+
$(
130+
impl RomanInteger for $ty {
131+
/// The largest number representable as a roman numeral.
132+
const ROMAN_MAX: Self = 3999;
133+
}
134+
)*
135+
}
136+
}
137+
138+
impl_roman_integer!(u16, u32, u64, u128, usize);
139+
140+
#[cfg(test)]
141+
mod tests {
142+
use super::*;
143+
144+
#[test]
145+
fn test_to_roman() {
146+
let roman =
147+
"I II III IV V VI VII VIII IX X XI XII XIII XIV XV XVI XVII XVIII XIX XX XXI XXII"
148+
.split(' ');
149+
for (i, x) in roman.enumerate() {
150+
let n = i + 1;
151+
assert_eq!(format!("{}", Roman::new(n).unwrap().ascii()), x);
152+
}
153+
assert_eq!(
154+
format!("{}", Roman::new(1984u32).unwrap().ascii()),
155+
"MCMLXXXIV"
156+
);
157+
assert_eq!(
158+
format!("{}", Roman::new(448u32).unwrap().ascii()),
159+
"CDXLVIII"
160+
);
161+
}
162+
}

0 commit comments

Comments
 (0)