Ryan-Neal Mes
Ryan-Neal Mes

Reputation: 6263

Difference between + and << in Ruby

Below are two identical classes with the difference of the operators + and <<. These can be found in the inject method. In the + case the tests pass and in the << some of them fail. Why?

class Integer
  ROMAN_NUMERALS = {
    0 => '',
    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'
  }

  def to_roman
    to_s.reverse.chars.each_with_index.inject("") do |roman_numeral, (character, index)|
      ROMAN_NUMERALS[character.to_i * 10 ** index] << roman_numeral
    end
  end
end

I get different results to when I run

class Integer
  ROMAN_NUMERALS = {
    0 => '',
    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'
  }

  def to_roman
    to_s.reverse.chars.each_with_index.inject("") do |roman_numeral, (character, index)|
      ROMAN_NUMERALS[character.to_i * 10 ** index] + roman_numeral
    end
  end
end

The tests I am using are below

require 'minitest/autorun'
require_relative 'roman'

class RomanTest < MiniTest::Unit::TestCase
  def test_1
    assert_equal 'I', 1.to_roman
  end

  def test_2
    assert_equal 'II', 2.to_roman
  end

  def test_3
    assert_equal 'III', 3.to_roman
  end

  def test_4
    assert_equal 'IV', 4.to_roman
  end

  def test_5
    assert_equal 'V', 5.to_roman
  end

  def test_6
    assert_equal 'VI', 6.to_roman
  end

  def test_9
    assert_equal 'IX', 9.to_roman
  end

  def test_27
    assert_equal 'XXVII', 27.to_roman
  end

  def test_48
    assert_equal 'XLVIII', 48.to_roman
  end

  def test_59
    assert_equal 'LIX', 59.to_roman
  end

  def test_93
    assert_equal 'XCIII', 93.to_roman
  end

  def test_141
    assert_equal 'CXLI', 141.to_roman
  end

  def test_163
    assert_equal 'CLXIII', 163.to_roman
  end

  def test_402
    assert_equal 'CDII', 402.to_roman
  end

  def test_575
    assert_equal 'DLXXV', 575.to_roman
  end

  def test_911
    assert_equal 'CMXI', 911.to_roman
  end

  def test_1024
    assert_equal 'MXXIV', 1024.to_roman
  end

  def test_3000
    assert_equal 'MMM', 3000.to_roman
  end
end

See how the specs fail in one case but not in the other. I thought these are meant to work in the same way.

Upvotes: 0

Views: 119

Answers (3)

Cary Swoveland
Cary Swoveland

Reputation: 110755

Now that your question has been answered, I would like to suggest an alternative method and also a different way to perform your tests. This requires Ruby v1.9+, so we can depend on the order of the hash keys.

Code

First, reverse the order of the hash elements.

RNR = Hash[ROMAN_NUMERALS.to_a.reverse]
  #=> {3000=>"MMM", 2000=>"MM", 1000=>"M",..., 2=>"II", 1=>"I", 0=>""}  

Then:

class Integer
  def to_roman
    num = self
    roman = ""
    while num > 0
      i,r = RNR.find { |i,r| i <= num }
      roman << r
      num -= i
    end
    roman
  end
end

Test objectives

We need to test a substantial number of integer values and make sure that we are testing each integer against the correct roman numeral equivalent. Both of these objectives can be met by creating a method that converts roman numerals to integers:

RNRI = RNR.invert
  #=> {"MMM"=>3000, "MM"=>2000, "M"=>1000,..., "II"=>2, "I"=>1, ""=>0} 

class String
  def roman_to_integer
    num = 0
    roman = self
    while roman.size > 0
      r, i = RNRI.find { |r,m| roman =~ /^#{r}/ }
      num += i
      roman = roman[r.size..-1]
    end
    num
  end  
end

Examples

Now let's invoke both Integer#to_roman and String#roman_to_integer for various integer values:

def check_one(i)
  roman = i.to_roman
  puts "#{i}.to_roman = #{roman}, #{roman}.roman_to_integer = " +
    #{roman.roman_to_integer}"
end

check_one(402)  # 'CDII'
  # 402.to_roman = CDII, CDII.roman_to_integer = 402
check_one(575)  # 'DLXXV'
  # 575.to_roman = DLXXV, DLXXV.roman_to_integer = 575
check_one(911)  # 'CMXI'
  # 911.to_roman = CMXI, CMXI.roman_to_integer = 911
check_one(1024) # 'MXXIV'
  # 1024.to_roman = MXXIV, MXXIV.roman_to_integer = 1024
check_one(3000) # 'MMM'
  # 3000.to_roman = MMM, MMM.roman_to_integer = 3000

Tests

So now in your testing you can use:

def test_all(n)
  (1..n).each { |i| test_one(i) }
end

def test_one(i)
  roman = i.to_roman
  assert_equal(i, roman.roman_to_integer, "#{i}.to_roman=#{roman}, " +
    "#{roman}.roman_to_integer = #{roman.roman_to_integer}")
end

Upvotes: 2

SteveTurczyn
SteveTurczyn

Reputation: 36880

This line is a problem...

ROMAN_NUMERALS[character.to_i * 10 ** index] << roman_numeral

It will return a string which is the value of the correct ROMAN_NUMERALS key plus roman_numeral, which is what you want, BUT it is also changing the value in the ROMAN_NUMERALS hash! The shovel operator << changes the string on the left of the operator (it's what we call a mutating operator).

So if you test for 1001 the unit 1 will return "I" (that's fine) then the zero will return an empty string BUT will change the value for zero into "I"... the second zero will return "I" (incorrect) and will change the value for zero into "II". The 1 in the thousands position will return "M" but then change the hash value into "MII".

Upvotes: 3

Bonifacio2
Bonifacio2

Reputation: 3870

When the line ROMAN_NUMERALS[character.to_i * 10 ** index] << roman_numeral is being executed you are replacing the value corresponding to the key character.to_i * 10 ** index with its value plus roman_numeral.

Upvotes: 2

Related Questions