loading...

Daily Challenge #209 - Roman Numerals

thepracticaldev profile image dev.to staff ・1 min read

Implement a function that takes a Roman numeral as its argument and returns its value as an integer. You don't need to validate the form of the Roman numeral.

Modern Roman numerals are written by expressing each digit of the number to be encoded separately, starting with the leftmost digit and skipping any 0s. So 1990 is rendered "MCMXC" (1000 = M, 900 = CM, 90 = XC) and 2008 is rendered "MMVIII" (2000 = MM, 8 = VIII). The Roman numeral for 1666, "MDCLXVI", uses each letter in descending order.

Here's a chart to help out:

Symbol    Value
I          1
V          5
X          10
L          50
C          100
D          500
M          1,000

Example

solution('XXI'); // should return 21

Tests

solution('I')
solution('IV')
solution('MMVIII')
solution('MDCLXVI')

Good luck!


This challenge comes from jhoffner on CodeWars. Thank you to CodeWars, who has licensed redistribution of this challenge under the 2-Clause BSD License!

Want to propose a challenge idea for a future post? Email yo+challenge@dev.to with your suggestions!

Posted on by:

thepracticaldev profile

dev.to staff

@thepracticaldev

The hardworking team behind dev.to ❤️

Discussion

markdown guide
 

Not a solution to this challenge, but I always loved the fact that Common Lisp's format function actually has a formatting directive for roman numerals:

(format nil "~@r" 1666)
==> MDCLXVI

There's even a directive for old-style Roman numerals where 4 is IIII and 9 VIIII etc., "~:@r".

 

Hah, there were lots of latin freaks out there in the 60s. Nice, didn't know about it.

 
const mapRomanNToN = {
  'I': 1,
  'V': 5,
  'X': 10,
  'L': 50,
  'C': 100,
  'D': 500,
  'M': 1000,
}

const solution = (str) => {
  const reducer = (acc, curVal) => {
    return acc += mapRomanNToN[curVal];
  }

  return str.split('').reduce(reducer, 0);
}
 

Not working, due to the fact that you ignored that if a value is smaller than the next one it's substracted not added... check the test case MCMXC it should be 1990 and yours is 2210

 

haha you re right! thanks. Was in a rush. will fix it

 

JS solution

const v = {
  I: 1,
  V: 5,
  X: 10,
  L: 50,
  C: 100,
  D: 500,
  M: 1000
};

const solution = nr => nr.split("").reduce((acc,cur,i,oa)=> {
  acc = i<oa.length-1 ? (v[cur] >= v[oa[i+1]] ? acc+v[cur] : acc - v[cur]) : acc+ v[cur];
  return acc;
},0);
 

TypeScript

type RomanNumeral = 'I' | 'V' | 'X' | 'L' | 'C' | 'D' | 'M';

const RomanNumerals: Record<RomanNumeral, number> = {
  I: 1,
  V: 5,
  X: 10,
  L: 50,
  C: 100,
  D: 500,
  M: 1000
};

/**
 * Convert a string of Roman Numerals to an integer
 * @param input String of Roman Numerals. Invalid characters are not considered.
 */
function solution(input: string): number {
  return Array.from(input)
    .map(n => RomanNumerals[n])
    .filter(Number)
    .reduce((total, val, idx, arr) => {
      if (val >= (arr[idx + 1] || 0)) {
        return (total += val);
      } else {
        return (total -= val);
      }
    }, 0);
}
 

Fun solution in elixir

def numerals(num) when num > 1000, do: "M" <> numerals(num - 1000)
def numerals(num) when num > 900, do: "CM" <> numerals(num - 900)
def numerals(num) when num > 500, do: "D" <> numerals(num - 500)
def numerals(num) when num > 400, do: "CD" <> numerals(num - 400)
def numerals(num) when num > 100, do: "C" <> numerals(num - 100)
def numerals(num) when num > 90, do: "XC" <> numerals(num - 90)
def numerals(num) when num > 50, do: "L" <> numerals(num - 50)
def numerals(num) when num > 40, do: "XL" <> numerals(num - 40)
def numerals(num) when num > 10, do: "X", <> numerals(num - 10)
def numerals(num) when num > 9, do: "IX" <> numerals(num - 9)
def numerals(num) when num > 5, do: "V" <> numerals(num - 5)
def numerals(num) when num > 4, do: "IV" <> numerals(num - 4)
def numerals(num) when num > 1, do: "I" <> numerals(num - 1)
def numerals(0), do: ""
 

