diff --git a/README.md b/README.md index 0be3a27..abb84ff 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,19 @@ echo Number::ordinal(1002); // "nd" echo Number::ordinal(-111); // "th" ``` +**Roman numbers** +```php +use PHPHumanizer\Number; + +echo Number::toRoman(1); // "I" +echo Number::toRoman(5); // "V" +echo Number::toRoman(1300); // "MCCC" + +echo Number::fromRoman("MMMCMXCIX"); // 3999 +echo Number::fromRoman("V"); // 5 +echo Number::fromRoman("CXXV"); // 125 +``` + **Binary Suffix** ```php diff --git a/spec/PHPHumanizer/NumberSpec.php b/spec/PHPHumanizer/NumberSpec.php index 344e921..ea7dbff 100644 --- a/spec/PHPHumanizer/NumberSpec.php +++ b/spec/PHPHumanizer/NumberSpec.php @@ -75,4 +75,38 @@ function it_throw_exception_when_converting_to_string_with_metric_suffix_non_num $this->shouldThrow(new \InvalidArgumentException("Metric suffix converter accept only numeric values.")) ->during('metricSuffix', array('as12')); } + + function it_converts_numbers_to_roman() + { + $this->toRoman(1)->shouldReturn("I"); + $this->toRoman(5)->shouldReturn("V"); + $this->toRoman(9)->shouldReturn("IX"); + $this->toRoman(10)->shouldReturn("X"); + $this->toRoman(125)->shouldReturn("CXXV"); + $this->toRoman(1300)->shouldReturn("MCCC"); + $this->toRoman(3999)->shouldReturn("MMMCMXCIX"); + } + + function it_throws_exception_when_converting_number_is_out_of_range() + { + $this->shouldThrow(new \InvalidArgumentException())->during('toRoman', array(-1)); + $this->shouldThrow(new \InvalidArgumentException())->during('toRoman', array(4000)); + } + + function it_converts_roman_numbers_to_arabic() + { + $this->fromRoman("I")->shouldReturn(1); + $this->fromRoman("V")->shouldReturn(5); + $this->fromRoman("IX")->shouldReturn(9); + $this->fromRoman("CXXV")->shouldReturn(125); + $this->fromRoman("MCCC")->shouldReturn(1300); + $this->fromRoman("MMMCMXCIX")->shouldReturn(3999); + } + + function it_throws_exception_when_converting_roman_number_is_invalid() + { + $this->shouldThrow(new \InvalidArgumentException())->during('fromRoman', array(1234)); + $this->shouldThrow(new \InvalidArgumentException())->during('fromRoman', array("")); + $this->shouldThrow(new \InvalidArgumentException())->during('fromRoman', array("foobar")); + } } diff --git a/src/PHPHumanizer/Number.php b/src/PHPHumanizer/Number.php index fff15b3..9ef590c 100644 --- a/src/PHPHumanizer/Number.php +++ b/src/PHPHumanizer/Number.php @@ -3,6 +3,7 @@ namespace PHPHumanizer; use PHPHumanizer\Number\Ordinal; +use PHPHumanizer\Number\RomanNumeral; use PHPHumanizer\String\BinarySuffix; use PHPHumanizer\String\MetricSuffix; @@ -29,4 +30,16 @@ public static function metricSuffix($number, $locale = 'en') $binarySuffix = new MetricSuffix($number, $locale); return $binarySuffix->convert(); } + + public static function toRoman($number) + { + $romanNumeral = new RomanNumeral(); + return $romanNumeral->toRoman($number); + } + + public function fromRoman($number) + { + $romanNumeral = new RomanNumeral(); + return $romanNumeral->fromRoman($number); + } } diff --git a/src/PHPHumanizer/Number/RomanNumeral.php b/src/PHPHumanizer/Number/RomanNumeral.php new file mode 100644 index 0000000..b1ab9ad --- /dev/null +++ b/src/PHPHumanizer/Number/RomanNumeral.php @@ -0,0 +1,90 @@ + 1000, + 'CM' => 900, + 'D' => 500, + 'CD' => 400, + 'C' => 100, + 'XC' => 90, + 'L' => 50, + 'XL' => 40, + 'X' => 10, + 'IX' => 9, + 'V' => 5, + 'IV' => 4, + 'I' => 1 + ); + + + /** + * @param $number + * @return string + * @throws \InvalidArgumentException + */ + public function toRoman($number) + { + if (($number < self::MIN_VALUE) || ($number > self::MAX_VALUE)) { + throw new \InvalidArgumentException(); + } + + $romanString = ''; + + while($number > 0) + { + foreach($this->map as $key => $value) + { + if($number >= $value) + { + $romanString .= $key; + $number -= $value; + break; + } + } + } + + return $romanString; + } + + /** + * @param $string + * @return int + * @throws \InvalidArgumentException + */ + public function fromRoman($string) + { + if (strlen($string) === 0 || 0 === preg_match(self::ROMAN_STRING_MATCHER, $string)) { + throw new \InvalidArgumentException(); + } + + $total = 0; + $i = strlen($string); + + while ($i > 0) + { + $digit = $this->map[$string{--$i}]; + + if ($i > 0) { + $previousDigit = $this->map[$string{$i - 1}]; + + if ($previousDigit < $digit) { + $digit -= $previousDigit; + $i--; + } + } + + $total += $digit; + } + + return $total; + } + +}