Lompat ke isi

Modul:Time: Perbedaan antara revisi

Dari Wikipedia bahasa Indonesia, ensiklopedia bebas
Konten dihapus Konten ditambahkan
RusdianaDablang (bicara | kontrib)
←Membuat halaman berisi '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 >...'
 
Farras (bicara | kontrib)
k update
Baris 1: Baris 1:
require('Module:No globals')
require ('Module:No globals')
local getArgs = require('Module:Arguments').getArgs
local getArgs = require ('Module:Arguments').getArgs


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


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


--[[--------------------------< I S _ S E T >------------------------------------------------------------------
When specifying utc offset do not use the minus character (U+2212) for offsets west of 0 meridian; use the minus-hyphen


Whether variable is set or not. A variable is set when it is not nil and not empty.


]]
]]


local function is_set( var )
local tz = { -- these are ordered by UTC offset +HH:MM followed by -HH:MM
return not (nil == var or '' == var);
end


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


--[[--------------------------< S U B S T I T U T E >----------------------------------------------------------
['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'
},


Populates numbered arguments in a message string using an argument table.


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


local function substitute (msg, args)
['pmst'] = {
return args and mw.message.newRawMessage (msg, args):plain() or msg;
abbr = 'PMST',
end
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<span class="plainlinks" style="font-size:120%%;">†</span>', 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 >--------------------------------------------------------
--[[--------------------------< E R R O R _ M S G >------------------------------------------------------------
-- this table entry filled by the code in p.time()


create an error message
['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 )
local function error_msg (msg, arg)
return substitute (cfg.err_msg, substitute (cfg.err_text[msg], arg))
return not (var == nil or var == '');
end
end


Baris 559: Baris 53:
local function decode_dst_event (dst_event_string)
local function decode_dst_event (dst_event_string)
local ord, day, month;
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
dst_event_string = dst_event_string:lower(); -- force the string to lower case because that is how the tables above are indexed
Baris 572: Baris 61:
end
end
return ordinals[ord], days[day], months[month];
return cfg.ordinals[ord], cfg.days[day], cfg.months[month];
end
end


Baris 610: Baris 99:
]]
]]


local function get_dst_month_day (timestamp, timezone, start)
local function get_dst_month_day (timestamp, start)
local ord, weekday_num, month;
local ord, weekday_num, month;
local first_day_of_dst_month_num;
local first_day_of_dst_month_num;
Baris 618: Baris 107:


if true == start then
if true == start then
ord, weekday_num, month = decode_dst_event (tz[timezone].dst_begins); -- get start string and convert to digits
ord, weekday_num, month = decode_dst_event (tz.dst_begins); -- get start string and convert to digits
else
else
ord, weekday_num, month = decode_dst_event (tz[timezone].dst_ends); -- get end string and convert to digits
ord, weekday_num, month = decode_dst_event (tz.dst_ends); -- get end string and convert to digits
end
end
Baris 631: Baris 120:
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')
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);
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}));
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);
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
else -- j = 7×n - 6 + (w - w1) mod 7
Baris 642: Baris 131:
--[[--------------------------< G E T _ U T C _ O F F S E T >--------------------------------------------------
--[[--------------------------< 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.
Get utc offset in hours and minutes, convert to seconds. If the offset can't be converted return nil.
TODO: return error message?
TODO: return error message?
TODO: limit check this? +/-n hours?
TODO: limit check this? +/-n hours?
]]
]]


local function get_utc_offset (timezone)
local function get_utc_offset ()
local sign;
local sign;
local hours;
local hours;
local minutes;
local minutes;
sign, hours, minutes = mw.ustring.match (tz[timezone].utc_offset, '([%+%-±−]?)(%d%d):(%d%d)');
sign, hours, minutes = mw.ustring.match (tz.utc_offset, '([%+%-±−]?)(%d%d):(%d%d)');