My solution in Haskell:

solution :: String -> Int
solution = parser . fmap lexer
  where
    parser :: [Int] -> Int
    parser []     = 0
    parser (x:[]) = x
    parser (x:x':xs)
      | x >= x'   = x + (parser (x':xs))
      | otherwise = -x + x' + (parser xs)

    lexer :: Char -> Int
    lexer s =
      case s of
        'I' -> 1
        'V' -> 5
        'X' -> 10
        'L' -> 50
        'C' -> 100
        'D' -> 500
        'M' -> 1000

The lexer function takes care of translating roman numeral tokens into numbers, which can be applied to whole string to get array of the respective integers.
The array of integers is then parsed with a recursive function.

I went with case in lexer, since that notation seemed neater than defining function for each argument case.

 

Using pattern matching and tail recursion in Elixir:

defmodule Roman do
  @moduledoc false

  @spec to_integer(binary()) :: {:ok, pos_integer()} | :error
  def to_integer(number, acc \\ 0)

  def to_integer("", acc), do: {:ok, acc}

  def to_integer(number, acc) do
    with {value, rest} <- num(number) do
      to_integer(rest, acc + value)
    end
  end

  @spec num(binary()) :: {pos_integer(), binary()} | :error
  defp num(number)

  defp num("M" <> rest), do: {1000, rest}
  defp num("CM" <> rest), do: {900, rest}
  defp num("D" <> rest), do: {500, rest}
  defp num("CD" <> rest), do: {400, rest}
  defp num("C" <> rest), do: {100, rest}
  defp num("XC" <> rest), do: {90, rest}
  defp num("L" <> rest), do: {50, rest}
  defp num("XL" <> rest), do: {40, rest}
  defp num("X" <> rest), do: {10, rest}
  defp num("IX" <> rest), do: {9, rest}
  defp num("V" <> rest), do: {5, rest}
  defp num("IV" <> rest), do: {4, rest}
  defp num("I" <> rest), do: {1, rest}
  defp num(_), do: :error
end

solution = fn x ->
  case Roman.to_integer(x) do
    {:ok, value} -> value
    :error -> raise ArgumentError, message: "Wrong roman numeral format"
  end
end
 

if you are going from back to front, it is a bit easier to "parse".
So this time F# instead of my usual C# posts.

Yes, still learning...

let roman list : int = 
    let getNumeralValue x =
        match x with
        | 'M' -> 1000
        | 'm' -> 1000
        | 'D' -> 500
        | 'd' -> 500
        | 'C' -> 100
        | 'c' -> 100
        | 'L' -> 50
        | 'l' -> 50
        | 'X' -> 10
        | 'x' -> 10
        | 'V' -> 5
        | 'v' -> 5
        | 'I' -> 1
        | 'i' -> 1
        | _ -> 0
    let folder (sum,last) current = 
        if last > current 
        then (sum-current,last) 
        else (sum+current,current)
    Seq.rev list 
    |> Seq.map getNumeralValue 
    |> Seq.fold folder (0,0) 
    |> fst

 

I did mine using JavaScript, not really a fast algorithm but I think it's readable and allows your pals to understand what's going on.

function romanCharToInt(c) {
    switch (c) {
        case 'I':
            return 1;
        case 'V':
            return 5;
        case 'X':
            return 10;
        case 'L':
            return 50;
        case 'C':
            return 100;
        case 'D':
            return 500;
        case 'M':
            return 1000;
    }
}

function romanToInt(s) {
    let finalSum = 0;
    let romanChars = s.split("");

    for (let i = 0; i < romanChars.length; i++) {
        if (romanChars[i + 1]) {
            if (romanCharToInt(romanChars[i]) >= romanCharToInt(romanChars[i + 1])) {
                finalSum += romanCharToInt(romanChars[i]);
            } else {
                finalSum += romanCharToInt(romanChars[i + 1]);
                finalSum -= romanCharToInt(romanChars[i]);
                i++;
            }
        } else {
            finalSum += romanCharToInt(romanChars[i]);
        }
    }

    return finalSum;
}