Jump to content

Module:Sandbox/Squc/Roman

From Wikipedia, the free encyclopedia
This is an old revision of this page, as edited by Squc (talk | contribs) at 08:30, 31 August 2013. The present address (URL) is a permanent link to this revision, which may differ significantly from the current revision.

-- 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%;">',
    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 = "|" 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 = ""            -- err: error message, rnseq: sequence of roman numerals,   t: table of html tags,
    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 vertical bar for each number in rnseq,
    local argn,tc,pn,ov,ovc = 1,0,0,0,-1 -- ov, rn: number of overline, rn tags nested    n: current character number
    local p = false           -- gt, sc: position of greater than, semicolon character    tag: current html 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
            if p then p = false else p = true end
        else
            local n = 0
            local cc = ""
            while n < #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)
                        n = n + #tag - 1
                        if 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.rn 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.sspan then
                            if #t == 0 then err=err.."Unbalanced \""..tags.sspan.."\" tag found"..atc(tc) 
                            elseif t[#t] == "ov" then ov = ov - 1
                            elseif t[#t] == "rn" then rn = rn - 1
                            end
                            t[#t] = nil
                        else err=err.."Unknown tag \""..tag.."\" found"..atc(tc)
                        end
                    end
                elseif cc == " " then
                    tc = tc + 1
                    err=(err.."Space found"..atc(tc))
                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 + #tag
                        n = n + #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
                        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
                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 = 4 end
                        rnseq[#rnseq + 1] = rn_ref[ccu] + ov*6 + vb
                        tcc[#rnseq], ovl[#rnseq], vbr[#rnseq] = tc, ov, vb  -- for error message purposes
                        ovc = tc
                    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 = mw.ustring.sub(err, 1, -3)
            err = "Syntax errors: "..err
            if err1 ~= "" then err = err.."  " end
        end
        if err1 ~= "" then err = err.."Roman numeral usage errors: "..err1 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
            return num.." "..disperr(err)
        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 (deca1) -- For <5000 subunit
    local function torom2 (deca2, a, b, c)
        local rom3 = ""
        if     deca2=="1" then rom3 = a
        elseif deca2=="2" then rom3 = a..a
        elseif deca2=="3" then rom3 = a..a..a
        elseif deca2=="4" then rom3 = a..b
        elseif deca2=="5" then rom3 = b
        elseif deca2=="6" then rom3 = b..a
        elseif deca2=="7" then rom3 = b..a..a
        elseif deca2=="8" then rom3 = b..a..a..a
        elseif deca2=="9" then rom3 = b..c
        end
        return rom3
    end
    deca1 = tostring(deca1)
    local deca2 = string.rep("0",4-#deca1)..deca1
    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(deca2:sub(1,1)) ) }
    for i=2, #deca1 do
        rom2[i] = torom2(deca2:sub(i,i), a[i], b[i], c[i])
    end
    local rom1 = table.concat(rom2)
    return rom1
end

local function torom (dec)
    
    local err = ""
    
    local floor = math.floor
    local conc
    
    if type(dec) ~= "number" then
        local ton = tonumber(dec)
        if ton == nil then err = err .. "Not a number; "
        else dec = ton end
    elseif type(dec) == "string" then
        dect = dec:gsub("[^%d]", "")
        if dect=="" then return -1, "No digits"
        else
            err=err.."Extra characters '"..dec:gsub("%d","").."' found; "
            dec = tonumber(dect)
        end
    else return -1, "Not a number or string"
    end
    
    local frp = dec - floor(dec)               -- frp: fractional part
    if    dec == 0 then return -1, "Input is zero"
    elseif dec < 0 then return -1, "Input is negative"
    else
        if frp ~= 0 then 
            err=err.."Input has fractional part "..frp..", truncating...; "
        end
        local romt = {}
        local dec4, dec3, dec2, dec1 = 0,0,0,0
        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 or greater; "
            local ov = floor( math.log10(dec/5)/3 ) - 2
            local dec5 = floor(dec/1e10)        -- ov: number of overlines
            local cdec = dec5
            for i = ov, ov, -1 do
                cdec = floor(cdec/10^(i*3-3))
                local romt2 = torom1(cdec)
                local romt1 = {}
                for j=1, #romt2 do romt1[i] = romt2:sub(i, i) end
                romt1[#romt1+1] = ""
                romt[#romt+1] = table.concat(romt1, string.rep("̅", ovn))
            end
        end
        if dec >= 5e8 then
            dec4 = floor( (dec-dec5*1e10) /1e8)
            romt[#romt+1] = od..torom1(dec4)..cl
        end
        if dec >= 5e6 then
            dec3 = floor( (dec-dec4*1e9) /1e6)
            romt[#romt+1] = vb..ov..torom1(dec3)..cl..vb
        end
        if dec >= 5e3 then
            dec2 = floor( (dec-dec3*1e6) /1e3)
            romt[#romt+1] = ov..torom1(dec2)..cl
        end
        dec1 = dec - dec2*1e3
        romt[#romt+1] = torom1(dec1)
        rom = table.concat(romt, " ")
    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 rom, err = torom(args[1])
    
    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
            return rom.." "..disperr(err)
        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 )
    rom, err = torom{ dec }
    return rom, err, os.clock()
end

return p