Module:Date complexe

La documentation pour ce module peut être créée à Module:Date complexe/doc

-- TODO: améliorer les synergies avec Module:Date (gestion par module:Date de dates sans lien et de "XIe siècle en astronautique"

local datemodule = require 'Module:Date'
local linguistic -- = require 'Module:Linguistique' -- chargé uniquement si nécessaire
local roman -- = require 'Module:Romain' -- chargé uniquement si nécessaire
local p = {}

local numericprecision = { -- convertir les précisions en valeurs numériques = à celles utilisées par Wikidata
	gigayear = 0,
	megayear = 3,
	millenium = 6,
	century = 7,
	decade = 8,
	year = 9,
	month = 10,
	day = 11,
	hour = 12,
	minute = 13,
	second = 14,
}

local avJC = '<abbr class="abbr nowrap" title="avant Jésus-Christ"> av. J.-C.</abbr>'

local function vowelfirst(str)
	linguistic = linguistic or require 'Module:Linguistique'
	return linguistic.vowelfirst(str)
end

function p.dateObject(orig, params)
	--[[ transforme un snak en un nouvel objet utilisable par des fonctions comme p.setprecision
		{type = 'dateobject', timestamp = str, era = '+' ou '-', year = number, month = number, day = number, calendar = calendar}
	]]--
	if not params then
		params = {}
	end

	local newobj = p.splitDate(orig.time, orig.calendarmodel)

	newobj.precision = params.precision or orig.precision
	newobj.type = 'dateobject'
	return newobj
end

function p.rangeObject(begin, ending, params)
	--[[
		objet comportant un timestamp pour le classement chronologique et deux dateobject (begin et ending)
	]]--
	local timestamp
	if begin then
		timestamp = begin.timestamp
	else
		timestamp = ending.timestamp
	end
	return {begin = begin, ending = ending, timestamp = timestamp, type = 'rangeobject'}
end

function p.objectToText(obj, params)
	if obj.type == 'dateobject' then
		return p.simplestring(obj, params)
	elseif obj.type == 'rangeobject' then
		return p.daterange(obj.begin, obj.ending, params)
	end
end

local function setprecision(obj, maxprecision)
	local precision
	if type(obj) == "string" then
		precision = tonumber(obj)
	elseif type(obj) == "number" then
		precision = obj
	elseif type(obj) == "table" then
		precision = tonumber(obj.precision) or numericprecision[obj.precision]
	end
	if not precision then
		precision = 0
	end
	-- maxprecision, surtout pour données Wikidata quand on veut afficher avec moins de précision que l'input (par exemple afficher seulement l'année)
	if maxprecision then
		maxprecision = tonumber(maxprecision) or numericprecision[maxprecision]
	end
	if maxprecision then
		return math.min(precision, maxprecision)
	end
	return precision
end

local function bigDate(year, precision) -- TODO : gestion de la précision
	local val, unit = 0, ""
	if year > 999999999 then
		unit = " [[giga|G]][[Année julienne|a]]"
		val = year / 1000000000
	elseif year > 999999 then
		unit = " [[méga|M]][[Année julienne|a]]"
		val = year / 1000000
	end
	val = mw.getContentLanguage():formatNum(val)
	return val .. unit
end

local function milleniumString(millenium, era, hideera)
	roman = roman or require 'Module:Romain'
	local str = roman.toRoman(millenium) .. '<sup>e</sup> millénaire'
	if era == '-' and (not hideera) then
		str = str .. avJC
	end
	return str
end

local function centuryString(century, era, hideera)
	roman = roman or require 'Module:Romain'
	local str = roman.toRoman(century) .. '<sup>e</sup> siècle'
	if era == '-' and (not hideera) then
		str = str .. avJC
	end
	return str
end

local function decadeString(decade, era, hideera)
	local str = 'années ' .. decade .. '0'
	if era == '-' and (not hideera) then
		str = str .. ' av. J.-C.|' .. str .. avJC
	end
	return '[[' .. str .. ']]'
end

