Roman Numeral Converter — Swift 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 Swift implementation — the same logic the interactive tool runs, in a shareable, citable form.
// roman-numeral-converter - Roman <-> Arabic (vinculum, 1..3,999,999).
//
// Language: Swift (5.9+, Foundation only for the showcase entry point)
// 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 crashes on bad input (returns "" / nil).
// - Functionally equivalent to the TS/Go reference: same inputs -> same outputs.
// - Self-contained: no third-party dependencies.
//
// Algorithm: one ordered (value, symbol) table for 1..3,999 drives both
// directions. toRoman 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. fromRoman 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).
//
// Swift note: a letter plus its combining overline forms ONE Character
// (grapheme cluster), so parsing iterates unicodeScalars - never characters.
import Foundation
enum RomanNumeral {
static let overline = "\u{0305}" // vinculum: value x 1,000
static let macron = "\u{0304}" // accepted on input, normalized
static let maxRoman = 3_999_999
static let base: [(value: Int, symbol: String)] = [
(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"),
]
static let letterValues: [Character: Int] = [
"I": 1, "V": 5, "X": 10, "L": 50, "C": 100, "D": 500, "M": 1000,
]
static func overline(_ s: String) -> String {
var out = ""
for c in s {
out.append(c)
out += overlineMark
}
return out
}
/// Greedy render of 1..3,999.
static func toRomanBase(_ v: Int) -> String {
var out = ""
var v = v
for (value, symbol) in base {
while v >= value {
out += symbol
v -= value
}
}
return out
}
/// Converts 1..3,999,999 ("" when out of range). Above 3,999 the
/// thousands part carries a combining overline per glyph.
static func toRoman(_ n: Int) -> String {
guard n >= 1, n <= maxRoman else { return "" }
if n <= 3999 { return toRomanBase(n) }
var out = overline(toRomanBase(n / 1000))
if n % 1000 > 0 { out += toRomanBase(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
/// fromRoman is the canonicality gate. Codepoints only (marks stripped).
static func scanValue(_ scalars: [Unicode.Scalar]) -> Int {
var total = 0
for (i, scalar) in scalars.enumerated() {
guard let v = letterValues[Character(scalar)] else { continue }
let next = i + 1 < scalars.count ? letterValues[Character(scalars[i + 1])] ?? 0 : 0
total += next > v ? -v : v
}
return total
}
/// Parses a canonical numeral (plain or vinculum), or nil. Trimmed and
/// uppercased first; a pasted macron counts as the overline mark.
static func fromRoman(_ s: String) -> Int? {
let input = s.trimmingCharacters(in: .whitespacesAndNewlines)
.uppercased()
.replacingOccurrences(of: macron, with: overlineMark)
var over: [Unicode.Scalar] = []
var plain: [Unicode.Scalar] = []
let scalars = Array(input.unicodeScalars)
var i = 0
while i < scalars.count {
guard letterValues[Character(scalars[i])] != nil else { return nil }
if i + 1 < scalars.count && scalars[i + 1].value == 0x0305 {
over.append(scalars[i])
i += 2
} else {
plain.append(scalars[i])
i += 1
}
}
var total = 0
if !over.isEmpty { total += scanValue(over) * 1000 }
if !plain.isEmpty { total += scanValue(plain) }
guard total >= 1, total <= maxRoman else { return nil }
return toRoman(total) == input ? total : nil
}
}
// Rename clarity: the mark constant ("overline" is already a function name).
private extension RomanNumeral {
static let overlineMark = "\u{0305}"
}
// ---------- showcase (run: swift swift.swift) ----------
print(RomanNumeral.showcase())
extension RomanNumeral {
static func showcase() -> String {
// toRoman - known values, both scales
assert(toRoman(1) == "I")
assert(toRoman(1994) == "MCMXCIV")
assert(toRoman(3999) == "MMMCMXCIX")
assert(toRoman(4000) == "I\u{0305}V\u{0305}")
assert(toRoman(4001) == "I\u{0305}V\u{0305}I")
assert(toRoman(3_999_999) == "M\u{0305}M\u{0305}M\u{0305}C\u{0305}M\u{0305}X\u{0305}C\u{0305}I\u{0305}X\u{0305}CMXCIX")
// toRoman - out of range
assert(toRoman(0) == "")
assert(toRoman(4_000_000) == "")
// fromRoman - canonical, with case/whitespace/macron tolerance
assert(fromRoman("MCMXCIV") == 1994)
assert(fromRoman(" mcmxciv ") == 1994)
assert(fromRoman("I\u{0305}V\u{0305}") == 4000)
assert(fromRoman("I\u{0304}V\u{0304}") == 4000) // macron
// fromRoman - non-canonical / invalid
assert(fromRoman("IIII") == nil)
assert(fromRoman("VV") == nil)
assert(fromRoman("IC") == nil)
assert(fromRoman("MMMM") == nil) // 4,000 must be vinculum
assert(fromRoman("ABC") == nil)
assert(fromRoman("") == nil)
return "all showcase assertions passed"
}
}
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 →