Difference between revisions of "Module:Date"

From CryptoWiki

wiki_crypto>Johnuniq
(update from sandbox: implement show=M (minutes) and show=s (seconds); better method to fill a partial date)
 
wiki_crypto>LD
(DIPP par eru, cf. demande.)
Line 1: Line 1:
-- Date functions for use by other modules.
local fun = {}
-- I18N and time zones are not supported.


local MINUS = '' -- Unicode U+2212 MINUS SIGN
local Outils = require 'Module:Outils'
local floor = math.floor
-- chargement de la base de données répertoriant certaines pages existant ou n'existant pas pour éviter les "ifexist".
local dataLiens
local success, resultat = pcall ( mw.loadData, 'Module:Date/Data' )
if success then
dataLiens = resultat
else
-- protection au cas où le sous-module serait mal modifié
dataLiens = { [''] = { mois = { aucun = 1000, tous = { 1773, 2014 } }, } }
end


local Date, DateDiff, diffmt  -- forward declarations
-- nettoie un paramètre non nommé (vire les espaces au début et à la fin)
local uniq = { 'unique identifier' }
-- retourne nil si le texte est vide ou n'est pas du texte. Attention c'est important pour les fonctions qui l'utilisent.
local trim = Outils.trim


local function is_date(t)
-- Fonction destinée à mettre la première lettre du mois en majuscule :
-- The system used to make a date read-only means there is no unique
-- utilisation de string car aucun mois ne commence par une lettre non ascii en français ou anglais.
-- metatable that is conveniently accessible to check.
local function ucfirst( str )
return type(t) == 'table' and t._id == uniq
return str:sub( 1, 1 ):upper() .. str:sub( 2 )
end
end


local function is_diff(t)
local modelePremier = '<abbr class="abbr" title="premier">1<sup>er</sup></abbr>'
return type(t) == 'table' and getmetatable(t) == diffmt
end


local function _list_join(list, sep)
return table.concat(list, sep)
end


local function collection()
-- liste des mois, écriture exacte et alias, en minuscule
-- Return a table to hold items.
local listeMois = {
return {
{ num = 1,  nJour = 31, abrev = 'janv.',  nom = 'janvier', alias = { 'jan.', 'janv.', 'jan', 'janv', 'january' } },
n = 0,
{ num = 2,  nJour = 29, abrev = 'fév.',  nom = 'février', alias = { 'fevrier', 'fev.', 'fev', 'fév.', 'fév', 'févr', 'févr.', 'february', 'feb', 'feb.' } },
add = function (self, item)
{ num = 3,  nJour = 31, abrev = 'mars',  nom = 'mars', alias = { 'mar.', 'mar', 'march' } },
self.n = self.n + 1
{ num = 4,  nJour = 30, abrev = 'avr.',  nom = 'avril', alias = { 'avr.', 'avr', 'apr', 'april'} },
self[self.n] = item
{ num = 5,  nJour = 31, abrev = 'mai',    nom = 'mai', alias = { 'may' } },
end,
{ num = 6,  nJour = 30, abrev = 'juin',  nom = 'juin', alias = { 'jun', 'june' } },
join = _list_join,
{ num = 7,  nJour = 31, abrev = 'juill.', nom = 'juillet', alias = { 'juil.', 'juil', 'juill.', 'juill', 'jul', 'july' } },
}
{ num = 8,  nJour = 31, abrev = 'août',  nom = 'août', alias = { 'aoû', 'aug', 'august' } },
end
{ num = 9,  nJour = 30, abrev = 'sept.',  nom = 'septembre', alias = { 'sept.', 'sept', 'sep.', 'sep', 'september' } },
{ num = 10, nJour = 31, abrev = 'oct.',  nom = 'octobre', alias = { 'oct.', 'oct', 'october' } },
{ num = 11, nJour = 30, abrev = 'nov.',  nom = 'novembre', alias = { 'nov.', 'nov', 'november' } },
{ num = 12, nJour = 31, abrev = 'déc.',  nom = 'décembre', alias = { 'decembre', 'déc.', 'dec.', 'dec', 'déc', 'december' } },
aout = { num = 8, nJour = 31, abrev = 'aout', nom = 'aout', alias = { 'aou' } },
}


local function strip_to_nil(text)
-- ajoute les noms, abréviations et alias en tant que clés de listeMois
-- If text is a string, return its trimmed content, or nil if empty.
for i = 1, 12 do
-- Otherwise return text (convenient when Date fields are provided from
local mois = listeMois[i]
-- another module which may pass a string, a number, or another type).
listeMois[tostring( i )] = mois
if type(text) == 'string' then
if i < 10 then
text = text:match('(%S.-)%s*$')
listeMois['0' .. i] = mois
end
listeMois[mois.nom] = mois
listeMois[mois.abrev] = mois
for j = 1, #mois.alias do
listeMois[mois.alias[j]] = mois
end
end
return text
end
for i = 1, #listeMois.aout.alias do
listeMois[listeMois.aout.alias[i]] = listeMois.aout
end
end


local function is_leap_year(year, calname)
local liste_saisons = {
-- Return true if year is a leap year.
{ 'printemps', 'spring', },
if calname == 'Julian' then
{ 'été', 'summer', },
return year % 4 == 0
{ 'automne', 'autumn', },
end
{ 'hiver', 'winter', },
return (year % 4 == 0 and year % 100 ~= 0) or year % 400 == 0
}
end


local function days_in_month(year, month, calname)
-- à partir d'un nom de saison (en français ou en anglais),
-- Return number of days (1..31) in given month (1..12).
-- retourne son nom canonique (exemple : "été")
if month == 2 and is_leap_year(year, calname) then
-- si non reconnu, retourne nil
return 29
function fun.determinationSaison( saison )
local s = trim( saison )
if s then
s = mw.ustring.lower( s )
for i = 1, 4 do
for j = 1, #liste_saisons[i] do
if s == liste_saisons[i][j] then
return liste_saisons[i][1]
end
end
end
end
end
return ({ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 })[month]
end
end


local function h_m_s(time)
---
-- Return hour, minute, second extracted from fraction of a day.
-- à partir d'un nom de mois (en français ou en anglais), de son numéro ou d'une abréviation,
time = floor(time * 24 * 3600 + 0.5) -- number of seconds
-- retourne son nom canonique (exemple : "juin") et son numéro (exemple : 6)
local second = time % 60
-- si non reconnu, retourne nil, nil
time = floor(time / 60)
function fun.determinationMois( mois )
return floor(time / 60), time % 60, second
local result
end


local function hms(date)
local num = tonumber( mois )
-- Return fraction of a day from date's time, where (0 <= fraction < 1)
if num then
-- if the values are valid, but could be anything if outside range.
result = listeMois[num]
return (date.hour + (date.minute + date.second / 60) / 60) / 24
else
end
local str = trim( mois )
if str then
result = listeMois[str]
if not result then
result = listeMois[mw.ustring.lower( str )]
end
end
end


local function julian_date(date)
if result then
-- Return jd, jdz from a Julian or Gregorian calendar date where
return result.nom, result.num
--  jd = Julian date and its fractional part is zero at noon
--  jdz = same, but assume time is 00:00:00 if no time given
-- http://www.tondering.dk/claus/cal/julperiod.php#formula
-- Testing shows this works for all dates from year -9999 to 9999!
-- JDN 0 is the 24-hour period starting at noon UTC on Monday
--    1 January 4713 BC  = (-4712, 1, 1)  Julian calendar
--  24 November 4714 BC = (-4713, 11, 24) Gregorian calendar
local offset
local a = floor((14 - date.month)/12)
local y = date.year + 4800 - a
if date.calendar == 'Julian' then
offset = floor(y/4) - 32083
else
else
offset = floor(y/4) - floor(y/100) + floor(y/400) - 32045
return nil, nil
end
end
local m = date.month + 12*a - 3
local jd = date.day + floor((153*m + 2)/5) + 365*y + offset
if date.hastime then
jd = jd + hms(date) - 0.5
return jd, jd
end
return jd, jd - 0.5
end
end


local function set_date_from_jd(date)
 
-- Set the fields of table date from its Julian date field.
-- fonction interne à modeleDate, pour déterminer si on peut se passer de faire un ifexist
-- Return true if date is valid.
local function existDate( dataQualificatif, annee, mois )
-- http://www.tondering.dk/claus/cal/julperiod.php#formula
local data
-- This handles the proleptic Julian and Gregorian calendars.
if mois then
-- Negative Julian dates are not defined but they work.
data = dataQualificatif.mois
local calname = date.calendar
local low, high  -- min/max limits for date ranges −9999-01-01 to 9999-12-31
if calname == 'Gregorian' then
low, high = -1930999.5, 5373484.49999
elseif calname == 'Julian' then
low, high = -1931076.5, 5373557.49999
else
else
data = dataQualificatif.annee
end
if type( data ) ~= 'table' then
-- si data n'existe pas c'est que l'on considère qu'il n'y a pas de lien.
return
return
end
end
local jd = date.jd
-- le qualificatif est remplacé par celui de la base de données, ce qui permet des alias.
if not (type(jd) == 'number' and low <= jd and jd <= high) then
local lien = annee
return
if dataQualificatif.qualificatif ~= '' then
lien = lien .. ' ' .. dataQualificatif.qualificatif
end
local seul = annee
if mois then
lien = mois .. ' ' .. lien
seul = ucfirst( mois ) .. ' ' .. annee
end
end
local jdn = floor(jd)
local aucun = tonumber( data.aucun )
if date.hastime then
if aucun and annee <= aucun then
local time = jd - jdn  -- 0 <= time < 1
-- si l'année est dans la partie 'aucun' on teste s'il y a malgré tout un lien isolé
if time >= 0.5 then   -- if at or after midnight of next day
if type( data.seul ) == 'table' then
jdn = jdn + 1
for i, v in ipairs( data.seul ) do
time = time - 0.5
if seul == v or seul == tonumber( v ) then
else
return lien
time = time + 0.5
end
end
end
-- partie aucun et pas de lien => nil
return nil
elseif type( data.tous ) == 'table' then
local tous1, tous2 = tonumber( data.tous[1] ), tonumber( data.tous[2] )
if tous1 and tous2 and annee >= tous1 and annee <= tous2 then
-- l'année est dans la partie 'tous' donc on retourne le lien
return lien
end
end
date.hour, date.minute, date.second = h_m_s(time)
else
date.second = 0
date.minute = 0
date.hour = 0
end
end
local b, c
-- l'année n'est ni dans la partie aucun, ni dans la partie tous donc il faut tester si la page existe.
if calname == 'Julian' then
local cibleLien = mw.title.new( lien )
b = 0
if cibleLien and cibleLien.exists then
c = jdn + 32082
return lien
else  -- Gregorian
local a = jdn + 32044
b = floor((4*a + 3)/146097)
c = a - floor(146097*b/4)
end
end
local d = floor((4*c + 3)/1461)
local e = c - floor(1461*d/4)
local m = floor((5*e + 2)/153)
date.day = e - floor((153*m + 2)/5) + 1
date.month = m + 3 - 12*floor(m/10)
date.year = 100*b + d - 4800 + floor(m/10)
return true
end
end


local function fix_numbers(numbers, y, m, d, H, M, S, partial, hastime, calendar)
---
-- Put the result of normalizing the given values in table numbers.
-- Supprime le jour de la semaine, et "le" avant une date
-- The result will have valid m, d values if y is valid; caller checks y.
function fun.nettoyageJour( jour )
-- The logic of PHP mktime is followed where m or d can be zero to mean
if type( jour ) == 'string' then
-- the previous unit, and -1 is the one before that, etc.
local nomJour = { '[Ll]undi', '[Mm]ardi', '[Mm]ercredi', '[Jj]eudi', '[Vv]endredi',
-- Positive values carry forward.
'[Ss]amedi', '[Dd]imanche', '^ *[Ll]e' }
local date
local premier = { '<abbr class="abbr ?" title="[Pp]remier" ?>1<sup>er</sup></abbr>', '1<sup>er</sup>', '1er' }
if not (1 <= m and m <= 12) then
for i = 1, #nomJour do
date = Date(y, 1, 1)
jour = jour:gsub( nomJour[i], '' )
if not date then return end
date = date + ((m - 1) .. 'm')
y, m = date.year, date.month
end
local days_hms
if not partial then
if hastime and H and M and S then
if not (0 <= H and H <= 23 and
0 <= M and M <= 59 and
0 <= S and S <= 59) then
days_hms = hms({ hour = H, minute = M, second = S })
end
end
end
if days_hms or not (1 <= d and d <= days_in_month(y, m, calendar)) then
for i = 1, #premier do
date = date or Date(y, m, 1)
jour = jour:gsub( premier[i], '1' )
if not date then return end
date = date + (d - 1 + (days_hms or 0))
y, m, d = date.year, date.month, date.day
if days_hms then
H, M, S = date.hour, date.minute, date.second
end
end
end
jour = trim( jour )
end
end
numbers.year = y
return jour
numbers.month = m
numbers.day = d
if days_hms then
-- Don't set H unless it was valid because a valid H will set hastime.
numbers.hour = H
numbers.minute = M
numbers.second = S
end
end
end


local function set_date_from_numbers(date, numbers, options)
---
-- Set the fields of table date from numeric values.
-- Sépare une chaine date en une table contenant les champs jour, mois et annee.
-- Return true if date is valid.
-- la date doit contenir le mois.
if type(numbers) ~= 'table' then
function fun.separationJourMoisAnnee( date )
return
date = trim( date )
end
if date then
local y = numbers.year  or date.year
local function erreur( periode, valeur )
local m = numbers.month  or date.month
return false, '<span class="error">' .. periode .. ' invalide (' .. valeur .. ')</span>'
local d = numbers.day    or date.day
local H = numbers.hour
local M = numbers.minute or date.minute or 0
local S = numbers.second or date.second or 0
local need_fix
if y and m and d then
date.partial = nil
if not (-9999 <= y and y <= 9999 and
1 <= m and m <= 12 and
1 <= d and d <= days_in_month(y, m, date.calendar)) then
if not date.want_fix then
return
end
need_fix = true
end
end
elseif y and date.partial then
 
if d or not (-9999 <= y and y <= 9999) then
local dateAvantCleanup = date
return
local jour, mois, annee, masquerMois, masquerAnnee, separateur
 
-- variable pour construire les regex
local j = '([0-3]?%d)'                            -- jour
local m = '([01]?%d)'                            -- mois numérique
local mmm = '([^%s%p%d]+[.]?)'                    -- mois en toute lettre
local mmm2 = '([^%s%p%d]+[.]?[-/][^%s%p%d]+[.]?)' -- mois-mois en toute lettre
local aj = '(%-?%d+)'                            -- année ou jour
local s = '[ ./-]+'                              -- séparateur simple
local sep = '([ ./-]+)'                          -- séparateur avec capture, pour le détecter deux fois
local moins = '(%-?)'                            -- signe moins pour signifier qu'il ne faut pas afficher cette donnée
 
date = fun.nettoyageJour( date )
if date == nil then
return erreur( 'Date', dateAvantCleanup )
end
end
if m and not (1 <= m and m <= 12) then
-- suppression catégorie, liens, balises
if not date.want_fix then
date = mw.ustring.gsub( date, '%[%[[Cc]at[ée]gor[yi]e?:.-%]%]', '' )
return
date = date :gsub( '%b<>', '' )
:gsub( '%[%[([^%[%]|]*)|?([^%[%]]*)%]%]', function ( l, t ) return trim( t ) or l end )
-- suppression des espaces insécables
-- nbsp
:gsub( '\194\160', ' ' )
:gsub( '&nbsp;', ' ' )
:gsub( '&#160;', ' ' )
-- narrow nbsp
:gsub( '\226\128\175', ' ' )
:gsub( '&#8239;', ' ' )
-- thin space
:gsub( '\226\128\137', ' ' )
:gsub( '&thinsp;', ' ' )
:gsub( '&#8201;', ' ' )
-- simple space
:gsub( '&#32;', ' ' )
-- plusieurs espaces
:gsub( ' +', ' ' )
-- réduction av. J-C pour simplifier un peu les regex :
:gsub( '(%d+) ?[Aa][Vv]%.? ?[Jj][ .-]*[Cc]%.?', '-%1' )
-- suppression de l'heure dans les dates ISO
:gsub( '^+?([%d-]*%d%d%-%d%d)T%d%d[%d:,.+-]*Z?$' , '%1')
 
