Module:Sandbox/Squc/Roman
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 {{#invoke:Sandbox/Squc/Roman|todecimal|MMXIII}}
, 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 {{#invoke:Sandbox/Squc/Roman|todecimal|MMXIII}}
.
To output errors directly into the text, use {{#invoke:Sandbox/Squc/Roman|todecimal|MMXIII}}
.
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 )
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.01708 | Decimal number: 50, Errors Roman numeral usage errors: More than four X in a row, suggestion: L? - roman numeral 5, char 5, Time 0.01782 | Decimal number: 190, Errors Roman numeral usage errors: Repeat after subtraction - XCC - roman numeral 3, char 3, Time 0.01814 | Decimal number: 100, Errors Syntax errors: Unknown character "A" found - char 1; Unknown character "B" found - char 2, Time 0.01844 |
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.01876
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.01904
- 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%;">',
errs = '<span class="error">',
sspan = '</span>',
pipe = '|',
}
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 == "|" or tag == "s" then
pn = pn + 1
if p then p = false else p = true end
elseif tag == "̅" or tag == "̅" 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 = "[num]\\n [err]\\e [time]\\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 = "[rom]\\r [err]\\e [time]\\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