Module:Weather/stableSandbox
Documentation for this module may be created at Module:Weather/stableSandbox/doc
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 <span style=\"background-color: #EEE; font-family: monospace;\">output</span> 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 <span style=\"background-color: #EEE; font-family: monospace;\">output</span> 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 <span style=\"background-color: #EEE; font-family: monospace;\">b</span>.") end if parameter_c then c = make_array(parameter_c, c, frame) else add_message("There is no content in parameter <span style=\"background-color: #EEE; font-family: monospace;\">c</span>.") 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 <span style=\"background-color: #EEE; font-family: monospace;\">temperature_color</span> 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 <span style=\"background-color: #EEE; font-family: monospace;\">temperature_CSS</span> 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("<span style=\"background-color: #EEE; font-family: monospace;\">if_yes</span> 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("<span style=\"background-color: #EEE; font-family: monospace;\">" .. output_format.first .. "</span>, the value for <span style=\"background-color: #EEE; font-family: monospace;\">first</span> in <span style=\"background-color: #EEE; font-family: monospace;\">output_format</span> 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 = "<br>" else converted_units_separator = " " end elseif output_format.line_break == "yes" then converted_units_separator = "<br>" 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<span style=\"color: red; font-size: 80%; line-height: 100%;\">" .. message .. "</span>" 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