{
  "schemaVersion": 1,
  "slug": "roman-numeral-calculator",
  "category": "math",
  "kind": "implementation",
  "independentValidation": false,
  "implementationHash": "98f73a8c05458d21b58bc9fd64790a12b45755ea29f4b136e32c7650d1fde478",
  "provenance": "Generated from the calculation contract used by the published page. Publication is not external validation.",
  "sourceFiles": [
    "data/calculators/roman-numeral-calculator.json",
    "src/engine.js"
  ],
  "runtimeSource": "/sources/engine.txt",
  "runtimeHash": "5c888d8898afbdee36267bb9977829e1e2ac568f6677c8ee87469c139864645c",
  "scope": "The compute function records implemented equations, constants, lookup tables and custom rules. Inputs record defaults and allowed values; outputs record precision and units. Defaults are examples, not recommendations. General background references are not evidence for this specific algorithm.",
  "publishedFormula": {
    "formula": "n₀ = n; v_j = max{v ∈ V ∶ v ≤ n_{j−1}}; n_j = n_{j−1} − v_j (j = 1,…,m); n_m = 0; R = s₁s₂⋯s_m; N = Σ_{k=1}^{ℓ} q_k v_k; q_k = {−1, v_k < v_{k+1}; 1, v_k ≥ v_{k+1}}; v_{ℓ+1} = 0",
    "vars": [
      [
        "n",
        "Decimal integer 1–3999"
      ],
      [
        "V",
        "Table value set {1,4,5,9,10,40,50,90,100,400,500,900,1000}"
      ],
      [
        "j",
        "Encoding step, 1 ≤ j ≤ m"
      ],
      [
        "v_j",
        "Largest value in V with v_j ≤ n_{j−1}"
      ],
      [
        "s_j",
        "Table symbol paired with v_j (I, IV, V, …, M)"
      ],
      [
        "n_j",
        "Remainder after step j, n_j = n_{j−1} − v_j"
      ],
      [
        "m",
        "Number of symbols in R; n_m = 0"
      ],
      [
        "R",
        "Roman numeral: concatenation s₁s₂⋯s_m"
      ],
      [
        "k",
        "Position in the input numeral, 1 ≤ k ≤ ℓ"
      ],
      [
        "ℓ",
        "Number of input symbols"
      ],
      [
        "v_k",
        "Value of the kth input symbol I,V,X,L,C,D,M"
      ],
      [
        "q_k",
        "−1 before a larger value, otherwise +1"
      ],
      [
        "v_{ℓ+1}",
        "Sentinel 0, so the last symbol has q_ℓ = 1"
      ],
      [
        "N",
        "Decoded number"
      ]
    ],
    "note": "Canonical Roman numerals only, from I to MMMCMXCIX (1–3999); invalid forms such as IIII and IC are rejected."
  },
  "externalEvidence": null,
  "contract": {
    "compute": "const table=[[1000,'M'],[900,'CM'],[500,'D'],[400,'CD'],[100,'C'],[90,'XC'],[50,'L'],[40,'XL'],[10,'X'],[9,'IX'],[5,'V'],[4,'IV'],[1,'I']]; const invalid=()=>({result:null,decimal:null,length:null,__error:'inputError'}); if(direction==='fromRoman'){const s=String(value).trim().toUpperCase();if(!/^(?=.)M{0,3}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$/.test(s))return invalid();let total=0;const vals={I:1,V:5,X:10,L:50,C:100,D:500,M:1000};for(let k=0;k<s.length;k++)total+=vals[s[k]]< (vals[s[k+1]]||0)?-vals[s[k]]:vals[s[k]];return{result:String(total),decimal:total,length:s.length};} const raw=String(value).trim();if(!/^\\d+$/.test(raw))return invalid();const num=Number(raw);if(!Number.isSafeInteger(num)||num<1||num>3999)return invalid();let rem=num,out='';for(const[v,sym]of table)while(rem>=v){out+=sym;rem-=v;}return{result:out,decimal:num,length:out.length};",
    "inputs": [
      {
        "name": "value",
        "type": "text",
        "default": "2026",
        "step": 1
      },
      {
        "name": "direction",
        "type": "select",
        "default": "toRoman",
        "options": [
          {
            "value": "toRoman"
          },
          {
            "value": "fromRoman"
          }
        ]
      }
    ],
    "outputs": [
      {
        "name": "result",
        "precision": 0,
        "unit": "",
        "type": "text"
      },
      {
        "name": "decimal",
        "precision": 0,
        "unit": ""
      },
      {
        "name": "length",
        "precision": 0,
        "unit": ""
      }
    ],
    "interpret": null
  }
}
