Module:Tropical cyclone season effects

-- Used for tropical cyclone season articles.

local invocation = require('Module:Template invocation').invocation
local getArgs = require('Module:Arguments').getArgs
local yesno = require('Module:Yesno')
local dateperiod = require('Module:Date period')._main
local Date = require('Module:Date')._Date
local p = {}

function p.main(frame)
	local args = getArgs(frame, {
		trim = true,
		removeBlanks = false
	})

    return p._main(frame, args)
end

--- Used for decoding attributes (encoded by the child template automatically)
function unencode(encoded)
	return string.gsub(encoded, "'", "'")
end

-- Guesses the table year from the article title, or from arguments
function guessYear(frame, args)
	if args["year"] or args["start-year"] then
		return tonumber(args["start-year"] or args["year"]), 
			tonumber(args["end-year"])
	end
	
	local pageTitle = mw.title.getCurrentTitle().prefixedText
	
	-- Note: These are different dashes (dash, ndash, mdash, respectively).
	rangeB = mw.ustring.match(pageTitle, "^%d+[%-–—](%d+)")
	rangeA = mw.ustring.match(pageTitle, "^(%d+)")
	if rangeB ~= nil then
		return tonumber(rangeA), tonumber(
			-- "2021" if "21", "2021" if "2021"
			args["end-year"] or (string.len(rangeB) > 2 and
				rangeB or string.sub(rangeA, 1, 2) .. rangeB)
		)
	elseif rangeA ~= nil then
		return tonumber(rangeA), nil
	else
		return nil, nil
	end
end

-- Module:Convert does not expose a module-friendly conversion system.
-- For this reason, we'll need to do this messy hack that is extremely
-- inefficient.
function convert(frame, args)
	args["disp"] = "number"
	args["comma"] = "off"
	return tonumber(frame:expandTemplate{ title = "convert", args = args })
end

