Module:Weather/stableSandbox

export = {}

degree = "°" -- used by add_unit_names()

minus = "−" -- used by makeRow() and makeTable()

thinSpace = mw.ustring.char(0x2009) -- used by makeCell()

-- Error message handling

message = ""

local function add_message(new_message)

if show then

if check_for_string(message) then

message = message .. " " .. new_message

else

message = "Notices: " .. new_message

end

end

end

-- Input and output parameters

local function get_format (frame)

local input_parameter = frame.args.input

local output_parameter = frame.args.output

if input_parameter == nil then

error("Please provide the number of values and a unit in the input parameter")

else

length = tonumber(string.match(input_parameter, "(%d+)")) -- Find digits in the input parameter.

input_unit = string.match(input_parameter, "([CF])") -- C or F

if string.find(input_parameter, "[^CF%d%s]") then

add_message("There are extraneous characters in the output parameter.")

end

end

if input_unit == "C" then

output_unit = "F"

elseif input_unit == "F" then

output_unit = "C"

else

error ("Please provide an input unit in the input parameter: F for Fahrenheit or C for Celsius", 0)

end

if length == nil then

error ("get_format has not found a length value in the input parameter")

end

if output_parameter == nil then

add_message("No output format has been provided in the output parameter.")

else

cell_format = {}

local n = 1

for unit in output_parameter:gmatch("[CF]") do

cell_format[n] = unit

n = n + 1

if n > 2 then

break

end

end

local function set_format(key, formatVariable, formatValue1, formatValue2)

if string.find(output_parameter, key) then

cell_format[formatVariable] = formatValue1

else

cell_format[formatVariable] = formatValue2

end

end

if cell_format[1] then

cell_format.first = cell_format[1]

else

error("C or F not found in output parameter")

end

if cell_format[2] == nil then

cell_format["convert_units"] = "no"

else

if cell_format[2] == cell_format[1] then

error("There should not be two of the same unit name in the output parameter.")

else

cell_format["convert_units"] = "yes"

end

end

set_format("unit", "unit_names", "yes", "no")

set_format("no ?color", "color", "no", "yes")

set_format("sort", "sortable", "yes", "no")

set_format("full ?size", "small_font", "no", "yes")

set_format("no ?brackets", "brackets", "no", "yes")

set_format("round", "decimals", "0", "")

if string.find(output_parameter, "line break") then

cell_format["line_break"] = "yes"

elseif string.find(output_parameter, "one line") then

cell_format["line_break"] = "no"

else

cell_format["line_break"] = "auto"

end

if string.find(output_parameter, "one line") and string.find(output_parameter, "line break") then

error("Place either \"one line\" or \"line break\" in the output parameter, not both")

end

end

if frame.args.palette == nil then

palette = "cool2avg"

else

palette = frame.args.palette

end

if frame.args.messages == "show" then

show = true

else

show = false

end

return length, input_unit, output_unit

end

-- Number and string-handling functions

local function check_for_number(value)

return type(tonumber(value)) == "number"

end

function check_for_string(string)

string = tostring(string)

return string ~= "" and string ~= nil

end

local function round(value, decimals)

value = tonumber(value)

if type(value) == "number" then

local string = string.format("%." .. decimals .. "f", value)

return string

elseif value == nil then

value = "nil"

add_message("Format was asked to operate on " .. value .. ", which cannot be converted to a number.", 2)

return ""

end

end

local function convert(value, decimals, unit) -- Unit is the unit being converted from. It defaults to input_unit.

if not unit then

unit = input_unit

end

if check_for_number(value) then

local value = tonumber(value)

if unit == "C" then

add_message(value .. " " .. degree .. unit .. " was converted.")

return round(value * 9/5 + 32, decimals)

elseif unit == "F" then

add_message(value .. " " .. degree .. unit .. " was converted.")

return round((value - 32) * 5/9, decimals)

else

error("Input unit not recognized", 2)

end

else

return "" -- Setting result to empty string if value is not a number avoids concatenation errors.

end

end

-- Input parsing

