Module:Utils

From Eco - English Wiki
Revision as of 22:31, 2 March 2022 by Demian (talk | contribs) (Undo revision 8382 by Demian (talk) Broke the function.)

This module provides utility functions used from other modules.

Usage

Add the following line of code at the top of your file.

local Utils = require("Module:Utils")

-- You may then call functions from this module in your script. For example:
local tableLength = Utils.tableLen(myTable)

local p = {}

--- Trims and parses the args into a table, then returns the table
-- @author User:Avaren
function p.normaliseArgs(frame)
	local origArgs = frame:getParent().args
	local args = {}

	for k, v in pairs(origArgs) do
		v = mw.text.trim(tostring(v))
		if v ~= '' then
			args[k] = v
		end
	end

	return args
end

--- Get path to icon file.
-- @author User:Avaren
function p.checkImage(name, too_expensive)
	local icon = name:gsub('%s+', '') .. '_Icon.png'
	if too_expensive then
		return icon
	end

	if mw.title.makeTitle('File', icon).file.exists then
		return icon
	else
		return 'NoImage.png'
	end
end

--- Check if <code>item</code> is in given <code>array</code>.
-- @param item Item to look for
-- @param #table array Table to check
-- @return #bool <code>true</code> if <code>item</code> is in <code>array</code>
-- @author User:Avaren
local function in_array(item, array)
	-- Should only use on short arrays
	local set = {}
	for _, l in ipairs(array) do
		set[l] = true
	end
	return set[item] ~= nil
end

--- Build HTML code for an icon image.
-- @param name string
-- @param size string|nil One of: <code>"iconNormal"</code> (64px) or <code>"iconRecipe"</code> (44px). Default: <code>"iconNormal"</code>
-- @param bg string|nil
-- @param border string|nil
-- @param too_expensive boolean|nil
-- @author User:Avaren
function p.build_icon(name, link, size, bg, border, too_expensive)
	local L = require('Module:Localization') -- local import

	if not size then
		size = 'iconNormal'
	end
	local icon_bg
	if bg then
		icon_bg = bg
	end
	local icon_border
	if border then
		icon_border = border
	end

	local item_data = mw.loadData('Module:ItemData')
	local item = item_data.items[name]
	local image
	if item then
		if item['group'] == L.t('Skill Books') then
			image = 'SkillBook.png'
			icon_bg = 'iconGold'
		elseif item['group'] == L.t('Skill Scrolls') then
			image = 'Skill Scroll'
			icon_bg = 'iconGold'
			-- Attempt to generate skill page
		elseif in_array(L.t('Basic Research'), item['tagGroups']) then
			image = string.sub(item['untranslated'], 1, -7):gsub('%s+', '') .. '_Icon.png'
			icon_bg = 'paperBasic'
		elseif in_array(L.t('Advanced Research'), item['tagGroups']) then
			image = string.sub(item['untranslated'], 1, -10):gsub('%s+', '') .. '_Icon.png'
			icon_bg = 'paperAdvanced'
		elseif in_array(L.t('Modern Research'), item['tagGroups']) then
			image = string.sub(item['untranslated'], 1, -8):gsub('%s+', '') .. '_Icon.png'
			icon_bg = 'paperModern'
		else
			image = p.checkImage(item['untranslated'], too_expensive)
		end
		if not icon_bg then
			if item['group'] == L.t('Food') then
				icon_bg = 'iconGreen'
			elseif item['carried'] == L.t('Hands') then
				icon_bg = 'iconBrown'
			end
		end
	else
		image = p.checkImage(name, too_expensive)
	end

	if not icon_bg then
		icon_bg = 'iconBlue'
	end

	if border then
		icon_border = border
	else
		icon_border = 'borderBlue'
	end

	if size == 'iconNormal' then
		icon_container = 'iconContainer'
	else
		icon_container = 'iconContainerSmall'
	end

	if not link then
		link = ''
	else
		link = '|link='..link
	end

	local file = '[[File:' .. image .. '|frameless|class=' .. size .. ' ' .. icon_bg .. link ..']]'
	return '<div class="' .. icon_container .. '"><div class="iconStack">' .. file .. '</div><div class="iconBorder ' .. icon_border .. '" style="position:absolute;"></div></div>'
