Nhảy đến nội dung

Mô tổ:Time

Wikipedia (chū-iû ê pek-kho-choân-su) beh kā lí kóng...

Nhưng ởMô tổ:Time/docThành lập này mô tổ lời thuyết minh kiện

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

localtz={};-- holds local copy of the specified timezone table from tz_data{}
localcfg={};-- 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.

]]

localfunctionis_set(var)
returnnot(nil==varor''==var);
end


--[[--------------------------< S U B S T I T U T E >----------------------------------------------------------

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

]]

localfunctionsubstitute(msg,args)
returnargsandmw.message.newRawMessage(msg,args):plain()ormsg;
end


--[[--------------------------< E R R O R _ M S G >------------------------------------------------------------

create an error message

]]

localfunctionerror_msg(msg,arg)
returnsubstitute(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

]]

localfunctiondecode_dst_event(dst_event_string)
localord,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+)');

ifnot(is_set(ord)andis_set(day)andis_set(month))then-- if one or more of these not set, then pattern didn't match
returnnil;
end

returncfg.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.

]]

localfunctionget_days_in_month(year,month)
localdays_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)or0==(year%400)))then-- is year a leap year?
return29;-- if leap year then 29 days in February
end
end
returndays_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

]]

localfunctionget_dst_month_day(timestamp,start)
localord,weekday_num,month;
localfirst_day_of_dst_month_num;
locallast_day_of_dst_month_num;
localdays_in_month;
localyear;

iftrue==startthen
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

ifnot(is_set(ord)andis_set(weekday_num)andis_set(month))then
returnnil;-- could not decode event string
end

year=os.date('%Y',timestamp);

if-1==ordthen-- 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}));
returnmonth,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}))
returnmonth,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?
]]

localfunctionget_utc_offset()
localsign;
localhours;
localminutes;

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

if'-'==signthensign=-1;elsesign=1;end
ifis_set(hours)andis_set(minutes)then
returnsign*((hours*3600)+(minutes*60));
else
returnnil;-- 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.

]]

localfunctionmake_dst_timestamps(timestamp)
localdst_begin,dst_end;-- dst begin and end time stamps
localyear;-- current year
localdst_b_month,dst_e_month,dst_day;-- month and date of dst event
localdst_hour,dst_minute;-- hour and minute of dst event on year-dst_month-dst_day
localinvert=false;-- flag to pass on when dst_begin month is numerically larger than dst_end month (southern hemisphere)
localutc_offset;
localutc_flag;

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

dst_b_month,dst_day=get_dst_month_day(timestamp,true);-- month and day that dst begins
ifnotis_set(dst_b_month)then
returnnil;
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
ifnotis_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
ifnotis_set(dst_e_month)then
returnnil;
end

ifis_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
ifnotis_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


ifdst_b_month>dst_e_monththen
invert=true;-- true for southern hemisphere eg: start September YYYY end April YYYY+1
end

returndst_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.

]]

localfunctionget_test_time(iso_date)
localyear,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)');
ifnotyearthen
year,month,day,hour,minute,second=iso_date:match('^(%d%d%d%d)(%d%d)(%d%d)(%d%d)(%d%d)(%d%d)$');
ifnotyearthen
returnnil;-- test time did not match the specified patterns
end
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

]]

localfunctionget_full_utc_offset(utc_offset)
localh,m,sep,sign;

localpatterns={
'^([%+%-±−]?)(%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_,patterninipairs(patterns)do-- loop through the patterns
sign,h,sep,m=mw.ustring.match(utc_offset,pattern);
ifhthen
break;-- if h is set then pattern matched
end
end

ifnoththen
returnutc_offset;-- did not match a pattern
end

sign=(''==sign)and'+'orsign;-- sign character is required; set to '+' if not specified

m=('.'==sep)and((sep..m)*60)ormor0;-- fractional h to m

returnstring.format('utc%s%02d:%02d',sign,h,m);
end


--[[--------------------------< T A B L E _ L E N >------------------------------------------------------------

return number of elements in table

]]

localfunctiontable_len(tbl)
localcount=0;
for_inpairs(tbl)do
count=count+1;
end
returncount;
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)

]]

localfunctionfirst_set(list,args)
locali=1;
localcount=table_len(list);-- get count of items in list

whilei<=countdo-- loop through all items in list
ifis_set(args[list[i]])then-- if parameter name in list is set in args
returnargs[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]]

]=]

localfunctiontime(frame)
localargs=getArgs(frame);
localutc_timestamp,timestamp;-- current or _TEST_TIME_ timestamps; timestamp is local ST or DST time used in output
localdst_begin_ts,dst_end_ts;-- DST begin and end timestamps in UTC
localtz_abbr;-- select ST or DST timezone abbreviaion used in output
localtime_string;-- holds output time/date in |df= format
localutc_offset;
localinvert;-- true when southern hemisphere
localDF;-- date format flag; the |df= parameter
localis_dst_tz;

