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 = require 'Module:Linguistique'
	return linguistic.vowelfirst(str)
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 .. 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
	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 c'est pas terminé
	if (precision >= 11) or (precision == 7) or (precision == 6)  then -- ont 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 -- ont 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 -- ont 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 -- ont 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 septembe à 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)
	if startpoint and endpoint then
		return fromuntil(startpoint, endpoint, displayformat)
	elseif startpoint then
		return fromdate(startpoint, displayformat)
	elseif endpoint then
		return upto(endpoint, displayformat)
	else
		return nil
	end
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