function make_array(parameter, array, frame)

local array = {}

local n = 1

for number in parameter:gmatch("%-?%d+%.?%d?") do

local number = number

if n == 1 then

local decimals = number:match("%.(%d+)")

if decimals == nil then

precision = "0"

else

precision = #decimals

end

end

table.insert(array, n, number)

n = n + 1

if n > length then

break

end

end

if not array[length] then

add_message("There are not " .. length .. " values in the " .. parameter .. " parameter.")

end

return array, precision

end

function make_arrays(frame)

get_format(frame)

local parameter_a = frame.args.a

local parameter_b = frame.args.b

local parameter_c = frame.args.c

if parameter_a then

a = make_array(parameter_a, a, frame)

else

error("Please provide a set of numbers in parameter a")

end

if parameter_b then

b = make_array(parameter_b, b, frame)

else

add_message("There is no content in parameter b.")

end

if parameter_c then

c = make_array(parameter_c, c, frame)

else

add_message("There is no content in parameter c.")

end

return a, b, c

end

-- Color generation

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 the color is 0 below 11 and above 44, and is 255 from 22 to 33.

-- The color rises from 0 to 255 between 11 and 22, and falls between 33 and 44.

cool = {

{ -42.75, 4.47, 41.5, 60 },

{ -42.75, 4.47, 4.5, 41.5 },

{ -90 , -42.78, 4.5, 23 },

white = { -23.3, 37.8 },

},

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 },

},

}

local function temperature_color(palette, value, out_rgb)

--[[ Return style for a table cell based on the given value which

should be a temperature in °C. ]]

local background_color, text_color

value = tonumber(value)

if value == nil then

background_color, text_color = 'FFF', '000'

add_message("Value supplied to temperature_color is not recognized.")

else

local min, max = unpack(palette.white or { -23, 35 })

if value < min or value >= max then

text_color = 'FFF'

else

text_color = '' -- This assumes that black text color is the default for most readers.

end

local background_rgb = out_rgb or {}

for i, v in ipairs(palette) do

local a, b, c, d = unpack(v)

if value <= a then

background_rgb[i] = 0

elseif value < b then

background_rgb[i] = (value - a) * 255 / (b - a)

elseif value <= c then

background_rgb[i] = 255

elseif value < d then

background_rgb[i] = 255 - ( (value - c) * 255 / (d - c) )

else

background_rgb[i] = 0

end

end

background_color = string.format('%02X%02X%02X', background_rgb[1], background_rgb[2], background_rgb[3])

end

if text_color == "" then

return background_color

else

return background_color, text_color

end

end

local function color_CSS(background_color, text_color)

if background_color and text_color then

return 'background: #' .. background_color .. '; color: #' .. text_color .. ';'

elseif background_color then

return 'background: #' .. background_color .. ';'

else

return ''

end

end

local function temperature_color_CSS(palette, value, out_rgb)

return color_CSS(temperature_color(palette, value, out_rgb))

end

function temperature_CSS(value, unit, palette)

local palette = palettes[palette] or palettes.cool

local value = tonumber(value)

if value == nil then

error("The function temperature_CSS is receiving a nil value")

else

if unit == 'C' then

return color_CSS(temperature_color(palette, value))

elseif unit == 'F' then

return color_CSS(temperature_color(palette, convert(value, decimals, 'F')))

else

unit_error(unit or "nil")

end

end

end

local function style_attribute(palette, value, out_rgb)

local font_size = "font-size: 85%;"

local color = temperature_color_CSS(palette, value, out_rgb)

return 'style=\"' .. color .. ' ' .. font_size .. '\"'

end

function export.temperature_style(frame) -- used by Template:Average temperature table/color

local palette = palettes[frame.args.palette] or palettes.cool

local unit = frame.args.unit or 'C'

local value = tonumber(frame.args[1])

if unit == 'C' then

return style_attribute(palette, value)

elseif unit == 'F' then

return style_attribute(palette, convert(value, 1, 'F'))

else

unit_error(unit)

end

end

-- ==== Cell, row, table generation ====

