Come abbini solo numeri romani validi a un'espressione regolare?


165

Pensando al mio altro problema , ho deciso che non riuscivo nemmeno a creare un'espressione regolare che corrispondesse ai numeri romani (per non parlare di una grammatica senza contesto che li genererà)

Il problema è che corrispondono solo numeri romani validi. Ad esempio, 990 NON è "XM", è "CMXC"

Il mio problema nel creare la regex per questo è che per consentire o non consentire determinati personaggi, ho bisogno di guardare indietro. Prendiamo migliaia e centinaia, per esempio.

Posso consentire M {0,2} C? M (per consentire 900, 1000, 1900, 2000, 2900 e 3000). Tuttavia, se la partita è su CM, non posso permettere che i seguenti personaggi siano C o D (perché sono già a 900).

Come posso esprimerlo in una regex?
Se semplicemente non è esprimibile in una regex, è espressibile in una grammatica senza contesto?

Risposte:


328

È possibile utilizzare la seguente regex per questo:

^M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$

Abbattendolo, M{0,4}specifica la sezione delle migliaia e sostanzialmente la limita tra 0e 4000. È relativamente semplice:

   0: <empty>  matched by M{0}
1000: M        matched by M{1}
2000: MM       matched by M{2}
3000: MMM      matched by M{3}
4000: MMMM     matched by M{4}

Naturalmente, potresti usare qualcosa di simile M*per consentire qualsiasi numero (incluso lo zero) di migliaia, se vuoi consentire numeri più grandi.

Il prossimo è (CM|CD|D?C{0,3}), leggermente più complesso, questo è per la sezione centinaia e copre tutte le possibilità:

  0: <empty>  matched by D?C{0} (with D not there)
100: C        matched by D?C{1} (with D not there)
200: CC       matched by D?C{2} (with D not there)
300: CCC      matched by D?C{3} (with D not there)
400: CD       matched by CD
500: D        matched by D?C{0} (with D there)
600: DC       matched by D?C{1} (with D there)
700: DCC      matched by D?C{2} (with D there)
800: DCCC     matched by D?C{3} (with D there)
900: CM       matched by CM

In terzo luogo, (XC|XL|L?X{0,3})segue le stesse regole della sezione precedente ma per il posto delle decine:

 0: <empty>  matched by L?X{0} (with L not there)
10: X        matched by L?X{1} (with L not there)
20: XX       matched by L?X{2} (with L not there)
30: XXX      matched by L?X{3} (with L not there)
40: XL       matched by XL
50: L        matched by L?X{0} (with L there)
60: LX       matched by L?X{1} (with L there)
70: LXX      matched by L?X{2} (with L there)
80: LXXX     matched by L?X{3} (with L there)
90: XC       matched by XC

E, infine, (IX|IV|V?I{0,3})è la sezione di unità, la manipolazione 0attraverso 9e simili a due sezioni precedenti (numeri romani, nonostante la loro apparente stranezza, seguono alcune regole logiche, una volta a capire cosa sono):

0: <empty>  matched by V?I{0} (with V not there)
1: I        matched by V?I{1} (with V not there)
2: II       matched by V?I{2} (with V not there)
3: III      matched by V?I{3} (with V not there)
4: IV       matched by IV
5: V        matched by V?I{0} (with V there)
6: VI       matched by V?I{1} (with V there)
7: VII      matched by V?I{2} (with V there)
8: VIII     matched by V?I{3} (with V there)
9: IX       matched by IX

Tieni presente che quel regex corrisponderà anche a una stringa vuota. Se non vuoi questo (e il tuo motore regex è abbastanza moderno), puoi usare look-behind e look-ahead positivi:

(?<=^)M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})(?=$)

