This module converts Roman numerals to decimal form, and rejects invalid ones. It can be used normally, or through another module. This is still under construction, check back in a few days!

This module is intended to run through a template, which is still under construction.

To convert Roman numerals to decimal form, use Template:Mlx, which outputs 2013. If the Roman numeral is invalid, it will throw an error. If JavaScript is enabled, you can click the Script error link, and the first sentence should show the cause of the error. The final value will still be given in brackets after the error messages.

To force no errors, use Template:Mlx.

To output errors directly into the text, use Template:Mlx.

To use this through another module, use something like this:

local roman = require( "Module:Sandbox/Squc/Roman" )

local romannum = "MMXIII"
local value = roman.todecimald( romannum )


Error demonstration
function No errors Warnings Invalid Errors
Example MMXIII XXXXX XCC ABC
todecimal 2013 50 Roman numeral usage errors: More than four X in a row, suggestion: L? - roman numeral 5, char 5 190 Roman numeral usage errors: Repeat after subtraction - XCC - roman numeral 3, char 3 100 Syntax errors: Unknown character "A" found - char 1; Unknown character "B" found - char 2
todecimal|mode=1 2013 50 190 100
todecimal|mode=2|disp=Decimal ... Decimal number: 2013, Errors , Time 0.09142 Decimal number: 50, Errors Roman numeral usage errors: More than four X in a row, suggestion: L? - roman numeral 5, char 5, Time 0.0948 Decimal number: 190, Errors Roman numeral usage errors: Repeat after subtraction - XCC - roman numeral 3, char 3, Time 0.09816 Decimal number: 100, Errors Syntax errors: Unknown character "A" found - char 1; Unknown character "B" found - char 2, Time 0.10124
todecimald Intended for use only from other modules. See below for more details.

Errors will be combined, e.g. "XXXXXXCC" gives:

Decimal number: 140, Errors Roman numeral usage errors: More than four X in a row, suggestion: L? - roman numeral 5, char 5; More than four X in a row, suggestion: L? - roman numeral 6, char 6; Number of X before X must be at most two - roman numeral 7, char 7; Repeat after subtraction - XCC - roman numeral 8, char 8, Time 0.1053

or with mode=2|disp=0 (default):

[num]140 [err]Roman numeral usage errors: More than four X in a row, suggestion: L? - roman numeral 5, char 5; More than four X in a row, suggestion: L? - roman numeral 6, char 6; Number of X before X must be at most two - roman numeral 7, char 7; Repeat after subtraction - XCC - roman numeral 8, char 8 [time]0.1086

todecimald

The function todecimald (direct) is only intended for use in other modules. The output will be in the form (comma-separated list): Decimal, Error message, Time taken

The decimal will be a number, it will be the converted number if there are no errors or if it is a warning (it is invalid but still can be converted), it will be -1 if there is an error (cannot be converted). The error message is a string and will always exist, it will be "" if no error is found.


-- Module to convert Roman numerals and reject invalid numerals

local p={}

local tags = {
    overline = '<span style="text-decoration:overline;">',
    doubleov = '<span style="border-top:double 3px">',
    
    rn =     '<span style="font-family:serif; font-size:118%;">',
    rnsize = '<span style="font-family:serif; font-size:122%;">',
    nrnsize = '<span style="font-size:114%;">',
    
    errs = '<span class="error">',
    sspan = '</span>',
    
    pipe = '&#124;',
}

local function atc(cn, rp)
    local s = " - ''"
    if rp ~= nil then s = s.."roman numeral "..tostring(rp)..", " end
    s = s.."char "..tostring(cn).."''; "
    return s
end

local function unesc( s )
    s = s:gsub("\\p", tags.pipe)
    s = s:gsub("\\\\", "\\")
    s = s:gsub("\\=", "\=")
    return s
end

local function disperr(err)
    return tags.errs..err..tags.sspan
end

local rn_ref = {I=1, V=2, X=3, L=4, C=5, D=6, M=7}
local ref_rn = {[1]="I", [2]="V", [3]="X", [4]="L", [5]="C", [6]="D", [7]="M"}

