Module:Weather/sandbox

local p = {}

require('strict')

local degree = "°" -- used by addUnitNames local minus = "−" -- used by makeRow and makeTable local thinSpace = mw.ustring.char(0x2009) -- used by makeCell

local precision, decimals

-- if not empty local function ine(var) var = tostring(var) if var == "" then return nil else return var end end

-- Error message handling local message = ""

local function addMessage(newMessage) if ine(message) then message = message .. " " .. newMessage else message = "Notices: " .. newMessage end end

local function monospace(str) return ' ' .. str .. ' ' end

-- Input and output parameters local function getFormat(inputParameter, outputParameter, palette, messages) local length, inputUnit, outputUnit, palette, show, cellFormat if inputParameter == nil then error('Please provide the number of values and a unit in the input parameter') else -- Find as many as two digits in the input parameter. length = tonumber(string.match(inputParameter, "(%d%d?)")) if not length then length = 13 addMessage('getFormat has not found a length value in the input parameter; length defaults to "13"') end -- Find C or F, but not both if string.find(inputParameter, "C") and string.find(inputParameter, "F") then error("Input unit must be either C (Celsius) or F (Fahrenheit)") else inputUnit = string.match(inputParameter, "([CF])") or error("Please provide an input unit in the input parameter: F for Fahrenheit or C for Celsius", 0) end if inputUnit == "C" then outputUnit = "F" else outputUnit = "C" end -- Make sure nothing except C, F, numbers, or spaces is in the input parameter. if string.find(inputParameter, "[^CF%d%s]") then addMessage("There are extraneous characters in the " .. monospace("output") .. " parameter.") end end if outputParameter == nil then -- Since there are default values, the module will still generate output with an empty output parameter. addMessage("No output format has been provided in the " .. monospace("output") .. " parameter, so default values will be used.") else cellFormat = {} for i, unit in require("Module:StringTools").imatch(outputParameter, "[CF]") do			cellFormat[i] = unit if i > 2 then break end end local function setFormat(key, variable, value) if string.find(outputParameter, key) then cellFormat[variable] = value else cellFormat[variable] = not value end end if cellFormat[1] then cellFormat.first = cellFormat[1] else error('C or F not found in output parameter') end if cellFormat[2] == nil then cellFormat["convertUnits"] = false else if cellFormat[2] == cellFormat[1] then error('There should not be two of the same unit name in the output parameter.') else cellFormat["convertUnits"] = true end end setFormat("unit", "unitNames", true) setFormat("no ?color", "color", false) setFormat("sort", "sortable", true) setFormat("full ?size", "smallFont", false) setFormat("no ?brackets", "brackets", false) setFormat("round", "decimals", "0", "") if string.find(outputParameter, "line break") then cellFormat["lineBreak"] = true elseif string.find(outputParameter, "one line") then cellFormat["lineBreak"] = false else cellFormat["lineBreak"] = "auto" end if string.find(outputParameter, "one line") and string.find(outputParameter, "line break") then error('Place either "one line" or "line break" in the output parameter, not both') end end palette = palette or "cool2avg" show = messages == "show" return { length = length, inputUnit = inputUnit, outputUnit = outputUnit, cellFormat = cellFormat, show = show, palette = palette } end

-- Math functions

local function round(value, decimals) value = tonumber(value) if type(value) == "number" then return string.format("%." .. decimals .. "f", value) else error("Format was asked to operate on " .. tostring(value) .. ", which cannot be converted to a number.", 2) return "" end end

local function convert(value, unit, decimals) -- Unit is the unit being converted from. if not unit then error("No unit supplied to convert.", 2) end if tonumber(value) then local value = tonumber(value) if unit == "C" then return round(value * 9/5 + 32, decimals) elseif unit == "F" then return round((value - 32) * 5/9, decimals) else error("Input unit not recognized", 2) end else -- to avoid concatenation errors return "" end end

-- Stick numbers into array. Find out if any have decimals. -- Throw an error if any are invalid. local function _makeArray(format) return function(parameter) if not parameter then return nil end local array = {} -- If there are multiple parameters for numbers, and the first doesn't have -- decimals, the rest will have their decimals rounded off. format.precision = format.precision or parameter:find("%d%.%d") and "1" or "0" local numbers = mw.text.split(parameter, "%s+") if #numbers ~= format.length then addMessage('There are not ' .. format.length .. ' values in the ' .. parameter .. ' parameter.') end for i, number in ipairs(numbers) do			if not number:find("^%-?%d%d?%d?.?(%d?)$") then error('The number "' .. number .. '" does not fit the expected pattern.') end table.insert(array, number) end return array end end

-- Color generation

