Module:Time/sandbox: Difference between revisions

Content deleted Content added
No edit summary
No edit summary
 
(18 intermediate revisions by 5 users not shown)
Line 1:
require ('Module:No globalsstrict')
local yesno = require('Module:Yesno')
local getArgs = require ('Module:Arguments').getArgs
local data = mw.loadData ('Module:Time/data' .. (mw.getCurrentFrame():getTitle():match ('/sandbox') or '')); -- load the data module
local cfg = data.cfg; -- for internationalization
 
local tz = {}; -- holds local copy of the specified timezone table from tz_data{}
local cfg = {}; -- for internationalization
 
--[[-------------------------< S E T _ S A N D B O X >--------------------------------------------------------
 
Weather or not to use sandbox version of data (returns /sandbox or '')
 
]]
 
local function set_sandbox (frame)
local data_sandbox;
if frame:getTitle():find('sandbox', 1, true) then
data_sandbox = true; -- bolean to toggle usage of sandbox data module when this module is in a sandbox (changeable)
else
data_sandbox = false; -- DO NOT CHANGE
end
if data_sandbox then
return '/sandbox'
else
return ''
end
end
 
--[[--------------------------< I S _ S E T >------------------------------------------------------------------
Line 44 ⟶ 25:
]]
 
local function substitute (msg, argsargs_t)
return argsargs_t and mw.message.newRawMessage (msg, argsargs_t):plain() or msg;
end
 
Line 52 ⟶ 33:
 
create an error message
<args_t> is a sequence where [1] is template name and [2] is substituted error message
 
]]
 
local function error_msg (msg, argargs_t)
return substitute (cfg.err_msg, substitute (cfg.err_text[msg], arg)args_t);
end
 
Line 168 ⟶ 149:
return sign * ((hours * 3600) + (minutes * 60));
else
return nil; -- we require that all timezone tabletables have what appears to be a valid offset
end
end
Line 240 ⟶ 221:
 
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(). Fallback to {{Timestamp}} format.
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
year, month, day, hour, minute, second = iso_date:match ('^(%d%d%d%d)(%d%d)(%d%d)(%d%d)(%d%d)(%d%d)$');
if not year then
return nil; -- test time did not match the specified patterns
end
end
return {['year'] = year, ['month'] = month, ['day'] = day, ['hour'] = hour, ['min'] = minute, ['sec'] = second};
end
 
