3

I am looking to take an Integer in Swift and convert it to a Roman Numeral String. Any ideas?

Leo Dabus
  • 229,809
  • 59
  • 489
  • 571
Brian Sachetta
  • 3,319
  • 2
  • 34
  • 45

6 Answers6

7

One could write an extension on Int, similar to the one seen below.

Please note: this code will return "" for numbers less than one. While this is probably okay in terms of Roman Numeral numbers (zero does not exist), you may want to handle this differently in your own implementation.

extension Int {
    var romanNumeral: String {
        var integerValue = self
        // Roman numerals cannot be represented in integers greater than 3999
        if self >= 4000 {
            return self
        } 
        var numeralString = ""
        let mappingList: [(Int, 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")]
        for i in mappingList {
            while (integerValue >= i.0) {
                integerValue -= i.0
                numeralString += i.1
            }
        }
        return numeralString
    }
}

Thanks to Kenneth Bruno for some suggestions on improving the code as well.

Charlton Provatas
  • 2,184
  • 25
  • 18
Brian Sachetta
  • 3,319
  • 2
  • 34
  • 45
  • 1
    You can simplify that code by changing line 6 and 7 to `for i in mappingList where (integerValue >= i.0) {` and all `mappingList[i]` to `i`. I'd also make it a computed property, it's a good fit for that. Seems like a good algorithm to me! –  Mar 17 '16 at 20:12
  • Thanks for the suggestions, @KennethBruno. I updated the code to use the revamped for loop declaration. Also, I'm new to swift, so when I get the chance I will look further into the computed property for the mappingList. – Brian Sachetta Mar 17 '16 at 20:56
  • To make it a computed property change line 2 to `var romanNumeral: String {`. Then you would call it as `number.romanNumeral`, just like a regular property. Oh and remove line 7 and 10, you don't need them any more. –  Mar 17 '16 at 20:59
  • Ok, good to know, thanks. I will certainly consider that. I think the while loop is still necessary, however, since you can have multiple instances of the same letter. For example, 3 = "III". – Brian Sachetta Mar 18 '16 at 01:12
  • Yep, you're right, it has to be in there. I totally missed that! –  Mar 18 '16 at 01:21
  • `for i in mappingList {` would suffice. That first where condition is redundant considering that the `while` method already takes care of it inside the closure. – Leo Dabus Mar 08 '19 at 18:32
3

Here's my version of an int to roman converter (without nested loop) :

extension Int {
    func toRoman() -> String {
        let conversionTable: [(intNumber: Int, romanNumber: 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")]
        var roman = ""
        var remainder = 0
        
        for entry in conversionTable {
            let quotient = (self - remainder) / entry.intNumber
            remainder += quotient * entry.intNumber
            roman += String(repeating: entry.romanNumber, count: quotient)
        }
        
        return roman
    }
}
Guillaume Ramey
  • 205
  • 2
  • 5
1

An addition to Brian Sachetta's version. If you want to go beyond 4999, you can use set of Enhanced Roman Numerals. Largest number in this set is 8,999,999,999,999, which is OZZZQZUQBUGBTGRTHREHMECMXCIX. Set uses all letters in Latin Alphabet.

extension Int {
    var romanNumeral: String {
        var integerValue = self
        var numeralString = ""
        let mappingList: [(Int, String)] = [(5000000000000, "O"), (4000000000000, "ZO"), (1000000000000, "Z"),
                                            (900000000000, "QZ"), (500000000000, "Y"), (400000000000, "QY"), (100000000000, "Q"),
                                            (90000000000, "UQ"), (50000000000, "W"), (40000000000, "UW"), (10000000000, "U"),
                                            (9000000000, "BU"), (5000000000, "A"), (4000000000, "BA"), (1000000000, "B"),
                                            (900000000, "GB"), (500000000, "J"), (400000000, "JG"), (100000000, "G"),
                                            (90000000, "TG"), (50000000, "S"), (40000000, "TS"), (10000000, "T"),
                                            (9000000, "RT"), (5000000, "P"), (4000000, "RP"), (1000000, "R"),
                                            (900000, "HR"), (500000, "K"), (400000, "HK"), (100000, "H"),
                                            (90000, "EH"), (50000, "F"), (40000, "EF"), (10000, "E"),
                                            (9000, "ME"), (5000, "N"), (4000, "MN"), (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")]
        for i in mappingList {
            while (integerValue >= i.0) {
                integerValue -= i.0
                numeralString += i.1
            }
        }
        return numeralString
    }
}
HangarRash
  • 7,314
  • 5
  • 5
  • 32
RichStwrt
  • 65
  • 6
1

Roman Numerals can be thought of as a cipher. You can program the rules for the compound cases, but they're not that consistent, so it's better to handle them as actual cases.

String([RomanNumeral](3456)) // MMMCDLVI
import Algorithms

/// A cipher between numbers and strings.
/// - Precondition: `allCases` is sorted.
public protocol NumericCipher: RawRepresentable & CaseIterable
where RawValue: BinaryInteger, AllCases: BidirectionalCollection { }

public extension Array where Element: NumericCipher {
  init(_ number: Element.RawValue) {
    self = .init(
      sequence(
        state: (remainder: number, index: Element.allCases.indices.last!)
      ) { state in
        guard let (index, element) = Element.allCases.indexed()
          .prefix(through: state.index)
          .last(where: { $0.element.rawValue <= state.remainder })
        else { return nil }

        state.remainder -= element.rawValue
        state.index = index
        return element
      }
    )
  }
}

public extension String {
  init(_ cipher: some Sequence<some NumericCipher>) {
    self = cipher.map { "\($0)" }.joined()
  }
}
public enum RomanNumeral: Int {
  case  i =    1
  case iv =    4
  case  v =    5
  case  x =   10
  case xl =   40
  case  l =   50
  case xc =   90
  case  c =  100
  case cd =  400
  case  d =  500
  case cm =  900
  case  m = 1000
}

extension RomanNumeral: CustomStringConvertible {
  public var description: String {
    switch self {
    case .i: return "I"
    case .iv: return "\(Self.i)\(Self.v)"
    case .v: return "V"
    case .x: return "X"
    case .xl: return "\(Self.x)\(Self.l)"
    case .l: return "L"
    case .xc: return "\(Self.x)\(Self.c)"
    case .c: return "C"
    case .cd: return "\(Self.c)\(Self.d)"
    case .d: return "D"
    case .cm: return "\(Self.c)\(Self.m)"
    case .m: return "M"
    }
  }
}

extension RomanNumeral: NumericCipher { }
0

One more for good measure:

fileprivate let romanNumerals: [String] = ["M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"]
fileprivate let arabicNumerals: [Int] = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1]

extension Int {
    var romanRepresentation: String {
        guard self > 0 && self < 4000 else {
            return "Invalid Number"
        }
        var control: Int = self
        return zip(arabicNumerals, romanNumerals)
            .reduce(into: "") { partialResult, ar in
                partialResult += String(repeating: ar.1, count: control/ar.0)
                control = control % ar.0
            }
    }
}
P. A. Monsaille
  • 152
  • 1
  • 1
  • 10
-2
    extension Int {
    func convertToOrdinal() -> String {
        let numberFormatter = NumberFormatter()
        numberFormatter.numberStyle = .ordinal
        
        guard let ordinalString = numberFormatter.string(from: NSNumber(value: self)) else {
            return "\(self)"
        }
        
        return ordinalString
    }
}