(l'altra alternativa è semplicemente verificare che la lunghezza non sia zero in anticipo).


12
Non dovrebbe essere M {0,3}?
Limone

3
qualche soluzione per evitare di abbinare la stringa vuota?
Facundo Casco,

11
@Aashish: quando i romani erano una forza da non sottovalutare, MMMMera la strada giusta. La rappresentazione overbar è arrivata molto tempo dopo che l'impero centrale è andato in pezzi.
paxdiablo,

2
@paxdiablo è così che ho scoperto che mmmcm fallisce. String regx = "^ M {0,3} (CM | CD | D? C {0,3}) (XC | XL | L? X {0,3}) (IX | IV | V? I {0, 3}) $ "; if (input.matches (regx)) -> questo viene valutato su false per MMMCM / MMMM in java.
modifica

2
/^M{0,3}(?:C[MD]|D?C{0,3})(?:X[CL]|L?X{0,3})(?:I[XV]|V?I{0,3})$/i
Crissov,

23

In realtà, la tua premessa è difettosa. 990 IS "XM", nonché "CMXC".

I romani erano molto meno preoccupati delle "regole" del tuo insegnante di terza elementare. Finché è stato aggiunto, è stato OK. Quindi "IIII" era buono quanto "IV" per 4. E "IIM" era completamente bello per 998.

(Se hai problemi ad affrontarlo ... Ricorda che l'ortografia inglese non fu formalizzata fino al 1700. Fino ad allora, fintanto che il lettore potesse capirlo, era abbastanza buono).


8
Certo, va bene. Ma la mia sintassi "rigorosa insegnante di terza elementare" rende un problema regex molto più interessante, secondo me ...
Daniel Magliola,

5
Buon punto James, uno dovrebbe essere un autore severo ma un lettore tollerante.
Corin,


13

Solo per salvarlo qui:

(^(?=[MDCLXVI])M*(C[MD]|D?C{0,3})(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$)

Abbina tutti i numeri romani. Non importa delle stringhe vuote (richiede almeno una lettera numerica romana). Dovrebbe funzionare in PCRE, Perl, Python e Ruby.

Demo online di Ruby: http://rubular.com/r/KLPR1zq3Hj

Conversione online: http://www.onlineconversion.com/roman_numerals_advanced.htm


2
Non so perché, ma la risposta principale non ha funzionato per me negli elenchi di traduzione automatica in MemoQ. Tuttavia, questa soluzione fa - escludendo i simboli di inizio / fine della stringa.
orlando2bjr,

1
@ orlando2bjr felice di aiutarti. Sì, in questo caso stavo abbinando un numero da solo, senza dintorni. Se lo cerchi in un testo, assicurati di dover rimuovere ^ $. Saluti!
smileart,

12

Per evitare corrispondente alla stringa vuota è necessario ripetere il modello quattro volte e sostituire ogni 0con una 1a sua volta, e rappresentano V, Le D:

(M{1,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})|M{0,4}(CM|C?D|D?C{1,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})|M{0,4}(CM|CD|D?C{0,3})(XC|X?L|L?X{1,3})(IX|IV|V?I{0,3})|M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|I?V|V?I{1,3}))

In questo caso (poiché questo modello usa ^e $) sarebbe meglio verificare prima le linee vuote e non preoccuparsi di abbinarle. Se stai usando i confini delle parole, allora non hai problemi perché non esiste una parola vuota. (Almeno regex non ne definisce uno; non iniziare a filosofare, qui sono pragmatico!)


Nel mio caso particolare (mondo reale) avevo bisogno di abbinare i numeri alla fine delle parole e non ho trovato altro modo per evitarlo. Avevo bisogno di cancellare i numeri delle note a piè di pagina dal mio documento di testo semplice, in cui erano stati convertiti testi come "il cl del Mar Rosso e il cli della Grande barriera corallina " the Red Seacl and the Great Barrier Reefcli. Ma ho ancora avuto problemi con parole valide come Tahitie fantasticsono scrub in Tahite fantasti.


Ho un problema simile (!): Eseguire un "taglio a sinistra" del numero romano residuo / residuo di un elenco di articoli (HTML OL di tipo I o i). Quindi, quando rimangono, ho bisogno di pulire (come una funzione di taglio) con la tua regex all'inizio (a sinistra) del testo dell'oggetto ... Ma più semplice: gli oggetti non usano mai Mo Co L, quindi, hai questo tipo di regex semplificato?
Peter Krauss,

... ok, qui sembra ok (!),(X{1,3}(IX|IV|V?I{0,3})|X{0,3}(IX|I?V|V?I{1,3}))
Peter Krauss,

1
non è necessario ripetere il modello, per rifiutare le stringhe vuote. Potresti usare un'asserzione lookahead
jfs

7

Fortunatamente, l'intervallo di numeri è limitato a 1-399 o giù di lì. Pertanto, è possibile creare il regex piece-meal.

<opt-thousands-part><opt-hundreds-part><opt-tens-part><opt-units-part>

Ognuna di queste parti affronterà i capricci della notazione romana. Ad esempio, usando la notazione Perl:

<opt-hundreds-part> = m/(CM|DC{0,3}|CD|C{1,3})?/;

Ripetere e assemblare.

Aggiunto : <opt-hundreds-part>può essere ulteriormente compresso:

<opt-hundreds-part> = m/(C[MD]|D?C{0,3})/;

Poiché la clausola 'D? C {0,3}' non può corrispondere a nulla, non è necessario il punto interrogativo. E, molto probabilmente, le parentesi dovrebbero essere il tipo non catturante - in Perl:

<opt-hundreds-part> = m/(?:C[MD]|D?C{0,3})/;

Naturalmente, dovrebbe anche essere insensibile al maiuscolo / minuscolo.

Puoi anche estenderlo per gestire le opzioni menzionate da James Curran (per consentire XM o IM per 990 o 999 e CCCC per 400, ecc.).

<opt-hundreds-part> = m/(?:[IXC][MD]|D?C{0,4})/;

A partire da thousands hundreds tens units, è facile creare un FSM che calcola e convalida determinati numeri romani
jfs

Cosa intendi per fortuna, l'intervallo di numeri è limitato a 1-399 o giù di lì ? Chi l'ha limitato?
SexyBeast,

@SexyBeast: Non esiste alcuna notazione romana standard per 5.000, figuriamoci numeri più grandi, quindi le regolarità che funzionano fino a quando smettono di funzionare.
Jonathan Leffler,

Non sono sicuro del motivo per cui ci credi, ma i numeri romani possono rappresentare numeri in milioni. en.wikipedia.org/wiki/Roman_numerals#Large_numbers
AmbroseChapel il

@AmbroseChapel: Come ho detto, non esiste alcuna (singola) notazione standard per 5.000, per non parlare dei numeri più grandi. Devi usare uno dei numerosi sistemi divergenti come indicato nell'articolo di Wikipedia a cui ti colleghi, e devi affrontare problemi con l'ortografia per il sistema con barre superiori, barre sottosopra o C rovesciate ecc. E dovrai spiegare a chiunque cosa sistema che stai usando e cosa significa; le persone non riconosceranno, in generale, i numeri romani al di là di M. Potresti scegliere di pensare diversamente; questa è la tua prerogativa, così come è mia prerogativa sostenere i miei precedenti commenti.
Jonathan Leffler,

7
import re
pattern = '^M{0,3}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$'
if re.search(pattern, 'XCCMCI'):
    print 'Valid Roman'
else:
    print 'Not valid Roman'

Per le persone che vogliono veramente capire la logica, dai un'occhiata a una spiegazione passo passo su 3 pagine su diveintopython .

L'unica differenza rispetto alla soluzione originale (che aveva M{0,4}) è perché ho scoperto che "MMMM" non è un numero romano valido (anche i vecchi romani molto probabilmente non hanno pensato a quel numero enorme e non saranno d'accordo con me). Se sei uno dei vecchi romani sgradevoli, ti prego di perdonarmi e utilizzare la versione {0,4}.


