Module:ConvertIB
Appearance
| This Lua module is used on approximately 762,000 pages, or roughly 1% of all pages. To avoid major disruption and server load, any changes should be tested in the module's /sandbox or /testcases subpages, or in your own module sandbox. The tested changes can be added to this page in a single edit. Consider discussing changes on the talk page before implementing them. |
A Lua module that wraps {{convert}}, designed for infoboxes. It implements:
- {{convinfobox}}
- {{Infobox settlement/areadisp}}
- {{Infobox settlement/lengthdisp}}
- {{Infobox settlement/densdisp}}
Usage
{{#invoke:ConvertIB|convert}}
- Like {{convinfobox}}, accepts alternating series of pairs of [blank|value], unit . When a unit has a non-blank value, it will get converted to all other units that do have blank values
- Accepts all named parameters that {{convert}} does
- Accepts groups of multiple units (e.g., "5 ft 6 in") that {{convert}} does
{{#invoke:ConvertIB|area}}
- Implements {{Infobox settlement/areadisp}}, automatically converting area units, using the output order specified by MOS:UNIT
{{#invoke:ConvertIB|length}}
- Implements {{Infobox settlement/lengthdisp}}, automatically converting length units, using the output order specified by MOS:UNIT
{{#invoke:ConvertIB|density}}
- Implements {{Infobox settlement/densdisp}}, parsing population and area, producting density in inhabitants per square km and square mile.
require('strict')
local p = {}
local getArgs = require('Module:Arguments').getArgs
-- Function to pull out values and units from numeric args
-- Returns:
-- values: list of numeric values, or "false" if no numeric argument is given
-- units: list of units (str)
-- value: if there is a last numeric value unpaired with a unit, it becomes the precision
-- anyValue: whether there is a non-false value in the values list
local function parseValuesUnits(args)
local values = {}
local units = {}
local indx = 1
local value = nil
local anyValue = false
-- loop through numeric arguments in pairs
while args[indx] or args[indx+1] do
value = args[indx]
anyValue = anyValue or value
-- if there is a unit, save in output lists
if args[indx+1] then
table.insert(values, value or false)
table.insert(units, args[indx+1])
value = nil
end
indx = indx+2
end
return values, units, value, anyValue
end
-- Function to identify multiple units and rewrite them as new input or output groups
-- Args:
-- values, units: numeric values and units, as lists with same length
-- Returns:
-- newValues, newUnits: same lists rewritten
local function parseMultiples(values, units)
local newValues = {}
local newUnits = {}
local i = 1
-- we will search for multiples with up to 4 entries (depending on length)
local maxMultiple = math.min(4,#units-1)
local valueFound = false -- flag to suppress second (and later) input values
--- Hack for handling "stone": check if only value supplied is "lb"
local onlyPounds = true
for i = 1, #units do
if values[i] and units[i] ~= 'lb' then
onlyPounds = false
break
end
end
local multiple = mw.loadData('Module:ConvertIB/data').multiple
-- sweep through units
while i <= #units do
-- determine index of last possible unit that could contain a multiple
local last_unit = math.min(i+maxMultiple-1,#units)
local multipleFound = false
-- try from longest multiple down to double multiple (prefer longest ones)
for j = last_unit, i+1, -1 do
local key = table.concat({unpack(units,i,j)}, '')
if multiple[key] then
-- we found a multiple unit
multipleFound = true
-- Hack for "stone": add either 'lb' or multiple unit string to output units
-- depending on whether 'lb' was the only unit string with a value
if mw.ustring.sub(key,1,2) == 'st' then
table.insert(newValues, false)
table.insert(newUnits, onlyPounds and key or 'lb')
end
-- if there are any value in the span of the multiple,
-- then the multiple is an input
-- assume all missing values after the first are zero
local firstValueFound = false
for k = i, j do
firstValueFound = not valueFound and (firstValueFound or values[k])
if firstValueFound then
table.insert(newValues, values[k] or 0)
table.insert(newUnits, units[k])
end
end
valueFound = valueFound or firstValueFound
-- if no values in the span of the multiple,
-- then the multiple is an output. Insert combined string as output unit
if not firstValueFound then
table.insert(newValues, false)
table.insert(newUnits, key)
end
i = j+1
break
end
end
--- If no multiple unit was found, insert value[i] and unit[i] into rewritten lists
if not multipleFound then
if valueFound then
table.insert(newValues, false) -- skip writing value if it is a duplicate
else
table.insert(newValues,values[i])
valueFound = values[i]
end
table.insert(newUnits, units[i])
i = i+1
end
end
return newValues, newUnits
end
-- Call {{convert}} with args
local function callConvert(args)
local frame = mw.getCurrentFrame()
return frame:expandTemplate{title='Convert', args=args}
end
-- Implement {{convinfobox}}
function p._convert(args)
-- find all values and units in numeric args (and the precision, if it exists)
local values, units, precision, anyValue = parseValuesUnits(args)
-- bail if no values at all
if not anyValue then
return nil
end
-- rewrite values and units if multiple units are found
values, units = parseMultiples(values, units)
-- sort input and outputs into different buckets
local input_values = {}
local input_units = {}
local output_units = {}
for i = 1, #units do
if values[i] then
table.insert(input_values, values[i])
table.insert(input_units, units[i])
else
table.insert(output_units, units[i])
end
end
-- bail if nothing to convert
if #input_values == 0 or #output_units == 0 then
return nil
end
-- assemble argument list to {{convert}}
local innerArgs = {}
-- First, pass all input unit(s)
for i, v in ipairs(input_values) do
table.insert(innerArgs,v)
table.insert(innerArgs,input_units[i])
end
-- Then the output unit(s) [concatenated as single argument]
table.insert(innerArgs,table.concat(output_units,"+"))
if precision then
table.insert(innerArgs,precision) -- last non-nil value contains precision
end
-- now handle all non-numeric arguments, passing to {{convert}}
innerArgs.abbr = 'on' -- abbr=on by default
for k, v in pairs(args) do
if not tonumber(k) then
innerArgs[k] = v
end
end
return callConvert(innerArgs)
end
local function impUnitPref(pref,country)
--- lower case all arguments
pref = pref and mw.ustring.lower(pref)
country = country and mw.ustring.lower(country)
--- determine imperial unit by going thru arguments in priority order
if pref and pref ~= 'dunam' then
local impPref = mw.loadData('Module:ConvertIB/data').impPref
return impPref[pref]
elseif country then
return mw.ustring.find(country,"united states",1,true)
or mw.ustring.find(country,"united kingdom",1,true)
end
return false
end
local largeUnits = {km2=true,mi2=true,sqmi=true,km=true,mi=true}
-- Implement {{Infobox settlement/areadisp}}
function p._area(args)
local pref = args['pref']
local country = args['name']
local impus = impUnitPref(pref, country)
local dunam = args['dunam'] or args['dunum']
local link = args['link']
local innerArgs = {}
innerArgs.abbr = 'on'
innerArgs.order = 'out'
for _, unit in ipairs({'km2','mi2','sqmi','ha','acre'}) do
if args[unit] then
table.insert(innerArgs,args[unit])
table.insert(innerArgs,unit)
if largeUnits[unit] then
table.insert(innerArgs,impus and 'sqmi km2' or 'km2 sqmi')
else
table.insert(innerArgs,impus and 'acre ha' or 'ha acre')
end
return callConvert(innerArgs)
end
end
if dunam then
table.insert(innerArgs,dunam)
table.insert(innerArgs,'dunam')
pref = pref and mw.ustring.lower(pref)
local order = pref == 'dunam' and 'dunam ' or ''
dunam = mw.getContentLanguage():parseFormattedNumber(dunam)
if impus then
order = order..(dunam and dunam < 2589 and 'acre ha' or 'sqmi km2')
else
order = order..(dunam and dunam < 1000 and 'ha acre' or 'km2 sqmi')
end
table.insert(innerArgs,order)
local yesNo = require('Module:Yesno')
if yesNo(link,true) and link ~= 'none' then
innerArgs.lk = 'in'
end
return callConvert(innerArgs)
end
return nil
end
-- Implement {{Infobox settlement/lengthdisp}}
function p._length(args)
local pref = args['pref']
if pref == 'dunam' then -- ignore dunam pref for this function
pref = nil
end
local country = args['name']
local impus = impUnitPref(pref, country)
local innerArgs = {}
innerArgs.abbr = 'on'
innerArgs.order = 'out'
for _, unit in ipairs({'km','mi','m','ft'}) do
if args[unit] then
table.insert(innerArgs,args[unit])
table.insert(innerArgs,unit)
if largeUnits[unit] then
table.insert(innerArgs,impus and 'mi km' or 'km mi')
else
table.insert(innerArgs,impus and 'ft m' or 'm ft')
end
return callConvert(innerArgs)
end
end
return nil
end
--Compute number of significant digits in a numeric string
local function computeSigFig(s)
local num_str = string.match(tostring(s), '^[+-]?[%d%.,]+%d*')
if not num_str then
return 0
end
-- Strip leading signs
num_str = string.gsub(num_str, '^[+-]', '')
-- Strip commas
num_str = string.gsub(num_str, ',', '')
-- Strip leading zeros
num_str = string.gsub(num_str, '^0*', '')
if num_str == '' then
return 0
end
-- If there's a decimal point, all trailing zeros are significant.
if string.find(num_str, '%.') then
return #string.gsub(num_str, '%.', '') -- Count all digits after removing decimal
end
-- If no decimal point, trailing zeros are not significant.
-- Count all digits up to the last non-zero one.
num_str = string.gsub(num_str, '0+$', '')
return #num_str
end
-- Implement {{Infobox settlement/densdisp}}
-- Returns table:
-- density = computed value if no error
-- error = error string if error.
-- These are only errors detected in this code, {{convert}} does its own error handling
function p._density(args)
local result = {}
local lang = mw.getContentLanguage()
local pref = args['pref']
if pref == 'dunam' then -- ignore dunam pref for this function
pref = nil
end
local country = args['name']
local per_km2 = args['/km2']
local per_mi2 = args['/mi2'] or args['/sqmi']
local impus = impUnitPref(pref, country, per_km2, per_mi2)
local per_km2_value = lang:parseFormattedNumber(per_km2)
local per_mi2_value = lang:parseFormattedNumber(per_mi2)
local innerArgs = {}
innerArgs.abbr = 'on'
if per_km2_value or per_mi2_value then
innerArgs.order = 'out'
if per_km2_value then
table.insert(innerArgs,per_km2_value)
table.insert(innerArgs,'/km2')
else
table.insert(innerArgs,per_mi2_value)
table.insert(innerArgs,'/sqmi')
end
table.insert(innerArgs,impus and '/sqmi /km2' or '/km2 /sqmi')
result.density = callConvert(innerArgs)
return result
end
if per_km2 ~= 'auto' and per_mi2 ~= 'auto' then
-- automatic computation not requested, fail silently
return result
end
if not args['pop'] then
-- fail silently if no population given
return result
end
local areaSigFig
local areaValue
local areaUnit
for _, unit in ipairs({'km2','mi2','sqmi','ha','acre','dunam','dunum'}) do
local value = lang:parseFormattedNumber(args[unit])
if value then
if value <= 0 then
result.error = unit.." value not positive"
return result
end
areaValue = value
areaUnit = unit
areaSigFig = computeSigFig(args[unit])
break
elseif args[unit] then
result.error = "Malformed "..unit.." value"
return result
end
end
if not areaSigFig then
-- fail silently if no area given
return result
end
if areaSigFig == 0 then
result.error = "Malformed area string"
return result
end
local popValue = lang:parseFormattedNumber(args['pop'])
if not popValue then
result.error = "Malformed population value"
return result
end
if popValue < 0 then
result.error = "Negative population value"
return result
end
table.insert(innerArgs,popValue/areaValue)
table.insert(innerArgs,'/'..areaUnit)
local popSigFig = computeSigFig(args['pop'])
local sigFig = popSigFig < areaSigFig and popSigFig or areaSigFig
if sigFig < 2 then
sigFig = 2
end
innerArgs.sigfig = sigFig
innerArgs.disp = 'out'
table.insert(innerArgs,'/km2')
local metric = callConvert(innerArgs)
innerArgs[3] = '/sqmi'
local imperial = callConvert(innerArgs)
if impus then
result.density = string.format("%s (%s)",imperial,metric)
else
result.density = string.format("%s (%s)",metric,imperial)
end
return result
end
function p.convert(frame)
local args = getArgs(frame)
return p._convert(args) or ""
end
function p.area(frame)
local args = getArgs(frame)
return p._area(args) or ""
end
function p.length(frame)
local args = getArgs(frame)
return p._length(args) or ""
end
function p.density(frame)
local args = getArgs(frame)
local result = p._density(args)
if result.density then
return result.density
end
if result.error then
local warning = require('Module:If_preview')._warning
local result = warning({result.error})
if mw.title.getCurrentTitle().namespace == 0 then
result = result..'[[Category:Pages using infobox settlement with bad density arguments]]'
end
return result
end
return ''
end
return p