function p._main(frame, args)
	local basin = args["basin"] or args["Basin"]
	if not yesno(args["no-header"]) and basin == nil then
		mw.addWarning("Basin not provided. A link will not be displayed leading to category definitions")
	end
	
	local startYear, endYear = guessYear(frame, args)
	if startYear == nil then
		return error("Could not guess starting year. Supply with the ''year'' or ''startYear'' parameter")
	end
	if startYear == nil and endYear ~= nil then
		return error("End year specified but start year not specified")
	end
	
    local tableEntries = args[1] or ""
    tableEntries = unencode(tableEntries)
   
    local totalStorms = 0
    local strongestWinds = nil
    local tableWindsUnit = args["winds-unit"]  -- nullable
    local lowestPressure = nil
    local tablePressureUnit = args["pressure-unit"]  -- nullable
    local totalDamages = 0
    local totalDeaths = 0
    
    local earliestFormed = nil
    local latestDissipated = nil
    local latestPresent = false
    
    for entryJSON in mw.ustring.gmatch(tableEntries, "data%-tcse%-entry='(.-)'") do
    	entry = mw.text.jsonDecode(entryJSON)
    	totalStorms = totalStorms + 1
    	
    	if tableWindsUnit == nil then
    		tableWindsUnit = entry["winds-unit"] or "kn"
		end
    	if tablePressureUnit == nil then
    		tablePressureUnit = entry["pressure-unit"] or "hPa"
    	end
	
    	-- Convert to table units first
    	convertedWinds = tonumber(entry["winds"]) ~= nil and (
    		entry["winds-unit"] == tableWindsUnit
    			and entry["winds"]
    			or convert(
    				frame,
    				{ entry["winds"], entry["winds-unit"] or "kn", tableWindsUnit }
				)
			) or nil
		convertedPressure = tonumber(entry["pressure"]) ~= nil and (
			entry["pressure-unit"] == tablePressureUnit
				and entry["pressure"]
    			or convert(
    				frame,
    				{ entry["pressure"], entry["pressure-unit"] or "hPa", tablePressureUnit }
				)
			) or nil
		
		deaths = tonumber(mw.ustring.match(entry["deaths"] or "", "%d+"))
		damages = tonumber(mw.ustring.match(entry["damages"] or "", "%d+"))
		
		-- Compare
		if convertedWinds ~= nil and (
			strongestWinds == nil or convertedWinds > strongestWinds
		) then
			strongestWinds = convertedWinds
		end
		if convertedPressure ~= nil and (
			lowestPressure == nil or convertedPressure < lowestPressure
		) then
			lowestPressure = convertedPressure
		end
		if deaths ~= nil then
			totalDeaths = totalDeaths + deaths
		end
		if damages ~= nil then
			totalDamages = totalDamages + damages
		end
		if string.lower(entry["dissipated"]) == "present" then
			latestPresent = true
		end
		
		formed = Date(entry["formed"])
		dissipated = Date(entry["dissipated"])
		if earliestFormed == nil or (formed ~= nil and earliestFormed > formed) then
			earliestFormed = formed
		end
		if latestDissipated == nil or (dissipated ~= nil and latestDissipated < dissipated) then
			latestDissipated = dissipated
		end
    end
    
	-- Using expandTemplate for modularity.
	local tcHeader = frame:expandTemplate{
		title = "Tropical cyclone season effects (top)",
		args = {
			["no-sort"] = totalStorms == 0 and "yes" or nil,
			["no-header"] = args["no-header"],
			["basin"] = basin,
			["start-year"] = startYear,
			["end-year"] = endYear,
			["currency-link"] = args["currency-link"]
		}
	}

	-- Template parameters not yet standardized. Hence the usage of capitalized
	-- parameter names.
	-- Using expandTemplate for modularity.
    local tcFooter = frame:expandTemplate{
    	title = "Tropical cyclone season effects (bottom)",
    	args = {
			["TC's"] = totalStorms .. " system" .. (totalStorms == 1 and "" or "s"),
			["dates"] = totalStorms == 0 and "Season not started" or
				dateperiod(
					earliestFormed,
					(latestPresent or yesno(args["active"]))
					    and "Season ongoing" or latestDissipated:text(
					    	args["date-format"] or "mdy"
				    	),
				    true
				),
			["winds"] = (totalStorms == 0 or strongestWinds == nil) and "" or
				(
					tableWindsUnit == "kn" and (
						frame:expandTemplate{
							title = "convert",
							args = {
								strongestWinds, 
								tableWindsUnit,
								args["winds-target"] or "kph",
								round = "5",
								abbr = "on",
								disp = "out"
							}
						} .. (args["winds-target2"] ~= "none" and " (" .. frame:expandTemplate{
							title = "convert",
							args = {
								strongestWinds, 
								tableWindsUnit,
								args["winds-target2"] or "mph",
								round = "5",
								abbr = "on",
								disp = "out"
							}
						} .. ")" or "")
					) or frame:expandTemplate{
						title = "convert",
						args = {
							strongestWinds, 
							tableWindsUnit,
							args["winds-target"] or "",
							round = "5",
							abbr = "on"
						}
					}
				),
			["pres"] = (totalStorms == 0 or lowestPressure == nil) and "" or
				frame:expandTemplate{
					title = "convert",
					args = {
						lowestPressure, 
						tablePressureUnit,
						args["pressure-target"] or "inHg",
						comma = "off",
						sigfig = 4,
						abbr = "on"
					}
				},
			["damage"] = (totalStorms == 0 or totalDamages == 0) and "" or
				frame:expandTemplate{
					title = totalDamages == 0 and "nts" or "ntsp",
					args = { totalDamages, "", totalDamages ~= 0 and (args["currency-symbol"] or "$") }
				},
			["deaths"] = (totalStorms == 0 or totalDeaths == 0) and "" or 
				frame:expandTemplate{
					title = "nts",
					args = { totalDeaths }
				},
			["Refs"] = args["footer-refs"] or ""
		}
	}
	
	return tcHeader .. "\n" .. tableEntries .. "\n" .. tcFooter
end

return p