Initial commit! 🎉
This commit is contained in:
		
						commit
						d4292a5cbe
					
				|  | @ -0,0 +1,17 @@ | |||
| # Generated by Cargo | ||||
| # will have compiled files and executables | ||||
| debug/ | ||||
| target/ | ||||
| 
 | ||||
| # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries | ||||
| # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html | ||||
| Cargo.lock | ||||
| 
 | ||||
| # These are backup files generated by rustfmt | ||||
| **/*.rs.bk | ||||
| 
 | ||||
| # MSVC Windows builds of rustc generate these, which store debugging information | ||||
| *.pdb | ||||
| 
 | ||||
| # Code coverage results | ||||
| .coverage | ||||
|  | @ -0,0 +1,20 @@ | |||
| # https://doc.rust-lang.org/cargo/reference/manifest.html | ||||
| 
 | ||||
| [package] | ||||
| name = "romantic" | ||||
| description = "Roman numeral toolkit" | ||||
| authors = ["Holllo <helllo@holllo.cc>"] | ||||
| version = "0.0.0" | ||||
| license = "MIT OR Apache-2.0" | ||||
| repository = "https://github.com/Holllo/romantic" | ||||
| edition = "2021" | ||||
| 
 | ||||
| [lib] | ||||
| path = "source/lib.rs" | ||||
| 
 | ||||
| [dependencies] | ||||
| num = "0.4.0" | ||||
| thiserror = "1.0.30" | ||||
| 
 | ||||
| [dev-dependencies] | ||||
| test-case = "2.0.2" | ||||
|  | @ -0,0 +1,201 @@ | |||
|                                  Apache License | ||||
|                            Version 2.0, January 2004 | ||||
|                         http://www.apache.org/licenses/ | ||||
| 
 | ||||
|    TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | ||||
| 
 | ||||
|    1. Definitions. | ||||
| 
 | ||||
|       "License" shall mean the terms and conditions for use, reproduction, | ||||
|       and distribution as defined by Sections 1 through 9 of this document. | ||||
| 
 | ||||
|       "Licensor" shall mean the copyright owner or entity authorized by | ||||
|       the copyright owner that is granting the License. | ||||
| 
 | ||||
|       "Legal Entity" shall mean the union of the acting entity and all | ||||
|       other entities that control, are controlled by, or are under common | ||||
|       control with that entity. For the purposes of this definition, | ||||
|       "control" means (i) the power, direct or indirect, to cause the | ||||
|       direction or management of such entity, whether by contract or | ||||
|       otherwise, or (ii) ownership of fifty percent (50%) or more of the | ||||
|       outstanding shares, or (iii) beneficial ownership of such entity. | ||||
| 
 | ||||
|       "You" (or "Your") shall mean an individual or Legal Entity | ||||
|       exercising permissions granted by this License. | ||||
| 
 | ||||
|       "Source" form shall mean the preferred form for making modifications, | ||||
|       including but not limited to software source code, documentation | ||||
|       source, and configuration files. | ||||
| 
 | ||||
|       "Object" form shall mean any form resulting from mechanical | ||||
|       transformation or translation of a Source form, including but | ||||
|       not limited to compiled object code, generated documentation, | ||||
|       and conversions to other media types. | ||||
| 
 | ||||
|       "Work" shall mean the work of authorship, whether in Source or | ||||
|       Object form, made available under the License, as indicated by a | ||||
|       copyright notice that is included in or attached to the work | ||||
|       (an example is provided in the Appendix below). | ||||
| 
 | ||||
|       "Derivative Works" shall mean any work, whether in Source or Object | ||||
|       form, that is based on (or derived from) the Work and for which the | ||||
|       editorial revisions, annotations, elaborations, or other modifications | ||||
|       represent, as a whole, an original work of authorship. For the purposes | ||||
|       of this License, Derivative Works shall not include works that remain | ||||
|       separable from, or merely link (or bind by name) to the interfaces of, | ||||
|       the Work and Derivative Works thereof. | ||||
| 
 | ||||
|       "Contribution" shall mean any work of authorship, including | ||||
|       the original version of the Work and any modifications or additions | ||||
|       to that Work or Derivative Works thereof, that is intentionally | ||||
|       submitted to Licensor for inclusion in the Work by the copyright owner | ||||
|       or by an individual or Legal Entity authorized to submit on behalf of | ||||
|       the copyright owner. For the purposes of this definition, "submitted" | ||||
|       means any form of electronic, verbal, or written communication sent | ||||
|       to the Licensor or its representatives, including but not limited to | ||||
|       communication on electronic mailing lists, source code control systems, | ||||
|       and issue tracking systems that are managed by, or on behalf of, the | ||||
|       Licensor for the purpose of discussing and improving the Work, but | ||||
|       excluding communication that is conspicuously marked or otherwise | ||||
|       designated in writing by the copyright owner as "Not a Contribution." | ||||
| 
 | ||||
|       "Contributor" shall mean Licensor and any individual or Legal Entity | ||||
|       on behalf of whom a Contribution has been received by Licensor and | ||||
|       subsequently incorporated within the Work. | ||||
| 
 | ||||
|    2. Grant of Copyright License. Subject to the terms and conditions of | ||||
|       this License, each Contributor hereby grants to You a perpetual, | ||||
|       worldwide, non-exclusive, no-charge, royalty-free, irrevocable | ||||
|       copyright license to reproduce, prepare Derivative Works of, | ||||
|       publicly display, publicly perform, sublicense, and distribute the | ||||
|       Work and such Derivative Works in Source or Object form. | ||||
| 
 | ||||
|    3. Grant of Patent License. Subject to the terms and conditions of | ||||
|       this License, each Contributor hereby grants to You a perpetual, | ||||
|       worldwide, non-exclusive, no-charge, royalty-free, irrevocable | ||||
|       (except as stated in this section) patent license to make, have made, | ||||
|       use, offer to sell, sell, import, and otherwise transfer the Work, | ||||
|       where such license applies only to those patent claims licensable | ||||
|       by such Contributor that are necessarily infringed by their | ||||
|       Contribution(s) alone or by combination of their Contribution(s) | ||||
|       with the Work to which such Contribution(s) was submitted. If You | ||||
|       institute patent litigation against any entity (including a | ||||
|       cross-claim or counterclaim in a lawsuit) alleging that the Work | ||||
|       or a Contribution incorporated within the Work constitutes direct | ||||
|       or contributory patent infringement, then any patent licenses | ||||
|       granted to You under this License for that Work shall terminate | ||||
|       as of the date such litigation is filed. | ||||
| 
 | ||||
|    4. Redistribution. You may reproduce and distribute copies of the | ||||
|       Work or Derivative Works thereof in any medium, with or without | ||||
|       modifications, and in Source or Object form, provided that You | ||||
|       meet the following conditions: | ||||
| 
 | ||||
|       (a) You must give any other recipients of the Work or | ||||
|           Derivative Works a copy of this License; and | ||||
| 
 | ||||
|       (b) You must cause any modified files to carry prominent notices | ||||
|           stating that You changed the files; and | ||||
| 
 | ||||
|       (c) You must retain, in the Source form of any Derivative Works | ||||
|           that You distribute, all copyright, patent, trademark, and | ||||
|           attribution notices from the Source form of the Work, | ||||
|           excluding those notices that do not pertain to any part of | ||||
|           the Derivative Works; and | ||||
| 
 | ||||
|       (d) If the Work includes a "NOTICE" text file as part of its | ||||
|           distribution, then any Derivative Works that You distribute must | ||||
|           include a readable copy of the attribution notices contained | ||||
|           within such NOTICE file, excluding those notices that do not | ||||
|           pertain to any part of the Derivative Works, in at least one | ||||
|           of the following places: within a NOTICE text file distributed | ||||
|           as part of the Derivative Works; within the Source form or | ||||
|           documentation, if provided along with the Derivative Works; or, | ||||
|           within a display generated by the Derivative Works, if and | ||||
|           wherever such third-party notices normally appear. The contents | ||||
|           of the NOTICE file are for informational purposes only and | ||||
|           do not modify the License. You may add Your own attribution | ||||
|           notices within Derivative Works that You distribute, alongside | ||||
|           or as an addendum to the NOTICE text from the Work, provided | ||||
|           that such additional attribution notices cannot be construed | ||||
|           as modifying the License. | ||||
| 
 | ||||
|       You may add Your own copyright statement to Your modifications and | ||||
|       may provide additional or different license terms and conditions | ||||
|       for use, reproduction, or distribution of Your modifications, or | ||||
|       for any such Derivative Works as a whole, provided Your use, | ||||
|       reproduction, and distribution of the Work otherwise complies with | ||||
|       the conditions stated in this License. | ||||
| 
 | ||||
|    5. Submission of Contributions. Unless You explicitly state otherwise, | ||||
|       any Contribution intentionally submitted for inclusion in the Work | ||||
|       by You to the Licensor shall be under the terms and conditions of | ||||
|       this License, without any additional terms or conditions. | ||||
|       Notwithstanding the above, nothing herein shall supersede or modify | ||||
|       the terms of any separate license agreement you may have executed | ||||
|       with Licensor regarding such Contributions. | ||||
| 
 | ||||
|    6. Trademarks. This License does not grant permission to use the trade | ||||
|       names, trademarks, service marks, or product names of the Licensor, | ||||
|       except as required for reasonable and customary use in describing the | ||||
|       origin of the Work and reproducing the content of the NOTICE file. | ||||
| 
 | ||||
|    7. Disclaimer of Warranty. Unless required by applicable law or | ||||
|       agreed to in writing, Licensor provides the Work (and each | ||||
|       Contributor provides its Contributions) on an "AS IS" BASIS, | ||||
|       WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | ||||
|       implied, including, without limitation, any warranties or conditions | ||||
|       of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | ||||
|       PARTICULAR PURPOSE. You are solely responsible for determining the | ||||
|       appropriateness of using or redistributing the Work and assume any | ||||
|       risks associated with Your exercise of permissions under this License. | ||||
| 
 | ||||
|    8. Limitation of Liability. In no event and under no legal theory, | ||||
|       whether in tort (including negligence), contract, or otherwise, | ||||
|       unless required by applicable law (such as deliberate and grossly | ||||
|       negligent acts) or agreed to in writing, shall any Contributor be | ||||
|       liable to You for damages, including any direct, indirect, special, | ||||
|       incidental, or consequential damages of any character arising as a | ||||
|       result of this License or out of the use or inability to use the | ||||
|       Work (including but not limited to damages for loss of goodwill, | ||||
|       work stoppage, computer failure or malfunction, or any and all | ||||
|       other commercial damages or losses), even if such Contributor | ||||
|       has been advised of the possibility of such damages. | ||||
| 
 | ||||
|    9. Accepting Warranty or Additional Liability. While redistributing | ||||
|       the Work or Derivative Works thereof, You may choose to offer, | ||||
|       and charge a fee for, acceptance of support, warranty, indemnity, | ||||
|       or other liability obligations and/or rights consistent with this | ||||
|       License. However, in accepting such obligations, You may act only | ||||
|       on Your own behalf and on Your sole responsibility, not on behalf | ||||
|       of any other Contributor, and only if You agree to indemnify, | ||||
|       defend, and hold each Contributor harmless for any liability | ||||
|       incurred by, or claims asserted against, such Contributor by reason | ||||
|       of your accepting any such warranty or additional liability. | ||||
| 
 | ||||
|    END OF TERMS AND CONDITIONS | ||||
| 
 | ||||
|    APPENDIX: How to apply the Apache License to your work. | ||||
| 
 | ||||
|       To apply the Apache License to your work, attach the following | ||||
|       boilerplate notice, with the fields enclosed by brackets "[]" | ||||
|       replaced with your own identifying information. (Don't include | ||||
|       the brackets!)  The text should be enclosed in the appropriate | ||||
|       comment syntax for the file format. We also recommend that a | ||||
|       file or class name and description of purpose be included on the | ||||
|       same "printed page" as the copyright notice for easier | ||||
|       identification within third-party archives. | ||||
| 
 | ||||
|    Copyright [yyyy] [name of copyright owner] | ||||
| 
 | ||||
|    Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|    you may not use this file except in compliance with the License. | ||||
|    You may obtain a copy of the License at | ||||
| 
 | ||||
|        http://www.apache.org/licenses/LICENSE-2.0 | ||||
| 
 | ||||
|    Unless required by applicable law or agreed to in writing, software | ||||
|    distributed under the License is distributed on an "AS IS" BASIS, | ||||
|    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|    See the License for the specific language governing permissions and | ||||
|    limitations under the License. | ||||
|  | @ -0,0 +1,21 @@ | |||
| MIT License | ||||
| 
 | ||||
| Copyright (c) 2022 Holllo <helllo@holllo.cc> | ||||
| 
 | ||||
| Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
| of this software and associated documentation files (the "Software"), to deal | ||||
| in the Software without restriction, including without limitation the rights | ||||
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||||
| copies of the Software, and to permit persons to whom the Software is | ||||
| furnished to do so, subject to the following conditions: | ||||
| 
 | ||||
| The above copyright notice and this permission notice shall be included in all | ||||
| copies or substantial portions of the Software. | ||||
| 
 | ||||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||||
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||
| SOFTWARE. | ||||
|  | @ -0,0 +1,39 @@ | |||
| [tasks.fmt] | ||||
| command = "cargo" | ||||
| args = ["fmt", "${@}"] | ||||
| 
 | ||||
| [tasks.check] | ||||
| command = "cargo" | ||||
| args = ["check", "${@}"] | ||||
| 
 | ||||
| [tasks.clippy] | ||||
| command = "cargo" | ||||
| args = ["clippy", "${@}"] | ||||
| 
 | ||||
| [tasks.test] | ||||
| command = "cargo" | ||||
| args = ["test", "${@}"] | ||||
| 
 | ||||
| [tasks.doc] | ||||
| command = "cargo" | ||||
| args = ["doc", "${@}"] | ||||
| 
 | ||||
| [tasks.build] | ||||
| command = "cargo" | ||||
| args = ["build", "${@}"] | ||||
| 
 | ||||
| [tasks.complete-check] | ||||
| dependencies = ["fmt", "check", "clippy", "test", "doc", "build"] | ||||
| 
 | ||||
| [tasks.code-coverage] | ||||
| workspace = false | ||||
| install_crate = "cargo-tarpaulin" | ||||
| command = "cargo" | ||||
| args = [ | ||||
|   "tarpaulin", | ||||
|   "--exclude-files=target/*", | ||||
|   "--out=html", | ||||
|   "--output-dir=.coverage", | ||||
|   "--skip-clean", | ||||
|   "--target-dir=target/tarpaulin" | ||||
| ] | ||||
|  | @ -0,0 +1,49 @@ | |||
| # Romantic | ||||
| 
 | ||||
| > Roman numeral toolkit | ||||
| 
 | ||||
| ## API | ||||
| 
 | ||||
| For full documentation see [docs.rs]. | ||||
| 
 | ||||
| [docs.rs]: https://docs.rs/romantic | ||||
| 
 | ||||
| ## Examples | ||||
| 
 | ||||
| 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::<i32>("MMXXII").unwrap(), 2022); | ||||
| 
 | ||||
| // The default Roman numeral 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::<i32>("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()); | ||||
| ``` | ||||
| 
 | ||||
| ## License | ||||
| 
 | ||||
| This project is licensed under either of [Apache License, Version 2.0](https://github.com/Holllo/romantic/blob/main/LICENSE-Apache) or [MIT license](https://github.com/Holllo/romantic/blob/main/LICENSE-MIT) at your option. | ||||
| 
 | ||||
| Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this project by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. | ||||
|  | @ -0,0 +1,2 @@ | |||
| max_width = 80 | ||||
| tab_spaces = 2 | ||||
|  | @ -0,0 +1,265 @@ | |||
| #![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::<i32>("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::<i32>("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<char, usize>, | ||||
| 
 | ||||
|   /// The mapping of a magnitude to its corresponding character (ie. 'I' = 1).
 | ||||
|   magnitude_character_map: HashMap<usize, char>, | ||||
| } | ||||
| 
 | ||||
| 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::<i32>("IX").unwrap(), 9);
 | ||||
|   ///
 | ||||
|   /// let custom = Roman::new(&['A', 'B', 'C']);
 | ||||
|   /// assert_eq!(custom.to_string(9).unwrap(), "AC");
 | ||||
|   /// assert_eq!(custom.from_str::<i32>("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::<i32>("IX").unwrap(), 9);
 | ||||
|   ///
 | ||||
|   /// let custom = Roman::new(&['A', 'B', 'C']);
 | ||||
|   /// assert_eq!(custom.from_str::<i32>("AC").unwrap(), 9);
 | ||||
|   /// ```
 | ||||
|   pub fn from_str<T: num::PrimInt>( | ||||
|     &self, | ||||
|     input: &str, | ||||
|   ) -> Result<T, ConversionError> { | ||||
|     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<T: num::PrimInt + ToString>( | ||||
|     &self, | ||||
|     number: T, | ||||
|   ) -> Result<String, ConversionError> { | ||||
|     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<String, ConversionError> { | ||||
|         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()) | ||||
|   } | ||||
| } | ||||
|  | @ -0,0 +1,38 @@ | |||
| use romantic::Roman; | ||||
| 
 | ||||
| use test_case::test_case; | ||||
| 
 | ||||
| #[test_case(00, ""; "empty")] | ||||
| #[test_case(01, "A"; "one")] | ||||
| #[test_case(02, "AA"; "two")] | ||||
| #[test_case(03, "AAA"; "three")] | ||||
| #[test_case(04, "AB"; "four")] | ||||
| #[test_case(05, "B"; "five")] | ||||
| #[test_case(06, "BA"; "six")] | ||||
| #[test_case(07, "BAA"; "seven")] | ||||
| #[test_case(08, "BAAA"; "eight")] | ||||
| #[test_case(09, "AC"; "nine")] | ||||
| #[test_case(10, "C"; "ten")] | ||||
| fn test_to_string<T: num::PrimInt + num::Signed + ToString>( | ||||
|   input: T, | ||||
|   expected: &str, | ||||
| ) { | ||||
|   let custom = Roman::new(&['A', 'B', 'C']); | ||||
|   assert_eq!(custom.to_string(input).unwrap(), expected); | ||||
| } | ||||
| 
 | ||||
| #[test_case("", 00; "empty")] | ||||
| #[test_case("A", 01; "one")] | ||||
| #[test_case("AA", 02; "two")] | ||||
| #[test_case("AAA", 03; "three")] | ||||
| #[test_case("AB", 04; "four")] | ||||
| #[test_case("B", 05; "five")] | ||||
| #[test_case("BA", 06; "six")] | ||||
| #[test_case("BAA", 07; "seven")] | ||||
| #[test_case("BAAA", 08; "eight")] | ||||
| #[test_case("AC", 09; "nine")] | ||||
| #[test_case("C", 10; "ten")] | ||||
| fn test_from_str(input: &str, expected: i32) { | ||||
|   let custom = Roman::new(&['A', 'B', 'C', 'D']); | ||||
|   assert_eq!(custom.from_str::<i32>(input).unwrap(), expected); | ||||
| } | ||||
|  | @ -0,0 +1,40 @@ | |||
| use romantic::Roman; | ||||
| 
 | ||||
| use test_case::test_case; | ||||
| 
 | ||||
| #[test_case(0, ""; "empty")] | ||||
| #[test_case(3888, "MMMDCCCLXXXVIII"; "all characters")] | ||||
| #[test_case(3999, "MMMCMXCIX"; "maximum")] | ||||
| #[test_case(01, "I"; "one")] | ||||
| #[test_case(02, "II"; "two")] | ||||
| #[test_case(03, "III"; "three")] | ||||
| #[test_case(04, "IV"; "four")] | ||||
| #[test_case(05, "V"; "five")] | ||||
| #[test_case(06, "VI"; "six")] | ||||
| #[test_case(07, "VII"; "seven")] | ||||
| #[test_case(08, "VIII"; "eight")] | ||||
| #[test_case(09, "IX"; "nine")] | ||||
| #[test_case(10, "X"; "ten")] | ||||
| fn test_to_string<T: num::PrimInt + num::Signed + ToString>( | ||||
|   input: T, | ||||
|   expected: &str, | ||||
| ) { | ||||
|   assert_eq!(Roman::default().to_string(input).unwrap(), expected); | ||||
| } | ||||
| 
 | ||||
| #[test_case("", 0; "empty")] | ||||
| #[test_case("MMMDCCCLXXXVIII", 3888; "complicated")] | ||||
| #[test_case("MMMCMXCIX", 3999; "maximum")] | ||||
| #[test_case("I", 01; "one")] | ||||
| #[test_case("II", 02; "two")] | ||||
| #[test_case("III", 03; "three")] | ||||
| #[test_case("IV", 04; "four")] | ||||
| #[test_case("V", 05; "five")] | ||||
| #[test_case("VI", 06; "six")] | ||||
| #[test_case("VII", 07; "seven")] | ||||
| #[test_case("VIII", 08; "eight")] | ||||
| #[test_case("IX", 09; "nine")] | ||||
| #[test_case("X", 10; "ten")] | ||||
| fn test_from_str(input: &str, expected: i32) { | ||||
|   assert_eq!(Roman::default().from_str::<i32>(input).unwrap(), expected); | ||||
| } | ||||
|  | @ -0,0 +1,14 @@ | |||
| use romantic::Roman; | ||||
| 
 | ||||
| use test_case::test_case; | ||||
| 
 | ||||
| #[test_case("A"; "invalid character")] | ||||
| fn test_from_str_error(input: &str) { | ||||
|   assert!(Roman::default().from_str::<i32>(input).is_err()); | ||||
| } | ||||
| 
 | ||||
| #[test_case(4000; "too high")] | ||||
| #[test_case(-100; "negative")] | ||||
| fn test_to_string_error(input: i32) { | ||||
|   assert!(Roman::default().to_string(input).is_err()); | ||||
| } | ||||
|  | @ -0,0 +1,27 @@ | |||
| use romantic::Roman; | ||||
| 
 | ||||
| #[test] | ||||
| fn test_readme_roman() { | ||||
|   let roman = Roman::default(); | ||||
| 
 | ||||
|   assert_eq!(roman.to_string(2022).unwrap(), "MMXXII"); | ||||
|   assert_eq!(roman.from_str::<i32>("MMXXII").unwrap(), 2022); | ||||
| 
 | ||||
|   // The default Roman numeral has a maximum of 3999.
 | ||||
|   assert!(roman.to_string(4000).is_err()); | ||||
| } | ||||
| 
 | ||||
| #[test] | ||||
| fn test_readme_custom() { | ||||
|   // 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::<i32>("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()); | ||||
| } | ||||
		Loading…
	
		Reference in New Issue