Module:Weather and Module:Weather/sandbox: Difference between pages
(Difference between pages)
imported>Erutuon m merge multi-line comments |
imported>WOSlinker use require('strict') instead of require('Module:No globals') |
||
| Line 1: | Line 1: | ||
-- | 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 | local function monospace(str) | ||
return '<span style="background-color: #EEE; font-family: monospace;">' .. str .. '</span>' | |||
end | |||
local function | -- Input and output parameters | ||
local function getFormat(inputParameter, outputParameter, palette, messages) | |||
-- | local length, inputUnit, outputUnit, palette, show, cellFormat | ||
if inputParameter == nil then | |||
if | 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 | end | ||
if | |||
if inputUnit == "C" then | |||
outputUnit = "F" | |||
else | 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 | ||
end | end | ||
if | |||
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 | |||
local | cellFormat = {} | ||
if | for i, unit in require("Module:StringTools").imatch(outputParameter, "[CF]") do | ||
cellFormat[i] = unit | |||
if i > 2 then | |||
break | |||
end | |||
end | |||
elseif | 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 | 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 | ||
end | end | ||
return | |||
palette = palette or "cool2avg" | |||
show = messages == "show" | |||
return { | |||
length = length, inputUnit = inputUnit, outputUnit = outputUnit, | |||
cellFormat = cellFormat, show = show, palette = palette | |||
} | |||
end | end | ||
local function | -- Math functions | ||
if | local function round(value, decimals) | ||
return | value = tonumber(value) | ||
if type(value) == "number" then | |||
return string.format("%." .. decimals .. "f", value) | |||
else | else | ||
error("Format was asked to operate on " .. tostring(value) .. ", which cannot be converted to a number.", 2) | |||
return "" | |||
end | end | ||
end | end | ||
local function | local function convert(value, unit, decimals) -- Unit is the unit being converted from. | ||
if not unit then | |||
error("No unit supplied to convert.", 2) | |||
if not | |||
end | end | ||
if tonumber(value) then | |||
if | local value = tonumber(value) | ||
local | if unit == "C" then | ||
if | return round(value * 9/5 + 32, decimals) | ||
elseif unit == "F" then | |||
return round((value - 32) * 5/9, decimals) | |||
else | else | ||
error("Input unit not recognized", 2) | |||
end | end | ||
else | |||
-- to avoid concatenation errors | |||
return "" | |||
return | |||
end | end | ||
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 | |||
if | 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 | end | ||
return array | |||
end | end | ||
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 = { | cool = { | ||
{ -42.75, 4.47, 41.5, 60 }, | { -42.75, 4.47, 41.5, 60 }, -- red | ||
{ -42.75, 4.47, 4.5, 41.5 }, | { -42.75, 4.47, 4.5, 41.5 }, -- green | ||
{ -90 , -42.78, 4.5, 23 }, | { -90 , -42.78, 4.5, 23 }, -- blue | ||
white = { -23.3, 37.8 }, | white = { -23.3, 37.8 }, -- background | ||
}, | }, | ||
cool2 = { | cool2 = { | ||
| Line 149: | Line 219: | ||
} | } | ||
local function | --[[ Return style for a table cell based on the given value which | ||
local | 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 < min or 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 | end | ||
local function | local function temperatureColorCSS(palette, value, outRGB) | ||
return | return colorCSS(temperatureColor(palette, value, outRGB)) | ||
end | end | ||
local function | 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 | end | ||
local function | local function styleAttribute(palette, value, outRGB) | ||
local fontSize = "font-size: 85%;" | |||
local color = temperatureColorCSS(palette, value, outRGB) | |||
return 'style=\"' .. color .. ' ' .. fontSize .. '\"' | |||
end | end | ||
local function | local style_attribute = styleAttribute | ||
--[=[ | |||
Used by {{Average temperature table/row/C/sandbox}}, | |||
{{Average temperature table/row/F/sandbox}}, | |||
{{Average temperature table/row/C/sandbox}}, | |||
{{Template:Avg temp row F/sandbox2}}, | |||
{{Template:Avg temp row C/sandbox2}}. | |||
]=] | |||
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 | 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 "<br>" or " " | |||
else | |||
convertedUnitsSeparator = outputFormat.lineBreak == true and "<br>" 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<span style=\"color: red; font-size: 80%; line-height: 100%;\">" .. message .. "</span>") | |||
end | |||
output = table.concat(output) | |||
output = hyphenToMinus(output) | |||
return output | |||
end | |||
local chart = [[ | local chart = [[ | ||
| Line 183: | Line 595: | ||
]] | ]] | ||
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() | local function collection() | ||
-- Return a table to hold items. | -- Return a table to hold items. | ||
| Line 191: | Line 603: | ||
n = 0, | n = 0, | ||
add = function (self, item) | add = function (self, item) | ||
self.n = self.n + 1 | if item then | ||
self.n = self.n + 1 | |||
self[self.n] = item | |||
end | |||
end, | end, | ||
join = function (self, sep) | join = function (self, sep) | ||
| Line 209: | Line 623: | ||
local function with_minus(value) | local function with_minus(value) | ||
if value < 0 then | if value < 0 then | ||
return | return minus .. tostring(-value) | ||
end | end | ||
return tostring(value) | return tostring(value) | ||
| Line 216: | Line 630: | ||
local first = args[1] or -90 | local first = args[1] or -90 | ||
local last = args[2] or 59 | local last = args[2] or 59 | ||
local palette = palettes[args.palette] or palettes.cool | local palette = p.palettes[args.palette] or p.palettes.cool | ||
local xvals, reds, greens, blues = collection(), collection(), collection(), collection() | local xvals, reds, greens, blues = collection(), collection(), collection(), collection() | ||
local wikitext = collection() | local wikitext = collection() | ||
wikitext:add( | wikitext:add('{| class="wikitable"\n|-\n') | ||
{| class="wikitable" | |||
|- | |||
local columns = 0 | local columns = 0 | ||
for celsius = first, last do | for celsius = first, last do | ||
local | local backgroundRGB = {} | ||
local style = | local style = styleAttribute(palette, celsius, backgroundRGB) | ||
local R = math.floor( | local R = math.floor(backgroundRGB[1]) | ||
local G = math.floor( | local G = math.floor(backgroundRGB[2]) | ||
local B = math.floor( | local B = math.floor(backgroundRGB[3]) | ||
xvals:add(celsius) | xvals:add(celsius) | ||
reds:add(R) | reds:add(R) | ||
| Line 250: | Line 659: | ||
end | end | ||
return | return p | ||