Skip to content

Commit

Permalink
#18 Improved roman numerals conversions and validation
Browse files Browse the repository at this point in the history
  • Loading branch information
janbarasek authored Jun 6, 2020
2 parents 7ee22e5 + 6ff6707 commit d440971
Show file tree
Hide file tree
Showing 14 changed files with 810 additions and 154 deletions.
138 changes: 33 additions & 105 deletions src/Converter/IntToRoman.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,133 +7,50 @@

use Brick\Math\BigInteger;
use Brick\Math\BigNumber;
use Brick\Math\BigRational;
use Brick\Math\Exception\RoundingNecessaryException;
use Brick\Math\RoundingMode;
use Mathematicator\Numbers\Exception\NumberFormatException;
use Mathematicator\Numbers\Exception\OutOfRomanNumberSetException;
use Nette\StaticClass;
use Mathematicator\Numbers\Exception\OutOfSetException;
use Stringable;

/**
* Convert integer to roman numerals
* Convert an integer to roman numerals
*
* Tip: Use validators if you want to do custom checks (e.g. not zero, or in original ancient set)
*
* @see https://en.wikipedia.org/wiki/Roman_numerals
* @see https://www.wolframalpha.com/input/?i=1000000+to+roman
* @see https://www.calculatorsoup.com/calculators/conversions/roman-numeral-converter.php
*/
final class IntToRoman
final class IntToRoman extends IntToRomanBasic
{
use StaticClass;

/** @var int[] */
private static $conversionTable = [
'M' => 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,
];

/** @var string[] */
private static $fractionConversionTable = [
'1/12' => '·',
'2/12' => '··',
'3/12' => '···',
'4/12' => '····',
'5/12' => '·····',
'6/12' => 'S',
'7/12' => '',
'8/12' => 'S··',
'9/12' => 'S···',
'10/12' => 'S····',
'11/12' => 'S·····',
];


/**
* @param BigNumber|int|string|Stringable $input
* @return string
* @throws OutOfRomanNumberSetException
* @throws OutOfSetException
*/
public static function convert($input): string
{
try {
$integer = BigInteger::of((string) $input);
} catch (RoundingNecessaryException $e) {
return self::convertRationalNumber($input);
}

return self::convertInteger($integer);
}


/**
* @param BigNumber|int|string|Stringable $input
* @return string
* @throws OutOfRomanNumberSetException
* @see https://en.wikipedia.org/wiki/Roman_numerals (Fractions)
*/
public static function convertRationalNumber($input): string
{
$rationalNumber = BigRational::of((string) $input)->simplified();

if ($rationalNumber->isLessThan('1/12')) {
throw new OutOfRomanNumberSetException((string) $input);
}

$denominatorOriginal = $rationalNumber->getDenominator();

if (in_array((string) $denominatorOriginal, ['2', '3', '4', '6', '12'], true) && $denominatorOriginal->isLessThanOrEqualTo(12)) {
$toFinalMultiplier = BigInteger::of(12)->dividedBy($denominatorOriginal)->toInt();
$numeratorMultiplied = $rationalNumber->getNumerator()->multipliedBy($toFinalMultiplier);
$allowedSetDescription = 'integers >= 0';

$intFinal = $numeratorMultiplied->quotient(12);
$numeratorFinal = $numeratorMultiplied->mod(12)->toInt();

$out = '';

// Integer part
if ($intFinal->isGreaterThan(0)) {
$out .= self::convertInteger($intFinal);
}

// + fraction part
return $out . self::$fractionConversionTable["$numeratorFinal/12"];
} else {
throw new OutOfRomanNumberSetException((string) $input);
}
}


/**
* @param BigInteger|int|string|Stringable $input
* @return string
* @throws OutOfRomanNumberSetException
*/
public static function convertInteger($input): string
{
try {
$int = BigInteger::of((string) $input);
} catch (RoundingNecessaryException $e) {
throw new OutOfRomanNumberSetException((string) $input);
throw new OutOfSetException($input . ' (not integer)', $allowedSetDescription);
}

// According to Wikipedia, largest valid roman number is 3999: https://en.wikipedia.org/wiki/Roman_numerals
if ($int->isLessThan(1) || $int->isGreaterThan(3999)) {
throw new OutOfRomanNumberSetException((string) $input);
if ($int->isLessThan(0)) {
throw new OutOfSetException($input . ' (negative)', $allowedSetDescription);
}

$out = '';

$conversionTable = self::$conversionTable;
// Prepare a conversion table
$numberLength = strlen((string) $int);
$numberThousands = ($numberLength - $numberLength % 3) / 3;
$conversionTable = RomanToInt::getConversionTable($numberThousands);

// Process each roman numeral
foreach ($conversionTable as $roman => $value) {
$matches = $int->dividedBy($value, RoundingMode::DOWN)->toInt();
$out .= str_repeat($roman, $matches);
Expand All @@ -145,12 +62,23 @@ public static function convertInteger($input): string


/**
* @param string $romanNumber
* @return BigInteger
* @throws NumberFormatException
* @param BigNumber|int|string|Stringable $input
* @return string
* @throws OutOfSetException
*/
public static function reverse($romanNumber): BigInteger
public static function convertToLatex($input): string
{
return RomanToInt::convert($romanNumber);
$out = self::convert($input);

// Get count of leading underscores (e.g. 2 for __M)
preg_match('/^_*/', $out, $leadingUnderscoresMatches);
$leadingUnderscoresCount = isset($leadingUnderscoresMatches[0]) ? strlen($leadingUnderscoresMatches[0]) : 0;

// Convert underscores to latex overline
for ($i = $leadingUnderscoresCount; $i > 0; $i--) {
$out = (string) preg_replace('/_([IVXLCDM]|(\\\overline\{[\w{}]*\}))/', '\\overline{$1}', $out);
}

return $out;
}
}
66 changes: 66 additions & 0 deletions src/Converter/IntToRomanBasic.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

declare(strict_types=1);

namespace Mathematicator\Numbers\Converter;


use Brick\Math\BigInteger;
use Brick\Math\BigNumber;
use Brick\Math\Exception\RoundingNecessaryException;
use Brick\Math\RoundingMode;
use Mathematicator\Numbers\Exception\NumberFormatException;
use Mathematicator\Numbers\Exception\OutOfSetException;
use Nette\StaticClass;
use Stringable;

/**
* Convert integer to basic roman numerals (original ancient set)
*
* @see https://en.wikipedia.org/wiki/Roman_numerals
*/
class IntToRomanBasic
{
use StaticClass;

/**
* @param BigNumber|int|string|Stringable $input
* @return string
* @throws OutOfSetException
*/
public static function convert($input): string
{
try {
$int = BigInteger::of((string) $input);
} catch (RoundingNecessaryException $e) {
throw new OutOfSetException($input . ' (not integer)');
}

if ($int->isLessThan(0)) {
throw new OutOfSetException((string) $input, 'integers >= 0');
}

$out = '';

$conversionTable = RomanToInt::getConversionTable(0);

foreach ($conversionTable as $roman => $value) {
$matches = $int->dividedBy($value, RoundingMode::DOWN)->toInt();
$out .= str_repeat($roman, $matches);
$int = $int->mod($value);
}

return $out;
}


/**
* @param string $romanNumber
* @return BigInteger
* @throws NumberFormatException
*/
public static function reverse($romanNumber): BigInteger
{
return RomanToInt::convert($romanNumber);
}
}
81 changes: 81 additions & 0 deletions src/Converter/RationalToRoman.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php

declare(strict_types=1);

namespace Mathematicator\Numbers\Converter;


use Brick\Math\BigInteger;
use Brick\Math\BigNumber;
use Brick\Math\BigRational;
use Mathematicator\Numbers\Exception\OutOfSetException;
use Nette\StaticClass;
use Stringable;

/**
* Convert rational number to basic roman fractions (original ancient set)
*
* @see https://en.wikipedia.org/wiki/Roman_numerals
*/
final class RationalToRoman
{
use StaticClass;

/** @var string[] */
protected static $fractionConversionTable = [
'1/12' => '·',
'2/12' => '··',
'3/12' => '···',
'4/12' => '····',
'5/12' => '·····',
'6/12' => 'S',
'7/12' => '',
'8/12' => 'S··',
'9/12' => 'S···',
'10/12' => 'S····',
'11/12' => 'S·····',
];


/**
* @param BigNumber|int|string|Stringable $input
* @return string
* @throws OutOfSetException
* @see https://en.wikipedia.org/wiki/Roman_numerals (Fractions)
*/
public static function convert($input): string
{
$rationalNumber = BigRational::of((string) $input)->simplified();

if ($rationalNumber->isLessThan('1/12')) {
throw new OutOfSetException((string) $input . ' (less than 1/12)', 'integers >= 0, fractions /12');
}

$denominatorOriginal = $rationalNumber->getDenominator();

if (in_array((string) $denominatorOriginal, ['1', '2', '3', '4', '6', '12'], true) && $denominatorOriginal->isLessThanOrEqualTo(12)) {
$toFinalMultiplier = BigInteger::of(12)->dividedBy($denominatorOriginal)->toInt();
$numeratorMultiplied = $rationalNumber->getNumerator()->multipliedBy($toFinalMultiplier);

$intFinal = $numeratorMultiplied->quotient(12);
$numeratorFinal = $numeratorMultiplied->mod(12)->toInt();

$out = '';

// Integer part
if ($intFinal->isGreaterThan(0)) {
$out .= IntToRoman::convert($intFinal);
}


// Fraction part
if ($numeratorFinal > 0) {
$out .= self::$fractionConversionTable["$numeratorFinal/12"];
}

return $out;
} else {
throw new OutOfSetException((string) $input);
}
}
}
Loading

0 comments on commit d440971

Please sign in to comment.