Jump to content

Module:Sandbox/Erutuon/Climate

From Wikipedia, the free encyclopedia
This is an old revision of this page, as edited by Erutuon (talk | contribs) at 23:50, 27 June 2018. The present address (URL) is a permanent link to this revision, which may differ significantly from the current revision.

local p = {}

local use_alt_CD_isotherm = false
local use_alt_hk_isotherm = false
local use_alt_w_isotherm = false

local map = require "Module:Utility".map

local function errorf(level, ...)
	if type(level) == number then
		return error(string.format(...), level + 1)
	else -- level is actually the format string.
		return error(string.format(level, ...), 2)
	end
end

local function fold(func, t, result)
	for _, v in ipairs(t) do
		result = func(result, v)
	end
	return result
end

local function count(func, t)
	local count = 0
	for _, v in ipairs(t) do
		if func(v) then
			count = count + 1
		end
	end
	return count
end

local function in_range(val, low, high)
	if low < high then -- |---i+++j---|
		return low <= val and val <= high
	else -- |+++j---i+++|
		return val < high or low < val
	end
end

local function mean_of_highs_and_lows(highs, lows)
	local high_count, low_count = #highs, #lows
	-- for now, no annual average accepted
	if not (high_count == 12 and low_count == 12) then
		errorf("Wrong number of highs or lows (%d, %d): expected 12 each",
			high_count, low_count)
	elseif high_count ~= low_count then
		errorf("Number of highs (%d) is not equal to number of lows (%d)",
			high_count, low_count)
	end
	
	local temperatures = {}
	
	for i = 1, high_count do
		if highs[i] <= lows[i] then
			errorf("High #%d (%d) is not greater than low #%d (%d)",
				i, highs[i], i, lows[i])
		end
		temperatures[i] = mean(highs[i], lows[i])
	end
	
	return temperatures
end

local function make_arr_and_varargs_func(varargs_func)
	local function arr_func (arr, i, j)
		i = i or 1
		j = j or #arr
		assert(i > 0 and j > 0)
		
		if i == j then
			return arr[i]
		elseif i < j then -- |---i+++j---|
			return varargs_func(unpack(arr, i, j))
		else -- |+++j---i+++|
			return varargs_func(varargs_func(unpack(arr, 1, j)),
				varargs_func(unpack(arr, i)))
		end
	end
	
	return function (...)
		if type(...) == 'table' then -- if first argument is table
			if select(2, ...) then -- if second argument
				return arr_func(...)
			else
				return varargs_func(unpack((...)))
			end
		else
			return varargs_func(...)
		end
	end
end

local function sum_varargs (...)
	local result = 0
	for i = 1, select('#', ...) do
		result = result + select(i, ...)
	end
	return result
end

local ops = {
	sum = make_arr_and_varargs_func(sum_varargs),
	mean = make_arr_and_varargs_func(
		function (...)
			return sum_varargs(...) / select('#', ...)
		end),
	min = make_arr_and_varargs_func(
		function (...)
			local min = math.huge
			local min_index
			for i = 1, select('#', ...) do
				local val = select(i, ...)
				if type(val) == "table" then
					val, i = val.value, val.index
				end
				if val < min then
					min = val
					min_index = i
				end
			end
			return { value = min, index = min_index }
		end),
	max = make_arr_and_varargs_func(
		function (...)
			local max = -math.huge
			local max_index
			for i = 1, select('#', ...) do
				local val = select(i, ...)
				if type(val) == "table" then
					val, i = val.value, val.index
				end
				if val > max then
					max = val
					max_index = i
				end
			end
			return { value = max, index = max_index }
		end),
}

local seasons = { summer = { south = { 10, 3 }, north = { 4, 9 } } }
seasons.winter = {}
seasons.winter.south = { seasons.summer.south[2] + 1, seasons.summer.south[1] - 1 }
seasons.winter.north = { seasons.summer.north[2] + 1, seasons.summer.north[1] - 1 }

