Difference between revisions of "Module:DateI18n"
Jump to navigation
Jump to search
(Undid revision 954131094 by RexxS (talk) errors should be fixed (sandboxes working) - try new version again) |
m (incorrect language code) |
||
Line 27: | Line 27: | ||
return val |
return val |
||
elseif type(val) == 'number' then |
elseif type(val) == 'number' then |
||
− | if val==1 then |
+ | if val == 1 then |
return true |
return true |
||
− | elseif val==0 then |
+ | elseif val == 0 then |
return false |
return false |
||
end |
end |
||
elseif type(val) == 'string' then |
elseif type(val) == 'string' then |
||
− | val = |
+ | val = string.lower(val) -- put in lower case |
if val == 'no' or val == 'n' or val == 'false' or tonumber(val) == 0 then |
if val == 'no' or val == 'n' or val == 'false' or tonumber(val) == 0 then |
||
return false |
return false |
||
Line 66: | Line 66: | ||
-- down date-time component strings or numbers |
-- down date-time component strings or numbers |
||
-- OUTPUT: |
-- OUTPUT: |
||
− | -- * datecode - a code specifying content of the array where Y' is year, 'M' is month, |
+ | -- * datecode - a code specifying content of the array where Y' is year, 'M' is month, 'D' is day, |
− | -- ' |
+ | -- 'h' is hour, 'm' minute, 's' is second. output has to be one of YMDhms, YMDhm, YMD, YM, Y, MDhms, MDhm, MD, M |
-- * datenum - same array but holding only numbers or nuls |
-- * datenum - same array but holding only numbers or nuls |
||
local function parserDatevec(datevec) |
local function parserDatevec(datevec) |
||
⚫ | |||
− | local month = datevec[2] |
||
− | if month and month~='' and not tonumber(month) then |
||
⚫ | |||
− | end |
||
− | |||
-- create datecode based on which variables are provided and check for out-of-bound values |
-- create datecode based on which variables are provided and check for out-of-bound values |
||
− | local maxval = { |
+ | local maxval = { 1/0, 12, 31, 23, 59, 60, 23, 59 } -- max values (or 1/0=+inf) for year, month, day, hour, minute, second, tzhour, tzmin |
− | local minval = { |
+ | local minval = { -1/0, 01, 01, 00, 00, 00, -23, 00 } -- min values (or -1/0=-inf) for year, month, ... |
− | local |
+ | local codes = { 'Y', 'M', 'D', 'h', 'm', 's', '', '' } -- WARNING: 'M' alone would be ambiguous if it does not follow 'Y' or 'h' |
local datecode = '' -- a string signifying which combination of variables was provided |
local datecode = '' -- a string signifying which combination of variables was provided |
||
− | local datenum = {} |
+ | local datenum = {} -- date-time encoded as a vector = [year, month, ... , second, tzhour, tzmin] |
− | for i = 1,8 do |
+ | for i = 1, 8 do |
− | + | local c, val = codes[i], datevec[i] |
|
− | + | if c == 'M' and type(val) == 'string' and val ~= '' and not tonumber(val) then |
|
⚫ | |||
⚫ | |||
⚫ | |||
+ | end |
||
+ | val = tonumber(val) |
||
+ | if val and val >= minval[i] and val <= maxval[i] then -- These tests work with infinite min/max values. |
||
+ | if c == 'm' then -- Field for minute accepted only if it follows another valid 'M', 'D' or 'h' field. |
||
+ | if not string.find('MDh', datecode:sub(-1)) then |
||
+ | c = ''; val = nil -- field for minute is invalid |
||
+ | end |
||
+ | elseif c == 's' then -- Field for leap second '60' is valid only at end of 23:59 UTC, on 30 June or 31 December of specific years. |
||
+ | if val == 60 and not( -- Leap second are are added (or dropped) on specific dates planned only some months before. |
||
+ | datenum[1] and -- A year is specified (to check it would require constantly maintaining a table of dates). |
||
+ | (datenum[2] == 6 and datenum[3] == 30 or datenum[2] == 12 and datenum[3] == 31) and |
||
+ | datenum[4] == 23 and datenum[5] == 59 |
||
+ | ) then |
||
+ | c = ''; val = nil -- Field for second is invalid in this case, don't add the field. |
||
+ | end |
||
+ | end |
||
⚫ | |||
+ | datenum[i] = val |
||
end |
end |
||
end |
end |
||
Line 94: | Line 107: | ||
-- process datevec |
-- process datevec |
||
-- INPUT: |
-- INPUT: |
||
− | -- * datecode - a code specifying content of the array where Y' is year, 'M' is month, |
+ | -- * datecode - a code specifying content of the array where Y' is year, 'M' is month, |
− | -- 'D' is day, 'H' is hour, ' |
+ | -- 'D' is day, 'H' is hour, 'i' minute, 's' is second. |
+ | -- Output has to be one of YMDhms, YMDhm, YMD, YM, Y, MDhms, MDhm, MD, M. |
||
-- * datenum - Array of {year,month,day,hour,minute,second, tzhour, tzmin} as numbers or nuls |
-- * datenum - Array of {year,month,day,hour,minute,second, tzhour, tzmin} as numbers or nuls |
||
-- OUTPUT: |
-- OUTPUT: |
||
− | -- * timeStamp - date string in the format taken by mw.language:formatDate lua function and {{#time}} |
+ | -- * timeStamp - date string in the format taken by mw.language:formatDate lua function and {{#time}} parser function |
-- https://www.mediawiki.org/wiki/Extension:Scribunto/Lua_reference_manual#mw.language:formatDate |
-- https://www.mediawiki.org/wiki/Extension:Scribunto/Lua_reference_manual#mw.language:formatDate |
||
-- https://www.mediawiki.org/wiki/Help:Extension:ParserFunctions#.23time |
-- https://www.mediawiki.org/wiki/Help:Extension:ParserFunctions#.23time |
||
-- * datecode - with possible corrections |
-- * datecode - with possible corrections |
||
local function getTimestamp(datecode, datenum) |
local function getTimestamp(datecode, datenum) |
||
− | -- create |
+ | -- create timestamp string (for example 2000-02-20 02:20:20) based on which variables were provided |
local timeStamp |
local timeStamp |
||
+ | -- date starting by a year |
||
− | if datecode == ' |
+ | if datecode == 'YMDhms' then |
timeStamp = string.format('%04i-%02i-%02i %02i:%02i:%02i', datenum[1], datenum[2], datenum[3], datenum[4], datenum[5], datenum[6] ) |
timeStamp = string.format('%04i-%02i-%02i %02i:%02i:%02i', datenum[1], datenum[2], datenum[3], datenum[4], datenum[5], datenum[6] ) |
||
− | elseif datecode == ' |
+ | elseif datecode == 'YMDhm' then |
timeStamp = string.format('%04i-%02i-%02i %02i:%02i', datenum[1], datenum[2], datenum[3], datenum[4], datenum[5] ) |
timeStamp = string.format('%04i-%02i-%02i %02i:%02i', datenum[1], datenum[2], datenum[3], datenum[4], datenum[5] ) |
||
elseif datecode:sub(1,3)=='YMD' then |
elseif datecode:sub(1,3)=='YMD' then |
||
timeStamp = string.format('%04i-%02i-%02i', datenum[1], datenum[2], datenum[3] ) |
timeStamp = string.format('%04i-%02i-%02i', datenum[1], datenum[2], datenum[3] ) |
||
− | datecode = 'YMD' -- ' |
+ | datecode = 'YMD' -- 'YMDhms', 'YMDhm' and 'YMD' are the only supported format starting with 'YMD'; all others will be converted to 'YMD'. |
− | elseif datecode == 'YM' then |
+ | elseif datecode:sub(1,2) == 'YM' then |
timeStamp = string.format('%04i-%02i', datenum[1], datenum[2] ) |
timeStamp = string.format('%04i-%02i', datenum[1], datenum[2] ) |
||
elseif datecode:sub(1,1)=='Y' then |
elseif datecode:sub(1,1)=='Y' then |
||
timeStamp = string.format('%04i', datenum[1] ) |
timeStamp = string.format('%04i', datenum[1] ) |
||
datecode = 'Y' |
datecode = 'Y' |
||
+ | -- date starting by a month (the implied year is 2000) |
||
⚫ | |||
+ | elseif datecode== 'MDhms' then |
||
⚫ | |||
+ | timeStamp = string.format('%04i-%02i-%02i %02i:%02i:%02i', 2000, datenum[2], datenum[3], datenum[4], datenum[5], datenum[6] ) |
||
⚫ | |||
⚫ | |||
+ | timeStamp = string.format('%04i-%02i-%02i %02i:%02i', 2000, datenum[2], datenum[3], datenum[4], datenum[5] ) |
||
+ | elseif datecode:sub(1,2) == 'MD' then |
||
timeStamp = string.format('%04i-%02i-%02i', 2000, datenum[2], datenum[3] ) |
timeStamp = string.format('%04i-%02i-%02i', 2000, datenum[2], datenum[3] ) |
||
+ | datecode = 'MD' -- 'MDhms', 'MDhm' and 'MD' are the only supported format starting with 'MD'; all others will be converted to 'MD' |
||
+ | elseif datecode:sub(1,1) == 'M' then -- Ambiguous: could mean minutes, but here means month (when parsed as a name/abbrev, not as a number). |
||
⚫ | |||
+ | -- other possible but unrecognized formats (e.g. 'DHis', 'DHi', 'D', 'His', 'Hi'); |
||
+ | -- note that 'Dh', 'D', 'h', 's' may eventually work, but not 'm' for minute only, which is ambiguous with 'M' for month only. |
||
else |
else |
||
timeStamp = nil -- format not supported |
timeStamp = nil -- format not supported |
||
Line 159: | Line 182: | ||
-- Look up proper format string to be passed to {{#time}} parser function |
-- Look up proper format string to be passed to {{#time}} parser function |
||
-- INPUTS: |
-- INPUTS: |
||
− | -- * datecode: |
+ | -- * datecode: YMDhms, YMDhm, YMD, YM, Y, MDhms, MDhm, MD, or M |
-- * day : Number between 1 and 31 (not needed for most languages) |
-- * day : Number between 1 and 31 (not needed for most languages) |
||
-- * lang : language |
-- * lang : language |
||
Line 167: | Line 190: | ||
local function parseFormat(dFormat, day) |
local function parseFormat(dFormat, day) |
||
if dFormat:find('default') and #dFormat>10 then |
if dFormat:find('default') and #dFormat>10 then |
||
− | -- |
+ | -- Special (and messy) case of dFormat code depending on a day number, where data is a |
+ | -- JSON-encoded table {”default”:”*”,”dDD”:”*”} including fields for specific 2-digit days. |
||
− | -- then json contains a string with more json containing "default" field and 2 digit day keys |
||
+ | -- Change curly double quotes (possibly used for easier editing in tabular data) in dFormat |
||
⚫ | |||
+ | -- to straight ASCII double quotes (required for parsing of this JSON-encoded table). |
||
− | dFormat = dFormat:gsub('”','"') -- change fancy double quote to a straight one, used for json marking |
||
− | local D = mw.text.jsonDecode( |
+ | local D = mw.text.jsonDecode(mw.ustring.gsub(dFormat, '[„“‟”]', '"')) --com = mw.dumpObject(D) |
⚫ | |||
− | day = string.format('d%02i',day) -- create day key |
||
− | dFormat = D[day] or D.default |
+ | dFormat = D[string.format('d%02i', day)] or D.default |
− | + | -- Change ASCII single quotes to ASCII double quotes used for {{#time}} marking. |
|
+ | -- Apostrophes needed in plain-text must not use ASCII single quotes but curly apostrophe |
||
+ | -- e.g. { ‟default”: ‟j”, ‟d01”: ‟j’'o'” }, not { ‟default”: ‟j”, ‟d01”: ‟j''o'” }. |
||
end |
end |
||
+ | dFormat = dFormat:gsub("'", '"') |
||
return dFormat |
return dFormat |
||
end |
end |
||
Line 185: | Line 211: | ||
T[id] = msg |
T[id] = msg |
||
end |
end |
||
+ | -- Compatibility of legacy data using 'HMS' or 'HM', where 'M' is ambiguous |
||
+ | T.YMDhms = T.YMDhms or T.YMDHMS |
||
+ | T.YMDhm = T.YMDhm or T.YMDHM |
||
+ | datecode = datecode == 'YMDHMS' and 'YMDhms' or datecode == 'YMDHM' and 'YMDhm' or datecode |
||
+ | |||
local dFormat = T[datecode] |
local dFormat = T[datecode] |
||
− | if dFormat=='default' and (datecode==' |
+ | if dFormat == 'default' and (datecode == 'YMDhms' or datecode == 'YMDhm') then |
− | -- |
+ | -- For most languages adding hour:minute:second is done by adding ", HH:ii:ss to the |
-- day precission date, those languages are skipped in DateI18n.tab and default to |
-- day precission date, those languages are skipped in DateI18n.tab and default to |
||
-- English which stores word "default" |
-- English which stores word "default" |
||
dFormat = parseFormat(T['YMD'], day).. ', H:i' |
dFormat = parseFormat(T['YMD'], day).. ', H:i' |
||
− | if datecode==' |
+ | if datecode == 'YMDhms' then |
dFormat = dFormat .. ':s' |
dFormat = dFormat .. ':s' |
||
end |
end |
||
Line 244: | Line 275: | ||
lang = mw.getCurrentFrame():callParserFunction( "int", "lang" ) -- get user's chosen language |
lang = mw.getCurrentFrame():callParserFunction( "int", "lang" ) -- get user's chosen language |
||
end |
end |
||
− | if lang == 'be- |
+ | if lang == 'be-tarask' then |
lang = 'be-x-old' |
lang = 'be-x-old' |
||
end |
end |
||
Line 265: | Line 296: | ||
-- phrases as it is done in [[c:Module:Complex date]] |
-- phrases as it is done in [[c:Module:Complex date]] |
||
case = case or '' |
case = case or '' |
||
− | if (lang=='qu' or lang=='qug') and |
+ | if (lang=='qu' or lang=='qug') and case=='nom' then |
-- Special case related to Quechua and Kichwa languages. The form in the I18n is |
-- Special case related to Quechua and Kichwa languages. The form in the I18n is |
||
-- Genitive case with suffix "pi" added to month names provided by {#time}} |
-- Genitive case with suffix "pi" added to month names provided by {#time}} |
||
Line 271: | Line 302: | ||
-- see https://commons.wikimedia.org/wiki/Template_talk:Date#Quechua from 2014 |
-- see https://commons.wikimedia.org/wiki/Template_talk:Date#Quechua from 2014 |
||
dFormat = dFormat:gsub('F"pi"', 'F') |
dFormat = dFormat:gsub('F"pi"', 'F') |
||
− | elseif |
+ | elseif case == 'gen' then |
dFormat = strReplace(dFormat, "F", "xg") |
dFormat = strReplace(dFormat, "F", "xg") |
||
− | elseif |
+ | elseif case == 'nom' then |
dFormat = strReplace(dFormat, "xg", "F") |
dFormat = strReplace(dFormat, "xg", "F") |
||
− | elseif |
+ | elseif case ~= '' then |
-- see is page [[Data:I18n/MonthCases.tab]] on Commons have name of the month |
-- see is page [[Data:I18n/MonthCases.tab]] on Commons have name of the month |
||
-- in specific gramatic case in desired language. If we have it than replace |
-- in specific gramatic case in desired language. If we have it than replace |
||
Line 286: | Line 317: | ||
end |
end |
||
− | -- Translate the date using specified format |
+ | -- Translate the date using specified format. |
-- See https://www.mediawiki.org/wiki/Extension:Scribunto/Lua_reference_manual#mw.language:formatDate and |
-- See https://www.mediawiki.org/wiki/Extension:Scribunto/Lua_reference_manual#mw.language:formatDate and |
||
-- https://www.mediawiki.org/wiki/Help:Extension:ParserFunctions##time for explanation of the format |
-- https://www.mediawiki.org/wiki/Help:Extension:ParserFunctions##time for explanation of the format |
||
− | local |
+ | local langObj = mw.language.new(lang) |
+ | local datestr = langObj:formatDate(dFormat, timeStamp) -- same as using {{#time}} parser function |
||
− | -- Special case related to Thai solar calendar: prior to 1940 new-year was at different |
+ | -- Special case related to Thai solar calendar: prior to 1940 new-year was at different time of year, |
− | -- |
+ | -- so just year (datecode == 'Y') is ambiguous and is replaced by "YYYY or YYYY" phrase |
if lang=='th' and datecode=='Y' and year<=1940 then |
if lang=='th' and datecode=='Y' and year<=1940 then |
||
datestr = string.format('%04i หรือ %04i', year+542, year+543 ) |
datestr = string.format('%04i หรือ %04i', year+542, year+543 ) |
||
end |
end |
||
− | -- If year<1000 than either keep the date padded to the length of 4 digits or trim it |
+ | -- If year < 1000 than either keep the date padded to the length of 4 digits or trim it. |
− | -- |
+ | -- Decide if the year will stay padded with zeros (for years in 0-999 range). |
− | if year and year<1000 then |
+ | if year and year < 1000 then |
− | + | trim_year = yesno(trim_year, '100-999') |
|
− | + | if type(trim_year) == 'string' then |
|
⚫ | |||
− | end |
||
⚫ | |||
− | local trim = yesno(trim_year,nil) -- convert to boolean |
||
− | if trim==nil and type(trim_year)=='string' then |
||
⚫ | |||
⚫ | |||
local YMin, YMax = trim_year:match( '(%d+)-(%d+)' ) |
local YMin, YMax = trim_year:match( '(%d+)-(%d+)' ) |
||
− | + | trim_year = YMin and year >= tonumber(YMin) and year <= tonumber(YMax) |
|
end |
end |
||
− | if |
+ | if trim_year then |
datestr = trimYear(datestr, year, lang) -- in datestr replace long year with trimmed one |
datestr = trimYear(datestr, year, lang) -- in datestr replace long year with trimmed one |
||
end |
end |
||
end |
end |
||
− | -- |
+ | -- Append a timezone if present (after the hour and minute of the day). |
− | if datenum[7] and (datecode == ' |
+ | if datenum[7] and (datecode.sub(1, 5) == 'YMDhm' or datecode.sub(1, 4) == 'MDhm') then |
− | -- |
+ | -- Use {{#time}} parser function to create timezone string, so that we use the correct character set. |
local sign = (datenum[7]<0) and '−' or '+' |
local sign = (datenum[7]<0) and '−' or '+' |
||
timeStamp = string.format("2000-01-01 %02i:%02i:00", math.abs(datenum[7]), datenum[8] or 0) |
timeStamp = string.format("2000-01-01 %02i:%02i:00", math.abs(datenum[7]), datenum[8] or 0) |
||
− | local timezone = |
+ | local timezone = langObj:formatDate('H:i', timeStamp) -- same as using {{#time}} parser function |
datestr = string.format("%s %s%s", datestr, sign, timezone ) |
datestr = string.format("%s %s%s", datestr, sign, timezone ) |
||
end |
end |
||
− | -- |
+ | -- HTML formating of date string and tagging for microformats (only for absolute dates with a year). |
− | if class and class ~= '' and datecode |
+ | if class and class ~= '' and datecode.sub(1,1) == 'Y' then -- |
+ | datestr = |
||
− | local DateHtmlTags = '<span style="white-space:nowrap"><time class="%s" datetime="%s">%s</time></span>' |
||
+ | ('<time class="%s" datetime="%s" lang="%s" dir="%s" style="white-space:nowrap">%s</time>') |
||
− | + | :format(class, timeStamp, lang, langObj:getDir(), datestr) |
|
end |
end |
||
return datestr |
return datestr |
Revision as of 23:40, 6 September 2021
Documentation for this module may be created at Module:DateI18n/doc
--[[ __ __ _ _ ____ _ ___ _ ___ | \/ | ___ __| |_ _| | ___ _| _ \ __ _| |_ ___|_ _/ |( _ ) _ __ | |\/| |/ _ \ / _` | | | | |/ _ (_) | | |/ _` | __/ _ \| || |/ _ \| '_ \ | | | | (_) | (_| | |_| | | __/_| |_| | (_| | || __/| || | (_) | | | | |_| |_|\___/ \__,_|\__,_|_|\___(_)____/ \__,_|\__\___|___|_|\___/|_| |_| This module is intended for processing of date strings. Please do not modify this code without applying the changes first at Module:Date/sandbox and testing at Module:Date/sandbox/testcases and Module talk:Date/sandbox/testcases. Authors and maintainers: * User:Parent5446 - original version of the function mimicking template:ISOdate * User:Jarekt - original version of the functions mimicking template:Date ]] require('Module:No globals') -- ================================================== -- === Internal functions =========================== -- ================================================== -- Function allowing for consistent treatment of boolean-like wikitext input. -- It works similarly to Module:Yesno local function yesno(val, default) if type(val) == 'boolean' then return val elseif type(val) == 'number' then if val == 1 then return true elseif val == 0 then return false end elseif type(val) == 'string' then val = string.lower(val) -- put in lower case if val == 'no' or val == 'n' or val == 'false' or tonumber(val) == 0 then return false elseif val == 'yes' or val == 'y' or val == 'true' or tonumber(val) == 1 then return true end end return default end --------------------------------------------------------------------------------------- -- String replacement that ignores part of the string in "..." local function strReplace(String, old, new) if String:find('"') then local T={} for i, str in ipairs(mw.text.split( String, '"', true )) do if i%2==1 then str = str:gsub(old, new) end table.insert(T, str) end return table.concat(T,'"') else return String:gsub(old, new) end end --------------------------------------------------------------------------------------- -- process datevec -- INPUT: -- * datevec - Array of {year,month,day,hour,minute,second, tzhour, tzmin} containing broken -- down date-time component strings or numbers -- OUTPUT: -- * datecode - a code specifying content of the array where Y' is year, 'M' is month, 'D' is day, -- 'h' is hour, 'm' minute, 's' is second. output has to be one of YMDhms, YMDhm, YMD, YM, Y, MDhms, MDhm, MD, M -- * datenum - same array but holding only numbers or nuls local function parserDatevec(datevec) -- create datecode based on which variables are provided and check for out-of-bound values local maxval = { 1/0, 12, 31, 23, 59, 60, 23, 59 } -- max values (or 1/0=+inf) for year, month, day, hour, minute, second, tzhour, tzmin local minval = { -1/0, 01, 01, 00, 00, 00, -23, 00 } -- min values (or -1/0=-inf) for year, month, ... local codes = { 'Y', 'M', 'D', 'h', 'm', 's', '', '' } -- WARNING: 'M' alone would be ambiguous if it does not follow 'Y' or 'h' local datecode = '' -- a string signifying which combination of variables was provided local datenum = {} -- date-time encoded as a vector = [year, month, ... , second, tzhour, tzmin] for i = 1, 8 do local c, val = codes[i], datevec[i] if c == 'M' and type(val) == 'string' and val ~= '' and not tonumber(val) then -- When the month is not a number, check if it's a month name in the project's language. val = mw.getContentLanguage():formatDate('n', val) end val = tonumber(val) if val and val >= minval[i] and val <= maxval[i] then -- These tests work with infinite min/max values. if c == 'm' then -- Field for minute accepted only if it follows another valid 'M', 'D' or 'h' field. if not string.find('MDh', datecode:sub(-1)) then c = ''; val = nil -- field for minute is invalid end elseif c == 's' then -- Field for leap second '60' is valid only at end of 23:59 UTC, on 30 June or 31 December of specific years. if val == 60 and not( -- Leap second are are added (or dropped) on specific dates planned only some months before. datenum[1] and -- A year is specified (to check it would require constantly maintaining a table of dates). (datenum[2] == 6 and datenum[3] == 30 or datenum[2] == 12 and datenum[3] == 31) and datenum[4] == 23 and datenum[5] == 59 ) then c = ''; val = nil -- Field for second is invalid in this case, don't add the field. end end datecode = datecode .. c datenum[i] = val end end return datecode, datenum end --------------------------------------------------------------------------------------- -- process datevec -- INPUT: -- * datecode - a code specifying content of the array where Y' is year, 'M' is month, -- 'D' is day, 'H' is hour, 'i' minute, 's' is second. -- Output has to be one of YMDhms, YMDhm, YMD, YM, Y, MDhms, MDhm, MD, M. -- * datenum - Array of {year,month,day,hour,minute,second, tzhour, tzmin} as numbers or nuls -- OUTPUT: -- * timeStamp - date string in the format taken by mw.language:formatDate lua function and {{#time}} parser function -- https://www.mediawiki.org/wiki/Extension:Scribunto/Lua_reference_manual#mw.language:formatDate -- https://www.mediawiki.org/wiki/Help:Extension:ParserFunctions#.23time -- * datecode - with possible corrections local function getTimestamp(datecode, datenum) -- create timestamp string (for example 2000-02-20 02:20:20) based on which variables were provided local timeStamp -- date starting by a year if datecode == 'YMDhms' then timeStamp = string.format('%04i-%02i-%02i %02i:%02i:%02i', datenum[1], datenum[2], datenum[3], datenum[4], datenum[5], datenum[6] ) elseif datecode == 'YMDhm' then timeStamp = string.format('%04i-%02i-%02i %02i:%02i', datenum[1], datenum[2], datenum[3], datenum[4], datenum[5] ) elseif datecode:sub(1,3)=='YMD' then timeStamp = string.format('%04i-%02i-%02i', datenum[1], datenum[2], datenum[3] ) datecode = 'YMD' -- 'YMDhms', 'YMDhm' and 'YMD' are the only supported format starting with 'YMD'; all others will be converted to 'YMD'. elseif datecode:sub(1,2) == 'YM' then timeStamp = string.format('%04i-%02i', datenum[1], datenum[2] ) elseif datecode:sub(1,1)=='Y' then timeStamp = string.format('%04i', datenum[1] ) datecode = 'Y' -- date starting by a month (the implied year is 2000) elseif datecode== 'MDhms' then timeStamp = string.format('%04i-%02i-%02i %02i:%02i:%02i', 2000, datenum[2], datenum[3], datenum[4], datenum[5], datenum[6] ) elseif datecode == 'MDhm' then timeStamp = string.format('%04i-%02i-%02i %02i:%02i', 2000, datenum[2], datenum[3], datenum[4], datenum[5] ) elseif datecode:sub(1,2) == 'MD' then timeStamp = string.format('%04i-%02i-%02i', 2000, datenum[2], datenum[3] ) datecode = 'MD' -- 'MDhms', 'MDhm' and 'MD' are the only supported format starting with 'MD'; all others will be converted to 'MD' elseif datecode:sub(1,1) == 'M' then -- Ambiguous: could mean minutes, but here means month (when parsed as a name/abbrev, not as a number). timeStamp = string.format('%04i-%02i-%02i', 2000, datenum[2], 1 ) -- other possible but unrecognized formats (e.g. 'DHis', 'DHi', 'D', 'His', 'Hi'); -- note that 'Dh', 'D', 'h', 's' may eventually work, but not 'm' for minute only, which is ambiguous with 'M' for month only. else timeStamp = nil -- format not supported end return timeStamp, datecode end --------------------------------------------------------------------------------------- -- trim leading zeros in years prior to year 1000 -- INPUT: -- * datestr - translated date string -- * lang - language of translation -- OUTPUT: -- * datestr - updated date string local function trimYear(datestr, year, lang) local yearStr0, yearStr1, yearStr2, zeroStr yearStr0 = string.format('%04i', year ) -- 4 digit year in standard form "0123" yearStr1 = mw.language.new(lang):formatDate( 'Y', yearStr0) -- same as calling {{#time}} parser function --yearStr1 = mw.getCurrentFrame():callParserFunction( "#time", { 'Y', yearStr0, lang } ) -- translate to a language if yearStr0==yearStr1 then -- most of languages use standard form of year yearStr2 = tostring(year) else -- some languages use different characters for numbers yearStr2 = yearStr1 zeroStr = mw.ustring.sub(yearStr1,1,1) -- get "0" in whatever language for i=1,3 do -- trim leading zeros if mw.ustring.sub(yearStr2,1,1)==zeroStr then yearStr2 = mw.ustring.sub(yearStr2, 2, 5-i) else break end end end return string.gsub(datestr, yearStr1, yearStr2 ) -- in datestr replace long year with trimmed one end --------------------------------------------------------------------------------------- -- Look up proper format string to be passed to {{#time}} parser function -- INPUTS: -- * datecode: YMDhms, YMDhm, YMD, YM, Y, MDhms, MDhm, MD, or M -- * day : Number between 1 and 31 (not needed for most languages) -- * lang : language -- OUTPUT: -- * dFormat : input to {{#time}} function local function getDateFormat(datecode, day, lang) local function parseFormat(dFormat, day) if dFormat:find('default') and #dFormat>10 then -- Special (and messy) case of dFormat code depending on a day number, where data is a -- JSON-encoded table {”default”:”*”,”dDD”:”*”} including fields for specific 2-digit days. -- Change curly double quotes (possibly used for easier editing in tabular data) in dFormat -- to straight ASCII double quotes (required for parsing of this JSON-encoded table). local D = mw.text.jsonDecode(mw.ustring.gsub(dFormat, '[„“‟”]', '"')) --com = mw.dumpObject(D) -- If the desired day is not in that JSON table, then use its "default" case. dFormat = D[string.format('d%02i', day)] or D.default -- Change ASCII single quotes to ASCII double quotes used for {{#time}} marking. -- Apostrophes needed in plain-text must not use ASCII single quotes but curly apostrophe -- e.g. { ‟default”: ‟j”, ‟d01”: ‟j’'o'” }, not { ‟default”: ‟j”, ‟d01”: ‟j''o'” }. end dFormat = dFormat:gsub("'", '"') return dFormat end local T = {} local tab = mw.ext.data.get('DateI18n.tab', lang) for _, row in pairs(tab.data) do -- convert the output into a dictionary table local id, _, msg = unpack(row) T[id] = msg end -- Compatibility of legacy data using 'HMS' or 'HM', where 'M' is ambiguous T.YMDhms = T.YMDhms or T.YMDHMS T.YMDhm = T.YMDhm or T.YMDHM datecode = datecode == 'YMDHMS' and 'YMDhms' or datecode == 'YMDHM' and 'YMDhm' or datecode local dFormat = T[datecode] if dFormat == 'default' and (datecode == 'YMDhms' or datecode == 'YMDhm') then -- For most languages adding hour:minute:second is done by adding ", HH:ii:ss to the -- day precission date, those languages are skipped in DateI18n.tab and default to -- English which stores word "default" dFormat = parseFormat(T['YMD'], day).. ', H:i' if datecode == 'YMDhms' then dFormat = dFormat .. ':s' end else dFormat = parseFormat(dFormat, day) end return dFormat end --------------------------------------------------------------------------------------- -- Look up proper format string to be passed to {{#time}} parser function -- INPUTS: -- * month : month number -- * case : gramatic case abbriviation, like "ins", "loc" -- * lang : language -- OUTPUT: -- * dFormat : input to {{#time}} function local function MonthCase(month, case, lang) local T = {{},{},{},{},{},{},{},{},{},{},{},{}} local tab = mw.ext.data.get('I18n/MonthCases.tab', lang) for _, row in pairs(tab.data) do local mth, cs, msg = unpack(row) T[mth][cs] = msg end return T[month][case] end -- ================================================== -- === External functions =========================== -- ================================================== local p = {} --[[ ======================================================================================== Date This function is the core part of the ISOdate template. Usage: local Date = require('Module:DateI18n')._Date local dateStr = Date({2020, 12, 30, 12, 20, 11}, lang) Parameters: * {year,month,day,hour,minute,second, tzhour, tzmin}: broken down date-time component strings or numbers tzhour, tzmin are timezone offsets from UTC, hours and minutes * lang: The language to display it in * case: Language format (genitive, etc.) for some languages * class: CSS class for the <time> node, use "" for no metadata at all ]] function p._Date(datevec, lang, case, class, trim_year) -- make sure inputs are in the right format if not lang or not mw.language.isValidCode( lang ) then lang = mw.getCurrentFrame():callParserFunction( "int", "lang" ) -- get user's chosen language end if lang == 'be-tarask' then lang = 'be-x-old' end -- process datevec and extract timeStamp and datecode strings as well as numeric datenum array local datecode, datenum = parserDatevec(datevec) local year, month, day = datenum[1], datenum[2], datenum[3] local timeStamp, datecode = getTimestamp(datecode, datenum) if not timeStamp then -- something went wrong in parserDatevec return '' end -- Commons [[Data:DateI18n.tab]] page stores prefered formats for diferent -- languages and datecodes (specifying year-month-day or just year of month-day, etc) -- Look up country specific format input to {{#time}} function local dFormat = getDateFormat(datecode, day, lang) -- By default the gramatical case is not specified (case=='') allowing the format to be specified -- in [[Data:DateI18n.tab]]. You can overwrite the default grammatical case of the month by -- specifying "case" variable. This is needed mostly by Slavic languages to create more complex -- phrases as it is done in [[c:Module:Complex date]] case = case or '' if (lang=='qu' or lang=='qug') and case=='nom' then -- Special case related to Quechua and Kichwa languages. The form in the I18n is -- Genitive case with suffix "pi" added to month names provided by {#time}} -- in Nominative case that "pi" should be removed -- see https://commons.wikimedia.org/wiki/Template_talk:Date#Quechua from 2014 dFormat = dFormat:gsub('F"pi"', 'F') elseif case == 'gen' then dFormat = strReplace(dFormat, "F", "xg") elseif case == 'nom' then dFormat = strReplace(dFormat, "xg", "F") elseif case ~= '' then -- see is page [[Data:I18n/MonthCases.tab]] on Commons have name of the month -- in specific gramatic case in desired language. If we have it than replace -- "F" and xg" in dFormat local monthMsg = MonthCase(month, case, lang) if monthMsg and monthMsg ~= '' then -- make sure it exists dFormat = strReplace(dFormat, 'F', '"'..monthMsg..'"') -- replace default month with month name we already looked up dFormat = strReplace(dFormat, 'xg', '"'..monthMsg..'"') end end -- Translate the date using specified format. -- See https://www.mediawiki.org/wiki/Extension:Scribunto/Lua_reference_manual#mw.language:formatDate and -- https://www.mediawiki.org/wiki/Help:Extension:ParserFunctions##time for explanation of the format local langObj = mw.language.new(lang) local datestr = langObj:formatDate(dFormat, timeStamp) -- same as using {{#time}} parser function -- Special case related to Thai solar calendar: prior to 1940 new-year was at different time of year, -- so just year (datecode == 'Y') is ambiguous and is replaced by "YYYY or YYYY" phrase if lang=='th' and datecode=='Y' and year<=1940 then datestr = string.format('%04i หรือ %04i', year+542, year+543 ) end -- If year < 1000 than either keep the date padded to the length of 4 digits or trim it. -- Decide if the year will stay padded with zeros (for years in 0-999 range). if year and year < 1000 then trim_year = yesno(trim_year, '100-999') if type(trim_year) == 'string' then -- If `trim_year` not a simple boolean, then it's a range of dates. -- For example '100-999' means to pad 1-or-2-digit years to be 4-digit long, while keeping 3-digit years as is. local YMin, YMax = trim_year:match( '(%d+)-(%d+)' ) trim_year = YMin and year >= tonumber(YMin) and year <= tonumber(YMax) end if trim_year then datestr = trimYear(datestr, year, lang) -- in datestr replace long year with trimmed one end end -- Append a timezone if present (after the hour and minute of the day). if datenum[7] and (datecode.sub(1, 5) == 'YMDhm' or datecode.sub(1, 4) == 'MDhm') then -- Use {{#time}} parser function to create timezone string, so that we use the correct character set. local sign = (datenum[7]<0) and '−' or '+' timeStamp = string.format("2000-01-01 %02i:%02i:00", math.abs(datenum[7]), datenum[8] or 0) local timezone = langObj:formatDate('H:i', timeStamp) -- same as using {{#time}} parser function datestr = string.format("%s %s%s", datestr, sign, timezone ) end -- HTML formating of date string and tagging for microformats (only for absolute dates with a year). if class and class ~= '' and datecode.sub(1,1) == 'Y' then -- datestr = ('<time class="%s" datetime="%s" lang="%s" dir="%s" style="white-space:nowrap">%s</time>') :format(class, timeStamp, lang, langObj:getDir(), datestr) end return datestr end --[[ ======================================================================================== Date This function is the core part of the ISOdate template. Usage: {{#invoke:DateI18n|Date|year=|month=|day=|hour=|minute=|second=|tzhour=|tzmin=|lang=en}} Parameters: * year, month, day, hour, minute, second: broken down date-time component strings * tzhour, tzmin: timezone offset from UTC, hours and minutes * lang: The language to display it in * case: Language format (genitive, etc.) for some languages * class: CSS class for the <time> node, use "" for no metadata at all ]] function p.Date(frame) local args = {} for name, value in pairs( frame.args ) do name = string.gsub( string.lower(name), ' ', '_') args[name] = value end return p._Date( { args.year, args.month, args.day, args.hour, args.minute, args.second, args.tzhour, args.tzmin }, args.lang, -- language args.case, -- allows to specify grammatical case for the month for languages that use them args.class or 'dtstart', -- allows to set the html class of the time node where the date is included. This is useful for microformats. args.trim_year or '100-999' -- by default pad one and 2 digit years to be 4 digit long, while keeping 3 digit years as is ) end return p