diff --git a/core/src/ast.rs b/core/src/ast.rs index 958fc54a..24e2f6cd 100644 --- a/core/src/ast.rs +++ b/core/src/ast.rs @@ -9,7 +9,7 @@ use crate::serialize::{Deserialize, Serialize}; use crate::value::{built_in_function::BuiltInFunction, ApplyMulHandling, Value}; use crate::Attrs; use std::sync::Arc; -use std::{cmp, fmt, io}; +use std::{borrow, cmp, fmt, io}; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) enum BitwiseBop { @@ -621,6 +621,15 @@ fn evaluate_as( } return Err(FendError::ExpectedANumber); } + "roman" | "roman_numeral" => { + let a = evaluate(a, scope, attrs, context, int)? + .expect_num()? + .try_as_usize(int)?; + if a == 0 { + return Err(FendError::RomanNumeralZero); + } + return Ok(Value::String(borrow::Cow::Owned(to_roman(a)))); + } _ => (), } } @@ -754,3 +763,29 @@ pub(crate) fn resolve_identifier( _ => return crate::units::query_unit(ident.as_str(), attrs, context, int), }) } + +fn to_roman(mut num: usize) -> String { + let mut result = String::new(); + for (r, n) in [ + ("M", 1000), + ("CM", 900), + ("D", 500), + ("CD", 400), + ("C", 100), + ("XC", 90), + ("L", 50), + ("XL", 40), + ("X", 10), + ("IX", 9), + ("V", 5), + ("IV", 4), + ("I", 1), + ] { + let q = num / n; + num -= q * n; + for _ in 0..q { + result.push_str(r); + } + } + result +} diff --git a/core/src/error.rs b/core/src/error.rs index 95050f46..f2a50b91 100644 --- a/core/src/error.rs +++ b/core/src/error.rs @@ -94,6 +94,7 @@ pub(crate) enum FendError { before: date::Date, after: date::Date, }, + RomanNumeralZero, } impl fmt::Display for FendError { @@ -248,6 +249,7 @@ impl fmt::Display for FendError { "{month} {expected_day}, {year} does not exist, did you mean {before} or {after}?", ) } + Self::RomanNumeralZero => write!(f, "zero cannot be represented as a roman numeral"), } } } diff --git a/core/tests/integration_tests.rs b/core/tests/integration_tests.rs index 1585d9f1..b8e0d0db 100644 --- a/core/tests/integration_tests.rs +++ b/core/tests/integration_tests.rs @@ -5899,3 +5899,40 @@ fn test_equality() { test_eval("2.010m == 200cm", "false"); test_eval("2.000m == approx. 200cm", "true"); } + +#[test] +fn test_roman() { + expect_error( + "0 to roman", + Some("zero cannot be represented as a roman numeral"), + ); + test_eval_simple("1 to roman", "I"); + test_eval_simple("2 to roman", "II"); + test_eval_simple("3 to roman", "III"); + test_eval_simple("4 to roman", "IV"); + test_eval_simple("5 to roman", "V"); + test_eval_simple("6 to roman", "VI"); + test_eval_simple("7 to roman", "VII"); + test_eval_simple("8 to roman", "VIII"); + test_eval_simple("9 to roman", "IX"); + test_eval_simple("10 to roman", "X"); + test_eval_simple("11 to roman", "XI"); + test_eval_simple("12 to roman", "XII"); + test_eval_simple("13 to roman", "XIII"); + test_eval_simple("14 to roman", "XIV"); + test_eval_simple("15 to roman", "XV"); + test_eval_simple("16 to roman", "XVI"); + test_eval_simple("17 to roman", "XVII"); + test_eval_simple("18 to roman", "XVIII"); + test_eval_simple("19 to roman", "XIX"); + test_eval_simple("20 to roman", "XX"); + test_eval_simple("21 to roman", "XXI"); + test_eval_simple("22 to roman", "XXII"); + test_eval_simple("45 to roman", "XLV"); + test_eval_simple("134 to roman", "CXXXIV"); + test_eval_simple("1965 to roman", "MCMLXV"); + test_eval_simple("2020 to roman", "MMXX"); + test_eval_simple("3456 to roman", "MMMCDLVI"); + test_eval_simple("1452 to roman", "MCDLII"); + test_eval_simple("20002 to roman", "MMMMMMMMMMMMMMMMMMMMII"); +}