The 50 substrings that validate any string of Roman numerals
Given a string of Roman numerals, decide whether it forms a valid Roman number. If not, output the substring that proves this, from the list of 50 strings described below.
Relevant fact
This challenge is based around the following fact:
A string of Roman numerals is a valid Roman number if and only if it contains none of the following 50 strings as a substring:
"CCCC", "CCD", "CCM", "CDC", "CMC", "CMD", "CMM", "DCD", "DCM", "DD", "DM", "IC", "ID", "IIII", "IIV", "IIX", "IL", "IM", "IVI", "IXC", "IXI", "IXL", "IXV", "IXX", "LC", "LD", "LL", "LM", "LXC", "LXL", "MMMM", "VC", "VD", "VIV", "VIX", "VL", "VM", "VV", "VX", "XCC", "XCD", "XCL", "XCM", "XCX", "XD", "XLX", "XM", "XXC", "XXL", "XXXX"
This works for any length string, if a valid Roman number is defined as follows:
Valid Roman numbers[1]
- Each numeral appears no more than 3 times consecutively.
- Each of
V(5),L(50), andD(500) appears no more than once consecutively. - A Roman number is constructed by concatenating the strings representing its thousands, hundreds, tens, and units components.
| Decimal | Thousands | Hundreds | Tens | Units |
|---|---|---|---|---|
| 1 | M | C | X | I |
| 2 | MM | CC | XX | II |
| 3 | MMM | CCC | XXX | III |
| 4 | CD | XL | IV | |
| 5 | D | L | V | |
| 6 | DC | LX | VI | |
| 7 | DCC | LXX | VII | |
| 8 | DCCC | LXXX | VIII | |
| 9 | CM | XC | IX |
So, for example, 2345 would be represented as the concatenation of MM (for 2000), CCC (for 300), XL (for 40), and V (for 5), or MMCCCXLV.
This defines a unique correct representation for each number from 1 to 3999 (4000 and above are not representable without breaking the first two rules). The 50 substrings method described above will identify each of these 3999 strings as valid, and all other strings of Roman numerals as invalid.
Input
- A string containing only Roman numerals (
I,V,X,L,C,D,M). - The string will have length at least 1.
- The string will have length at most 15 (this is the length of the longest valid string of Roman numerals).
- You may choose to take input in lower case instead, provided that you also use lower case in your output for an input that is not a valid Roman number.
Output
- If the input is a valid Roman number, output a consistent value indicating this.
- Consistent means that the value must be the same for all valid Roman numbers.
- The output for a valid Roman number must not be one of the strings from the list of 50.
- Since the consistent value can be anything (and specifically does not need to contain Roman numerals), there is no requirement for it to be upper or lower case.
- If the input is not a valid Roman number, output exactly 1 string from the list of 50.
- The output in this case must be a substring of the input.
- If the input has 2 or more of the strings from the list of 50 as substrings, you may choose any 1 of them to be the output, but you must choose only 1 of them (you must not output 2 or more).
- If you chose to take input in lower case, then this output string must also be in lower case.
Examples
A valid Roman number
The input MCMXCVI is the unique correct representation of 1996. It contains none of the 50 strings.
A string that is not a valid Roman number
Although the input MMXDIII might be suspected of representing 2493, it is not the unique correct representation of this number (which is MMCDXCIII). Note that it has XD as a substring, identifying it as invalid. The only correct output is therefore XD.
An invalid string with more than 1 potential output
The input MMCCMDXXV has 2 substrings that make it invalid, so either CCM or CMD would be correct outputs. It would not be correct to output both of these, or to output their overlap CCMD, as this is not one of the 50 strings.
Test cases
Test cases are in the format INPUT : VALID, OUTPUTS. Note that only one of the valid outputs can be chosen - outputting 2 or more is incorrect.
The output "VALID" is just an example - for an input that is a valid Roman number you may choose to output any consistent value distinct from the 50 strings.
Upper case test cases
These reflect the case used in the rest of the challenge wording, although there is no requirement to use upper case for this challenge.
I : VALID
V : VALID
X : VALID
L : VALID
C : VALID
D : VALID
M : VALID
II : VALID
VV : VV
XX : VALID
LL : LL
CC : VALID
DD : DD
MM : VALID
III : VALID
VII : VALID
IVI : IVI
IIV : IIV
CCI : VALID
CCV : VALID
CCX : VALID
CCL : VALID
CCC : VALID
CCD : CCD
CCM : CCM
IIII : IIII
MLDI : LD
MXXC : XXC
DCIIX : IIX
MCXXXX : XXXX
MCCCCXVI : CCCC
MMLXCVII : LXC
MMMCMXCIX : VALID
MMMDCCCLXXXVIII : VALID
MMCCCXLV : VALID
MCMXCVI : VALID
MMXDIII : XD
MMCDXCIII : VALID
MMCCMDXXV : CCM, CMD
XXX : VALID
CLLX : LL
DXXDMMV : DM, XD
CCDDDIMDD : DD, IM, CCD
VLCXIVXMCVXLC : VX, VL, LC, XM
DVLIILVCXVXVMLI : VX, VL, IL, VM, VC
VVDLMIVILXXDX : VV, VD, IL, XD, LM, IVI
DMXXCMILVCMLLMV : DM, IL, LL, VC, LM, XXC, XCM
CMXDVLCCDDLXLXC : DD, VL, LC, XD, XLX, LXC, CCD, LXL
XDIXCLLMVVLCMCM : VV, VL, LC, XD, LL, LM, IXC, CMC, XCL
IIXXCDVVLMILVDD : DD, VV, VL, VD, IL, LM, XXC, IXX, IIX, XCD
DDMIIXXCMCCDCMM : DD, DM, CMC, CMM, XXC, IXX, DCM, CDC, XCM, CCD, IIX
Lower case test cases
These are the same test cases in lower case, in case you can benefit from taking lower case input.
Note that if you take lower case input then you must also give lower case output where it is one of the 50 strings.
i : valid
v : valid
x : valid
l : valid
c : valid
d : valid
m : valid
ii : valid
vv : vv
xx : valid
ll : ll
cc : valid
dd : dd
mm : valid
iii : valid
vii : valid
ivi : ivi
iiv : iiv
cci : valid
ccv : valid
ccx : valid
ccl : valid
ccc : valid
ccd : ccd
ccm : ccm
iiii : iiii
mldi : ld
mxxc : xxc
dciix : iix
mcxxxx : xxxx
mccccxvi : cccc
mmlxcvii : lxc
mmmcmxcix : valid
mmmdccclxxxviii : valid
mmcccxlv : valid
mcmxcvi : valid
mmxdiii : xd
mmcdxciii : valid
mmccmdxxv : ccm, cmd
xxx : valid
cllx : ll
dxxdmmv : dm, xd
ccdddimdd : dd, im, ccd
vlcxivxmcvxlc : vx, vl, lc, xm
dvliilvcxvxvmli : vx, vl, il, vm, vc
vvdlmivilxxdx : vv, vd, il, xd, lm, ivi
dmxxcmilvcmllmv : dm, il, ll, vc, lm, xxc, xcm
cmxdvlccddlxlxc : dd, vl, lc, xd, xlx, lxc, ccd, lxl
xdixcllmvvlcmcm : vv, vl, lc, xd, ll, lm, ixc, cmc, xcl
iixxcdvvlmilvdd : dd, vv, vl, vd, il, lm, xxc, ixx, iix, xcd
ddmiixxcmccdcmm : dd, dm, cmc, cmm, xxc, ixx, dcm, cdc, xcm, ccd, iix
Scoring
This is a code golf challenge. Your score is the number of bytes in your code. Lowest score for each language wins.
Explanations are optional, but I'm more likely to upvote answers that have one.
-
This is a common modern set of rules, described as Standard form on Wikipedia. It does not reflect all usages during history, but will be the basis of this challenge, since otherwise the 50 substrings approach does not work. ↩︎
[Python], 142 bytes …
4mo ago
[Bash], 118 bytes same reg …
4mo ago
Perl, 119 bytes Read a si …
4mo ago
[Bash], 205 bytes for s …
2y ago
[Python], 220 bytes Works w …
2y ago
Python, 338 bytes 534 bytes …
5mo ago
Vyxal, 65 bitsv2, 8.125 bytes …
3mo ago
vim, 212 bytes ```vim A:"x …
3mo ago
Python, 205 bytes Coding th …
4mo ago
Rust, 258 254 bytes - Saved …
2y ago
[Haskell], 271 bytes …
2y ago
Google Sheets, 155 bytes `` …
2y ago
JavaScript, 149 bytes ``` …
2y ago
13 answers
Python, 142 bytes
import re
r=lambda c:re.search(r'([CIMX])\1{3}|C[CM][DM]|C[DM]C|I[IX][VX]|I[VX]I|VI?[VX]|XC[CLX]|XLX|[DX]C?[DM]|[ILV][CDLM]|[ILX]X[CL]|',c)[0]
- almost same regex as my Perl answer (trailing
|ensures a match) - thanks to @__blackjack__ for the 7 byte improvement
1 comment thread
Perl, 119 bytes
Read a single Roman numeral on stdin (optionally newline-terminated). Outputs nothing unless one of the 50 strings is found, in which case it is printed (without trailing newline):
perl -pe'($_)=/([CIMX])\1{3}|C[CM][DM]|C[DM]C|I[IX][VX]|I[VX]I|VI?[VX]|XC[CLX]|XLX|[DX]C?[DM]|[ILV][CDLM]|[ILX]X[CL]/g'
Minimising a regex is nontrivial but will probably be the bulk of any solution. I expect it is possible to shrink it even more than this attempt.
-
-p- load each line of input as$_, manipulate, then print -
-e- program follows -
/.../g- return list of all matches -
($_) = ...- store first match in$_(if no matches, assigns empty string)
To read a newline-separated list of numerals and to print "VALID" instead of nothing increases code to 126 bytes:
perl -ple'$_=/([CIMX])\1{3}|C[CM][DM]|C[DM]C|I[IX][VX]|I[VX]I|VI?[VX]|XC[CLX]|XLX|[DX]C?[DM]|[ILV][CDLM]|[ILX]X[CL]/?$&:VALID'
-
$_ = /.../ ? $& : VALID- ternary assignment to$_- if a match is found, assign its value ($&), otherwise assign barewordVALID
Python, 220 bytes
Works with Python 3.8 or newer.
n=" IVXLCDM"
x=("".join([n[(d:=ord(c)-32)//8]+n[d-8*(d//8)]for c in'&PW!H.!@/$HF$@G"H6"@7"03#P?%N%O%U%]%^%_&N&O!*!+!1!=!9!<!:!;$=$<"*"+#M#N#L#O#K#C#=#<%MH))\'_X;;']))
r=lambda c:max(s if s in c else"A"for s in x.split())
There are 8 kind of characters, " IVXLCDM". 2 characters are 6 bits that can be easily mapped to printable characters (after code point 32). This string is much shorter (half) then the "DD DM IC ..." string itself. Even if I add the extraction it is still somewhat shorter.
I just regenerate the " IVXLCDM" characters from this string.
So it is the same as the code below just with some decoding from 6-bit-string.
238 bytes
If the r= part is not needed then 2 bytes less
r=lambda c:max(s if s in c else"A"for s in'DD DM IC ID IL IM LC LD LL LM VC VD VL VM VV VX XD XM CCD CCM CDC CMC CMD CMM DCD DCM IIV IIX IVI IXC IXI IXL IXV IXX LXC LXL VIV VIX XCC XCD XCL XCM XCX XLX XXC XXL CCCC IIII MMMM XXXX'.split())
Bash, 205 bytes
for s in C{C{CC,D,M},DC,MC} {CM,DC,D}{D,M} I{C,D,I{II,V,X},L,M,VI,X{C,I,L,V,X}} L{C,D,L,M,XC,XL} MMMM V{C,D,IV,IX,L,M,V,X} X{C{C,D,L,M,X},D,LX,M,X{C,L,XX}}
do echo $1|grep -q $s&&echo $s&&exit
done
echo T
The golfing is done mostly in the list of 50 strings. Other than that, the code is pretty straightforward, with the only additional golfing being the removal of unnecessary whitespace and use of a single-letter variable name.
0 comment threads
Bash, 118 bytes
- same regex as my Perl answer
- the longer version here is pure bash, the short one calls out to POSIX(ish) utilities
grep -Eo '([CIMX])\1{3}|C[CM][DM]|C[DM]C|I[IX][VX]|I[VX]I|VI?[VX]|XC[CLX]|XLX|[DX]C?[DM]|[ILV][CDLM]|[ILX]X[CL]'|sed q
-
-E- use POSIX ERE regex flavour -
-o(non-standard) - outputs each match on separate line
Bash, 133 bytes
[[ $1 =~ ([CIMX])\1{3}|C[CM][DM]|C[DM]C|I[IX][VX]|I[VX]I|VI?[VX]|XC[CLX]|XLX|[DX]C?[DM]|[ILV][CDLM]|[ILX]X[CL] ]]&&echo $BASH_REMATCH
0 comment threads
Python, 338 bytes
534 bytes:
This is not golfed in any way. But it's the current winner!
def validate_roman_numeral(candidate):
invalid_strings = ["CCCC", "CCD", "CCM", "CDC", "CMC", "CMD", "CMM", "DCD", "DCM", "DD", "DM", "IC", "ID", "IIII", "IIV", "IIX", "IL", "IM", "IVI", "IXC", "IXI", "IXL", "IXV", "IXX", "LC", "LD", "LL", "LM", "LXC", "LXL", "MMMM", "VC", "VD", "VIV", "VIX", "VL", "VM", "VV", "VX", "XCC", "XCD", "XCL", "XCM", "XCX", "XD", "XLX", "XM", "XXC", "XXL", "XXXX"]
for invalid_string in invalid_strings:
if invalid_string in candidate:
return invalid_string
return "VALID"
379 bytes: Here's a minimal effort golf of the above:
def v(c):
i=['DD','DM','IC','ID','IL','IM','LC','LD','LL','LM','VC','VD','VL','VM','VV','VX','XD','XM','CCD','CCM','CDC','CMC','CMD','CMM','DCD','DCM','IIV','IIX','IVI','IXC','IXI','IXL','IXV','IXX','LXC','LXL','VIV','VIX','XCC','XCD','XCL','XCM','XCX','XLX','XXC','XXL','CCCC','IIII','MMMM','XXXX']
for s in i:
if s in c:
return s
return "VALID"
344 bytes: A little more effort:
def v(c):
o="VALID"
for s in 'DD','DM','IC','ID','IL','IM','LC','LD','LL','LM','VC','VD','VL','VM','VV','VX','XD','XM','CCD','CCM','CDC','CMC','CMD','CMM','DCD','DCM','IIV','IIX','IVI','IXC','IXI','IXL','IXV','IXX','LXC','LXL','VIV','VIX','XCC','XCD','XCL','XCM','XCX','XLX','XXC','XXL','C'*4,'I'*4,'M'*4,'X'*4:
if s in c:
o=s
return o
338 bytes: Return 1 instead of 'VALID'
def v(c):
o=1
for s in 'DD','DM','IC','ID','IL','IM','LC','LD','LL','LM','VC','VD','VL','VM','VV','VX','XD','XM','CCD','CCM','CDC','CMC','CMD','CMM','DCD','DCM','IIV','IIX','IVI','IXC','IXI','IXL','IXV','IXX','LXC','LXL','VIV','VIX','XCC','XCD','XCL','XCM','XCX','XLX','XXC','XXL','C'*4,'I'*4,'M'*4,'X'*4:
if s in c:
o=s
return o
Haskell, 271 bytes
import Data.Char
import Data.List
n=" IVXLCDM"
r x=maximum[if(s`isInfixOf`x)then s else"A"|s<-words$concat$map(\x->(n!!div x 8):[(n!!(x-8*(div x 8)))])$map(\x->ord x-32)"&PW!H.!@/$HF$@G\"H6\"@7\"03#P?%N%O%U%]%^%_&N&O!*!+!1!=!9!<!:!;$=$<\"*\"+#M#N#L#O#K#C#=#<%MH))\'_X;;"]
0 comment threads
Rust, 258 254 bytes
- Saved four bytes thanks to trichoplax.
fn v(s:&str)->&str{for b in "CCCC
CCD
CCM
CDC
CMC
CMD
CMM
DCD
DCM
DD
DM
IC
ID
IIII
IIV
IIX
IL
IM
IVI
IXC
IXI
IXL
IXV
IXX
LC
LD
LL
LM
LXC
LXL
MMMM
VC
VD
VIV
VIX
VL
VM
VV
VX
XCC
XCD
XCL
XCM
XCX
XD
XLX
XM
XXC
XXL
XXXX".lines(){if s.contains(b){return b}}""}
I tried using a closure instead of a function, but I got lifetime errors which, by the time they're fixed, probably wouldn't make the code any shorter.
3 comment threads
Vyxal, 65 bitsv2, 8.125 bytes
ǎ'øṘøṘ≠;Þg
Bitstring:
11010101110010011010011100100011010110100000100001101110100000000
I'm fairly certain this is correct.
Explained
ǎ'øṘøṘ≠;Þg
ǎ # Push all substrings of the input
' ; # and filter by:
øṘøṘ≠ # Converting from roman numerals to base 10 and then back is NOT the same
Þg # Get the shortest such substring.
💎
Created with the help of Luminespire.
0 comment threads
Python, 205 bytes
Coding the string with the substrings as base 95 number is the shortest I get with this approach:
n=0
for c in"),|9'OJ#SdHlCSD(LqRhc.|)_#s]`N:?foSIb*.BJVa=AH^).&&w*e(<')vw6T=jv[xbyt8voESZtk>E^5J{,":n=n*94+ord(c)-32
S=""
while n:S+=" IVXLCDM"[n&7];n>>=3
f=lambda s:next((a for a in S.split()if a in s),1)
206 bytes
Arpad Horvath's packing two six bit characters into one ASCII character could make the 209 byte solution even better, so this builds on his work. The unpacking is a bit shorter and the final function works different and is also a bit shorter.
n=" IVXLCDM"
x="".join(n[(d:=ord(c)-32)>>3]+n[d&7]for c in'&PW!H.!@/$HF$@G"H6"@7"03#P?%N%O%U%]%^%_&N&O!*!+!1!=!9!<!:!;$=$<"*"+#M#N#L#O#K#C#=#<%MH))\'_X;;')
f=lambda s:next((a for a in x.split()if a in s),1)
209 bytes
I've also noticed that each character of the string with all 50 substrings could be encoded in 3 bits, so I stuffed the whole string into one big integer number and then encoded that as a string with base 36. Because the int() function can easily decode that back to a number.
That's better than the leading solution by Arpad Horvath at the time.
S="";n=int("1ie3bbwb28v97xgfzvwp5cdss8z4647b6lrucee80ozdnm1e7wjv2fx4vczji4341fpac33oekqbpauv3hxf2hhtk87drmxgdfqis1zua0qe",36)
while n:S+=" IVXLCDM"[n&7];n>>=3
f=lambda s:next((a for a in S.split()if a in s),1)
235 bytes
This is what I was starting with. Pretty straight forward: take the first substring that is contained or 1 if no substring was found.
f=lambda s:next((a for a in"DD DM IC ID IL IM LC LD LL LM VC VD VL VM VV VX XD XM CCD CCM CDC CMC CMD CMM DCD DCM IIV IIX IVI IXC IXI IXL IXV IXX LXC LXL VIV VIX XCC XCD XCL XCM XCX XLX XXC XXL CCCC IIII MMMM XXXX".split()if a in s),1)
0 comment threads
vim, 212 bytes
A:<ESC>"xddO11@aj044j116jdj31@ar2j242jdj314jd3j$51@ar4j42d3j512jdG<ESC>:%s/\(\d\d\)/@ar\1@bj/g
"dDqaA 0<ESC>qqbYp$<C-A>qxkDqc@a6@bjq(7@c(@doI
X
C
M<ESC><C-V>3kY3P:%!tr 0-6 IVXLCDM
:%s/ //g
:%s/.*/:s\/.*\\(\0\\).*\/\\1\/e
ggd)"xp@"df:kdd
<C-V> is 0x16. <C-A> is 0x01. <ESC> is 0x1b.
Annotated
A:<ESC>"xdd
Put <INPUT>: in register x
O11@aj044j116jdj31@ar2j242jdj314jd3j$51@ar4j42d3j512jdG<ESC>
Buffer now contains a series of "commands" consisting of two digits, AB, interspersed with actual vim commands. We'll get to what these actually do later.
:%s/\(\d\d\)/@ar\1@bj/g
Each digit pair, AB, becomes @arAB@bj, which will execute macro a, replace
the character under the cursor with (the digit) A, execute macro b B times,
and move the cursor down one line.
"dD
move that series of commands into register d
qaA 0<ESC>q
record macro a: append 0 to the current line
qbYp$<C-A>q
record macro b: make a copy of the current line on the line below, and increment the last number in it
xkD
Delete some junk
qc@a6@bjq
record macro c: make 6 copies of the current line, appending 0 to the original, 1 to the first copy, etc.
The buffer now contains 0 through 6, on separate lines. (These numbers will eventually be replaced by IVXLCDM.)
(7@c
Scroll to the beginning and run macro c 7 times.
The buffer now contains 49 lines (plus some blank ones at the end):
0 0
0 1
0 2
0 3
0 4
0 5
0 6
1 0
1 1
...
6 4
6 5
6 6
Which is to say, all length-2 roman numerals (including invalid ones)
(@d
Scroll to the beginning and run the macro we put in register d earlier. This macro runs through the length-2 sequences and uses macros a and b (plus the occasional deletion and character replacement) to expand the length-2 sequences into the set of all length-2 and length-3 illegal sequences.
oI
X
C
M<ESC>
Append lines with I, X, C, and M to the end
<C-V>3kY3P
Change them to IIII, XXXX, CCCC, MMMM
:%!tr 0-6 IVXLCDM
:%s/ //g
Replace the digits with the corresponding roman numerals, and delete spaces The buffer now contains all 50 illegal sequences.
:%s/.*/:s\/.*\\(\0\\).*\/\\1\/e
Replace each sequence with a regex replace command that replaces any line containing that sequence with just the sequence itself.
e.g. XCD becomes :s/.*\(XCD\).*/\1/e
The e at the end says that it is not an error for the regex to fail to match.
ggd)
Move all those commands into register d
"xp
Bring back our original input (with a : at the end)
@"
Run all our regexes. All illegal roman numerals have now been replaced with one of the subsequences that makes them illegal.
df:
Delete to the colon. Only the legal inputs still have that, so it's a no-op for the illegal inputs. Legal inputs have now been replaced with a blank line.
kdd
There's an extra empty line at the beginning. Delete it.
0 comment threads
Google Sheets, 155 bytes
=regexextract(A1,"CCCC|CC[DM]|CM[CDM]|DC[DM]|DD|DM|I[CDLM]|IIII|II[VX]|IVI|IX[CILVX]|L[CDLM]|LX[CL]|MMMM|V[CDLMVX]|VI[VX]|XCC[DLMX]|XD|XLX|XM|XX[CL]|XXXX")
The same regex as in my JavaScript answer.
Returns an error on valid strings.
JavaScript, 149 bytes
t=>t.match(/CCCC|CC[DM]|CM[CDM]|DC[DM]|DD|DM|I[CDLM]|IIII|II[VX]|IVI|IX[CILVX]|L[CDLM]|LX[CL]|MMMM|V[CDLMVX]|VI[VX]|XCC[DLMX]|XD|XLX|XM|XX[CL]|XXXX/)
Simple regex. Returns null on valid strings.

1 comment thread