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 _ T E S T _ T I M E >----------------------------------------------------  
decode ISO formatted date/time into a table suitable for os.time. Fallback to format.
For testing, this time is UTC just as is returned by the os.time function.  
local function get_test_time (iso_date)
local year, month, day, hour, minute, second;  
year, month, day, hour, minute, second = iso_date:match ('(%d%d%d%d)%-(%d%d)%-(%d%d)T(%d%d):(%d%d):(%d%d)');
if not year then
year, month, day, hour, minute, second = iso_date:match ('^(%d%d%d%d)(%d%d)(%d%d)(%d%d)(%d%d)(%d%d)$');
if not year then
return nil; -- test time did not match the specified patterns
end
end
return {['year'] = year, ['month'] = month, ['day'] = day, ['hour'] = hour, ['min'] = minute, ['sec'] = second};
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:
<sign><hour><separator><portion>
where:
<sign> – optional; one of the characters: '+', '-' (hyphen), '±', '−' (minus); defaults to '+'
<hour> - one or two digits
<separator> - one of the characters '.' or ':'; required when <portion> is included; ignored else
<portion> - optional; one or two digits when <separator> 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%d?)$', -- one or two 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 -to- (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  
--[=[-------------------------< T I M E >----------------------------------------------------------------------  
This template takes several parameters (some positonal, some not); 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  
TODO: convert _TEST_TIME_ to |time=?  
Timezone abbreviations can be found here: List_of_time_zone_abbreviations  
For custom date format parameters |df-cust=, |df-cust-a=, |df-cust-p= use codes
described here: :mw:Help:Extension:ParserFunctions##time  
]=]  
local function time
local args = getArgs ;
local utc_timestamp, timestamp; -- current or _TEST_TIME_ timestamps; timestamp is local ST or DST time used in output
local dst_begin_ts, dst_end_ts; -- DST begin and end timestamps in UTC
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;
local invert; -- true when southern hemisphere
local DF; -- date format flag; the |df= parameter
local is_dst_tz;  
local data = table.concat ({'Module:Time/data', :getTitle:find('sandbox', 1, true) and '/sandbox' or ''}); -- make a data module name; sandbox or live
data = mw.loadData (data); -- load the data module
cfg = data.cfg; -- get the configuration table
local tz_aliases = data.tz_aliases; -- get the aliases table
local tz_data = data.tz_data; -- get the tz data table  
local Timeonly = yesno(first_set (cfg.aliases['timeonly'], args)); -- boolean
local Dateonly = yesno(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 = yesno(first_set (cfg.aliases['hide-refresh'], args)); -- boolean
local Hide_tz = yesno(first_set (cfg.aliases['hide-tz'], args)); -- boolean
local Unlink_tz = yesno(first_set (cfg.aliases['unlink-tz'], args)); -- boolean
local DST = first_set (cfg.aliases['dst'], args) or true; -- string 'always' or boolean  
local Lang = first_set (cfg.aliases['lang'], args); -- to render in a language other than the local wiki's language  
local DF_cust = first_set (cfg.aliases['df-cust'], args); -- custom date/time formats  
local DF_cust_a = first_set (cfg.aliases['df-cust-a'], args); -- for am/pm sensitive formats
local DF_cust_p = first_set (cfg.aliases['df-cust-p'], args);  
if not ((DF_cust_a and DF_cust_p) or -- DF_cust_a xor DF_cust_p
(not DF_cust_a and not DF_cust_p))then
return error_msg ('bad_df_pair'); -- both are required
end  
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 not cfg.df_vals[DF] then
return error_msg ('bad_format', DF);
end