local output_formats = {

high_low_average_F =

{ first = "F",

convert_units = "yes",

unit_names = "no",

color = "yes",

small_font = "yes",

sortable = "yes",

decimals = "0",

brackets = "yes",

line_break = "auto", },

high_low_average_C =

{ first = "C",

convert_units = "yes",

unit_names = "no",

color = "yes",

small_font = "yes",

sortable = "yes",

decimals = "0",

brackets = "yes",

line_break = "auto", },

high_low_F =

{ first = "F",

convert_units = "yes",

unit_names = "no",

color = "no",

small_font = "yes",

sortable = "no",

decimals = "",

brackets = "yes",

line_break = "auto", },

high_low_C =

{ first = "C",

convert_units = "yes",

unit_names = "no",

color = "no",

small_font = "yes",

sortable = "no",

decimals = "0",

brackets = "yes",

line_break = "auto", },

average_F =

{ first = "F",

convert_units = "yes",

unit_names = "no",

color = "yes",

small_font = "yes",

sortable = "no",

decimals = "0",

brackets = "yes",

line_break = "auto", },

average_C =

{ first = "C",

convert_units = "yes",

unit_names = "no",

color = "yes",

small_font = "yes",

sortable = "no",

decimals = "0",

brackets = "yes",

line_break = "auto", },

}

local function add_unit_names(value, unit)

if not unit then unit = input_unit end

if output_format.unit_names == "yes" then

if check_for_string(value) then

return value .. " " .. degree .. unit

else

return value -- Don't add a unit name to an empty string

end

else

return value

end

end

local function if_yes(parameter, realization1, realization2)

if realization1 then

if realization2 then

if parameter == "yes" then

parameter = { realization1, realization2 }

else

parameter = { "", "" }

end

else

if parameter == "yes" then

parameter = realization1

else

parameter = ""

end

end

else

parameter = ""

add_message("if_yes needs at least one realization")

end

return parameter

end

function makeCell(output_format, a, b, c)

local cell, cell_content = "", ""

local color_CSS, other_CSS, title_attribute, sortkey, attribute_separator, converted_units_separator = "", "", "", "", "", "", ""

local style_attribute, high_low_separator, brackets, values, converted_units = {"", ""}, {"", ""}, {"", ""}, {"", ""}, {"", ""}

if check_for_number(output_format.decimals) then

decimals = output_format.decimals

--[[ Precision is the number of decimals in the first number of the last array.

This may be a problem for data from Weatherbase,

which seems to inappropriately remove .0 from numbers that have it. ]]

else

decimals = precision

end

if check_for_number(b) and check_for_number(a) then

values, high_low_separator = { round(a, decimals), round(b, decimals) }, { thinSpace .. "/" .. thinSpace, if_yes(output_format.convert_units, thinSpace .. "/" .. thinSpace) }

elseif check_for_number(a) then

values = { round(a, decimals), "" }

elseif check_for_number(c) then

values = { round(c, decimals), "" }

end

if output_format.first == input_unit then

if output_format.convert_units == "yes" then

converted_units = { add_unit_names(convert(values[1], decimals), output_unit), add_unit_names(convert(values[2], decimals), output_unit) }

end

values = { add_unit_names(values[1]), add_unit_names(values[2]) }

elseif output_format.first == "C" or output_format.first == "F" then

if output_format.convert_units == "yes" then

converted_units = { add_unit_names(values[1]), add_unit_names(values[2]) }

end

values = { add_unit_names(convert(values[1], decimals), output_unit), add_unit_names(convert(values[2], decimals), output_unit) }

else

if output_format.first == nil then

output_format.first = "nil"

end