1
la regex nella risposta consente numeri vuoti. Se non lo vuoi; potresti usare un'asserzione lookahead , per rifiutare stringhe vuote (ignora anche il caso delle lettere).
jfs,

2

Sto rispondendo a questa domanda Espressione regolare in Python per numeri romani qui
perché è stato contrassegnato come un duplicato esatto di questa domanda.

Potrebbe essere simile nel nome, ma questa è una domanda / problema regex specifico
come può essere visto da questa risposta a quella domanda.

Gli elementi ricercati possono essere combinati in un'unica alternanza e quindi
racchiusi in un gruppo di acquisizione che verrà inserito in un elenco con la funzione findall ()
.
È fatto così:

>>> import re
>>> target = (
... r"this should pass v" + "\n"
... r"this is a test iii" + "\n"
... )
>>>
>>> re.findall( r"(?m)\s(i{1,3}v*|v)$", target )
['v', 'iii']

Le modifiche regex per fattorizzare e catturare solo i numeri sono queste:

 (?m)
 \s 
 (                     # (1 start)
      i{1,3} 
      v* 
   |  v
 )                     # (1 end)
 $

1

Come hanno sottolineato Jeremy e Pax sopra ... '^ M {0,4} (CM | CD | D? C {0,3}) (XC | XL | L? X {0,3}) (IX | IV | V? I {0,3}) $ 'dovrebbe essere la soluzione che stai cercando ...

