From 533bcaf826cc466a30b99bca457e6c8d14900231 Mon Sep 17 00:00:00 2001
From: Oliver Davies <oliver@oliverdavies.uk>
Date: Fri, 3 Jul 2020 20:39:28 +0100
Subject: [PATCH] Roman numeral converter

---
 composer.json                        |   3 +
 composer.lock                        |  67 ++++++++++++-
 src/RomanNumeralsConverter.php       |  50 ++++++++++
 tests/RomanNumeralsConverterTest.php | 142 +++++++++++++++++++++++++++
 4 files changed, 260 insertions(+), 2 deletions(-)
 create mode 100644 src/RomanNumeralsConverter.php
 create mode 100644 tests/RomanNumeralsConverterTest.php

diff --git a/composer.json b/composer.json
index 0f09d08..be4d09c 100644
--- a/composer.json
+++ b/composer.json
@@ -1,6 +1,9 @@
 {
     "minimum-stability": "dev",
     "prefer-stable": true,
+    "require": {
+        "beberlei/assert": "^3.2"
+    },
     "require-dev": {
         "phpunit/phpunit": "^9.0"
     },
diff --git a/composer.lock b/composer.lock
index 06e4fb5..41e757b 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,8 +4,71 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "73cde71d5e2a36a709acfdfab2ad5dfb",
-    "packages": [],
+    "content-hash": "7aa2245203ff9ea2bce7a2e52abd5654",
+    "packages": [
+        {
+            "name": "beberlei/assert",
+            "version": "v3.2.7",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/beberlei/assert.git",
+                "reference": "d63a6943fc4fd1a2aedb65994e3548715105abcf"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/beberlei/assert/zipball/d63a6943fc4fd1a2aedb65994e3548715105abcf",
+                "reference": "d63a6943fc4fd1a2aedb65994e3548715105abcf",
+                "shasum": ""
+            },
+            "require": {
+                "ext-ctype": "*",
+                "ext-json": "*",
+                "ext-mbstring": "*",
+                "ext-simplexml": "*",
+                "php": "^7"
+            },
+            "require-dev": {
+                "friendsofphp/php-cs-fixer": "*",
+                "phpstan/phpstan-shim": "*",
+                "phpunit/phpunit": ">=6.0.0 <8"
+            },
+            "suggest": {
+                "ext-intl": "Needed to allow Assertion::count(), Assertion::isCountable(), Assertion::minCount(), and Assertion::maxCount() to operate on ResourceBundles"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Assert\\": "lib/Assert"
+                },
+                "files": [
+                    "lib/Assert/functions.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-2-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Benjamin Eberlei",
+                    "email": "kontakt@beberlei.de",
+                    "role": "Lead Developer"
+                },
+                {
+                    "name": "Richard Quadling",
+                    "email": "rquadling@gmail.com",
+                    "role": "Collaborator"
+                }
+            ],
+            "description": "Thin assertion library for input validation in business models.",
+            "keywords": [
+                "assert",
+                "assertion",
+                "validation"
+            ],
+            "time": "2019-12-19T17:51:41+00:00"
+        }
+    ],
     "packages-dev": [
         {
             "name": "doctrine/instantiator",
diff --git a/src/RomanNumeralsConverter.php b/src/RomanNumeralsConverter.php
new file mode 100644
index 0000000..07eabe4
--- /dev/null
+++ b/src/RomanNumeralsConverter.php
@@ -0,0 +1,50 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App;
+
+use Assert\Assert;
+
+final class RomanNumeralsConverter
+{
+    private static $map = [
+        1000 => 'M',
+        900 => 'CM',
+        500 => 'D',
+        400 => 'CD',
+        100 => 'C',
+        90 => 'XC',
+        50 => 'L',
+        40 => 'XL',
+        10 => 'X',
+        9 => 'IX',
+        5 => 'V',
+        4 => 'IV',
+        3 => 'III',
+        2 => 'II',
+        1 => 'I',
+    ];
+
+    public static function convert(int $input): string
+    {
+        Assert::that($input)
+            ->greaterOrEqualThan(0, 'Cannot convert negative numbers');
+
+        $letters = '';
+
+        while ($input > 0) {
+            foreach (static::$map as $number => $letter) {
+                if ($input >= $number) {
+                    // Add the appropriate numeral and reduce the value of
+                    // $input accordingly.
+                    $letters .= $letter;
+                    $input = ($input - $number);
+                    break;
+                }
+            }
+        }
+
+        return $letters;
+    }
+}
diff --git a/tests/RomanNumeralsConverterTest.php b/tests/RomanNumeralsConverterTest.php
new file mode 100644
index 0000000..e3227f9
--- /dev/null
+++ b/tests/RomanNumeralsConverterTest.php
@@ -0,0 +1,142 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Tests;
+
+use App\RomanNumeralsConverter;
+use Assert\AssertionFailedException;
+use PHPUnit\Framework\Assert;
+use PHPUnit\Framework\TestCase;
+
+final class RomanNumeralsConverterTest extends TestCase
+{
+    /**
+     * @test
+     * @dataProvider numeralProvider
+     */
+    public function it_converts_a_number(int $number, string $expected): void
+    {
+        Assert::assertSame(
+            $expected,
+            RomanNumeralsConverter::convert($number)
+        );
+    }
+
+    public function numeralProvider(): array
+    {
+        return [
+            1 => [
+                'number' => 1,
+                'expected' => 'I',
+            ],
+            2 => [
+                'number' => 2,
+                'expected' => 'II',
+            ],
+            3 => [
+                'number' => 3,
+                'expected' => 'III',
+            ],
+            4 => [
+                'number' => 4,
+                'expected' => 'IV',
+            ],
+            5 => [
+                'number' => 5,
+                'expected' => 'V',
+            ],
+            9 => [
+                'number' => 9,
+                'expected' => 'IX',
+            ],
+            10 => [
+                'number' => 10,
+                'expected' => 'X',
+            ],
+            15 => [
+                'number' => 15,
+                'expected' => 'XV',
+            ],
+            19 => [
+                'number' => 19,
+                'expected' => 'XIX',
+            ],
+            20 => [
+                'number' => 20,
+                'expected' => 'XX',
+            ],
+            21 => [
+                'number' => 21,
+                'expected' => 'XXI',
+            ],
+            40 => [
+                'number' => 40,
+                'expected' => 'XL',
+            ],
+            50 => [
+                'number' => 50,
+                'expected' => 'L',
+            ],
+            80 => [
+                'number' => 80,
+                'expected' => 'LXXX',
+            ],
+            90 => [
+                'number' => 90,
+                'expected' => 'XC',
+            ],
+            100 => [
+                'number' => 100,
+                'expected' => 'C',
+            ],
+            110 => [
+                'number' => 110,
+                'expected' => 'CX',
+            ],
+            400 => [
+                'number' => 400,
+                'expected' => 'CD',
+            ],
+            500 => [
+                'number' => 500,
+                'expected' => 'D',
+            ],
+            700 => [
+                'number' => 700,
+                'expected' => 'DCC',
+            ],
+            900 => [
+                'number' => 900,
+                'expected' => 'CM',
+            ],
+            1000 => [
+                'number' => 1000,
+                'expected' => 'M',
+            ],
+            1986 => [
+                'number' => 1986,
+                'expected' => 'MCMLXXXVI',
+            ],
+            1990 => [
+                'number' => 1990,
+                'expected' => 'MCMXC',
+            ],
+            2020 => [
+                'number' => 2020,
+                'expected' => 'MMXX',
+            ],
+        ];
+    }
+
+    /**
+     * @test
+     */
+    public function it_cannot_convert_negative_numbers(): void
+    {
+        $this->expectException(AssertionFailedException::class);
+        $this->expectExceptionMessage('Cannot convert negative numbers');
+
+        RomanNumeralsConverter::convert(-1);
+    }
+}