p.palettes = { --		The first three arrays in each palette defines background color using a		table of four numbers, say { 11, 22, 33, 44 } (values in °C).		That means that, on the scale from 0 (black) to 255 (saturated), the color		is 0 below 11°C and above 44°C, and is 255 from 22°C to 33°C.		The color rises from 0 to 255 between 11°C and 22°C, and falls from 255 to 0		between 33°C and 44°C. cool = { { -42.75,  4.47, 41.5, 60   }, -- red { -42.75,  4.47,  4.5, 41.5 }, -- green { -90 , -42.78,  4.5, 23   }, -- blue white = { -23.3, 37.8 },		-- background },	cool2 = { { -42.75,  4.5, 41.5, 56   },		{ -42.75,   4.5 ,  4.5, 41.5 },		{ -90   , -42.78,  4.5, 23   },		white = { -23.3, 35 }, },	cool2avg = { { -38,  4.5, 25 , 45   },		{ -38,   4.5,  4.5, 30   },		{ -70, -38  ,  4.5, 23   },		white = { -23.3, 25 }, }, }

-- Return style for a table cell based on the given value which	should be a temperature in °C. local function temperatureColor(palette, value, outRGB) local backgroundColor, textColor value = tonumber(value) if not value then backgroundColor, textColor = 'FFF', '000' addMessage("Value supplied to " .. monospace("temperatureColor") .. " is not recognized.") else local min, max = unpack(palette.white or { -23, 35 }) if value = max then textColor = 'FFF' -- Else nil. -- This assumes that black text color is the default for most readers. end

local backgroundRGB = outRGB or {} for i, v in ipairs(palette) do			local a, b, c, d = unpack(v) if value <= a then backgroundRGB[i] = 0 elseif value < b then backgroundRGB[i] = (value - a) * 255 / (b - a)			elseif value <= c then backgroundRGB[i] = 255 elseif value < d then backgroundRGB[i] = 255 - ( (value - c) * 255 / (d - c) ) else backgroundRGB[i] = 0 end end backgroundColor = string.format('%02X%02X%02X', unpack(backgroundRGB)) end return backgroundColor, textColor end

local function colorCSS(backgroundColor, textColor) if backgroundColor and textColor then return 'background: #' .. backgroundColor .. '; color: #' .. textColor .. ';'	elseif backgroundColor then return 'background: #' .. backgroundColor .. ';'	else return '' end end

local function temperatureColorCSS(palette, value, outRGB) return colorCSS(temperatureColor(palette, value, outRGB)) end

local function temperatureCSS(value, unit, palette) local palette = p.palettes[palette] or p.palettes.cool local value = tonumber(value) if value == nil then error("The function " .. monospace("temperatureCSS") .. " is receiving a nil value") else if unit == 'F' then value = convert(value, 'F', decimals) elseif unit ~= 'C' then unitError(unit or "nil") end return colorCSS(temperatureColor(palette, value)) end end

local function styleAttribute(palette, value, outRGB) local fontSize = "font-size: 85%;" local color = temperatureColorCSS(palette, value, outRGB) return 'style=\"' .. color .. ' ' .. fontSize .. '\"' end

local style_attribute = styleAttribute

--[=[	Used by , ,	,	,	. ]=] function p.temperatureStyle(frame) local palette = p.palettes[frame.args.palette] or p.palettes.cool local unit = frame.args.unit or 'C'	local value = tonumber(frame.args[1]) if unit == 'F' then value = convert(value, 'F', 1) elseif unit ~= 'C' then error('Unrecognized unit: ' .. unit) end return styleAttribute(palette, value) end

p.temperature_style = p.temperatureStyle

-- ==== Cell, row, table generation ==== local outputFormats = { high_low_average_F = { first = "F", convertUnits = true, unitNames = false, color = true, smallFont = true, sortable = true, decimals = "0", brackets = true, lineBreak = "auto", }, high_low_average_C = { first = "C", convertUnits = true, unitNames = false, color = true, smallFont = true, sortable = true, decimals = "0", brackets = true, lineBreak = "auto", }, high_low_F = { first = "F", convertUnits = true, unitNames = false, color = false, smallFont = true, sortable = false, decimals = "", brackets = true, lineBreak = "auto", }, high_low_C = { first = "C", convertUnits = true, unitNames = false, color = false, smallFont = true, sortable = false, decimals = "0", brackets = true, lineBreak = "auto", }, average_F = { first = "F", convertUnits = true, unitNames = false, color = true, smallFont = true, sortable = false, decimals = "0", brackets = true, lineBreak = "auto", }, average_C = { first = "C", convertUnits = true, unitNames = false, color = true, smallFont = true, sortable = false, decimals = "0", brackets = true, lineBreak = "auto", }, }

local outputFormat