local function todec1 (rns, ovl, vbr, tcc)

    local err = ""

    local cex, cfr, crn, run, num = 0,0,0,0,0,0     -- prn, crn: previous, current roman numeral value
    local pex, pfr, prn = 0,0,0
        -- (current) cex: exponent (10^1, 10^2 etc.), cfr: fractional part, run: amount of character so far
    local rnc = "" -- roman numeral character

    for i = 1, #rns do             -- cex = 2, cfr =   0 or 0.5,
        cex, cfr = math.modf((rns[i]-1)/2)  -- crn = 100 or 500 etc.
        if cfr == 0 then crn = 10^cex else crn = 5*10^cex end
        
        tc = tcc[i]
        
        local function rncg(j, ia)
            if j == nil then j = 0 end
            rn, ov, vb = rns[i+j], ovl[i+j], vbr[i+j]
            rncr = rn - ov*6 - vb*4
            if ia == 1 then
                if rn == 13 and rncr == 7 then
                    rncr, ov, vb = 4, 1, 1
                elseif rncr == 7 then
                    rncr = 1
                    ov = ov + 1
                else rncr = rncr + 1
                end
            end
            rnc = ref_rn[rncr]
            local rnc_vb = ""
            if vb == 1 then rnc_vb = "&#124;" end
            rnc = rnc_vb .. rnc .. string.rep("̅", ov) .. rnc_vb
            return rnc
        end
        rnc = rncg()
        
        if crn < prn or prn == 0 then

            num = num + prn*run
            run = 1

        elseif crn == prn then
 
            if cfr == 0 then
                if run > 3 then                         -- e.g. "XXXXX" for 50, "L" suggested
                    err = err.."More than four "..rnc.." in a row, suggestion: "..rncg(0,1).."?"..atc(tc, i)
                    run = run + 1
                elseif run == 0 then                    -- e.g. occurs after crn > prn (below) e.g. "XCC"
                    err = err.."Repeat after subtraction - " .. rncg(-2) .. rncg(-1) .. rnc .. atc(tc, i)
                    run = 1 -- In "XCC", assume "XC" is a unit, so the current "C" is counted separately.
                else
                    run = run + 1
                end
            elseif cfr == 0.5 then                      -- e.g. "VV" for 10, "X" suggested
                err = err..rncg(-1).." cannot be with another "..rnc..", suggestion: "..rncg(0,1).."?"..atc(tc,i)
            else return -1, ("Unknown error 1") end
 
        elseif crn > prn then
 
            if crn > prn * 10 then                      -- e.g. "XM" or "IL"
                err = err..rnc.." cannot follow "..rncg(-1).." (Subtraction can only be within the same digit)"..atc(tc,i)
            elseif pfr == 0.5 then                      -- e.g. "LC" for 50
                err = err..rnc.." cannot follow "..rncg(-1).." (Cannot subtract from " .. tostring(prn) .. ")" ..atc(tc,i)
            elseif run > 2 then                         -- e.g. "XXXL" for 20
                err = err .. "Number of " .. rncg(-1) .. " before " .. rnc .. " must be at most two" .. atc(tc, i)
            end
            num = num - prn*run + crn
            run = 0
 
        else return -1, ("Unknown error 2") end
 
        prn = crn
        pex = cex
        pfr = cfr
    end
    num = num + prn*run
    
    if err ~= "" then err = err:sub(1, -3) end
    return num, err
end