function p.simplestring(dateobject, displayformat)

	-- transforme un object date ponctuel en texte
	-- les dates de type ISO devraient passer par Module:Date, mais il faut pouvoir désactiver les liens
	if type(dateobject) == 'string' or type(dateobject) == 'nil' then
		return dateobject
	end

	-- si le date object comporte déjà le texte souhaité on le retourne
	if dateobject.string then
		return dateobject.string
	end

	if (not dateobject.year) and (not dateobject.month) and dateobject.day then -- si seul le jour est passé, par exemple à cause de removeclutter, le format n'est pas pris en charge par module:Date
		if displayformat.precision and numericprecision[displayformat.precision] < 11 then
			return ''
		else
			return tostring(dateobject.day)
		end
	end

	local era = dateobject.era

	if not displayformat then
		displayformat = {}
	end
	local linktopic = displayformat.linktopic
	local nolinks
	if linktopic == '-' then
		nolinks = true
	end

	local str
	local precision = setprecision(dateobject, displayformat.precision)

	-- formats gérés par ce module
	local year = tonumber(dateobject.year) or 0

	if year > 999999 then -- grosses dates pour l'astronomie, la paléontologie
		return bigDate(year, precision)
	end

	local hideera = displayformat.hideera

	if precision == 6 then
		local millenium = math.floor((year - 1)/1000) + 1
		str = milleniumString(millenium, era, hideera)
	elseif precision == 7 then
		local century = math.floor((year - 1)/100) + 1
		str = centuryString(century, era, hideera)
	elseif precision == 8 then
		local decade = tostring(math.floor(year/10))
		str = decadeString(decade, era, hideera)
	end
	if str then
		return str
	end

	-- formats gérés par Module:Date
	local year = dateobject.year
	if year and (era == '-') then
		year = 0 - year
	end
	local month, day

	if precision > 9 then
		month = dateobject.month
		if precision > 10 then
			day = dateobject.day
		end
	end

	local avJC -- équivalent de hideera pour modeleDate
	if displayformat.hideera then
		avJC = 'non'
	end
	str = datemodule.modeleDate{jour = day, mois = month, annee = year, qualificatif = linktopic, nolinks = nolinks, avJC = avJC, liens = true}
	return str or ''
end

local function fromToNow(datestr, precision) -- retourne "depuis" plutôt que "à partir de" quand ce n'est pas terminé
	if (precision >= 11) or (precision == 7) or (precision == 6) then -- on dit "depuis le" pour les dates avec jour, les siècles, les millénaires
		if vowelfirst(datestr) then -- suppose l'absence de lien interne
			return "depuis l'" .. datestr
		else
			return "depuis le " .. datestr
		end
	end
	if (precision == 8) then -- on dit "depuis les" pour les décennies ("années ...")
		return "depuis les " .. datestr
	end
	return "depuis " .. datestr
end

local function fromdate(d, displayformat) -- retourne "à partir de date" en langage naturel
	displayformat = displayformat or {}
	local precision = setprecision(d, displayformat.precision)
	local datestr = p.simplestring(d, displayformat)
	if displayformat and displayformat.textformat == 'minimum' then
		return datestr -- par exemple pour les classements MH, juste afficher la date de début
	end
	if displayformat and displayformat.textformat == 'short' then
		return datestr .. '&nbsp;&ndash;&nbsp;' -- pour certaines infobox (footballeur par exemple), afficher date de début et un tiret
	end
	if p.before(os.date("!%Y-%m-%dT%TZ"), d) and (displayformat.stilltrue ~= "?") and (displayformat.stilltrue ~= false) then
		return fromToNow(datestr, precision)
	end
	if (precision >= 11) or (precision == 7) or (precision == 6) then -- on dit "à partir du" pour les dates avec jour, les siècles, les millénaires
		return 'à partir du ' .. datestr
	end
	if (precision == 10) and (vowelfirst(datemodule.determinationMois(d.month))) then
		return "à partir d'" .. datestr
	end
	if (precision == 8) then -- on dit "à partir des" pour les décennies
		return 'à partir des ' .. datestr
	end
	return 'à partir de ' .. datestr
end

local function upto(d, displayformat) -- retourne "jusqu'à date' en langage naturel
	displayformat = displayformat or {}
	local datestring = p.simplestring(d, displayformat)
	local precision = setprecision(d, displayformat.precision)
	if displayformat and displayformat.textformat == 'infobox' then
		return '&nbsp;&ndash;&nbsp;'.. datestring -- pour certaines infobox (footballeur par exemple), afficher date de début et un tiret
	end
	if displayformat and displayformat.textformat == 'short' then
		return'&nbsp;&ndash;&nbsp;' .. datestring -- pour certaines infobox (footballeur par exemple), afficher date de début et un tiret
	end
	if (precision >= 11) or (precision == 7) or (precision == 6) then -- on dit "jusqu'au" pour les dates avec jour, et pour les siècles
		return "jusqu'au " .. datestring
	elseif (precision > 9) then
		return "jusqu'à " .. datestring
	elseif (precision == 8) then
		return "jusqu'aux " .. datestring
	else
		return "jusqu'en " .. datestring
	end
