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
| Language | Name in Excel |
|---|---|
| Arabic | ROMAN |
| Bulgarian | ROMAN |
| Czech | ROMAN |
| Danish | ROMERTAL |
| German | ROMAN |
| Greek | ROMAN |
| English (Australia) | ROMAN |
| English (United Kingdom) | ROMAN |
| English (United States) | ROMAN |
| Spanish | NUMERO.ROMANO |
| Estonian | ROMAN |
| Finnish | ROMAN |
| French | ROMAIN |
| Hebrew | ROMAN |
| Croatian | ROMAN |
| Hungarian | RÓMAI |
| Indonesian | ROMAN |
| Italian | ROMAN |
| Japanese | ROMAN |
| Korean | ROMAN |
| Lithuanian | ROMAN |
| Latvian | ROMAN |
| Norwegian | ROMAN |
| Dutch | ROMEINS |
| Polish | RZYMSKIE |
| Portuguese (Brazil) | ROMAN |
| Portuguese (Portugal) | ROMANO |
| Romanian | ROMAN |
| Russian | РИМСКОЕ |
| Slovak | ROMAN |
| Slovenian | ROMAN |
| Serbian (Latin) | ROMAN |
| Swedish | ROMERSK |
| Thai | ROMAN |
| Turkish | ROMEN |
| Ukrainian | ROMAN |
| Vietnamese | ROMAN |
| 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:
| Place | Groups |
|---|---|
| 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
| Argument | Meaning | Default |
|---|---|---|
number | The Arabic numeral to convert. Required. | — |
form | How concise the Roman numeral should be. Optional. | 0 (classic) |
Microsoft's VBA reference documents form as follows, verbatim:
| Form | Type |
|---|---|
| 0 or omitted | Classic. |
| 1 | More concise. |
| 2 | More concise. |
| 3 | More concise. |
| 4 | Simplified. |
| TRUE | Classic. |
| FALSE | Simplified. |
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
Quirks
The five forms
- Form 1 adds
VLfor 45–49 andVCfor 95–99 in the tens/units, andLDfor 450–499,LMfor 950–999 in the hundreds. - Form 2 adds
ILfor 49 andICfor 99, andXDfor 490–499,XMfor 990–999. - Form 3 adds
VDfor 495–499 andVMfor 995–999. - Form 4 adds
IDfor 499 andIMfor 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
formis likewise truncated. - Text length. The longest result is
MMMDCCCLXXXVIIIfor 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 GitHub42 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 GitHubfn 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 GitHubfn 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 GitHubfn 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 GitHubfn 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 GitHubfn 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 GitHubfn 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 GitHubfn 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 GitHubfn 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 GitHubfn 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 GitHubpub 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:ARABICmust accept the concise forms thatROMANcan emit, and it accepts many stringsROMANwould never produce. Round-trippingROMANthenARABICis the natural consistency test and is on the probe list below.TEXT— the general number-to-string formatter, which has no Roman format code.ROMANis the only route.BASEandDECIMAL— the other pair of numeral-system converters, for radix 2–36. They are the structural analogue ofROMAN/ARABICfor positional systems, and the contrast is instructive:BASEis total on its domain and mechanical, whileROMANneeds a five-way style argument because the target notation is not positional and admits genuine ambiguity.- Confused with
ROUNDby 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