local function construct(self, val, Southern_Hemisphere)
	val = val or {}
	local hemisphere = Southern_Hemisphere and "south" or "north"
	val.summer_months, val.winter_months =
		seasons.summer[hemisphere], seasons.winter[hemisphere]
	return setmetatable(val, self)
end

local stats_mt = {}
function stats_mt:__index(key)
	if type(key) ~= "string" then
		return nil
	end
	
	local season, op = key:match("^(%a-)_?(%a+)$")
	if not (season == "" or season == "summer" or season == "winter") then
		errorf("Unrecognized season %s", season)
	elseif not ops[op] then
		errorf("Unrecognized operation %s", op)
	end
	
	local result = ops[op](self)
	if season == "" then
		result = ops[op](self)
	else
		result = ops[op](self, unpack(self[season .. "_months"]))
	end
	self[key] = result
	return result
end

local temperatures_mt = {}
function temperatures_mt:__index(key)
	if key == "above_10" then
		local above_10 = fold(
			function (count, temperature)
				if temperature > 10 then
					return count + 1
				end
				return count
			end,
			self, 0)
		self.above_10 = above_10
		return above_10
	else
		return stats_mt.__index(self, key)
	end
end

local function get_aridity_threshold(mean_temp, total_precip, total_summer_precip)
	local summer_precip_fraction = total_summer_precip / total_precip
	return mean_temp * 20
		+ (summer_precip_fraction >= 0.7 and 280
		or summer_precip_fraction >= 0.3 and 140
		or 0)
end

local function get_first_letter(min_month_temp, max_month_temp)
	return max_month_temp <  10 and "E"
		or min_month_temp <  (use_alt_CD_isotherm and -3 or 0) and "D"
		or min_month_temp >= 18 and "A"
		or "C"
end

-- in C:      cond ? a : b
-- in Python: a if cond else b
local function ternary(cond, a, b)
	if cond then
		return a
	else
		return b
	end
end

