Jump to content

Module:Weather and Module:Weather/sandbox: Difference between pages

(Difference between pages)
Page 1
Page 2
Content deleted Content added
m merge multi-line comments
 
use require('strict') instead of require('Module:No globals')
 
Line 1: Line 1:
local p = {}
--[[
Efficient (fast) functions to implement cells in tables of weather data.
Temperature conversion is built-in, but for simplicity, temperatures
are assumed to be for habitable locations (from -100 to 100 °C).
]]


require('strict')
local MINUS = '−' -- Unicode U+2212 MINUS SIGN


local degree = "°" -- used by addUnitNames()
local function temperature_style(palette, value, out_rgb)
local minus = "−" -- used by makeRow() and makeTable()
-- Return style for a table cell based on the given value which
local thinSpace = mw.ustring.char(0x2009) -- used by makeCell()
-- should be a temperature in °C.

local function style(bg, fg)
local precision, decimals
local min, max = unpack(palette.white or { -23, 35 })

if not fg and value and (value < min or value >= max) then
-- if not empty
fg = 'FFFFFF'
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 '<span style="background-color: #EEE; font-family: monospace;">' .. str .. '</span>'
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
end
if fg then
fg = 'color:#' .. fg .. ';'
-- 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
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)
fg = ''
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
return 'style="background:#' .. bg .. ';' .. fg .. ' font-size:100%;"'
end
if type(value) ~= 'number' then
return style('FFFFFF', '000000')
end
end
local rgb = out_rgb or {}
if outputParameter == nil then
for i, v in ipairs(palette) do
-- Since there are default values, the module will still generate output with an empty output parameter.
local a, b, c, d = unpack(v)
addMessage("No output format has been provided in the " .. monospace("output") .. " parameter, so default values will be used.")
if value <= a then
else
rgb[i] = 0
cellFormat = {}
elseif value < b then
for i, unit in require("Module:StringTools").imatch(outputParameter, "[CF]") do
rgb[i] = (value - a) * 255 / (b - a)
cellFormat[i] = unit
elseif value <= c then
rgb[i] = 255
if i > 2 then
break
elseif value < d then
end
rgb[i] = 255 - ( (value - c) * 255 / (d - c) )
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
else
error('C or F not found in output parameter')
rgb[i] = 0
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
end
end
return style(string.format('%02X%02X%02X', rgb[1], rgb[2], rgb[3]))
palette = palette or "cool2avg"
show = messages == "show"
return {
length = length, inputUnit = inputUnit, outputUnit = outputUnit,
cellFormat = cellFormat, show = show, palette = palette
}
end
end


-- Math functions
local function format_cell(palette, value, intext, outtext)

-- Return one line of wikitext to make a cell in a table.
if not value then
local function round(value, decimals)
value = tonumber(value)
return '|\n'
if type(value) == "number" then
end
return string.format("%." .. decimals .. "f", value)
local text
if outtext then
text = intext .. '<br>(' .. outtext .. ')'
else
else
error("Format was asked to operate on " .. tostring(value) .. ", which cannot be converted to a number.", 2)
text = intext
return ""
end
end
return '| ' .. temperature_style(palette, value) .. ' | ' .. text .. '\n'
end
end


local function process_temperature(intext, inunit, swap)
local function convert(value, unit, decimals) -- Unit is the unit being converted from.
if not unit then
--[[ Convert °C to °F or vice versa, assuming the temperature is for a
error("No unit supplied to convert.", 2)
habitable location, well inside the range -100 to 100 °C.
That simplifies determining precision and formatting (no commas are needed).
Return (celsius_value, intext, outtext) if valid; otherwise return nil.
The returned input and output are swapped if requested.
Each returned string has a Unicode MINUS as sign, if negative. ]]
local invalue = tonumber(intext)
if not invalue then return nil end
local integer, dot, decimals = intext:match('^%s*%-?(%d+)(%.?)(%d*)%s*$')
if not integer then return nil end
if invalue < 0 then
intext = MINUS .. integer .. dot .. decimals
end
end
if tonumber(value) then
local outtext
local value = tonumber(value)
if inunit == 'C' or inunit == 'F' then
if unit == "C" then
local celsius_value, outvalue
return round(value * 9/5 + 32, decimals)
if inunit == 'C' then
elseif unit == "F" then
outvalue = invalue * (9/5) + 32
return round((value - 32) * 5/9, decimals)
celsius_value = invalue
else
else
error("Input unit not recognized", 2)
outvalue = (invalue - 32) * (5/9)
celsius_value = outvalue
end
end
else
local precision = dot == '' and 0 or #decimals
-- to avoid concatenation errors
outtext = string.format('%.' .. precision .. 'f', math.abs(outvalue) + 2e-14)
return ""
if outvalue < 0 and tonumber(outtext) ~= 0 then
-- Don't show minus if result is negative but rounds to zero.
outtext = MINUS .. outtext
end
if swap then
return celsius_value, outtext, intext
end
return celsius_value, intext, outtext
end
end
-- LATER Think about whether a no-conversion option would be useful.
return invalue, intext, outtext
end
end


