| This Lua module is used on 5,000+ pages and changes may be widely noticed. Test changes in the module's /sandbox or /testcases subpages, or in your own Module:Roman. Consider discussing changes on the talk page before implementing them. |
This module implements the {{Roman}} template. For behavioral documentation, please see the template page. For test cases, please see Template:Roman/testcases.
- Module supports 0 as a Roman numeral, displays as 'N'.
- Prior to 25 April 2016, used to display 69105 as LXVMMMMCV. With the addition of IX and IV being 9000 and 4000 respectively, we now display 69105 as LXIXCV.
- Module handles decimal, fractional, and arithmetic expressions to a precision of 1/1728. Uses
#expr:
at line 118.
Handling tricky cases (like 0.00001 and 99.99999)
- Find the Roman numerals for the integer part of the number.
- If the number is not an integer:
- Add half of the smallest unit (1/1728) to simulate rounding instead of truncation.
- Ensure this new result is between 1/1728 and 1727/1728. (actually 1.1/1728 and 1727.1/1728 due to floating point rounding issues)
- Hence, 0.00001 is guaranteed to have at least the smallest unit symbol (instead of being blank or 0), and 99.99999 does not display as 100 or 99 and 2 halves.
Validation
-- This module implements {{Roman}}.
local p = {}
-- This function implements the {{overline}} template.
local function overline(s)
return mw.ustring.format( '<span style="text-decoration:overline;">%s</span>', s )
end
-- Gets the Roman numerals for a given numeral table. Returns both the string of
-- numerals and the value of the number after it is finished being processed.
local function getLetters(num, t)
local ret = {}
for _, v in ipairs(t) do
local val, letter = unpack(v)
while num >= val do
num = num - val
table.insert(ret, letter)
end
end
return table.concat(ret), num
end
-- The main control flow of the module.
local function _main(args)
-- Get input and exit displaying nothing if the input is empty.
if args[1] == nil then return end
local num = tonumber(args[1])
if not num or num < 0 or num == math.huge then
error('Invalid number ' .. args[1], 2)
elseif num == 0 then
return 'N'
end
-- Return a message for numbers too big to be expressed in Roman numerals.
if num >= 5000000 then
return args[2] or 'N/A'
end
local ret = ''
-- Find the Roman numerals for the large part of numbers.
-- 23 April 2016 - tweaked to >= 4000 to accept big Roman 'IV'
-- The if statement is not strictly necessary, but makes the algorithm
-- more efficient for smaller numbers.
if num >= 4000 then
local bigRomans = {
{ 1000000, 'M' },
{ 900000, 'CM' }, { 500000, 'D' }, { 400000, 'CD' }, { 100000, 'C' },
{ 90000, 'XC' }, { 50000, 'L' }, { 40000, 'XL' }, { 10000, 'X' },
{ 9000, 'IX' }, { 5000, 'V' }, { 4000, 'IV' },
}
local bigLetters
bigLetters, num = getLetters(num, bigRomans)
ret = overline(bigLetters)
end
-- Find the Roman numerals for numbers less than the big Roman threshold.
local smallRomans = {
{ 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' }
}
local smallLetters = getLetters( num, smallRomans )
ret = ret .. smallLetters
if args.fraction == 'yes' then
-- Find the Roman numerals for the fractional parts of numbers.
-- If num is not a whole number, add half of 1/1728 (the smallest unit) to equate to rounding.
-- Ensure we're not less than the smallest unit or larger than 1 - smallest unit
-- to avoid getting two "half" symbols or no symbols at all
num = num - math.floor(num)
if num ~= 0 then
num = math.max(1.1/1728, math.min(1727.1/1728, num + 1/3456))
end
local fractionalRomans = {
{ 1/2, 'S' }, { 5/12, "''':'''•''':'''" }, { 1/3, "'''::'''" },
{ 1/4, "''':'''•" }, { 1/6, "''':'''" }, { 1/12, '•' },
{ 1/24, 'Є' }, { 1/36, 'ƧƧ' }, { 1/48, 'Ɔ' }, { 1/72, 'Ƨ' }, { 1/144, '<s>Ƨ</s>' },
{ 1/288, '℈' }, { 1/1728, '»' },
}
local fractionalLetters = getLetters(num, fractionalRomans)
ret = ret .. fractionalLetters
end
return ret
end
function p.main(frame)
-- If called via #invoke, use the args passed into the invoking
-- template, or the args passed to #invoke if any exist. Otherwise
-- assume args are being passed directly in from the debug console
-- or from another Lua module.
local origArgs
if frame == mw.getCurrentFrame() then
origArgs = frame:getParent().args
for k, v in pairs(frame.args) do
origArgs = frame.args
break
end
else
origArgs = frame
end
-- Trim whitespace and remove blank arguments.
local args = {}
for k, v in pairs(origArgs) do
if type( v ) == 'string' then
v = mw.text.trim(v)
end
if v ~= '' then
args[k] = v
end
end
-- exit if not given anything
if args == nil or args == {} then return end
-- Given mathematical expression, simplify to a number
if type(args[1]) == 'string' then
args[1] = mw.ext.ParserFunctions.expr(args[1])
end
return _main(args)
end
return p