g | x | w | all
Bytes Lang Time Link
349Google Sheets240129T175115Zdoubleun
068Jelly240127T144709ZNick Ken
079Charcoal240128T095727ZNeil

Google Sheets, 349 bytes

=let(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().

Relaxing Romans.png

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 
      ) 
    ))) 
  )))
)

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.

Charcoal, 79 bytes

NθNηF⁷«≦⁻⁶ι≔⊗÷⊖ι²ζF⮌⊞OE⊕⌊⟦ζη⟧⟦⁻ζκι⟧⟦ι⟧«≔↨Eκ×Xχ÷λ²⊕×⁴﹪λ²±¹εW¬‹θε«Fκ§IVXLCDMμ≧⁻εθ

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

NθNη

Input the number and the relaxation.

F⁷«≦⁻⁶ι

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.

F⮌⊞OE⊕⌊⟦ζη⟧⟦⁻ζκι⟧⟦ι⟧«

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.

≔↨Eκ×Xχ÷λ²⊕×⁴﹪λ²±¹ε

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.)

W¬‹θε«

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

Fκ§IVXLCDMμ

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

≧⁻εθ

... subtract the value from the input.