L'URL specifico che avrebbe dovuto essere allegato (IMHO) è http://thehazeltree.org/diveintopython/7.html

L'esempio 7.8 è la forma abbreviata che utilizza {n, m}


1

Nel mio caso, stavo cercando di trovare e sostituire tutte le occorrenze di numeri romani con una parola all'interno del testo, quindi non potevo usare l'inizio e la fine delle righe. Quindi la soluzione @paxdiablo ha trovato molte corrispondenze di lunghezza zero. Ho finito con la seguente espressione:

(?=\b[MCDXLVI]{1,6}\b)M{0,4}(?:CM|CD|D?C{0,3})(?:XC|XL|L?X{0,3})(?:IX|IV|V?I{0,3})

Il mio codice Python finale era così:

import re
text = "RULES OF LIFE: I. STAY CURIOUS; II. NEVER STOP LEARNING"
text = re.sub(r'(?=\b[MCDXLVI]{1,6}\b)M{0,4}(?:CM|CD|D?C{0,3})(?:XC|XL|L?X{0,3})(?:IX|IV|V?I{0,3})', 'ROMAN', text)
print(text)

Produzione:

RULES OF LIFE: ROMAN. STAY CURIOUS; ROMAN. NEVER STOP LEARNING

0

Steven Levithan usa questa regex nel suo post che convalida i numeri romani prima di "deromanizzare" il valore:

/^M*(?:D?C{0,3}|C[MD])(?:L?X{0,3}|X[CL])(?:V?I{0,3}|I[XV])$/

0

Ho visto più risposte che non coprono stringhe vuote o usano lookaheads per risolvere questo problema. E voglio aggiungere una nuova risposta che copra le stringhe vuote e non usi lookahead. Il regex è il seguente:

^(I[VX]|VI{0,3}|I{1,3})|((X[LC]|LX{0,3}|X{1,3})(I[VX]|V?I{0,3}))|((C[DM]|DC{0,3}|C{1,3})(X[LC]|L?X{0,3})(I[VX]|V?I{0,3}))|(M+(C[DM]|D?C{0,3})(X[LC]|L?X{0,3})(I[VX]|V?I{0,3}))$

Sto permettendo l'infinito M, M+ma ovviamente qualcuno potrebbe cambiare M{1,4}per consentire solo 1 o 4 se lo si desidera.

Di seguito è una visualizzazione che aiuta a capire cosa sta facendo, preceduta da due demo online:

Debuggex Demo

Demo Regex 101

Visualizzazione delle espressioni regolari


0

Funziona con i motori regex Java e PCRE e ora dovrebbe funzionare con l'ultimo JavaScript ma potrebbe non funzionare in tutti i contesti.

(?<![A-Z])(M*(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3}))(?![A-Z])

La prima parte è l'atroce sguardo negativo dietro. Ma, per scopi logici, è il più facile da capire. Fondamentalmente, il primo (?<!)dice che non corrisponde al centro ([MATCH])se ci sono lettere che arrivano prima del centro ([MATCH])e l'ultimo (?!)dice che non corrispondono al centro ([MATCH])se ci sono lettere che seguono.

Il mezzo ([MATCH])è solo la regex più comunemente usata per abbinare la sequenza di numeri romani. Ma ora, non vuoi abbinarlo se ci sono lettere attorno.

Guarda tu stesso. https://regexr.com/4vce5


-1

Il problema della soluzione di Jeremy e Pax è che non corrisponde nemmeno a "niente".

La seguente regex prevede almeno un numero romano:

^(M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})|[IDCXMLV])$

