Moduuli:Wd:Date

Wikipediasta
Siirry navigaatioon Siirry hakuun

--[[  
 
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 and template:ISOyear

]]

 
local p = {}

-- =======================================
-- === Dependencies ======================
-- =======================================
local i18n     = require('Module:Wd:i18n/date')		-- get localized translations of date formats
local Fallback = require('Module:Wd:Fallback')			-- get fallback functions
local yesno    = require('Module:Yesno')

--[[
ISOyear
 
This function returns year part of date string.
 
Usage:
{{#invoke:Date|ISOyear|target_string}}
 
Parameters
    1: The date string 
 
Error Handling:
   If the string does not look like it contain the year than the function will not return anything.
   That is the preferred treatment for the template:Creator which is the main (only?) template calling it.
]]
function p.ISOyear( frame )
	local input = frame.args[1]
	if not input then
		input = frame.args["s"]
	end
	input = mw.text.trim( input )
    
	-- if empty string then return it
	if input == "" then
		return input
	end
    
	-- if number then return it
	if tonumber( input ) then
		return mw.ustring.format( '%04i', input )
	end
    
	-- otherwise use regular expression match
	input = mw.ustring.match( input, '^(-?%d%d?%d?%d?)-' )
	if input then
		return mw.ustring.format( '%04i', input )
	else
		return ''
	end
end

--[[
ISOdate
 
This function is the core part of the ISOdate template. 
 
Usage:
{{#invoke:Date|ISOdate|target_string|lang=}}
 
Parameters:
     1: The date string 
  lang: The language to display it in
  form: Language format (genitive, etc.) for some languages
 class: CSS class for the <time> node

 Error Handling:
   If the string does not look like it contain the proper ISO date than the function will return the original string.
   
   That is the preferred treatment for the template:Information (and similar templates) which calling it.
]]
function p.ISOdate(frame)

	local dateRegex = {
		"^%d%d?%d?%d?",							-- year will be 1 to 4 digits long
		"^%d%d?%d?%d?-(%d%d?)",					-- month will be 1 or 2 digits long
		"^%d%d?%d?%d?-%d%d?-(%d%d?)",			-- day will be 1 or 2 digits long
		"^%d+-%d+-%d+[ T](%d%d?)",				-- hour will be 1 or 2 digits long
		"^%d+-%d+-%d+[ T]%d%d?:(%d%d?)",		-- minutes will be 1 or 2 digits long
		"^%d+-%d+-%d+[ T]%d%d?:%d%d?:(%d%d?)",	-- seconds will be 1 or 2 digits long
	}
	
	-- create datevec based on which variables are provided
	local input = mw.text.trim( frame.args[1] )
	local datevec = {'','','','','',''}
	local k = 6
	for i, Regex in ipairs( dateRegex ) do
		datevec[i] = input:match( Regex ) or ''
		if datevec[i]=='' then
			k = i-1
			break
		end
	end
	if k==0 then
		-- quickly return if input does not look like date (it could be a template)
		return input
	end

	-- handle the "tail" of the string or the part after the date
	local tail = input:gsub( dateRegex[k], '' )	-- grab "input" without the part of the string we managed to parse
	tail = tail:gsub('Z\s*' ,'')				-- some dates like 1999-08-18 08:15:30Z have a "Z" tagging at the end
	if tail~='' then							-- if there is a tail than add a space in front to separate it from the date
		tail = ' ' .. tail
	end

	-- format date string
	local succeded, datestr
	succeded, datestr = pcall( 
		p._Date,							-- call p._Date function with following arguments:
		datevec,							-- {year, month, day, hour, minute, second} strings packed in a vector
		frame.args["lang"],					-- language
		frame.args["case"]  or '',			-- allows to specify grammatical case for the month for languages that use them
		frame.args["class"] or 'dtstart',	-- allows to set the html class of the time node where the date is included. This is useful for microformats.
		frame.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
	)
	if succeded and datestr~='' then
		return datestr .. tail
	else
		-- in case of errors return the original string
		return input
	end	
end

function p.BC(datestr, lang) -- adds "BC", or equivalent, to date < 0
	local val = require('Module:Wd:Fallback')._langSwitch( {
		ar = function() return datestr .. '&nbsp;ق.م.' end,
		bn = function() return datestr .. '&nbsp;খ্রিস্টপূর্ব' end,
		ca = function() return datestr .. '&nbsp;aC' end,
		cs = function() return datestr .. 'př.&nbsp;n.&nbsp;l.' end,
		de = function() return datestr .. 'v.&nbsp;Chr.' end,
		el = function() return datestr .. 'π.Χ.' end,
		en = function() return datestr .. '&nbsp;BC' end,
		es = function() return datestr .. '&nbsp;a.C.' end,
		fa = function() return datestr .. '&nbsp;پ.م.' end,
		fi = function() return datestr .. '&nbsp;eaa.' end,
		fr = function() return datestr .. '&nbsp;av.&nbsp;J.-C..' end,
		gl = function() return datestr .. '&nbsp;a.C.' end,
		he = function() return datestr .. 'לפני הספירה' end,
		it = function() return datestr .. '&nbsp;a.C.' end,
		ja = function() return '紀元前' .. datestr end,
		mk = function() return datestr .. '&nbspп.н.е.' end,
		ml = function() return 'ക്രി.മു.&nbsp;' ..datestr end,
		nds = function() return datestr .. '&nbsp;vör uns Tied' end,
		nl = function() return datestr .. '&nbsp;v.Chr.' end,
		nn = function() return datestr .. '&nbsp;f.Kr.' end,
		no = function() return datestr .. '&nbsp;f.Kr.' end,
		pl = function() return datestr .. '&nbsp;p.n.e.' end,
		ro = function() return datestr .. '&nbsp;î.Hr.' end,
		ru = function() return datestr .. '&nbsp;ق;до&nbsp;н.&nbsp;э.' end,
		sl = function() return datestr .. '&nbsp;;pr. n. št.' end,
		sv = function() return datestr .. '&nbsp;f.Kr.' end,
		vi = function() return datestr .. '&nbsp;TCN' end,
		zh = function() return '前' ..datestr end,
}, lang)
	return val()
end
function p.parsedatestring(datestring) -- takes a Wikibase string and makes it a table
	if not datestring then return nil end
	if type(datestring) ~= 'string' or (#datestring ~= 28) then
		return error("invalid date provided (datatype(" .. type(datestring) .. ')' )
	end
	return {
	string.sub(datestring, 9, 12), --year
	string.sub(datestring, 14, 15), -- month
	string.sub(datestring, 17, 18),  -- day
	string.sub(datestring, 20, 21), -- hour
	string.sub(datestring, 23, 24), -- minute
	string.sub(datestring, 26, 27) --second
	}, string.sub(datestring, 1, 1) --BC or AD
end


function p.Wikibasedate(datedata, lang, case, class, trim_year)	
	-- convert Wikibase date string into year month day minute seconds
	if not datedata then
		return nil
	end
	if not lang then 
		return error("language not provided")
	end
	if type(datedata) ~= 'table' or (not datedata.time) then  
		return error("invalid date structure, should be a Wikibase time-type snak")
	end
	local datestring = datedata.time
	local precision = tonumber(datedata.precision)
	local calendar = 'gregorian'
	local datetable, era = p.parsedatestring(datestring)
	if datedata.calendarmodel == 'http://www.wikidata.org/entity/Q1985786' then 	--todo here: convert calendards
		calendar = 'julian'
	end

	-- adapt value to precision (should be done after the conversion of the string so that we can later have finer treatment like around 1957 rather than "1950s"
	if precision < 10 then
		datetable[2] = ''
	end
	if precision < 11 then
		datetable[3] = ''
	end
	if precision < 12 then
		datetable[4] = ''
	end
	if precision < 13 then
		datetable[5] = ''
	end
	if precision < 14 then
		datetable[6] = ''
	end
	local datestr  = p._Date(datetable, lang, case, (class or 'dtstart'), trim_year)
	if era == '-' then
		return p.BC(datestr, lang)
	else
		return datestr
	end
end

--[[
Date
 
This function is the core part of the ISOdate template. 
 
Usage:
{#invoke:Date|Date|year=|month=|day=|hour=|minute=|second=|lang=en}}
 
Parameters:
     year,month,day,hour,minute,second: broken down date-time component strings
  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

 Error Handling:

]]


function p.Date(frame)
	return p._Date(	
		{ 
			frame.args["year"] or '', 
			frame.args["month"] or '',
			frame.args["day"] or '', 
			frame.args["hour"] or '', 
			frame.args["minute"] or '', 
			frame.args["second"]or ''
		},
		frame.args["lang"],                  -- language
		frame.args["case"]  or '',           -- allows to specify grammatical case for the month for languages that use them
		frame.args["class"] or 'dtstart',    -- allows to set the html class of the time node where the date is included. This is useful for microformats.
		frame.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

function p._Date(datavec, lang, case, class, trim_year)	
	-- if language is not provided than look up users language
	-- WARNING: This step should be done by the template as it does not seem to work as well here (cache issues?)
	if not lang or  not mw.language.isValidCode( lang ) then
		lang = mw.message.new( "lang" ):plain()
	end
	
	-- Just in case someone broke the internationalization code than fix the english defaults
	if i18n.DateLang['en'] == nil then
		i18n.DateLang['en'] = 'en-form'
	end	
	if i18n.DateFormat['en-form'] == nil then
		i18n.DateFormat['en-form'] = {YMDHMS='j F Y, H:i:s', YMDHM='j F Y, H:i', YMD='j F Y', YM='F Y', MD='j F', Y='Y'}
	end

	-- create datecode based on which variables are provided and check for out of bound values
	local maxval = {9999, 12, 31, 23, 59, 60} -- max values for year, month, ...
	local c = {'Y', 'M', 'D', 'H', 'M', 'S'}
	local datecode = '' -- a string signifying which combination of variables was provided
	local datenum = {}  -- date-time encoded as a vector = [year, month, ... , second]
	for i, v in ipairs( datavec ) do
		if v~= nil and v~='' then
			datecode = datecode .. c[i]
			datenum[i] = tonumber(v)
			if datenum[i]==nil and i==2 then
				-- month is not a number -> check if it is a month name in English
				v = mw.language.new('en'):formatDate( "n", v)
				datenum[i] = tonumber(v)
			end
			if datenum[i]==nil or datenum[i]>maxval[i] then
				-- Some numbers are out of range -> abort and return the empty string
				return ''
			end
		end
	end
	
	-- create time stamp string (for example 2000-02-20 02:20:20) based on which variables were provided
	local timeStamp
	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' -- 'YMD', 'YMDHMS' and 'YMDHM' are the only supported format starting with 'YMD'. All others will be converted to 'YMD'
	elseif datecode == 'YM' then
		timeStamp = string.format('%04i-%02i', datenum[1], datenum[2] )
	elseif datecode == 'Y' then
		timeStamp = string.format('%04i', datenum[1] )
	elseif datecode == 'M' then
		timeStamp = string.format('%04i-%02i-%02i', 2000, datenum[2], 1 )
		class = '' -- date not complete -> no html formating or micro-tagging of date string
	elseif datecode == 'MD' then
		timeStamp = string.format('%04i-%02i-%02i', 2000, datenum[2], datenum[3] )
		class = '' -- date not complete -> no html formating or micro-tagging of date string
	else
		return ''  -- format not supported
	end

	-- ==========================================================
	-- === Create Date String using in chosen language
	-- ==========================================================
	
	-- which form should the date take? 
	-- Use Fallback module to handle rare languages which are more likely to use different for than default EN form
	local langDateForm = Fallback._langSwitch(i18n.DateLang, lang)
	
	-- special case of French and Gallic dates, which require different date format for the 1st day of the month
	if datenum[3]==1 and (langDateForm=='fr-form' or langDateForm=='ga-form') then
		langDateForm = langDateForm .. '1' -- ordinal form for the first day of the month
	end
	-- Look up country specific format input to {{#time}} function
	local dFormat = i18n.DateFormat[langDateForm][datecode]
	
	-- overwrite default grammatical case of the month (applies mostly to Slavic languages)
	if (case=='gen') then
		-- CAUTION: at the moment i18n.DateFormat uses "F" only as month name, but this might change and this operation does not check if 'F' is in "" brackets or not, so if some language starts using 'F'  in "" than this will not work for that language
		dFormat = dFormat:gsub("F", "xg"); 
	end
	if (case=='nom') then
		-- CAUTION: at the moment i18n.DateFormat uses "xg" only as month name, but this might change and this operation does not check if 'xg' is in "" brackets or not, so if some language starts using 'xg'  in "" than this will not work for that language
		dFormat = dFormat:gsub("xg", "F");
	end
	if ((lang=='ru' or lang=='pl' or lang=='cs' or lang=='sl') and (case=='loc' or case=='ins')) or
		(lang=='fi' and (case=='ptv' or case=='ine'or case=='ela'or case=='ill') ) then
		local monthEn =  mw.language.new('en'):formatDate( "F", timeStamp) -- month name in English
		-- month name using proper case and language. It relies on messages stored in MediaWiki namespace for some cases and languages
		-- That is why this IF statement uses "lang" not "langDateForm" variable to decide
		local monthMsg =  mw.message.new( string.format('%s-%s', monthEn, case ) ):inLanguage( lang )
		if not monthMsg:isDisabled() then -- make sure it exists
			local month=monthMsg:plain()
			dFormat = dFormat:gsub('F', '"'..month..'"'); -- replace default month with month name we already looked up
			dFormat = dFormat:gsub('xg', '"'..month..'"');
		end
	end

	-- Lua only date formating using {{#time}} parser function (new)
		-- prefered call which gives "Lua error: too many language codes requested." on the [[Module talk:Date/sandbox/testcases]] page
		--local datestr = mw.language.new(lang):formatDate( dFormat, timeStamp) 
	local datestr = mw.getCurrentFrame():callParserFunction( "#time", { dFormat, timeStamp, lang } )
	
	-- Another special case related to Thai solar calendar
	if lang=='th' and datenum[1]~= nil and datenum[1]<=1940 then
		-- As of 2014 {{#time}} parser function did not resolve those cases properly
		-- See https://en.wikipedia.org/wiki/Thai_solar_calendar#New_year for reference
		-- Disable once https://bugzilla.wikimedia.org/show_bug.cgi?id=66648 is fixed
		if datecode=='Y' then -- date is ambiguous
			datestr = string.format('%04i หรือ %04i', datenum[1]+542, datenum[1]+543 ) 
		elseif datenum[2]<=3 then -- year is wrong (one too many)
			datestr = datestr:gsub( string.format('%04i', datenum[1]+543), string.format('%04i', datenum[1]+542 ) )
		end
end

	-- trim leading zeros, WIkidata compatibility to check
	-- below: long version ,check Wikidata compatiblity
	--[[
	-- If year<1000 than either keep it 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 datenum[1]~= nil and datenum[1]<1000 then
		local trim = yesno(trim_year,nil)
		if trim == nil then
			local YMin, YMax = trim_year:match( '(%d+)-(%d+)' )
			trim = (YMin~=nil and datenum[1]>=tonumber(YMin) and datenum[1]<=tonumber(YMax)) 
		end
	
		-- If the date form isn't the Thai solar calendar, don't zero pad years in the range of 100-999.  
		-- If at some point support for Islamic/Hebrew/Japanese years is added, they may need to be skipped as well. 
		if trim then
			--local yearStr1 = mw.language.new(lang):formatDate( 'Y', timeStamp)
			local yearStr1 = mw.getCurrentFrame():callParserFunction( "#time", { 'Y', timeStamp, lang } )
			--local yearStr1 = datestr:match( '%d%d%d%d' ) -- 4 digits in a row (in any language) - that must be a year
			local yearStr2 = yearStr1
			local zeroStr = mw.ustring.sub(yearStr1,1,1)
			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
			datestr = datestr:gsub( yearStr1, yearStr2 )
			--datestr = string.format('%s (%s, %s)', datestr, yearStr1, yearStr2 )
		end
	end
	]]--
	-- html formating and tagging of date string
	if class ~= '' then
		local DateHtmlTags = '<span style="white-space:nowrap"><time class="%s" datetime="%s">%s</time></span>'
		datestr = DateHtmlTags:format(class, timeStamp, datestr)
	end
	return datestr
end

return p