end

--- Get HTML code for an icon image.
-- @author User:Avaren
function p.Icon(frame)
	args = p.normaliseArgs(frame)
	return p.build_icon(args.name, args.link, args.size, args.bg, args.border, args.too_expensive)
end

--- Calculate the length of a table by iterating over every item in it.
--
-- <code>mw.LoadData</code> prevents <code>#tbl</code> from working correctly.
-- @param #table tbl Table to calculate the length of
-- @return #number Length of the table.
-- @author User:Avaren
function p.tableLen(tbl)
	local count = 0
	for _, v in ipairs(tbl) do
		if v == nil then
			return count
		end
		count = count + 1
	end
	return count
end

--- Check if <code>value</code> is not <code>nil</code> and return it or if it is <code>nil</code> fall back to <code>default</code>.
-- @param value Value to check
-- @param default Value to fall back to
-- @return <code>value</code> if it is not <code>nil</code>
-- @return <code>default</code> if <code>value</code> is <code>nil</code>
-- @author User:Demian
-- @see valueOrDash
-- @see formatNilToYesNo
-- @see formatBoolToYesNo
function p.valueOrDefault(value, default)
	return nil == value and default or value
end

--- Check if <code>value</code> is not <code>nil</code> and return it or if it is <code>nil</code> fall back to the em-dash (—).
--
-- The em-dash (—) is commonly used represent a missing, not applicable (N/A), or a negative ("no") value with just a single character.
-- @param value Value to check
-- @return <code>value</code> if it is not <code>nil</code>
-- @return #string "—" if <code>value</code> is <code>nil</code>
-- @author User:Demian
-- @see valueOrDefault
function p.valueOrDash(value)
	return nil == value and "—" or value
end

--- Check if <code>value</code> is not <code>nil</code> and return "Yes" or "No".
-- @param value Value to check
-- @return #string "Yes" if <code>value</code> is not <code>nil</code>
-- @return #string "No" if <code>value</code> is <code>nil</code>
-- @author User:Demian
-- @see valueOrDefault
-- @usage formatNilToYesNo("Hello") == "Yes"
-- @usage formatNilToYesNo(nil) == "No"
function p.formatNilToYesNo(value)
	-- TODO: Support i18n.
	return nil == value and "No" or "Yes"
end

--- Check if <code>value</code> <em>evaluates</em> as <code>true</code> and return "Yes" or "No".
-- @param value Value to evaluate. Does not have to be a bool.
-- @return #string "Yes" if <code>value</code> evaluates as <code>true</code>
-- @return #string "No" if <code>value</code> evaluates as <code>false</code>
-- @author User:Demian
-- @see valueOrDefault
-- @usage formatBoolToYesNo("") == true
-- @usage formatBoolToYesNo(123) == true
-- @usage formatBoolToYesNo(nil) == false
function p.formatBoolToYesNo(value)
	-- TODO: Support i18n.
	return value and "Yes" or "No"
end

--- Format the input values into a string representing the range between the values.
--
-- Returning an an empty string intended to ease concatenation with other strings.
-- The en-dash (–) (instead of the hyphen-minus "-") is the appropriate character to signify a range of values.
-- @param #number min Minimum value (left side)
-- @param #number max Maximum value (right side)
-- @param #number default Default value in case of an error (only value).
-- @param #string valueFormat Format string used with <code>mw.ustring.format</code>.
-- @return #string "<code>min</code>–<code>max</code>" if <code>min < max</code>
-- @return #string "<code>default</code>" formatted with <code>valueFormat</code> if <code>min == max</code> or <code>min > max</code> and <code>default ~= nil</code>
-- @return #string "" (empty string) if either <code>min</code> <strong>or</strong> <code>max</code> do not convert to a numerical value
-- @return #nil <code>nil</code> if <code>min == max</code> or <code>min > max</code> and <code>default == nil</code>
-- @author User:Demian
function p.toRangeString(min, max, default, valueFormat)
	min = tonumber(p.valueOrDefault(min, nil))
	max = tonumber(p.valueOrDefault(max, nil))
	default = tonumber(p.valueOrDefault(default, nil))

	if nil ~= min and nil ~= max then
		if min < max then
			return mw.ustring.format(mw.ustring.format("%s–%s", valueFormat, valueFormat), min, max)
		elseif nil == default then
			return nil
		else
			return mw.ustring.format(valueFormat, default)
		end
	end

	return ""
