Как сопоставить только действительные римские цифры с регулярным выражением? - PullRequest
147 голосов
/ 06 ноября 2008

Думая о моей другой проблеме , я решил, что не могу даже создать регулярное выражение, которое будет соответствовать римским цифрам (не говоря уже о неконтекстной грамматике, которая будет генерировать их)

Проблема в сопоставлении только действительных римских цифр. Например, 990 это не "XM", это "CMXC"

Моя проблема в создании регулярного выражения для этого заключается в том, что для того, чтобы разрешить или запретить определенные символы, мне нужно оглянуться назад. Возьмем, к примеру, тысячи и сотни.

Я могу допустить M {0,2} C? M (чтобы учесть 900, 1000, 1900, 2000, 2900 и 3000). Однако, если совпадение на CM, я не могу допустить, чтобы следующие символы были C или D (потому что я уже на 900).

Как я могу выразить это в регулярном выражении?
Если это просто невозможно выразить в регулярном выражении, можно ли это выразить в контекстно-свободной грамматике?

Ответы [ 11 ]

307 голосов
/ 06 ноября 2008

Попробуйте:

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

Разбивка:


M{0,4}

Это определяет раздел тысяч и в основном ограничивает его между 0 и 4000. Это относительно просто:

   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}

(CM|CD|D?C{0,3})

Немного сложнее, это для сотен раздел и охватывает все возможности:

  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

(XC|XL|L?X{0,3})

Те же правила, что и в предыдущем разделе, но для десятки:

 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

(IX|IV|V?I{0,3})

Это раздел единиц, обрабатывающий от 0 до 9, а также аналогичный предыдущим двум разделам (римские цифры, несмотря на их кажущуюся странность, следуют некоторым логическим правилам, как только вы выясните, что они из себя представляют):

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
19 голосов
/ 06 ноября 2008

На самом деле, ваша предпосылка ошибочна. 990 IS"XM", а также "CMXC".

Римляне были гораздо меньше озабочены "правилами", чем ваш учитель в третьем классе. Пока это сложилось, все было в порядке. Следовательно, "IIII" был так же хорош, как "IV" для 4. А "IIM" был совершенно крут для 998.

(Если у вас есть проблемы с этим ... Помните, что английское правописание не было формализовано до 1700-х годов. До тех пор, пока читатель мог это понять, это было достаточно хорошо).

12 голосов
/ 04 мая 2012

Чтобы избежать совпадения с пустой строкой, вам нужно будет повторить шаблон четыре раза и заменить каждый 0 на 1 по очереди и учитывать V, L и 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}))

В этом случае (поскольку в этом шаблоне используются ^ и $), вам лучше сначала проверить пустые строки и не пытаться их сопоставить. Если вы используете границы слова , то у вас нет проблем, потому что нет такого понятия, как пустое слово. (По крайней мере, регулярное выражение не определяет один; не начинайте философствовать, я здесь прагматичен!)


В моем конкретном (реальном мире) случае мне были нужны цифры совпадений в конце слов, и я не нашел другого выхода. Мне нужно было вычеркнуть номера сносок из моего простого текстового документа, где текст, такой как «Красное море cl и Большой Барьерный риф cli » был преобразован в the Red Seacl and the Great Barrier Reefcli. Но у меня все еще были проблемы с правильными словами, такими как Tahiti и fantastic, добавленные в Tahit и fantasti.

11 голосов
/ 12 апреля 2016

Просто чтобы сохранить его здесь:

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

Соответствует всем римским цифрам. Не заботится о пустых строках (требуется хотя бы одна буква римской цифры). Должен работать в PCRE, Perl, Python и Ruby.

Онлайн-демонстрация Ruby: http://rubular.com/r/KLPR1zq3Hj

Онлайн конверсия: http://www.onlineconversion.com/roman_numerals_advanced.htm

7 голосов
/ 06 ноября 2008

К счастью, диапазон номеров ограничен 1..3999 или около того. Следовательно, вы можете создать регулярное регулярное блюдо.

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

Каждая из этих частей будет иметь дело с капризами римской нотации. Например, используя запись Perl:

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

Повторите и соберите.

Добавлено : <opt-hundreds-part> может быть дополнительно сжато:

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

Поскольку предложение 'D? C {0,3}' не может ничего совпадать, знак вопроса не требуется. И, скорее всего, скобки должны быть не захватывающего типа - в Perl:

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

Конечно, все должно быть без учета регистра.

Вы также можете расширить это для работы с опциями, упомянутыми Джеймсом Керраном (чтобы разрешить XM или IM для 990 или 999, и CCCC для 400 и т. Д.).

<opt-hundreds-part> = m/(?:[IXC][MD]|D?C{0,4})/;
6 голосов
/ 31 октября 2014
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'

Для людей, которые действительно хотят понять логику, пожалуйста, ознакомьтесь с пошаговым объяснением на 3 страницах diveintopython .

Единственное отличие от оригинального решения (которое имело M{0,4}) состоит в том, что я обнаружил, что «ММММ» не является действительным римским числом (также старые римляне, скорее всего, не думали об этом огромном числе и не согласятся со мной). Если вы один из неприличных старых римлян, пожалуйста, простите меня и используйте версию {0,4}.

1 голос
/ 06 ноября 2008

Как Джереми и Пакс указали выше ... «^ М {0,4} (СМ | CD | D C {0,3}?) (XC | XL |? L X {0,3}) (IX | IV |? V I {0,3}) $ 'должно быть решением, которое вы ищете ...

Конкретный URL, который должен был быть прикреплен (IMHO): http://thehazeltree.org/diveintopython/7.html

Пример 7.8 - это краткая форма с использованием {n, m}

0 голосов
/ 22 декабря 2018

В моем случае я пытался найти и заменить все вхождения римских чисел одним словом внутри текста, поэтому я не мог использовать начало и конец строк. Таким образом, решение @paxdiablo нашло много совпадений нулевой длины. Я закончил со следующим выражением:

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

Мой окончательный код Python был таким:

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)

Выход:

RULES OF LIFE: ROMAN. STAY CURIOUS; ROMAN. NEVER STOP LEARNING
0 голосов
/ 01 января 2015

Я бы написал функции для своей работы за меня. Вот две функции римской цифры в 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
    {
    }
}
0 голосов
/ 22 июня 2014

Стивен Левитан использует это регулярное выражение в своем посте , в котором проверяются римские цифры перед тем, как "демероманизировать" значение:

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