Как сопоставить только действительные римские цифры с регулярным выражением?

165

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

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

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

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

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

Даниэль Маглиола
источник

Ответы:

328

Для этого вы можете использовать следующее регулярное выражение:

^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}

Конечно, вы можете использовать что-то вроде M*разрешения, чтобы разрешить любое число (включая ноль) тысяч, если вы хотите разрешить большее число.

Далее (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

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

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

(Другой вариант - просто проверить, что длина не равна нулю заранее).

paxdiablo
источник
12
Разве это не должно быть М {0,3}?
лимон
3
любое решение, чтобы избежать совпадения с пустой строкой?
Факундо Каско
11
@Aashish: Когда римляне были силой, с которой нужно считаться, это MMMMбыл правильный путь. Представление за решеткой появилось задолго до того, как основная империя распалась.
paxdiablo
2
@paxdiablo вот как я обнаружил, что mmmcm не удается. Строка 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)) -> это превращается в false для MMMCM / MMMM в Java.
amIT
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

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

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

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

Джеймс Керран
источник
8
Конечно, это круто. Но моя потребность в синтаксисе «строгого учителя третьего класса» делает намного более интересной проблему регулярных выражений, на мой взгляд ...
Даниэль Маглиола,
5
Хороший вопрос, Джеймс, нужно быть строгим автором, но прощающим читателем.
Корин
13

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

(^(?=[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

smileart
источник
2
Я не знаю почему, но основной ответ не работал для меня в списках автоматического перевода в MemoQ. Однако, это решение делает - исключая символы начала / конца строки, хотя.
orlando2bjr
1
@ orlando2bjr рад помочь. Да, в этом случае я сопоставлял число самостоятельно, без окружения. Если вы ищете его в тексте, обязательно удалите ^ $. Ура!
smileart
12

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

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


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

Корин
источник
У меня похожая проблема (!): Сделать «левую обрезку» оставшегося / остаточного римского номера списка элементов (HTML OL типа I или i). Итак, когда есть еще, мне нужно очистить (как функция обрезки) с вашим регулярным выражением в начале (слева) от элемента-текста ... Но более просто: элементы никогда не используют Mили, Cили L, так, у вас есть это вид упрощенного регулярного выражения?
Питер Краусс
... хорошо, здесь, кажется, хорошо (!),(X{1,3}(IX|IV|V?I{0,3})|X{0,3}(IX|I?V|V?I{1,3}))
Питер Краусс
1
вам не нужно повторять шаблон, чтобы отклонить пустые строки. Вы можете использовать опережающее утверждение
jfs
7

К счастью, диапазон номеров ограничен 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})/;
Джонатан Леффлер
источник
Начнем с того thousands hundreds tens units, что легко создать FSM, который вычисляет и проверяет данные римскими цифрами
jfs
Что вы имеете в виду, к счастью, диапазон чисел ограничен 1,3999 или около того ? Кто это ограничил?
SexyBeast
@SexyBeast: Нет стандартной римской записи для 5000, не говоря уже о больших числах, поэтому закономерности, которые работают до тех пор, перестают работать.
Джонатан Леффлер
Не уверен, почему вы в это верите, но римские цифры могут представлять цифры в миллионы. en.wikipedia.org/wiki/Roman_numerals#Large_numbers
AmbroseChapel
@AmbroseChapel: Как я уже говорил, не существует (единой) стандартной записи для 5000, не говоря уже о больших числах. Вы должны использовать одну из нескольких расходящихся систем, как описано в статье Википедии, на которую вы ссылаетесь, и вы столкнетесь с проблемами с орфографией для системы с перекладинами, нижними чертами или перевернутой буквой C и т. Д. И вам придется объяснить кому-либо, что система, которую вы используете и что это значит; люди, как правило, не узнают римские цифры за пределами М. Вы можете думать иначе; это ваша прерогатива, так же как и моя прерогатива поддерживать мои предыдущие комментарии.
Джонатан Леффлер
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'

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

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

Сальвадор Дали
источник
1
регулярное выражение в ответе допускает пустые цифры. Если ты не хочешь этого; Вы можете использовать утверждение типа «lookahead» , чтобы отклонить пустые строки (это также игнорирует регистр букв).
JFS
2

Я отвечаю на этот вопрос Регулярное выражение в Python для римских цифр здесь,
потому что оно было помечено как точная копия этого вопроса.

Оно может быть похожим по названию, но это конкретный вопрос / проблема регулярного выражения,
как видно из этого ответа на этот вопрос.

Разыскиваемые элементы могут быть объединены в одно чередование, а затем
заключены в группу захвата, которая будет помещена в список с помощью функции findall ()
.
Это делается так:

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

Модификации regex для разложения и захвата только цифр таковы:

 (?m)
 \s 
 (                     # (1 start)
      i{1,3} 
      v* 
   |  v
 )                     # (1 end)
 $
x15
источник
1

Как Джереми и Пакс указывали выше ... '^ M {0,4} (CM | 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}

Джонатан Леффлер
источник
1

В моем случае я пытался найти и заменить все вхождения римских чисел одним словом в тексте, поэтому я не мог использовать начало и конец строк. Таким образом, решение @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
user2936263
источник
0

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

/^M*(?:D?C{0,3}|C[MD])(?:L?X{0,3}|X[CL])(?:V?I{0,3}|I[XV])$/
Mottie
источник
0

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

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

Я допускаю бесконечное M, M+но, конечно, кто-то может изменить, M{1,4}чтобы разрешить только 1 или 4 при желании.

Ниже приведена визуализация, которая помогает понять, что она делает, и ей предшествуют две онлайн-демонстрации:

Debuggex Demo

Regex 101 Demo

Визуализация регулярных выражений

Бернардо Дуарте
источник
0

Это работает в Java и механизмах регулярных выражений PCRE и теперь должно работать в последней версии JavaScript, но может работать не во всех контекстах.

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

Первая часть - отвратительный негативный взгляд. Но для логических целей это легче всего понять. По сути, первый (?<!)говорит, что не соответствует середине, ([MATCH])если есть буквы перед серединой, ([MATCH])а последний (?!)говорит, что не соответствует середине([MATCH]) если есть буквы после него.

Середина ([MATCH])- это наиболее часто используемое регулярное выражение для сопоставления последовательности римских цифр. Но теперь вы не хотите соответствовать этому, если вокруг него есть буквы.

Посмотреть на себя. https://regexr.com/4vce5

ketenks
источник
-1

Проблема решения от Джереми и Пакса состоит в том, что оно также соответствует «ничему».

Следующее регулярное выражение ожидает по крайней мере одну римскую цифру:

^(M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})|[IDCXMLV])$
Марвин Фроммхолд
источник
6
это не сработает (если вы не используете очень странную реализацию регулярных выражений) - левая часть |может соответствовать пустой строке и всем допустимым римским цифрам, поэтому правая часть полностью избыточна. и да, он по-прежнему соответствует пустой строке.
DirtY ICE
«Проблема решения от Джереми и Пакса» ... точно такая же, как и проблема, которая есть в этом ответе. Если вы собираетесь предложить решение предполагаемой проблемы, вам, вероятно, следует проверить его. :-)
paxdiablo
Я получил пустую строку с этим
Амина Нураини
-2

Я бы написал функции для моей работы для меня. Вот две функции римской цифры в 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
    {
    }
}
Винс Ипма
источник