-- Stick numbers into array. Find out if any have decimals.
local function temperature_row(palette, row, inunit, swap)
-- Throw an error if any are invalid.
--[[
local function _makeArray(format)
Return 13 lines specifying the style/content of 13 table cells.
return function(parameter)
Input is 13 space-separated words, each a number (°C or °F).
if not parameter then
Any word that is not a number gives a blank cell ("M" for a missing cell).
return nil
Any excess words are ignored.
Function Input Output
------------------------
CtoF C C/F
FfromC C F/C
CfromF F C/F
FtoC F F/C ]]
local nrcol = 13
local results, n = {}, 0
for word in row:gmatch('%S+') do
n = n + 1
if n > nrcol then
break
end
end
local array = {}
results[n] = format_cell(palette, process_temperature(word, inunit, swap))
-- 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
for i = n + 1, nrcol do
results[i] = format_cell()
end
return table.concat(results)
end
end


-- Color generation
local palettes = {

-- A background color entry in a palette is a table of four numbers,
p.palettes = {
-- say { 11, 22, 33, 44 } (values in °C).
--[[
-- That means the color is 0 below 11 and above 44, and is 255 from 22 to 33.
The first three arrays in each palette defines background color using a
-- The color rises from 0 to 255 between 11 and 22, and falls between 33 and 44.
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:
}
}


--[[ Return style for a table cell based on the given value which
local function temperatures(frame, inunit, swap)
should be a temperature in °C. ]]
local palette = palettes[frame.args.palette] or palettes.cool
local function temperatureColor(palette, value, outRGB)
return temperature_row(palette, frame.args[1], inunit, swap)
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
end


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


local function CfromF(frame)
local function temperatureColorCSS(palette, value, outRGB)
return temperatures(frame, 'F', true)
return colorCSS(temperatureColor(palette, value, outRGB))
end
end


local function FtoC(frame)
local function temperatureCSS(value, unit, palette)
local palette = p.palettes[palette] or p.palettes.cool
return temperatures(frame, 'F')
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 FfromC(frame)
local function styleAttribute(palette, value, outRGB)
local fontSize = "font-size: 85%;"
return temperatures(frame, 'C', true)
local color = temperatureColorCSS(palette, value, outRGB)
return 'style=\"' .. color .. ' ' .. fontSize .. '\"'
end
end

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

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 .. "&nbsp;" .. 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 "&nbsp;"
else
convertedUnitsSeparator = outputFormat.lineBreak == true and "<br>" or outputFormat.lineBreak == false and "&nbsp;" 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:
]]
]]


local function show(frame)
function p.show(frame)
--[[ For testing, return wikitext to show graphs of how the red/green/blue colors
-- For testing, return wikitext to show graphs of how the red/green/blue colors
vary with temperature, and a table of the resulting 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[self.n] = item
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 MINUS .. tostring(-value)
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 rgb = {}
local backgroundRGB = {}
local style = temperature_style(palette, celsius, rgb)
local style = styleAttribute(palette, celsius, backgroundRGB)
local R = math.floor(rgb[1])
local R = math.floor(backgroundRGB[1])
local G = math.floor(rgb[2])
local G = math.floor(backgroundRGB[2])
local B = math.floor(rgb[3])
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
CtoF = CtoF,
CfromF = CfromF,
FtoC = FtoC,
FfromC = FfromC,
show = show,
}