Spring naar hoofdtekst

Romeinse cijfers in PHP

Geplaatst op door .
Laatste aanpassing op .

Inleiding

Romeinse cijfers worden niet vaak gebruikt, maar ik vond het deze week eens tijd om de logica van dat systeem in PHP te gieten. Kabouter Wesley zou terecht vragen: "Wat is daar 't praktisch nut van?". Een antwoord blijf ik dit keer schuldig.

Eén en ander begint met de gebruikte symbolen, waarbij specifieke letters staan voor een bepaalde waarde – ongeacht hun positie in het getal. Hierbij valt meteen op dat er geen symbool is voor het cijfer 0 en dat negatieve getallen niet kunnen worden genoteerd.

<?php
$symbols = [
    1000 => 'M',
    900 => 'CM',
    500 => 'D',
    400 => 'CD',
    100 => 'C',
    90 => 'XC',
    50 => 'L',
    40 => 'XL',
    10 => 'X',
    9 => 'IX',
    5 => 'V',
    4 => 'IV',
    1 => 'I'
];

Logica

Aldus begon ik met de conversie van normale, decimale cijfers (positieve gehele getallen) naar Romeinse notatie. Om geen extreem lange, onleesbare getallen te krijgen, beperkte ik de code van 1 tot 4999. Als jaartallen kom je daarmee al een heel eind.

<?php
function dec2roman(int $i) : string
{
    if ($i <= 0 || $i > 4999) {
        return '?';
    }
    global $symbols;

    $symbolKeys = array_keys($symbols);
    $symbolValues = array_values($symbols);
    $o = '';

    foreach ($symbolKeys as $ix => $k) {
        if ($i >= $k) {
            $d = intdiv($i, $k);
            $o .= str_repeat($symbolValues[$ix], $d);
            $i -= ($d * $k);
        }
    }
    return $o;
}

De functie loopt door de bekende symbolen en kijkt of de waarde van het huidige symbool in het getal past. Zo ja, dan wordt dat aantal keer het symbool toegevoegd aan de output. De input wordt verminderd met diezelfde factor.

Ik moet eerlijk toegeven: de volgorde en volledigheid van de lijst met symbolen was wel bepalend voor het succes van deze functie. Ik had even nogig om te beseffen dat de lijst van groot (M = 1000) naar klein (I = 1) gesorteerd moest worden. En dat de buitenbeentjes (CM = 900, CD = 400, XC = 90, XL = 40, IX = 9 en IV = 4) in deze lijst moesten staan.

Andersom

Daarna volgde natuurlijk ook de omgekeerde berekening: van een Romeins cijfer terug naar een decimaal getal. Hier maakte ik opnieuw gebruik van de lijst met symbolen en hun waarden. Ik zocht mijn toevlucht tot een uitgebreide reguliere expressie:

$romanNumeralRegexp
 = "!^                        # anchor the regex's start
 (?<m>M*)                     # zero or more 'M's in group 'm'
 (?<cm>CM)?                   # zero or one 'CM' in group 'cm'
 (?:(?<d>D)|(?<cd>CD))?       # either 'D' or 'CD' in separate groups
 (?:(?<c>C{0,3})|(?<xc>XC))?  # zero to three 'C's, or 'XC' in separate groups
 (?:(?<l>L)|(?<xl>XL))?       # either 'L' or 'XL' in separate groups
 (?:(?<x>X{0,3})|(?<ix>IX))?  # zero to three 'X's, or 'IX' in separate groups
 (?:(?<v>V)|(?<iv>IV))?       # either 'V' or 'IV' in separate groups
 (?<i>I{0,3})?                # zero to three 'I's in group 'i'
$!x";                         // anchor the regex's end

Hiermee kon ik niet alleen een ingevoerd Romeins cijfer valideren, maar ook meteen de verschillende onderdelen (symbolen) extraheren voor later gebruik.

function roman2dec(string $s) : int
{
    if (empty($s)) {
        return -1;
    }
    global $symbols, romanNumeralRegexp;
    $m = [];

    if (!preg_match($romanNumeralRegexp, $s, $m)) {
        return -1;
    }
    $o = 0;
    foreach ($symbols as $v => $c) {
        $factor = 1;
        if (in_array($v, [1000, 100, 10, 1])) {
            $factor = strlen($m[strtolower($c)]);
        }
        if (!empty($m[strtolower($c)])) {
            $o += ($factor * $v);
        }
    }
    return $o;
}

Als de reguliere expressie niet matcht, is er geen geldig cijfer ingevoerd; de functie geeft dan -1 terug. Daarna wordt de lijst van symbolen doorlopen en bepaald hoeveel van dezelfde symbolen achter elkaar staan (bij M, C, X en I). Tot slot volgt waar het daadwerkelijk om draait: als de betreffende regex groep gevuld is, dan moet de waarde van dat symbool (vermenigvuldigd met de factor) meetellen in de output.

Conclusie

Dit project was weer eens ouderwets code kloppen en logisch nadenken; een welkome uitdaging. Met dank aan rapidtables.com voor een handige tabel en zelfs een online converter ter controle.

Terug naar boven

Inhoudsopgave

Delen

Met de deel-knop van uw browser, of met onderstaande koppelingen deelt u deze pagina via sociale media of e-mail.

Atom-feed van FWiePs weblog

Artikelen


Categorieën

Doorzoek de onderstaande categorieën om de lijst met artikelen te filteren.


Terug naar boven