-- Temperatures and precipitation are tables of mean monthly temperature and
-- precipitation. Or temperatures can be a table containing a table of monthly
-- mean of daily highs and monthly mean of daily lows.
-- Units: °C, mm.
function p.Koeppen(temperatures, precipitation, Southern_Hemisphere, location)
	local temperature_count = #temperatures
	if temperature_count == 2 then
		local highs_and_lows = temperatures
		temperatures = mean_of_highs_and_lows(unpack(temperatures))
	elseif temperature_count ~=  12 then
		errorf("Wrong number of temperatures (expected 12, got %d)",
			temperature_count)
	elseif #precipitation ~= 12 then
		errorf("Wrong number of precipitation stats (expected 12, got %d)",
			#precipitation)
	end
	
	temperatures = construct(temperatures_mt, temperatures, Southern_Hemisphere)
	precipitation = construct(stats_mt, precipitation, Southern_Hemisphere)
	
	local aridity_threshold =
		get_aridity_threshold(temperatures.mean, precipitation.sum, precipitation.summer_sum)
	
	if precipitation.sum <= aridity_threshold then
		return "B"
			 .. (precipitation.sum < aridity_threshold / 2 and "W" or "S") -- arid, semi-arid
			 .. (ternary(use_alt_hk_isotherm, temperatures.mean < 18,
			 	temperatures.min.value < 0)
			 	and "k" or "h")
	end
	
	local first_letter = get_first_letter(temperatures.min.value, temperatures.max.value)
	
	if first_letter == "E" then
		return first_letter .. (temperatures.max.value < 0 and "F" or "T")
	elseif first_letter == "A" then
		return first_letter
			.. (precipitation.min.value >= 60 and "f"
			or  precipitation.min.value / precipitation.sum > 0.04 and "m"
			or  in_range(precipitation.min.index, unpack(precipitation.summer_months))
				and "s"
			or  "w")
	else
		local second_letter =
			ternary(use_alt_w_isotherm, precipitation.sum / precipitation.summer_sum >= 0.7,
				precipitation.summer_max.value > precipitation.winter_min.value * 10)
				and "w"
			or precipitation.summer_min.value < 30
				and precipitation.winter_max.value > precipitation.summer_min.value * 3
				and "s"
			or "f"
		
		local third_letter
		if temperatures.above_10 <= 3 then
			if temperatures.min.value < -38 then
				third_letter = "d"
			else
				third_letter = "c"
			end
		elseif temperatures.max.value < 22 then
			third_letter = "b"
		else
			third_letter = "a"
		end
		
		return first_letter .. second_letter .. third_letter
	end
end

local function gather_numbers(str)
	local arr = {}
	local i = 0
	for number in str:gmatch('%-?%d+%.?%d*') do
		i = i + 1
		arr[i] = tonumber(number)
	end
	return arr
end

function p.example(frame)
	local args = frame.args
	local temperature_string = args[1]
	local precipitation_string = args[2]
	
	local temperatures, precipitation =
		gather_numbers(temperature_string), gather_numbers(precipitation_string)
	local yesno = require 'Module:yesno'
	local Southern_Hemisphere = yesno(args[3])
	if yesno(args.alt_CD) then
		use_alt_CD_isotherm = true
	end
	if yesno(args.alt_hk) then
		use_alt_hk_isotherm = true
	end
	if yesno(args.alt_w) then
		use_alt_w_isotherm = true
	end
	
	local location = args.location
	local result = p.Koeppen(temperatures, precipitation, Southern_Hemisphere, location)
	-- mw.logObject{ temperatures = temperatures, precipitation = precipitation }
	
	return ("[%s %s]: %s"):format(args.url, location, result)
end

local function convert(values, to, from)
	if to == from then
		errorf("Cannot convert from %s to %s", from, to)
	elseif to == "C" and from == "F" then
		return map(function(value) return (value - 32) * 5/9 end, values)
	elseif to == "F" and from == "C" then
		return map(function(value) return (value * 9/5) + 32 end, values)
	elseif to == "mm" then
		if from == "inch" then
			return map(function(value) return value * 254 end, values)
		elseif from == "cm" then
			return map(function(value) return value * 10 end, values)
		end
	end
	errorf("Conversion from %s to %s not implemented", from, to)
end

local month_to_number = require "Module:Table".invert {
	"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
}
setmetatable(month_to_number, {
	__index = function (self, key)
		errorf("Month %s not recognized", key)
	end
})

local precipitation_units = require "Module:Table".listToSet {
	"inch", "cm", "mm"
}

function p.Weather_box_Koeppen(frame)
	local temperatures, precipitation = {}, {}
	local args = frame:getParent().args
	for k, v in pairs(args) do
		local month, unit = arg:match("(%u%l%l) mean ([CF])")
		if month then
			if temperatures.unit and temperatures.unit ~= unit then
				error("Mean monthly temperature given in two different units, "
						.. "%s and %s", temperatures.unit, unit)
			end
			temperatures.unit = unit
			temperatures[month_to_number[month]] = tonumber(v)
				or errorf("Value of parameter '|%s=%s' cannot be parsed as a number",
					k, v)
		else
			month, unit = arg:match("(%u%l%l) precipitation (%l+)")
			if month and precipitation_units[unit] then
				if precipitation.unit and precipitation.unit ~= unit then
					errorf("Mean monthly precipitation given in two different units, "
						.. "%s and %s", precipitation.unit, unit)
				end
				precipitation[month_to_number[month]]
					= tonumber(v)
					or errorf("Value of parameter '|%s=%s' cannot be parsed as a number",
						k, v)
			end
		end
	end
	
	if temperatures.unit ~= "C" then
		temperatures = convert(temperatures, "C", temperatures.unit)
	end
	if precipitation.unit ~= "mm" then
		precipitation = convert(precipitation, "mm", precipitation.unit)
	end
	
	local Southern_Hemisphere = require "Module:Yesno" (args.south)
	
	return p.Koeppen(temperatures, precipitation, Southern_Hemisphere)
end

return p