5
\$\begingroup\$

Challenged by Sandy Metz's newsletter, I've tried to implement this kata from exercism. However, my solution, which passes the tests, is quite a lot simpler than Sandy's solution. Ok, so she's using refinements and also supports the inverse, but still... As she's probably smarter than me, do I miss something?

class Fixnum
  ROMAN = {
    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 = ""
    ROMAN.reduce(self) { |number, (divider, letter)|
      letter_multiplier, remainder = number.divmod(divider)
      result << (letter * letter_multiplier)
      remainder
    }
    result
  end
end
\$\endgroup\$
3
  • 2
    \$\begingroup\$ Her solution is over-engineered, imo. your solution is pretty nice but the way you're using reduce isn't idiomatic. The "initial" value you pass in should be the thing you are building up, in this case the roman string. the problems is you also have a temporary variable -- the "number". better to define that outside of reduce, so you can let the reduce expression be the return value. \$\endgroup\$
    – Jonah
    Commented Jun 1, 2016 at 23:13
  • \$\begingroup\$ True. Initially I had solved it with recursion, but I wanted to get rid of the conditional. But now the reduce has a side effect, which is not nice. \$\endgroup\$
    – iGEL
    Commented Jun 2, 2016 at 7:38
  • 2
    \$\begingroup\$ Please do not update the code in your question to incorporate feedback from answers, doing so goes against the Question + Answer style of Code Review. This is not a forum where you should keep the most updated version in your question. Please see what you may and may not do after receiving answers. \$\endgroup\$ Commented Jun 2, 2016 at 7:58

1 Answer 1

3
\$\begingroup\$

I like your solution. You usually see it done with while/for loops that substract multipliers instead of taking the quotient/remainder. Some notes about your code:

  • ROMAN.reduce(self) { |number, (divider, letter)|. In a multiline block is idiomatic to use do/end.
  • Prefer building an array + join at the end, that appending to a string. Not that it matters with those sizes, but in general it's the recommended way (+= in a string is typically O(n^2), << on an array is O(n)).
  • inject is a functional abstraction. While you can perform in-place updates within the block, it will break the assumption of many readers. One solution is to encapsulate the output within the state (using a hash, for example).

I'd write:

class Fixnum
  ROMAN = {...}

  def to_roman
    ROMAN.reduce(number: self, result: []) do |state, (divider, letter)|
      letter_multiplier, remainder = state[:number].divmod(divider)
      new_result = state[:result] + [letter * letter_multiplier]
      {number: remainder, result: new_result}
    end[:result].join
  end
end

Note that this is a state machine: it iterates over an input with an initial state, on each loop the state is updated, and the result is the final state.

\$\endgroup\$
2
  • \$\begingroup\$ Thanks. While you were writing, I already came up with a solution thanks to Jonah's comment, that fixes the last point. Good point about the array + join and the hash is also nicer than my array. About the first point: I'm aware of that convention and that it's very popular in the Ruby world, however I like the Weirich convention better. \$\endgroup\$
    – iGEL
    Commented Jun 2, 2016 at 7:57
  • \$\begingroup\$ I knew about that convention (via raganwald). I don't dislike it, but I am afraid it hasn't gotten traction. \$\endgroup\$
    – tokland
    Commented Jun 2, 2016 at 8:17

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.