Module:Time/sandbox

require ('Module:No globals') local getArgs = require ('Module:Arguments').getArgs local tz = {};																	-- holds local copy of the specified timezone table from tz_data{} local cfg = {};																	-- for internationalization

--[[-< S E T _ S A N D B O X >

Weather or not to use sandbox version of data (returns /sandbox or '')

]]

local function set_sandbox (frame) local data_sandbox; if frame:getTitle:find('sandbox', 1, true) then data_sandbox = true;                                                   -- bolean to toggle usage of sandbox data module when this module is in a sandbox (changeable) else data_sandbox = false;                                                  -- DO NOT CHANGE end if data_sandbox then return '/sandbox' else return '' end end

--[[--< I S _ S E T >--

Whether variable is set or not. A variable is set when it is not nil and not empty.

]]

local function is_set( var ) return not (nil == var or '' == var); end

--[[--< S U B S T I T U T E >--

Populates numbered arguments in a message string using an argument table.

]]

local function substitute (msg, args) return args and mw.message.newRawMessage (msg, args):plain or msg; end

--[[--< E R R O R _ M S G >

create an error message

]]

local function error_msg (msg, arg) return substitute (cfg.err_msg, substitute (cfg.err_text[msg], arg)) end

--[[--< D E C O D E _ D S T _ E V E N T >--

extract ordinal, day-name, and month from daylight saving start/end definition string as digits: Second Sunday in March returns 2 0 3

Casing doesn't matter but the form of the string does: – all are separated by spaces

]]

local function decode_dst_event (dst_event_string) local ord, day, month; dst_event_string = dst_event_string:lower;								-- force the string to lower case because that is how the tables above are indexed ord, day, month = dst_event_string:match ('([%a%d]+)%s+(%a+)%s+%a+%s+(%a+)'); if not (is_set (ord) and is_set (day) and is_set (month)) then				-- if one or more of these not set, then pattern didn't match return nil; end return cfg.ordinals[ord], cfg.days[day], cfg.months[month]; end

--[[--< G E T _ D A Y S _ I N _ M O N T H >

Returns the number of days in the month where month is a number 1–12 and year is four-digit Gregorian calendar. Accounts for leap year.

]]

local function get_days_in_month (year, month) local days_in_month = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; year = tonumber (year);														-- force these to be numbers just in case month = tonumber (month);

if (2 == month) then														-- if February if (0 == (year%4) and (0 ~= (year%100) or 0 == (year%400))) then		-- is year a leap year? return 29;															-- if leap year then 29 days in February end end return days_in_month [month]; end

--[[--< G E T _ D S T _ M O N T H _ D A Y >

Return the date (month and day of the month) for the day that is the ordinal (nth) day-name in month (second Friday in June) of the current year

timestamp is today's date-time number from os.time; used to supply year timezone is the timezone parameter value from the template call

Equations used in this function taken from Template:Weekday_in_month

]]

local function get_dst_month_day (timestamp, start) local ord, weekday_num, month; local first_day_of_dst_month_num; local last_day_of_dst_month_num; local days_in_month; local year;

if true == start then ord, weekday_num, month = decode_dst_event (tz.dst_begins);				-- get start string and convert to digits else ord, weekday_num, month = decode_dst_event (tz.dst_ends);				-- get end string and convert to digits end if not (is_set (ord) and is_set (weekday_num) and is_set (month)) then return nil;																-- could not decode event string end year = os.date ('%Y', timestamp);

if -1 == ord then		-- j = t + 7×(n + 1) - (wt - w) mod 7				-- if event occurs on the last day-name of the month ('last Sunday of October') days_in_month = get_days_in_month (year, month); last_day_of_dst_month_num = os.date ('%w', os.time ({['year']=year, ['month']=month, ['day']=days_in_month})); return month, days_in_month + 7*(ord + 1) - ((last_day_of_dst_month_num - weekday_num) % 7); else	-- j = 7×n - 6 + (w - w1) mod 7 first_day_of_dst_month_num = os.date ('%w', os.time ({['year']=year, ['month']=month, ['day']=1})) return month, 7 * ord - 6 + (weekday_num - first_day_of_dst_month_num) % 7;		-- return month and calculated date end end