localdata=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
localtz_aliases=data.tz_aliases;-- get the aliases table
localtz_data=data.tz_data;-- get the tz data table

localTimeonly='yes'==first_set(cfg.aliases['timeonly'],args);-- boolean
localDateonly='yes'==first_set(cfg.aliases['dateonly'],args);-- boolean
ifTimeonlyandDateonlythen-- invalid condition when both are set
Timeonly,Dateonly=false;
end

localHide_refresh='yes'==first_set(cfg.aliases['hide-refresh'],args);-- boolean
localHide_tz='yes'==first_set(cfg.aliases['hide-tz'],args);-- boolean
localUnlink_tz='yes'==first_set(cfg.aliases['unlink-tz'],args);-- boolean
localDST=first_set(cfg.aliases['dst'],args);

localLang=first_set(cfg.aliases['lang'],args);-- to render in a language other than the local wiki's language

localDF_cust=first_set(cfg.aliases['df-cust'],args);-- custom date/time formats

localDF_cust_a=first_set(cfg.aliases['df-cust-a'],args);-- for am/pm sensitive formats
localDF_cust_p=first_set(cfg.aliases['df-cust-p'],args);

ifnot((DF_cust_aandDF_cust_p)or-- DF_cust_a xor DF_cust_p
(notDF_cust_aandnotDF_cust_p))then
returnerror_msg('bad_df_pair');-- both are required
end

ifargs[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

ifmw.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
locals,t=mw.ustring.match(tz.utc_offset,'(±)(%d%d:%d%d)');-- ± only valid for offset 00:00
ifsand'00:00'~=tthen
returnerror_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]]andtz_data[tz_aliases[args[1]]]ortz_data[args[1]];-- make a local copy of the timezone table from tz_data{}
ifnottzthen
returnerror_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)orargs[2]ortz.dforcfg.default_df;-- template |df= overrides typical df from tz properties
DF=DF:lower();-- normalize to lower case
ifnotcfg.df_vals[DF]then
returnerror_msg('bad_format',DF);
end

ifis_set(args._TEST_TIME_)then-- typically used to test the code at a specific utc time
localtest_time=get_test_time(args._TEST_TIME_);
ifnottest_timethen
returnerror_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'==DSTthen-- 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'==DSTthen-- if needed to always display dst time
timestamp=timestamp+3600;-- add a hour for dst
tz_abbr=tz.dst_abbr;-- dst abbreviation
else
ifis_set(tz.dst_begins)andis_set(tz.dst_ends)andis_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

ifnil==dst_begin_tsornil==dst_end_tsthen
returnerror_msg('bad_dst');
end

ifinvertthen-- southern hemisphere; use beginning and ending of standard time in the comparison
ifutc_timestamp>=dst_end_tsandutc_timestamp<dst_begin_tsthen-- 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
ifutc_timestamp>=dst_begin_tsandutc_timestamp<dst_end_tsthen-- all timestamps are UTC
timestamp=timestamp+3600;-- add an hour
tz_abbr=tz.dst_abbr;
else
tz_abbr=tz.abbr;
end
end
elseifis_set(tz.dst_begins)oris_set(tz.dst_ends)oris_set(tz.dst_time)then-- if some but not all not all parts then emit error message
returnerror_msg('bad_def',args[1]:upper());
else
tz_abbr=tz.abbr;-- dst not observed for this timezone
end
end

ifDateonlythen
if'iso'==DFthen-- |df=iso
DF='iso_date';
elseifDF:find('^dmy')or'y'==DFthen-- |df=dmy, |df=dmy12, |df=dmy24, |df=y
DF='dmy_date';
else
DF='mdy_date';-- default
end

elseifTimeonlyorDF: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'==DFor'dmy24'==DFthen
DF='dmy';

elseif'mdy24'==DFthen
DF='mdy';
end

localdformat;
ifis_set(DF_cust)then
dformat=DF_cust;
elseifis_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}});
ifLangthen
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

ifnotis_set(tz.article)then-- if some but not all not all parts then emit error message
returnerror_msg('bad_def',args[1]:upper());
end

localrefresh_link=(Hide_refreshand'')or
table.concat({
' <span class= "plainlinks" style= "font-size:85%;" >[[',-- open span
mw.title.getCurrentTitle():fullUrl({action='purge'}),-- add the a refresh link url
' ',
cfg['refresh-label'],-- add the label
']]</span>',-- close the span
});

localtz_tag=(Hide_tzand'')or
((Unlink_tzandtable.concat({' ',tz_abbr}))or-- unlinked
table.concat({' [[',tz.article,'|',tz_abbr,']]'}));-- linked

returntable.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}