end

--- Get all keys from <code>tbl</code> and sort them in alphabetical order.
-- @param #table tbl Table to get keys from
-- @return #table Input table keys in alphabetical order.
-- @author User:Demian
-- @see getSortedValues
function p.getSortedKeys(tbl)
	local sorted = {}

	for key in pairs(tbl) do
		table.insert(sorted, key)
	end

	table.sort(sorted)

	return sorted
end

--- Get all values from <code>tbl</code> and sort them in alphabetical order.
-- @param #table tbl Table to get values from
-- @return #table Input table values in alphabetical order.
-- @author User:Demian
-- @see getSortedKeys
function p.getSortedValues(tbl)
	local sorted = {}

	for _, value in ipairs(tbl) do
		table.insert(sorted, value)
	end

	table.sort(sorted)

	return sorted
end

--- Split <code>str</code> by the given character.
-- @param #string str String to split
-- @param #string separator String that separates values in <code>str</code>. May optionally be surrounded by 1 <em>whitespace</em> character by default.
-- @return #table Table of strings that were split from <code>str</code>.
-- @author User:Demian
-- @usage splitString("hello, world", ",") == {"hello", "world"}
function p.splitString(str, separator)
	local tbl = {}

	for token in mw.ustring.gmatch(str, mw.ustring.format("%%s?([^%s]+)%%s?", separator)) do
		table.insert(tbl, token)
	end

	return tbl
end

--- Sort items in the given list of values <code>str</code> separated with <code>separator</code> and return them as a single string.
-- @param #string str String with values separated by <code>separator</code>
-- @param #string separator String that separates values in <code>str</code>
-- @param #string joiner String used to join sorted values from <code>str</code>.
-- @return #string <code>str</code> with items sorted in alphabetical order.
-- @author User:Demian
-- @usage sortListString("Dog,Ape, Cat", ",", ";") == "Ape;Cat;Dog"
function p.sortListString(str, separator, joiner)
	-- Split string by commas.
	-- Sort items.
	-- Rejoin into string.
	return table.concat(p.getSortedValues(p.splitString(str, separator)), joiner)
end

--- Check if a page with the title "<code>name</code> (<code>disambiguationTitle</code>)" exists in the database and return that page title, otherwise return "<code>name</code>".
--
-- Use sparingly as this uses a comparatively slow MediaWiki function p.to check if a page exists.
--
-- Using this function p.will create a new entry in the <code>Special:WantedPages</code> list.
-- Be careful when calling this function p.and do not pass garbage into its parameters so you do not clog up that list.
-- This is a long-standing issue with MediaWiki that has not yet been solved, and may not be possible to solve without an architectural change to the software.
-- @param #string name Name of a page.
-- @param #string disambiguationTitle Disambiguation clarifier in a page title.
-- @return #string "<code>name</code> (<code>disambiguationTitle</code>)"
-- @return #string "<code>name</code>"
-- @author User:Demian
function p.getDirectPageName(name, disambiguationTitle)
	-- Try to get the actual end page instead of the disambiguation page if it exists.
	-- E.g. Salmon has "Salmon (animal)" and "Salmon (item)" as well as the "Salmon" disambiguation page between these two.
	local directPage = mw.ustring.format("%s (%s)", name, disambiguationTitle)
	return mw.title.new(directPage).exists and directPage or name
end