6
quello non funzionerà (a meno che tu non stia usando un'implementazione regex molto strana) - la parte sinistra della |può corrispondere a una stringa vuota e tutti i numeri romani validi, quindi il lato destro è completamente ridondante. e sì, corrisponde ancora a una stringa vuota.
DirtY iCE

"Il problema della soluzione di Jeremy e Pax è" ... esattamente lo stesso del problema che ha questa risposta. Se hai intenzione di proporre una soluzione a un presunto problema, probabilmente dovresti testarlo. :-)
paxdiablo,

Ho una stringa vuota con questo
Aminah Nuraini,

-2

Scriverei funzioni per il mio lavoro per me. Ecco due funzioni numeriche romane in PowerShell.

function ConvertFrom-RomanNumeral
{
  <#
    .SYNOPSIS
        Converts a Roman numeral to a number.
    .DESCRIPTION
        Converts a Roman numeral - in the range of I..MMMCMXCIX - to a number.
    .EXAMPLE
        ConvertFrom-RomanNumeral -Numeral MMXIV
    .EXAMPLE
        "MMXIV" | ConvertFrom-RomanNumeral
  #>
    [CmdletBinding()]
    [OutputType([int])]
    Param
    (
        [Parameter(Mandatory=$true,
                   HelpMessage="Enter a roman numeral in the range I..MMMCMXCIX",
                   ValueFromPipeline=$true,
                   Position=0)]
        [ValidatePattern("^M{0,3}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$")]
        [string]
        $Numeral
    )

    Begin
    {
        $RomanToDecimal = [ordered]@{
            M  = 1000
            CM =  900
            D  =  500
            CD =  400
            C  =  100
            XC =   90
            L  =   50
            X  =   10
            IX =    9
            V  =    5
            IV =    4
            I  =    1
        }
    }
    Process
    {
        $roman = $Numeral + " "
        $value = 0

        do
        {
            foreach ($key in $RomanToDecimal.Keys)
            {
                if ($key.Length -eq 1)
                {
                    if ($key -match $roman.Substring(0,1))
                    {
                        $value += $RomanToDecimal.$key
                        $roman  = $roman.Substring(1)
                        break
                    }
                }
                else
                {
                    if ($key -match $roman.Substring(0,2))
                    {
                        $value += $RomanToDecimal.$key
                        $roman  = $roman.Substring(2)
                        break
                    }
                }
            }
        }
        until ($roman -eq " ")

        $value
    }
    End
    {
    }
}

function ConvertTo-RomanNumeral
{
  <#
    .SYNOPSIS
        Converts a number to a Roman numeral.
    .DESCRIPTION
        Converts a number - in the range of 1 to 3,999 - to a Roman numeral.
    .EXAMPLE
        ConvertTo-RomanNumeral -Number (Get-Date).Year
    .EXAMPLE
        (Get-Date).Year | ConvertTo-RomanNumeral
  #>
    [CmdletBinding()]
    [OutputType([string])]
    Param
    (
        [Parameter(Mandatory=$true,
                   HelpMessage="Enter an integer in the range 1 to 3,999",
                   ValueFromPipeline=$true,
                   Position=0)]
        [ValidateRange(1,3999)]
        [int]
        $Number
    )

    Begin
    {
        $DecimalToRoman = @{
            Ones      = "","I","II","III","IV","V","VI","VII","VIII","IX";
            Tens      = "","X","XX","XXX","XL","L","LX","LXX","LXXX","XC";
            Hundreds  = "","C","CC","CCC","CD","D","DC","DCC","DCCC","CM";
            Thousands = "","M","MM","MMM"
        }

        $column = @{Thousands = 0; Hundreds = 1; Tens = 2; Ones = 3}
    }
    Process
    {
        [int[]]$digits = $Number.ToString().PadLeft(4,"0").ToCharArray() |
                            ForEach-Object { [Char]::GetNumericValue($_) }

        $RomanNumeral  = ""
        $RomanNumeral += $DecimalToRoman.Thousands[$digits[$column.Thousands]]
        $RomanNumeral += $DecimalToRoman.Hundreds[$digits[$column.Hundreds]]
        $RomanNumeral += $DecimalToRoman.Tens[$digits[$column.Tens]]
        $RomanNumeral += $DecimalToRoman.Ones[$digits[$column.Ones]]

        $RomanNumeral
    }
    End
    {
    }
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.