local function todec( args )  -- pn: number of pipes (vertical bar) found so far,         p: in a vertical bar(X100)?
    local err, tag = "", ""   -- err: error message, rnseq: sequence of roman numerals,   tag: current html tag
    local rnseq, t = {},{}    -- tc: total character count so far, argn: argument number, cc: current character
    local ovl, vbr, tcc = {},{},{}         -- ovl,vbr,tcc: tc, status of overline and     t: table of html tags,
    local argn, tc, pn, ovc = 1, 0, 0, -1  -- vertical bar for each number in rnseq,      n: current character number
    local ov, dv, rn = 0,0,0  -- ov, dv, rn: number of overline, double overline, rn tags nested
    local p = false           -- gt, sc: position of greater than, semicolon character
    local ierr = ""           -- ierr: errors already in input (in error style span tag)
    local carg = args[argn]   -- atc(): produces " - Char 123; " for error messages  | defined at
    while carg ~= nil do      -- tags: table of html tags                            | the start
        if carg == "" then    -- ovc: position of overline character (U+0305) modified roman numeral
            pn = pn + 1       --      (or another overline character)
            if p then p = false else p = true end
        else
            local n = 0
            local cc = ""
            local cplen = mw.ustring.len    -- codepoint length
            while n < cplen(carg) do
                n = n + 1
                cc = mw.ustring.sub(carg, n, n)
                if cc == "<" then
                    local gt = mw.ustring.find(carg, ">", n, true)
                    if gt == nil then
                        tc = tc + 1
                        err=err.."Unbalanced '<' found"..atc(tc)
                    else tag = mw.ustring.sub(carg, n, gt)
                        local taglen = cplen(tag)
                        n = n + taglen - 1
                        if tag == tags.sspan then
                            ct = t[#t]      -- current t
                            if     ct == "ov" then ov = ov - 1
                            elseif ct == "rn" then rn = rn - 1
                            elseif ct == "dv" then dv = dv - 1
                            end
                            if #t == 0 then err=err.."Unbalanced \""..tags.sspan.."\" tag found"..atc(tc)
                            else t[#t] = nil end
                        elseif tag == tags.overline then
                            ov = ov + 1
                            t[#t + 1] = "ov"
                            if ov > 1 then err=err..ov.." nested overline tags found"..atc(tc) end
                        elseif tag == tags.doubleov then
                            dv = dv + 1
                            t[#t + 1] = "dv"
                            if dv > 1 then err=err..dv.." nested double overline tags found"..atc(tc) end
                        elseif tag == tags.rn or tag == tags.rnsize then
                            rn = rn + 1
                            t[#t + 1] = "rn"
                            if rn > 1 then err=err..rn.." nested rn tags found"..atc(tc) end
                        elseif tag == tags.nrnsize then -- Large font size span tag
                            t[#t + 1] = "sz"      -- for overlines to show properly
                        elseif tag == tags.errs then    -- close span tag start, end point
                            local csp, cep = mw.ustring.find(carg, tags.sspan, n, true)
                            ierr = ierr .. ", " .. mw.ustring.sub(carg, n+1, csp-1)
                            n = cep
                        else
                            err=err.."Unknown tag \""..tag.."\" found"..atc(tc)
                            t[#t + 1] = "uk"
                        end
                    end
                elseif cc == " " then tc = tc + 1       -- spaces
                elseif cc == "&" then
                    local sc = mw.ustring.find(carg, ";", n, true)
                    if sc == nil then
                        tc = tc + 1
                        err=err.."Extra character '&' found"..atc(tc)
                    else
                        tag = mw.ustring.sub(carg, n, sc)
                        tc = tc + cplen(tag)
                        n = n + cplen(tag) - 1
                        if tag == "&#124;" or tag == "&#x73;" then
                            pn = pn + 1
                            if p then p = false else p = true end
                        elseif tag == "&#773;" or tag == "&#x305;" then
                            if ovc+1 < tc then 
                                err=err.."Overline character is not over a roman numeral"..atc(tc)
                            end
                            rnseq[#rnseq] = rnseq[#rnseq] + 6
                            ovl[#rnseq] = ovl[#rnseq] + 1
                            ovc = tc
                        else err=err.."Unknown tag \""..tag.."\" found"..atc(tc)
                        end
                    end
                elseif cc == "̅" then
                    tc = tc + 1
                    if ovc+1 < tc then
                        err=err.."Overline character is not over a roman numeral"..atc(tc)
                    end
                    rnseq[#rnseq] = rnseq[#rnseq] + 6
                    ovl[#rnseq] = ovl[#rnseq] + 1
                    ovc = tc
                elseif cc == "|" then       -- Possible by calling from another module
                    pn = pn + 1
                    if p then p = false else p = true end
                else tc = tc + 1
                    ccu = cc:upper()
                    if rn_ref[ccu] == nil then
                        err=err.."Unknown character \""..cc.."\" found"..atc(tc)
                    else   -- vb: vertical bar modifier
                        local vb = 0
                        if p then vb = 1 end
                        rnseq[#rnseq + 1] = rn_ref[ccu] + ov*6 + dv*12 + vb*4
                        tcc[#rnseq], ovl[#rnseq], vbr[#rnseq] = tc, ov + dv*2, vb
                        ovc = tc    -- for error message purposes ^
                    end
                end
            end
        end
        argn = argn + 1
        carg = args[argn]
    end
    if argn == 0 then return -1, "Input is empty"
    elseif #rnseq == 0 then return -1, "No roman numerals found"
    else
        num, err1 = todec1(rnseq, ovl, vbr, tcc)
        if err  ~="" then err = "Syntax errors: "..mw.ustring.sub(err, 1, -3).."  " end
        if err1 ~="" then err=err.."Roman numeral usage errors: "..err1.."  " end
        if ierr ~="" then err=err.."Errors already in the input: "..mw.ustring.sub(ierr, 3).."  " end
        if err ~= "" then err = mw.ustring.sub(err, 1, -3) end
        return num, err
    end
end

function p.todecimal( frame )
    local fargs = frame.args
    if fargs.d == "0" then
        pframe = frame:getParent()
        args = pframe.args
    else
        args = fargs
    end
    mode = fargs.mode or "0"
    disp = fargs.disp or "0"
    
    local num, err = todec(args)
    
    if mode == "0" then       -- Normal mode
        if num == nil then return disperr("Unknown error 4") end
        if err == "" then
            if num ~= -1 then return num
            else return disperr("Unknown error 3") end
        else
            if num == -1 then return disperr(err)
            else return num.." "..disperr(err) end
        end
    elseif mode == "1" then   -- Supress errors
        if num == nil then num = -2 end
        return num
    elseif mode == "2" then   -- Display all
        if disp == "0" or disp == "" then
            disp = "&#91;num&#93;\\n &#91;err&#93;\\e &#91;time&#93;\\t"
        end
        tim = os.clock()
        disp = unesc(disp)
        disp = disp:gsub("\\n", num)
        disp = disp:gsub("\\e", err)
        disp = disp:gsub("\\t", tim)
        return disp
    else return disperr("Unknown mode")
    end
end

function p.todecimald( roman )
    num, err = todec{ roman }
    return num, err, os.clock()
end

--  Decimal to roman numeral  --

local function torom1 (dec1) -- For <5000 subunit
    local function torom2 (dec2, a, b, c)
        local rom3 = ""
        if     dec2=="1" then rom3 = a
        elseif dec2=="2" then rom3 = a..a
        elseif dec2=="3" then rom3 = a..a..a
        elseif dec2=="4" then rom3 = a..b
        elseif dec2=="5" then rom3 = b
        elseif dec2=="6" then rom3 = b..a
        elseif dec2=="7" then rom3 = b..a..a
        elseif dec2=="8" then rom3 = b..a..a..a
        elseif dec2=="9" then rom3 = a..c
        end
        return rom3
    end
    dec1 = tostring(dec1)
    local dec2 = string.rep("0",4-#dec1)..dec1
    local a = {[2]="C", [3]="X", [4]="I"}
    local b = {[2]="D", [3]="L", [4]="V"}
    local c = {[2]="M", [3]="C", [4]="X"}
    local rom2 = { ""..string.rep("M", tonumber(dec2:sub(1,1)) ) }
    for i=2, 4 do
        rom2[i] = torom2(dec2:sub(i,i), a[i], b[i], c[i])
    end
    local rom1 = table.concat(rom2)
    return rom1
end

local function torom (dec, rndisp)
    
    local err, ierr = "", "" -- ierr: errors already in the input
    local rn, rc = "", ""
    if rndisp then
        rn = tags.rn
        rc = tags.sspan -- </span>
    end
    
    local floor = math.floor
    
    if type(dec) == "string" then   -- sp, ep: start point, end point
        errsp, errep = mw.ustring.find(dec, tags.errs, 1, true) -- error position (if present)
        while errsp do
            endsp, endep = mw.ustring.find(dec, tags.sspan, errep, true)
            if endsp then
                ierr = ierr .. ", " .. mw.ustring.sub(dec, errep+1, endsp-1)
                dec = mw.ustring.sub(dec, 1, errsp-1)..mw.ustring.sub(dec, endep+1)
            end
            errsp, errep = mw.ustring.find(dec, tags.errs, 1, true)
        end
        local ton = tonumber(dec)
        if ton == nil then
            err = err .. "Not a number; "
            dect = dec:gsub("[^%d]", "")
            if dect=="" then return -1, "No digits"
            else
                err=err.."Extra characters '"..dec:gsub("%d","").."' found; "
                dec = dec:gsub("[^%d.]", "")
                dec = tonumber(dec)
            end
        else dec = ton
        end
    elseif type(dec) ~= "number" then
        local ton = tonumber(dec)
        if ton == nil then return -1, "Not a number or string" end
    end
    
    if dec < 1 then return -1, "Input ("..dec..") is less than 1"
    else
        local dec, frp = math.modf(dec)     -- frp: fractional part
        if frp ~= 0 then 
            err=err.."Input has fractional part "..frp..", ignoring...; "
        end
        local romt = {}
        local rdec = dec                    -- rdec: remaining dec
        local od = tags.doubleov
        local ov = tags.overline
        local cl = tags.sspan -- close
        local vb = tags.pipe  -- vertical bar
        if dec >= 5e9 then
            err = err .. "Input is 5,000,000,000 (5e9) or greater; "
            local ov = floor( math.log10(dec/5)/3 )
            local cdec = 0                  -- ov: number of overlines
            for i = ov, 3, -1 do
                cdec = floor(rdec/10^(i*3))
                rdec = rdec - cdec*10^(i*3)
                local romt2 = torom1(cdec)
                local romt1 = {}
                for j=1, #romt2 do romt1[j] = romt2:sub(j, j) end
                romt1[#romt1+1] = ""
                if rndisp then size = tags.rnsize else size = tags.nrnsize end
                romt[#romt+1] = size..table.concat(romt1, string.rep("̅",i))..cl
            end
        end
        if dec >= 5e8 then
            cdec = floor( rdec /1e6)
            rdec = rdec - cdec*1e6
            romt[#romt+1] = od..rn..torom1(cdec)..rc..cl
        end
        if dec >= 5e6 then
            cdec = floor( rdec /1e5)
            rdec = rdec - cdec*1e5
            romt[#romt+1] = vb..ov..rn..torom1(cdec)..rc..cl..vb
        end
        if dec >= 5e3 then
            cdec = floor( rdec /1e3)
            rdec = rdec - cdec*1e3
            romt[#romt+1] = ov..rn..torom1(cdec)..rc..cl
        end
        cdec = rdec
        romt[#romt+1] = rn..torom1(cdec)..rc
        rom = table.concat(romt, " ")
    end
    if err ~= "" then err = mw.ustring.sub(err, 1, -3).."  " end
    if ierr ~="" then err = err.."Errors already in the input: "..mw.ustring.sub(ierr, 3).."  " end
    if err ~= "" then err = mw.ustring.sub(err, 1, -3) end
    return rom, err
end

function p.fromdecimal( frame )
    fargs = frame.args
    if fargs.d == "0" then
        pframe = frame:getParent()
        args = pframe.args
    else
        args = fargs
    end
    mode = fargs.mode or "0"
    disp = fargs.disp or "0"
    local rn
    if fargs.rn == "1" then rn = true else rn = false end
    
    local rom, err = torom(args[1], rn)
    
    if mode == "0" then       -- Normal mode
        if rom == nil then return disperr("Unknown error 6") end
        if err == "" then
            if rom ~= -1 then return rom
            else return disperr("Unknown error 5") end
        else
            if rom == -1 then return disperr(err)
            else return rom.." "..disperr(err) end
        end
    elseif mode == "1" then   -- No error mode
        if rom == nil then rom = -2 end
        return rom
    elseif mode == "2" then   -- Display all
        if disp == "0" or disp == "" then
            disp = "&#91;rom&#93;\\r &#91;err&#93;\\e &#91;time&#93;\\t"
        end
        tim = os.clock()
        disp:unesc()
        disp:gsub("\\r", rom)
        disp:gsub("\\e", err)
        disp:gsub("\\t", tim)
        return disp
    else return disperr("Unknown mode")
    end
end

function p.fromdecimald( dec, rn )
    if rn == "1" then rndisp = true else rndisp = false end
    rom, err = torom(dec, rndisp)
    return rom, err, os.clock()
end

return p