Bagaimana Anda mencocokkan hanya angka romawi yang valid dengan ekspresi reguler?

165

Memikirkan masalah saya yang lain , saya memutuskan saya bahkan tidak bisa membuat ekspresi reguler yang akan cocok dengan angka romawi (apalagi tata bahasa bebas konteks yang akan menghasilkan mereka)

Masalahnya hanya cocok dengan angka romawi yang valid. Misalnya, 990 BUKAN "XM", itu "CMXC"

Masalah saya dalam membuat regex untuk ini adalah bahwa untuk mengizinkan atau tidak mengizinkan karakter tertentu, saya perlu melihat ke belakang. Mari kita ambil ribuan dan ratusan, misalnya.

Saya dapat mengizinkan M {0,2} C? M (untuk memungkinkan 900, 1000, 1900, 2000, 2900 dan 3000). Namun, Jika kecocokannya di CM, saya tidak bisa membiarkan karakter berikut menjadi C atau D (karena saya sudah di 900).

Bagaimana saya bisa mengekspresikan ini dalam suatu regex?
Jika itu sama sekali tidak dapat diungkapkan dalam suatu regex, apakah itu dapat diungkapkan dalam tata bahasa bebas konteks?

Daniel Magliola
sumber

Jawaban:

328

Anda dapat menggunakan regex berikut untuk ini:

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

Memecahnya, M{0,4}menentukan bagian ribuan dan pada dasarnya menahannya antara 0dan 4000. Ini relatif sederhana:

   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}

Anda bisa, tentu saja, menggunakan sesuatu seperti M*mengizinkan angka berapa pun (termasuk nol) ribuan, jika Anda ingin memperbolehkan angka yang lebih besar.

Berikutnya adalah (CM|CD|D?C{0,3}), sedikit lebih kompleks, ini untuk bagian ratusan dan mencakup semua kemungkinan:

  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

Ketiga, (XC|XL|L?X{0,3})ikuti aturan yang sama seperti bagian sebelumnya tetapi untuk tempat puluhan:

 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

Dan, akhirnya, (IX|IV|V?I{0,3})adalah bagian unit, penanganan 0melalui 9dan juga mirip dengan dua bagian sebelumnya (angka Romawi, meskipun keanehan tampak mereka, mengikuti beberapa aturan yang logis setelah Anda mengetahui apa yang mereka):

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

Perlu diingat bahwa regex itu juga akan cocok dengan string kosong. Jika Anda tidak menginginkan ini (dan mesin regex Anda cukup modern), Anda dapat menggunakan pandangan ke belakang dan melihat ke depan:

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

(Alternatif lain adalah hanya memeriksa bahwa panjangnya tidak nol sebelumnya).

paxdiablo
sumber
12
Bukankah seharusnya M {0,3}?
lemon
3
solusi apa pun untuk menghindari pencocokan string kosong?
Facundo Casco
11
@ Aashish: Ketika Romawi adalah kekuatan yang harus diperhitungkan, MMMMadalah cara yang benar. Representasi overbar datang jauh setelah kerajaan inti hancur berkeping-keping.
paxdiablo
2
@paxdiablo beginilah cara saya menemukan mmmcm gagal. 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)) -> ini dievaluasi menjadi false untuk MMMCM / MMMM di java.
pagi
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

Sebenarnya, premis Anda cacat. 990 IS "XM", serta "CMXC".

Bangsa Romawi tidak terlalu peduli tentang "aturan" daripada guru kelas tiga Anda. Selama ditambahkan, itu OK. Karenanya "IIII" sama baiknya dengan "IV" untuk 4. Dan "IIM" benar-benar keren untuk 998.

(Jika Anda mengalami kesulitan menangani itu ... Ingat ejaan bahasa Inggris tidak diformalkan sampai tahun 1700-an. Sampai saat itu, selama pembaca dapat mengetahuinya, itu cukup baik).

James Curran
sumber
8
Tentu, itu keren. Tapi sintaks "guru kelas tiga yang ketat" saya perlu membuat masalah regex yang jauh lebih menarik, menurut saya ...
Daniel Magliola
5
Poin bagus James, seseorang harus menjadi penulis yang ketat tetapi pembaca yang pemaaf.
Corin
@Corin: alias prinsip ketahanan Postel
jfs
13

Untuk menyimpannya di sini:

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

Cocok dengan semua angka Romawi. Tidak peduli dengan string kosong (membutuhkan setidaknya satu huruf angka Romawi). Harus bekerja di PCRE, Perl, Python, dan Ruby.

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

Konversi Online: http://www.onlineconversion.com/roman_numerals_advanced.htm

smileart
sumber
2
Saya tidak tahu mengapa, tetapi jawaban utama tidak berfungsi untuk saya dalam daftar terjemahan otomatis di MemoQ. Namun, solusi ini tidak - tidak termasuk simbol awal / akhir string.
orlando2bjr
1
@ orlando2bjr senang membantu. Ya, dalam hal ini saya mencocokkan nomor sendiri, tanpa lingkungan. Jika Anda mencarinya dalam sebuah teks, tentu Anda harus menghapus ^ $. Bersulang!
smileart
12