--[[--< G E T _ U T C _ O F F S E T >--

Get utc offset in hours and minutes, convert to seconds. If the offset can't be converted return nil. TODO: return error message? TODO: limit check this? +/-n hours? ]]

local function get_utc_offset local sign; local hours; local minutes; sign, hours, minutes = mw.ustring.match (tz.utc_offset, '([%+%-±−]?)(%d%d):(%d%d)');

if '-' == sign then sign = -1; else sign = 1; end if is_set (hours) and is_set (minutes) then return sign * ((hours * 3600) + (minutes * 60)); else return nil;																-- we require that all timezone table have what appears to be a valid offset end end

--[[--< M A K E _ D S T _ T I M E S T A M P S >

Return UTC timestamps for the date/time of daylight saving time events (beginning and ending). These timestamps will be compared to current UTC time. A dst timestamp is the date/time in seconds UTC for the timezone at the hour of the dst event.

For dst rules that specify local event times, the timestamp is the sum of: timestamp = current year + dst_month + dst_day + dst_time (all in seconds) local time Adjust local time to UTC by subtracting utc_offset: timestamp = timestamp - utc_offset (in seconds) For dst_end timestamp, subtract an hour for DST timestamp = timestamp - 3600 (in seconds)

For dst rules that specify utc event time the process is the same except that utc offset is not subtracted.

]]

local function make_dst_timestamps (timestamp) local dst_begin, dst_end;													-- dst begin and end time stamps local year;																	-- current year local dst_b_month, dst_e_month, dst_day;									-- month and date of dst event local dst_hour, dst_minute;													-- hour and minute of dst event on year-dst_month-dst_day local invert = false;														-- flag to pass on when dst_begin month is numerically larger than dst_end month (southern hemisphere) local utc_offset; local utc_flag;

year = os.date ('%Y', timestamp);											-- current year utc_offset = get_utc_offset ;												-- in seconds if not is_set (utc_offset) then												-- utc offset is a required timezone property return nil; end

dst_b_month, dst_day = get_dst_month_day (timestamp, true);					-- month and day that dst begins if not is_set (dst_b_month) then return nil; end dst_hour, dst_minute = tz.dst_time:match ('(%d%d):(%d%d)');					-- get dst time utc_flag = tz.dst_time:find ('[Uu][Tt][Cc]%s*$');							-- set flag when dst events occur at a specified utc time

dst_begin = os.time ({['year'] = year, ['month'] = dst_b_month, ['day'] = dst_day, ['hour'] = dst_hour, ['min'] = dst_minute});	-- form start timestamp if not is_set (utc_flag) then												-- if dst events are specified to occur at local time dst_begin = dst_begin - utc_offset;										-- adjust local time to utc by subtracting utc offset end

dst_e_month, dst_day = get_dst_month_day (timestamp, false);				-- month and day that dst ends if not is_set (dst_e_month) then return nil; end if is_set (tz.dst_e_time) then dst_hour, dst_minute = tz.dst_e_time:match ('(%d%d):(%d%d)');			-- get ending dst time; this one for those locales that use different start and end times utc_flag = tz.dst_e_time:find ('[Uu][Tt][Cc]%s*$');						-- set flag if dst is pegged to utc time end

dst_end = os.time ({['year'] = year, ['month'] = dst_e_month, ['day'] = dst_day, ['hour'] = dst_hour, ['min'] = dst_minute});	-- form end timestamp if not is_set (utc_flag) then												-- if dst events are specified to occur at local time dst_end = dst_end - 3600;												-- assume that local end time is DST so adjust to local ST		dst_end = dst_end - utc_offset;											-- adjust local time to utc by subtracting utc offset end

if dst_b_month > dst_e_month then invert = true;															-- true for southern hemisphere eg: start September YYYY end April YYYY+1 end

return dst_begin, dst_end, invert; end

--[[--< G E T _ F U L L _ U T C _ O F F S E T >---

Creates a standard UTC offset from numerical inputs, for function time to convert to a table. Expected inputs shall have the form: where: – optional; one of the characters: '+', '-' (hyphen), '±', '−' (minus); defaults to '+' - one or two digits - one of the characters '.' or ':'; required when is included; ignored else - optional; one or two digits when is '.'; two digits else

returns correct utc offset string when input has a correct form; else returns the unmodified input

]]

local function get_full_utc_offset (utc_offset) local h, m, sep, sign; local patterns = { '^([%+%-±−]?)(%d%d?)(%.)(%d+)$',										-- one or more fractional hour digits '^([%+%-±−]?)(%d%d?)(%d%d)$',										-- two minute digits '^([%+%-±−]?)(%d%d?)[%.:]?$',											-- hours only; ignore trailing separator }	for _, pattern in ipairs(patterns) do										-- loop through the patterns sign, h, sep, m = mw.ustring.match (utc_offset, pattern); if h then break;																-- if h is set then pattern matched end end

if not h then return utc_offset;														-- did not match a pattern end sign = ('' == sign) and '+' or sign;										-- sign character is required; set to '+' if not specified

m = ('.' == sep) and ((sep .. m) * 60) or m or 0;							-- fractional h to m

return string.format ('utc%s%02d:%02d', sign, h, m); end

--[[--< T A B L E _ L E N >

return number of elements in table

]]

local function table_len (tbl) local count = 0; for _ in pairs (tbl) do		count = count + 1; end return count; end

--[[--< F I R S T _ S E T >

scans through a list of parameter names that are aliases of each other and returns the value assigned to the first args[alias] that has a set value; nil else. scan direction is right-to-left (top-to-bottom)

]]

local function first_set (list, args) local i = 1; local count = table_len (list);												-- get count of items in list while i <= count do															-- loop through all items in list if is_set( args[list[i]] ) then											-- if parameter name in list is set in args return args[list[i]];												-- return the value assigned to the args parameter end i = i + 1;																-- point to next end end

--[[--< I S _ D S T _ A C T I V E >--

Return 1 of dst is active or 0 if it's not

]] local function is_dst_active (timestamp,utc_timestamp) local invert;	                                                           		-- true when southern hemisphere local dst_begin_ts, dst_end_ts;														-- DST begin and end timestamps in UTC if is_set (tz.dst_begins) and is_set (tz.dst_ends) and is_set (tz.dst_time) then	-- make sure we have all of the parts dst_begin_ts, dst_end_ts, invert = make_dst_timestamps (timestamp);	   		-- get begin and end dst timestamps and invert flag if nil == dst_begin_ts or nil == dst_end_ts then return error_msg ('bad_dst'); end if invert then														   		-- southern hemisphere; use beginning and ending of standard time in the comparison if utc_timestamp >= dst_end_ts and utc_timestamp < dst_begin_ts then		-- is current date time standard time? return 0 else return 1 end else																   		-- northern hemisphere if utc_timestamp >= dst_begin_ts and utc_timestamp < dst_end_ts then		-- all timestamps are UTC return 1 else return 0 end end elseif is_set (tz.dst_begins) or is_set (tz.dst_ends) or is_set (tz.dst_time) then	-- if some but not all not all parts then emit error message return error_msg ('bad_def', args[1]:upper); else return 0 													           		-- dst not observed for this timezone end end

--[[--< D S T _ A C T I V E >--

Invokeable version of above. This function takes one parameter. 1. the time zone abbreviation (positional or 1=)

]]

local function dst (frame) local args = getArgs (frame); local data = 'Module:Time/data'..set_sandbox(frame)                        -- make a data module name; sandbox or live data = mw.loadData (data);													-- load the data module cfg = data.cfg;																-- get the configuration table args[1] = args[1] or 'utc'                                                 -- default to utc args[1] = args[1]:lower;                                                 -- lowecase if data['tz_aliases'][args[1]] then args[1] = data['tz_aliases'][args[1]]; elseif data['dst_tz'][args[1]] then args[1] = data['dst_tz'][args[1]]; end tz = data['tz_data'][args[1]]; if not tz then return error_msg ('unknown_tz', args[1]);						    	-- if the timezone given isn't in module:time/data(/sandbox) end

local os_time = os.time local timestamp = os_time + get_utc_offset ;								-- make local time timestamp return is_dst_active(timestamp, os_time) 								   -- set is_dst to 0 or 1 end

--[=[-< T I M E >--

This function takes several parameters (some positonal, some not); none are required: 1. the time zone abbreviation/UTC offset (positional, always the first unnamed parameter) 2. a date format flag; second positional parameter or |df=; can have one of several values 3. |dst= when set to 'no' disables dst calculations for locations that do not observe dst – Arizona in MST 4. |timeonly= when set to 'yes' only display the time 5. |dateonly= when set to 'yes' only display the date 6. |hide-refresh = when set to 'yes' removes the refresh link 7. |hide-tz = when set to 'yes' removes the timezone name 8. |unlink-tz = when set to 'yes' unlinks the timzone name 9. |test_time = a specific utc time in ISO date time format used for testing this code

Timezone abbreviations can be found here: List_of_time_zone_abbreviations

]=]

local function time (frame) local args = getArgs (frame);                                              -- create the args table local utc_timestamp, timestamp;												-- current or time timestamps; timestamp is local ST or DST time used in output local tz_abbr;																-- select ST or DST timezone abbreviaion used in output local time_string;															-- holds output time/date in |df= format local utc_offset;															-- true when southern hemisphere local DF;																	-- date format flag; the |df= parameter local is_dst;                                                              -- wether or not dst is active local data = 'Module:Time/data'..set_sandbox(frame)                        -- make a data module name; sandbox or live data = mw.loadData (data);													-- load the data module cfg = data.cfg;																-- get the configuration table local formats = cfg.formats;                                               -- get the formats table local tz_aliases = {}                                                      -- create the general aliases table local dst_table = data.dst_tz;                                             -- get the dst aliases talbe local aliases_table = data.tz_aliases;										-- get the other aliases table local tz_data = data.tz_data;												-- get the tz data table for k,v in pairs(dst_table) do                                             -- add dst offsets to tz_aliases tz_aliases[k] = v 	end for k,v in pairs(aliases_table) do                                         -- add dst offsets to tz_aliases tz_aliases[k] = v 	end local Timeonly = 'yes' == first_set (cfg.aliases['timeonly'], args);		-- boolean local Dateonly = 'yes' == first_set (cfg.aliases['dateonly'], args);		-- boolean if Timeonly and Dateonly then												-- invalid condition when both are set Timeonly, Dateonly = false; end local Hide_refresh = 'yes' == first_set (cfg.aliases['hide-refresh'], args);-- boolean local Hide_tz = 'yes' == first_set (cfg.aliases['hide-tz'], args);			-- boolean local Unlink_tz = 'yes' == first_set (cfg.aliases['unlink-tz'], args);		-- boolean local DST = first_set (cfg.aliases['dst'], args);

if args[1] then args[1] = get_full_utc_offset (args[1]):lower;						-- make lower case because tz table member indexes are lower case else args[1] = 'utc';														-- default to utc end

if mw.ustring.match (args[1], 'utc[%+%-±−]%d%d:%d%d') then					-- if rendering time for a UTC offset timezone tz.abbr = args[1]:upper:gsub('%-', '−');								-- set the link label to upper case and replace hyphen with a minus character (U+2212) tz.article = tz.abbr;													-- article title same as abbreviation tz.utc_offset = mw.ustring.match (args[1], 'utc([%+%-±−]?%d%d:%d%d)'):gsub('−', '%-');	-- extract the offset value; replace minus character with hyphen local s, t = mw.ustring.match (tz.utc_offset, '(±)(%d%d:%d%d)');		-- ± only valid for offset 00:00 if s and '00:00' ~= t then return error_msg ('bad_sign'); end tz.df = 'iso'; args[1] = 'utc_offsets';												-- spoof to show that we recognize this timezone else tz = tz_aliases[args[1]] and tz_data[tz_aliases[args[1]]] or tz_data[args[1]];	-- make a local copy of the timezone table from tz_data{} if not tz then return error_msg ('unknown_tz', args[1]);							-- if the timezone given isn't in module:time/data(/sandbox) end end

DF = first_set (cfg.aliases['df'], args) or args[2] or tz.df or cfg.default_df;	-- template |df= overrides typical df from tz properties DF = DF:lower;															-- normalize to lower case

if is_set (args.test_time) or is_set (args._TEST_TIME_) then	           -- typically used to test the code at a specific utc time if is_set (args._TEST_TIME_) then args.test_time = args._TEST_TIME_ end local arg_index = {'year','month','day','hour','min','sec'}            -- ISO 8601 local arguments = {['year']=nil,['month']=nil,['day']=nil,['hour']=nil,['min']=nil,['sec']=nil} -- ISO 8601 local i = 1 for val in string.gmatch(args.test_time, "%d+") do                     -- extract values arguments[arg_index[i]] = val i = i + 1 end if not arguments.day then                                              -- required by os.time return error_msg ('test_time')                                     -- return error end utc_timestamp = os.time(arguments);                                	-- get timestamp of test_time else utc_timestamp = os.time ;												-- get current server time (UTC) end utc_offset = get_utc_offset ;												-- utc offset for specified timezone in seconds timestamp = utc_timestamp + utc_offset;										-- make local time timestamp is_dst = is_dst_active(timestamp,utc_timestamp) 							-- set is_dst to 0 or 1 if 'no' == DST or is_dst == 0 then											-- dst isn't active or not observed tz_abbr = tz.abbr;														-- use standard time elseif 'always' == DST or is_dst == 1 then									-- if dst is set to always or dst is active timestamp = timestamp + 3600;											-- add a hour for dst tz_abbr = tz.dst_abbr;													-- dst abbreviation else                                                                       -- error at is_dst_active return is_dst                                                          -- return the error end if args["dst_active"] then                                                 -- there's really no other module/palce to put it		if tz_abbr == tz.dst_abbr then return '1' else return '0' end end if Dateonly then if 'iso' == DF then														-- |df=iso DF = 'iso_date'; elseif DF:find ('^dmy') or 'y' == DF then								-- |df=dmy, |df=dmy12, |df=dmy24, |df=y DF = 'dmy_date'; else DF = 'mdy_date';													-- default end

elseif Timeonly or DF == '12' or DF == '24' then DF = table.concat ({'t', DF:match ('%l*(12)') or '24'});				-- |df=12, |df=24, |df=dmy12, |df=dmy24, |df=mdy12, |df=mdy24; default to t24 elseif 'y' == DF or 'dmy24' == DF then DF = 'dmy';

elseif 'mdy24' == DF then DF = 'mdy'; end if not formats[DF] then return error_msg ('bad_format', DF); end time_string = mw.language.getContentLanguage:formatDate (formats[DF], '@'..timestamp);

if not is_set (tz.article) then												-- if some but not all not all parts then emit error message return error_msg ('bad_def', args[1]:upper); end local refresh_link = (Hide_refresh and '') or		table.concat ({			' ',				-- open span			mw.title.getCurrentTitle:fullUrl({action = 'purge'}),				-- add the a refresh link url			' refresh ',												-- close the span			});

local tz_tag = (Hide_tz and '')                                            -- none or (Unlink_tz and ' '..tz_abbr)                                            -- unlinked or (' '..tz_abbr..'');			                       -- linked return time_string..tz_tag..refresh_link

end

--[[--< E X P O R T E D  F U N C T I O N S >--

]]

return { time = time, dst = dst, }