Roman Numeral Converter — C++ source
Convert integers up to 3,999,999 to Roman numerals and back. Vinculum overline above 3,999, canonical-form validation, a step-by-step greedy breakdown, and 14 language sources. Runs entirely in your browser.
This is the C++ implementation — the same logic the interactive tool runs, in a shareable, citable form.
// roman-numeral-converter - Roman <-> Arabic (vinculum, 1..3,999,999).
//
// Language: C++ (C++17, standard library only)
// Source: CosmoDev polyglot showcase port of the Roman Numeral Converter tool,
// ported from src/lib/roman-numeral.ts (the canonical TypeScript
// implementation); kept in lock-step with the Go twin at
// cli/roman-numeral-converter/roman-numeral-converter.go.
// License: display source - part of CosmoDev's polyglot tool pages.
//
// Design goals:
// - Pure + deterministic; never throws on bad input (returns "" / false).
// - Functionally equivalent to the TS/Go reference: same inputs -> same outputs.
// - Self-contained: stdlib only.
//
// Algorithm: one ordered (value, symbol) table for 1..3,999 drives both
// directions. to_roman greedily subtracts the largest fitting symbol; above
// 3,999 the thousands part is rendered with the same table and each glyph
// gains a combining overline (U+0305) meaning x 1,000. from_roman scans
// left-to-right where a smaller letter before a larger one subtracts
// (IV = 4, CM = 900), then RE-RENDERS the parsed total and rejects anything
// that doesn't round-trip - that one check enforces canonical form
// (rejecting "IIII", "VV", "IC", plain "MMMM" for 4,000).
#include <cctype>
#include <cstring>
#include <string>
#include <utility>
namespace roman {
constexpr long kMax = 3'999'999;
// U+0305 combining overline (vinculum: value x 1,000), as UTF-8 bytes.
constexpr const char* kOverline = "\314\205";
// U+0304 macron - visually near-identical; accepted on input, normalized.
constexpr const char* kMacron = "\314\204";
const std::pair<long, std::string> kBase[13] = {
{1000, "M"}, {900, "CM"}, {500, "D"}, {400, "CD"},
{100, "C"}, {90, "XC"}, {50, "L"}, {40, "XL"},
{10, "X"}, {9, "IX"}, {5, "V"}, {4, "IV"}, {1, "I"},
};
long letter_value(char c) {
switch (c) {
case 'I': return 1;
case 'V': return 5;
case 'X': return 10;
case 'L': return 50;
case 'C': return 100;
case 'D': return 500;
case 'M': return 1000;
default: return 0;
}
}
std::string overline(const std::string& s) {
std::string out;
for (char c : s) out += std::string(1, c) + kOverline;
return out;
}
// Greedy render of 1..3,999.
std::string to_roman_base(long v) {
std::string out;
for (const auto& [val, sym] : kBase) {
while (v >= val) {
out += sym;
v -= val;
}
}
return out;
}
// Converts 1..3,999,999 ("" when out of range). Above 3,999 the thousands
// part carries a combining overline per glyph.
std::string to_roman(long n) {
if (n < 1 || n > kMax) return "";
if (n <= 3999) return to_roman_base(n);
std::string out = overline(to_roman_base(n / 1000));
if (n % 1000) out += to_roman_base(n % 1000);
return out;
}
// One left-to-right pass where a smaller letter before a larger one
// subtracts. Returns junk for non-canonical strings - the round-trip in
// from_roman is the canonicality gate.
long scan_value(const std::string& s) {
long total = 0;
for (size_t i = 0; i < s.size(); ++i) {
long v = letter_value(s[i]);
long next = i + 1 < s.size() ? letter_value(s[i + 1]) : 0;
total += next > v ? -v : v;
}
return total;
}
// Parses a canonical numeral (plain or vinculum). Trimmed and uppercased
// first; a pasted macron counts as the overline mark.
bool from_roman(std::string s, long& out) {
// Normalize: trim ends, uppercase, macron -> overline.
size_t a = 0, b = s.size();
while (a < b && std::isspace(static_cast<unsigned char>(s[a]))) ++a;
while (b > a && std::isspace(static_cast<unsigned char>(s[b - 1]))) --b;
std::string input;
for (size_t i = a; i < b;) {
if (s.compare(i, 2, kMacron) == 0) {
input += kOverline;
i += 2;
} else {
input += static_cast<char>(std::toupper(static_cast<unsigned char>(s[i])));
++i;
}
}
// Split into overlined glyphs (letter + mark) and plain letters.
std::string over, plain;
for (size_t i = 0; i < input.size();) {
if (letter_value(input[i]) == 0) return false;
if (input.compare(i + 1, 2, kOverline) == 0) {
over += input[i];
i += 3; // letter (1 byte) + overline (2 bytes)
} else {
plain += input[i];
++i;
}
}
long total = 0;
if (!over.empty()) total += scan_value(over) * 1000;
if (!plain.empty()) total += scan_value(plain);
if (total < 1 || total > kMax) return false;
if (to_roman(total) != input) return false;
out = total;
return true;
}
} // namespace roman
// ---------- showcase (run: c++ -std=c++17 cpp.cpp && ./a.out) ----------
#include <cassert>
#include <iostream>
int main() {
using namespace roman;
// to_roman - known values, both scales
assert(to_roman(1) == "I");
assert(to_roman(1994) == "MCMXCIV");
assert(to_roman(3999) == "MMMCMXCIX");
assert(to_roman(4000) == "I\314\205V\314\205");
assert(to_roman(4001) == "I\314\205V\314\205I");
assert(to_roman(3999999) ==
"M\314\205M\314\205M\314\205C\314\205M\314\205X\314\205C\314\205I\314\205X\314\205CMXCIX");
// to_roman - out of range
assert(to_roman(0).empty());
assert(to_roman(4000000).empty());
// from_roman - canonical, with case/whitespace/macron tolerance
long v = 0;
assert(from_roman("MCMXCIV", v) && v == 1994);
assert(from_roman(" mcmxciv ", v) && v == 1994);
assert(from_roman("I\314\205V\314\205", v) && v == 4000);
assert(from_roman("I\314\204V\314\204", v) && v == 4000); // macron
// from_roman - non-canonical / invalid
assert(!from_roman("IIII", v));
assert(!from_roman("VV", v));
assert(!from_roman("IC", v));
assert(!from_roman("MMMM", v)); // 4,000 must be vinculum
assert(!from_roman("ABC", v));
assert(!from_roman("", v));
std::cout << "all showcase assertions passed\n";
return 0;
}
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →