require('strict')
local yesno = require('Module:Yesno')
local getArgs = require ('Module:Arguments').getArgs  
local tz = {}; -- holds local copy of the specified timezone table from tz_data{}
local cfg = {}; -- for internationalization  
----------------------------< 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:
<ordinal> <day> <any single word> <month> – 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 tables 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