{{time}} supports two positional and several named parameters; none are required:

|<time zone> – the first positional (unnamed) parameter, specifies the time zone for which the template is to provide a time output. If omitted, the template displays UTC time. A time zone is identified by an abbreviation of the time zone's standard-time name. Do not use a summertime or daylight saving time abbreviation; they will be ignored and produce an error message.
|<df> – the positional (unnamed) version of |df= which see
|df= – date format; this parameter takes one of several values; values other than these are ignored:
|df=dmy – specifies day month year date format; time in 24-hour format; alias: |df=dmy24
|df=dmy12 – same as |df=dmy except time in 12-hour AM/PM format
|df=mdy – specifies month day, year format; time in 24-hour format; default when a date format is not specified in the time zone's properties; alias: |df=mdy24
|df=mdy12 – same as |df=mdy except time in 12-hour AM/PM format
|df=iso – renders the date/time in a form roughly adhering to the ISO 8601 format (seconds omitted)
|df=y – legacy {{time}} parameter, same as |df=dmy
|df=12 – time-only display 12-hour AM/PM format
|df=24 – time-only display 24-hour format
|dst= – when set to no, disables the daylight saving time calculation for the time zone; other values ignored; useful for locations within a time zone that do not observe daylight saving time; Arizona, for example
|dateonly= – suppresses time display
|timeonly= – suppresses date display
|hide-refresh= – suppresses the refresh link
|hide-tz= – suppresses the timezone abbreviation
|unlink-tz= – renders unlinked timezone abbreviation
|_TEST_TIME_= – a parameter that was useful during the development of the template's code. The value assigned to this parameter must be in ISO 8601 format without time zone designator and is interpreted by the template as UTC


Template:Time

Template:Time/doc


require ('Module:No globals')
local getArgs = require ('Module:Arguments').getArgs

local tz = {};																	-- holds local copy of the specified timezone table from tz_data{} 


--[[--------------------------< 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 (var == nil or var == '');
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:
	<ordinal> <day> <any single word> <month> – all are separated by spaces

]]

local function decode_dst_event (dst_event_string)
	local ord, day, month;
	
	local ordinals = {['1st'] = 1, ['first'] = 1, ['2nd'] = 2, ['second'] = 2, ['3rd'] = 3, ['third'] = 3, ['4th'] = 4, ['fourth'] = 4, ['5th'] = 5, ['fifth'] = 5, ['last'] = -1};
	local days = {['ᱥᱤᱸᱜᱮ ᱢᱟᱦᱟᱸ'] = 0, ['ᱚᱛᱮ ᱢᱟᱦᱟᱸ'] = 1, ['ᱵᱟᱞᱮ ᱢᱟᱦᱟᱸ'] = 2, ['ᱥᱟᱹᱜᱩᱱ ᱢᱟᱦᱟᱸ'] = 3, ['ᱥᱟᱹᱨᱫᱤ ᱢᱟᱦᱟᱸ'] = 4, ['ᱡᱟᱹᱨᱩᱢ ᱢᱟᱦᱟᱸ'] = 5, ['ᱧᱩᱦᱩᱢ ᱢᱟᱦᱟᱸ'] = 6};
	local months = {['ᱡᱟᱱᱩᱣᱟᱨᱤ'] = 1, ['ᱯᱷᱮᱵᱽᱨᱩᱣᱟᱨᱤ'] = 2, ['ᱢᱟᱨᱪ'] = 3, ['ᱮᱯᱨᱤᱞl'] = 4, ['ᱢᱮ'] = 5, ['ᱡᱩᱱ'] = 6,
		['ᱡᱩᱞᱟᱭ'] = 7, ['ᱚᱜᱚᱥᱴ'] = 8, ['ᱥᱮᱯᱴᱮᱢᱵᱚᱨ'] = 9, ['ᱚᱠᱴᱚᱵᱚᱨ'] = 10, ['ᱱᱚᱵᱷᱮᱢᱵᱚᱨ'] = 11, ['ᱰᱤᱥᱮᱢᱵᱚᱨ'] = 12};
	
	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 ordinals[ord], days[day], 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 _ T E S T _ T I M E >----------------------------------------------------

decode ISO formatted date/time into a table suitable for os.time().  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
		return nil;																-- test time did not match the specified pattern
	end
	return {['year'] = year, ['month'] = month, ['day'] = day, ['hour'] = hour, ['min'] = minute, ['sec'] = second};
end


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

This template takes several parameters; none are required:
	1. the time zone abbreviation (positional, always the first unnamed parameter)
	2. a date format flag; second positional parameter or |df=; can have one of several assigned values:
		y – display output time in dmy format
		dmy – same as 'y'
		mdy – default; included for completeness
		iso – display output time in YYYY-MM-DDTHH:mm format
	3. |dst= when set to 'no' disables dst calculations for locations that do not observe dst – Arizona in MST
	4. |_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]]

]=]