local function addUnitNames(value, yesOrNo, unit) if not unit then error("No unit supplied as argument 3 to addUnitNames", 2) end -- Don't add a unit name to an empty string value = yesOrNo == true and ine(value) and value .. " " .. degree .. unit or value return value end

local function ifYes(parameter, realization1, realization2) local result if realization1 then if realization2 then result = parameter == true and { realization1, realization2 } or { "", "" } else result = parameter == true and realization1 or "" end else result = "" addMessage(monospace("ifYes") .. " needs at least one realization.") end return result end

local function makeCell(outputFormat, a, b, c, format) local cell, cellContent = "", "" local colorCSS, otherCSS, titleAttribute, sortkey, attributeSeparator, convertedUnitsSeparator = "", "", "", "", "", "", ""	-- Distinguish styleAttribute variable from styleAttribute function above. local styleAttribute, highLowSeparator, brackets, values, convertedUnits = {"", ""}, {"", ""}, {"", ""}, {"", ""}, {"", ""}	-- Precision is 1 if any number has one or more decimals. decimals = tonumber(outputFormat.decimals) and outputFormat.decimals or format.precision if tonumber(b) and tonumber(a) then values, highLowSeparator = { round(a, decimals), round(b, decimals) }, { thinSpace .. "/" .. thinSpace, ifYes(outputFormat.convertUnits, thinSpace .. "/" .. thinSpace) } elseif tonumber(a) then values = { round(a, decimals), "" } elseif tonumber(c) then values = { round(c, decimals), "" } end if outputFormat.first == format.inputUnit then if outputFormat.convertUnits == true then convertedUnits = { addUnitNames(convert(values[1], format.inputUnit, decimals), outputFormat.unitNames, format.outputUnit), addUnitNames(convert(values[2], format.inputUnit, decimals), outputFormat.unitNames, format.outputUnit) } end values = { addUnitNames(values[1], outputFormat.unitNames, format.inputUnit), addUnitNames(values[2], outputFormat.unitNames, format.inputUnit) } elseif outputFormat.first == "C" or outputFormat.first == "F" then if outputFormat.convertUnits == true then convertedUnits = { addUnitNames(values[1], outputFormat.unitNames, format.inputUnit), addUnitNames(values[2], outputFormat.unitNames, format.inputUnit) } end values = { addUnitNames(convert(values[1], format.inputUnit, decimals), outputFormat.unitNames, format.outputUnit), addUnitNames(convert(values[2], format.inputUnit, decimals), outputFormat.unitNames, format.outputUnit) } else addMessage(monospace(tostring(outputFormat.first)) .. ", the value for " .. monospace("first") .. " in " .. monospace("outputFormat") .. " is not recognized.") end --		Regarding line breaks:		If there are two values, there will be at least three characters: 9/1.		If there is one decimal, numbers will be three to five characters long		and there will be 3 to 10 characters total even without unit conversion:			1.1, 116.5/88.0.		If there are units, that adds three characters per number: 25 °C/20 °C.		In each of these cases, a line break is needed so that table cells are not too wide;		even more so when more than one of these things are true. if outputFormat.convertUnits == true then brackets = outputFormat.brackets == true and { "(", ")" } or { "", "" } if outputFormat.lineBreak == "auto" then convertedUnitsSeparator = ( ine(values[2]) or decimals ~= "0" or outputFormat.showUnits == true ) and " " or " " else convertedUnitsSeparator = outputFormat.lineBreak == true and " " or outputFormat.lineBreak == false and " " or error('Value for lineBreak not recognized') end end cellContent = values[1] .. highLowSeparator[1] .. values[2] .. convertedUnitsSeparator .. brackets[1] .. convertedUnits[1] .. highLowSeparator[2] .. convertedUnits[2] .. brackets[2] if tonumber(c) then colorCSS = outputFormat.color == true and temperatureCSS(c, format.inputUnit, format.palette, format.inputUnit) or "" if tonumber(b) and tonumber(a) then local attributeValue = outputFormat.first == format.inputUnit and c or convert(c, format.inputUnit, decimals) sortkey = outputFormat.sortable == true and " data-sort-value=\"" .. attributeValue .. "\"" or "" titleAttribute = " title=\"Average temperature: " .. attributeValue .. " " .. degree .. outputFormat.first .. "\"" end elseif tonumber(b) then colorCSS = "" elseif tonumber(a) then colorCSS = outputFormat.color == true and temperatureCSS(a, format.inputUnit, format.palette) or "" else addMessage('Neither a nor b nor c are strings.') end otherCSS = outputFormat.smallFont == true and "font-size: 85%;" or "" if ine(colorCSS) or ine(otherCSS) then styleAttribute = { "style=\"", "\"" } end if ine(otherCSS) or ine(colorCSS) or ine(titleAttribute) or ine(sortkey) then attributeSeparator = " | " end cell = "\n| " .. styleAttribute[1] .. colorCSS .. otherCSS .. styleAttribute[2] .. titleAttribute .. sortkey .. attributeSeparator .. cellContent return cell end

