1

I have a function roman(x) that takes an int (>= 1) and returns the roman numeral as a string. I have achieved a working example with:

def roman(x: int) -> str:
    """Convert `int` to roman numeral"""
    
    L = [(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')]

    y = ""
    for val, sym in L:
        y += sym*(x//val)
        x -= val*(x//val)

    return y
>>> roman(399)
'CCCXCIX'

I am asking how to convert the for loop:

y = ""
for val, sym in L:
    y += sym*(x//val)
    x -= val*(x//val)

return y

Into a list comprehension

return "".join([ ... ])

L Does not need to be embedded into the list comprehension, and can remain as is. Such that the function is in this form:

def roman(x: int) -> str:
    """Convert into to roman numeral"""

    L = [(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')]

    # Return list comprehension as a string
    return "".join([ ... ])
Freddy Mcloughlan
  • 4,129
  • 1
  • 13
  • 29
  • 4
    I don't think there's any good way to reduce this to a list comprehension. And even if there is, doing it as a `for` loop like you're currently doing is almost certainly going to be cleaner code, and thus should be preferred – Green Cloak Guy Apr 05 '22 at 04:55
  • Thank you, I will keep it as is. Should I close my question? – Freddy Mcloughlan Apr 05 '22 at 04:57
  • since it's state changing during enumeration, `reduce` may help – JL0PD Apr 05 '22 at 04:58
  • 1
    [This answer](https://stackoverflow.com/a/28777781/) makes use of list comprehension on the generator function that yielded the expected tokens, which is about as far as what is sensible because list comprehension generally do not mutate other states (i.e. [side effects in list comprehension is bad practice](https://stackoverflow.com/q/5753597)); since this calculation requires mutating an external value beyond what is included, this is not the use case. To be clear, I mean having all the conditions being mutated inside the expression, while the linked answer has all mutations self-contained. – metatoaster Apr 05 '22 at 05:10
  • @metatoaster, this is what I was thinking. Thanks for the read – Freddy Mcloughlan Apr 05 '22 at 05:13

3 Answers3

1

If you don't mind recursion, this is actually a pretty good use case for it.

L = [(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 roman(num):
  if num == 0: # Base case: when we reach zero, return empty string
    return ''
  # Recursive step: iterate over values and their characters
  for v, n in L:
    if num >= v: # If their difference is positive, we recurse
      return n + roman(num - v) # We append the result to the numeral

roman(399)
>> 'CCCXCIX'
vstack17
  • 86
  • 5
1
def roman(x: int) -> str:
    """Convert into to roman numeral"""

    L = [(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')]

    return ''.join([(sym*(x//val), (x:=x-val*(x//val)))[0] for val, sym in L])


print(roman(399))  # CCCXCIX
PurpleBall
  • 26
  • 1
0

Version using functools.reduce

from functools import reduce

def roman(x: int) -> str:
    """Convert into to roman numeral"""

    L = [(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')]

    return reduce(lambda s, c: (s[0] + c[1]*(s[1]//c[0]), s[1] - c[0]*(s[1]//c[0])), L, ("", x))[0]

print(roman(399)) # CCCXCIX

It's scary, it's unreadable. Don't use it

JL0PD
  • 3,698
  • 2
  • 15
  • 23