local function time (frame)
	local args = getArgs (frame);
	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 timeonly = 'yes' == args.timeonly;									-- boolean
	local dateonly = 'yes' == args.dateonly;									-- boolean
	local hide_refresh = 'yes' == args['hide-refresh'];							-- boolean
	local hide_tz = 'yes' == args['hide-tz'];									-- boolean
	local unlink_tz = 'yes' == args['unlink-tz'];								-- boolean

	if timeonly and dateonly then												-- invalid condition when both are set
		timeonly, dateonly = false;
	end
	
	local tz_data = table.concat ({'Module:Time/data', frame:getTitle():find('sandbox', 1, true) and '/sandbox' or ''}); -- make a data module name; sandbox or live
	tz_data =  mw.loadData (tz_data).tz_data;									-- load the data table

	if args[1] then
		args[1] = 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
		tz.df = 'iso';
		args[1] = 'utc_offsets';												-- spoof to show that we recognize this timezone
	else
		tz = tz_data[args[1]];													-- make a local copy of the timezone table from tz_data{}
	end

	if not is_set (tz_data[args[1]]) then
		return '<span style="font-size:100%" class="error">{{time}} – unknown timezone ([[Template:Time#Error messages|help]])</span>';
	end
	
	df = args.df or args[2] or tz.df or 'mdy';									-- template |df= overrides typical df from tz properties TODO: error check these values?
	if is_set (df) then
		df = df:lower();														-- lower case because we will compare to lower case values later
	end

	if is_set (args._TEST_TIME_) then											-- typically used to test the code at a specific utc time
		local test_time = get_test_time (args._TEST_TIME_);
		if not test_time then
			return '<span style="font-size:100%" class="error">{{time}} – malformed or incomplete _TEST_TIME_ ([[Template:Time#Error messages|help]])</span>';
		end

--		utc_timestamp = os.time(get_test_time (args._TEST_TIME_));
		utc_timestamp = os.time(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

	if 'no' == args.dst then													-- for timezones that DO observe dst but for this location ...
		tz_abbr = tz.abbr;														-- ... dst is not observed (|dst=no) show time as standard time
	else
		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 '<span style="font-size:100%" class="error">{{time}} – error calculating dst timestamps ([[Template:Time#Error messages|help]])</span>';
			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?
					tz_abbr = tz.abbr;											-- standard time abbreviation
				else		
					timestamp = timestamp + 3600;								-- add an hour
					tz_abbr = tz.dst_abbr;										-- dst abbreviation
				end
			else																-- northern hemisphere
				if utc_timestamp >= dst_begin_ts and utc_timestamp < dst_end_ts then	-- all timestamps are UTC
					timestamp = timestamp + 3600;								-- add an hour 
					tz_abbr = tz.dst_abbr;
				else
					tz_abbr = tz.abbr;
				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 table.concat ({'<span style="font-size:100%" class="error">{{time}} – incomplete definition for ', args[1]:upper(), ' ([[Template:Time#Error messages|help]])</span>'});
		else
			tz_abbr = tz.abbr;													-- dst not observed for this timezone
		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:match ('^%d+$') then									-- time only of |df= is just digits
		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
	
	local format = {
		t12 = '%l:%M %p',														-- time only
		t24 = '%R',
		
		iso_date ='%F',															-- date only
		dmy_date = '%e %B %Y',
		mdy_date = '%B %e, %Y',
		
		dmy12 = '%l:%M %p, %e %B %Y',											-- 12hr time and date
		mdy12 = '%l:%M %p, %B %e, %Y',
		dmy = '%R, %e %B %Y',													-- 24hr time and date
		mdy = '%R, %B %e, %Y',
		iso = '%FT%R'
		}
	
	if format[df] then
		time_string = mw.text.trim (os.date (format[df], timestamp));
	else
		return table.concat ({'<span style="font-size:100%" class="error">{{time}} – invalid date format ', df, ' ([[Template:Time#Error messages|help]])</span>'});
	end
	
	if not is_set (tz.article) then												-- if some but not all not all parts then emit error message
		return table.concat ({'<span style="font-size:100%" class="error">{{time}} – incomplete definition for ', args[1]:upper(), ' ([[Template:Time#Error messages|help]])</span>'});
	end
	
	local refresh_link = (hide_refresh and '') or
		table.concat ({
			' <span class="plainlinks" style="font-size:85%;">[[',				-- open span
			mw.title.getCurrentTitle():fullUrl({action = 'purge'}),				-- add the a refresh link url
			' refresh]]</span>',												-- close the span
			});

	local tz_tag = (hide_tz and '') or
		((unlink_tz and table.concat ({' ', tz_abbr})) or						-- unlinked
			table.concat ({' [[', tz.article, '|', tz_abbr, ']]'}));			-- linked
	
	return table.concat ({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}