Module:ConvertIB
Appearance
Documentation for this module may be created at Module:ConvertIB/doc
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
-- Implement {{Infobox settlement/areadisp}}
function p._area(args)
local pref = args['pref']
local country = args['name']
local impus = impUnitPref(pref, country)
local km2 = args['km2']
local mi2 = args['mi2'] or args['sqmi']
local ha = args['ha']
local acre = args['acre']
local dunam = args['dunam'] or args['dunum']
local link = args['link']
local innerArgs = {}
innerArgs.abbr = 'on'
innerArgs.order = 'out'
if km2 then
table.insert(innerArgs,km2)
table.insert(innerArgs,'km2')
elseif mi2 then
table.insert(innerArgs,mi2)
table.insert(innerArgs,'sqmi')
end
if km2 or mi2 then
table.insert(innerArgs,impus and 'sqmi km2' or 'km2 sqmi')
return callConvert(innerArgs)
end
if ha then
table.insert(innerArgs,ha)
table.insert(innerArgs,'ha')
elseif acre then
table.insert(innerArgs,acre)
table.insert(innerArgs,'acre')
end
if ha or acre then
table.insert(innerArgs,impus and 'acre ha' or 'ha acre')
return callConvert(innerArgs)
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 km = args['km']
local mi = args['mi']
local m = args['m']
local ft = args['ft']
local innerArgs = {}
innerArgs.abbr = 'on'
innerArgs.order = 'out'
if km then
table.insert(innerArgs,km)
table.insert(innerArgs,'km')
elseif mi then
table.insert(innerArgs,mi)
table.insert(innerArgs,'mi')
end
if km or mi then
table.insert(innerArgs,impus and 'mi km' or 'km mi')
return callConvert(innerArgs)
end
if m then
table.insert(innerArgs,m)
table.insert(innerArgs,'m')
elseif ft then
table.insert(innerArgs,ft)
table.insert(innerArgs,'ft')
end
if m or ft then
table.insert(innerArgs,impus and 'ft m' or 'm ft')
return callConvert(innerArgs)
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