Math and trigonometry functions

ROMAN

Matches all 4 recorded answers

Recorded: 4 workbook cells. How we measure this · See the Rust code

Local names in 40 languages
LanguageName in Excel
ArabicROMAN
BulgarianROMAN
CzechROMAN
DanishROMERTAL
GermanROMAN
GreekROMAN
English (Australia)ROMAN
English (United Kingdom)ROMAN
English (United States)ROMAN
SpanishNUMERO.ROMANO
EstonianROMAN
FinnishROMAN
FrenchROMAIN
HebrewROMAN
CroatianROMAN
HungarianRÓMAI
IndonesianROMAN
ItalianROMAN
JapaneseROMAN
KoreanROMAN
LithuanianROMAN
LatvianROMAN
NorwegianROMAN
DutchROMEINS
PolishRZYMSKIE
Portuguese (Brazil)ROMAN
Portuguese (Portugal)ROMANO
RomanianROMAN
RussianРИМСКОЕ
SlovakROMAN
SlovenianROMAN
Serbian (Latin)ROMAN
SwedishROMERSK
ThaiROMAN
TurkishROMEN
UkrainianROMAN
VietnameseROMAN
Chinese (Simplified)ROMAN
Chinese (Hong Kong)ROMAN
Chinese (Traditional)ROMAN

Names from Microsoft's support site.

What it does

ROMAN(number, [form])

ROMAN(number, [form]) converts an Arabic numeral to a Roman numeral, returned as text.

The classical system is additive with a subtractive shorthand: the letters I V X L C D M have values 1, 5, 10, 50, 100, 500, 1000, and a smaller letter placed before a larger one is subtracted. Standard rendering decomposes the number by decimal place and emits the canonical group for each:

PlaceGroups
thousands"", M, MM, MMM
hundreds"", C, CC, CCC, CD, D, DC, DCC, DCCC, CM
tens"", X, XX, XXX, XL, L, LX, LXX, LXXX, XC
units"", I, II, III, IV, V, VI, VII, VIII, IX

The domain ceiling of 3999 follows directly: the thousands row runs out at MMM, and the classical system has no letter for 5000 — the overbar notation for larger values is not available in plain text. The floor is 0, which the classical system also has no letter for.

form selects how aggressively the rendering may abbreviate beyond the canonical groups. That is where the function stops being mathematics and becomes a table of conventions — see below.

Arguments

ArgumentMeaningDefault
numberThe Arabic numeral to convert. Required.—
formHow concise the Roman numeral should be. Optional.0 (classic)

Microsoft's VBA reference documents form as follows, verbatim:

FormType
0 or omittedClassic.
1More concise.
2More concise.
3More concise.
4Simplified.
TRUEClassic.
FALSESimplified.

What the documentation does not say is the whole difficulty. Forms 1, 2 and 3 are all described as "More concise", with no statement of how they differ, no examples, and no rule. A reader cannot determine from Microsoft's documentation what ROMAN(499, 2) should be. The only way to know is to observe it.

Try it

Input cells (optional)

Example: {"B2": 3, "B3": 4}. A1 holds the formula. Other cells need values before a formula can read them.

Enter a formula, then calculate.

Runs locally in your browser with OneCalc. Its answer may differ from Excel. Use English function names and commas between arguments.

Quirks

The five forms

  • Form 1 adds VL for 45–49 and VC for 95–99 in the tens/units, and LD for 450–499, LM for 950–999 in the hundreds.
  • Form 2 adds IL for 49 and IC for 99, and XD for 490–499, XM for 990–999.
  • Form 3 adds VD for 495–499 and VM for 995–999.
  • Form 4 adds ID for 499 and IM for 999.

The right way to read the ladder is as a nesting: ROMAN(n, k) is never longer than ROMAN(n, k−1), and the forms differ only on numbers close to 50, 100, 500 and 1000 from below. For every other input all five forms agree.

Result and edge cases

Returns Text — a string, not a number. This is the only surface in this batch whose result kind is not Number, and it matters downstream: the result does not participate in arithmetic without coercion, and it sorts as text.

  • Non-integral form is likewise truncated.
  • Text length. The longest result is MMMDCCCLXXXVIII for 3888 in form 0 — fifteen characters, comfortably inside every text limit.

How to implement

Notes for implementers

The place-value decomposition is the whole algorithm for form 0, and it should be written as four table lookups rather than a greedy subtraction loop. The greedy loop happens to produce the same answer for form 0 and cannot express forms 1–4 at all, because those introduce contractions that cross place boundaries (VL spans tens and units; LD spans hundreds and tens).