--- Create a wikilink with [[square brackets]] from parameters.
-- @param #string pageName The actual name of a page to create a link to
-- @param #string displayText Text to display as a clickable link instead of the page name. If <code>nil</code>, <code>pageName</code> is displayed instead.
-- @param #bool twoLineDisplayText Force the <em>last word</em> of <code>displayText</code> on the next line
-- @return #string "[[<code>name</code>|<code>displayText</code>]]" if <code>displayText</code> is not <code>nil</code>
-- @return #string "[[<code>name</code>]]" if <code>displayText</code> is <code>nil</code> or the same string as <code>name</code>
-- @author User:Demian
function p.formatWikilink(pageName, displayText, twoLineDisplayText)
	local finalDisplayText = p.valueOrDefault(displayText, pageName)

	if twoLineDisplayText then
		local lastSpaceIdx = mw.ustring.find(finalDisplayText, " [^ ]*$")

		if nil ~= lastSpaceIdx then
			finalDisplayText = mw.ustring.format("%s<br>%s", mw.ustring.sub(finalDisplayText, 0, lastSpaceIdx-1), mw.ustring.sub(finalDisplayText, lastSpaceIdx+1))
		end
	end

	if pageName == finalDisplayText then
		return mw.ustring.format("[[%s]]", pageName)
	else
		return mw.ustring.format("[[%s|%s]]", pageName, finalDisplayText)
	end
end

--- Add thousands separator to given number and use custom decimal point.
--
-- Extension:NumberFormat is more extensive, but more cumbersome to use and also not installed at the moment.
-- @param #string number Number to format. Is processed as a string regardless of type.
-- @param #string thousandsSeparator String to place between each set of 3 digits. Default: " "
-- @param #string decimalPoint String to place between the whole and fractional part of the number. Default: "."
-- @return #string <code>number</code> with the specified thousands separator and decimal point.
-- @return #string <code>number</code> unchanged if it contained 1 or more characters that are <strong>not</strong> a: digit, one of ".,-", a space.
-- @author User:Demian
-- @usage formatNumber(-1234567.89) = "-1 234 567.89"
-- @usage formatNumber("1234567,89", ".", "_") = "1,234,567_89"
function p.formatNumber(number, thousandsSeparator, decimalPoint)
	-- Default separator to space.
	if nil == thousandsSeparator then
		thousandsSeparator = " "
	end

	-- Default point to period.
	if nil == decimalPoint then
		decimalPoint = "."
	end

	-- We're dealing with formatting a string here.
	local numberString = tostring(number)

	-- Check if the input number is reasonable.
	-- Does NOT check for multiple instance of each character.
	-- E.g. Inputting something like 123-456.789 will lead to incorrect results.
	-- I can't handle every edge case: garbage in, garbage out.
	-- The user has to have some responsibility in inputting reasonable numbers.
	if mw.ustring.find(numberString, "[^%d%.%-, ]") then
		return number
	end

	-- Split input into parts.
	-- 1st group: MAY start with a "-".
	-- 2nd group: MUST contain 1 or more digits-
	-- 3rd group: MAY start with with one of ".,"
	-- 3rd group: MAY have 0 or more digits.
	local _, _, minus, digits, fraction = mw.ustring.find(tostring(numberString), "(-?)(%d+)([%.,]?%d*)")

	-- Reverse the string of digits.
	-- Append the thousands separator after (before when reversed again) each set of 3 digits.
	digits = mw.ustring.gsub(string.reverse(digits), "(%d%d%d)", mw.ustring.format("%%1%s", thousandsSeparator))

	-- Replace the existing decimal separator with the specified one.
	if "" ~= fraction then
		fraction = mw.ustring.format("%s%s",decimalPoint, mw.ustring.sub(fraction, 2))
	end

	-- Reverse the string of digits back to the original direction.
	-- If the string digits starts with the thousands separator, remove the separator.
	-- Add the optional minus in front and the optional fractional part at the back.
	-- Need to remember to escape the thousandsSeparator, it could be "." which would translate to "any character"!
	return mw.ustring.format("%s%s%s", minus, mw.ustring.gsub(string.reverse(digits), mw.ustring.format("^%%s", thousandsSeparator), ""), fraction)
end

--- Check if the string is empty or <code>nil</code>.
-- @param #string str String to check
-- @return #bool <code>true</code> if <code>str</code> is <code>nil</code> or an empty string ("").
-- @author User:Demian
function p.isEmpty(str)
	return nil == str or "" == str
end

return p