Skip to content

Instantly share code, notes, and snippets.

@ceottaki
Last active January 4, 2016 16:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ceottaki/8650187 to your computer and use it in GitHub Desktop.
Save ceottaki/8650187 to your computer and use it in GitHub Desktop.
An utility class that handles numbers written in Roman numerals, with methods for converting Hindu-Arabic numbers into Roman numbers and vice-versa.
// ----------------------------------------------------------------------------------------------------------------------------
// <copyright file="RomanizerUtility.cs" company="Felipe Ceotto">Copyright (c) Felipe Ceotto. All rights reserved.</copyright>
//
// This file is a standalone utility and was created as part of an exercise.
//
// Although it is not a 'software' in itself, it is freely provided and you can redistribute it and/or modify it under the
// terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License,
// or (at your option) any later version.
//
// This utility is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the
// implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
// details.
//
// Please refer to http://www.gnu.org/licenses/ for a copy of the GNU General Public License.
// ----------------------------------------------------------------------------------------------------------------------------
namespace Romanizer
{
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
/// <summary>
/// Represents an utility that handles numbers written in Roman numerals.
/// </summary>
public static class RomanizerUtility
{
/// <summary>
/// Gets the Roman number equivalent to a given decimal number in Hindu-Arabic format.
/// </summary>
/// <param name="number">The decimal number in Hindu-Arabic format.</param>
/// <returns>The Roman number equivalent to the given <paramref name="number"/>.</returns>
/// <remarks>
/// <para>Negative numbers are presented with a minus in front, e.g. -2 => -II.</para>
/// <para>Zero returns an empty string as there is no representation for zero in classic Roman numerals.</para>
/// </remarks>
public static string GetRomanNumber(int number)
{
// Short-circuit exit clause, if number is zero returns an empty string.
if (number == 0)
{
return string.Empty;
}
// Initialises the result as an empty string and gets the dictionary of roman numerals.
string result = string.Empty;
IDictionary<int, string> romanNumbers = GetDictionaryOfRomanNumerals(true);
// Checks if given number is negative and adds a minus to the start of result if it is...
// ...then inverts the number to guarantee that we are always working with positive numbers after here.
if (number < 0)
{
result += "-";
number = number * -1;
}
// Runs through the number dividing it by the biggest key in the dictionary that is not bigger than the number itself.
// The result of the division is the number of characters equivalent to that key to add to the Roman number, which are added to the result.
// The number is then reduced by that number of characters times the value of the key, which is the value already added to the Roman number.
while (number > 0)
{
int currentDivisor = romanNumbers.Keys.Where(n => n <= number).Max();
int numberOfCharacters = number / currentDivisor;
for (int i = 0; i < numberOfCharacters; i++)
{
result += romanNumbers[currentDivisor];
}
number -= currentDivisor * numberOfCharacters;
}
return result;
}
/// <summary>
/// Gets the Hindu-Arabic number equivalent to a given number in Roman format.
/// </summary>
/// <param name="romanNumber">The roman number.</param>
/// <returns>The Hindu-Arabic number equivalent to the given <paramref name="romanNumber"/>.</returns>
/// <remarks>
/// <para>Negative numbers are presented with a minus in front, e.g. -II => -2.</para>
/// <para>Invalid Roman numbers such as ones with invalid characters or characters in an incorrect order will return zero. Empty or <c>null</c> strings will also return zero.</para>
/// </remarks>
public static int GetHinduArabicNumber(string romanNumber)
{
// Short-circuit exit clause, if Roman number is an null or an empty string, returns zero.
if (string.IsNullOrWhiteSpace(romanNumber))
{
return 0;
}
// Initialises the result as zero and gets the dictionary of roman numerals.
int result = 0;
IDictionary<int, string> romanNumbers = GetDictionaryOfRomanNumerals(true);
// Checks if roman number starts with a negative sign and sets the multiplier accordingly, removing the negative sign from the number if present.
// This allows the algorithm to work only with positive numbers from here onwards.
int multiplier = 1;
if (romanNumber.StartsWith("-", StringComparison.OrdinalIgnoreCase))
{
multiplier = -1;
romanNumber = romanNumber.Substring(1);
}
// Checks if the given roman number contains any characters that are not part of the roman numerals, in which case returns zero.
if (romanNumber.Any(c => romanNumbers.Values.All(v => v != c.ToString(CultureInfo.InvariantCulture))))
{
return 0;
}
// Runs through each character of the roman number, processing each one individually.
int subtractor = 0;
for (int i = 1; i < romanNumber.Length + 1; i++)
{
// Gets the decimal value of the current character.
int currentCharValue = romanNumbers.Where(n => n.Value == romanNumber[i - 1].ToString()).First().Key;
// Gets the decimal value of the character next to the current character if there is one; zero otherwise.
int nextCharValue = i < romanNumber.Length ? romanNumbers.Where(n => n.Value == romanNumber[i].ToString()).First().Key : 0;
// Checks if current character has a smaller value than the next character...
if (currentCharValue < nextCharValue)
{
// ... in which case checks if this combination of characters is a valid one, to avoid cases like XM or IC, and returns zero if the combination is invalid.
if (romanNumbers.All(n => n.Value != string.Concat(romanNumber[i - 1], romanNumber[i])))
{
return 0;
}
// If the combination is valid, the value of the current character is set as the subtractor for when the next character is processed.
subtractor = currentCharValue;
}
else
{
// If current character has the same or higher value than its next character then simply adds that value to the result.
// The subtractor is taken into consideration when adding the value and then it is reset to zero.
result += currentCharValue - subtractor;
subtractor = 0;
}
}
// Applies the multiplier.
result *= multiplier;
return result;
}
/// <summary>
/// Gets the dictionary of roman numerals.
/// </summary>
/// <param name="includeSubtractions">If set to <c>true</c>, include subtractions in the dictionary such as 4 => IV.</param>
/// <returns>
/// A dictionary of roman numerals with the key being the equivalent Hindu-Arabic numeral and the value being the roman numeral.
/// </returns>
/// <example>
/// A call to this function returns exactly this (with items in <i>italic</i> returned only if <paramref name="includeSubtractions" /> is <c>true</c>):
/// <table>
/// <thead><tr><th>Key</th><th>Value</th></tr></thead>
/// <tbody>
/// <tr><td>1</td><td>I</td></tr>
/// <tr><td><i>4</i></td><td><i>IV</i></td></tr>
/// <tr><td>5</td><td>V</td></tr>
/// <tr><td><i>9</i></td><td><i>IX</i></td></tr>
/// <tr><td>10</td><td>X</td></tr>
/// <tr><td><i>40</i></td><td><i>XL</i></td></tr>
/// <tr><td>50</td><td>L</td></tr>
/// <tr><td><i>90</i></td><td><i>XC</i></td></tr>
/// <tr><td>100</td><td>C</td></tr>
/// <tr><td><i>400</i></td><td><i>CD</i></td></tr>
/// <tr><td>500</td><td>D</td></tr>
/// <tr><td><i>900</i></td><td><i>CM</i></td></tr>
/// <tr><td>1000</td><td>M</td></tr>
/// </tbody>
/// </table>
/// </example>
private static IDictionary<int, string> GetDictionaryOfRomanNumerals(bool includeSubtractions)
{
SortedDictionary<int, string> romanNumbers = new SortedDictionary<int, string>();
romanNumbers.Add(1, "I");
romanNumbers.Add(5, "V");
romanNumbers.Add(10, "X");
romanNumbers.Add(50, "L");
romanNumbers.Add(100, "C");
romanNumbers.Add(500, "D");
romanNumbers.Add(1000, "M");
if (includeSubtractions)
{
romanNumbers.Add(4, "IV");
romanNumbers.Add(9, "IX");
romanNumbers.Add(40, "XL");
romanNumbers.Add(90, "XC");
romanNumbers.Add(400, "CD");
romanNumbers.Add(900, "CM");
}
return new Dictionary<int, string>(romanNumbers);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment