如何使用正则表达式匹配有效的罗马数字?

191

考虑到我的另一个问题,我决定我甚至不能创建一个匹配罗马数字的正则表达式(更不用说能生成它们的上下文无关文法了)。

问题在于匹配只有有效的罗马数字。 例如,990不是"XM",而是"CMXC"。

我在制作这个正则表达式时的问题是为了允许或不允许某些字符,我需要向后查看。 以千位和百位为例。

我可以允许M{0,2}C?M(以允许900、1000、1900、2000、2900和3000)。然而,如果匹配是在CM上,我就不能允许后面的字符是C或D(因为我已经到了900)。

如何在正则表达式中表达这个问题? 如果在正则表达式中简单地不能表达它,那么它是否可以用上下文无关文法来表达?

17个回答

377
你可以使用以下的正则表达式来完成这个任务:
^M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$

简单来说,M{0,4}指定了千位数,并将其限制在04000之间。这是一个相对简单的规则。
   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})是单位部分,处理09,并且与前两个部分相似(罗马数字,尽管看起来很奇怪,但一旦你弄清楚它们的规则,它们就会遵循一些逻辑规律):
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})$

这是一个“检查匹配但丢弃”的操作,意味着它向前查找以检查第一个字符(.)是否存在于起始标记(^)之后,但不吸收该第一个字符。例如,如果字符串是M,那么它将匹配.,但仍然可用于正则表达式的下一部分M{0,4}。然而,空字符串将无法匹配前瞻,因此会失败。
另一种选择是,如果您不仅仅限于使用正则表达式,可以事先检查长度是否为零。

3
有没有避免匹配空字符串的解决方案? - Facundo Casco
12
当罗马人是一支不可忽视的力量时,“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})$"; 如果 (input.matches(regx)) -> 在 Java 中,这将对 MMMCM / MMMM 求值为 false。 - 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
1
我认为这个仍然无法处理空字符串。 - undefined
显示剩余16条评论

27

实际上,你的前提是错误的。990 就是"XM",也是"CMXC"。

罗马人对于“规则”不像你的三年级老师那么关注。只要加起来正确,它就可以。因此,“IIII”和“IV”一样适用于表示4。而“IIM”对于表示998也完全没问题。

(如果你对此有困难...请记住英语拼写直到18世纪才被正式化。在此之前,只要读者能够理解,就足够了。)


10
没问题,这很酷。但我认为,我的“严格的三年级老师”式语法需要让正则表达式问题更有趣…… - Daniel Magliola
7
好观点,詹姆斯。作为作者应该严格要求自己,但作为读者则应宽容大度。 - Corin
@Corin:也称波斯特鲁斯稳健原则 - jfs

21

2
我不知道为什么,在MemoQ中自动翻译列表时,主要答案对我无效。然而,这个解决方案可以——尽管排除了字符串的开头/结尾符号。 - orlando2bjr
1
@orlando2bjr 很高兴能帮忙。是的,在这种情况下,我只匹配了一个数字本身,没有周围的字符。如果你在文本中查找它,确保你需要去掉 ^$。祝好! - smileart
我该如何使这个匹配在文本块的任何位置。只有当行仅包含数字字符时,才会进行匹配。 - Verty00
@Verty00 请查看之前的评论。 - smileart
1
这里是使用非捕获组的版本,以更好地清理结果。如果您愿意,还可以在外部使用单词边界\b甚至是另一个非捕获组(?:) (\b(?=[MDCLXVI])M*(?:C[MD]|D?C{0,3})(?:X[CL]|L?X{0,3})(?:I[XV]|V?I{0,3})\b) - brandonscript

13
为避免匹配空字符串,您需要将模式重复四次,并依次用1替换每个0,并考虑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。但我仍然有问题,例如有效的单词Tahitifantastic也被清除为Tahitfantasti


我有类似的问题:需要对项目列表(HTML OL类型为I或i)中剩余/残留的罗马数字进行“左修剪”。因此,当存在剩余时,我需要使用您的正则表达式在项目文本的开头(左侧)进行清理(类似于修剪函数)...但更简单:项目从不使用MCL,所以,您是否有这种简化的正则表达式? - Peter Krauss
...好的,这里看起来没问题(!), (X{1,3}(IX|IV|V?I{0,3})|X{0,3}(IX|I?V|V?I{1,3})) - Peter Krauss
2
你不需要重复模式,也不需要拒绝空字符串。你可以使用前瞻断言。 - jfs
@jfs 有些程序(例如 sed)不支持前瞻,因此像这样的“原始”解决方案作为替代方案非常受欢迎。 - Alice M.

8

幸运的是,数字范围仅限于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> = m/(C[MD]|D?C{0,3})/;

'D?C{0,3}'这个子句可能匹配不到任何内容,所以问号是不必要的。而且,在Perl中,括号应该是非捕获类型:

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

