# Relaxing Romans

The Roman numeral for 499 is usually given as CDXCIX which breaks down as CD + XC + IX = 400 + 90 + 9 where numerals like CD and IX are written using subtractive notation. Some programming languages have a roman() function that extends this subtractive notation through various relaxation levels. From Google Sheets documentation:

• 0 indicates strict precedence rules, where I may only precede V and X, X may only precede L and C, and C may only precede D and M. Therefore ROMAN(499,0) is CDXCIX.

• 1 indicates a relaxation where V may precede L and C and L may precede D and M. Therefore ROMAN(499,1) is LDVLIV.

• 2 indicates a further relaxation where I may precede L and C, and X may precede D and M. Therefore ROMAN(499,2) is XDIX.

• 3 indicates a further relaxation where V may precede D and M. Therefore ROMAN(499,3) is VDIV.

• 4 indicates a further relaxation where I may precede D and M. Therefore ROMAN(499,4) is ID.

Write a function that takes a number and a relaxation level as input and outputs or returns a Roman numeral.

Input: number, an integer between 1 and 3999 inclusive, and level, an integer between 0 and 4 inclusive. Input format is flexible and you may also use delimited strings and the like. Inputs can be passed as parameters or read from any source.

Output: a Roman numeral. When level is 0, the output must conform to the standard form. In other levels, the output must observe the list above and give the shortest allowed form that implements the subtractive notation shown in a given level. If your language has a built-in for Roman numerals, you are free to use it to generate the standard form, but you must use your own code to manage other levels. Output format is flexible as long as it can be read as illustrated in these test cases:

1, 0 -> I
1, 4 -> I
499, 0 -> CDXCIX
499, 4 -> ID
1999, 0 -> MCMXCIX
1999, 1 -> MLMVLIV
1999, 2 -> MXMIX
1999, 3 -> MVMIV
1999, 4 -> MIM
2024, 0 -> MMXXIV
48, 1 -> VLIII
993, 2 -> XMIII
1996, 3 -> MVMI
1998, 4 -> MVMIII
3999, 2 -> MMMXMIX
3999, 4 -> MMMIM


This is . The winning criteria is shortest code by language, as long as the output meets the criteria above. Default loopholes forbidden.

(this question spent a week in the Sandbox with no negative or positive votes)

• Do you allow builtins or functions from libraries? Commented Jan 25 at 21:19
• Yes, but for the standard form only, as outlined in the question. Commented Jan 25 at 21:23
• @Arnauld I agree, on level 0 VX and LC do not make sense because that would not be standard form. Don't know why Google's documentation includes them (their arabic() function does not accept those forms either.) Edited the question accordingly. Commented Jan 25 at 22:36
• @LevelRiverSt the point of the question is with managing the transformations between levels. Any subjective prettiness does not come into play. I agree that some forms will be confusing but the output should nevertheless match the list of relaxation levels. Edited the question to clarify the matter. Commented Jan 25 at 22:51
• @NickKennedy using level from 1 to 5 is just fine. Commented Jan 27 at 21:07

# Google Sheets, 364 bytes

=let(ø,tocol(,1),r,roman(A2),a,vstack({"C(D|M)XC","L$1XL";"C(D|M)L","L$1";"X(L|C)IX","V$1IV";"X(L|C)V","V$1"},if(B1<2,ø,{"V(L|C)IV","I$1";"L(D|M)XL","X$1";"L(D|M)VL","X$1V";"L(D|M)IL","X$1IX"}),if(B1<3,ø,{"X(D|M)V","V$1";"X(D|M)IX","V$1IV"}),if(B1<4,ø,{"V(D|M)IV","I$1"})),if(B1,reduce(r,sequence(rows(a)),lambda(r,i,regexreplace(r,index(a,i,1),index(a,i,2)))),r))  Put the number in cell A2, the level in cell B1, and the formula in cell B2. Try it. After posting the challenge, I started wondering whether the transformation could be done with regexes. It turns out that just 11 replaces are required to cover all levels in the number range 1...3999. A smaller number of regexes would probably suffice in languages where replace() can take a function argument. The formula builds an array of regexes and replacement strings that accumulate by level. The array is applied to the standard form using reduce(). Ungolfed: =let( regexReplaceAll_, lambda(text, regexes, replacements, reduce(text, sequence(rows(regexes)), lambda(acc, i, regexreplace(acc, chooserows(regexes, i), chooserows(replacements, i)) )) ), nullRow, tocol(æ, 2), map(B1:F1, lambda(level, let( regexes, vstack( { "C(D|M)XC", "L$1XL"; "C(D|M)L", "L$1"; "X(L|C)IX", "V$1IV"; "X(L|C)V", "V$1" }, if(level < 2, nullRow, { "V(L|C)IV", "I$1"; "L(D|M)XL", "X$1"; "L(D|M)VL", "X$1V"; "L(D|M)IL", "X$1IX" } ), if(level < 3, nullRow, { "X(D|M)V", "V$1"; "X(D|M)IX", "V$1IV" } ), if(level < 4, nullRow, { "V(D|M)IV", "I$1" }
)
),
map(A2:A, lambda(number, let(
standardForm, roman(number),
if(level,
regexReplaceAll_(standardForm, choosecols(regexes, 1), choosecols(regexes, 2)),
standardForm
)
)))
)))
)


# Charcoal, 79 bytes

ＮθＮηＦ⁷«≦⁻⁶ι≔⊗÷⊖ι²ζＦ⮌⊞ＯＥ⊕⌊⟦ζη⟧⟦⁻ζκι⟧⟦ι⟧«≔↨Ｅκ×Ｘχ÷λ²⊕×⁴﹪λ²±¹εＷ¬‹θε«Ｆκ§IVXLCDMμ≧⁻εθ


