where df may be one of:
mdy, dmy, iso, ymd  
on entry, year, month, day are presumed to be correct for the date that they represent; all are required  
in this module, makeDate is sometimes given an iso-format date in year:
makeDate (2018-09-20, nil, nil, df)
this works because table.concat sees only one table member  
local function makeDate (year, month, day, df)
local format = {
['dmy'] = 'j F Y',
['mdy'] = 'F j, Y',
['ymd'] = 'Y F j',
['iso'] = 'Y-m-d',
};  
local date = table.concat ({year, month, day}, '-'); -- assemble year-initial numeric-format date (zero padding not required here)  
if non_western_digits then --this wiki uses non-western digits?
date = mw.ustring.gsub (date, '%d', digits); -- convert this wiki's non-western digits to western digits
end  
return mw.getContentLanguage:formatDate (format[df], date);
end  
----------------------------< I S _ V A L I D _ D A T E >----------------------------------------------------  
Returns true if date is after 31 December 1899 (why is 1900 the min year? shouldn't the internet's date-of-birth
be min year?), not after today's date, and represents a valid date (29 February 2017 is not a valid date). Applies
Gregorian leapyear rules.  
all arguments are required  
local function is_valid_date (year, month, day)
local days_in_month = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
local month_length;
local y, m, d;
local today = os.date ('*t'); -- fetch a table of current date parts  
if not year or * == year or not month or * == month or not day or '' == day then
return false; -- something missing
end  
y = tonumber (year);
m = tonumber (month);
d = tonumber (day);  
if 1900 > y or today.year < y or 1 > m or 12 < m then -- year and month are within bounds TODO: 1900?
return false;
end  
if (2==m) then -- if February
month_length = 28; -- then 28 days unless
if (0==(y%4) and (0~=(y%100) or 0==(y%400))) then -- is a leap year?
month_length = 29; -- if leap year then 29 days in February
end
else
month_length=days_in_month[m];
end  
if 1 > d or month_length < d then -- day is within bounds
return false;
end
-- here when date parts represent a valid date
return os.time({['year']=y, ['month']=m, ['day']=d, ['hour']=0}) <= os.time; -- date at midnight must be less than or equal to current date/time
end  
----------------------------< decodeWebciteDate >-----------------------  
Given a URI-path to Webcite (eg. /67xHmVFWP) return the encoded date in df format  
returns date string in df format - webcite date is a unix timestamp encoded as bae62
or the string 'query'  
local function decodeWebciteDate(path, df)  
local dt = {};
local decode;  
dt = mw.text.split(path, "/")  
-- valid URL formats that are not base62  
-- http://www.webcitation.org/query?id=1138911916587475
-- http://www.webcitation.org/query?url=http..&date=2012-06-01+21:40:03
-- http://www.webcitation.org/1138911916587475
-- http://www.webcitation.org/cache/73e53dd1f16cf8c5da298418d2a6e452870cf50e
-- http://www.webcitation.org/getfile.php?fileid=1c46e791d68e89e12d0c2532cc3cf629b8bc8c8e  
if dt[2]:find ('query', 1, true) or
dt[2]:find ('cache', 1, true) or
dt[2]:find ('getfile', 1, true) or
tonumber(dt[2]) then
return 'query';
end  
decode = base62(dt[2]); -- base62 string -> exponential number
if not decode then
return nil; -- nil return when dt[2] contains characters not in %w
end
dt = os.date('*t', string.format("%d", decode):sub(1,10)) -- exponential number -> text -> first 10 characters (a unix timestamp) -> a table of date parts  
decode = makeDate (dt.year, dt.month, dt.day, 'iso'); -- date comparisons are all done in iso format with western digits
if non_western_digits then --this wiki uses non-western digits?
decode = mw.ustring.gsub (decode, '%d', digits); -- convert this wiki's non-western digits to western digits
end  
return decode;
end  
----------------------------< decodeWaybackDate >-----------------------  
Given a URI-path to Wayback (eg. /web/20160901010101/http://example.com )
or Library of Congress Web Archives (/all/20160901010101/http://example.com)
return the formatted date eg. "September 1, 2016" in df format
Handle non-digits in snapshot ID such as "re_" and "-" and "*"  
returns two values:
first value is one of these:
valid date string in df format - wayback date is valid (including the text string 'index' when date is '/*/')
empty string - wayback date is malformed (less than 8 digits, not a valid date)
nil - wayback date is '/save/' or otherwise not a number  
second return value is an appropriate 'message' may or may not be formatted  
local function decodeWaybackDate(path, df)  
local msg, snapdate;  
snapdate = path:gsub ('^/all/', *):gsub ('^/web/', *):gsub ('^/', ''); -- remove leading '/all/', leading '/web/' or leading '/'
snapdate = snapdate:match ('^[^/]+'); -- get timestamp
if snapdate == "*" then -- eg. /web/*/http.. or /all/*/http..
return 'index'; -- return indicator that this url has an index date
end  
snapdate = snapdate:gsub ('%a%a_%d?$', *):gsub ('%-', *); -- from date, remove any trailing "re_", dashes  
msg = '';
if snapdate:match ('%*$') then -- a trailing '*' causes calendar display at archive .org
snapdate = snapdate:gsub ('%*$', ''); -- remove so not part of length calc later
msg = inlineRed (err_warn_msgs.ts_cal, 'warning'); -- make a message
end  
if not tonumber(snapdate) then
return nil, 'ts_nan'; -- return nil (fatal error flag) and message selector
end  
local dlen = snapdate:len;
if dlen < 8 then -- we need 8 digits TODO: but shouldn't this be testing for 14 digits?
return '', inlineRed (err_warn_msgs.ts_short, 'error'); -- return empty string and error message
end  
local year, month, day = snapdate:match ('(%d%d%d%d)(%d%d)(%d%d)'); -- no need for snapdatelong here  
if not is_valid_date (year, month, day) then
return '', inlineRed (err_warn_msgs.ts_date, 'error'); -- return empty string and error message
end  
snapdate = table.concat ({year, month, day}, '-'); -- date comparisons are all done in iso format
if 14 == dlen then
return snapdate, msg; -- return date with message if any
else
return snapdate, msg .. inlineRed (err_warn_msgs.ts_len, 'warning'); -- return date with warning message(s)
end
end  
----------------------------< decodeArchiveisDate >-----------------------  
Given an Archive.is "long link" URI-path (e.g. /2016.08.28-144552/http://example.com)
return the date in df format (e.g. if df = dmy, return 28 August 2016)
Handles "." and "-" in snapshot date, so 2016.08.28-144552 is same as 20160828144552  
returns two values:
first value is one of these:
valid date string in df format - archive.is date is valid (including the text string 'short link' when url is the short form)
empty string - wayback date is malformed (not a number, less than 8 digits, not a valid date)
nil - wayback date is '/save/'  
second return value is an appropriate 'message' may or may not be formatted  
local function decodeArchiveisDate(path, df)
local snapdate  
if path:match ('^/%w+$') then -- short form url path is '/' followed by some number of base 62 digits and nothing else
return "short link" -- e.g. http://archive.is/hD1qz
end  
snapdate = mw.text.split (path, '/')[2]:gsub('[%.%-]', ''); -- get snapshot date, e.g. 2016.08.28-144552; remove periods and hyphens  
local dlen = string.len(snapdate)
if dlen < 8 then -- we need 8 digits TODO: but shouldn't this be testing for 14 digits?
return '', inlineRed (err_warn_msgs.ts_short, 'error'); -- return empty string and error message
end  
local year, month, day = snapdate:match ('(%d%d%d%d)(%d%d)(%d%d)'); -- no need for snapdatelong here  
if not is_valid_date (year, month, day) then
return '', inlineRed (err_warn_msgs.ts_date, 'error'); -- return empty string and error message
end  
snapdate = table.concat ({year, month, day}, '-'); -- date comparisons are all done in iso format
if 14 == dlen then
return snapdate; -- return date
else
return snapdate, inlineRed (err_warn_msgs.ts_len, 'warning'); -- return date with warning message
end