From e6a0447f9df43aa140affbcc1ad371ea2f362496 Mon Sep 17 00:00:00 2001 From: Martin Haug Date: Wed, 2 Feb 2022 20:56:29 +0100 Subject: [PATCH] Add roman numeral and footnote formatting --- src/library/mod.rs | 2 + src/library/utility.rs | 73 ++++++++++++++++++++++++++++++++++ tests/ref/utility/strings.png | Bin 0 -> 10699 bytes tests/typ/utility/strings.typ | 23 +++++++++++ 4 files changed, 98 insertions(+) create mode 100644 tests/ref/utility/strings.png create mode 100644 tests/typ/utility/strings.typ diff --git a/src/library/mod.rs b/src/library/mod.rs index ae7fc3a16..97d30e317 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -143,6 +143,8 @@ pub fn new() -> Scope { std.def_func("rgb", rgb); std.def_func("lower", lower); std.def_func("upper", upper); + std.def_func("roman", roman); + std.def_func("symbol", symbol); std.def_func("len", len); std.def_func("sorted", sorted); diff --git a/src/library/utility.rs b/src/library/utility.rs index 10c5980ab..1909dff21 100644 --- a/src/library/utility.rs +++ b/src/library/utility.rs @@ -6,6 +6,31 @@ use std::str::FromStr; use super::prelude::*; use crate::eval::Array; +static ROMAN_PAIRS: &'static [(&'static str, u32)] = &[ + ("M̅", 1000000), + ("D̅", 500000), + ("C̅", 100000), + ("L̅", 50000), + ("X̅", 10000), + ("V̅", 5000), + ("I̅V̅", 4000), + ("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), +]; + +static SYMBOLS: &'static [char] = &['*', '†', '‡', '§', '‖', '¶']; + /// Ensure that a condition is fulfilled. pub fn assert(_: &mut EvalContext, args: &mut Args) -> TypResult { let Spanned { v, span } = args.expect::>("condition")?; @@ -201,6 +226,54 @@ pub fn upper(_: &mut EvalContext, args: &mut Args) -> TypResult { Ok(args.expect::("string")?.to_uppercase().into()) } +/// Converts an integer into a roman numeral. +/// +/// Works for integer between 0 and 3,999,999 inclusive, returns None otherwise. +/// Adapted from Yann Villessuzanne's roman.rs under the Unlicense, at +/// https://github.com/linfir/roman.rs/ +pub fn roman(_: &mut EvalContext, args: &mut Args) -> TypResult { + let source: Spanned = args.expect("integer")?; + let mut n = source.v as u32; + + match n { + 0 => return Ok("N".into()), + i if i > 3_999_999 => { + bail!( + source.span, + "cannot convert integers greater than 3,999,999 to roman numerals" + ) + } + _ => {} + } + + let mut roman = String::new(); + for &(name, value) in ROMAN_PAIRS.iter() { + while n >= value { + n -= value; + roman.push_str(name); + } + } + + Ok(roman.into()) +} + +/// Convert a number into a roman numeral. +pub fn symbol(_: &mut EvalContext, args: &mut Args) -> TypResult { + let source: Spanned = args.expect("integer")?; + let n: usize = match source.v.try_into() { + Ok(n) => n, + Err(_) if source.v < 0 => bail!(source.span, "number must not be negative"), + Err(_) => bail!(source.span, "number too large"), + }; + + let symbol = SYMBOLS[n % SYMBOLS.len()]; + let amount = (n / SYMBOLS.len()) + 1; + + let symbols: String = std::iter::repeat(symbol).take(amount).collect(); + + Ok(symbols.into()) +} + /// The length of a string, an array or a dictionary. pub fn len(_: &mut EvalContext, args: &mut Args) -> TypResult { let Spanned { v, span } = args.expect("collection")?; diff --git a/tests/ref/utility/strings.png b/tests/ref/utility/strings.png new file mode 100644 index 0000000000000000000000000000000000000000..c623aa00dc72404c364fe1859b0590133b3e4c0b GIT binary patch literal 10699 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 literal 0 HcmV?d00001 diff --git a/tests/typ/utility/strings.typ b/tests/typ/utility/strings.typ new file mode 100644 index 000000000..15550f168 --- /dev/null +++ b/tests/typ/utility/strings.typ @@ -0,0 +1,23 @@ +// Test string functions. + +--- +// Test the `upper`, `lower`, and number formatting functions. +#upper("Abc 8 def") + +#lower("SCREAMING MUST BE SILENCED in " + roman(1672) + " years") + +#for i in range(9) { + symbol(i) + [ and ] + roman(i) + [ for #i] + parbreak() +} + +--- +// Error: 8-15 cannot convert integers greater than 3,999,999 to roman numerals +#roman(8000000) + +--- +// Error: 9-11 number must not be negative +#symbol(-1)