Module:Road data/util

From WikiMD's Wellness Encyclopedia

Collection of utility functions for use within Module:Road data modules. See the code for function usages. Exported functions are: addAll, arrayToString, convertLengths and err.


local util = {}

local insert = table.insert
local concat = table.concat
local format = mw.ustring.format

---
-- Add all entries in `arr` into `target`.
-- An error is raised if `overwrite` is not true
-- and any key in `arr` is already in `target`.
function util.addAll(target, arr, overwrite)
	if type(target) ~= "table" then
		error("target is not a table")
	end
	for key,value in pairs(arr) do
		if overwrite or target[key] == nil then
			target[key] = value
		else
			error("Duplicate key: " .. tostring(key))
		end
	end
end

local function comp(e1, e2)
	local t1 = type(e1)
	local t2 = type(e2)
	if t1 ~= t2 then return t1 < t2 end
	if t1 == "function" then
		error("Unexpected function type")
	end
	return e1 < e2
end

local arrayToStringAux
arrayToStringAux = function(arr, indent)
	if type(arr) ~= "table" then
		error("arr is not a table")
	end
	if type(indent) ~= "number" then
		error("indent is not a number")
	end
	local result = {}
	local keys = {}
	for key in pairs(arr) do insert(keys, key) end
	table.sort(keys, comp)
	for _,key in ipairs(keys) do
		local value = arr[key]
		local keyPrint
		if type(key) == "string" then
			keyPrint = format("\"%s\"", key)
		else
			keyPrint = tostring(key)
		end
		local valuePrint
		if type(value) == "table" then
			valuePrint = format("{\n%s\n%s}",
				arrayToStringAux(value, indent + 4),
				string.rep(" ", indent))
		elseif type(value) == "string" then
			valuePrint = format("\"%s\"", value)
		else
			valuePrint = tostring(value)
		end
		insert(result, format("%s[%s] = %s",
			string.rep(" ", indent),
			keyPrint,
			valuePrint))
	end
	return concat(result, ", \n")
end

--- Return a string representation of `arr`.
function util.arrayToString(arr, indent)
	return arrayToStringAux(arr, indent or 0)
end

local function convert(distance, multiplier, desiredPrec)
	if type(distance) ~= "string" then
		error("distance is not a string")
	end
	if type(multiplier) ~= "number" then
		error("multiplier is not a number")
	end
	-- Import math functions.
	local math = require "Module:Math"
	-- This function returns the precision of a given string representing a number.
	local precision = math._precision
	-- This function returns the order of magnitude of a given string representing a number.
	local order = math._order
	-- This function rounds a given number to the given number of digits.
	local round = math._precision_format

	local prec = desiredPrec or precision(distance)
	if not desiredPrec then
		local ord = order(distance)
		-- Adjust precision based on multiplier, as done in {{convert}}.
		prec = prec - order(multiplier / 0.2)
	end

	local converted = distance * multiplier
	local magnitude = order(converted)
	if prec <= -magnitude then
		-- Ensure the result has at least two significant digits.
		prec = -magnitude + 1
	end
	return round(converted, prec)
end

--[[-
Convert length specified in one unit (mi or km) to length in the other unit.
@param #map<#string, #string> lengths
	a map from unit to distance (as a string) in that unit;
	may contain entry `prec` indicating desired conversion precision
@param #string blank text to be used if length is unspecified
@return #table a table containing the conversion result:
	orig = source unit;
	comp = target unit;
	mi = length in miles;
	ft = converted length in feet;
	km = length in kilometers;
	m = converted length in meters;
	error = error message, if any
]]
function util.convertLengths(lengths, blank)
	-- Import math functions.
	local math = require "Module:Math"
	-- In Lua, storing functions locally results in more efficient execution.
	-- This function rounds a given number to the given number of digits.
	local round = math._precision_format
	-- This function returns the precision of a given string representing a number.
	local precision = math._precision

	local kmPerMile = 1.609344
	local ftPerMile = 5280
	-- The length in kilometers as passed to the function.
	local km = lengths.km
	-- The length in miles as passed to the function.
	local mi = lengths.mi
	-- Precision for the converted length.
	local prec = lengths.prec
	local errMsg = {}
	-- Sanitize inputs.
	local km_ = tonumber(km)
	if km and not km_ then
		insert(errMsg, util.err("km is not a number"))
	end
	local mi_ = tonumber(mi)
	if mi and not mi_ then
		insert(errMsg, util.err("mi is not a number"))
	end
	local prec_ = tonumber(prec)
	if prec and not prec_ then
		insert(errMsg, util.err("prec is not a number"))
	end
	prec = prec_

	local ft
	local m
	local orig = "mi"
	local comp = "km"
	if mi and km then
		insert(errMsg, util.err("Both mi and km are specified"))
	elseif mi then
		-- Length in miles was passed.
		if mi_ then
			-- If `mi` is indeed a number, compute and round the length in kilometers.
			km = convert(mi, kmPerMile, prec)
			m = convert(mi, kmPerMile * 1000, prec)
			-- format mi (insert separators as in 1,000)
			mi = round(mi_, precision(mi))
		else
			-- `mi` is not a number.
			km = blank
			m = blank
		end
	elseif km then
		-- Length in kilometers was passed.
		-- Swap units.
		orig, comp = comp, orig
		if km_ then
			-- If `km` is indeed a number, compute and round the length in miles.
			mi = convert(km, 1 / kmPerMile, prec)
			ft = convert(km, ftPerMile / kmPerMile, prec)
			-- format km (insert separators as in 1,000)
			km = round(km_, precision(km))
		else
			-- `km` is not a number.
			mi = blank
			ft = blank
		end
	else
		mi = blank
		ft = blank
		km = blank
		m = blank
	end
	local error = concat(errMsg)
	if error == "" then error = nil end
	return {mi = mi, ft = ft, km = km, m = m, orig = orig, comp = comp,
		error = error}
end

--- Generates wikitext error messages.
function util.err(msg)
	if msg == nil then
		error("Unspecified error message")
	end
	return format('<strong class="error">Error: %s</strong>', msg)
end

return util