Skip to content

Commit 6937f91

Browse files
feat: add Roman numeral conversion (#987)
1 parent 81e485e commit 6937f91

File tree

3 files changed

+360
-0
lines changed

3 files changed

+360
-0
lines changed

DIRECTORY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
* [Octal to Hexadecimal](https://github.com/TheAlgorithms/Rust/blob/master/src/conversions/octal_to_hexadecimal.rs)
8080
* [Order of Magnitude Conversion](https://github.com/TheAlgorithms/Rust/blob/master/src/conversions/order_of_magnitude_conversion.rs)
8181
* [RGB-CMYK Conversion](https://github.com/TheAlgorithms/Rust/blob/master/src/conversions/rgb_cmyk_conversion.rs)
82+
* [Roman Numerals](https://github.com/TheAlgorithms/Rust/blob/master/src/conversions/roman_numerals.rs)
8283
* Data Structures
8384
* [AVL Tree](https://github.com/TheAlgorithms/Rust/blob/master/src/data_structures/avl_tree.rs)
8485
* [B-Tree](https://github.com/TheAlgorithms/Rust/blob/master/src/data_structures/b_tree.rs)

src/conversions/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ mod octal_to_decimal;
1313
mod octal_to_hexadecimal;
1414
mod order_of_magnitude_conversion;
1515
mod rgb_cmyk_conversion;
16+
mod roman_numerals;
1617

1718
pub use self::binary_to_decimal::binary_to_decimal;
1819
pub use self::binary_to_hexadecimal::binary_to_hexadecimal;
@@ -31,3 +32,4 @@ pub use self::order_of_magnitude_conversion::{
3132
convert_metric_length, metric_length_conversion, MetricLengthUnit,
3233
};
3334
pub use self::rgb_cmyk_conversion::rgb_to_cmyk;
35+
pub use self::roman_numerals::{int_to_roman, roman_to_int};

src/conversions/roman_numerals.rs

Lines changed: 357 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,357 @@
1+
//! Roman Numeral Conversion
2+
//!
3+
//! This module provides conversion between Roman numerals and integers.
4+
//!
5+
//! Roman numerals use combinations of letters from the Latin alphabet:
6+
//! I, V, X, L, C, D, and M to represent numbers.
7+
//!
8+
//! # Rules
9+
//!
10+
//! - I = 1, V = 5, X = 10, L = 50, C = 100, D = 500, M = 1000
11+
//! - When a smaller value appears before a larger value, subtract the smaller
12+
//! (e.g., IV = 4, IX = 9)
13+
//! - When a smaller value appears after a larger value, add the smaller
14+
//! (e.g., VI = 6, XI = 11)
15+
//!
16+
//! # References
17+
//!
18+
//! - [Roman Numerals - Wikipedia](https://en.wikipedia.org/wiki/Roman_numerals)
19+
//! - [LeetCode #13 - Roman to Integer](https://leetcode.com/problems/roman-to-integer/)
20+
21+
/// Roman numeral symbols and their corresponding values in descending order.
22+
/// Used for conversion from integer to Roman numeral.
23+
const ROMAN_NUMERALS: [(u32, &str); 13] = [
24+
(1000, "M"),
25+
(900, "CM"),
26+
(500, "D"),
27+
(400, "CD"),
28+
(100, "C"),
29+
(90, "XC"),
30+
(50, "L"),
31+
(40, "XL"),
32+
(10, "X"),
33+
(9, "IX"),
34+
(5, "V"),
35+
(4, "IV"),
36+
(1, "I"),
37+
];
38+
39+
/// Converts a Roman numeral string to an integer.
40+
///
41+
/// # Arguments
42+
///
43+
/// * `roman` - A string slice containing a valid Roman numeral
44+
///
45+
/// # Returns
46+
///
47+
/// `Ok(u32)` with the integer value, or `Err(String)` if the input is invalid
48+
///
49+
/// # Rules
50+
///
51+
/// - Valid Roman numerals are in range 1-3999
52+
/// - Uses standard subtractive notation (IV, IX, XL, XC, CD, CM)
53+
/// - Input must contain only valid Roman numeral characters: I, V, X, L, C, D, M
54+
///
55+
/// # Example
56+
///
57+
/// ```
58+
/// use the_algorithms_rust::conversions::roman_to_int;
59+
///
60+
/// assert_eq!(roman_to_int("III").unwrap(), 3);
61+
/// assert_eq!(roman_to_int("CLIV").unwrap(), 154);
62+
/// assert_eq!(roman_to_int("MIX").unwrap(), 1009);
63+
/// assert_eq!(roman_to_int("MMMCMXCIX").unwrap(), 3999);
64+
///
65+
/// // Invalid input returns error
66+
/// assert!(roman_to_int("INVALID").is_err());
67+
/// ```
68+
pub fn roman_to_int(roman: &str) -> Result<u32, String> {
69+
if roman.is_empty() {
70+
return Err("Roman numeral cannot be empty".to_string());
71+
}
72+
73+
// Convert to uppercase for case-insensitive processing
74+
let roman = roman.to_uppercase();
75+
let chars: Vec<char> = roman.chars().collect();
76+
77+
// Validate that all characters are valid Roman numerals
78+
for ch in &chars {
79+
if !matches!(ch, 'I' | 'V' | 'X' | 'L' | 'C' | 'D' | 'M') {
80+
return Err(format!("Invalid Roman numeral character: '{ch}'"));
81+
}
82+
}
83+
84+
let mut total: u32 = 0;
85+
let mut place = 0;
86+
87+
while place < chars.len() {
88+
let current_val = char_to_value(chars[place]);
89+
90+
// Check if we need to use subtractive notation
91+
if place + 1 < chars.len() {
92+
let next_val = char_to_value(chars[place + 1]);
93+
94+
if current_val < next_val {
95+
// Subtractive case (e.g., IV, IX, XL, XC, CD, CM)
96+
total += next_val - current_val;
97+
place += 2;
98+
continue;
99+
}
100+
}
101+
102+
// Normal case - just add the value
103+
total += current_val;
104+
place += 1;
105+
}
106+
107+
if total == 0 || total > 3999 {
108+
return Err(format!(
109+
"Result {total} is out of valid range (1-3999) for Roman numerals"
110+
));
111+
}
112+
113+
Ok(total)
114+
}
115+
116+
/// Converts an integer to a Roman numeral string.
117+
///
118+
/// # Arguments
119+
///
120+
/// * `number` - An integer in the range 1-3999
121+
///
122+
/// # Returns
123+
///
124+
/// `Ok(String)` with the Roman numeral representation, or `Err(String)` if out of range
125+
///
126+
/// # Rules
127+
///
128+
/// - Valid input range is 1-3999
129+
/// - Uses standard subtractive notation (IV, IX, XL, XC, CD, CM)
130+
/// - Returns the shortest possible representation
131+
///
132+
/// # Example
133+
///
134+
/// ```
135+
/// use the_algorithms_rust::conversions::int_to_roman;
136+
///
137+
/// assert_eq!(int_to_roman(3).unwrap(), "III");
138+
/// assert_eq!(int_to_roman(154).unwrap(), "CLIV");
139+
/// assert_eq!(int_to_roman(1009).unwrap(), "MIX");
140+
/// assert_eq!(int_to_roman(3999).unwrap(), "MMMCMXCIX");
141+
///
142+
/// // Out of range returns error
143+
/// assert!(int_to_roman(0).is_err());
144+
/// assert!(int_to_roman(4000).is_err());
145+
/// ```
146+
pub fn int_to_roman(mut number: u32) -> Result<String, String> {
147+
if number == 0 || number > 3999 {
148+
return Err(format!(
149+
"Number {number} is out of valid range (1-3999) for Roman numerals"
150+
));
151+
}
152+
153+
let mut result = String::new();
154+
155+
for (value, numeral) in ROMAN_NUMERALS.iter() {
156+
let count = number / value;
157+
if count > 0 {
158+
result.push_str(&numeral.repeat(count as usize));
159+
number %= value;
160+
}
161+
162+
if number == 0 {
163+
break;
164+
}
165+
}
166+
167+
Ok(result)
168+
}
169+
170+
/// Helper function to convert a Roman numeral character to its integer value.
171+
///
172+
/// # Arguments
173+
///
174+
/// * `ch` - A Roman numeral character (I, V, X, L, C, D, M)
175+
///
176+
/// # Returns
177+
///
178+
/// The integer value of the character
179+
///
180+
/// # Panics
181+
///
182+
/// Panics if an invalid character is provided (this should be caught by validation)
183+
fn char_to_value(ch: char) -> u32 {
184+
match ch {
185+
'I' => 1,
186+
'V' => 5,
187+
'X' => 10,
188+
'L' => 50,
189+
'C' => 100,
190+
'D' => 500,
191+
'M' => 1000,
192+
_ => panic!("Invalid Roman numeral character: {ch}"),
193+
}
194+
}
195+
196+
#[cfg(test)]
197+
mod tests {
198+
use super::*;
199+
200+
#[test]
201+
fn test_roman_to_int_basic() {
202+
assert_eq!(roman_to_int("I").unwrap(), 1);
203+
assert_eq!(roman_to_int("V").unwrap(), 5);
204+
assert_eq!(roman_to_int("X").unwrap(), 10);
205+
assert_eq!(roman_to_int("L").unwrap(), 50);
206+
assert_eq!(roman_to_int("C").unwrap(), 100);
207+
assert_eq!(roman_to_int("D").unwrap(), 500);
208+
assert_eq!(roman_to_int("M").unwrap(), 1000);
209+
}
210+
211+
#[test]
212+
fn test_roman_to_int_additive() {
213+
assert_eq!(roman_to_int("II").unwrap(), 2);
214+
assert_eq!(roman_to_int("III").unwrap(), 3);
215+
assert_eq!(roman_to_int("VI").unwrap(), 6);
216+
assert_eq!(roman_to_int("VII").unwrap(), 7);
217+
assert_eq!(roman_to_int("VIII").unwrap(), 8);
218+
assert_eq!(roman_to_int("XI").unwrap(), 11);
219+
assert_eq!(roman_to_int("XV").unwrap(), 15);
220+
assert_eq!(roman_to_int("XX").unwrap(), 20);
221+
assert_eq!(roman_to_int("XXX").unwrap(), 30);
222+
}
223+
224+
#[test]
225+
fn test_roman_to_int_subtractive() {
226+
assert_eq!(roman_to_int("IV").unwrap(), 4);
227+
assert_eq!(roman_to_int("IX").unwrap(), 9);
228+
assert_eq!(roman_to_int("XL").unwrap(), 40);
229+
assert_eq!(roman_to_int("XC").unwrap(), 90);
230+
assert_eq!(roman_to_int("CD").unwrap(), 400);
231+
assert_eq!(roman_to_int("CM").unwrap(), 900);
232+
}
233+
234+
#[test]
235+
fn test_roman_to_int_complex() {
236+
assert_eq!(roman_to_int("CLIV").unwrap(), 154);
237+
assert_eq!(roman_to_int("MCMXC").unwrap(), 1990);
238+
assert_eq!(roman_to_int("MMXIV").unwrap(), 2014);
239+
assert_eq!(roman_to_int("MIX").unwrap(), 1009);
240+
assert_eq!(roman_to_int("MMD").unwrap(), 2500);
241+
assert_eq!(roman_to_int("MMMCMXCIX").unwrap(), 3999);
242+
}
243+
244+
#[test]
245+
fn test_roman_to_int_case_insensitive() {
246+
assert_eq!(roman_to_int("iii").unwrap(), 3);
247+
assert_eq!(roman_to_int("Cliv").unwrap(), 154);
248+
assert_eq!(roman_to_int("mIx").unwrap(), 1009);
249+
}
250+
251+
#[test]
252+
fn test_roman_to_int_invalid_character() {
253+
assert!(roman_to_int("INVALID").is_err());
254+
assert!(roman_to_int("XYZ").is_err());
255+
assert!(roman_to_int("123").is_err());
256+
assert!(roman_to_int("X5").is_err());
257+
}
258+
259+
#[test]
260+
fn test_roman_to_int_empty() {
261+
assert!(roman_to_int("").is_err());
262+
}
263+
264+
#[test]
265+
fn test_int_to_roman_basic() {
266+
assert_eq!(int_to_roman(1).unwrap(), "I");
267+
assert_eq!(int_to_roman(5).unwrap(), "V");
268+
assert_eq!(int_to_roman(10).unwrap(), "X");
269+
assert_eq!(int_to_roman(50).unwrap(), "L");
270+
assert_eq!(int_to_roman(100).unwrap(), "C");
271+
assert_eq!(int_to_roman(500).unwrap(), "D");
272+
assert_eq!(int_to_roman(1000).unwrap(), "M");
273+
}
274+
275+
#[test]
276+
fn test_int_to_roman_additive() {
277+
assert_eq!(int_to_roman(2).unwrap(), "II");
278+
assert_eq!(int_to_roman(3).unwrap(), "III");
279+
assert_eq!(int_to_roman(6).unwrap(), "VI");
280+
assert_eq!(int_to_roman(7).unwrap(), "VII");
281+
assert_eq!(int_to_roman(8).unwrap(), "VIII");
282+
assert_eq!(int_to_roman(11).unwrap(), "XI");
283+
assert_eq!(int_to_roman(15).unwrap(), "XV");
284+
assert_eq!(int_to_roman(20).unwrap(), "XX");
285+
assert_eq!(int_to_roman(30).unwrap(), "XXX");
286+
}
287+
288+
#[test]
289+
fn test_int_to_roman_subtractive() {
290+
assert_eq!(int_to_roman(4).unwrap(), "IV");
291+
assert_eq!(int_to_roman(9).unwrap(), "IX");
292+
assert_eq!(int_to_roman(40).unwrap(), "XL");
293+
assert_eq!(int_to_roman(90).unwrap(), "XC");
294+
assert_eq!(int_to_roman(400).unwrap(), "CD");
295+
assert_eq!(int_to_roman(900).unwrap(), "CM");
296+
}
297+
298+
#[test]
299+
fn test_int_to_roman_complex() {
300+
assert_eq!(int_to_roman(154).unwrap(), "CLIV");
301+
assert_eq!(int_to_roman(1990).unwrap(), "MCMXC");
302+
assert_eq!(int_to_roman(2014).unwrap(), "MMXIV");
303+
assert_eq!(int_to_roman(1009).unwrap(), "MIX");
304+
assert_eq!(int_to_roman(2500).unwrap(), "MMD");
305+
assert_eq!(int_to_roman(3999).unwrap(), "MMMCMXCIX");
306+
}
307+
308+
#[test]
309+
fn test_int_to_roman_out_of_range() {
310+
assert!(int_to_roman(0).is_err());
311+
assert!(int_to_roman(4000).is_err());
312+
assert!(int_to_roman(5000).is_err());
313+
}
314+
315+
#[test]
316+
fn test_roundtrip_conversion() {
317+
// Test that converting to Roman and back gives the same number
318+
for i in 1..=3999 {
319+
let roman = int_to_roman(i).unwrap();
320+
let back = roman_to_int(&roman).unwrap();
321+
assert_eq!(i, back, "Roundtrip failed for {i}: {roman} -> {back}");
322+
}
323+
}
324+
325+
#[test]
326+
fn test_all_examples_from_python() {
327+
// Test cases from the original Python implementation
328+
let tests = [
329+
("III", 3),
330+
("CLIV", 154),
331+
("MIX", 1009),
332+
("MMD", 2500),
333+
("MMMCMXCIX", 3999),
334+
];
335+
336+
for (roman, expected) in tests.iter() {
337+
assert_eq!(roman_to_int(roman).unwrap(), *expected);
338+
assert_eq!(int_to_roman(*expected).unwrap(), *roman);
339+
}
340+
}
341+
342+
#[test]
343+
fn test_edge_cases() {
344+
// Minimum value
345+
assert_eq!(int_to_roman(1).unwrap(), "I");
346+
assert_eq!(roman_to_int("I").unwrap(), 1);
347+
348+
// Maximum value
349+
assert_eq!(int_to_roman(3999).unwrap(), "MMMCMXCIX");
350+
assert_eq!(roman_to_int("MMMCMXCIX").unwrap(), 3999);
351+
352+
// Powers of 10
353+
assert_eq!(int_to_roman(10).unwrap(), "X");
354+
assert_eq!(int_to_roman(100).unwrap(), "C");
355+
assert_eq!(int_to_roman(1000).unwrap(), "M");
356+
}
357+
}

0 commit comments

Comments
 (0)