end

local function fromuntillong(startstr, endstr, era, startprecision, endprecision)
	-- on dit "du 3 au 14 janvier" mais "de septembre à octobre"
	local longstartstr
	if startprecision >= 11 then -- >= day
		longstartstr = "du " .. startstr
	elseif startprecision == 8 then -- == décennie ("années")
		longstartstr = "des " .. startstr
	else
		if vowelfirst(startstr) then
			longstartstr = "d'" .. startstr
		else
			longstartstr = "de " .. startstr
		end
	end
	local longendstr
	if endprecision >= 11 then -- >= day
		longendstr = " au " .. endstr .. era
	elseif endprecision == 8 then -- == décennie ("années")
		longendstr = " aux " .. endstr .. era
	else
		longendstr = " à " .. endstr .. era
	end
	return longstartstr .. longendstr
end

local function removeclutter(startpoint, endpoint, precision, displayformat) -- prépare à rendre la date plus jolie : "juin 445 av-JC-juillet 445 av-JC -> juin-juillet 445-av-JC"
	if (type(startpoint) ~= 'table') or (type(endpoint) ~= 'table') then
		return startpoint, endpoint, precision, displayformat
	end
	local era = endpoint.era
	local sameera = false
	if startpoint.era == endpoint.era then
		sameera = true
	end
	if sameera and (endpoint.year == startpoint.year) then
		startpoint.year = nil
		if (startpoint.month == endpoint.month) then
			startpoint.month = nil
			if (startpoint.day == endpoint.day) then
				startpoint.day = nil
			end
		end
	end
	return startpoint, endpoint, era, displayformat, sameera
end

function p.between(startpoint, endpoint, displayformat)
	displayformat = displayformat or {}
	local precision = setprecision(endpoint, displayformat.precision) or 9

	local startpoint = p.simplestring(startpoint, displayformat)
	local endpoint = p.simplestring(endpoint, displayformat)

	if not (startpoint or endpoint) then
		return nil
	end
	if not endpoint then
		if precision <= 10 then
			return "après " .. startpoint
		else
			return "après le " .. startpoint
		end
	end
	if not startpoint then
		if precision <= 10 then
			return "avant " .. endpoint
		else
			return "avant le " .. endpoint
		end
	end

 	-- analyse les paramètres pour éviter les redondances

	local startpoint, endpoint, era, displayformat, sameera = removeclutter(startpoint, endpoint, precision, displayformat)

	local startstr, endstr = p.simplestring(startpoint, displayformat), p.simplestring(endpoint, displayformat)
	displayformat.hideera = true

	if (startstr == '') or (startstr == endstr) then
		if (not sameera) then
			displayformat.hideera = false -- sinon c'est incompréhensible
			return p.simplestring(endpoint, displayformat)
		end
		return endstr
	end
	-- pour éviter les tournures répétitives comme "du 13 septembre 2006 au 18 septembre 2006"
	if era == "-" then
		era = avJC
	else
		era = ""
	end

	if precision <= 10 then
		return "entre " .. startstr .. " et " .. endstr .. era
	else
		return "entre le " .. startstr .. " et le " .. endstr .. era
	end
end

