<?php
namespace JoshDiFabio;
/*
* Note that I have included the interface and both concrete classes in a single file purely to
* simplify the process of distributing and reviewing the code. Ordinarily I would use a separate
* file for each class according to PSR-0 and use an autoloader to load them.
*
* This implementation assumes the following integer => numeral mappings which might not always be
* considered implicit: 4 => IV, 9 => IX, 40 => XL, 90 => XC, 400 => CD, 900 => CM. This is because,
* according to Wikipedia, I can precede V and X, X can precede L and C, etc.
*
* I have assumed that the rules of Roman Numerals are static; hence the fast, graceful but fairly
* inflexible (in the sense that it can't be configured) solution.
*
* Requires PHP >= 5.3.0
*/
interface RomanNumeralGeneratorInterface
{
/**
* @param int $number
* @return string
* @throws \InvalidArgumentException Argument not an integer or out of range
* @author Josh Di Fabio <joshdifabio@hotmail.com>
*/
public function generate($number);
}
/**
* Class defining basic rules of roman numerals
*/
class RomanNumeral
{
const ONE = 'I';
const FOUR = 'IV';
const FIVE = 'V';
const NINE = 'IX';
const TEN = 'X';
const FOURTY = 'XL';
const FIFTY = 'L';
const NINETY = 'XC';
const ONE_HUNDRED = 'C';
const FOUR_HUNDRED = 'CD';
const FIVE_HUNDRED = 'D';
const NINE_HUNDRED = 'CM';
const ONE_THOUSAND = 'M';
/**
* @return array
* @author Josh Di Fabio <joshdifabio@hotmail.com>
*/
public static function getIntegerToNumeralMap()
{
return array(
1 => self::ONE,
4 => self::FOUR,
5 => self::FIVE,
9 => self::NINE,
10 => self::TEN,
40 => self::FOURTY,
50 => self::FIFTY,
90 => self::NINETY,
100 => self::ONE_HUNDRED,
400 => self::FOUR_HUNDRED,
500 => self::FIVE_HUNDRED,
900 => self::NINE_HUNDRED,
1000 => self::ONE_THOUSAND,
);
}
}
/**
* Class for generating roman numeral strings based on integers
*/
class RomanNumeralGenerator implements RomanNumeralGeneratorInterface
{
/**
* @param int $inputNumber
* @return string
* @throws \InvalidArgumentException Argument not an integer or out of range
* @author Josh Di Fabio <joshdifabio@hotmail.com>
*/
public function generate($inputNumber)
{
if (!is_integer($inputNumber)) {
throw new \InvalidArgumentException('Provided argument is not an integer.');
}
if (1 > $inputNumber || 3999 < $inputNumber) {
throw new \InvalidArgumentException(
'The provided number is not within the allowed range of 1 to 3999.');
}
$numeralString = '';
// get integer to numeral map and sort from largest to smallest
$integerToNumeralMap = RomanNumeral::getIntegerToNumeralMap();
krsort($integerToNumeralMap);
while (0 < $inputNumber) {
foreach ($integerToNumeralMap as $_numeralAsInteger => $_numeral) {
/*
* if input number minus any already selected numerals is larger than the current
* numeral, select the current numeral and deduct its integer value from the input
* number
*/
if ($inputNumber >= $_numeralAsInteger) {
/*
* append matching numeral to output string and deduct its integer value from
* input number
*/
$numeralString .= $_numeral;
$inputNumber -= $_numeralAsInteger;
break;
}
}
}
return $numeralString;
}
}
$generator = new RomanNumeralGenerator;
echo $generator->generate(3999);