--	Replaces hyphens that have a punctuation or space character before them and a number after them,	making sure that hyphens in "data-sort-type" are not replaced with minuses.	If Lua had (?<=), a capture would not be necessary. local function hyphenToMinus(str) return str:gsub("([%p%s])-(%d)", "%1" .. minus .. "%2") end

function p.makeRow(frame) local args = frame.args local format = getFormat(args.input, args.output, args.palette, args.messages) local makeArray = _makeArray(format) local a, b, c = makeArray(args.a), makeArray(args.b), makeArray(args.c)	local output = {} if args[1] then table.insert(output, "\n|-") table.insert(output, "\n! " .. args[1]) if args[2] then table.insert(output, " !! " .. args[2]) end end if format.cellFormat then outputFormat = format.cellFormat end -- Assumes that if c is defined, b and a are, and if b is defined, a is. if c then if not outputFormat then outputFormat = outputFormats.high_low_average_F end for i = 1, format.length do			table.insert(output, makeCell(outputFormat, a[i], b[i], c[i], format)) end elseif b then if not outputFormat then outputFormat = outputFormats.high_low_F end for i = 1, format.length do			table.insert(output, makeCell(outputFormat, a[i], b[i], nil, format)) end elseif a then if not outputFormat then outputFormat = outputFormats.average_F end for i = 1, format.length do			table.insert(output, makeCell(outputFormat, a[i], nil, nil, format)) end end output = table.concat(output) output = hyphenToMinus(output) return output end

function p.makeTable(frame) local args = frame.args local format = getFormat(args.input, args.output, args.palette, args.messages) local makeArray = _makeArray(format) local a, b, c = makeArray(args.a), makeArray(args.b), makeArray(args.c)	local output = { "{| class=\"wikitable center nowrap\"" } if format.cellFormat then outputFormat = format.cellFormat end -- Assumes that if c is defined, b and a are, and if b is defined, a is. if c then for i = 1, format.length do			if not outputFormat then outputFormat = outputFormats.high_low_average_F end table.insert(output, makeCell(outputFormat, a[i], b[i], c[i], format)) end elseif b then for i = 1, format.length do			if not outputFormat then outputFormat = outputFormats.high_low_F end table.insert(output, makeCell(outputFormat, a[i], b[i], nil, format)) end elseif a then for i = 1, format.length do			if not outputFormat then outputFormat = outputFormats.average_F end table.insert(output, makeCell(outputFormat, a[i], nil, nil, format)) end end table.insert(output, "\n|}") if format.show then table.insert(output, "\n\n" .. message .. " ") end output = table.concat(output) output = hyphenToMinus(output) return output end

local chart = [[

]]

function p.show(frame) -- For testing, return wikitext to show graphs of how the red/green/blue colors -- vary with temperature, and a table of the resulting colors. local function collection -- Return a table to hold items. return { n = 0, add = function (self, item) if item then self.n = self.n + 1 self[self.n] = item end end, join = function (self, sep) return table.concat(self, sep) end, }	end local function make_chart(result, color, xvalues, yvalues) result:add('\n') result:add(frame:preprocess((chart:gsub('__[A-Z]+', { __COLOR = color, __XVALUES = xvalues:join(','), __YVALUES = yvalues:join(','), }))))	end local function with_minus(value) if value < 0 then return minus .. tostring(-value) end return tostring(value) end local args = frame.args local first = args[1] or -90 local last = args[2] or 59 local palette = p.palettes[args.palette] or p.palettes.cool local xvals, reds, greens, blues = collection, collection, collection, collection local wikitext = collection wikitext:add('{| class="wikitable"\n|-\n') local columns = 0 for celsius = first, last do		local backgroundRGB = {} local style = styleAttribute(palette, celsius, backgroundRGB) local R = math.floor(backgroundRGB[1]) local G = math.floor(backgroundRGB[2]) local B = math.floor(backgroundRGB[3]) xvals:add(celsius) reds:add(R) greens:add(G) blues:add(B) wikitext:add('| ' .. style .. ' | ' .. with_minus(celsius) .. '\n') columns = columns + 1 if columns >= 10 then columns = 0 wikitext:add('|-\n') end end wikitext:add('|}\n') make_chart(wikitext, 'Red', xvals, reds) make_chart(wikitext, 'Green', xvals, greens) make_chart(wikitext, 'Blue', xvals, blues) return wikitext:join end

return p