-- test année seule
if date:match( '^'..aj..'$' ) then
annee = date:match( '^'..aj..'$' )
elseif date:match( '^'..aj..s..aj..moins..'$' ) then
-- jj/mm, mm/aaaa ou aaaa/mm
local a, separateur, b, sb = date:match( '^'..aj..sep..aj..moins..'$' )
a, b = tonumber( a ), tonumber( b )
if separateur:match( '^.+%-$' ) then
-- probablement mm/-aaaa, année av.JC
b = 0 - b
end
if  a > 12 and ( b < 1 or b > 31 ) or
b > 12 and ( a < 1 or a > 31 ) then
return erreur( 'Date', dateAvantCleanup )
elseif b < 1 or b > 31 then
mois, annee, masquerAnnee = a, b, sb
elseif a < 1 or a > 31 then
annee, mois = a, b
elseif b > 12 then
return erreur( 'Mois', b )
else
jour, mois, masquerMois = a, b, sb
end
elseif date:match( '^'..aj..sep..m..moins..'%2'..aj..moins..'$' ) then
-- jj/mm/aaaa ou aaaa/mm/jj
jour, separateur, mois, masquerMois, annee, masquerAnnee = date:match( '^'..aj..sep..m..moins..'%2'..aj..moins..'$' )
if separateur == '-' and masquerMois == '-' and masquerAnnee == '' and tonumber( annee ) > 0 then
-- date au format jj-mm--aaaa type 17-06--44 pour 17 juin 44 av. JC
masquerMois = nil
annee = 0 - annee
end
elseif date:match( '^'..j..sep..mmm..moins..'%2'..aj..moins..'$' ) then
-- jj mmm aaaa
jour, separateur, mois, masquerMois, annee, masquerAnnee = date:match( '^'..j..sep..mmm..moins..'%2'..aj..moins..'$' )
elseif date:match( '^'..mmm..s..aj..moins..'$' ) then
-- mmm aaaa
mois, separateur, annee, masquerAnnee = date:match( '^'..mmm..sep..aj..moins..'$' )
if separateur:match( '^.+%-$' ) then
annee = '-' .. annee
end
elseif date:match( '^'..mmm2..s..aj..moins..'$' ) then
-- mmm-mmm aaaa
mois, separateur, annee, masquerAnnee = date:match( '^'..mmm2..sep..aj..moins..'$' )
if separateur:match( '^.+%-$' ) then
annee = '-' .. annee
end
end
need_fix = true
elseif date:match( '^'..j..s..mmm..moins..'$' ) then
-- jj mmm
jour, mois, masquerMois = date:match( '^'..j..s..mmm..moins..'$' )
elseif date:match( '^'..mmm..s..j..', ?'..aj..'$') then
-- mmm jj, aaaa (format anglo-saxon)
mois, jour, annee = date:match( '^'..mmm..s..j..', ?'..aj..'$')
elseif date:match( '^'..mmm..'$' ) then
mois = date
else
return erreur( 'Date', dateAvantCleanup )
end
end
local jn, an = tonumber( jour ), tonumber( annee )
if jn and an and ( jn > 31 or jn < 0 or #jour >= 3 ) and an <= 31 then
-- cas notamment des date ISO 2015-06-17, -0044-06-17 et -0002-06-17
-- inversion du jour et de l'année
local temp = annee
annee = jour
jour = temp
end
return fun.validationJourMoisAnnee{
jour, mois, annee,
masquerAnnee = trim( masquerAnnee ) and true or nil,
masquerMois = ( trim( masquerAnnee ) or not annee ) and trim( masquerMois ) and true or nil,
-- or nil sert juste à éviter de trainer une valeur false dans tous les tests unitaires.
}
else
else
return
return true, {}
end
end
 
 
---
-- validationJourMoisAnnee vérifie que les paramètres correspondent à une date valide.
-- la date peut être dans les paramètres 1 à 3, ou dans des paramètres jour, mois et annee.
-- La fonction retourne true suivi d'une table avec la date en paramètres nommés (sans accent sur année)
-- ou false suivi d'un message d'erreur.
function fun.validationJourMoisAnnee( frame )
local args = Outils.extractArgs( frame )
local jour, mois, numMois, annee
local bjour = args[1] or args['jour'] or ''
local bmois = tostring( args[2] or args['mois'] or '' )
local bannee = args[3] or args['annee'] or args['année'] or ''
 
local function erreur( periode, valeur )
return false, '<span class="error">' .. periode .. ' invalide (' .. valeur .. ')</span>'
end
end
if date.partial then
 
H = nil  -- ignore any time
-- on traite l'année
M = nil
if Outils.notEmpty( bannee ) then
S = nil
annee = tonumber( bannee )
else
if annee == nil and type( bannee ) == 'string' then
if H then
-- test si l'année contient av. J.-C.
-- It is not possible to set M or S without also setting H.
annee = bannee:upper():match( '^(%d+) ?[Aa][Vv]%.? ?[Jj][ .-]*[Cc]%.?' )
date.hastime = true
annee = tonumber( annee )
else
if annee then
H = 0
annee = 0 - annee
end
if not (0 <= H and H <= 23 and
0 <= M and M <= 59 and
0 <= S and S <= 59) then
if date.want_fix then
need_fix = true
else
else
return
return erreur( 'Année', bannee )
end
end
elseif annee == 0 then
return erreur( 'Année', 0 )
end
end
else
annee = nil
end
end
date.want_fix = nil
 
if need_fix then
-- on traite le mois
fix_numbers(numbers, y, m, d, H, M, S, date.partial, date.hastime, date.calendar)
if Outils.notEmpty( bmois ) then
return set_date_from_numbers(date, numbers, options)
mois, numMois = fun.determinationMois( bmois )
end
if mois == nil then
date.year = y    -- -9999 to 9999 ('n BC' → year = 1 - n)
mois = fun.determinationSaison( bmois )
date.month = m  -- 1 to 12 (may be nil if partial)
if mois == nil then
date.day = d    -- 1 to 31 (* = nil if partial)
local mois1, sep, mois2 = bmois:match( '^([^%s%p%d]+[.]?)([-/])([^%s%p%d]+[.]?)$' )
date.hour = H    -- 0 to 59 (*)
if mois1 then
date.minute = M  -- 0 to 59 (*)
mois1 = fun.determinationMois( mois1 )
date.second = S  -- 0 to 59 (*)
mois2 = fun.determinationMois( mois2 )
if type(options) == 'table' then
if mois1 == nil or mois2 == nil then
for _, k in ipairs({ 'am', 'era', 'format' }) do
return erreur( 'Mois', bmois )
if options[k] then
end
date.options[k] = options[k]
mois = mois1 .. sep .. mois2
else
return erreur( 'Mois', bmois )
end
end
end
end
end
end
-- on traite le jour si présent
return true
if Outils.notEmpty( bjour ) then
end
if not numMois then
 
erreur( 'Date', 'jour avec saison ou plusieurs mois' )
local function make_option_table(options1, options2)
end
-- If options1 is a string, return a table with its settings, or
jour = tonumber( bjour )
-- if it is a table, use its settings.
if jour == nil then
-- Missing options are set from table options2 or defaults.
jour = tonumber( fun.nettoyageJour( bjour ) )
-- If a default is used, a flag is set so caller knows the value was not intentionally set.
end
-- Valid option settings are:
if jour == nil then
-- am: 'am', 'a.m.', 'AM', 'A.M.'
return erreur( 'Jour', bjour )
--     'pm', 'p.m.', 'PM', 'P.M.' (each has same meaning as corresponding item above)
end
-- era: 'BCMINUS', 'BCNEGATIVE', 'BC', 'B.C.', 'BCE', 'B.C.E.', 'AD', 'A.D.', 'CE', 'C.E.'
-- on valide que le jour est correct
-- Option am = 'am' does not mean the hour is AM; it means 'am' or 'pm' is used, depending on the hour,
if jour < 1 or jour > 31 then
--    and am = 'pm' has the same meaning.
return erreur( 'Jour', bjour )
-- Similarly, era = 'BC' means 'BC' is used if year <= 0.
elseif jour > listeMois[numMois].nJour then
-- BCMINUS displays a MINUS if year < 0 and the display format does not include %{era}.
return erreur( 'Jour', bjour .. ' ' .. mois )
-- BCNEGATIVE is similar but displays a hyphen.
elseif jour == 29 and numMois == 2 and annee and ( math.fmod( annee, 4 ) ~= 0 ) then
local result = { bydefault = {} }
-- l'année bisextile sur les siècles est toujours acceptée pour être compatible avec les dates juliennes.
if type(options1) == 'table' then
return erreur( 'Jour', '29 février ' .. annee )
result.am = options1.am
end
result.era = options1.era
else
elseif type(options1) == 'string' then
-- S'il n'y a pas de jour on regarde si la première lettre du mois est en majuscule
-- Example: 'am:AM era:BC' or 'am=AM era=BC'.
if bmois:match( '^%u' ) then
for item in options1:gmatch('%S+') do
-- oui, on passe la première lettre en majuscule
local lhs, rhs = item:match('^(%w+)[:=](.+)$')
mois = ucfirst( mois )
if lhs then
result[lhs] = rhs
end
end
-- s'il n'y a pas d'année non plus on retourne le mois simple
end
end
end
else
options2 = type(options2) == 'table' and options2 or {}
-- on teste le jour si présent
local defaults = { am = 'am', era = 'BC' }
if Outils.notEmpty( bjour ) then
for k, v in pairs(defaults) do
if annee then
if not result[k] then
return erreur( 'Mois', 'absent' )
if options2[k] then
result[k] = options2[k]
else
else
result[k] = v
bjour = fun.nettoyageJour( bjour )
result.bydefault[k] = true
jour = tonumber( bjour )
if jour then
if jour > 31 or jour < 1 then
annee = jour
jour = nil
else
return erreur( 'Date', 'jour seul : ' .. bjour )
end
else
return erreur( 'Jour', bjour )
end
end
end
end
end
end
end
return result
 
-- vérification de l'absence d'un décalage
if annee and annee < 13 and annee > 0 and not jour and ( tonumber( bmois ) or ( not mois and tonumber( args[4] ) ) ) then
return false, '<span class="error">année improbable (' .. annee .. ')</span>'
end
 
local resultat = {
jour = jour,
mois = mois,
numMois = numMois,
annee = annee,
masquerAnnee = args.masquerAnnee,
masquerMois = args.masquerMois,
}
return true, resultat
end
end


local ampm_options = {
-- lhs = input text accepted as an am/pm option
-- rhs = code used internally
['am']  = 'am',
['AM']  = 'AM',
['a.m.'] = 'a.m.',
['A.M.'] = 'A.M.',
['pm']  = 'am',  -- same as am
['PM']  = 'AM',
['p.m.'] = 'a.m.',
['P.M.'] = 'A.M.',
}


local era_text = {
---
-- Text for displaying an era with a positive year (after adjusting
-- émule le modèle {{m|Date}}.
-- by replacing year with 1 - year if date.year <= 0).
-- Paramètres :
-- options.era = { year<=0 , year>0 }
-- 1 : jour (numéro ou "1er") ou la date complète
['BCMINUS']    = { 'BC'    , ''    , isbc = true, sign = MINUS },
-- 2 : mois (en toutes lettres) ou spécialité de l'année
['BCNEGATIVE'] = { 'BC'    , ''    , isbc = true, sign = '-'  },
-- 3 : année (nombre)
['BC']        = { 'BC'    , ''   , isbc = true },
-- 4 : spécialité de l'année
['B.C.']      = { 'B.C.' , ''   , isbc = true },
-- julien : date dans le calendrier julien
['BCE']        = { 'BCE'   , ''    , isbc = true },
-- compact : affiche le mois sous forme d'abréviation
['B.C.E.']    = { 'B.C.E.', ''    , isbc = true },
-- avJC : non pour désactiver l'affichage de « av. J.-C. » pour les dates négatives
['AD']        = { 'BC'    , 'AD'  },
-- âge : ajoute la durée depuis cette date
['A.D.']      = { 'B.C.'  , 'A.D.' },
-- agePrefix : préfixe pour l'age, 'à ' par défaut pour les décès
['CE']        = { 'BCE'   , 'CE'  },
-- nolinks : ne met pas de lien sur la date
['C.E.']      = { 'B.C.E.', 'C.E.' },
-- afficherErreurs : en cas d'erreur, si défini à "non" ne retourne pas un message d'erreur, mais le 1er argument inchangé
}
-- categoriserErreurs : en cas d'erreur, si défini à "non" ne catégorise pas ; peut aussi être défini avec une catégorie à utiliser à la place de celle par défaut
-- naissance : ajoute la class "bday"
-- mort : ajoute la class "dday"
function fun.modeleDate( frame )
local Yesno = require 'Module:Yesno'
 
local args = Outils.extractArgs( frame )
local resultat


local function get_era_for_year(era, year)
local dateNaissanceMort
return (era_text[era] or era_text['BC'])[year > 0 and 2 or 1] or ''
end


local function strftime(date, format, options)
-- analyse des paramètres non nommés (ou paramètres de la date jour, mois, annee)
-- Return date formatted as a string using codes similar to those
local test, params
-- in the C strftime library function.
local arg1, arg2, arg3 = fun.nettoyageJour( args[1] ), trim( args[2] ), trim( args[3] )
local sformat = string.format
if type( arg1 ) == 'string' and arg3 == nil and ( arg1:match( '[^ ./-][ ./-]+[^ ./-]' ) or arg2 == nil or dataLiens[arg2] or mw.ustring.match( arg2, '%a %a' ) ) then
local shortcuts = {
-- la date est dans le premier paramètre
['%c'] = '%-I:%M %p %-d %B %-Y %{era}', -- date and time: 2:30 pm 1 April 2016
test, params = fun.separationJourMoisAnnee( arg1 )
['%x'] = '%-d %B %-Y %{era}',            -- date:          1 April 2016
if test then
['%X'] = '%-I:%M %p',                    -- time:          2:30 pm
dateNaissanceMort = trim( arg2 )
}
params.qualificatif = trim( arg2 )
if shortcuts[format] then
end
format = shortcuts[format]
elseif type( arg1 ) == 'string' and type( arg2 ) == 'string' and arg3 ~= nil and arg4 == nil and ( arg1:match( '[^ ./-][ ./-]+[^ ./-]' ) or dataLiens[arg3] or mw.ustring.match( arg3, '%a %a' ) ) then
end
-- la date est dans le premier paramètre
local codes = {
test, params = fun.separationJourMoisAnnee( arg1 )
a = { field = 'dayabbr' },
if test then
A = { field = 'dayname' },
dateNaissanceMort = trim( arg2 )
b = { field = 'monthabbr' },
params.qualificatif = trim( arg3 )
B = { field = 'monthname' },
end
u = { fmt = '%d'  , field = 'dowiso' },
else
w = { fmt = '%d'  , field = 'dow' },
local function masquerParam( p )
d = { fmt = '%02d', fmt2 = '%d', field = 'day' },
-- sépare le signe moins final éventuel signifiant que le paramètre ne doit pas être affiché.
m = { fmt = '%02d', fmt2 = '%d', field = 'month' },
if type( p ) ~= 'string' then
Y = { fmt = '%04d', fmt2 = '%d', field = 'year' },
return p, nil
H = { fmt = '%02d', fmt2 = '%d', field = 'hour' },
M = { fmt = '%02d', fmt2 = '%d', field = 'minute' },
S = { fmt = '%02d', fmt2 = '%d', field = 'second' },
j = { fmt = '%03d', fmt2 = '%d', field = 'dayofyear' },
I = { fmt = '%02d', fmt2 = '%d', field = 'hour', special = 'hour12' },
p = { field = 'hour', special = 'am' },
}
options = make_option_table(options, date.options)
local amopt = options.am
local eraopt = options.era
local function replace_code(spaces, modifier, id)
local code = codes[id]
if code then
local fmt = code.fmt
if modifier == '-' and code.fmt2 then
fmt = code.fmt2
end
end
local value = date[code.field]
local value, mask = p:match( '^%s*(.-)(%-?)%s*$' )
if not value then
return value, ( mask == '-' or nil )
return nil  -- an undefined field in a partial date
end
end
local cleanArgs = { arg1 or args.jour }
local special = code.special
cleanArgs[2], cleanArgs.masquerMois = masquerParam( args[2] or args.mois )
if special then
cleanArgs[3], cleanArgs.masquerAnnee = masquerParam( args[3] or args.annee or args['année'] )
if special == 'hour12' then
 
value = value % 12
test, params = fun.validationJourMoisAnnee( cleanArgs )
value = value == 0 and 12 or value
if test then
elseif special == 'am' then
params.qualificatif = trim( args[4] )
local ap = ({
['a.m.'] = { 'a.m.', 'p.m.' },
['AM'] = { 'AM', 'PM' },
['A.M.'] = { 'A.M.', 'P.M.' },
})[ampm_options[amopt]] or { 'am', 'pm' }
return (spaces == '' and '' or '&nbsp;') .. (value < 12 and ap[1] or ap[2])
end
end
if code.field == 'year' then
local sign = (era_text[eraopt] or {}).sign
if not sign or format:find('%{era}', 1, true) then
sign = ''
if value <= 0 then
value = 1 - value
end
else
if value >= 0 then
sign = ''
else
value = -value
end
end
return spaces .. sign .. sformat(fmt, value)
end
return spaces .. (fmt and sformat(fmt, value) or value)
end
end
end
end
local function replace_property(spaces, id)
 
if id == 'era' then
-- analyse des paramètres nommés
-- Special case so can use local era option.
if test then
local result = get_era_for_year(eraopt, date.year)
params.agePrefix = args.agePrefix
if result == '' then
if args.qualificatif and args.qualificatif ~= '' then
return ''
params.qualificatif = args.qualificatif
end
 
-- julien peut avoir trois valeurs : inactif, format standard (true), format court
params.julien = Yesno( args.julien, 'court', false )
params.avJC = Yesno( args.avJC )
 
if args['républicain'] and args['républicain'] ~= '' then
if args['républicain'] == 'liens' then
params.republicain = 'liens'
else
params.republicain = Yesno( args['républicain'], false )
end
end
return (spaces == '' and '' or '&nbsp;') .. result
else
params.republicain = false
end
end
local result = date[id]
if args.dateNaissanceMort and args.dateNaissanceMort ~= '' then
if type(result) == 'string' then
dateNaissanceMort = args.dateNaissanceMort
return spaces .. result
elseif args['dateNaissanceÉvénement'] and args['dateNaissanceÉvénement'] ~= '' then
dateNaissanceMort = args['dateNaissanceÉvénement']
end
end
if type(result) == 'number' then
if dateNaissanceMort then
return  spaces .. tostring(result)
local testNaissanceMort, paramsNaissanceMort = fun.separationJourMoisAnnee( dateNaissanceMort )
if testNaissanceMort then
params.anneeNaissanceMort, params.moisNaissanceMort, params.numMoisNaissanceMort, params.jourNaissanceMort = paramsNaissanceMort.annee, paramsNaissanceMort.mois, paramsNaissanceMort.numMois, paramsNaissanceMort.jour
end
end
end
if type(result) == 'boolean' then
 
return  spaces .. (result and '1' or '0')
local listeParam = {
age = 'âge',
['âge'] = 'âge',
naissance = 'naissance',
mort = 'mort',
['événement'] = 'événement',
evenement = 'evenement',
['décès'] = 'mort',
apJC = 'apJC',
nolinks = 'nolinks',
compact = 'compact',
compacte = 'compact',
}
for n, v in pairs( listeParam ) do
params[v] = params[v] or Yesno( args[n], true, false ) or nil
end
end
-- This occurs if id is an undefined field in a partial date, or is the name of a function.
return nil
end
local PERCENT = '\127PERCENT\127'
return (format
:gsub('%%%%', PERCENT)
:gsub('(%s*)%%{(%w+)}', replace_property)
:gsub('(%s*)%%(%-?)(%a)', replace_code)
:gsub(PERCENT, '%%')
)
end


local function _date_text(date, fmt, options)
-- sortie pour les tests unitaire, ou pour débugger
-- Return a formatted string representing the given date.
if args.debug then
if not is_date(date) then
return params
error('date:text: need a date (use "date:text()" with a colon)', 2)
end
if type(fmt) == 'string' and fmt:match('%S') then
if fmt:find('%', 1, true) then
return strftime(date, fmt, options)
end
end
elseif date.partial then
 
fmt = date.month and 'my' or 'y'
resultat = fun._modeleDate( params )
 
else
else
fmt = 'dmy'
local yn_afficherErreurs = Yesno( args.afficherErreurs )
if date.hastime then
if yn_afficherErreurs == nil or yn_afficherErreurs == true then
fmt = (date.second > 0 and 'hms ' or 'hm ') .. fmt
resultat = params
end
end
local function bad_format()
-- For consistency with other format processing, return given format
-- (or cleaned format if original was not a string) if invalid.
return mw.text.nowiki(fmt)
end
if date.partial then
-- Ignore days in standard formats like 'ymd'.
if fmt == 'ym' or fmt == 'ymd' then
fmt = date.month and '%Y-%m %{era}' or '%Y %{era}'
elseif fmt == 'my' or fmt == 'dmy' or fmt == 'mdy' then
fmt = date.month and '%B %-Y %{era}' or '%-Y %{era}'
elseif fmt == 'y' then
fmt = date.month and '%-Y %{era}' or '%-Y %{era}'
else
else
return bad_format()
resultat = args[1]
end
end
return strftime(date, fmt, options)
end
local function hm_fmt()
local plain = make_option_table(options, date.options).bydefault.am
return plain and '%H:%M' or '%-I:%M %p'
end
local need_time = date.hastime
local t = collection()
for item in fmt:gmatch('%S+') do
local f
if item == 'hm' then
f = hm_fmt()
need_time = false
elseif item == 'hms' then
f = '%H:%M:%S'
need_time = false
elseif item == 'ymd' then
f = '%Y-%m-%d %{era}'
elseif item == 'mdy' then
f = '%B %-d, %-Y %{era}'
elseif item == 'dmy' then
f = '%-d %B %-Y %{era}'
else
return bad_format()
end
t:add(f)
end
fmt = t:join(' ')
if need_time then
fmt = hm_fmt() .. ' ' .. fmt
end
return strftime(date, fmt, options)
end


local day_info = {
local currentTitle = mw.title.getCurrentTitle()
-- 0=Sun to 6=Sat
[0] = { 'Sun', 'Sunday' },
{ 'Mon', 'Monday' },
{ 'Tue', 'Tuesday' },
{ 'Wed', 'Wednesday' },
{ 'Thu', 'Thursday' },
{ 'Fri', 'Friday' },
{ 'Sat', 'Saturday' },
}


local month_info = {
if currentTitle:inNamespaces( 0, 4, 10, 14, 100 )
-- 1=Jan to 12=Dec
and not Outils.notEmpty( args.nocat )
{ 'Jan', 'January' },
and not currentTitle.prefixedText:match( '^Modèle:.+/Test$' ) then
{ 'Feb', 'February' },
local categorie
{ 'Mar', 'March' },
local yn_categoriserErreurs = Yesno( args.categoriserErreurs, 'custom', true )
{ 'Apr', 'April' },
if yn_categoriserErreurs == nil or yn_categoriserErreurs == true then
{ 'May', 'May' },
categorie = '[[Catégorie:Page utilisant le modèle date avec une syntaxe erronée]]'
{ 'Jun', 'June' },
elseif yn_categoriserErreurs == false then
{ 'Jul', 'July' },
categorie = ''
{ 'Aug', 'August' },
else
{ 'Sep', 'September' },
local nomCategorie = args.categoriserErreurs
{ 'Oct', 'October' },
:gsub( '^%[%[', '' )
{ 'Nov', 'November' },
:gsub( '%]%]$', '' )
{ 'Dec', 'December' },
:gsub( '^:?[Cc]atégorie:', '' )
}
:gsub( '^:?[Cc]atégory:', '' )
 
categorie = '[[Catégorie:' .. nomCategorie .. ']]'
local function name_to_number(text, translate)
end
if type(text) == 'string' then
resultat = resultat .. categorie
return translate[text:lower()]
end
end
end
end


local function day_number(text)
return resultat or ''
return name_to_number(text, {
sun = 0, sunday = 0,
mon = 1, monday = 1,
tue = 2, tuesday = 2,
wed = 3, wednesday = 3,
thu = 4, thursday = 4,
fri = 5, friday = 5,
sat = 6, saturday = 6,
})
end
end


local function month_number(text)
function fun._modeleDate( args )
return name_to_number(text, {
local annee, mois, numMois, jour = args.annee, args.mois, args.numMois, args.jour
jan = 1, january = 1,
local qualificatif = args.qualificatif
feb = 2, february = 2,
mar = 3, march = 3,
apr = 4, april = 4,
may = 5,
jun = 6, june = 6,
jul = 7, july = 7,
aug = 8, august = 8,
sep = 9, september = 9, sept = 9,
oct = 10, october = 10,
nov = 11, november = 11,
dec = 12, december = 12,
})
end


local function _list_text(list, fmt)
if ( annee or mois or jour ) == nil then
-- Return a list of formatted strings from a list of dates.
return
if not type(list) == 'table' then
error('date:list:text: need "list:text()" with a colon', 2)
end
end
local result = { join = _list_join }
 
for i, date in ipairs(list) do
-- on traite l'âge, naissance et mort
result[i] = date:text(fmt)
local agePrefix = args.agePrefix
local age = args['âge'] and fun.age( annee, numMois, jour )
local naissance = args.naissance
local mort = args.mort
local evenement = args['événement'] or args.evenement
if mort and args.anneeNaissanceMort then
age = fun.age( args.anneeNaissanceMort, args.numMoisNaissanceMort, args.jourNaissanceMort, annee, numMois, jour )
agePrefix = agePrefix or 'à ' -- faut-il mettre \194\160 ?
elseif evenement and args.anneeNaissanceMort then
if naissance then
age = fun.age( annee, numMois, jour, args.anneeNaissanceMort, args.numMoisNaissanceMort, args.jourNaissanceMort )
else
age = fun.age(args.anneeNaissanceMort, args.numMoisNaissanceMort, args.jourNaissanceMort,  annee, numMois, jour )
end
end
end
return result
agePrefix = agePrefix or ''
end


local function _date_list(date, spec)
-- on traite le calendrier
-- Return a possibly empty numbered table of dates meeting the specification.
local gannee, gmois, gjour = annee, numMois, jour  -- date suivant le calendrier grégorien pour <time>
-- Dates in the list are in ascending order (oldest date first).
local jannee, jmois, jjour = annee, mois, jour      -- date suivant le calendrier julien si necessaire
-- The spec should be a string of form "<count> <day> <op>"
local julienDate, julienSup, julienSep              -- servira éventuellement à afficher la date selon le calendrier julien
-- where each item is optional and
local gregAprMois, gregAprAn, gregFin              -- message de calendrier grégorien lorsque la date est selon le calendrier julien
--  count = number of items wanted in list
if annee and jour then
--  day = abbreviation or name such as Mon or Monday
local amj = annee * 10000 + numMois * 100 + jour
--  op = >, >=, <, <= (default is > meaning after date)
if amj < 15821014 then
-- If no count is given, the list is for the specified days in date's month.
if annee > 0 then
-- The default day is date's day.
gannee, gmois, gjour = fun.julianToGregorian( annee, numMois, jour )
-- The spec can also be a positive or negative number:
else
--  -5 is equivalent to '5 <'
-- calendrier grégorien proleptique avec année 0.
--  5  is equivalent to '5' which is '5 >'
gannee, gmois, gjour = fun.julianToGregorian( annee + 1, numMois, jour )
if not is_date(date) then
error('date:list: need a date (use "date:list()" with a colon)', 2)
end
local list = { text = _list_text }
if date.partial then
return list
end
local count, offset, operation
local ops = {
['>='] = { before = false, include = true  },
['>']  = { before = false, include = false },
['<='] = { before = true , include = true  },
['<']  = { before = true , include = false },
}
if spec then
if type(spec) == 'number' then
count = floor(spec + 0.5)
if count < 0 then
count = -count
operation = ops['<']
end
end
elseif type(spec) == 'string' then
args.julien = false
local num, day, op = spec:match('^%s*(%d*)%s*(%a*)%s*([<>=]*)%s*$')
 
if not num then
elseif args.julien then
return list
gannee, gmois, gjour = fun.julianToGregorian( annee, numMois, jour )
annee, mois, jour = gannee, listeMois[gmois].nom, gjour
if jjour == 1 then
jjour = modelePremier
end
end
if num ~= '' then
if args.compact then
count = tonumber(num)
jmois = listeMois[jmois].abrev
end
end
if day ~= '' then
if args.julien == 'court' then
local dow = day_number(day:gsub('[sS]$', ''))  -- accept plural days
julienDate = jjour .. ' ' .. jmois .. ' '
if not dow then
julienSup = '<sup>[[calendrier julien|jul.]]</sup>'
return list
if jannee == annee then
gregAprMois = '<sup>[[calendrier grégorien|grég.]]</sup>'
else
julienDate = julienDate .. jannee .. ' '
gregAprAn = '<sup>[[calendrier grégorien|grég.]]</sup>'
end
end
offset = dow - date.dow
julienSep = ' / '
else
julienDate = jjour .. ' ' .. jmois .. ' ' .. jannee
julienSep = ' ('
gregFin = ' [[Passage du calendrier julien au calendrier grégorien|dans le calendrier grégorien]])'
end
end
operation = ops[op]
 
else
elseif args.republicain then
return list
local DateRep = require 'Module:Date républicaine'
local RepSansLiens
if args.republicain == 'liens' then
RepSansLiens = false
else
RepSansLiens = true
end
dateRepublicaine = DateRep._date_republicaine(
RepSansLiens,
{ fun.formatRepCal( fun.do_toRepCal{gannee, gmois, gjour} ) }
)
end
else
if annee and annee < 0 then
gannee = gannee + 1
end
end
args.julien = false
args.republicain = false
end
end
offset = offset or 0
 
operation = operation or ops['>']
-- on génère le résultat
local datefrom, dayfirst, daylast
 
if operation.before then
-- Déclarations des variables
if offset > 0 or (offset == 0 and not operation.include) then
local wikiListe = {}                  -- reçoit le texte affiché pour chaque paramètre
offset = offset - 7
local iso = {}                        -- reçoit le format date ISO de ce paramètre
end
local texteMois = mois                -- texte du mois qui sera affiché (éventuellement l'abréviation)
if count then
if args.compact then
if count > 1 then
if not numMois then
offset = offset - 7*(count - 1)
-- mois est autre chose qu'un simple mois : saison, mois-mois... auquel cas, pas d'abréviation (provoquait erreur Lua)
end
-- (les abréviations pour le cas "mois[-/]mois" seraient théoriquement possibles, mais ça reste à implémenter)
datefrom = date + offset
else
else
daylast = date.day + offset
if args.nolinks then
dayfirst = daylast % 7
texteMois = '<abbr class=abbr title="' .. mois .. '">' .. listeMois[mois].abrev .. '</abbr>'
if dayfirst == 0 then
else
dayfirst = 7
texteMois = listeMois[mois].abrev
end
end
end
end
else
end
if offset < 0 or (offset == 0 and not operation.include) then
mois = mois and mois:gsub( 'aout', 'août' )
offset = offset + 7
 
local dataQualificatif, dataCat
if not args.nolinks then
dataQualificatif = dataLiens[qualificatif or '']
if type( dataQualificatif ) ~= 'table' then
-- si le qualificatif n'est pas dans la base de données, on crée une table minimum,
-- qui imposera un test sur l'année, mais considère qu'il n'y a pas de lien sur le jour ou le mois
dataQualificatif = { qualificatif = qualificatif, annee = { } }
end
end
if count then
dataCat = dataLiens[dataQualificatif.cat]
datefrom = date + offset
if type( dataCat ) ~= 'table' or dataCat == dataQualificatif then
else
dataCat = { qualificatif = '' }
dayfirst = date.day + offset
daylast = date.monthdays
end
end
end
end
if not count then
local function wikiLien( lien, texte )
if daylast < dayfirst then
if lien == texte then
return list
return '[[' .. texte .. ']]'
else
return '[[' .. lien .. '|' .. texte .. ']]'
end
end
count = floor((daylast - dayfirst)/7) + 1
datefrom = Date(date, {day = dayfirst})
end
end
for i = 1, count do
if not datefrom then break end  -- exceeds date limits
list[i] = datefrom
datefrom = datefrom + 7
end
return list
end


-- A table to get the current date/time (UTC), but only if needed.
local current = setmetatable({}, {
__index = function (self, key)
local d = os.date('!*t')
self.year = d.year
self.month = d.month
self.day = d.day
self.hour = d.hour
self.minute = d.min
self.second = d.sec
return rawget(self, key)
end })


local function extract_date(newdate, text)
-- le jour si présent
-- Parse the date/time in text and return n, o where
local qualifJour = ''
--  n = table of numbers with date/time fields
if jour then
--  o = table of options for AM/PM or AD/BC or format, if any
if args.nolinks then
-- or return nothing if date is known to be invalid.
if jour == 1 then
-- Caller determines if the values in n are valid.
jour = modelePremier
-- A year must be positive ('1' to '9999'); use 'BC' for BC.
-- In a y-m-d string, the year must be four digits to avoid ambiguity
-- ('0001' to '9999'). The only way to enter year <= 0 is by specifying
-- the date as three numeric parameters like ymd Date(-1, 1, 1).
-- Dates of form d/m/y, m/d/y, y/m/d are rejected as potentially ambiguous.
local date, options = {}, {}
if text:sub(-1) == 'Z' then
-- Extract date/time from a Wikidata timestamp.
-- The year can be 1 to 16 digits but this module handles 1 to 4 digits only.
-- Examples: '+2016-06-21T14:30:00Z', '-0000000180-00-00T00:00:00Z'.
local sign, y, m, d, H, M, S = text:match('^([+%-])(%d+)%-(%d%d)%-(%d%d)T(%d%d):(%d%d):(%d%d)Z$')
if sign then
y = tonumber(y)
if sign == '-' and y > 0 then
y = -y
end
end
if y <= 0 then
table.insert( wikiListe, jour )
options.era = 'BCE'
else
qualifJour = dataQualificatif.jour and dataQualificatif.qualificatif
or dataCat.jour and dataCat.qualificatif
or ''
local texteJour, lien
if jour == 1 then
texteJour = '1<sup>er</sup>'
lien = '1er ' .. mois
else
texteJour = jour
lien = jour .. ' ' .. mois
end
end
date.year = y
if qualifJour ~= '' then
m = tonumber(m)
lien = lien .. ' ' .. qualifJour
d = tonumber(d)
H = tonumber(H)
M = tonumber(M)
S = tonumber(S)
if m == 0 then
newdate.partial = true
return date, options
end
end
date.month = m
-- s'il n'y a pas de lien sur le mois, il sera affiché avec le jour.
if d == 0 then
table.insert( wikiListe, wikiLien( lien, texteJour ) )
newdate.partial = true
table.insert( wikiListe, wikiLien( lien, texteJour .. ' '.. texteMois ) )
return date, options
end
date.day = d
if H > 0 or M > 0 or S > 0 then
date.hour = H
date.minute = M
date.second = S
end
return date, options
end
end
return
table.insert( iso, 1, string.sub( '0' .. gjour, -2 ) )
end
end
local function extract_ymd(item)
 
-- Called when no day or month has been set.
-- le mois
local y, m, d = item:match('^(%d%d%d%d)%-(%w+)%-(%d%d?)$')
if mois then
if y then
if #wikiListe == 0 and annee == nil then
if date.year then
return texteMois
return
end
if args.nolinks then
if not args.masquerMois then
table.insert( wikiListe, texteMois )
end
end
if m:match('^%d%d?$') then
else
m = tonumber(m)
local lien
else
if annee then
m = month_number(m)
if not numMois then
end
-- mois est autre chose qu'un simple mois : saison, mois-mois... auquel cas, pas de lien
if m then
else
date.year = tonumber(y)
lien = existDate( dataQualificatif, annee, mois ) or existDate( dataCat, annee, mois )
date.month = m
if lien == nil and qualificatif and qualifJour == '' then
date.day = tonumber(d)
-- nouveau test sans le qualificatif uniquement s'il n'y a pas d'éphémérides pour ce qualificatif.
return true
lien = existDate( dataLiens[''], annee, mois )
end
end
end
end
end
if lien or args.masquerMois then
end
-- s'il y a un lien on retire le lien affichant 'jour mois' pour ajouter '[[mois annee|mois]]'
local function extract_day_or_year(item)
table.remove( wikiListe )
-- Called when a day would be valid, or
if not args.masquerMois then
-- when a year would be valid if no year has been set and partial is set.
table.insert( wikiListe, wikiLien( lien, texteMois ) )
local number, suffix = item:match('^(%d%d?%d?%d?)(.*)$')
if number then
local n = tonumber(number)
if #number <= 2 and n <= 31 then
suffix = suffix:lower()
if suffix == '' or suffix == 'st' or suffix == 'nd' or suffix == 'rd' or suffix == 'th' then
date.day = n
return true
end
end
elseif suffix == '' and newdate.partial and not date.year then
elseif #wikiListe > 0 then
date.year = n
-- sinon on retire le lien affichant 'jour' pour ne garder que le lien 'jour mois'
return true
table.remove( wikiListe, #wikiListe - 1 )
elseif args.masquerAnnee then
-- s'il n'y a pas de jour et que l'année n'est pas affichée, on insère le mois seul.
table.insert( wikiListe, texteMois )
end
end
end
end
end
if gmois then
local function extract_month(item)
table.insert( iso, 1, string.sub( '0' .. gmois, -2 ) )
-- A month must be given as a name or abbreviation; a number could be ambiguous.
local m = month_number(item)
if m then
date.month = m
return true
end
end
table.insert( wikiListe, gregAprMois )
end
end
local function extract_time(item)
 
local h, m, s = item:match('^(%d%d?):(%d%d)(:?%d*)$')
-- l'année
if date.hour or not h then
if annee and not (args.julien == true and args.nolinks and jannee == annee ) then
return
if not args.masquerAnnee then
end
local texteAnnee = annee
if s ~= '' then
local lien
s = s:match('^:(%d%d)$')
if annee < 0 then
if not s then
local annneeAvJc = 0 - annee
return
lien = lien or ( annneeAvJc .. ' av. J.-C.' )
if args.avJC == false then
texteAnnee = annneeAvJc
else
texteAnnee = annneeAvJc .. ' <abbr class="abbr" title="'
.. annneeAvJc .. ' avant Jésus-Christ">av. J.-C.</abbr>'
end
elseif args.apJC then
texteAnnee = texteAnnee .. ' <abbr class="abbr" title="'
.. texteAnnee .. ' après Jésus-Christ">apr. J.-C.</abbr>'
end
end
end
if args.nolinks then -- seulement si on doit l'afficher
date.hour = tonumber(h)
table.insert( wikiListe, texteAnnee )
date.minute = tonumber(m)
date.second = tonumber(s)  -- nil if empty string
return true
end
local item_count = 0
local index_time
local function set_ampm(item)
local H = date.hour
if H and not options.am and index_time + 1 == item_count then
options.am = ampm_options[item]  -- caller checked this is not nil
if item:match('^[Aa]') then
if not (1 <= H and H <= 12) then
return
end
if H == 12 then
date.hour = 0
end
else
else
if not (1 <= H and H <= 23) then
lien = existDate( dataQualificatif, annee ) or existDate( dataCat, annee ) or lien or annee
return
if mois and #wikiListe == 0 then
end
-- si le mois n'a pas de lien et n'est pas affiché avec le jour, il est affiché avec l'année.
if H <= 11 then
texteAnnee = texteMois .. ' ' .. texteAnnee
date.hour = H + 12
end
end
table.insert( wikiListe, wikiLien( lien, texteAnnee ) )
end
end
return true
end
end
end
end
for item in text:gsub(',', ' '):gsub('&nbsp;', ' '):gmatch('%S+') do
if annee then
item_count = item_count + 1
if gannee > 999 then
if era_text[item] then
table.insert( iso, 1, gannee )
-- Era is accepted in peculiar places.
elseif gannee > -1 then
if options.era then
table.insert( iso, 1, string.sub( '000' .. gannee , -4 ) )
return
elseif gannee > -999 then
end
-- calendrier grégorien proleptique avec année 0.
options.era = item
table.insert( iso, 1, 'U-' .. string.sub( '000' .. ( 0 - gannee ), -4 ) )
elseif ampm_options[item] then
if not set_ampm(item) then
return
end
elseif item:find(':', 1, true) then
if not extract_time(item) then
return
end
index_time = item_count
elseif date.day and date.month then
if date.year then
return  -- should be nothing more so item is invalid
end
if not item:match('^(%d%d?%d?%d?)$') then
return
end
date.year = tonumber(item)
elseif date.day then
if not extract_month(item) then
return
end
elseif date.month then
if not extract_day_or_year(item) then
return
end
elseif extract_month(item) then
options.format = 'mdy'
elseif extract_ymd(item) then
options.format = 'ymd'
elseif extract_day_or_year(item) then
if date.day then
options.format = 'dmy'
end
else
else
return
table.insert( iso, 1, 'U' .. gannee )
end
end
end
end
if not date.year or date.year == 0 then
table.insert( wikiListe, gregAprAn )
return
end
local era = era_text[options.era]
if era and era.isbc then
date.year = 1 - date.year
end
return date, options
end


local function autofill(date1, date2)
-- l'age
-- Fill any missing month or day in each date using the
if type( age ) == 'number' and age >= 0 and ( not naissance or age < 120 ) then
-- corresponding component from the other date, if present,
if age == 0 then
-- or with 1 if both dates are missing the month or day.
age = '(' .. agePrefix .. 'moins d’un\194\160an)'
-- This gives a good result for calculating the difference
elseif age == 1 then
-- between two partial dates when no range is wanted.
age = '(' .. agePrefix .. '1\194\160an)'
-- Return filled date1, date2 (two full dates).
else
local function filled(a, b)
age = '('.. agePrefix .. age .. '\194\160ans)'
-- Return date a filled, if necessary, with month and/or day from date b.
-- The filled day is truncated to fit the number of days in the month.
local fillmonth, fillday
if not a.month then
fillmonth = b.month or 1
end
end
if not a.day then
else
fillday = b.day or 1
age = false
end
if fillmonth or fillday then  -- need to create a new date
a = Date(a, {
month = fillmonth,
day = math.min(fillday or a.day, days_in_month(a.year, fillmonth or a.month, a.calendar))
})
end
return a
end
end
return filled(date1, date2), filled(date2, date1)
end


local function date_add_sub(lhs, rhs, is_sub)
 
-- Return a new date from calculating (lhs + rhs) or (lhs - rhs),
-- compilation du résultat
-- or return nothing if invalid.
local wikiTexte = table.concat( wikiListe, ' ' )
-- The result is nil if the calculated date exceeds allowable limits.
local isoTexte = table.concat( iso, '-' )
-- Caller ensures that lhs is a date; its properties are copied for the new date.
 
if lhs.partial then
-- On ajoute un peu de sémantique.
-- Adding to a partial is not supported.
local wikiHtml = mw.html.create( '' )
-- Can subtract a date or partial from a partial, but this is not called for that.
 
return
if julienDate then
wikiHtml:tag( 'span')
:addClass( 'nowrap' )
:attr( 'data-sort-value', isoTexte )
:wikitext( julienDate )
:node( julienSup )
:done()
:wikitext( julienSep )
end
end
local function is_prefix(text, word, minlen)
 
local n = #text
local dateHtml = wikiHtml:tag( 'time' )
return (minlen or 1) <= n and n <= #word and text == word:sub(1, n)
:wikitext( wikiTexte )
end
if wikiTexte:match( ' ' ) then
local function do_days(n)
dateHtml:addClass( 'nowrap' )
local forcetime, jd
if floor(n) == n then
jd = lhs.jd
else
forcetime = not lhs.hastime
jd = lhs.jdz
end
jd = jd + (is_sub and -n or n)
if forcetime then
jd = tostring(jd)
if not jd:find('.', 1, true) then
jd = jd .. '.0'
end
end
return Date(lhs, 'juliandate', jd)
end
end
if type(rhs) == 'number' then
if isoTexte ~= wikiTexte then
-- Add/subtract days, including fractional days.
dateHtml:attr( 'datetime', isoTexte )
return do_days(rhs)
:attr( 'data-sort-value', isoTexte )
end
end
if type(rhs) == 'string' then
if not args.nolinks then
-- rhs is a single component like '26m' or '26 months' (with optional sign).
dateHtml:addClass( 'date-lien' )
-- Fractions like '3.25d' are accepted for the units which are handled as days.
local sign, numstr, id = rhs:match('^%s*([+-]?)([%d%.]+)%s*(%a+)$')
if sign then
if sign == '-' then
is_sub = not (is_sub and true or false)
end
local y, m, days
local num = tonumber(numstr)
if not num then
return
end
id = id:lower()
if is_prefix(id, 'years') then
y = num
m = 0
elseif is_prefix(id, 'months') then
y = floor(num / 12)
m = num % 12
elseif is_prefix(id, 'weeks') then
days = num * 7
elseif is_prefix(id, 'days') then
days = num
elseif is_prefix(id, 'hours') then
days = num / 24
elseif is_prefix(id, 'minutes', 3) then
days = num / (24 * 60)
elseif is_prefix(id, 'seconds') then
days = num / (24 * 3600)
else
return
end
if days then
return do_days(days)
end
if numstr:find('.', 1, true) then
return
end
if is_sub then
y = -y
m = -m
end
assert(-11 <= m and m <= 11)
y = lhs.year + y
m = lhs.month + m
if m > 12 then
y = y + 1
m = m - 12
elseif m < 1 then
y = y - 1
m = m + 12
end
local d = math.min(lhs.day, days_in_month(y, m, lhs.calendar))
return Date(lhs, y, m, d)
end
end
end
if is_diff(rhs) then
if naissance then
local days = rhs.age_days
dateHtml:addClass( 'bday' )
if (is_sub or false) ~= (rhs.isnegative or false) then
elseif mort then
days = -days
dateHtml:addClass( 'dday' )
end
return lhs + days
end
end
end


local full_date_only = {
wikiHtml:wikitext( gregFin )
dayabbr = true,
dayname = true,
dow = true,
dayofweek = true,
dowiso = true,
dayofweekiso = true,
dayofyear = true,
gsd = true,
juliandate = true,
jd = true,
jdz = true,
jdnoon = true,
}
 
-- Metatable for a date's calculated fields.
local datemt = {
__index = function (self, key)
if rawget(self, 'partial') then
if full_date_only[key] then return end
if key == 'monthabbr' or key == 'monthdays' or key == 'monthname' then
if not self.month then return end
end
end
local value
if key == 'dayabbr' then
value = day_info[self.dow][1]
elseif key == 'dayname' then
value = day_info[self.dow][2]
elseif key == 'dow' then
value = (self.jdnoon + 1) % 7  -- day-of-week 0=Sun to 6=Sat
elseif key == 'dayofweek' then
value = self.dow
elseif key == 'dowiso' then
value = (self.jdnoon % 7) + 1  -- ISO day-of-week 1=Mon to 7=Sun
elseif key == 'dayofweekiso' then
value = self.dowiso
elseif key == 'dayofyear' then
local first = Date(self.year, 1, 1, self.calendar).jdnoon
value = self.jdnoon - first + 1  -- day-of-year 1 to 366
elseif key == 'era' then
-- Era text (never a negative sign) from year and options.
value = get_era_for_year(self.options.era, self.year)
elseif key == 'format' then
value = self.options.format or 'dmy'
elseif key == 'gsd' then
-- GSD = 1 from 00:00:00 to 23:59:59 on 1 January 1 AD Gregorian calendar,
-- which is from jd 1721425.5 to 1721426.49999.
value = floor(self.jd - 1721424.5)
elseif key == 'juliandate' or key == 'jd' or key == 'jdz' then
local jd, jdz = julian_date(self)
rawset(self, 'juliandate', jd)
rawset(self, 'jd', jd)
rawset(self, 'jdz', jdz)
return key == 'jdz' and jdz or jd
elseif key == 'jdnoon' then
-- Julian date at noon (an integer) on the calendar day when jd occurs.
value = floor(self.jd + 0.5)
elseif key == 'isleapyear' then
value = is_leap_year(self.year, self.calendar)
elseif key == 'monthabbr' then
value = month_info[self.month][1]
elseif key == 'monthdays' then
value = days_in_month(self.year, self.month, self.calendar)
elseif key == 'monthname' then
value = month_info[self.month][2]
end
if value ~= nil then
rawset(self, key, value)
return value
end
end,
}


-- Date operators.
if args.republicain then
local function mt_date_add(lhs, rhs)
wikiHtml:wikitext( ' (', dateRepublicaine, ')' )
if not is_date(lhs) then
lhs, rhs = rhs, lhs  -- put date on left (it must be a date for this to have been called)
end
end
return date_add_sub(lhs, rhs)
end


local function mt_date_sub(lhs, rhs)
if age then
if is_date(lhs) then
wikiHtml:wikitext( ' ' )
if is_date(rhs) then
:tag( 'span' )
return DateDiff(lhs, rhs)
:addClass( 'noprint')
end
:wikitext( age )
return date_add_sub(lhs, rhs, true)
:done()
end
end
end


local function mt_date_concat(lhs, rhs)
return tostring( wikiHtml )
return tostring(lhs) .. tostring(rhs)
end
end


local function mt_date_tostring(self)
return self:text()
end


local function mt_date_eq(lhs, rhs)
---
-- Return true if dates identify same date/time where, for example,
-- fonction destinée aux infobox, notamment pour afficher les dates de naissance et de mort
-- Date(-4712, 1, 1, 'Julian') == Date(-4713, 11, 24, 'Gregorian') is true.
-- les liens présent dans les dates fournies sont automatiquement supprimés pour gérer les cas où
-- This is called only if lhs and rhs have the same type and the same metamethod.
-- le paramètre contient déjà un modèle date.
if lhs.partial or rhs.partial then
-- Paramètres :
-- One date is partial; the other is a partial or a full date.
-- 1 : type de date à afficher (naissance / n, mort / m, ou date / d)
-- The months may both be nil, but must be the same.
-- 1 : Date ou date de naissance
return lhs.year == rhs.year and lhs.month == rhs.month and lhs.calendar == rhs.calendar
-- 2 : Date de mort si type n ou m
-- qualificatif = suffixe des page de date à lier (exemple : en musique)
-- nolinks : n'affiche pas de lien
-- préfixe : préfixe à afficher s'il y a un jour (par défaut '')
-- préfixe sans jour : préfixe à afficher s'il n'y a pas de jour (par défaut : '')
function fun.dateInfobox( frame )
local args = frame.args
if type( args ) ~= 'table' or not ( args[1] and args[2] ) then
return
end
end
return lhs.jdz == rhs.jdz
end


local function mt_date_lt(lhs, rhs)
-- analyseDate sépare la date du contenu qui suit, supprime les liens, et retourne si possible une table avec jour mois année
-- Return true if lhs < rhs, for example,
local function analyseDate( d )
-- Date('1 Jan 2016') < Date('06:00 1 Jan 2016') is true.
if trim( d ) then
-- This is called only if lhs and rhs have the same type and the same metamethod.
local analyse = d:match( ' ou ') or d:match( 'entre ' ) or d:match( 'vers ' ) or d:match( 'après ' ) or d:match( 'avant ' )
if lhs.partial or rhs.partial then
if analyse then
-- One date is partial; the other is a partial or a full date.
return d
if lhs.calendar ~= rhs.calendar then
return lhs.calendar == 'Julian'
end
if lhs.partial then
lhs = lhs.partial.first
end
if rhs.partial then
rhs = rhs.partial.first
end
end
return lhs.jdz < rhs.jdz
end
 
--[[ Examples of syntax to construct a date:
Date(y, m, d, 'julian')             default calendar is 'gregorian'
Date(y, m, d, H, M, S, 'julian')
Date('juliandate', jd, 'julian')   if jd contains "." text output includes H:M:S
Date('currentdate')
Date('currentdatetime')
Date('1 April 1995', 'julian')      parse date from text
Date('1 April 1995 AD', 'julian')  using an era sets a flag to do the same for output
Date('04:30:59 1 April 1995', 'julian')
Date(date)                          copy of an existing date
Date(date, t)                      same, updated with y,m,d,H,M,S fields from table t
Date(t)                      date with y,m,d,H,M,S fields from table t
]]
function Date(...)  -- for forward declaration above
-- Return a table holding a date assuming a uniform calendar always applies
-- (proleptic Gregorian calendar or proleptic Julian calendar), or
-- return nothing if date is invalid.
-- A partial date has a valid year, however its month may be nil, and
-- its day and time fields are nil.
-- Field partial is set to false (if a full date) or a table (if a partial date).
local calendars = { julian = 'Julian', gregorian = 'Gregorian' }
local newdate = {
_id = uniq,
calendar = 'Gregorian',  -- default is Gregorian calendar
hastime = false,  -- true if input sets a time
hour = 0,  -- always set hour/minute/second so don't have to handle nil
minute = 0,
second = 0,
options = {},
list = _date_list,
subtract = function (self, rhs, options)
return DateDiff(self, rhs, options)
end,
text = _date_text,
}
local argtype, datetext, is_copy, jd_number, tnums
local numindex = 0
local numfields = { 'year', 'month', 'day', 'hour', 'minute', 'second' }
local numbers = {}
for _, v in ipairs({...}) do
v = strip_to_nil(v)
local vlower = type(v) == 'string' and v:lower() or nil
if v == nil then
-- Ignore empty arguments after stripping so modules can directly pass template parameters.
elseif calendars[vlower] then
newdate.calendar = calendars[vlower]
elseif vlower == 'partial' then
newdate.partial = true
elseif vlower == 'fix' then
newdate.want_fix = true
elseif is_date(v) then
-- Copy existing date (items can be overridden by other arguments).
if is_copy or tnums then
return
end
is_copy = true
newdate.calendar = v.calendar
newdate.partial = v.partial
newdate.hastime = v.hastime
newdate.options = v.options
newdate.year = v.year
newdate.month = v.month
newdate.day = v.day
newdate.hour = v.hour
newdate.minute = v.minute
newdate.second = v.second
elseif type(v) == 'table' then
if tnums then
return
end
tnums = {}
local tfields = { year=1, month=1, day=1, hour=2, minute=2, second=2 }
for tk, tv in pairs(v) do
if tfields[tk] then
tnums[tk] = tonumber(tv)
end
if tfields[tk] == 2 then
newdate.hastime = true
end
end
end
else
analyse = d:match( 'datetime="([%d-]+)"' ) or d
local num = tonumber(v)
-- sépare la date (avec ses liens) d'une référence ou contenu commençant par un espace)
if not num and argtype == 'setdate' and numindex == 1 then
local debut, fin = analyse:match( '(.-%d%d%d%]*%-?)([\127 ].+)' )
num = month_number(v)
if not debut then
-- sépare la date du contenu commençant par <br>
debut, fin = analyse:match( '(.-%d%d%d%]*%-?)(<br ?/?>.+)' )
end
end
if num then
analyse = debut or analyse
if not argtype then
-- supprime les liens
argtype = 'setdate'
analyse = analyse:gsub(
end
'%[%[([^%[%]|]*)|?([^%[%]]*)%]%]',
if argtype == 'setdate' and numindex < 6 then
function ( l, t )
numindex = numindex + 1
return trim( t ) or l
numbers[numfields[numindex]] = num
elseif argtype == 'juliandate' and not jd_number then
jd_number = num
if type(v) == 'string' then
if v:find('.', 1, true) then
newdate.hastime = true
end
elseif num ~= floor(num) then
-- The given value was a number. The time will be used
-- if the fractional part is nonzero.
newdate.hastime = true
end
else
return
end
elseif argtype then
return
elseif type(v) == 'string' then
if v == 'currentdate' or v == 'currentdatetime' or v == 'juliandate' then
argtype = v
else
argtype = 'datetext'
datetext = v
end
end
)
local t, r = fun.separationJourMoisAnnee( analyse )
if t then
return r, fin
else
else
return
return d, fin
end
end
end
end
end
end
if argtype == 'datetext' then
-- prefix ajoute un préfixe en fonction de la présence ou non du jour si le paramètre "préfixe sans jour" est défini
if tnums or not set_date_from_numbers(newdate, extract_date(newdate, datetext)) then
local function prefix( dateString )
return
if dateString then
end
local datetime = dateString:match( 'datetime="([U%d%-]+)"' )
elseif argtype == 'juliandate' then
if datetime and datetime:match('%-%d%d%-%d%d') and trim( args['préfixe'] ) then
newdate.partial = nil
return args['préfixe'] .. ' ' .. dateString
newdate.jd = jd_number
end
if not set_date_from_jd(newdate) then
if trim( args['préfixe sans jour'] ) then
return
return args['préfixe sans jour'] .. ' ' .. dateString
end
end
end
elseif argtype == 'currentdate' or argtype == 'currentdatetime' then
return dateString
newdate.partial = nil
newdate.year = current.year
newdate.month = current.month
newdate.day = current.day
if argtype == 'currentdatetime' then
newdate.hour = current.hour
newdate.minute = current.minute
newdate.second = current.second
newdate.hastime = true
end
newdate.calendar = 'Gregorian'  -- ignore any given calendar name
elseif argtype == 'setdate' then
if tnums or not set_date_from_numbers(newdate, numbers) then
return
end
elseif not (is_copy or tnums) then
return
end
if tnums then
newdate.jd = nil  -- force recalculation in case jd was set before changes from tnums
if not set_date_from_numbers(newdate, tnums) then
return
end
end
if newdate.partial then
local year = newdate.year
local month = newdate.month
local first = Date(year, month or 1, 1, newdate.calendar)
month = month or 12
local last = Date(year, month, days_in_month(year, month), newdate.calendar)
newdate.partial = { first = first, last = last }
else
newdate.partial = false  -- avoid index lookup
end
end
setmetatable(newdate, datemt)
local readonly = {}
local mt = {
__index = newdate,
__newindex = function(t, k, v) error('date.' .. tostring(k) .. ' is read-only', 2) end,
__add = mt_date_add,
__sub = mt_date_sub,
__concat = mt_date_concat,
__tostring = mt_date_tostring,
__eq = mt_date_eq,
__lt = mt_date_lt,
}
return setmetatable(readonly, mt)
end


local function _diff_age(diff, code, options)
local naissance = args[1]:match( '^n' ) == 'n'
-- Return a tuple of integer values from diff as specified by code, except that
local mort = args[1]:match( '^m' ) or args[1]:match( 'décès' )
-- each integer may be a list of two integers for a diff with a partial date, or
local evenement = args[1]:match( '^é' )
-- return nil if the code is not supported.
local affichageDate, qualificatif = args[2], args[4]
-- If want round, the least significant unit is rounded to nearest whole unit.
local affichageDateTab, resultatDate, complementDate
-- For a duration, an extra day is added.
local dateNaissance, dateMort
local wantround, wantduration, wantrange
if mort or evenement then
if type(options) == 'table' then
affichageDate = args[3]
wantround = options.round
wantduration = options.duration
wantrange = options.range
else
wantround = options
end
end
if not is_diff(diff) then
if not trim( affichageDate ) then
local f = wantduration and 'duration' or 'age'
return
error(f .. ': need a date difference (use "diff:' .. f .. '()" with a colon)', 2)
end
end
if diff.partial then
if affichageDate:match( '</time>' ) then
-- Ignore wantround, wantduration.
-- S'il y a des liens il y a probablement déjà un modèle date, évitons de l'exécuter une 2e fois
local function choose(v)
if ( naissance or mort or evenement ) and ( affichageDate:match( 'wikidata%-linkback' )) then
if type(v) == 'table' then
dateNaissance = analyseDate( args[2] )
if not wantrange or v[1] == v[2] then
dateMort = analyseDate( args[3] )
-- Example: Date('partial', 2005) - Date('partial', 2001) gives
resultatDate = affichageDate
-- diff.years = { 3, 4 } to show the range of possible results.
else
-- If do not want a range, choose the second value as more expected.
return prefix( affichageDate )
return v[2]
end
end
return v
end
end
if code == 'ym' or code == 'ymd' then
else
if not wantrange and diff.iszero then
affichageDateTab, complementDate = analyseDate( affichageDate )
-- This avoids an unexpected result such as
if type( affichageDateTab ) ~= 'table' then
-- Date('partial', 2001) - Date('partial', 2001)
return affichageDateTab
-- giving diff = { years = 0, months = { 0, 11 } }
else
-- which would be reported as 0 years and 11 months.
if naissance then
return 0, 0
dateNaissance = affichageDateTab
dateMort = analyseDate( args[3] )
elseif mort then
dateNaissance = analyseDate( args[2] )
dateMort = affichageDateTab
else
qualificatif = args[3]
end
end
return choose(diff.partial.years), choose(diff.partial.months)
affichageDateTab.naissance = naissance
end
affichageDateTab.mort = mort
if code == 'y' then
affichageDateTab.evenement = evenement
return choose(diff.partial.years)
affichageDateTab.qualificatif = args.qualificatif or qualificatif
end
affichageDateTab.nolinks = args.nolinks
if code == 'm' or code == 'w' or code == 'd' then
affichageDateTab.nocat = args.nocat
return choose({ diff.partial.mindiff:age(code), diff.partial.maxdiff:age(code) })
affichageDateTab.julien = args.julien
end
end
return nil
end
end
local extra_days = wantduration and 1 or 0
resultatDate = resultatDate or fun.modeleDate( affichageDateTab )
if code == 'wd' or code == 'w' or code == 'd' then
 
local offset = wantround and 0.5 or 0
local age, prefixAge, suffixAge, calculAge = '', ' <span class="noprint">(', ')</span>', nil
local days = diff.age_days + extra_days
if naissance and
if code == 'wd' or code == 'd' then
dateNaissance and
days = floor(days + offset)
not dateMort and
if code == 'd' then
type( dateNaissance ) == 'table'
return days
then
end
calculAge = fun.age( dateNaissance.annee, dateNaissance.numMois, dateNaissance.jour )
return floor(days/7), days % 7
if calculAge and calculAge > 120 then
calculAge = nil
end
end
return floor(days/7 + offset)
elseif (mort or evenement) and
dateNaissance and
dateMort and
type( dateNaissance ) == 'table'
and type( dateMort ) == 'table'
then
calculAge = fun.age(
dateNaissance.annee,
dateNaissance.numMois,
dateNaissance.jour,
dateMort.annee,
dateMort.numMois,
dateMort.jour
)
prefixAge = ' (à '
suffixAge = ')'
end
end
local H, M, S = diff.hours, diff.minutes, diff.seconds
if tonumber( calculAge ) then
if code == 'dh' or code == 'dhm' or code == 'dhms' or code == 'h' or code == 'hm' or code == 'hms' or code == 'M' or code == 's' then
if calculAge > 1 then
local days = floor(diff.age_days + extra_days)
age = prefixAge .. calculAge .. '\194\160ans' .. suffixAge
local inc_hour
elseif calculAge == 1 then
if wantround then
age = prefixAge .. 'un\194\160an' .. suffixAge
if code == 'dh' or code == 'h' then
elseif calculAge == 0 then
if M >= 30 then
age = prefixAge .. 'moins d’un\194\160an' .. suffixAge
inc_hour = true
end
elseif code == 'dhm' or code == 'hm' then
if S >= 30 then
M = M + 1
if M >= 60 then
M = 0
inc_hour = true
end
end
elseif code == 'M' then
if S >= 30 then
M = M + 1
end
else
-- Nothing needed because S is an integer.
end
if inc_hour then
H = H + 1
if H >= 24 then
H = 0
days = days + 1
end
end
end
end
if code == 'dh' or code == 'dhm' or code == 'dhms' then
if complementDate and complementDate:match( 'ans?%)' ) then
if code == 'dh' then
complementDate = ''
return days, H
elseif code == 'dhm' then
return days, H, M
else
return days, H, M, S
end
end
end
local hours = days * 24 + H
if code == 'h' then
return hours
elseif code == 'hm' then
return hours, M
elseif code == 'M' or code == 's' then
M = hours * 60 + M
if code == 'M' then
return M
end
return M * 60 + S
end
return hours, M, S
end
end
if wantround then
 
local inc_hour
return prefix( resultatDate ) .. ( complementDate or '' ) .. age
if code == 'ymdh' or code == 'ymwdh' then
end
if M >= 30 then
 
inc_hour = true
 
---
-- la fonction dateISO renvoie un date au format aaaa-mm-jj (sans liens)
-- l'année peut être sous la forme 2013 ou [[2013 en litérature|2013]]
-- le mois peut être en lettres ou en chiffres
-- le jour peut être sous la forme '05', '{{1er}}' ou 'vendredi 13'
function fun.dateISO( frame )
local args = Outils.extractArgs( frame )
local annee = Outils.notEmpty( args['année'], args.annee, args.year, args.date )
-- extraction de l'année
if type( annee ) == 'string' then
annee = ( tonumber( annee ) -- match '2013'
or string.match ( annee, '%D(%d%d%d%d)%D' ) -- match '[[2013 en musique|2013]]'
or string.match ( annee, '%D(%d%d%d%d)$' )  -- match '17 septembre 2013'
or string.match ( annee, '^(%d%d%d%d)%D' )  -- match '2013-09-17'
)
end
annee = tonumber( annee )
 
-- le format de date iso est défini suivant le calendrier grégorien.
-- Avant l'année 1583 la date est calendrier est probablement du calendrier julien,
-- donc autant s'abstenir.
if annee and annee > 1582 then
local mois = Outils.notEmpty( args.mois, args.month )
-- num mois trouve le numéro du mois, qu'il soit numérique ou texte, complet ou abrégé.
local nomMois, numMois = fun.determinationMois( mois )
if numMois then
mois = '-' .. string.sub( '0' .. numMois, -2 )
 
local jour = Outils.notEmpty( args.jour, args.day, args['quantième'] )
if type( jour ) == 'string' then
jour = tonumber( jour ) or tonumber( string.match ( jour, '%d+') )
end
end
elseif code == 'ymdhm' or code == 'ymwdhm' then
jour = tonumber( jour )
if S >= 30 then
if jour and jour <= listeMois[numMois].nJour then
M = M + 1
jour = '-' .. string.sub( '0' .. jour, -2 )
if M >= 60 then
return annee .. mois .. jour
M = 0
else
inc_hour = true
return annee .. mois
end
end
elseif code == 'ymd' or code == 'ymwd' or code == 'yd' or code == 'md' then
if H >= 12 then
extra_days = extra_days + 1
end
end
if inc_hour then
H = H + 1
if H >= 24 then
H = 0
extra_days = extra_days + 1
end
end
else
return tostring( annee )
end
end
end
end
local y, m, d = diff.years, diff.months, diff.days
end
if extra_days > 0 then
 
d = d + extra_days
---
if d > 28 or code == 'yd' then
-- Rang du jour dans l'année
-- Recalculate in case have passed a month.
-- Usage : do_dayRank{année,mois,jour}
diff = diff.date1 + extra_days - diff.date2
function fun.do_dayRank(arguments)
y, m, d = diff.years, diff.months, diff.days
local yr = tonumber(arguments.year or arguments[1]) or 1
end
local mt = tonumber(arguments.month or arguments[2]) or 1
local dy = tonumber(arguments.day or arguments[3]) or 1
-- Rangs des premiers des mois
local ranks = {0,31,59,90,120,151,181,212,243,273,304,334}
 
local rank = (ranks[mt] or 0) + dy - 1
if(fun.isLeapYear(yr) and (mt >= 3)) then
rank = rank+1
end
end
if code == 'ymd' then
return rank
return y, m, d
end
elseif code == 'yd' then
 
if y > 0 then
-- Nombre de jours entre deux années (du 1er janvier au 1er janvier)
-- It is known that diff.date1 > diff.date2.
-- Suit le calendrier grégorien
diff = diff.date1 - (diff.date2 + (y .. 'y'))
function fun.do_daysBetween(arguments)
end
local yr1 = tonumber(arguments[1]) or 0
return y, floor(diff.age_days)
local yr2 = tonumber(arguments[2]) or 0
elseif code == 'md' then
 
return y * 12 + m, d
return fun.daysSinceOrigin(yr2) - fun.daysSinceOrigin(yr1)
elseif code == 'ym' or code == 'm' then
end
if wantround then
 
if d >= 16 then
-- Nombre de jours depuis l'année 1 (du 1er janvier au 1er janvier)
m = m + 1
function fun.daysSinceOrigin(year)
if m >= 12 then
local yr = year-1
m = 0
return 365*yr + math.floor(yr/4) - math.floor(yr/100) + math.floor(yr/400)
y = y + 1
end
end
 
end
-- Test d'année bissextile (Suit le calendrier grégorien)
end
function fun.isLeapYear(year)
if code == 'ym' then
local yr = tonumber(year) or 1
return y, m
return (yr%4 == 0) and ((yr%100 ~= 0) or (yr%400 == 0))
end
end
return y * 12 + m
 
elseif code == 'ymw' then
-- Conversion d'un nombre en chiffres romains
local weeks = floor(d/7)
function fun.toRoman(number)
if wantround then
local n = math.floor(number)
local days = d % 7
local letters = {"I","V","X","L","C","D","M","",""}
if days > 3 or (days == 3 and H >= 12) then
local pattern = {"","0","00","000","01","1","10","100","1000","02"}
weeks = weeks + 1
local result = ""
if(n<=0 or n>=4000) then
result = "---"
else
for i=1,7,2 do
local p = pattern[n%10 + 1]
for j=0,2 do
p = string.gsub(p,tostring(j),letters[i+j])
end
end
result = p .. result
n = math.floor(n/10)
end
end
return y, m, weeks
elseif code == 'ymwd' then
return y, m, floor(d/7), d % 7
elseif code == 'ymdh' then
return y, m, d, H
elseif code == 'ymwdh' then
return y, m, floor(d/7), d % 7, H
elseif code == 'ymdhm' then
return y, m, d, H, M
elseif code == 'ymwdhm' then
return y, m, floor(d/7), d % 7, H, M
end
end
if code == 'y' then
return result
if wantround and m >= 6 then
end
y = y + 1
 
end
-- Conversion et affichage d'une date dans le calendrier républicain
return y
function fun.dateRepublicain(frame)
local pframe = frame:getParent()
local arguments = pframe.args
return fun.formatRepCal(fun.do_toRepCal(arguments))
end
 
---
-- Calcul d'une date dans le calendrier républicain
-- On suppose que les années 4n+3 sont sextiles (3, 7, 11...)
function fun.do_toRepCal(arguments)
local yr = tonumber(arguments.year or arguments[1]) or 2000
-- rang absolu du jour demandé, le jour 0 étant le 22 septembre 1792 (1er jour de l'an I)
local repDays = fun.do_dayRank(arguments) + fun.do_daysBetween{1792,yr} - fun.do_dayRank{1792,9,22}
local repYear = math.floor((repDays+731)/365.25) - 1
local repDayRank = repDays - 365*(repYear-1) - math.floor(repYear/4)
local repMonth, repDay = math.floor(repDayRank/30)+1, (repDayRank%30)+1
return {repYear, repMonth, repDay}
end
 
---
-- Formatage d'une date selon le calendrier républicain
-- Usage : fun.formatRepCal{année,mois,jour}
function fun.formatRepCal(arguments)
local months = {"Vendémiaire","Brumaire","Frimaire","Nivôse","Pluviôse","Ventôse","Germinal","Floréal","Prairial","Messidor","Thermidor","Fructidor"}
local extras = {"de la vertu","du génie","du travail","des récompenses","de l'opinion","de la Révolution"}
local result = ""
if(arguments[2] < 13) then
result = result .. tostring(arguments[3]) .. "\194\160" .. months[arguments[2]]
else
result = result .. "jour " .. extras[arguments[3]]
end
end
return nil
result = result .. " de l'an " .. fun.toRoman(arguments[1])
return result
end
end


local function _diff_duration(diff, code, options)
---
if type(options) ~= 'table' then
-- Voir Modèle:Âge
options = { round = options }
-- retourne l'âge en fonction de la ou les dates fournies. La valeur retournée est de type 'number'
-- Paramètres :
-- 1, 2, 3 : année, mois jour de naissance (supposé dans le calendrier grégorien)
-- 4, 5, 6 : année, mois, jour du calcul (facultatif, par défaut la date UTC courante).
function fun.age( an, mn, jn, ac, mc, jc )
if ac == nil then
local today = os.date( '!*t' )
ac = today.year
mc = today.month
jc = today.day
else
ac = tonumber( ac )
mc = tonumber( mc )
jc = tonumber( jc )
end
end
options.duration = true
return _diff_age(diff, code, options)
end


-- Metatable for some operations on date differences.
local an = tonumber( an )
diffmt = {  -- for forward declaration above
local mn = tonumber( mn )
__concat = function (lhs, rhs)
local jn = tonumber( jn )
return tostring(lhs) .. tostring(rhs)
end,
__tostring = function (self)
return tostring(self.age_days)
end,
__index = function (self, key)
local value
if key == 'age_days' then
if rawget(self, 'partial') then
local function jdz(date)
return (date.partial and date.partial.first or date).jdz
end
value = jdz(self.date1) - jdz(self.date2)
else
value = self.date1.jdz - self.date2.jdz
end
end
if value ~= nil then
rawset(self, key, value)
return value
end
end,
}


function DateDiff(date1, date2, options)  -- for forward declaration above
if an == nil or ac == nil or mn == nil or mc == nil then
-- Return a table with the difference between two dates (date1 - date2).
-- pas de message d'erreur qui risque de faire planter la fonction appelante
-- The difference is negative if date1 is older than date2.
-- à elle de gérer ce retour.
-- Return nothing if invalid.
-- If d = date1 - date2 then
--    date1 = date2 + d
-- If date1 >= date2 and the dates have no H:M:S time specified then
--     date1 = date2 + (d.years..'y') + (d.months..'m') + d.days
-- where the larger time units are added first.
-- The result of Date(2015,1,x) + '1m' is Date(2015,2,28) for
-- x = 28, 29, 30, 31. That means, for example,
--    d = Date(2015,3,3) - Date(2015,1,31)
-- gives d.years, d.months, d.days = 0, 1, 3 (excluding date1).
if not (is_date(date1) and is_date(date2) and date1.calendar == date2.calendar) then
return
return
end
end
local wantfill
 
if type(options) == 'table' then
local age = ac - an
wantfill = options.fill
if mc == mn then
if jc == nil or jn == nil then
return
end
return age-tonumber( jc < jn and 1 or 0 )
else
return age-tonumber( mc < mn and 1 or 0 )
end
end
local isnegative = false
end
local iszero = false
 
if date1 < date2 then
function fun.modeleAge( frame )
isnegative = true
local args = Outils.extractArgs( frame )
date1, date2 = date2, date1
local age = fun.age (
elseif date1 == date2 then
args[1] or args['année'],
iszero = true
args[2] or args['mois'],
args[3] or args['jour'],
args[4],
args[5],
args[6]
)
if age then
return age
else
return '<span class="error">Paramètres incorrects ou insuffisants pour calculer l\'âge précis</span>'
end
end
-- It is known that date1 >= date2 (period is from date2 to date1).
end
if date1.partial or date2.partial then
 
-- Two partial dates might have timelines:
---
---------------------A=================B--- date1 is from A to B inclusive
-- calcul du jour julien à partir d'une date du calendrier grégorien
--------C=======D-------------------------- date2 is from C to D inclusive
function fun.julianDay( year, month, day, hour, min, sec )
-- date1 > date2 iff A > C (date1.partial.first > date2.partial.first)
local julian
-- The periods can overlap ('April 2001' - '2001'):
julian = math.floor( math.floor( ( year * 12 + month + 57609 ) / 12 - 1 ) * 1461 / 4 )
-------------A===B------------------------- A=2001-04-01  B=2001-04-30
- math.floor( math.floor( ( year * 12 + month + 57609 ) / 12 - 1 ) / 100 )
--------C=====================D------------ C=2001-01-01 D=2001-12-31
+ math.floor( math.floor( ( year * 12 + month + 57609 ) / 12 - 1 ) / 400 )
if wantfill then
+ math.floor( ( math.fmod( month + 57609, 12 ) + 4 ) * 153 / 5 )
date1, date2 = autofill(date1, date2)
+ day + ( hour or 12 ) / 24 + ( min or 0 ) / 1440 + ( sec or 0 ) / 86400
- 32167.5
return julian
end
 
---
-- calcul du jour julien à partir d'une date du calendrier julien
function fun.julianDayJulian( year, month, day, hour, min, sec )
local julian
julian = math.floor( math.floor( ( year * 12 + month + 57609 ) / 12 - 1 ) * 1461 / 4 )
+ math.floor( ( math.fmod( month + 57609, 12 ) + 4 ) * 153 / 5 )
+ day + ( hour or 12 ) / 24 + ( min or 0 ) / 1440 + ( sec or 0 ) / 86400
- 32205.5
return julian
end
 
---
-- calcul d'une date dans le calendrier grégorien à partir du jour julien
function fun.julianDayToGregorian( julianDay )
local base = math.floor( julianDay + 32044.5 )  -- 1 March -4800 (proleptic Gregorian date)
local nCentury = math.floor( ( base * 4 + 3 ) / 146097 )
local sinceCentury = base - math.floor( nCentury * 146097 / 4 )
local nYear = math.floor( ( sinceCentury * 4 + 3 ) / 1461 )
local sinceYear = sinceCentury - math.floor( nYear * 1461 / 4 )
local nMonth = math.floor( ( sinceYear * 5 + 2 ) / 153 )
 
local day = sinceYear - math.floor( ( nMonth * 153 + 2 ) / 5 ) + 1
local month = nMonth - math.floor( nMonth / 10 ) * 12 + 3
local year = math.floor( sinceYear / 306 ) + nYear + 100 * nCentury - 4800
 
return year, month, day
end
 
---
-- calcul d'une date dans le calendrier julien à partir du jour julien
-- calcul basé sur l'algorithme de la page fr.wikipedia.org/wiki/Jour_julien (1/10/2013)
function fun.julianDayToJulian( julianDay )
local year = math.modf( ( julianDay * 4 - 6884469 ) / 1461 )
local r2 = julianDay - math.modf( ( 1461 * year + 6884472 ) / 4 )
local month = math.modf( ( 5 * r2 + 461 ) / 153 )
local day = r2 - math.modf( ( 153 * month - 457 ) / 5 ) + 1
if month > 12 then
year = year + 1
month = month - 12
end
return year, month, day
end
 
---
-- calcul d'une date dans le calendrier grégorien à partir d'une date dans le calendrier julien
function fun.julianToGregorian( year, month, day )
return fun.julianDayToGregorian( fun.julianDayJulian( year, month, day ) )
end
 
---
-- calcul d'une date dans le calendrier julien à partir d'une date dans le calendrier grégorien
function fun.gregorianToJulian( year, month, day )
year = tonumber(year)
if month then month = tonumber(month) else month = 6 end --prend une valeur centrale pour donner un best "guess"
if day then day = tonumber(day) else day = 15 end
return fun.julianDayToJulian( fun.julianDay( year, month, day ) )
end
 
 
--[[
  Cette fonction retourne "CET" ou "CEST" selon que dans la pseudo-timezone en cours
    c'est l'heure d'été ou l'heure d'hiver.
  Cette fonction n'a de sens a priori que pour des modèles utilisés en Europe
 
  Paramètre optionnel non nommé : "sans lien" : retourne le texte CET/CEST. sinon
    retourne ce même texte avec un wikilien vers les articles correspondants
--]]
function fun.CEST(frame)
-- option : ne pas créer de wikilien
local opt = trim(frame.args[1] or frame:getParent().args[1])
-- on récupère l'information dans la zone courante
local t = mw.getContentLanguage():formatDate("I", nil, true)
 
if (t == "1") then -- heure d'été
if (opt == "sans lien") then
return "CEST"
elseif (opt == "décalage") then
return "2"
else
else
local function zdiff(date1, date2)
return "[[Heure d'été d'Europe centrale|CEST]]"
local diff = date1 - date2
if diff.isnegative then
return date1 - date1  -- a valid diff in case we call its methods
end
return diff
end
local function getdate(date, which)
return date.partial and date.partial[which] or date
end
local maxdiff = zdiff(getdate(date1, 'last'), getdate(date2, 'first'))
local mindiff = zdiff(getdate(date1, 'first'), getdate(date2, 'last'))
local years, months
if maxdiff.years == mindiff.years then
years = maxdiff.years
if maxdiff.months == mindiff.months then
months = maxdiff.months
else
months = { mindiff.months, maxdiff.months }
end
else
years = { mindiff.years, maxdiff.years }
end
return setmetatable({
date1 = date1,
date2 = date2,
partial = {
years = years,
months = months,
maxdiff = maxdiff,
mindiff = mindiff,
},
isnegative = isnegative,
iszero = iszero,
age = _diff_age,
duration = _diff_duration,
}, diffmt)
end
end
end
else  -- heure d'hiver (ou autre zone où ça ne s'applique pas)
local y1, m1 = date1.year, date1.month
if (opt == "sans lien") then
local y2, m2 = date2.year, date2.month
return "CET"
local years = y1 - y2
elseif (opt == "décalage") then
local months = m1 - m2
return "1"
local d1 = date1.day + hms(date1)
local d2 = date2.day + hms(date2)
local days, time
if d1 >= d2 then
days = d1 - d2
else
months = months - 1
-- Get days in previous month (before the "to" date) given December has 31 days.
local dpm = m1 > 1 and days_in_month(y1, m1 - 1, date1.calendar) or 31
if d2 >= dpm then
days = d1 - hms(date2)
else
else
days = dpm - d2 + d1
return "[[Heure normale d'Europe centrale|CET]]"
end
end
end
end
if months < 0 then
years = years - 1
months = months + 12
end
days, time = math.modf(days)
local H, M, S = h_m_s(time)
return setmetatable({
date1 = date1,
date2 = date2,
partial = false,  -- avoid index lookup
years = years,
months = months,
days = days,
hours = H,
minutes = M,
seconds = S,
isnegative = isnegative,
iszero = iszero,
age = _diff_age,
duration = _diff_duration,
}, diffmt)
end
end


return {
return fun
_current = current,
_Date = Date,
_days_in_month = days_in_month,
}

Revision as of 11:46, 5 December 2021

This module provides date functions for use by other modules. Dates in the Gregorian calendar and the Julian calendar are supported, from 9999 BCE to 9999 CE. The calendars are proleptic—they are assumed to apply at all times with no irregularities.

A date, with an optional time, can be specified in a variety of formats, and can be converted for display using a variety of formats, for example, 1 April 2016 or April 1, 2016. The properties of a date include its Julian date and its Gregorian serial date, as well as the day-of-week and day-of-year.

Dates can be compared (for example, date1 <= date2), and can be used with add or subtract (for example, date + '3 months'). The difference between two dates can be determined with date1 - date2. These operations work with both Gregorian and Julian calendar dates, but date1 - date2 is nil if the two dates use different calendars.

The module provides the following items.

Export Description
_current Table with the current year, month, day, hour, minute, second.
_Date Function that returns a table for a specified date.
_days_in_month Function that returns the number of days in a month.

The following has examples of using the module:

Formatted output

A date can be formatted as text. <syntaxhighlight lang="lua"> local Date = require('Module:Date')._Date local text = Date(2016, 7, 1):text() -- result is '1 July 2016' local text = Date(2016, 7, 1):text('%-d %B') -- result is '1 July' local text = Date('1 July 2016'):text('mdy') -- result is 'July 1, 2016' </syntaxhighlight>

The following simplified formatting codes are available.

Code Result
hm hour:minute, with "am" or "pm" or variant, if specified (14:30 or 2:30 pm or variant)
hms hour:minute:second (14:30:45)
ymd year-month-day (2016-07-01)
mdy month day, year (July 1, 2016)
dmy day month year (1 July 2016)

The following formatting codes (similar to strftime) are available.

Code Result
%a Day abbreviation: Mon, Tue, ...
%A Day name: Monday, Tuesday, ...
%u Day of week: 1 to 7 (Monday to Sunday)
%w Day of week: 0 to 6 (Sunday to Saturday)
%d Day of month zero-padded: 01 to 31
%b Month abbreviation: Jan to Dec
%B Month name: January to December
%m Month zero-padded: 01 to 12
%Y Year zero-padded: 0012, 0120, 1200
%H Hour 24-hour clock zero-padded: 00 to 23
%I Hour 12-hour clock zero-padded: 01 to 12
%p AM or PM or as in options
%M Minute zero-padded: 00 to 59
%S Second zero-padded: 00 to 59
%j Day of year zero-padded: 001 to 366
%-d Day of month: 1 to 31
%-m Month: 1 to 12
%-Y Year: 12, 120, 1200
%-H Hour: 0 to 23
%-M Minute: 0 to 59
%-S Second: 0 to 59
%-j Day of year: 1 to 366
%-I Hour: 1 to 12
%% %

In addition, %{property} (where property is any property of a date) can be used.

For example, Date('1 Feb 2015 14:30:45 A.D.') has the following properties.

Code Result
%{calendar} Gregorian
%{year} 2015
%{month} 2
%{day} 1
%{hour} 14
%{minute} 30
%{second} 45
%{dayabbr} Sun
%{dayname} Sunday
%{dayofweek} 0
%{dow} 0 (same as dayofweek)
%{dayofweekiso} 7
%{dowiso} 7 (same as dayofweekiso)
%{dayofyear} 32
%{era} A.D.
%{gsd} 735630 (numbers of days from 1 January 1 CE; the first is day 1)
%{juliandate} 2457055.1046875 (Julian day)
%{jd} 2457055.1046875 (same as juliandate)
%{isleapyear} false
%{monthdays} 28
%{monthabbr} Feb
%{monthname} February

Some shortcuts are available. Given date = Date('1 Feb 2015 14:30'), the following results would occur.

Code Description Example result Equivalent format
date:text('%c') date and time 2:30 pm 1 February 2015 %-I:%M %p %-d %B %-Y %{era}
date:text('%x') date 1 February 2015 %-d %B %-Y %{era}
date:text('%X') time 2:30 pm %-I:%M %p

Julian date

The following has an example of converting a Julian date to a date, then obtaining information about the date. <syntaxhighlight lang="lua"> -- Code -- Result Date = require('Module:Date')._Date date = Date('juliandate', 320) number = date.gsd -- -1721105 number = date.jd -- 320 text = date.dayname -- Saturday text = date:text() -- 9 October 4713 BC text = date:text('%Y-%m-%d') -- 4713-10-09 text = date:text('%{era} %Y-%m-%d') -- BC 4713-10-09 text = date:text('%Y-%m-%d %{era}') -- 4713-10-09 BC text = date:text('%Y-%m-%d %{era}', 'era=B.C.E.') -- 4713-10-09 B.C.E. text = date:text('%Y-%m-%d', 'era=BCNEGATIVE') -- -4712-10-09 text = date:text('%Y-%m-%d', 'era=BCMINUS') -- −4712-10-09 (uses Unicode MINUS SIGN U+2212) text = Date('juliandate',320):text('%{gsd} %{jd}') -- -1721105 320 text = Date('Oct 9, 4713 B.C.E.'):text('%{gsd} %{jd}') -- -1721105 320 text = Date(-4712,10,9):text('%{gsd} %{jd}') -- -1721105 320 </syntaxhighlight>

Date differences

The difference between two dates can be determined with date1 - date2. The result is valid if both dates use the Gregorian calendar or if both dates use the Julian calendar, otherwise the result is nil. An age and duration can be calculated from a date difference.

For example: <syntaxhighlight lang="lua"> -- Code -- Result Date = require('Module:Date')._Date date1 = Date('21 Mar 2015') date2 = Date('4 Dec 1999') diff = date1 - date2 d = diff.age_days -- 5586 y, m, d = diff.years, diff.months, diff.days -- 15, 3, 17 (15 years + 3 months + 17 days) y, m, d = diff:age('ymd') -- 15, 3, 17 y, m, w, d = diff:age('ymwd') -- 15, 3, 2, 3 (15 years + 3 months + 2 weeks + 3 days) y, m, w, d = diff:duration('ymwd') -- 15, 3, 2, 4 d = diff:duration('d') -- 5587 (a duration includes the final day) </syntaxhighlight>

A date difference holds the original dates except they are swapped so diff.date1 >= diff.date2 (diff.date1 is the more recent date). This is shown in the following. <syntaxhighlight lang="lua"> date1 = Date('21 Mar 2015') date2 = Date('4 Dec 1999') diff = date1 - date2 neg = diff.isnegative -- false text = diff.date1:text() -- 21 March 2015 text = diff.date2:text() -- 4 December 1999 diff = date2 - date1 neg = diff.isnegative -- true (dates have been swapped) text = diff.date1:text() -- 21 March 2015 text = diff.date2:text() -- 4 December 1999 </syntaxhighlight>

A date difference also holds a time difference: <syntaxhighlight lang="lua"> date1 = Date('8 Mar 2016 0:30:45') date2 = Date('19 Jan 2014 22:55') diff = date1 - date2 y, m, d = diff.years, diff.months, diff.days -- 2, 1, 17 H, M, S = diff.hours, diff.minutes, diff.seconds -- 1, 35, 45 </syntaxhighlight>

A date difference can be added to a date, or subtracted from a date. <syntaxhighlight lang="lua"> date1 = Date('8 Mar 2016 0:30:45') date2 = Date('19 Jan 2014 22:55') diff = date1 - date2 date3 = date2 + diff date4 = date1 - diff text = date3:text('ymd hms') -- 2016-03-08 00:30:45 text = date4:text('ymd hms') -- 2014-01-19 22:55:00 equal = (date1 == date3) -- true equal = (date2 == date4) -- true </syntaxhighlight>

The age and duration methods of a date difference accept a code that identifies the components that should be returned. An extra day is included for the duration method because it includes the final day.

Code Returned values
'ymwd' years, months, weeks, days
'ymd' years, months, days
'ym' years, months
'y' years
'm' months
'wd' weeks, days
'w' weeks
'd' days

local fun = {}

local Outils = require 'Module:Outils'
-- chargement de la base de données répertoriant certaines pages existant ou n'existant pas pour éviter les "ifexist".
local dataLiens
local success, resultat = pcall ( mw.loadData, 'Module:Date/Data' )
if success then
	dataLiens = resultat
else
	-- protection au cas où le sous-module serait mal modifié
	dataLiens = { [''] = { mois = { aucun = 1000, tous = { 1773, 2014 } }, } }
end

-- nettoie un paramètre non nommé (vire les espaces au début et à la fin)
-- retourne nil si le texte est vide ou n'est pas du texte. Attention c'est important pour les fonctions qui l'utilisent.
local trim = Outils.trim

-- Fonction destinée à mettre la première lettre du mois en majuscule :
-- utilisation de string car aucun mois ne commence par une lettre non ascii en français ou anglais.
local function ucfirst( str )
	return str:sub( 1, 1 ):upper() .. str:sub( 2 )
end

local modelePremier = '<abbr class="abbr" title="premier">1<sup>er</sup></abbr>'


-- liste des mois, écriture exacte et alias, en minuscule
local listeMois = {
	{ num = 1,  nJour = 31, abrev = 'janv.',  nom = 'janvier', alias = { 'jan.', 'janv.', 'jan', 'janv', 'january' } },
	{ num = 2,  nJour = 29, abrev = 'fév.',   nom = 'février', alias = { 'fevrier', 'fev.', 'fev', 'fév.', 'fév', 'févr', 'févr.', 'february', 'feb', 'feb.' } },
	{ num = 3,  nJour = 31, abrev = 'mars',   nom = 'mars', alias = { 'mar.', 'mar', 'march' } },
	{ num = 4,  nJour = 30, abrev = 'avr.',   nom = 'avril', alias = { 'avr.', 'avr', 'apr', 'april'} },
	{ num = 5,  nJour = 31, abrev = 'mai',    nom = 'mai', alias = { 'may' } },
	{ num = 6,  nJour = 30, abrev = 'juin',   nom = 'juin', alias = { 'jun', 'june' } },
	{ num = 7,  nJour = 31, abrev = 'juill.', nom = 'juillet', alias = { 'juil.', 'juil', 'juill.', 'juill', 'jul', 'july' } },
	{ num = 8,  nJour = 31, abrev = 'août',   nom = 'août', alias = { 'aoû', 'aug', 'august' } },
	{ num = 9,  nJour = 30, abrev = 'sept.',  nom = 'septembre', alias = { 'sept.', 'sept', 'sep.', 'sep', 'september' } },
	{ num = 10, nJour = 31, abrev = 'oct.',   nom = 'octobre', alias = { 'oct.', 'oct', 'october' } },
	{ num = 11, nJour = 30, abrev = 'nov.',   nom = 'novembre', alias = { 'nov.', 'nov', 'november' } },
	{ num = 12, nJour = 31, abrev = 'déc.',   nom = 'décembre', alias = { 'decembre', 'déc.', 'dec.', 'dec', 'déc', 'december' } },
	aout = { num = 8, nJour = 31, abrev = 'aout', nom = 'aout', alias = { 'aou' } },
}

-- ajoute les noms, abréviations et alias en tant que clés de listeMois
for i = 1, 12 do
	local mois = listeMois[i]
	listeMois[tostring( i )] = mois
	if i < 10 then
		listeMois['0' .. i] = mois
	end
	listeMois[mois.nom] = mois
	listeMois[mois.abrev] = mois
	for j = 1, #mois.alias do
		listeMois[mois.alias[j]] = mois
	end
end
for i = 1, #listeMois.aout.alias do
	listeMois[listeMois.aout.alias[i]] = listeMois.aout
end

local liste_saisons = {
	{ 'printemps', 'spring', },
	{ 'été', 'summer', },
	{ 'automne', 'autumn', },
	{ 'hiver', 'winter', },
}

-- à partir d'un nom de saison (en français ou en anglais),
-- retourne son nom canonique (exemple : "été")
-- si non reconnu, retourne nil
function fun.determinationSaison( saison )
	local s = trim( saison )
	if s then
		s = mw.ustring.lower( s )
		for i = 1, 4 do
			for j = 1, #liste_saisons[i] do
				if s == liste_saisons[i][j] then
					return liste_saisons[i][1]
				end
			end
		end
	end
end

---
-- à partir d'un nom de mois (en français ou en anglais), de son numéro ou d'une abréviation,
-- retourne son nom canonique (exemple : "juin") et son numéro (exemple : 6)
-- si non reconnu, retourne nil, nil
function fun.determinationMois( mois )
	local result

	local num = tonumber( mois )
	if num then
		result = listeMois[num]
	else
		local str = trim( mois )
		if str then
			result = listeMois[str]
			if not result then
				result = listeMois[mw.ustring.lower( str )]
			end
		end
	end

	if result then
		return result.nom, result.num
	else
		return nil, nil
	end
end


-- fonction interne à modeleDate, pour déterminer si on peut se passer de faire un ifexist
local function existDate( dataQualificatif, annee, mois )
	local data
	if mois then
		data = dataQualificatif.mois
	else
		data = dataQualificatif.annee
	end
	if type( data ) ~= 'table' then
		-- si data n'existe pas c'est que l'on considère qu'il n'y a pas de lien.
		return
	end
	-- le qualificatif est remplacé par celui de la base de données, ce qui permet des alias.
	local lien = annee
	if dataQualificatif.qualificatif ~= '' then
		lien = lien .. ' ' .. dataQualificatif.qualificatif
	end
	local seul = annee
	if mois then
		lien = mois .. ' ' .. lien
		seul = ucfirst( mois ) .. ' ' .. annee
	end
	local aucun = tonumber( data.aucun )
	if aucun and annee <= aucun then
		-- si l'année est dans la partie 'aucun' on teste s'il y a malgré tout un lien isolé
		if type( data.seul ) == 'table' then
			for i, v in ipairs( data.seul ) do
				if seul == v or seul == tonumber( v ) then
					return lien
				end
			end
		end
		-- partie aucun et pas de lien => nil
		return nil
	elseif type( data.tous ) == 'table' then
		local tous1, tous2 = tonumber( data.tous[1] ), tonumber( data.tous[2] )
		if tous1 and tous2 and annee >= tous1 and annee <= tous2 then
			-- l'année est dans la partie 'tous' donc on retourne le lien
			return lien
		end
	end
	-- l'année n'est ni dans la partie aucun, ni dans la partie tous donc il faut tester si la page existe.
	local cibleLien = mw.title.new( lien )
	if cibleLien and cibleLien.exists then
		return lien
	end
end

---
-- Supprime le jour de la semaine, et "le" avant une date
function fun.nettoyageJour( jour )
	if type( jour ) == 'string' then
		local nomJour = { '[Ll]undi', '[Mm]ardi', '[Mm]ercredi', '[Jj]eudi', '[Vv]endredi',
			'[Ss]amedi', '[Dd]imanche', '^ *[Ll]e' }
		local premier = { '<abbr class="abbr ?" title="[Pp]remier" ?>1<sup>er</sup></abbr>', '1<sup>er</sup>', '1er' }
		for i = 1, #nomJour do
			jour = jour:gsub( nomJour[i], '' )
		end
		for i = 1, #premier do
			jour = jour:gsub( premier[i], '1' )
		end
		jour = trim( jour )
	end
	return jour
end

---
-- Sépare une chaine date en une table contenant les champs jour, mois et annee.
-- la date doit contenir le mois.
function fun.separationJourMoisAnnee( date )
	date = trim( date )
	if date then
		local function erreur( periode, valeur )
			return false, '<span class="error">' .. periode .. ' invalide (' .. valeur .. ')</span>'
		end

		local dateAvantCleanup = date
		local jour, mois, annee, masquerMois, masquerAnnee, separateur

		-- variable pour construire les regex
		local j = '([0-3]?%d)'                            -- jour
		local m = '([01]?%d)'                             -- mois numérique
		local mmm = '([^%s%p%d]+[.]?)'                    -- mois en toute lettre
		local mmm2 = '([^%s%p%d]+[.]?[-/][^%s%p%d]+[.]?)' -- mois-mois en toute lettre
		local aj = '(%-?%d+)'                             -- année ou jour
		local s = '[ ./-]+'                               -- séparateur simple
		local sep = '([ ./-]+)'                           -- séparateur avec capture, pour le détecter deux fois
		local moins = '(%-?)'                             -- signe moins pour signifier qu'il ne faut pas afficher cette donnée

		date = fun.nettoyageJour( date )
		if date == nil then
			return erreur( 'Date', dateAvantCleanup )
		end
		-- suppression catégorie, liens, balises
		date = mw.ustring.gsub( date, '%[%[[Cc]at[ée]gor[yi]e?:.-%]%]', '' )
		date = date	:gsub( '%b<>', '' )
					:gsub( '%[%[([^%[%]|]*)|?([^%[%]]*)%]%]', function ( l, t ) return trim( t ) or l end )
		-- suppression des espaces insécables
					-- nbsp
					:gsub( '\194\160', ' ' )
					:gsub( '&nbsp;', ' ' )
					:gsub( '&#160;', ' ' )
					-- narrow nbsp
					:gsub( '\226\128\175', ' ' )
					:gsub( '&#8239;', ' ' )
					-- thin space
					:gsub( '\226\128\137', ' ' )
					:gsub( '&thinsp;', ' ' )
					:gsub( '&#8201;', ' ' )
					-- simple space
					:gsub( '&#32;', ' ' )
					-- plusieurs espaces
					:gsub( ' +', ' ' )
		-- réduction av. J-C pour simplifier un peu les regex :
					:gsub( '(%d+) ?[Aa][Vv]%.? ?[Jj][ .-]*[Cc]%.?', '-%1' )
		-- suppression de l'heure dans les dates ISO
					:gsub( '^+?([%d-]*%d%d%-%d%d)T%d%d[%d:,.+-]*Z?$' , '%1')

		-- test année seule
		if date:match( '^'..aj..'$' ) then
			annee = date:match( '^'..aj..'$' )
		elseif date:match( '^'..aj..s..aj..moins..'$' ) then
			-- jj/mm, mm/aaaa ou aaaa/mm
			local a, separateur, b, sb = date:match( '^'..aj..sep..aj..moins..'$' )
			a, b = tonumber( a ), tonumber( b )
			if separateur:match( '^.+%-$' ) then
				-- probablement mm/-aaaa, année av.JC
				b = 0 - b
			end
			if  a > 12 and ( b < 1 or b > 31 ) or
				b > 12 and ( a < 1 or a > 31 ) then
				return erreur( 'Date', dateAvantCleanup )
			elseif b < 1 or b > 31 then
				mois, annee, masquerAnnee = a, b, sb
			elseif a < 1 or a > 31 then
				annee, mois = a, b
			elseif b > 12 then
				return erreur( 'Mois', b )
			else
				jour, mois, masquerMois = a, b, sb
			end
		elseif date:match( '^'..aj..sep..m..moins..'%2'..aj..moins..'$' ) then
			-- jj/mm/aaaa ou aaaa/mm/jj
			jour, separateur, mois, masquerMois, annee, masquerAnnee = date:match( '^'..aj..sep..m..moins..'%2'..aj..moins..'$' )
			if separateur == '-' and masquerMois == '-' and masquerAnnee == '' and tonumber( annee ) > 0 then
				-- date au format jj-mm--aaaa type 17-06--44 pour 17 juin 44 av. JC
				masquerMois = nil
				annee = 0 - annee
			end
		elseif date:match( '^'..j..sep..mmm..moins..'%2'..aj..moins..'$' ) then
			-- jj mmm aaaa
			jour, separateur, mois, masquerMois, annee, masquerAnnee = date:match( '^'..j..sep..mmm..moins..'%2'..aj..moins..'$' )
		elseif date:match( '^'..mmm..s..aj..moins..'$' ) then
			-- mmm aaaa
			mois, separateur, annee, masquerAnnee = date:match( '^'..mmm..sep..aj..moins..'$' )
			if separateur:match( '^.+%-$' ) then
				annee = '-' .. annee
			end
		elseif date:match( '^'..mmm2..s..aj..moins..'$' ) then
			-- mmm-mmm aaaa
			mois, separateur, annee, masquerAnnee = date:match( '^'..mmm2..sep..aj..moins..'$' )
			if separateur:match( '^.+%-$' ) then
				annee = '-' .. annee
			end
		elseif date:match( '^'..j..s..mmm..moins..'$' ) then
			-- jj mmm
			jour, mois, masquerMois = date:match( '^'..j..s..mmm..moins..'$' )
		elseif date:match( '^'..mmm..s..j..', ?'..aj..'$') then
			-- mmm jj, aaaa (format anglo-saxon)
			mois, jour, annee = date:match( '^'..mmm..s..j..', ?'..aj..'$')
		elseif date:match( '^'..mmm..'$' ) then
			mois = date
		else
			return erreur( 'Date', dateAvantCleanup )
		end
		local jn, an = tonumber( jour ), tonumber( annee )
		if jn and an and ( jn > 31 or jn < 0 or #jour >= 3 ) and an <= 31 then
			-- cas notamment des date ISO 2015-06-17, -0044-06-17 et -0002-06-17
			-- inversion du jour et de l'année
			local temp = annee
			annee = jour
			jour = temp
		end

		return fun.validationJourMoisAnnee{
			jour, mois, annee,
			masquerAnnee = trim( masquerAnnee ) and true or nil,
			masquerMois = ( trim( masquerAnnee ) or not annee ) and trim( masquerMois ) and true or nil,
			-- or nil sert juste à éviter de trainer une valeur false dans tous les tests unitaires.
		}
	else
		return true, {}
	end
end


---
-- validationJourMoisAnnee vérifie que les paramètres correspondent à une date valide.
-- la date peut être dans les paramètres 1 à 3, ou dans des paramètres jour, mois et annee.
-- La fonction retourne true suivi d'une table avec la date en paramètres nommés (sans accent sur année)
-- ou false suivi d'un message d'erreur.
function fun.validationJourMoisAnnee( frame )
	local args = Outils.extractArgs( frame )
	local jour, mois, numMois, annee
	local bjour = args[1] or args['jour'] or ''
	local bmois = tostring( args[2] or args['mois'] or '' )
	local bannee = args[3] or args['annee'] or args['année'] or ''

	local function erreur( periode, valeur )
		return false, '<span class="error">' .. periode .. ' invalide (' .. valeur .. ')</span>'
	end

	-- on traite l'année
	if Outils.notEmpty( bannee ) then
		annee = tonumber( bannee )
		if annee == nil and type( bannee ) == 'string' then
			-- test si l'année contient av. J.-C.
			annee = bannee:upper():match( '^(%d+) ?[Aa][Vv]%.? ?[Jj][ .-]*[Cc]%.?' )
			annee = tonumber( annee )
			if annee then
				annee = 0 - annee
			else
				return erreur( 'Année', bannee )
			end
		elseif annee == 0 then
			return erreur( 'Année', 0 )
		end
	else
		annee = nil
	end

	-- on traite le mois
	if Outils.notEmpty( bmois ) then
		mois, numMois = fun.determinationMois( bmois )
		if mois == nil then
			mois = fun.determinationSaison( bmois )
			if mois == nil then
				local mois1, sep, mois2 = bmois:match( '^([^%s%p%d]+[.]?)([-/])([^%s%p%d]+[.]?)$' )
				if mois1 then
					mois1 = fun.determinationMois( mois1 )
					mois2 = fun.determinationMois( mois2 )
					if mois1 == nil or mois2 == nil then
						return erreur( 'Mois', bmois )
					end
					mois = mois1 .. sep .. mois2
				else
					return erreur( 'Mois', bmois )
				end
			end
		end
		-- on traite le jour si présent
		if Outils.notEmpty( bjour ) then
			if not numMois then
				erreur( 'Date', 'jour avec saison ou plusieurs mois' )
			end
			jour = tonumber( bjour )
			if jour == nil then
				jour = tonumber( fun.nettoyageJour( bjour ) )
			end
			if jour == nil then
				return erreur( 'Jour', bjour )
			end
			-- on valide que le jour est correct
			if jour < 1 or jour > 31 then
				return erreur( 'Jour', bjour )
			elseif jour > listeMois[numMois].nJour then
				return erreur( 'Jour', bjour .. ' ' .. mois )
			elseif jour == 29 and numMois == 2 and annee and ( math.fmod( annee, 4 ) ~= 0 ) then
				-- l'année bisextile sur les siècles est toujours acceptée pour être compatible avec les dates juliennes.
				return erreur( 'Jour', '29 février ' .. annee )
			end
		else
			-- S'il n'y a pas de jour on regarde si la première lettre du mois est en majuscule
			if bmois:match( '^%u' ) then
				-- oui, on passe la première lettre en majuscule
				mois = ucfirst( mois )
			end
			-- s'il n'y a pas d'année non plus on retourne le mois simple
		end
	else
		-- on teste le jour si présent
		if Outils.notEmpty( bjour ) then
			if annee then
				return erreur( 'Mois', 'absent' )
			else
				bjour = fun.nettoyageJour( bjour )
				jour = tonumber( bjour )
				if jour then
					if jour > 31 or jour < 1 then
						annee = jour
						jour = nil
					else
						return erreur( 'Date', 'jour seul : ' .. bjour )
					end
				else
					return erreur( 'Jour', bjour )
				end
			end
		end
	end

	-- vérification de l'absence d'un décalage
	if annee and annee < 13 and annee > 0 and not jour and ( tonumber( bmois ) or ( not mois and tonumber( args[4] ) ) ) then
		return false, '<span class="error">année improbable (' .. annee .. ')</span>'
	end

	local resultat = {
		jour = jour,
		mois = mois,
		numMois = numMois,
		annee = annee,
		masquerAnnee = args.masquerAnnee,
		masquerMois = args.masquerMois,
	}
	return true, resultat
end


---
-- émule le modèle {{m|Date}}.
-- Paramètres :
--		1 : jour (numéro ou "1er") ou la date complète
--		2 : mois (en toutes lettres) ou spécialité de l'année
--		3 : année (nombre)
--		4 : spécialité de l'année
--		julien : date dans le calendrier julien
--		compact : affiche le mois sous forme d'abréviation
--		avJC : non pour désactiver l'affichage de « av. J.-C. » pour les dates négatives
--		âge : ajoute la durée depuis cette date
--		agePrefix : préfixe pour l'age, 'à ' par défaut pour les décès
--		nolinks : ne met pas de lien sur la date
--		afficherErreurs : en cas d'erreur, si défini à "non" ne retourne pas un message d'erreur, mais le 1er argument inchangé
--		categoriserErreurs : en cas d'erreur, si défini à "non" ne catégorise pas ; peut aussi être défini avec une catégorie à utiliser à la place de celle par défaut
--		naissance : ajoute la class "bday"
--		mort : ajoute la class "dday"
function fun.modeleDate( frame )
	local Yesno = require 'Module:Yesno'

	local args = Outils.extractArgs( frame )
	local resultat

	local dateNaissanceMort

	-- analyse des paramètres non nommés (ou paramètres de la date jour, mois, annee)
	local test, params
	local arg1, arg2, arg3 = fun.nettoyageJour( args[1] ), trim( args[2] ), trim( args[3] )
	if type( arg1 ) == 'string' and arg3 == nil and ( arg1:match( '[^ ./-][ ./-]+[^ ./-]' ) or arg2 == nil or dataLiens[arg2] or mw.ustring.match( arg2, '%a %a' ) ) then
		-- la date est dans le premier paramètre
		test, params = fun.separationJourMoisAnnee( arg1 )
		if test then
			dateNaissanceMort = trim( arg2 )
			params.qualificatif = trim( arg2 )
		end
	elseif type( arg1 ) == 'string' and type( arg2 ) == 'string' and arg3 ~= nil and arg4 == nil and ( arg1:match( '[^ ./-][ ./-]+[^ ./-]' ) or dataLiens[arg3] or mw.ustring.match( arg3, '%a %a' ) ) then
		-- la date est dans le premier paramètre
		test, params = fun.separationJourMoisAnnee( arg1 )
		if test then
			dateNaissanceMort = trim( arg2 )
			params.qualificatif = trim( arg3 )
		end
	else
		local function masquerParam( p )
			-- sépare le signe moins final éventuel signifiant que le paramètre ne doit pas être affiché.
			if type( p ) ~= 'string' then
				return p, nil
			end
			local value, mask = p:match( '^%s*(.-)(%-?)%s*$' )
			return value, ( mask == '-' or nil )
		end
		local cleanArgs = { arg1 or args.jour }
		cleanArgs[2], cleanArgs.masquerMois = masquerParam( args[2] or args.mois )
		cleanArgs[3], cleanArgs.masquerAnnee = masquerParam( args[3] or args.annee or args['année'] )

		test, params = fun.validationJourMoisAnnee( cleanArgs )
		if test then
			params.qualificatif = trim( args[4] )
		end
	end

	-- analyse des paramètres nommés
	if test then
		params.agePrefix = args.agePrefix
		if args.qualificatif and args.qualificatif ~= '' then
			params.qualificatif = args.qualificatif
		end

		-- julien peut avoir trois valeurs : inactif, format standard (true), format court
		params.julien = Yesno( args.julien, 'court', false )
		params.avJC = Yesno( args.avJC )

		if args['républicain'] and args['républicain'] ~= '' then
			if args['républicain'] == 'liens' then
				params.republicain = 'liens'
			else
				params.republicain = Yesno( args['républicain'], false )
			end
		else
			params.republicain = false
		end
		if args.dateNaissanceMort and args.dateNaissanceMort ~= '' then
			dateNaissanceMort = args.dateNaissanceMort
		elseif args['dateNaissanceÉvénement'] and args['dateNaissanceÉvénement'] ~= '' then
			dateNaissanceMort = args['dateNaissanceÉvénement'] 
		end
		if dateNaissanceMort then
			local testNaissanceMort, paramsNaissanceMort = fun.separationJourMoisAnnee( dateNaissanceMort )
			if testNaissanceMort then
				params.anneeNaissanceMort, params.moisNaissanceMort, params.numMoisNaissanceMort, params.jourNaissanceMort = paramsNaissanceMort.annee, paramsNaissanceMort.mois, paramsNaissanceMort.numMois, paramsNaissanceMort.jour
			end
		end

		local listeParam = {
			age = 'âge',
			['âge'] = 'âge',
			naissance = 'naissance',
			mort = 'mort',
			['événement'] = 'événement',
			evenement = 'evenement',
			['décès'] = 'mort',
			apJC = 'apJC',
			nolinks = 'nolinks',
			compact = 'compact',
			compacte = 'compact',
		}
		for n, v in pairs( listeParam ) do
			params[v] = params[v] or Yesno( args[n], true, false ) or nil
		end

		-- sortie pour les tests unitaire, ou pour débugger
		if args.debug then
			return params
		end

		resultat = fun._modeleDate( params )

	else
		local yn_afficherErreurs = Yesno( args.afficherErreurs )
		if yn_afficherErreurs == nil or yn_afficherErreurs == true then
			resultat = params
		else
			resultat = args[1]
		end

		local currentTitle = mw.title.getCurrentTitle()

		if currentTitle:inNamespaces( 0, 4, 10, 14, 100 )
		and not Outils.notEmpty( args.nocat )
		and not currentTitle.prefixedText:match( '^Modèle:.+/Test$' ) then
			local categorie
			local yn_categoriserErreurs = Yesno( args.categoriserErreurs, 'custom', true )
			if yn_categoriserErreurs == nil or yn_categoriserErreurs == true then
				categorie = '[[Catégorie:Page utilisant le modèle date avec une syntaxe erronée]]'
			elseif yn_categoriserErreurs == false then
				categorie = ''
			else
				local nomCategorie = args.categoriserErreurs
					:gsub( '^%[%[', '' )
					:gsub( '%]%]$', '' )
					:gsub( '^:?[Cc]atégorie:', '' )
					:gsub( '^:?[Cc]atégory:', '' )
				categorie = '[[Catégorie:' .. nomCategorie .. ']]'
			end
			resultat = resultat .. categorie
		end
	end

	return resultat or ''
end

function fun._modeleDate( args )
	local annee, mois, numMois, jour = args.annee, args.mois, args.numMois, args.jour
	local qualificatif = args.qualificatif

	if ( annee or mois or jour ) == nil then
		return
	end

	-- on traite l'âge, naissance et mort
	local agePrefix = args.agePrefix
	local age = args['âge'] and fun.age( annee, numMois, jour )
	local naissance = args.naissance
	local mort = args.mort
	local evenement = args['événement'] or args.evenement
	if mort and args.anneeNaissanceMort then
		age = fun.age( args.anneeNaissanceMort, args.numMoisNaissanceMort, args.jourNaissanceMort, annee, numMois, jour )
		agePrefix = agePrefix or 'à ' -- faut-il mettre \194\160 ?
	elseif evenement and args.anneeNaissanceMort then
		if naissance then
			age = fun.age( annee, numMois, jour, args.anneeNaissanceMort, args.numMoisNaissanceMort, args.jourNaissanceMort )
		else
			age = fun.age(args.anneeNaissanceMort, args.numMoisNaissanceMort, args.jourNaissanceMort,  annee, numMois, jour )
		end
	end
	agePrefix = agePrefix or ''

	-- on traite le calendrier
	local gannee, gmois, gjour = annee, numMois, jour   -- date suivant le calendrier grégorien pour <time>
	local jannee, jmois, jjour = annee, mois, jour      -- date suivant le calendrier julien si necessaire
	local julienDate, julienSup, julienSep              -- servira éventuellement à afficher la date selon le calendrier julien
	local gregAprMois, gregAprAn, gregFin               -- message de calendrier grégorien lorsque la date est selon le calendrier julien
	if annee and jour then
		local amj = annee * 10000 + numMois * 100 + jour
		if amj < 15821014 then
			if annee > 0 then
				gannee, gmois, gjour = fun.julianToGregorian( annee, numMois, jour )
			else
				-- calendrier grégorien proleptique avec année 0.
				gannee, gmois, gjour = fun.julianToGregorian( annee + 1, numMois, jour )
			end
			args.julien = false

		elseif args.julien then
			gannee, gmois, gjour = fun.julianToGregorian( annee, numMois, jour )
			annee, mois, jour = gannee, listeMois[gmois].nom, gjour
			if jjour == 1 then
				jjour = modelePremier
			end
			if args.compact then
				jmois = listeMois[jmois].abrev
			end
			if args.julien == 'court' then
				julienDate = jjour .. ' ' .. jmois .. ' '
				julienSup = '<sup>[[calendrier julien|jul.]]</sup>'
				if jannee == annee then
					gregAprMois = '<sup>[[calendrier grégorien|grég.]]</sup>'
				else
					julienDate = julienDate .. jannee .. ' '
					gregAprAn = '<sup>[[calendrier grégorien|grég.]]</sup>'
				end
				julienSep = ' / '
			else
				julienDate = jjour .. ' ' .. jmois .. ' ' .. jannee
				julienSep = ' ('
				gregFin = ' [[Passage du calendrier julien au calendrier grégorien|dans le calendrier grégorien]])'
			end

		elseif args.republicain then
			local DateRep = require 'Module:Date républicaine'
			local RepSansLiens
			if args.republicain == 'liens' then
				RepSansLiens = false
			else
				RepSansLiens = true
			end
 			dateRepublicaine = DateRep._date_republicaine(
 				RepSansLiens,
 				{ fun.formatRepCal( fun.do_toRepCal{gannee, gmois, gjour} ) }
 			)
		end
	else
		if annee and annee < 0 then
			gannee = gannee + 1
		end
		args.julien = false
		args.republicain = false
	end

	-- on génère le résultat

	-- Déclarations des variables
	local wikiListe = {}                   -- reçoit le texte affiché pour chaque paramètre
	local iso = {}                         -- reçoit le format date ISO de ce paramètre
	local texteMois = mois                 -- texte du mois qui sera affiché (éventuellement l'abréviation)
	if args.compact then
		if not numMois then
			-- mois est autre chose qu'un simple mois : saison, mois-mois... auquel cas, pas d'abréviation (provoquait erreur Lua)
			-- (les abréviations pour le cas "mois[-/]mois" seraient théoriquement possibles, mais ça reste à implémenter)
		else
			if args.nolinks then
				texteMois = '<abbr class=abbr title="' .. mois .. '">' .. listeMois[mois].abrev .. '</abbr>'
			else
				texteMois = listeMois[mois].abrev
			end
		end
	end
	mois = mois and mois:gsub( 'aout', 'août' )

	local dataQualificatif, dataCat
	if not args.nolinks then
		dataQualificatif = dataLiens[qualificatif or '']
		if type( dataQualificatif ) ~= 'table' then
			-- si le qualificatif n'est pas dans la base de données, on crée une table minimum,
			-- qui imposera un test sur l'année, mais considère qu'il n'y a pas de lien sur le jour ou le mois
			dataQualificatif = { qualificatif = qualificatif, annee = { } }
		end
		dataCat = dataLiens[dataQualificatif.cat]
		if type( dataCat ) ~= 'table' or dataCat == dataQualificatif then
			dataCat = { qualificatif = '' }
		end
	end
	local function wikiLien( lien, texte )
		if lien == texte then
			return '[[' .. texte .. ']]'
		else
			return '[[' .. lien .. '|' .. texte .. ']]'
		end
	end


	-- le jour si présent
	local qualifJour = ''
	if jour then
		if args.nolinks then
			if jour == 1 then
				jour = modelePremier
			end
			table.insert( wikiListe, jour )
		else
			qualifJour = dataQualificatif.jour and dataQualificatif.qualificatif
				or dataCat.jour and dataCat.qualificatif
				or ''
			local texteJour, lien
			if jour == 1 then
				texteJour = '1<sup>er</sup>'
				lien = '1er ' .. mois
			else
				texteJour = jour
				lien = jour .. ' ' .. mois
			end
			if qualifJour ~= '' then
				lien = lien .. ' ' .. qualifJour
			end
			-- s'il n'y a pas de lien sur le mois, il sera affiché avec le jour.
			table.insert( wikiListe, wikiLien( lien, texteJour ) )
			table.insert( wikiListe, wikiLien( lien, texteJour .. ' '.. texteMois ) )
		end
		table.insert( iso, 1, string.sub( '0' .. gjour, -2 ) )
	end

	-- le mois
	if mois then
		if #wikiListe == 0 and annee == nil then
			return texteMois
		end
		if args.nolinks then
			if not args.masquerMois then
				table.insert( wikiListe, texteMois )
			end
		else
			local lien
			if annee then
				if not numMois then
					-- mois est autre chose qu'un simple mois : saison, mois-mois... auquel cas, pas de lien
				else
					lien = existDate( dataQualificatif, annee, mois ) or existDate( dataCat, annee, mois )
					if lien == nil and qualificatif and qualifJour == '' then
						-- nouveau test sans le qualificatif uniquement s'il n'y a pas d'éphémérides pour ce qualificatif.
						lien = existDate( dataLiens[''], annee, mois )
					end
				end
			end
			if lien or args.masquerMois then
				-- s'il y a un lien on retire le lien affichant 'jour mois' pour ajouter '[[mois annee|mois]]'
				table.remove( wikiListe )
				if not args.masquerMois then
					table.insert( wikiListe, wikiLien( lien, texteMois ) )
				end
			elseif #wikiListe > 0 then
				-- sinon on retire le lien affichant 'jour' pour ne garder que le lien 'jour mois'
				table.remove( wikiListe, #wikiListe - 1 )
			elseif args.masquerAnnee then
				-- s'il n'y a pas de jour et que l'année n'est pas affichée, on insère le mois seul.
				table.insert( wikiListe, texteMois )
			end
		end
		if gmois then
			table.insert( iso, 1, string.sub( '0' .. gmois, -2 ) )
		end
		table.insert( wikiListe, gregAprMois )
	end

	-- l'année
	if annee and not (args.julien == true and args.nolinks and jannee == annee ) then
		if not args.masquerAnnee then
			local texteAnnee = annee
			local lien
			if annee < 0 then
				local annneeAvJc = 0 - annee
				lien = lien or ( annneeAvJc .. ' av. J.-C.' )
				if args.avJC == false then
					texteAnnee = annneeAvJc
				else
					texteAnnee = annneeAvJc .. ' <abbr class="abbr" title="'
						.. annneeAvJc .. ' avant Jésus-Christ">av. J.-C.</abbr>'
				end
			elseif args.apJC then
				texteAnnee = texteAnnee .. ' <abbr class="abbr" title="'
					.. texteAnnee .. ' après Jésus-Christ">apr. J.-C.</abbr>'
			end
			if args.nolinks then -- seulement si on doit l'afficher
				table.insert( wikiListe, texteAnnee )
			else
				lien = existDate( dataQualificatif, annee ) or existDate( dataCat, annee ) or lien or annee
				if mois and #wikiListe == 0 then
					-- si le mois n'a pas de lien et n'est pas affiché avec le jour, il est affiché avec l'année.
					texteAnnee = texteMois .. ' ' .. texteAnnee
				end
				table.insert( wikiListe, wikiLien( lien, texteAnnee ) )
			end
		end
	end
	if annee then
		if gannee > 999 then
			table.insert( iso, 1, gannee )
		elseif gannee > -1 then
			table.insert( iso, 1, string.sub( '000' .. gannee , -4 ) )
		elseif gannee > -999 then
			-- calendrier grégorien proleptique avec année 0.
			table.insert( iso, 1, 'U-' .. string.sub( '000' .. ( 0 - gannee ), -4 ) )
		else
			table.insert( iso, 1, 'U' .. gannee )
		end
	end
	table.insert( wikiListe, gregAprAn )

	-- l'age
	if type( age ) == 'number' and age >= 0 and ( not naissance or age < 120 ) then
		if age == 0 then
			age = '(' .. agePrefix .. 'moins d’un\194\160an)'
		elseif age == 1 then
			age = '(' .. agePrefix .. '1\194\160an)'
		else
			age = '('.. agePrefix .. age .. '\194\160ans)'
		end
	else
		age = false
	end


	-- compilation du résultat
	local wikiTexte = table.concat( wikiListe, ' ' )
	local isoTexte = table.concat( iso, '-' )

	-- On ajoute un peu de sémantique.
	local wikiHtml = mw.html.create( '' )

	if julienDate then
		wikiHtml:tag( 'span')
				:addClass( 'nowrap' )
				:attr( 'data-sort-value', isoTexte )
				:wikitext( julienDate )
				:node( julienSup )
				:done()
			:wikitext( julienSep )
	end

	local dateHtml = wikiHtml:tag( 'time' )
			:wikitext( wikiTexte )
	if wikiTexte:match( ' ' ) then
		dateHtml:addClass( 'nowrap' )
	end
	if isoTexte ~= wikiTexte then
		dateHtml:attr( 'datetime', isoTexte )
				:attr( 'data-sort-value', isoTexte )
	end
	if not args.nolinks then
		dateHtml:addClass( 'date-lien' )
	end
	if naissance then
		dateHtml:addClass( 'bday' )
	elseif mort then
		dateHtml:addClass( 'dday' )
	end

	wikiHtml:wikitext( gregFin )

	if args.republicain then
		wikiHtml:wikitext( ' (', dateRepublicaine, ')' )
	end

	if age then
		wikiHtml:wikitext( ' ' )
				:tag( 'span' )
					:addClass( 'noprint')
					:wikitext( age )
					:done()
	end

	return tostring( wikiHtml )
end


---
-- fonction destinée aux infobox, notamment pour afficher les dates de naissance et de mort
-- les liens présent dans les dates fournies sont automatiquement supprimés pour gérer les cas où
-- le paramètre contient déjà un modèle date.
-- Paramètres :
-- 		1 : type de date à afficher (naissance / n, mort / m, ou date / d)
-- 		1 : Date ou date de naissance
-- 		2 : Date de mort si type n ou m
-- 		qualificatif = suffixe des page de date à lier (exemple : en musique)
-- 		nolinks : n'affiche pas de lien
--		préfixe : préfixe à afficher s'il y a un jour (par défaut '')
--		préfixe sans jour : préfixe à afficher s'il n'y a pas de jour (par défaut : '')
function fun.dateInfobox( frame )
	local args = frame.args
	if type( args ) ~= 'table' or not ( args[1] and args[2] ) then
		return
	end

	-- analyseDate sépare la date du contenu qui suit, supprime les liens, et retourne si possible une table avec jour mois année
	local function analyseDate( d )
		if trim( d ) then
			local analyse = d:match( ' ou ') or d:match( 'entre ' ) or d:match( 'vers ' ) or d:match( 'après ' ) or d:match( 'avant ' )
			if analyse then
				return d
			end
			analyse = d:match( 'datetime="([%d-]+)"' ) or d
			-- sépare la date (avec ses liens) d'une référence ou contenu commençant par un espace)
			local debut, fin = analyse:match( '(.-%d%d%d%]*%-?)([\127 ].+)' )
			if not debut then
				-- sépare la date du contenu commençant par <br>
				debut, fin = analyse:match( '(.-%d%d%d%]*%-?)(<br ?/?>.+)' )
			end
			analyse = debut or analyse
			-- supprime les liens
			analyse = analyse:gsub(
				'%[%[([^%[%]|]*)|?([^%[%]]*)%]%]',
				function ( l, t )
					return trim( t ) or l
				end
			)
			local t, r = fun.separationJourMoisAnnee( analyse )
			if t then
				return r, fin
			else
				return d, fin
			end
		end
	end
	-- prefix ajoute un préfixe en fonction de la présence ou non du jour si le paramètre "préfixe sans jour" est défini
	local function prefix( dateString )
		if dateString then
			local datetime = dateString:match( 'datetime="([U%d%-]+)"' )
			if datetime and datetime:match('%-%d%d%-%d%d') and trim( args['préfixe'] ) then
				return args['préfixe'] .. ' ' .. dateString
			end
			if trim( args['préfixe sans jour'] ) then
				return args['préfixe sans jour'] .. ' ' .. dateString
			end
		end
		return dateString
	end

	local naissance = args[1]:match( '^n' ) == 'n'
	local mort = args[1]:match( '^m' ) or args[1]:match( 'décès' )
	local evenement = args[1]:match( '^é' )
	local affichageDate, qualificatif = args[2], args[4]
	local affichageDateTab, resultatDate, complementDate
	local dateNaissance, dateMort
	if mort or evenement then
		affichageDate = args[3]
	end
	if not trim( affichageDate ) then
		return
	end
	if affichageDate:match( '</time>' ) then
		-- S'il y a des liens il y a probablement déjà un modèle date, évitons de l'exécuter une 2e fois
		if ( naissance or mort or evenement ) and ( affichageDate:match( 'wikidata%-linkback' )) then
			dateNaissance = analyseDate( args[2] )
			dateMort = analyseDate( args[3] )
			resultatDate = affichageDate
		else
			return prefix( affichageDate )
		end
	else
		affichageDateTab, complementDate = analyseDate( affichageDate )
		if type( affichageDateTab ) ~= 'table' then
			return affichageDateTab
		else
			if naissance then
				dateNaissance = affichageDateTab
				dateMort = analyseDate( args[3] )
			elseif mort then
				dateNaissance = analyseDate( args[2] )
				dateMort = affichageDateTab
			else
				qualificatif = args[3]
			end
			affichageDateTab.naissance = naissance
			affichageDateTab.mort = mort
			affichageDateTab.evenement = evenement
			affichageDateTab.qualificatif = args.qualificatif or qualificatif
			affichageDateTab.nolinks = args.nolinks
			affichageDateTab.nocat = args.nocat
			affichageDateTab.julien = args.julien
		end
	end
	resultatDate = resultatDate or fun.modeleDate( affichageDateTab )

	local age, prefixAge, suffixAge, calculAge = '', ' <span class="noprint">(', ')</span>', nil
	if naissance and
		dateNaissance and
		not dateMort and
		type( dateNaissance ) == 'table'
	then
		calculAge = fun.age( dateNaissance.annee, dateNaissance.numMois, dateNaissance.jour )
		if calculAge and calculAge > 120 then
			calculAge = nil
		end
	elseif (mort or evenement) and
		dateNaissance and
		dateMort and
		type( dateNaissance ) == 'table'
		and type( dateMort ) == 'table'
	then
		calculAge = fun.age(
			dateNaissance.annee,
			dateNaissance.numMois,
			dateNaissance.jour,
			dateMort.annee,
			dateMort.numMois,
			dateMort.jour
		)
		prefixAge = ' (à '
		suffixAge = ')'
	end
	if tonumber( calculAge ) then
		if calculAge > 1 then
			age = prefixAge .. calculAge .. '\194\160ans' .. suffixAge
		elseif calculAge == 1 then
			age = prefixAge .. 'un\194\160an' .. suffixAge
		elseif calculAge == 0 then
			age = prefixAge .. 'moins d’un\194\160an' .. suffixAge
		end
		if complementDate and complementDate:match( 'ans?%)' ) then
			complementDate = ''
		end
	end

	return prefix( resultatDate ) .. ( complementDate or '' ) .. age
end


---
-- la fonction dateISO renvoie un date au format aaaa-mm-jj (sans liens)
-- l'année peut être sous la forme 2013 ou [[2013 en litérature|2013]]
-- le mois peut être en lettres ou en chiffres
-- le jour peut être sous la forme '05', '{{1er}}' ou 'vendredi 13'
function fun.dateISO( frame )
	local args = Outils.extractArgs( frame )
	local annee = Outils.notEmpty( args['année'], args.annee, args.year, args.date )
	-- extraction de l'année
	if type( annee ) == 'string' then
		annee = ( tonumber( annee )	-- match '2013'
				or string.match ( annee, '%D(%d%d%d%d)%D' ) -- match '[[2013 en musique|2013]]'
				or string.match ( annee, '%D(%d%d%d%d)$' )  -- match '17 septembre 2013'
				or string.match ( annee, '^(%d%d%d%d)%D' )  -- match '2013-09-17'
		)
	end
	annee = tonumber( annee )

	-- le format de date iso est défini suivant le calendrier grégorien.
	-- Avant l'année 1583 la date est calendrier est probablement du calendrier julien,
	-- donc autant s'abstenir.
	if annee and annee > 1582 then
		local mois = Outils.notEmpty( args.mois, args.month )
		-- num mois trouve le numéro du mois, qu'il soit numérique ou texte, complet ou abrégé.
		local nomMois, numMois = fun.determinationMois( mois )
		if numMois then
			mois = '-' .. string.sub( '0' .. numMois, -2 )

			local jour = Outils.notEmpty( args.jour, args.day, args['quantième'] )
			if type( jour ) == 'string' then
				jour = tonumber( jour ) or tonumber( string.match ( jour, '%d+') )
			end
			jour = tonumber( jour )
			if jour and jour <= listeMois[numMois].nJour then
				jour = '-' .. string.sub( '0' .. jour, -2 )
				return annee .. mois .. jour
			else
				return annee .. mois
			end
		else
			return tostring( annee )
		end
	end
end

---
-- Rang du jour dans l'année
-- Usage : do_dayRank{année,mois,jour}
function fun.do_dayRank(arguments)
	local yr = tonumber(arguments.year or arguments[1]) or 1
	local mt = tonumber(arguments.month or arguments[2]) or 1
	local dy = tonumber(arguments.day or arguments[3]) or 1
	-- Rangs des premiers des mois
	local ranks = {0,31,59,90,120,151,181,212,243,273,304,334}

	local rank = (ranks[mt] or 0) + dy - 1
	if(fun.isLeapYear(yr) and (mt >= 3)) then
		rank = rank+1
	end
	return rank
end

-- Nombre de jours entre deux années (du 1er janvier au 1er janvier)
-- Suit le calendrier grégorien
function fun.do_daysBetween(arguments)
	local yr1 = tonumber(arguments[1]) or 0
	local yr2 = tonumber(arguments[2]) or 0

	return fun.daysSinceOrigin(yr2) - fun.daysSinceOrigin(yr1)
end

-- Nombre de jours depuis l'année 1 (du 1er janvier au 1er janvier)
function fun.daysSinceOrigin(year)
	local yr = year-1
	return 365*yr + math.floor(yr/4) - math.floor(yr/100) + math.floor(yr/400)
end

-- Test d'année bissextile (Suit le calendrier grégorien)
function fun.isLeapYear(year)
	local yr = tonumber(year) or 1
	return (yr%4 == 0) and ((yr%100 ~= 0) or (yr%400 == 0))
end

-- Conversion d'un nombre en chiffres romains
function fun.toRoman(number)
	local n = math.floor(number)
	local letters = {"I","V","X","L","C","D","M","",""}
	local pattern = {"","0","00","000","01","1","10","100","1000","02"}
	local result = ""
	if(n<=0 or n>=4000) then
		result = "---"
	else
		for i=1,7,2 do
			local p = pattern[n%10 + 1]
			for j=0,2 do
				p = string.gsub(p,tostring(j),letters[i+j])
			end
			result = p .. result
			n = math.floor(n/10)
		end
	end
	return result
end

-- Conversion et affichage d'une date dans le calendrier républicain
function fun.dateRepublicain(frame)
	local pframe = frame:getParent()
	local arguments = pframe.args
	return fun.formatRepCal(fun.do_toRepCal(arguments))
end

---
-- Calcul d'une date dans le calendrier républicain
-- On suppose que les années 4n+3 sont sextiles (3, 7, 11...)
function fun.do_toRepCal(arguments)
	local yr = tonumber(arguments.year or arguments[1]) or 2000
	-- rang absolu du jour demandé, le jour 0 étant le 22 septembre 1792 (1er jour de l'an I)
	local repDays = fun.do_dayRank(arguments) + fun.do_daysBetween{1792,yr} - fun.do_dayRank{1792,9,22}
	local repYear = math.floor((repDays+731)/365.25) - 1
	local repDayRank = repDays - 365*(repYear-1) - math.floor(repYear/4)
	local repMonth, repDay = math.floor(repDayRank/30)+1, (repDayRank%30)+1
	return {repYear, repMonth, repDay}
end

---
-- Formatage d'une date selon le calendrier républicain
-- Usage : fun.formatRepCal{année,mois,jour}
function fun.formatRepCal(arguments)
	local months = {"Vendémiaire","Brumaire","Frimaire","Nivôse","Pluviôse","Ventôse","Germinal","Floréal","Prairial","Messidor","Thermidor","Fructidor"}
	local extras = {"de la vertu","du génie","du travail","des récompenses","de l'opinion","de la Révolution"}
	local result = ""
	if(arguments[2] < 13) then
		result = result .. tostring(arguments[3]) .. "\194\160" .. months[arguments[2]]
	else
		result = result .. "jour " .. extras[arguments[3]]
	end
	result = result .. " de l'an " .. fun.toRoman(arguments[1])
	return result
end

---
-- Voir Modèle:Âge
-- retourne l'âge en fonction de la ou les dates fournies. La valeur retournée est de type 'number'
-- Paramètres :
-- 1, 2, 3 : année, mois jour de naissance (supposé dans le calendrier grégorien)
-- 4, 5, 6 : année, mois, jour du calcul (facultatif, par défaut la date UTC courante).
function fun.age( an, mn, jn, ac, mc, jc )
	if ac == nil then
		local today = os.date( '!*t' )
		ac = today.year
		mc = today.month
		jc = today.day
	else
		ac = tonumber( ac )
		mc = tonumber( mc )
		jc = tonumber( jc )
	end

	local an = tonumber( an )
	local mn = tonumber( mn )
	local jn = tonumber( jn )

	if an == nil or ac == nil or mn == nil or mc == nil then
		-- pas de message d'erreur qui risque de faire planter la fonction appelante
		-- à elle de gérer ce retour.
		return
	end

	local age = ac - an
	if mc == mn then
		if jc == nil or jn == nil then
			return
		end
		return age-tonumber( jc < jn and 1 or 0 )
	else
		return age-tonumber( mc < mn and 1 or 0 )
	end
end

function fun.modeleAge( frame )
	local args = Outils.extractArgs( frame )
	local age = fun.age (
		args[1] or args['année'],
		args[2] or args['mois'],
		args[3] or args['jour'],
		args[4],
		args[5],
		args[6]
	)
	if age then
		return age
	else
		return '<span class="error">Paramètres incorrects ou insuffisants pour calculer l\'âge précis</span>'
	end
end

---
-- calcul du jour julien à partir d'une date du calendrier grégorien
function fun.julianDay( year, month, day, hour, min, sec )
	local julian
	julian = math.floor( math.floor( ( year * 12 + month + 57609 ) / 12 - 1 ) * 1461 / 4 )
			- math.floor( math.floor( ( year * 12 + month + 57609 ) / 12 - 1 ) / 100 )
			+ math.floor( math.floor( ( year * 12 + month + 57609 ) / 12 - 1 ) / 400 )
			+ math.floor( ( math.fmod( month + 57609, 12 ) + 4 ) * 153 / 5 )
			+ day + ( hour or 12 ) / 24 + ( min or 0 ) / 1440 + ( sec or 0 ) / 86400
			- 32167.5
	return julian
end

---
-- calcul du jour julien à partir d'une date du calendrier julien
function fun.julianDayJulian( year, month, day, hour, min, sec )
	local julian
	julian = math.floor( math.floor( ( year * 12 + month + 57609 ) / 12 - 1 ) * 1461 / 4 )
			+ math.floor( ( math.fmod( month + 57609, 12 ) + 4 ) * 153 / 5 )
			+ day + ( hour or 12 ) / 24 + ( min or 0 ) / 1440 + ( sec or 0 ) / 86400
			- 32205.5
	return julian
end

---
-- calcul d'une date dans le calendrier grégorien à partir du jour julien
function fun.julianDayToGregorian( julianDay )
	local base = math.floor( julianDay + 32044.5 )  -- 1 March -4800 (proleptic Gregorian date)
	local nCentury = math.floor( ( base * 4 + 3 ) / 146097 )
	local sinceCentury = base - math.floor( nCentury * 146097 / 4 )
	local nYear = math.floor( ( sinceCentury * 4 + 3 ) / 1461 )
	local sinceYear = sinceCentury - math.floor( nYear * 1461 / 4 )
	local nMonth = math.floor( ( sinceYear * 5 + 2 ) / 153 )

	local day = sinceYear - math.floor( ( nMonth * 153 + 2 ) / 5 ) + 1
	local month = nMonth - math.floor( nMonth / 10 ) * 12 + 3
	local year = math.floor( sinceYear / 306 ) + nYear + 100 * nCentury - 4800

	return year, month, day
end

---
-- calcul d'une date dans le calendrier julien à partir du jour julien
-- calcul basé sur l'algorithme de la page fr.wikipedia.org/wiki/Jour_julien (1/10/2013)
function fun.julianDayToJulian( julianDay )
	local year = math.modf( ( julianDay * 4 - 6884469 ) / 1461 )
	local r2 = julianDay - math.modf( ( 1461 * year + 6884472 ) / 4 )
	local month = math.modf( ( 5 * r2 + 461 ) / 153 )
	local day = r2 - math.modf( ( 153 * month - 457 ) / 5 ) + 1
	if month > 12 then
		year = year + 1
		month = month - 12
	end
	return year, month, day
end

---
-- calcul d'une date dans le calendrier grégorien à partir d'une date dans le calendrier julien
function fun.julianToGregorian( year, month, day )
	return fun.julianDayToGregorian( fun.julianDayJulian( year, month, day ) )
end

---
-- calcul d'une date dans le calendrier julien à partir d'une date dans le calendrier grégorien
function fun.gregorianToJulian( year, month, day )
	year = tonumber(year)
	if month then month = tonumber(month) else month = 6 end --prend une valeur centrale pour donner un best "guess"
	if day then day = tonumber(day) else day = 15 end
	return fun.julianDayToJulian( fun.julianDay( year, month, day ) )
end


--[[
  Cette fonction retourne "CET" ou "CEST" selon que dans la pseudo-timezone en cours
    c'est l'heure d'été ou l'heure d'hiver.
  Cette fonction n'a de sens a priori que pour des modèles utilisés en Europe

  Paramètre optionnel non nommé : "sans lien" : retourne le texte CET/CEST. sinon
    retourne ce même texte avec un wikilien vers les articles correspondants
--]]
function fun.CEST(frame)
	-- option : ne pas créer de wikilien
	local opt = trim(frame.args[1] or frame:getParent().args[1])
	-- on récupère l'information dans la zone courante
	local t = mw.getContentLanguage():formatDate("I", nil, true)

	if (t == "1") then  -- heure d'été
		if (opt == "sans lien") then
			return "CEST"
		elseif (opt == "décalage") then
			return "2"
		else
			return "[[Heure d'été d'Europe centrale|CEST]]"
		end
	else  -- heure d'hiver (ou autre zone où ça ne s'applique pas)
		if (opt == "sans lien") then
			return "CET"
		elseif (opt == "décalage") then
			return "1"
		else
			return "[[Heure normale d'Europe centrale|CET]]"
		end
	end
end

return fun