CP-1610 assembly (Intellivision), 52 ... 48 47 DECLEs1 = 59 bytes
Let's try this on a system that predates Perl by a good 7 years. :-)
Takes a pointer to a null-terminated string in R4. Sets the Zero flag if the input is a valid Roman numeral, or clears it otherwise.
ROMW 10 ; use 10-bit ROM width
ORG $4800 ; map this program at $4800
;; ------------------------------------------------------------- ;;
;; test code ;;
;; ------------------------------------------------------------- ;;
4800 EIS ; enable interrupts
4801 SDBD ; R5 = pointer into test case index
4802 MVII #ndx, R5
4805 MVII #$214, R3 ; R3 = backtab pointer
4807 MVII #11, R0 ; R0 = number of test cases
4809 loop SDBD ; R4 = pointer to next test case
480A MVI@ R5, R4
480B PSHR R0 ; save R0, R3, R5 onto the stack
480C PSHR R3
480D PSHR R5
480E CALL isRoman ; invoke our routine
4811 PULR R5 ; restore R5 and R3
4812 PULR R3
4813 MVII #$1A7, R0 ; use a white 'T' by default
4815 BEQ disp
4817 MVII #$137, R0 ; or a white 'F' is the Z flag was cleared
4819 disp MVO@ R0, R3 ; draw it
481A INCR R3 ; increment the backtab pointer
481B PULR R0 ; restore R0
481C DECR R0 ; and advance to the next test case, if any
481D BNEQ loop
481F DECR R7 ; loop forever
;; ------------------------------------------------------------- ;;
;; test cases ;;
;; ------------------------------------------------------------- ;;
4820 ndx BIDECLE test0, test1, test2, test3
4828 BIDECLE test4, test5, test6, test7, test8, test9, test10
; truthy
4836 test0 STRING "MCCXXXIV", 0
483F test1 STRING "CMLXXXVIII", 0
484A test2 STRING "DXIV", 0
484F test3 STRING "CI", 0
; falsy
4852 test4 STRING "MMIXVIII", 0
485B test5 STRING "IVX", 0
485F test6 STRING "IXV", 0
4863 test7 STRING "MMMM", 0
4868 test8 STRING "XXXVX", 0
486E test9 STRING "IVI", 0
4872 test10 STRING "VIV", 0
;; ------------------------------------------------------------- ;;
;; routine ;;
;; ------------------------------------------------------------- ;;
isRoman PROC
4876 PSHR R5 ; push the return address
4877 MOVR R7, R2 ; R2 = dummy 1st suffix
4878 MOVR R2, R5 ; R5 = pointer into table
4879 ADDI #@tbl-$+1,R5
487B @loop MVI@ R5, R1 ; R1 = main digit (M, C, X, I)
487C MVI@ R5, R3 ; R3 = prefix or 2nd suffix (-, D, L, V)
487D MVI@ R4, R0 ; R0 = next digit
487E CMPR R0, R3 ; if this is the prefix ...
487F BNEQ @main
4881 COMR R2 ; ... disable the suffixes
4882 COMR R3 ; by setting them to invalid values
4883 MVI@ R4, R0 ; and read R0 again
4884 @main CMPR R0, R1 ; if R0 is not equal to the main digit,
4885 BNEQ @back ; assume that this part is over
4887 MVI@ R4, R0 ; R0 = next digit
4888 CMPR R0, R1 ; if this is a 2nd occurrence
4889 BNEQ @suffix ; of the main digit ...
488B CMP@ R4, R1 ; ... it may be followed by a 3rd occurrence
488C BNEQ @back
488E MOVR R2, R0 ; if so, force the test below to succeed
488F @suffix CMPR R0, R2 ; otherwise, it may be either the 1st suffix
4890 BEQ @next
4892 CMPR R0, R3 ; or the 2nd suffix (these tests always fail
4893 BEQ @next ; if the suffixes were disabled above)
4895 @back DECR R4 ; the last digit either belongs to the next
; iteration or is invalid
4896 @next MOVR R1, R2 ; use the current main digit
; as the next 1st suffix
4897 SUBI #'I', R1 ; was it the last iteration? ...
4899 BNEQ @loop
489B CMP@ R4, R1 ; ... yes: make sure that we've also reached
; the end of the input
489C PULR R7 ; return
489D @tbl DECLE 'M', '-' ; table format: main digit, 2nd suffix
489F DECLE 'C', 'D'
48A1 DECLE 'X', 'L'
48A3 DECLE 'I', 'V'
ENDP
How?
The regular expression can be rewritten as 4 groups with the same structure, provided that #
is any invalid character that is guaranteed not be present in the input string.
+-------+---> main digit
| |
(M[##]|#?M{0,3})(C[MD]|D?C{0,3})(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})
|| |
|+--+-----> prefix or second suffix
|
+---------> first suffix
The first suffix of the group \$N\$ is the main digit of the group \$N-1\$. Therefore, we can store the patterns with the pair \$(\text{main_digit}, \text{second_suffix})\$ alone.
Our routine attempts to parse the input string character by character according to these patterns and eventually checks whether the end of the string is reached.
Output
screenshot of jzIntv
1. A CP-1610 opcode is encoded with a 10-bit value, known as a 'DECLE'. This routine is 47 DECLEs long, starting at $4876 and ending at $48A4 (included).
MMMM
invalid? Is there a letter for 5000 that should be used instead for M<letter>? \$\endgroup\$I,V,X,L,C,D,M
. \$\endgroup\$