当然,这一切也应该是不区分大小写的。
您还可以将其扩展以处理James Curran提到的选项(允许XM或IM用于990或999,CCCC用于400等)。
<opt-hundreds-part> = m/(?:[IXC][MD]|D?C{0,4})/;

从“千百十个”开始,很容易创建一个有限状态机来计算和验证给定的罗马数字 - jfs
你说的“幸运的是,数字范围仅限于1..3999或其周围”,是什么意思?谁进行了限制? - SexyBeast
@SexyBeast:甚至没有标准的罗马符号表示5000,更别说更大的数字了,因此到那时为止有效的规律就停止了。 - Jonathan Leffler
1
不确定您为什么认为罗马数字无法表示百万级别的数,但实际上罗马数字可以表示百万以上的数字。具体请参考维基百科链接:https://en.wikipedia.org/wiki/Roman_numerals#Large_numbers - AmbroseChapel
@AmbroseChapel:正如我所说,甚至没有一个标准符号来表示5,000,更不用说更大的数字了。你必须使用维基百科文章中概述的众多不同系统之一,并且你会面临使用上划线、下划线或反向C等系统时的拼写问题。而且你必须向任何人解释你正在使用什么系统以及它的含义;一般人不会认识超过M的罗马数字。你可以选择不同的想法;这是你的权利,就像坚持我的先前评论一样是我的权利。 - 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'

如果您真的想理解逻辑,请查看diveintopython上三页的详细说明。

唯一与原始解决方案不同的是,我发现'MMMM'不是有效的罗马数字(而且古罗马人很可能没有考虑过那么大的数字,并且会不同意我的观点)。如果您是不同意我的古老罗马人之一,请原谅我并使用{0,4}版本。


1
答案中的正则表达式允许空数字。如果您不想要它,您可以使用前瞻断言来拒绝空字符串(它还忽略字母的大小写)。 - jfs

4

在我的情况下,我试图在文本中查找并替换所有罗马数字为一个词,因此我无法使用行的开头和结尾。所以@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

尝试使用 text = "I'm RULES OF LIFE: I. STAY CURIOUS; II. NEVER STOP LEARNING" 进行测试,它将输出 ROMAN'm RULES OF LIFE: ROMAN. STAY CURIOUS; ROMAN. NEVER STOP LEARNING - Ste
这也是在JavaScript中对我有效的。 - user732456

3
这里有一些真正惊人的答案,但没有一个适合我,因为我需要能够匹配字符串中仅为有效罗马数字的内容,而不匹配空字符串,并且只匹配独立的数字(即不在单词内)。
让我向您介绍Reilly的现代罗马数字严格表达式
^(?=[MDCLXVI])M*(C[MD]|D?C{0,3})(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$

“开箱即用”,它与我所需的相当接近,但它只能匹配独立的罗马数字。当更改为在字符串中匹配时,它会在某些点上匹配空字符串(其中一个单词以大写字母V、M等开头),并且还将给出无效罗马数字的部分匹配,例如MMLLVVDD、XXLLVVDD、MMMMDLVX、XVXDLMM和MMMCCMLXXV。
因此,在进行一些修改后,我最终得到了这个结果:
(?<![MDCLXVI])(?=[MDCLXVI])M{0,3}(?:C[MD]|D?C{0,3})(?:X[CL]|L?X{0,3})(?:I[XV]|V?I{0,3})[^ ]\b

添加的 负向回顾后发断言 将确保它不会做无效罗马数字的部分匹配,并将第一个 M 锁定为 3,因为这是 罗马数字标准形式 中最高的数字。

截至目前,这是唯一通过我 广泛测试套件 的正则表达式,其中包括从 1 到 3999 的所有可能的罗马数字、字符串中的罗马数字和我上面提到的无效罗马数字。

以下是它在 https://regex101.com/ 中的运行截图: 4


1
我在开头添加了一个单词边界,这样它就不会捕捉以拉丁数字结尾的单词(如果字符串匹配不区分大小写)。 - dearsina

2

我看到了多个答案,但它们并没有涵盖空字符串或使用前瞻来解决此问题。我想添加一个新的答案,它可以涵盖空字符串,并且不使用前瞻。正则表达式如下:

^(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演示

Regex 101演示

正则表达式可视化


1
我非常喜欢这个。虽然它更长,但似乎性能更好。为了减少结果的复杂性,您可以使用非捕获组:(?: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})) - brandonscript

1

@paxdiablo建议使用正向预查和反向预查来避免匹配空字符串,但在我看来似乎并没有起作用。

我已经通过使用负向预查来解决这个问题:

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

注意: 如果你在正则表达式末尾添加了一些内容(例如"foobar"),那么显然你需要将(?!$)替换为(?!f)(其中f是"foobar"的第一个字符)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接