add_message("" .. output_format.first .. ", the value for first in output_format 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 output_format.convert_units == "yes" then

brackets = if_yes(output_format.brackets, "(", ")" )

if output_format.line_break == "auto" then

if check_for_string(values[2]) or decimals ~= "0" or output_format.show_units == "yes" then

converted_units_separator = "
"

else

converted_units_separator = " "

end

elseif output_format.line_break == "yes" then

converted_units_separator = "
"

elseif output_format.line_break == "no" then

converted_units_separator = " "

else

error("Value for line_break not recognized")

end

end

cell_content = values[1] .. high_low_separator[1] .. values[2] .. converted_units_separator .. brackets[1] .. converted_units[1] .. high_low_separator[2] .. converted_units[2] .. brackets[2]

if check_for_number(c) then

color_CSS = if_yes(output_format.color, temperature_CSS(c, input_unit, palette))

if check_for_number(b) and check_for_number(a) then

local attribute_value

if output_format.first == input_unit then

attribute_value = c

else

attribute_value = convert(c, decimals)

end

sortkey = if_yes(output_format.sortable, " data-sort-value=\"" .. attribute_value .. "\"")

title_attribute = " title=\"Average temperature: " .. attribute_value .. " " .. degree .. output_format.first .. "\""

end

elseif check_for_number(b) then

color_css = ""

elseif check_for_number(a) then

color_CSS = if_yes(output_format.color, temperature_CSS(a, input_unit, palette))

else

add_message("Neither a nor b nor c are strings.")

end

other_CSS = if_yes(output_format.small_font, "font-size: 85%;")

if check_for_string(color_CSS) or check_for_string(other_CSS) then

style_attribute = { "style=\"", "\"" }

end

if check_for_string(other_CSS) or check_for_string(color_CSS) or check_for_string(title_attribute) or check_for_string(sortkey) then

attribute_separator = " | "

end

cell = "\n| " .. style_attribute[1] .. color_CSS .. other_CSS .. style_attribute[2] .. title_attribute .. sortkey .. attribute_separator .. cell_content

return cell

end

function export.makeRow(frame)

make_arrays(frame)

local output = ""

if frame.args[1] then

output = "\n|-"

output = output .. "\n! " .. frame.args[1]

if frame.args[2] then

output = output .. " !! " .. frame.args[2]

end

end

if cell_format then

output_format = cell_format

end

if a and b and c then

for i = 1, length do

if not output_format then

output_format = output_formats.high_low_average_F

end

output = output .. makeCell(output_format, a[i], b[i], c[i])

end

elseif a and b then

for i = 1, length do

if not output_format then

output_format = output_formats.high_low_F

end

output = output .. makeCell(output_format, a[i], b[i])

end

elseif a then

for i = 1, length do

if not output_format then

output_format = output_formats.average_F

end

output = output .. makeCell(output_format, a[i])

end

end

output = mw.ustring.gsub(output, "([%p%s])-(%d)", "%1" .. minus .. "%2")

return output

end

function export.makeTable(frame)

make_arrays(frame)

local output = "

class=\"wikitable center nowrap\""

if cell_format then

output_format = cell_format

end

if a and b and c then

for i = 1, length do

if not output_format then

output_format = output_formats.high_low_average_F

end

output = output .. makeCell(output_format, a[i], b[i], c[i])

end

elseif a and b then

for i = 1, length do

if not output_format then

output_format = output_formats.high_low_F

end

output = output .. makeCell(output_format, a[i], b[i])

end

elseif a then

for i = 1, length do

if not output_format then

output_format = output_formats.average_F

end

output = output .. makeCell(output_format, a[i])

end

end

output = mw.ustring.gsub(output, "([%p%s])-(%d)", "%1" .. minus .. "%2")

--[[ 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. ]]

output = output .. "\n

"

if show then

output = output .. "\n\n" .. message .. ""

end

return output

end

local chart = [[

{{Graph:Chart

|width=600

|height=180

|xAxisTitle=Celsius

|yAxisTitle=__COLOR

|type=line

|x=__XVALUES

|y=__YVALUES

|colors=__COLOR

}}

]]

function export.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)

self.n = self.n + 1

self[self.n] = item

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 = palettes[args.palette] or 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 background_rgb = {}

local style = style_attribute(palette, celsius, background_rgb)

local R = math.floor(background_rgb[1])

local G = math.floor(background_rgb[2])

local B = math.floor(background_rgb[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 export