Line 260 ⟶ 262:
local patterns = {
'^([%+%-±−]?)(%d%d?)(%.)(%d+%d?)$', -- one or moretwo fractional hour digits
'^([%+%-±−]?)(%d%d?)(:)(%d%d)$', -- two minute digits
'^([%+%-±−]?)(%d%d?)[%.:]?$', -- hours only; ignore trailing separator
Line 318 ⟶ 320:
end
 
--[[--------------------------< I S _ D S T _ A C T I V E >----------------------------------------------------------
 
Return 1 of dst is active or 0 if it's not
 
]]
local function is_dst_active (timestamp,utc_timestamp)
local invert; -- true when southern hemisphere
local dst_begin_ts, dst_end_ts; -- DST begin and end timestamps in UTC
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?
return 0
else
return 1
end
else -- northern hemisphere
if utc_timestamp >= dst_begin_ts and utc_timestamp < dst_end_ts then -- all timestamps are UTC
return 1
else
return 0
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
return 0 -- dst not observed for this timezone
end
end
 
--[[--------------------------< D S T _ A C T I V E >----------------------------------------------------------
 
Invokeable version of above. This function takes one parameter.
1. the time zone abbreviation (positional or 1=)
 
]]
 
local function dst (frame)
local args = getArgs (frame);
local data = 'Module:Time/data'..set_sandbox(frame) -- make a data module name; sandbox or live
data = mw.loadData (data); -- load the data module
cfg = data.cfg; -- get the configuration table
args[1] = args[1]:lower(); -- lowecase
args[1] = args[1] or 'utc' -- default to utc
if data['tz_aliases'][args[1]] then
args[1] = data['tz_aliases'][args[1]];
elseif data['dst_tz'][args[1]] then
args[1] = data['dst_tz'][args[1]];
end
tz = data['tz_data'][args[1]];
if not tz then
return error_msg ('unknown_tz', args[1]); -- if the timezone given isn't in module:time/data(/sandbox)
end
 
local os_time = os.time ()
local timestamp = os_time + get_utc_offset (); -- make local time timestamp
return is_dst_active(timestamp, os_time) -- set is_dst to 0 or 1
end
 
--[=[-------------------------< T I M E >----------------------------------------------------------------------
 
This functiontemplate 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
Line 401 ⟶ 332:
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 _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); -- create the args table
local utc_timestamp, timestamp; -- current or time_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 is_dst; -- wether or not dst is active
 
local data = 'Module:Time/data'..set_sandbox(frame) -- make a data module name; sandbox or live
datalocal tz_aliases = mwdata.loadData (data)tz_aliases; -- loadget the dataaliases moduletable
cfg = data.cfg; -- get the configuration table
local formats = cfg.formats; -- get the formats table
local tz_aliases = {} -- create the general aliases table
local dst_table = data.dst_tz; -- get the dst aliases talbe
local aliases_table = data.tz_aliases; -- get the other aliases table
local tz_data = data.tz_data; -- get the tz data table
 
for k,v in pairs(dst_table) do -- add dst offsets to tz_aliases
local Timeonly = yesno(first_set (cfg.aliases['timeonly'], args)); -- boolean
tz_aliases[k] = v
local Dateonly = yesno(first_set (cfg.aliases['dateonly'], args)); -- boolean
end
for k,v in pairs(aliases_table) do -- add dst offsets to tz_aliases
tz_aliases[k] = v
end
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' == yesno(first_set (cfg.aliases['hide-refresh'], args)); -- boolean
local Hide_tz = 'yes' == yesno(first_set (cfg.aliases['hide-tz'], args)); -- boolean
local Unlink_tz = 'yes' == yesno(first_set (cfg.aliases['unlink-tz'], args)); -- boolean
local DST = first_set (cfg.aliases['dst'], args) or true; -- string 'always' or boolean
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 ({'Time', cfg.err_text['bad_df_pair']}); -- both are required
end
 
if args[1] then
Line 454 ⟶ 392:
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 ({'Time', cfg.err_text['bad_sign']});
end
tz.df = 'iso';
Line 461 ⟶ 399:
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 ({'Time', substitute (cfg.err_text['unknown_tz'], args[1])}); -- if the timezone given isn't in module:time/data(/sandbox)
end
end
Line 467 ⟶ 405:
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 ({'Time', substitute (cfg.err_text['bad_format'], DF)});
end
 
if is_set (args.test_time) or is_set (args._TEST_TIME_) then -- typically used to test the code at a specific utc time
iflocal is_settest_time = get_test_time (args._TEST_TIME_) then;
if not test_time then
args.test_time = args._TEST_TIME_
return error_msg ({'Time', cfg.err_text['test_time']});
end
local arg_index = {'year','month','day','hour','min','sec'} -- ISO 8601
local arguments = {['year']=nil,['month']=nil,['day']=nil,['hour']=nil,['min']=nil,['sec']=nil} -- ISO 8601
local i = 1
for val in string.gmatch(args.test_time, "%d+") do -- extract values
arguments[arg_index[i]] = val
i = i + 1
end
 
if not arguments.day then -- required by os.time
utc_timestamp = os.time(test_time);
return error_msg ('test_time') -- return error
end
utc_timestamp = os.time(arguments); -- get timestamp of 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 'always' == DST then -- if needed to always display dst time
is_dst = is_dst_active(timestamp,utc_timestamp) -- set is_dst to 0 or 1
if 'no' == DST or is_dst == 0 then -- dst isn't active or not observed
tz_abbr = tz.abbr; -- use standard time
elseif 'always' == DST or is_dst == 1 then -- if dst is set to always or dst is active
timestamp = timestamp + 3600; -- add a hour for dst
tz_abbr = tz.dst_abbr; -- dst abbreviation
elseif not yesno(DST) then -- for timezones that DO observe dst but for this ___location ...
else -- error at is_dst_active
tz_abbr = tz.abbr; -- ... dst is not observed (|dst=no) show time as standard time
return is_dst -- return the error
else
end
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 ({'Time', cfg.err_text['bad_dst']});
end
if invert then -- southern hemisphere; use beginning and ending of standard time in the comparison
if args["dst_active"] then -- there's really no other module/palce to put it
if utc_timestamp >= dst_end_ts and utc_timestamp < dst_begin_ts then -- is current date time standard time?
if tz_abbr == tz.dst_abbr then
tz_abbr = tz.abbr; -- standard time abbreviation
return '1'
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 ({'Time', substitute (cfg.err_text['bad_def'], args_t[1]:upper())});
else
tz_abbr = tz.abbr; -- dst not observed for this timezone
return '0'
end
end
Line 518 ⟶ 466:
end
 
elseif Timeonly or DF:match == ('12' or DF == '24^%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
Line 528 ⟶ 476:
end
local dformat;
if not formats[DF] then
if is_set (DF_cust) then
return error_msg ('bad_format', DF);
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
time_string = mw.language.getContentLanguage():formatDate (formats[DF], '@'..timestamp);
 
if not is_set (tz.article) then -- if some but not all not all parts then emit error message
return error_msg ({'Time', substitute (cfg.err_text['bad_def'], argsargs_t[1]:upper())});
end
Line 542 ⟶ 508:
' <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
cfg['refresh-label'], -- add the label
']]</span>', -- close the span
});
 
local tz_tag = (Hide_tz and '') -- noneor
or ((Unlink_tz and table.concat ({' '.., tz_abbr})) or -- unlinked
or table.concat ({' [['.., tz.article.., '|'.., tz_abbr.., ']]'})); -- linked
return table.concat ({time_string.., tz_tag.., refresh_link});
 
end
 
 
--[[--------------------------< E X P O RU T EC D_ O F F US N CE T I O N S >----------------------------------------------------------
 
implements {{UTC offset}}
 
mimics templates {{Time/GMT offset}}, {{Time/EST offset}}, etc.
 
{{#invoke:Time|utc_offset|<tz>}} – for a stand-alone invoke
{{#invoke:Time|utc_offset}} – for an invoke in a template (<tz> is first positional parameter in the template call)
 
where <tz> is a timezone abbreviation known to Module:Time/data
 
returns a UTC offset string suitable for use with the {{#time:}} parser function:
{{#time:H:i | {{#invoke:Time|utc_offset|MST}} }}
{{#time:H:i | {{UTC_offset|MST}} }}
 
]]
 
local function utc_offset (frame)
local function apply_dst_ajdust (offset) -- local function to adjust standard time to daylight time; called when adjustment is needed
local hours, minutes = offset:match ('^(%-?%d%d):(%d%d)'); -- extract signed hours and minutes from specified offset
return string.format ('%s:%s', tonumber (hours) + 1, minutes); -- return optional sign hh:mm string
end
 
local args_t = getArgs (frame); -- fetch arguments; only {{{1}}}, timesone specifier is used
 
if not args_t[1] then -- no timezone specifier
return error_msg ({'UTC offset', cfg.err_text['missing_tz']}); -- abandon with error message
end
 
local timezone = args_t[1]:lower(); -- lowercase for indexing into tz data tables
timezone = data.tz_aliases[timezone] or timezone; -- if <timezone> is an alias, map to its canonical value
 
if not data.tz_data[timezone] then -- timezone specifier not known
return error_msg ({'UTC offset', substitute (cfg.err_text['unknown_tz'], {timezone})}); -- abandon with error message
end
 
tz = data.tz_aliases[timezone] and data.tz_data[data.tz_aliases[timezone]] or data.tz_data[timezone]; -- fetch a copy of this timezone's data; <tz> is a page-global table used by functions called from this function
local utc_timestamp = os.time (); -- get current server time (UTC) in seconds; used to determine when dst adjustment should be applied
 
local timestamp = utc_timestamp + get_utc_offset (); -- make local time timestamp (in seconds)
local utc_offset;
 
local DST = first_set (cfg.aliases['dst'], args_t) or true; -- string 'always' or boolean
if 'always' == DST then -- if needed to always display dst time
utc_offset = apply_dst_ajdust (tz.utc_offset); -- return dst-adjusted timezone-offset from utc
elseif not yesno (DST) then -- for timezones that DO observe dst but for this ___location ...
utc_offset = tz.utc_offset; -- ... dst is not observed (|dst=no) show time as standard time
else
if is_set (tz.dst_begins) and is_set (tz.dst_ends) and is_set (tz.dst_time) then -- make sure we have all of the parts
local 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 either of these are nil
return error_msg ({'UTC offset', cfg.err_text['bad_dst']}); -- abandon with error message
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?
utc_offset = tz.utc_offset; -- return timezone-offset from utc
else
utc_offset = apply_dst_ajdust (tz.utc_offset); -- return dst-adjusted timezone-offset from utc
end
else -- northern hemisphere
if utc_timestamp >= dst_begin_ts and utc_timestamp < dst_end_ts then -- is current date time daylight time?
utc_offset = apply_dst_ajdust (tz.utc_offset); -- return dst-adjusted timezone-offset from utc
else
utc_offset = tz.utc_offset; -- return timezone-offset from utc
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 ({'UTC offset', substitute (cfg.err_text['bad_def'], args_t[1]:upper())});
else -- timezone does not use dst
utc_offset = tz.utc_offset; -- return timezone-offset from utc
end
end
local sign, hours, minutes = utc_offset:match ('^([%-%+]?)(%d%d?):(%d%d)')
if '' == sign then
sign = '+';
end
if 0 ~= tonumber (minutes) then
return string.format ('%s%s %s %s%s minutes', sign, tonumber(hours), ('1' == hours) and 'hour' or 'hours', sign, tonumber(minutes));
else
return string.format ('%s%s %s', sign, tonumber(hours), ('1' == hours) and 'hour' or 'hours');
end
end
 
 
--[[--------------------------< E X P O R T E D F U N C T I O N S >------------------------------------------
]]
 
return {
time = time,
utc_offset = utc_offset,
dst = dst,
}