#![forbid(unsafe_code)] #![warn(missing_docs, clippy::missing_docs_in_private_items)] //! # Romantic //! //! Using the default Roman numeral system. //! //! ```rust //! use romantic::Roman; //! //! let roman = Roman::default(); //! //! assert_eq!(roman.to_string(2022).unwrap(), "MMXXII"); //! assert_eq!(roman.from_str::("MMXXII").unwrap(), 2022); //! //! // The default Roman numeral system has a maximum of 3999. //! assert!(roman.to_string(4000).is_err()); //! ``` //! //! Using your own custom character set. //! //! ```rust //! use romantic::Roman; //! //! // The order of characters in the array determines their value. //! // Here, A equals 1 and B equals 5. //! let custom = Roman::new(&['A', 'B']); //! //! assert_eq!(custom.to_string(6).unwrap(), "BA"); //! assert_eq!(custom.from_str::("BA").unwrap(), 6); //! //! // With only 2 characters, the maximum value you can get is 8 //! // (the equivalent of VIII). To increase the maximum range, use //! // more characters. //! assert!(custom.to_string(9).is_err()); //! ``` use std::collections::HashMap; /// All possible errors that can occur during conversion. #[derive(Debug, thiserror::Error)] pub enum ConversionError { /// The error when converting from a [`usize`] to [`num::PrimInt`] fails. #[error("Conversion error with generic integer")] GenericConversion, /// The error when an input character does not have an associated value in the /// [`Roman`] set. #[error("Invalid character \"{0}\" encountered")] InvalidCharacter(char), /// The error when an input magnitude does not have an associated character in /// the [`Roman`] set. #[error("Missing magnitude \"{0}\" for input number")] MissingMagnitude(usize), /// The error when an input number is negative. #[error("Input number cannot be negative")] NegativeNumber, /// The error when calculating an integer would cause an overflow. #[error("Operation would cause overflow")] Overflow, } /// The main struct for [`romantic`][crate]. #[derive(Debug)] pub struct Roman { /// The mapping of a character to its corresponding magnitude (ie. 1 = 'I'). character_magnitude_map: HashMap, /// The mapping of a magnitude to its corresponding character (ie. 'I' = 1). magnitude_character_map: HashMap, } impl Default for Roman { fn default() -> Self { Self::new(&['I', 'V', 'X', 'L', 'C', 'D', 'M']) } } impl Roman { /// Creates a new [`Roman`] using the characters in `character_set`. /// /// The order of the `character_set` determines their magnitude, for example /// using the default numeral system: /// /// | Index | Magnitude | Character | /// |-------|-----------|-----------| /// | 0 | 1 | 'I' | /// | 1 | 5 | 'V' | /// | 2 | 10 | 'X' | /// | 3 | 50 | 'L' | /// | 4 | 100 | 'C' | /// | 5 | 500 | 'D' | /// | 6 | 1000 | 'M' | /// | ... | ... | ... | /// /// ## Example /// /// ```rust /// use romantic::Roman; /// /// let roman = Roman::default(); /// assert_eq!(roman.to_string(9).unwrap(), "IX"); /// assert_eq!(roman.from_str::("IX").unwrap(), 9); /// /// let custom = Roman::new(&['A', 'B', 'C']); /// assert_eq!(custom.to_string(9).unwrap(), "AC"); /// assert_eq!(custom.from_str::("AC").unwrap(), 9); /// ``` pub fn new(character_set: &[char]) -> Self { let mut character_magnitude_map = HashMap::new(); let mut magnitude_character_map = HashMap::new(); let values = [1, 5]; let modulo = values.len(); let mut magnitude = 1; for (index, &character) in character_set.iter().enumerate() { if index > 0 && index % modulo == 0 { magnitude *= 10; } let value = magnitude * values[index % modulo]; character_magnitude_map.insert(character, value); magnitude_character_map.insert(value, character); } Self { character_magnitude_map, magnitude_character_map, } } /// Converts a [`str`] to a generic integer [`num::PrimInt`]. /// /// ## Example /// /// ```rust /// use romantic::Roman; /// /// let roman = Roman::default(); /// assert_eq!(roman.from_str::("IX").unwrap(), 9); /// /// let custom = Roman::new(&['A', 'B', 'C']); /// assert_eq!(custom.from_str::("AC").unwrap(), 9); /// ``` pub fn from_str( &self, input: &str, ) -> Result { let mut characters = input.chars().peekable(); let mut result = T::zero(); while let Some(character) = characters.next() { let value = self .character_magnitude_map .get(&character) .ok_or(ConversionError::InvalidCharacter(character))?; let generic_value = T::from(*value).ok_or(ConversionError::GenericConversion)?; if let Some(next) = characters.peek() { let next = self.character_magnitude_map.get(next); let subtract = match next { Some(&next_value) => { (value * 5 == next_value) || (value * 10 == next_value) } None => false, }; if subtract { result = result .checked_sub(&generic_value) .ok_or(ConversionError::Overflow)?; continue; } } result = result .checked_add(&generic_value) .ok_or(ConversionError::Overflow)?; } Ok(result) } /// Converts a generic integer [`num::PrimInt`] to a [`String`]. /// /// ## Example /// /// ```rust /// use romantic::Roman; /// /// let roman = Roman::default(); /// assert_eq!(roman.to_string(9).unwrap(), "IX"); /// /// let custom = Roman::new(&['A', 'B', 'C']); /// assert_eq!(custom.to_string(9).unwrap(), "AC"); /// ``` pub fn to_string( &self, number: T, ) -> Result { if number < T::zero() { return Err(ConversionError::NegativeNumber); } let mut result = String::new(); for (index, digit) in number.to_string().chars().rev().enumerate() { // Skip any zeroes in the number since we don't have to do anything for it. if digit == '0' { continue; } // Safe to unwrap since this can't be anything other than a 1..=9 digit. let digit = digit.to_digit(10).unwrap() as usize; let magnitude = num::pow::pow(10, index); // Get all the units for this magnitude and intentionally leave them as // `Result`s here. Since the default Roman numeral set only goes up to // 4000, we can't require unit 5 and 10 for magnitude 1000 (5000, 10000). // So once we go to add them to the result string, only then use `Result?` // to get their characters. let value_of_character = |m: usize| -> Result { self .magnitude_character_map .get(&m) .map(ToString::to_string) .ok_or(ConversionError::MissingMagnitude(m)) }; let unit_1 = value_of_character(magnitude); let unit_5 = value_of_character(magnitude * 5); let unit_10 = value_of_character(magnitude * 10); // Map the digit to its character, using magnitude 1 as examples. result += &match digit { // 1 through 3 equals I, II, III. 1..=3 => unit_1?.repeat(digit), // 4 equals IV (note the reversed formatting). 4 => format!("{}{}", unit_5?, unit_1?), // 5 equals V. 5 => unit_5?, // 6 through 8 equals VI, VII, VIII (also reversed). 6..=8 => format!("{}{}", unit_1?.repeat(digit - 5), unit_5?), // 9 equals IX (also reversed). 9 => format!("{}{}", unit_10?, unit_1?), _ => unreachable!(), }; } Ok(result.chars().rev().collect()) } }