Forms are ranges, not points. The concise contractions apply to bands — 45–49, 95–99, 450–499, 490–499, 495–499, 499 — each of which is rendered as a prefix plus the standard rendering of the remainder. Getting a band boundary wrong produces a wrong answer on exactly one or two inputs out of four thousand, which no casual test will find. The full domain is only 4000 × 5 inputs; an implementation should simply enumerate all 20,000 and compare.

The thousands prefix is form-independent. All five forms render the thousands digit as ""/M/MM/MMM and apply their contractions only to the remainder below 1000. That is what makes IM for 999 possible while MIM for 1999 comes out as M + IM.

Preserve the value kind. The result is text. An implementation that returns a number-shaped value, or that lets the empty-string case become an empty cell, changes downstream behaviour — see the raw-versus-published boundary in The value universe.

Rust code

Show the CalcFunc source of this function

Quoted from the public repository DnaCalc/DnaCalc, crates calc-func and calc-fml, at commit 9e1884369a67. Comments are the source's own. A kernel shared with other functions appears once, on its module page.

ROMAN · 15 lines · calc-func/src/catalogue/math.rs on GitHub
42 roman "ROMAN" (1, 2) [V "number", V "form"] |s, a, sv| {
    let n = num(s, a, 0)?;
    let form = match a.get(1).map(CalcValue::core_ref) {
        None | Some(CoreRef::Missing | CoreRef::Empty | CoreRef::Logical(true)) => 0,
        Some(CoreRef::Logical(false)) => 4,
        Some(_) => {
            let f = num(s, a, 1)?.trunc();
            if !(0.0..=4.0).contains(&f) {
                return fail(WorksheetErrorCode::Value);
            }
            f as i32
        }
    };
    out_text(k::roman_fn::roman_kernel(n, form))
}
standard_roman · 7 lines · calc-func/src/ported/roman_fn.rs on GitHub
fn standard_roman(number: u32) -> String {
    let thousands = THOUSANDS[(number / 1000) as usize];
    let hundreds = HUNDREDS[((number % 1000) / 100) as usize];
    let tens = TENS[((number % 100) / 10) as usize];
    let ones = ONES[(number % 10) as usize];
    format!("{thousands}{hundreds}{tens}{ones}")
}
with_standard_suffix · 7 lines · calc-func/src/ported/roman_fn.rs on GitHub
fn with_standard_suffix(prefix: &str, remainder: u32) -> String {
    if remainder == 0 {
        prefix.to_string()
    } else {
        format!("{prefix}{}", standard_roman(remainder))
    }
}
roman_form1_under_100 · 7 lines · calc-func/src/ported/roman_fn.rs on GitHub
fn roman_form1_under_100(number: u32) -> String {
    match number {
        45..=49 => with_standard_suffix("VL", number - 45),
        95..=99 => with_standard_suffix("VC", number - 95),
        _ => standard_roman(number),
    }
}
roman_form2_under_100 · 9 lines · calc-func/src/ported/roman_fn.rs on GitHub
fn roman_form2_under_100(number: u32) -> String {
    match number {
        49 => "IL".to_string(),
        45..=48 => with_standard_suffix("VL", number - 45),
        99 => "IC".to_string(),
        95..=98 => with_standard_suffix("VC", number - 95),
        _ => standard_roman(number),
    }
}
roman_form1_under_1000 · 23 lines · calc-func/src/ported/roman_fn.rs on GitHub
fn roman_form1_under_1000(number: u32) -> String {
    match number {
        0..=99 => roman_form1_under_100(number),
        100..=399 => format!(
            "{}{}",
            "C".repeat((number / 100) as usize),
            roman_form1_under_100(number % 100)
        ),
        400..=449 => format!("CD{}", roman_form1_under_100(number - 400)),
        450..=499 => format!("LD{}", roman_form1_under_100(number - 450)),
        500..=899 => {
            let remainder = number - 500;
            format!(
                "D{}{}",
                "C".repeat((remainder / 100) as usize),
                roman_form1_under_100(remainder % 100)
            )
        }
        900..=949 => format!("CM{}", roman_form1_under_100(number - 900)),
        950..=999 => format!("LM{}", roman_form1_under_100(number - 950)),
        _ => unreachable!("number out of range"),
    }
}
roman_form2_under_1000 · 23 lines · calc-func/src/ported/roman_fn.rs on GitHub
fn roman_form2_under_1000(number: u32) -> String {
    match number {
        0..=99 => roman_form2_under_100(number),
        100..=399 => format!(
            "{}{}",
            "C".repeat((number / 100) as usize),
            roman_form2_under_100(number % 100)
        ),
        400..=489 => format!("CD{}", roman_form2_under_100(number - 400)),
        490..=499 => format!("XD{}", standard_roman(number - 490)),
        500..=899 => {
            let remainder = number - 500;
            format!(
                "D{}{}",
                "C".repeat((remainder / 100) as usize),
                roman_form2_under_100(remainder % 100)
            )
        }
        900..=989 => format!("CM{}", roman_form2_under_100(number - 900)),
        990..=999 => format!("XM{}", standard_roman(number - 990)),
        _ => unreachable!("number out of range"),
    }
}
roman_form3_under_1000 · 25 lines · calc-func/src/ported/roman_fn.rs on GitHub
fn roman_form3_under_1000(number: u32) -> String {
    match number {
        0..=99 => roman_form2_under_100(number),
        100..=399 => format!(
            "{}{}",
            "C".repeat((number / 100) as usize),
            roman_form2_under_100(number % 100)
        ),
        400..=489 => format!("CD{}", roman_form2_under_100(number - 400)),
        490..=494 => format!("XD{}", standard_roman(number - 490)),
        495..=499 => format!("VD{}", standard_roman(number - 495)),
        500..=899 => {
            let remainder = number - 500;
            format!(
                "D{}{}",
                "C".repeat((remainder / 100) as usize),
                roman_form2_under_100(remainder % 100)
            )
        }
        900..=989 => format!("CM{}", roman_form2_under_100(number - 900)),
        990..=994 => format!("XM{}", standard_roman(number - 990)),
        995..=999 => format!("VM{}", standard_roman(number - 995)),
        _ => unreachable!("number out of range"),
    }
}
roman_form4_under_1000 · 27 lines · calc-func/src/ported/roman_fn.rs on GitHub
fn roman_form4_under_1000(number: u32) -> String {
    match number {
        499 => "ID".to_string(),
        999 => "IM".to_string(),
        0..=99 => roman_form2_under_100(number),
        100..=399 => format!(
            "{}{}",
            "C".repeat((number / 100) as usize),
            roman_form2_under_100(number % 100)
        ),
        400..=489 => format!("CD{}", roman_form2_under_100(number - 400)),
        490..=494 => format!("XD{}", standard_roman(number - 490)),
        495..=498 => format!("VD{}", standard_roman(number - 495)),
        500..=899 => {
            let remainder = number - 500;
            format!(
                "D{}{}",
                "C".repeat((remainder / 100) as usize),
                roman_form2_under_100(remainder % 100)
            )
        }
        900..=989 => format!("CM{}", roman_form2_under_100(number - 900)),
        990..=994 => format!("XM{}", standard_roman(number - 990)),
        995..=998 => format!("VM{}", standard_roman(number - 995)),
        _ => unreachable!("number out of range"),
    }
}
roman_by_form · 13 lines · calc-func/src/ported/roman_fn.rs on GitHub
fn roman_by_form(number: u32, form: i32) -> String {
    let thousands = THOUSANDS[(number / 1000) as usize];
    let remainder = number % 1000;
    let tail = match form {
        0 => standard_roman(remainder),
        1 => roman_form1_under_1000(remainder),
        2 => roman_form2_under_1000(remainder),
        3 => roman_form3_under_1000(remainder),
        4 => roman_form4_under_1000(remainder),
        _ => unreachable!("validated before dispatch"),
    };
    format!("{thousands}{tail}")
}
roman_kernel · 11 lines · calc-func/src/ported/roman_fn.rs on GitHub
pub fn roman_kernel(number: f64, form: i32) -> Result<ExcelText, WorksheetErrorCode> {
    let number = number.trunc();
    if !number.is_finite() || !(0.0..=3999.0).contains(&number) || !(0..=4).contains(&form) {
        return Err(WorksheetErrorCode::Value);
    }

    let roman = roman_by_form(number as u32, form);
    Ok(ExcelText::from_utf16_code_units(
        roman.encode_utf16().collect(),
    ))
}

Argument and result plumbing

No implementation has been published with a Handbook test suite for this function.

Version differences

No comparison between specific Excel builds is recorded here yet. This does not establish that all builds behave alike.

Alternatives

Relationships

  • ARABIC — the inverse direction, parsing a Roman numeral back to a number. The pair is not a clean bijection: ARABIC must accept the concise forms that ROMAN can emit, and it accepts many strings ROMAN would never produce. Round-tripping ROMAN then ARABIC is the natural consistency test and is on the probe list below.
  • TEXT — the general number-to-string formatter, which has no Roman format code. ROMAN is the only route.
  • BASE and DECIMAL — the other pair of numeral-system converters, for radix 2–36. They are the structural analogue of ROMAN/ARABIC for positional systems, and the contrast is instructive: BASE is total on its domain and mechanical, while ROMAN needs a five-way style argument because the target notation is not positional and admits genuine ambiguity.
  • Confused with ROUND by autocomplete alone. They share four letters and nothing else.

Named variants

No named variants are recorded yet.

Suggest a correction or contribute an implementation · For agents