Jump to content

Module:ConvertIB

From The Republic Wiki
Revision as of 13:58, 28 September 2025 by Richard470l (talk | contribs) (1 revision imported: election and country infoboxes)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

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
Cookies help us deliver our services. By using our services, you agree to our use of cookies.