if '-' == sign then sign = -1; else sign = 1; end
if '-' == sign then sign = -1; else sign = 1; end
if is_set (hours) and is_set (minutes) then
if is_set (hours) and is_set (minutes) then
return sign * ((hours * 3600) + (minutes * 60));
return sign * ((hours * 3600) + (minutes * 60));
else
else
return nil; -- we require that all timezone table have what appears to be a valid offset
return nil; -- we require that all timezone tables have what appears to be a valid offset
end
end
end
end
Baris 677: Baris 166:


For dst rules that specify utc event time the process is the same except that utc offset is not subtracted.
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 function make_dst_timestamps (timestamp)
local dst_begin, dst_end; -- dst begin and end time stamps
local dst_begin, dst_end; -- dst begin and end time stamps
local year; -- current year
local year; -- current year
Baris 691: Baris 179:


year = os.date ('%Y', timestamp); -- current year
year = os.date ('%Y', timestamp); -- current year
utc_offset = get_utc_offset (timezone);
utc_offset = get_utc_offset (); -- in seconds
if not is_set (utc_offset) then -- utc offset is a required timezone property
if not is_set (utc_offset) then -- utc offset is a required timezone property
return nil;
return nil;
end
end


dst_b_month, dst_day = get_dst_month_day (timestamp, timezone, true); -- month and day that dst begins
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
if not is_set (dst_b_month) then
return nil;
return nil;
end
end
dst_hour, dst_minute = tz[timezone].dst_time:match ('(%d%d):(%d%d)'); -- get dst time
dst_hour, dst_minute = tz.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
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
dst_begin = os.time ({['year'] = year, ['month'] = dst_b_month, ['day'] = dst_day, ['hour'] = dst_hour, ['min'] = dst_minute}); -- form start timestamp
Baris 709: Baris 197:
end
end


dst_e_month, dst_day = get_dst_month_day (timestamp, timezone, false); -- month and day that dst ends
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
if not is_set (dst_e_month) then
return nil;
return nil;
end
end
if is_set (tz[timezone].dst_e_time) then
if is_set (tz.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
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[timezone].dst_e_time:find ('[Uu][Tt][Cc]%s*$'); -- set flag if dst is pegged to utc time
utc_flag = tz.dst_e_time:find ('[Uu][Tt][Cc]%s*$'); -- set flag if dst is pegged to utc time
end
end


Baris 744: Baris 232:
local year, month, day, hour, minute, second;
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)');
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
if not year then
return nil; -- test time did not match the specified pattern
return nil; -- test time did not match the specified pattern
Baris 751: Baris 239:
end
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:
--[=[-------------------------< P . T I M E >------------------------------------------------------------------
<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
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
local function get_full_utc_offset (utc_offset)
dmy – same as 'y'
local h, m, sep, sign;
mdy – default; included for completeness
iso – display output time in YYYY-MM-DDTHH:mm format
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 right-to-left (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); none 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
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
4. |_TEST_TIME_= a specific utc time in ISO date time format used for testing this code
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=?
TODO: convert _TEST_TIME_ to |time=?


Timezone abbreviations can be found here: [[List_of_time_zone_abbreviations]]
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]]


]=]
]=]


