Module:Time

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

local p = {}

--[[--< T I M E Z O N E  P R O P E R T I E S >

When specifying utc offset do not use the minus character (U+2212) for offsets west of 0 meridian; use the minus-hyphen

]]

local tz = {																	-- these are ordered by UTC offset +HH:MM followed by -HH:MM

--< E A S T  U T C + H H : M M >--

['utc'] = { abbr = 'UTC',			-- (required) abbreviation to use during standard time dst_abbr = '',			-- abbreviation to use during daylight saving time utc_offset = '00:00',	-- (required) hours and minutes offset from UTC for this timezone; '+' is optional; '-' (hyphen-minus) is required for timezones west of 0 meridian df = 'dmy',				-- date format typically used in the time zone dst_begins = '',		-- daylight saving begins; e.g. 2nd Sunday in March; also last; empty string if not observed dst_ends = '',			-- (required if dst_begins is set) daylight saving begins; e.g. 1st Sunday in November; ignored if dst_begins not set dst_time = '',			-- (required if dst_begins is set) local time on the day that dst begins/ends; for EU DST rules specify utc time: e.g.: '01:00 UTC' dst_e_time = '',		-- local daylight saving time on the day that dst ends; only when different from dst_time; see acst for an example article = 'Coordinated Universal Time'	-- (required) name of related Wikipedia article without markup }, ['gmt'] = { abbr = 'GMT', dst_abbr = '', utc_offset = '00:00', df = 'dmy', dst_begins = '', dst_ends = '', dst_time = '', article = 'Greenwich Mean Time' }, ['gmt-uk'] = { abbr = 'GMT', dst_abbr = 'BST',															-- British Summer Time utc_offset = '00:00', df = 'dmy', dst_begins = 'last Sunday of March', dst_ends = 'last Sunday of October', dst_time = '01:00 UTC',														-- 01:00 standard time is same as 01:00 UTC article = 'Time in the United Kingdom' }, ['gmt-ie'] = {																	-- ist is shared with Indian Standard Time so use gmt-ie abbr = 'GMT',																-- winter time dst_abbr = 'IST',															-- Irish Standard Time occurs in summer utc_offset = '00:00', df = 'dmy', dst_begins = 'last Sunday of March', dst_ends = 'last Sunday of October', dst_time = '01:00 UTC',														-- 01:00 standard time is same as 01:00 UTC article = 'Time in Ireland' }, ['wet'] = { abbr = 'WET', dst_abbr = 'WEST', utc_offset = '00:00', df = 'dmy', dst_begins = 'last Sunday of March', dst_ends = 'last Sunday of October', dst_time = '01:00 UTC',														-- 01:00 standard time is same as 01:00 UTC article = 'Western European Time' }, ['cet'] = { abbr = 'CET', dst_abbr = 'CEST', utc_offset = '01:00', df = 'dmy', dst_begins = 'last Sunday of March', dst_ends = 'last Sunday of October', dst_time = '01:00 UTC',														-- 02:00 standard time is same as 01:00 UTC article = 'Central European Time' }, ['eet'] = { abbr = 'EET', dst_abbr = 'EEST', utc_offset = '02:00', df = 'dmy', dst_begins = 'last Sunday of March', dst_ends = 'last Sunday of October', dst_time = '01:00 UTC',														-- 03:00 standard time is same as 01:00 UTC article = 'Eastern European Time' }, ['usz1'] = { abbr = 'USZ1', dst_abbr = '', utc_offset = '02:00', df = 'dmy', dst_begins = '', dst_ends = '', dst_time = '', article = 'Kaliningrad Time' }, ['msk'] = { abbr = 'MSK', dst_abbr = '', utc_offset = '03:00', df = 'dmy', dst_begins = '', dst_ends = '', dst_time = '', article = 'Moscow Time' }, ['samt'] = { abbr = 'SAMT', dst_abbr = '', utc_offset = '04:00', df = 'dmy', dst_begins = '', dst_ends = '', dst_time = '', article = 'Samara Time' }, ['yekt'] = { abbr = 'YEKT', dst_abbr = '', utc_offset = '05:00', df = 'dmy', dst_begins = '', dst_ends = '', dst_time = '', article = 'Yekaterinburg Time' }, ['pkt'] = { abbr = 'PKT', dst_abbr = '', utc_offset = '05:00', df = 'dmy', dst_begins = '', dst_ends = '', dst_time = '', article = 'Pakistan Standard Time' }, ['ist'] = { abbr = 'IST', utc_offset = '05:30', df = 'dmy', dst_begins = '', dst_ends = '', dst_time = '', article = 'Indian Standard Time' }, ['npt'] = { abbr = 'NPT', dst_abbr = '', utc_offset = '05:45', df = 'dmy', dst_begins = '', dst_ends = '', dst_time = '', article = 'Nepal Time' }, ['bst'] = { abbr = 'BST', utc_offset = '06:00', df = 'dmy', dst_begins = '', dst_ends = '', dst_time = '', article = 'Bangladesh Standard Time' }, ['omst'] = { abbr = 'OMST', dst_abbr = '', utc_offset = '06:00', df = 'dmy', dst_begins = '', dst_ends = '', dst_time = '', article = 'Omsk Time' }, ['krat'] = { abbr = 'KRAT', dst_abbr = '', utc_offset = '07:00', df = 'dmy', dst_begins = '', dst_ends = '', dst_time = '', article = 'Krasnoyarsk Time' }, ['wib'] = {																		-- western indonesia abbr = 'WIB', dst_abbr = '', utc_offset = '07:00', df = 'dmy', dst_begins = '', dst_ends = '', dst_time = '', article = 'Time in Indonesia' }, ['awst'] = { abbr = 'AWST', dst_abbr = '', utc_offset = '08:00', df = 'dmy', dst_begins = '', dst_ends = '', dst_time = '', article = 'Time in Australia' }, ['bt'] = {																		-- same as China standard time (CST) abbr = 'BT', dst_abbr = '', utc_offset = '08:00', df = 'dmy', dst_begins = '', dst_ends = '', dst_time = '', article = 'Time in China' }, ['irkt'] = { abbr = 'IRKT', dst_abbr = '', utc_offset = '08:00', df = 'dmy', dst_begins = '', dst_ends = '', dst_time = '', article = 'Irkutsk Time' }, ['wita'] = {		-- central abbr = 'WITA', dst_abbr = '', utc_offset = '08:00', df = 'dmy', dst_begins = '', dst_ends = '', dst_time = '', article = 'Time in Indonesia' }, ['jst'] = { abbr = 'JST', dst_abbr = '', utc_offset = '09:00', df = 'iso', dst_begins = '', dst_ends = '', dst_time = '', article = 'Japan Standard Time' }, ['kst'] = { abbr = 'KST', dst_abbr = '', utc_offset = '09:00', df = 'iso', dst_begins = '', dst_ends = '', dst_time = '', dst_e_time = '', article = 'Time in South Korea' }, ['wit'] = {																		-- eastern Indonesia (same as old template's eit) abbr = 'WIT', dst_abbr = '', utc_offset = '09:00', df = 'dmy', dst_begins = '', dst_ends = '', dst_time = '', article = 'Time in Indonesia' }, ['yakt'] = { abbr = 'YAKT', dst_abbr = '', utc_offset = '09:00', df = 'dmy', dst_begins = '', dst_ends = '', dst_time = '', article = 'Yakutsk Time' }, ['acst'] = {																	-- Northern Territory, South Australia time abbr = 'ACST', dst_abbr = 'ACDT', utc_offset = '09:30', df = 'dmy', dst_begins = 'first Sunday in October', dst_ends = 'first Sunday in April', dst_time = '02:00', dst_e_time = '03:00', article = 'Time in Australia' }, ['aest'] = {		-- QLD, NSW, TAS, VIC, ACT abbr = 'AEST', dst_abbr = 'AEDT', utc_offset = '10:00', df = 'dmy', dst_begins = 'first Sunday in October', dst_ends = 'first Sunday in April', dst_time = '02:00',															-- begins at 02:00 AEST dst_e_time = '03:00',														-- ends at 02:00 AEST which is 03:00 AEDT article = 'Time in Australia' }, ['chst'] = { abbr = 'ChST', dst_abbr = '', utc_offset = '10:00', df = 'mdy',																	-- because a US territory? dst_begins = '', dst_ends = '', dst_time = '', article = 'Chamorro Time Zone' }, ['vlat'] = { abbr = 'VLAT', dst_abbr = '', utc_offset = '10:00', df = 'dmy', dst_begins = '', dst_ends = '', dst_time = '', article = 'Vladivostok Time' }, ['sret'] = { abbr = 'SRET', dst_abbr = '', utc_offset = '11:00', df = 'dmy', dst_begins = '', dst_ends = '', dst_time = '', article = 'Srednekolymsk Time' }, ['nzst'] = { abbr = 'NZST', dst_abbr = 'NZDT', utc_offset = '12:00', df = 'dmy', dst_begins = 'last Sunday in September', dst_ends = 'first Sunday in April', dst_time = '02:00',															-- begins at 02:00 NZST dst_e_time = '03:00',														-- ends at 02:00 NZST which is 03:00 NZDT article = 'Time in New Zealand' }, ['pett'] = { abbr = 'PETT', dst_abbr = '', utc_offset = '12:00', df = 'dmy', dst_begins = '', dst_ends = '', dst_time = '', article = 'Kamchatka Time' }, ['idle'] = {																	-- international dateline east abbr = 'IDLE', dst_abbr = '', utc_offset = '12:00', df = 'iso', dst_begins = '', dst_ends = '', dst_time = '', article = 'UTC+12:00' },

--< W E S T  U T C - H H : M M >--

['pmst'] = { abbr = 'PMST', dst_abbr = 'PMDT', utc_offset = '-03:00', df = 'dmy',																	-- because France dst_begins = '2nd Sunday in March',											-- North American rules dst_ends = '1st Sunday in November', dst_time = '02:00', article = 'UTC−03:00' }, ['wgt'] = {																		-- western greenland time? Not for Thule Airbase which observes US DST rules; requires own properties abbr = 'WGT', dst_abbr = 'WGST',															-- ??? what is the correct abbreviation? utc_offset = '-03:00', df = 'dmy', dst_begins = 'last Sunday in March', dst_ends = 'last Sunday in October', dst_time = '01:00 UTC', article = 'Time in Denmark' }, ['nst'] = { abbr = 'NST', dst_abbr = 'NDT', utc_offset = '-03:30', df = 'dmy', dst_begins = '2nd Sunday in March', dst_ends = '1st Sunday in November', dst_time = '02:00', article = 'Newfoundland Standard Time' }, ['ast'] = { abbr = 'AST', dst_abbr = 'ADT', utc_offset = '-04:00', df = 'dmy', dst_begins = '2nd Sunday in March', dst_ends = '1st Sunday in November', dst_time = '02:00', article = 'Atlantic Time Zone' }, ['est'] = { abbr = 'EST', dst_abbr = 'EDT', utc_offset = '-05:00', df = 'mdy', dst_begins = '2nd Sunday in March', dst_ends = '1st Sunday in November', dst_time = '02:00', article = 'Eastern Time Zone' }, ['cst'] = { abbr = 'CST', dst_abbr = 'CDT', utc_offset = '-06:00', df = 'mdy', dst_begins = '2nd Sunday in March', dst_ends = '1st Sunday in November', dst_time = '02:00', article = 'Central Time Zone' }, ['mst'] = { abbr = 'MST', dst_abbr = 'MDT', utc_offset = '-07:00', df = 'mdy', dst_begins = '2nd Sunday in March', dst_ends = '1st Sunday in November', dst_time = '02:00', article = 'Mountain Time Zone' }, ['pst'] = { abbr = 'PST', dst_abbr = 'PDT', utc_offset = '-08:00', df = 'mdy', dst_begins = '2nd Sunday in March', dst_ends = '1st Sunday in November', dst_time = '02:00', article = 'Pacific Time Zone' }, ['akst'] = { abbr = 'AKST', dst_abbr = 'AKDT', utc_offset = '-09:00', df = 'mdy', dst_begins = '2nd Sunday in March', dst_ends = '1st Sunday in November', dst_time = '02:00', article = 'Alaska Time Zone' }, ['hast'] = {	-- same as AleutST and HST abbr = 'HAST', dst_abbr = 'HADT', utc_offset = '-10:00', df = 'mdy', dst_begins = '2nd Sunday in March', dst_ends = '1st Sunday in November', dst_time = '02:00', article = 'Hawaii–Aleutian Time Zone' }, ['idlw'] = {																	-- international dateline west abbr = 'IDLW', dst_abbr = '', utc_offset = '-12:00', df = 'iso', dst_begins = '', dst_ends = '', dst_time = '', article = 'UTC−12:00' }, ['aoe'] = {																		-- last location for any date abbr = 'AoE', dst_abbr = '', utc_offset = '-12:00', df = 'iso', dst_begins = '', dst_ends = '', dst_time = '', article = 'Anywhere on Earth' },

--< M I L I T A R Y >-- -- --http://astro.ukho.gov.uk/nao/miscellanea/WMTZ/Wmtz160224.pdf ['a'] = {abbr = 'A', utc_offset = '01:00', df = 'iso', article = 'List of military time zones'}, ['b'] = {abbr = 'B', utc_offset = '02:00', df = 'iso', article = 'List of military time zones'}, ['c'] = {abbr = 'C', utc_offset = '03:00', df = 'iso', article = 'List of military time zones'}, ['c*'] = {abbr = 'C*', utc_offset = '03:30', df = 'iso', article = 'List of military time zones'}, ['d'] = {abbr = 'D', utc_offset = '04:00', df = 'iso', article = 'List of military time zones'}, ['d*'] = {abbr = 'D*', utc_offset = '04:30', df = 'iso', article = 'List of military time zones'}, ['e'] = {abbr = 'E', utc_offset = '05:00', df = 'iso', article = 'List of military time zones'}, ['e*'] = {abbr = 'E*', utc_offset = '05:30', df = 'iso', article = 'List of military time zones'}, ['e+'] = {abbr = 'E † ', utc_offset = '05:45', df = 'iso', article = 'List of military time zones'}, ['f'] = {abbr = 'F', utc_offset = '06:00', df = 'iso', article = 'List of military time zones'}, ['f*'] = {abbr = 'F*', utc_offset = '06:30', df = 'iso', article = 'List of military time zones'}, ['g'] = {abbr = 'G', utc_offset = '07:00', df = 'iso', article = 'List of military time zones'}, ['h'] = {abbr = 'H', utc_offset = '08:00', df = 'iso', article = 'List of military time zones'}, ['h*'] = {abbr = 'H*', utc_offset = '08:30', df = 'iso', article = 'List of military time zones'}, ['i'] = {abbr = 'I', utc_offset = '09:00', df = 'iso', article = 'List of military time zones'}, ['i*'] = {abbr = 'I*', utc_offset = '09:30', df = 'iso', article = 'List of military time zones'}, ['k'] = {abbr = 'K', utc_offset = '10:00', df = 'iso', article = 'List of military time zones'}, ['k*'] = {abbr = 'K*', utc_offset = '10:30', df = 'iso', article = 'List of military time zones'}, ['l'] = {abbr = 'L', utc_offset = '11:00', df = 'iso', article = 'List of military time zones'}, ['l*'] = {abbr = 'L*', utc_offset = '11:30', df = 'iso', article = 'List of military time zones'}, ['m'] = {abbr = 'M', utc_offset = '12:00', df = 'iso', article = 'List of military time zones'}, ['m++'] = {abbr = 'M‡', utc_offset = '12:45', df = 'iso', article = 'List of military time zones'}, ['m*'] = {abbr = 'M*', utc_offset = '13:00', df = 'iso', article = 'List of military time zones'}, ['m+'] = {abbr = 'M†', utc_offset = '14:00', df = 'iso', df = 'iso', article = 'List of military time zones'}, ['n'] = {abbr = 'N', utc_offset = '-01:00', df = 'iso', article = 'List of military time zones'}, ['o'] = {abbr = 'O', utc_offset = '-02:00', df = 'iso', article = 'List of military time zones'}, ['p'] = {abbr = 'P', utc_offset = '-03:00', df = 'iso', article = 'List of military time zones'}, ['p*'] = {abbr = 'P*', utc_offset = '-03:30', df = 'iso', article = 'List of military time zones'}, ['q'] = {abbr = 'Q', utc_offset = '-04:00', df = 'iso', article = 'List of military time zones'}, ['q*'] = {abbr = 'Q*', utc_offset = '-04:30', df = 'iso', article = 'List of military time zones'}, ['r'] = {abbr = 'R', utc_offset = '-05:00', df = 'iso', article = 'List of military time zones'}, ['s'] = {abbr = 'S', utc_offset = '-06:00', df = 'iso', article = 'List of military time zones'}, ['t'] = {abbr = 'T', utc_offset = '-07:00', df = 'iso', article = 'List of military time zones'}, ['u'] = {abbr = 'U', utc_offset = '-08:00', df = 'iso', article = 'List of military time zones'}, ['u*'] = {abbr = 'U*', utc_offset = '-08:30', df = 'iso', article = 'List of military time zones'}, ['v'] = {abbr = 'V', utc_offset = '-09:00', df = 'iso', article = 'List of military time zones'}, ['v*'] = {abbr = 'V*', utc_offset = '-09:30', df = 'iso', article = 'List of military time zones'}, ['w'] = {abbr = 'W', utc_offset = '-10:00', df = 'iso', article = 'List of military time zones'}, ['x'] = {abbr = 'X', utc_offset = '-11:00', df = 'iso', article = 'List of military time zones'}, ['y'] = {abbr = 'Y', utc_offset = '-12:00', df = 'iso', article = 'List of military time zones'}, ['z'] = {abbr = 'Z', utc_offset = '±00:00', df = 'iso', article = 'List of military time zones'},

--< U T C  O F F S E T S > -- this table entry filled by the code in p.time

['utc_offsets'] = {abbr = , utc_offset = , df = 'iso', article = ''}, }																				-- end of tz table

--[[--< 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: – 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 = {['sunday'] = 0, ['monday'] = 1, ['tuesday'] = 2, ['wednesday'] = 3, ['thursday'] = 4, ['friday'] = 5, ['saturday'] = 6}; local months = {['january'] = 1, ['february'] = 2, ['march'] = 3, ['april'] = 4, ['may'] = 5, ['june'] = 6, ['july'] = 7, ['august'] = 8, ['september'] = 9, ['october'] = 10, ['november'] = 11, ['december'] = 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, timezone, 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[timezone].dst_begins);	-- get start string and convert to digits else ord, weekday_num, month = decode_dst_event (tz[timezone].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 0. TODO: return error message? TODO: limit check this? +/-n hours? ]]

local function get_utc_offset (timezone) local sign; local hours; local minutes; sign, hours, minutes = mw.ustring.match (tz[timezone].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, timezone) 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 (timezone); 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, timezone, true);		-- month and day that dst begins if not is_set (dst_b_month) then return nil; end dst_hour, dst_minute = tz[timezone].dst_time:match ('(%d%d):(%d%d)');		-- get dst time utc_flag = tz[timezone].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, timezone, false);		-- month and day that dst ends if not is_set (dst_e_month) then return nil; end if is_set (tz[timezone].dst_e_time) then dst_hour, dst_minute = tz[timezone].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[timezone].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

--[=[-< P. 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

]=]

function p.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 tz_string;															-- output time in |df= format local utc_offset; local invert;																-- true when southern hemisphere local df;																	-- date format flag; the |df= parameter local hf;																	-- hour format flag; derived from the |df=dmy12 and |df=mdy12 parameters

if args[1] then args[1] = args[1]:lower;												-- make lower case because tz table member indexes are lower case

if mw.ustring.match (args[1], 'utc[%+%-±−]?%d%d:%d%d') then				-- if rendering time for a UTC offset timezone tz['utc_offsets'].abbr = args[1]:upper:gsub('%-', '−');			-- set the link label to upper case and replace hyphen with a minus character (U+2212) tz['utc_offsets'].article = tz['utc_offsets'].abbr;					-- article title same as abbreviation tz['utc_offsets'].utc_offset = mw.ustring.match (args[1], 'utc([%+%-±−]?%d%d:%d%d)'):gsub('−', '%-');	-- extract the offset value; replace minus character with hyphen args[1] = 'utc_offsets';											-- point to the generic utc offsets table end if not is_set (tz[args[1]]) then return ' July 27, 2024 – unknown timezone (help) '; end else args[1] = 'utc';														-- default to utc end df = args.df or args[2] or tz[args[1]].df or '';							-- 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

hf = df:match ('%l%ly(12)');												-- hf == '12' selects 12-hour AM/PM display; other values ignored and 24 hour clock time displayed if is_set (hf) then df = df:match ('(%l%ly)12');											-- extract df	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 ' July 27, 2024 – malformed or incomplete _TEST_TIME_ (help) '; 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 (args[1]);										-- utc offset for specified timezone 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[args[1]].abbr;												-- ... dst is not observed (|dst=no) show time as standard time else if is_set (tz[args[1]].dst_begins) and is_set (tz[args[1]].dst_ends) and is_set (tz[args[1]].dst_time) then		-- make sure we have all of the parts dst_begin_ts, dst_end_ts, invert = make_dst_timestamps (timestamp, args[1]);	-- get begin and end dst timestamps and invert flag

if nil == dst_begin_ts or nil == dst_end_ts then return ' July 27, 2024 – error calculating dst timestamps (help) '; 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[args[1]].abbr;									-- standard time abbreviation else timestamp = timestamp + 3600;								-- add an hour for tz_abbr = tz[args[1]].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[args[1]].dst_abbr; else tz_abbr = tz[args[1]].abbr; end end elseif is_set (tz[args[1]].dst_begins) or is_set (tz[args[1]].dst_ends) or is_set (tz[args[1]].dst_time) then	-- if some but not all not all parts then emit error message return ' July 27, 2024 – incomplete definition for ' .. args[1]:upper .. ' (help) '; else tz_abbr = tz[args[1]].abbr;											-- dst not observed for this timezone end end if 'y' == df or 'dmy' == df then											-- format the output (|df=y is legacy from original template) if '12' == hf then tz_string = mw.text.trim (os.date ('%l:%M %p, %e %B %Y', timestamp));	-- dmy, 12 hour am/pm else tz_string = os.date ('%R, %e %B %Y', timestamp);					-- dmy end elseif 'iso' == df then tz_string = os.date ('%FT%R', timestamp);								-- iso else if '12' == hf then tz_string = mw.text.trim (os.date ('%l:%M %p, %B %e, %Y', timestamp));	-- mdy (legacy default) else tz_string = os.date ('%R, %B %e, %Y', timestamp);					-- mdy (legacy default) end end if not is_set (tz[args[1]].article) then									-- if some but not all not all parts then emit error message return ' July 27, 2024 – incomplete definition for ' .. args[1]:upper .. ' (help) '; end

local refreshLink = mw.title.getCurrentTitle:fullUrl{action = 'purge'}	-- create a refresh link return string.format ('%s %s %s refresh ', tz_string, tz[args[1]].article, tz_abbr, refreshLink); end

return p;