2
\$\begingroup\$

The following converts Arabic numbers to Roman numerals.

I decided to factor each digit into its place value, then use a mapping to convert it to its Roman equivalent.

For example, 1997 is factored to:

\$(1 \times 10^3) + (9 \times 10^2) + (9 \times 10^1) + (7 \times 10^0) = 1000 + 900 + 90 + 7\$

The Roman numerals for those numbers are then retrieved from the mapping.

class Integer
  def to_roman
    mappings = {
         1 => 'I',
         2 => 'II',
         3 => 'III',
         4 => 'IV',
         5 => 'V',
         6 => 'VI',
         7 => 'VII',
         8 => 'VIII',
         9 => 'IX',
        10 => 'X',
        20 => 'XX',
        30 => 'XXX',
        40 => 'XL',
        50 => 'L',
        60 => 'LX',
        70 => 'LXX',
        80 => 'LXXX',
        90 => 'XC',
       100 => 'C',
       200 => 'CC',
       300 => 'CCC',
       400 => 'CD',
       500 => 'D',
       600 => 'DC',
       700 => 'DCC',
       800 => 'DCCC',
       900 => 'CM',
      1000 => 'M',
      2000 => 'MM',
      3000 => 'MMM'
    }

    roman = []
    to_s.chars.reverse.each.with_index(0) do |digit, i|
      roman.unshift(mappings[digit.to_i * 10**i])
    end

    roman.join
  end
end

I like using exponents to factor each digit into its value place. I don't like having to define a mapping for each number from 1 to 3 and 6 to 8, and repeat that for the 10s, 100s, and 1000s digits.

\$\endgroup\$
2
  • \$\begingroup\$ Roman numerals sure are very archaic, I hope there is a way to map them more efficiently, but I'm not holding my breath... \$\endgroup\$
    – Phrancis
    Commented Apr 15, 2016 at 2:19
  • 1
    \$\begingroup\$ Some nice previous work: codereview.stackexchange.com/questions/7937/… \$\endgroup\$
    – Dan Kohn
    Commented Apr 15, 2016 at 3:05

2 Answers 2

2
\$\begingroup\$

Similar to dankohn's comment, there are really only a handful of operations going on, just with different character sets.

Here is my quick implementation. It only handles the base 10 digits, and dynamically swaps out character sets depending on the power of the number.

def to_roman(digits)
    numeral_sets = {  1=>['I', 'V', 'X'], 
                     10=>['X', 'L', 'C'], 
                    100=>['C', 'D', 'M'], 
                   1000=>['M', "\u2181", "\u2182"] }

    str = ''
    digits.reverse.each.with_index do |d, ix|
        set = numeral_sets[10**ix]
        case d
            when 0 then roman = ''
            when 1..3 then roman = set[0] * d
            when 4 then roman = "%s%s" % [set[0], set[1]]
            when 5 then roman = set[1]
            when 6..8 then roman = "%s%s" % [set[1], set[0] * (d-5)]
            when 9 then roman = "%s%s" % [set[0], set[2]]
        end
        str = str.insert(0, roman)
    end
    str
end

And a few tests:

puts to_roman [1,9,9,7] # expected MCMXCVII
puts to_roman [1,9,0,7] # expected MCMVII
puts to_roman [5,6,7,8] # expected ↁDCLXXVIII
\$\endgroup\$
1
\$\begingroup\$

This should do it too:

class Fixnum
  NUMERAL_SETS = {
    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"
  }

  def to_roman
    result, num = [], self
    until num.zero?
      NUMERAL_SETS.each do |key, val|
        div, mod = num.divmod(key)
        result << val * div
        num = mod
      end
    end
    result.join ''
  end
end

The good thing about this is you don't need to enumerate all the numbers from 1 to 9, then 10 to 90, then 100 to 900, then 1000 to 3000.

The NUMERAL_SETS hash will only contain the bare minimum that you need to construct a Roman numeral.

The divmod method will return [x / y, x % y] so you immediately can append the letter to a result array (complexity: O(n)) and then join this array into a string (complexity: O(n)).

I'm not saying this is the best way to do it, but just thought I might point out another solution :)

You can test with:

RSpec.describe Fixnum do
  it '#to_roman converts 1 to I' do
    expect(1.to_roman).to eq 'I'
  end
  it '#to_roman converts 2 to II' do
    expect(2.to_roman).to eq 'II'
  end
  it '#to_roman converts 3 to III' do
    expect(3.to_roman).to eq 'III'
  end
  it '#to_roman converts 5 to V' do
    expect(5.to_roman).to eq 'V'
  end
  it '#to_roman converts 999 to CMXCIX' do
    expect(999.to_roman).to eq 'CMXCIX'
  end
  it '#to_roman converts 532 to DXXXII' do
    expect(532.to_roman).to eq 'DXXXII'
  end
  it '#to_roman converts 2016 to MMXVI' do
    expect(2016.to_roman).to eq 'MMXVI'
  end
end
\$\endgroup\$
1
  • \$\begingroup\$ Please provide more critique and/or explain what is better in your solution \$\endgroup\$
    – Caridorc
    Commented Jun 2, 2016 at 16:53

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.