Untuk menghindari pencocokan string kosong Anda harus mengulangi pola empat kali dan mengganti masing 0- masing dengan 1pada gilirannya, dan akun untuk V, Ldan 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}))

Dalam hal ini (karena pola ini menggunakan ^dan $) Anda akan lebih baik memeriksa baris kosong terlebih dahulu dan jangan repot-repot mencocokkannya. Jika Anda menggunakan batas kata maka Anda tidak memiliki masalah karena tidak ada kata kosong. (Setidaknya regex tidak mendefinisikan satu; jangan mulai berfilsafat, saya bersikap pragmatis di sini!)


Dalam kasus (dunia nyata) saya sendiri, saya membutuhkan angka yang cocok pada akhir kata dan saya tidak menemukan cara lain untuk mengatasinya. Saya butuhkan untuk menggosok off nomor catatan kaki dari dokumen teks biasa saya, di mana teks seperti "Laut Merah cl dan Great Barrier Reef cli " telah dikonversi ke the Red Seacl and the Great Barrier Reefcli. Tapi saya masih punya masalah dengan kata-kata yang valid seperti Tahitidan fantasticdigosok ke dalam Tahitdan fantasti.

Corin
sumber
Saya memiliki masalah yang sama (!): Untuk melakukan "trim kiri" dari sisa / sisa nomor romawi dari daftar item (HTML OL dari tipe I atau i). Jadi, ketika ada sisa, saya harus bersih (seperti fungsi trim) dengan regex Anda di awal (kiri) dari item-teks ... Tapi yang lebih sederhana: item tidak pernah menggunakan Matau Catau L, sehingga, apakah Anda memiliki ini jenis regex yang disederhanakan?
Peter Krauss
... ok, ini sepertinya ok (!),(X{1,3}(IX|IV|V?I{0,3})|X{0,3}(IX|I?V|V?I{1,3}))
Peter Krauss
1
Anda tidak perlu mengulangi polanya, untuk menolak string kosong. Anda bisa menggunakan pernyataan lookahead
jfs
7

Untungnya, kisaran angka terbatas pada 1..3999 atau sekitar itu. Oleh karena itu, Anda dapat membangun sepotong makan regex.

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

Masing-masing bagian akan berurusan dengan tingkah laku notasi Romawi. Misalnya, menggunakan notasi Perl:

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

Ulangi dan rakit.

Ditambahkan : <opt-hundreds-part>Dapat dikompresi lebih lanjut:

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

Karena klausa 'D? C {0,3}' tidak cocok dengan apa pun, tidak perlu ada tanda tanya. Dan, kemungkinan besar, tanda kurung harus menjadi tipe yang tidak menangkap - di Perl:

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

Tentu saja, semua harus case-sensitive juga.

Anda juga dapat memperluas ini untuk menangani opsi yang disebutkan oleh James Curran (untuk memungkinkan XM atau IM untuk 990 atau 999, dan CCCC untuk 400, dll).

<opt-hundreds-part> = m/(?:[IXC][MD]|D?C{0,4})/;
Jonathan Leffler
sumber
Dimulai dengan thousands hundreds tens units, mudah untuk membuat FSM yang menghitung dan memvalidasi angka Romawi
jfs
Apa yang Anda maksud dengan Untungnya, kisaran angka terbatas pada 1..3999 atau sekitar itu ? Siapa yang membatasi?
SexyBeast
@SexyBeast: Tidak ada notasi Romawi standar untuk 5.000, apalagi angka yang lebih besar, sehingga keteraturan yang bekerja kemudian berhenti bekerja.
Jonathan Leffler
Tidak yakin mengapa Anda percaya itu, tetapi angka Romawi dapat mewakili angka menjadi jutaan. en.wikipedia.org/wiki/Roman_numerals#Large_numbers
AmbroseChapel
@ AmbroseChapel: Seperti yang saya katakan, tidak ada notasi standar (tunggal) untuk 5.000, apalagi angka yang lebih besar. Anda harus menggunakan salah satu dari sejumlah sistem yang berbeda seperti yang diuraikan dalam artikel Wikipedia yang Anda tautkan, dan Anda menghadapi masalah dengan ortografi untuk sistem dengan overbars, underbar, atau terbalik C dll. Dan Anda harus menjelaskan kepada siapa pun apa sistem yang Anda gunakan dan apa artinya; orang tidak akan, secara umum, mengenali angka Romawi di luar M. Anda dapat memilih untuk berpikir sebaliknya; itu adalah hak prerogatif Anda, sama seperti hak prerogatif saya untuk mendukung komentar saya sebelumnya.
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'

Bagi orang yang benar-benar ingin memahami logika, silakan lihat penjelasan langkah demi langkah pada 3 halaman tentang diveintopython .

Satu-satunya perbedaan dari solusi asli (yang dimiliki M{0,4}) adalah karena saya menemukan bahwa 'MMMM' bukan angka Romawi yang valid (juga orang Romawi lama kemungkinan besar belum memikirkan jumlah yang besar itu dan tidak akan setuju dengan saya). Jika Anda salah satu dari orang Romawi yang tidak setuju, tolong maafkan saya dan gunakan versi {0,4}.