function p.time (frame)
local function time (frame)
local args = getArgs(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 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 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_abbr; -- select ST or DST timezone abbreviaion used in output
local tz_string; -- output time in |df= format
local time_string; -- holds output time/date in |df= format
local utc_offset;
local utc_offset;
local invert; -- true when southern hemisphere
local invert; -- true when southern hemisphere
local df; -- date format flag; the |df= parameter
local DF; -- date format flag; the |df= parameter
local is_dst_tz;

local data = table.concat ({'Module:Time/data', frame: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 = 'yes' == first_set (cfg.aliases['timeonly'], args); -- boolean
local Dateonly = 'yes' == 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 = 'yes' == first_set (cfg.aliases['hide-refresh'], args);-- boolean
local Hide_tz = 'yes' == first_set (cfg.aliases['hide-tz'], args); -- boolean
local Unlink_tz = 'yes' == first_set (cfg.aliases['unlink-tz'], args); -- boolean
local DST = first_set (cfg.aliases['dst'], args);
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
if args[1] then
args[1] = args[1]:lower(); -- make lower case because tz table member indexes are lower case
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
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.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.article = tz.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
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
args[1] = 'utc_offsets'; -- point to the generic utc offsets table
if s and '00:00' ~= t then
end
return error_msg ('bad_sign');
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
end
tz.df = 'iso';
args[1] = 'utc_offsets'; -- spoof to show that we recognize this timezone
else
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{}
args[1] = 'utc'; -- default to utc
if not tz then
return error_msg ('unknown_tz', args[1]); -- if the timezone given isn't in module:time/data(/sandbox)
end
end
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?
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 is_set (df) then
if not cfg.df_vals[DF] then
df = df:lower(); -- lower case because we will compare to lower case values later
return error_msg ('bad_format', DF);
end
end


Baris 804: Baris 410:
local test_time = get_test_time (args._TEST_TIME_);
local test_time = get_test_time (args._TEST_TIME_);
if not test_time then
if not test_time then
return error_msg ('test_time');
return '<span style="font-size:100%" class="error">{{time}} – malformed or incomplete _TEST_TIME_ ([[Template:Time#Error messages|help]])</span>';
end
end


-- utc_timestamp = os.time(get_test_time (args._TEST_TIME_));
utc_timestamp = os.time(test_time);
utc_timestamp = os.time(test_time);
else
else
utc_timestamp = os.time (); -- get current server time (UTC)
utc_timestamp = os.time (); -- get current server time (UTC)
end
end
utc_offset = get_utc_offset (args[1]); -- utc offset for specified timezone
utc_offset = get_utc_offset (); -- utc offset for specified timezone in seconds
timestamp = utc_timestamp + utc_offset; -- make local time timestamp
timestamp = utc_timestamp + utc_offset; -- make local time timestamp


if 'no' == args.dst then -- for timezones that DO observe dst but for this location ...
if 'no' == 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
tz_abbr = tz.abbr; -- ... dst is not observed (|dst=no) show time as standard time
elseif 'always' == DST then -- if needed to always display dst time
timestamp = timestamp + 3600; -- add a hour for dst
tz_abbr = tz.dst_abbr; -- dst abbreviation
else
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
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, args[1]); -- get begin and end dst timestamps and invert flag
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
if nil == dst_begin_ts or nil == dst_end_ts then
return error_msg ('bad_dst');
return '<span style="font-size:100%" class="error">{{time}} – error calculating dst timestamps ([[Template:Time#Error messages|help]])</span>';
end
end
if invert then -- southern hemisphere; use beginning and ending of standard time in the comparison
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?
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
tz_abbr = tz.abbr; -- standard time abbreviation
else
else
timestamp = timestamp + 3600; -- add an hour for
timestamp = timestamp + 3600; -- add an hour
tz_abbr = tz[args[1]].dst_abbr; -- dst abbreviation
tz_abbr = tz.dst_abbr; -- dst abbreviation
end
end
else -- northern hemisphere
else -- northern hemisphere
if utc_timestamp >= dst_begin_ts and utc_timestamp < dst_end_ts then -- all timestamps are UTC
if utc_timestamp >= dst_begin_ts and utc_timestamp < dst_end_ts then -- all timestamps are UTC
timestamp = timestamp + 3600; -- add an hour
timestamp = timestamp + 3600; -- add an hour
tz_abbr = tz[args[1]].dst_abbr;
tz_abbr = tz.dst_abbr;
else
else
tz_abbr = tz[args[1]].abbr;
tz_abbr = tz.abbr;
end
end
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
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 error_msg ('bad_def', args[1]:upper());
return '<span style="font-size:100%" class="error">{{time}} – incomplete definition for ' .. args[1]:upper() .. ' ([[Template:Time#Error messages|help]])</span>';
else
else
tz_abbr = tz[args[1]].abbr; -- dst not observed for this timezone
tz_abbr = tz.abbr; -- dst not observed for this timezone
end
end
end
end
if Dateonly then
if 'y' == df or 'dmy' == df then -- format the output (|df=y is legacy from original template)
tz_string = os.date ('%R, %e %B %Y', timestamp); -- dmy
if 'iso' == DF then -- |df=iso
DF = 'iso_date';
elseif 'iso' == df then
elseif DF:find ('^dmy') or 'y' == DF then -- |df=dmy, |df=dmy12, |df=dmy24, |df=y
tz_string = os.date ('%FT%R', timestamp); -- iso
DF = 'dmy_date';
else
else
tz_string = os.date ('%R, %B %e, %Y', timestamp); -- mdy (legacy default)
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
end
local dformat;
if not is_set (tz[args[1]].article) then -- if some but not all not all parts then emit error message
if is_set (DF_cust) then
return '<span style="font-size:100%" class="error">{{time}} – incomplete definition for ' .. args[1]:upper() .. ' ([[Template:Time#Error messages|help]])</span>';
dformat=DF_cust;
elseif is_set (DF_cust_a) then -- custom format is am/pm sensitive?
if 'am' == os.date ('%P', timestamp) then -- if current time is am
dformat = DF_cust_a; -- use custom am format
else
dformat = DF_cust_p; -- use custom pm format
end
else
dformat = cfg.format[DF]; -- use format from tables or from |df=
end
end


time_string = frame:callParserFunction ({name='#time', args={dformat, '@'..timestamp, Lang}});
local refreshLink = mw.title.getCurrentTitle():fullUrl{action = 'purge'} -- create a refresh link
if Lang then
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);
time_string = table.concat ({ -- bidirectional isolation of non-local language; yeah, rather brute force but simple
'<bdi lang="', -- start of opening bdi tag
Lang, -- insert rendered language code
'">', -- end of opening tag
time_string, -- insert the time string
'</bdi>' -- and close the tag
});
end

if not is_set (tz.article) then -- if some but not all not all parts then emit error message
return error_msg ('bad_def', args[1]:upper());
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
end



return p;
--[[--------------------------< E X P O R T E D F U N C T I O N S >------------------------------------------
]]

return {time = time}

Revisi per 11 Januari 2019 14.57

require ('Module:No globals')
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
		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

--[[----------------------< 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 right-to-left (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); none 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 (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 is_dst_tz;

	local data = table.concat ({'Module:Time/data', frame: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 = 'yes' == first_set (cfg.aliases['timeonly'], args);		-- boolean
	local Dateonly = 'yes' == 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 = 'yes' == first_set (cfg.aliases['hide-refresh'], args);-- boolean
	local Hide_tz = 'yes' == first_set (cfg.aliases['hide-tz'], args);			-- boolean
	local Unlink_tz = 'yes' == first_set (cfg.aliases['unlink-tz'], args);		-- boolean
	local DST = first_set (cfg.aliases['dst'], args);
	
	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

	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 error_msg ('test_time');
		end

		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' == 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
	elseif 'always' == DST then													-- if needed to always display dst time
		timestamp = timestamp + 3600;											-- add a hour for dst
		tz_abbr = tz.dst_abbr;													-- dst abbreviation
	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 error_msg ('bad_dst');
			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 error_msg ('bad_def', args[1]:upper());
		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 dformat;
	if is_set (DF_cust) then
		dformat=DF_cust;
	elseif is_set (DF_cust_a) then												-- custom format is am/pm sensitive?
		if 'am' == os.date ('%P', timestamp) then								-- if current time is am
			dformat = DF_cust_a;												-- use custom am format
		else
			dformat = DF_cust_p;												-- use custom pm format
		end
	else
		dformat = cfg.format[DF];												-- use format from tables or from |df=
	end

	time_string = frame:callParserFunction ({name='#time', args={dformat, '@'..timestamp, Lang}});
	if Lang then
		time_string = table.concat ({											-- bidirectional isolation of non-local language; yeah, rather brute force but simple
			'<bdi lang="',														-- start of opening bdi tag
			Lang,																-- insert rendered language code
			'">',																-- end of opening tag
			time_string,														-- insert the time string
			'</bdi>'															-- and close the tag
			});
	end

	if not is_set (tz.article) then												-- if some but not all not all parts then emit error message
		return error_msg ('bad_def', args[1]:upper());
	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}