Module:Time
Revision as of 10:51, 15 August 2018 by blackwiki>Trappist the monk (+sst;)
| This module is subject to page protection. It is a highly visible module in use by a very large number of pages, or is substituted very frequently. Because vandalism or mistakes would affect many pages, and even trivial editing might cause substantial load on the servers, it is protected from editing. |
Implements {{time}}. See also {{time/sandbox}} and Template:Time/testcases.
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 ends; 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'
},
['sst'] = {
abbr = 'SST',
dst_abbr = '',
utc_offset = '08:00',
df = 'dmy',
dst_begins = '',
dst_ends = '',
dst_time = '',
article = 'Singapore Standard 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 >--------------------------------------------------------------
-- {{cite web |url=http://wwp.greenwichmeantime.com/info/timezone.htm |title=Military and Civilian Time Designations |accessdate=2016-03-25 |website=Greenwich Mean Time}}
--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', 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:
<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 = {['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 '<span style="font-size:100%" class="error">{{time}} – unknown timezone ([[Template:Time#Error messages|help]])</span>';
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 '<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 (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 '<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[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 '<span style="font-size:100%" class="error">{{time}} – incomplete definition for ' .. args[1]:upper() .. ' ([[Template:Time#Error messages|help]])</span>';
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 '<span style="font-size:100%" class="error">{{time}} – incomplete definition for ' .. args[1]:upper() .. ' ([[Template:Time#Error messages|help]])</span>';
end
local refreshLink = mw.title.getCurrentTitle():fullUrl{action = 'purge'} -- create a refresh link
return string.format ('%s [[%s|%s]] <span class="plainlinks" style="font-size:80%%;">[[%s refresh]]</span>', tz_string, tz[args[1]].article, tz_abbr, refreshLink);
end
return p;