local function fromuntil(startpoint, endpoint, displayformat)
	displayformat = displayformat or {}
	local startprecision = setprecision(startpoint, displayformat.precision)
	local endprecision = setprecision(endpoint, displayformat.precision)

 	-- analyse les paramètres pour éviter les redondances

	local startpoint, endpoint, era, displayformat, sameera = removeclutter(startpoint, endpoint, endprecision, displayformat)

	local hideera= displayformat.hideera
	displayformat.hideera = true -- pour les chaînes intermédiaires

	local startstr, endstr = p.simplestring(startpoint, displayformat), p.simplestring(endpoint, displayformat)

	if (startstr == '') or (startstr == endstr) then
		displayformat.hideera = hideera -- on va faire une chaîne simple, on reprend donc le format initialement demandé
		if (not sameera) then
			displayformat.hideera = false -- sinon c'est incompréhensible
		end
		return p.simplestring(endpoint, displayformat)
	end
	-- pour éviter les tournures répétitives comme "du 13 septembre 2006 au 18 septembre 2006"
	local hasStartera = false
	if era == '-' then
		era = avJC
	else
		era = ''
		if not (sameera == nil) and not sameera then
			startstr = startstr .. avJC
			hasStartera = true
		end
	end
	if displayformat.textformat == 'long' then
		return fromuntillong(startstr, endstr, era, startprecision, endprecision)
	elseif (type(startprecision) == "number") and (startprecision > 9) or (type(endprecision) == "number") and (endprecision > 9) or hasStartera then -- si les date contiennent des mois ou jours, ou si il y a un era avant, il vaut mieux un espace
		return startstr .. ' -<wbr> ' .. endstr .. era
	else
		return startstr .. '-<wbr>' .. endstr .. era
	end
end

function p.daterange(startpoint, endpoint, displayformat)
	local result
	if startpoint and endpoint then
		result = fromuntil(startpoint, endpoint, displayformat)
	elseif startpoint then
		result = fromdate(startpoint, displayformat)
	elseif endpoint then
		result = upto(endpoint, displayformat)
	else
		result = nil
	end
	if result and displayformat and displayformat.ucfirst and displayformat.ucfirst ~= '-' then
		linguistic = linguistic or require 'Module:Linguistique'
		result = linguistic.ucfirst(result)
	end
	return result
end

function p.duration(start, ending)
	if (not start) or (not ending) then
		return nil -- ?
	end
	return datemodule.age(start.year, start.month, start.day, ending.year, ending.month, ending.day)
end

local function splitWDdate(str) -- depuis datavalue.value.time de Wikidata, fonctionnerait aussi en utilisant simplement splitISO
	local pattern = "(%W)(%d+)%-(%d+)%-(%d+)"
	local era, year, month, day = str:match(pattern)
	return era, year, month, day
end

local function splitISO(str)
	str = mw.text.trim(str)
	local era, year, month, day
	era = string.sub(str, 1, 1)
	if tonumber(era) then
		era = '+'
	end
	local f = string.gmatch(str, '%d+')
	year, month, day = f(), f(), f()
	return era, year, month, day

end
function p.splitDate(orig, calendar)
	if not orig then
		return nil
	end
	if type(orig) == 'table' then
		return orig
	end
	if type(orig) ~= 'string' then
		return error("bad datatype for date, string expected, got " .. type(orig))
	end
	local era, y, m, d = splitWDdate(orig)
	if not era then
		era, y, m, d = splitISO(orig)
	end

	y, m, d = tonumber(y or 1), tonumber(m or 1), tonumber(d or 1)
	return {day = d, month = m, year = y, era = era, type = 'dateobject', calendar = calendar}
end

function p.before(a, b) -- return true if b is before a or if at least one of a or b is missing
	a = p.splitDate(a)
	b = p.splitDate(b)
	if (not a) or (not b) then
		return true
	end
	local order = {'year', 'month', 'day'}
	if a['era'] == '+' then
		if b['era'] == '+' then
			for i, j in ipairs(order) do
				if b[j] < a[j] then
					return true
				elseif b[j] > a[j] then
					return false
				end
			end
		else -- b -
			return true
		end
	else -- a -
		if b['era'] == '+' then
			return false
		else -- b -
			for i, j in ipairs(order) do
				if b[j] > a[j] then
					return true
				elseif b[j] < a[j] then
					return false
				end
			end
		end
	end
	return true
end

function p.equal(a, b, precision)
	a = p.splitDate(a)
	b = p.splitDate(b)

	if type(precision) == "string" then
		precision = tonumber(precision) or numericprecision[mw.text.trim(precision)]
	end

	if not precision then
		precision = 11 -- day by default ?
	end

	if (not a) or (not b) then
		return true
	end

	if a.era and b.era and (b.era ~= a.era) then
		return false
	end

	if (precision >= 11) then
		if a.day and b.day and (b.day ~= a.day) then
			return false
		end
	end

	if (precision >= 10) then
		if a.month and b.month and (b.month ~= a.month) then
			return false
		end
	end

	if (precision >= 9) then
		if a.year and b.year and (b.year ~= a.year) then
			return false
		end
	end

	return true
end

return p