Try it online! Link is to verbose version of code. Explanation: Greedy algorithm.

ＮθＮη


Input the number and the relaxation.

Ｆ⁷«≦⁻⁶ι


Iterate over the indices of Roman numerals in the string IVXLCDM in reverse order.

≔⊗÷⊖ι²ζ


Calculate the index of the Roman numeral that precedes it at a relaxation of 0.

Ｆ⮌⊞ＯＥ⊕⌊⟦ζη⟧⟦⁻ζκι⟧⟦ι⟧«


Create pairs of indices for up to the given relaxation level, plus also consider the Roman numeral index on its own, and loop over these lists in reverse order, so that the highest value is considered first.

≔↨Ｅκ×Ｘχ÷λ²⊕×⁴﹪λ²±¹ε


Calculate the decimal value of the list, which is obtained by converting each element of the list to decimal and then taking the list as base -1. (The newer version of Charcoal on ATO can vectorise this calculation for a saving of 2 bytes.)

Ｗ¬‹θε«


While the current value does not exceed the input, ...

Ｆκ§IVXLCDMμ


... convert the list to a string and output it, and...

≧⁻εθ


... subtract the value from the input.

# Jelly, 68 bytes

IḢ_ḂṪµƝ>⁴;Ṫ>ḢƊ3Ƥ;_ṪṠ‘Ʋ4ƤḌ=⁵ƲṆ5,2ṁṖPƲ€N<ƝT$¦SƊ}Ẹ? 1ḃ7Ç⁼ɗ1#ḃ7ị“IVXLCDM  A full program taking the Arabic number as the first argument and the 1-indexed relaxation level as the second argument. Prints the Roman numeral to STDOUT. Full explanation to follow, but overall strategy is: 1. Starting at 1, find the first integer that satisfies the following: 1. Convert to bijective base 7 2. Check if any of the following are true: • A smaller integer precedes a larger one, and the gap is too big per the relaxation rules; this is determined by taking the difference between consecutive digits, subtracting one if the second digit was odd, and then comparing to the relaxation level • We have a pattern a, b, c where a < c; examples would be 1, 1, 7 or 1, 3, 5. • We have a pattern a, b, c, b where a < b and c < b, for example 1,7,1,7 3. If none of those were true, index the digit list into 1,5,10,50,100,500,1000, negate smaller digits that precede larger ones, and sum. 4. Compare to the desired Arabic number 2. If different, proceed to the next integer and run through the steps above 3. If we’ve reached our target, bijective convert to base 7 and index into "IVXLCDM" ## Explanation (outdated) IḢ_ḂṪµƝ>⁴;Ṫ>ḢƊ3ƤṆ5,2ṁṖPƲ€N<ƝT$¦SƊ}Ẹ?  # ‎⁡Helper link: test whether a Roman numeral (expressed as digits from 1 to 7) is valid, and if so, convert to Arabic
µƝ                               # ‎⁢Following as a monad for each neighbouring pair of digits:
I                                     # ‎⁣- Increments (differences)
Ḣ                                    # ‎⁤- Head (effectively just remove the one and only increment from its list)
_Ḃ                                  # ‎⁢⁡- Subtract the original pair mod 2
Ṫ                                 # ‎⁢⁢- Tail; this will effectively be (b - a) - (b % 2)
>⁴                             # ‎⁢⁣Greater then the relaxation level
;   Ɗ3Ƥ                      # ‎⁢⁤Concatenate to the following, run as a monad over each overlapping infix length 3:
Ṫ>Ḣ                         # ‎⁣⁡- Tail greater than head
Ẹ?  # ‎⁣⁢If any are non-zero:
Ṇ                     # ‎⁣⁣- Not (will yield zero)
Ɗ}    # ‎⁣⁤Else: following applied as a monad to the link’s original argument (the digit list)
Ʋ€             # ‎⁤⁡- For each digit:
5,2ṁ                 # ‎⁤⁢  - Mould [5,2] into that length (so 5 becomes [5,2,5,2,5]
Ṗ                # ‎⁤⁣  - Remove last (so 5 would now be [5,2,5,2]
P               # ‎⁤⁤  - Product (so 5 would now be 100)
N<ƝT\$¦       # ‎⁢⁡⁡- Negate those where a smaller digit precedes a larger
S      # ‎⁢⁡⁢- Sum
‎⁢⁡⁣
1ḃ7Ç⁼ɗ1#ḃ7ị“                          # ‎⁢⁡⁤Main link
1    ɗ1#                              # ‎⁢⁢⁡Starting at 1, find the first positive integer which satisfies the following, called as a monad with the main link’s left argument as its right:
ḃ7                                   # ‎⁢⁢⁢- Convert to bijective base 7
Ç                                  # ‎⁢⁢⁣- Call helper link
⁼                                 # ‎⁢⁢⁤- Equal to (main link’s left argument)
ḃ7                            # ‎⁢⁣⁡Convert to bijective base 7
ị“IVXLCDM                   # ‎⁢⁣⁢Index into "IVXLCDM" (last closing quote is implicit) and then implicitly print
💎


Created with the help of Luminespire.

• Thanks for the detailed explanation. The code is a bit hard to test because lower levels cause timeouts at TIO. Have you verified it against all test cases? Commented Jan 28 at 18:53
• @doubleunary it worked for almost all, but failed for 1998,5 where it gave IMIM; this also happened for 18,1 which gave IXIX. Now fixed at a cost of 12 bytes so works for all test cases. Commented Jan 30 at 21:03