Salvador Dali
sumber
1
regex dalam jawaban memungkinkan angka kosong. Jika Anda tidak menginginkannya; Anda bisa menggunakan pernyataan lookahead , untuk menolak string kosong (itu juga mengabaikan huruf).
jfs
2

Saya menjawab pertanyaan ini Ekspresi Reguler dalam Python untuk Angka Romawi di sini
karena itu ditandai sebagai duplikat yang tepat dari pertanyaan ini.

Mungkin mirip dalam nama, tetapi ini adalah pertanyaan / masalah regex spesifik
seperti yang dapat dilihat oleh jawaban untuk pertanyaan itu.

Item yang dicari dapat digabungkan menjadi satu pergantian tunggal dan kemudian
terbungkus di dalam grup tangkap yang akan dimasukkan ke dalam daftar dengan fungsi findall ()
.
Ini dilakukan seperti ini:

>>> 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']

Modifikasi regex untuk faktor dan menangkap hanya angka adalah ini:

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

Seperti ditunjukkan Jeremy dan Pax di atas ... '^ M {0,4} (CM | CD | D? C {0,3}) (XC | XL | L? X {0,3}) (IX | IV | V? Saya {0,3}) $ 'harus menjadi solusi yang Anda cari ...

URL spesifik yang seharusnya dilampirkan (IMHO) adalah http://thehazeltree.org/diveintopython/7.html

Contoh 7.8 adalah formulir singkat menggunakan {n, m}

Jonathan Leffler
sumber
1

Dalam kasus saya, saya mencoba mencari dan mengganti semua kemunculan angka romawi dengan satu kata di dalam teks, jadi saya tidak bisa menggunakan awal dan akhir baris. Jadi solusi @paxdiablo menemukan banyak kecocokan dengan panjang nol. Saya berakhir dengan ungkapan berikut:

(?=\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})

Kode Python terakhir saya adalah seperti ini:

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)

Keluaran:

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

Steven Levithan menggunakan regex ini dalam posnya yang memvalidasi angka romawi sebelum "menghilangkan nilai" nilainya:

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

Saya telah melihat beberapa jawaban yang tidak mencakup string kosong atau menggunakan lookaheads untuk menyelesaikan ini. Dan saya ingin menambahkan jawaban baru yang mencakup string kosong dan tidak menggunakan lookahead. Regex adalah yang berikut:

^(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}))$

Saya mengizinkan untuk yang tak terbatas M, dengan M+tetapi tentu saja seseorang dapat berubah M{1,4}untuk mengizinkan hanya 1 atau 4 jika diinginkan.

Di bawah ini adalah visualisasi yang membantu untuk memahami apa yang dilakukannya, didahului oleh dua demo online:

Demo Debuggex

Regex 101 Demo

Visualisasi ekspresi reguler

Bernardo Duarte
sumber
0

Ini berfungsi di mesin regex Java dan PCRE dan sekarang harus berfungsi dalam JavaScript terbaru tetapi mungkin tidak berfungsi di semua konteks.

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

Bagian pertama adalah tampilan negatif yang mengerikan. Tapi, untuk tujuan logis, ini yang paling mudah dipahami. Pada dasarnya, yang pertama (?<!)mengatakan tidak cocok dengan tengah ([MATCH])jika ada huruf yang datang sebelum tengah ([MATCH])dan yang terakhir (?!)mengatakan tidak cocok dengan tengah([MATCH]) jika ada huruf yang datang setelah itu.

Bagian tengah ([MATCH])hanyalah regex yang paling umum digunakan untuk mencocokkan urutan Angka Romawi. Tapi sekarang, Anda tidak ingin mencocokkannya jika ada surat di sekitarnya.

Lihat diri mu sendiri. https://regexr.com/4vce5

ketel
sumber
-1

Masalah dari solusi dari Jeremy dan Pax adalah, itu tidak cocok dengan "tidak ada".

Regex berikut ini mengharapkan setidaknya satu angka romawi:

^(M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})|[IDCXMLV])$
Marvin Frommhold
sumber
6
yang tidak akan berfungsi (kecuali jika Anda menggunakan implementasi regex yang sangat aneh) - bagian kiri |dapat mencocokkan string kosong dan semua angka romawi yang valid, sehingga sisi kanan sepenuhnya mubazir. dan ya, itu masih cocok dengan string kosong.
Kotor iCE
"Masalah solusi dari Jeremy dan Pax adalah" ... persis sama dengan masalah yang dimiliki jawaban ini. Jika Anda akan mengusulkan solusi untuk masalah yang seharusnya, Anda mungkin harus mengujinya. :-)
paxdiablo
Saya mendapat string kosong dengan ini
Aminah Nuraini
-2

Saya akan menulis fungsi untuk pekerjaan saya untuk saya. Berikut adalah dua fungsi angka romawi di 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
    {
    }
}
Vince Ypma
sumber