Difference between revisions of "Module:Date"

From CryptoWiki

wiki_crypto>LD
(DIPP par eru, cf. demande.)
m (1 revision imported)
 
(One intermediate revision by the same user not shown)
Line 1: Line 1:
local fun = {}
-- Date functions for use by other modules.
-- I18N and time zones are not supported.


local Outils = require 'Module:Outils'
local MINUS = '' -- Unicode U+2212 MINUS SIGN
-- chargement de la base de données répertoriant certaines pages existant ou n'existant pas pour éviter les "ifexist".
local floor = math.floor
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)
local Date, DateDiff, diffmt  -- forward declarations
-- retourne nil si le texte est vide ou n'est pas du texte. Attention c'est important pour les fonctions qui l'utilisent.
local uniq = { 'unique identifier' }
local trim = Outils.trim


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


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


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


-- liste des mois, écriture exacte et alias, en minuscule
local function collection()
local listeMois = {
-- Return a table to hold items.
{ num = 1,  nJour = 31, abrev = 'janv.',  nom = 'janvier', alias = { 'jan.', 'janv.', 'jan', 'janv', 'january' } },
return {
{ 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.' } },
n = 0,
{ num = 3,  nJour = 31, abrev = 'mars',  nom = 'mars', alias = { 'mar.', 'mar', 'march' } },
add = function (self, item)
{ num = 4,  nJour = 30, abrev = 'avr.',  nom = 'avril', alias = { 'avr.', 'avr', 'apr', 'april'} },
self.n = self.n + 1
{ num = 5,  nJour = 31, abrev = 'mai',    nom = 'mai', alias = { 'may' } },
self[self.n] = item
{ num = 6,  nJour = 30, abrev = 'juin',  nom = 'juin', alias = { 'jun', 'june' } },
end,
{ num = 7,  nJour = 31, abrev = 'juill.', nom = 'juillet', alias = { 'juil.', 'juil', 'juill.', 'juill', 'jul', 'july' } },
join = _list_join,
{ 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' } },
end
{ 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
local function strip_to_nil(text)
for i = 1, 12 do
-- If text is a string, return its trimmed content, or nil if empty.
local mois = listeMois[i]
-- Otherwise return text (convenient when Date fields are provided from
listeMois[tostring( i )] = mois
-- another module which may pass a string, a number, or another type).
if i < 10 then
if type(text) == 'string' then
listeMois['0' .. i] = mois
text = text:match('(%S.-)%s*$')
end
end
listeMois[mois.nom] = mois
return text
listeMois[mois.abrev] = mois
end
for j = 1, #mois.alias do
 
listeMois[mois.alias[j]] = mois
local function is_leap_year(year, calname)
-- Return true if year is a leap year.
if calname == 'Julian' then
return year % 4 == 0
end
end
return (year % 4 == 0 and year % 100 ~= 0) or year % 400 == 0
end
end
for i = 1, #listeMois.aout.alias do
 
listeMois[listeMois.aout.alias[i]] = listeMois.aout
local function days_in_month(year, month, calname)
-- Return number of days (1..31) in given month (1..12).
if month == 2 and is_leap_year(year, calname) then
return 29
end
return ({ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 })[month]
end
end


local liste_saisons = {
local function h_m_s(time)
{ 'printemps', 'spring', },
-- Return hour, minute, second extracted from fraction of a day.
{ 'été', 'summer', },
time = floor(time * 24 * 3600 + 0.5)  -- number of seconds
{ 'automne', 'autumn', },
local second = time % 60
{ 'hiver', 'winter', },
time = floor(time / 60)
}
return floor(time / 60), time % 60, second
end


-- à partir d'un nom de saison (en français ou en anglais),
local function hms(date)
-- retourne son nom canonique (exemple : "été")
-- Return fraction of a day from date's time, where (0 <= fraction < 1)
-- si non reconnu, retourne nil
-- if the values are valid, but could be anything if outside range.
function fun.determinationSaison( saison )
return (date.hour + (date.minute + date.second / 60) / 60) / 24
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
end


---
local function julian_date(date)
-- à partir d'un nom de mois (en français ou en anglais), de son numéro ou d'une abréviation,
-- Return jd, jdz from a Julian or Gregorian calendar date where
-- retourne son nom canonique (exemple : "juin") et son numéro (exemple : 6)
--  jd = Julian date and its fractional part is zero at noon
-- si non reconnu, retourne nil, nil
--   jdz = same, but assume time is 00:00:00 if no time given
function fun.determinationMois( mois )
-- http://www.tondering.dk/claus/cal/julperiod.php#formula
local result
-- 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
local num = tonumber( mois )
--    1 January 4713 BC  = (-4712, 1, 1)   Julian calendar
if num then
--   24 November 4714 BC = (-4713, 11, 24) Gregorian calendar
result = listeMois[num]
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
local str = trim( mois )
offset = floor(y/4) - floor(y/100) + floor(y/400) - 32045
if str then
result = listeMois[str]
if not result then
result = listeMois[mw.ustring.lower( str )]
end
end
end
end
 
local m = date.month + 12*a - 3
if result then
local jd = date.day + floor((153*m + 2)/5) + 365*y + offset
return result.nom, result.num
if date.hastime then
else
jd = jd + hms(date) - 0.5
return nil, nil
return jd, jd
end
end
return jd, jd - 0.5
end
end


 
local function set_date_from_jd(date)
-- fonction interne à modeleDate, pour déterminer si on peut se passer de faire un ifexist
-- Set the fields of table date from its Julian date field.
local function existDate( dataQualificatif, annee, mois )
-- Return true if date is valid.
local data
-- http://www.tondering.dk/claus/cal/julperiod.php#formula
if mois then
-- This handles the proleptic Julian and Gregorian calendars.
data = dataQualificatif.mois
-- Negative Julian dates are not defined but they work.
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
return
end
end
if type( data ) ~= 'table' then
local jd = date.jd
-- si data n'existe pas c'est que l'on considère qu'il n'y a pas de lien.
if not (type(jd) == 'number' and low <= jd and jd <= high) then
return
return
end
end
-- le qualificatif est remplacé par celui de la base de données, ce qui permet des alias.
local jdn = floor(jd)
local lien = annee
if date.hastime then
if dataQualificatif.qualificatif ~= '' then
local time = jd - jdn  -- 0 <= time < 1
lien = lien .. ' ' .. dataQualificatif.qualificatif
if time >= 0.5 then    -- if at or after midnight of next day
jdn = jdn + 1
time = time - 0.5
else
time = time + 0.5
end
date.hour, date.minute, date.second = h_m_s(time)
else
date.second = 0
date.minute = 0
date.hour = 0
end
local b, c
if calname == 'Julian' then
b = 0
c = jdn + 32082
else  -- Gregorian
local a = jdn + 32044
b = floor((4*a + 3)/146097)
c = a - floor(146097*b/4)
end
end
local seul = annee
local d = floor((4*c + 3)/1461)
if mois then
local e = c - floor(1461*d/4)
lien = mois .. ' ' .. lien
local m = floor((5*e + 2)/153)
seul = ucfirst( mois ) .. ' ' .. annee
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
 
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.
-- The result will have valid m, d values if y is valid; caller checks y.
-- The logic of PHP mktime is followed where m or d can be zero to mean
-- the previous unit, and -1 is the one before that, etc.
-- Positive values carry forward.
local date
if not (1 <= m and m <= 12) then
date = Date(y, 1, 1)
if not date then return end
date = date + ((m - 1) .. 'm')
y, m = date.year, date.month
end
end
local aucun = tonumber( data.aucun )
local days_hms
if aucun and annee <= aucun then
if not partial then
-- si l'année est dans la partie 'aucun' on teste s'il y a malgré tout un lien isolé
if hastime and H and M and S then
if type( data.seul ) == 'table' then
if not (0 <= H and H <= 23 and
for i, v in ipairs( data.seul ) do
0 <= M and M <= 59 and
if seul == v or seul == tonumber( v ) then
0 <= S and S <= 59) then
return lien
days_hms = hms({ hour = H, minute = M, second = S })
end
end
end
end
end
-- partie aucun et pas de lien => nil
if days_hms or not (1 <= d and d <= days_in_month(y, m, calendar)) then
return nil
date = date or Date(y, m, 1)
elseif type( data.tous ) == 'table' then
if not date then return end
local tous1, tous2 = tonumber( data.tous[1] ), tonumber( data.tous[2] )
date = date + (d - 1 + (days_hms or 0))
if tous1 and tous2 and annee >= tous1 and annee <= tous2 then
y, m, d = date.year, date.month, date.day
-- l'année est dans la partie 'tous' donc on retourne le lien
if days_hms then
return lien
H, M, S = date.hour, date.minute, date.second
end
end
end
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.
numbers.year = y
local cibleLien = mw.title.new( lien )
numbers.month = m
if cibleLien and cibleLien.exists then
numbers.day = d
return lien
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
end


---
local function set_date_from_numbers(date, numbers, options)
-- Supprime le jour de la semaine, et "le" avant une date
-- Set the fields of table date from numeric values.
function fun.nettoyageJour( jour )
-- Return true if date is valid.
if type( jour ) == 'string' then
if type(numbers) ~= 'table' then
local nomJour = { '[Ll]undi', '[Mm]ardi', '[Mm]ercredi', '[Jj]eudi', '[Vv]endredi',
return
'[Ss]amedi', '[Dd]imanche', '^ *[Ll]e' }
end
local premier = { '<abbr class="abbr ?" title="[Pp]remier" ?>1<sup>er</sup></abbr>', '1<sup>er</sup>', '1er' }
local y = numbers.year  or date.year
for i = 1, #nomJour do
local m = numbers.month  or date.month
jour = jour:gsub( nomJour[i], '' )
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
for i = 1, #premier do
elseif y and date.partial then
jour = jour:gsub( premier[i], '1' )
if d or not (-9999 <= y and y <= 9999) then
return
end
end
jour = trim( jour )
if m and not (1 <= m and m <= 12) then
if not date.want_fix then
return
end
need_fix = true
end
else
return
end
end
return jour
if date.partial then
end
H = nil  -- ignore any time
 
M = nil
---
S = nil
-- Sépare une chaine date en une table contenant les champs jour, mois et annee.
else
-- la date doit contenir le mois.
if H then
function fun.separationJourMoisAnnee( date )
-- It is not possible to set M or S without also setting H.
date = trim( date )
date.hastime = true
if date then
else
local function erreur( periode, valeur )
H = 0
return false, '<span class="error">' .. periode .. ' invalide (' .. valeur .. ')</span>'
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
return
end
end
end
 
end
local dateAvantCleanup = date
date.want_fix = nil
local jour, mois, annee, masquerMois, masquerAnnee, separateur
if need_fix then
 
fix_numbers(numbers, y, m, d, H, M, S, date.partial, date.hastime, date.calendar)
-- variable pour construire les regex
return set_date_from_numbers(date, numbers, options)
local j = '([0-3]?%d)'                            -- jour
end
local m = '([01]?%d)'                             -- mois numérique
date.year = y    -- -9999 to 9999 ('n BC' → year = 1 - n)
local mmm = '([^%s%p%d]+[.]?)'                    -- mois en toute lettre
date.month = m  -- 1 to 12 (may be nil if partial)
local mmm2 = '([^%s%p%d]+[.]?[-/][^%s%p%d]+[.]?)' -- mois-mois en toute lettre
date.day = d     -- 1 to 31 (* = nil if partial)
local aj = '(%-?%d+)'                            -- année ou jour
date.hour = H    -- 0 to 59 (*)
local s = '[ ./-]+'                              -- séparateur simple
date.minute = -- 0 to 59 (*)
local sep = '([ ./-]+)'                          -- séparateur avec capture, pour le détecter deux fois
date.second = S  -- 0 to 59 (*)
local moins = '(%-?)'                            -- signe moins pour signifier qu'il ne faut pas afficher cette donnée
if type(options) == 'table' then
 
for _, k in ipairs({ 'am', 'era', 'format' }) do
date = fun.nettoyageJour( date )
if options[k] then
if date == nil then
date.options[k] = options[k]
return erreur( 'Date', dateAvantCleanup )
end
end
end
-- suppression catégorie, liens, balises
end
date = mw.ustring.gsub( date, '%[%[[Cc]at[ée]gor[yi]e?:.-%]%]', '' )
return true
date = date :gsub( '%b<>', '' )
end
: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
local function make_option_table(options1, options2)
if date:match( '^'..aj..'$' ) then
-- If options1 is a string, return a table with its settings, or
annee = date:match( '^'..aj..'$' )
-- if it is a table, use its settings.
elseif date:match( '^'..aj..s..aj..moins..'$' ) then
-- Missing options are set from table options2 or defaults.
-- jj/mm, mm/aaaa ou aaaa/mm
-- If a default is used, a flag is set so caller knows the value was not intentionally set.
local a, separateur, b, sb = date:match( '^'..aj..sep..aj..moins..'$' )
-- Valid option settings are:
a, b = tonumber( a ), tonumber( b )
-- am: 'am', 'a.m.', 'AM', 'A.M.'
if separateur:match( '^.+%-$' ) then
--    'pm', 'p.m.', 'PM', 'P.M.' (each has same meaning as corresponding item above)
-- probablement mm/-aaaa, année av.JC
-- era: 'BCMINUS', 'BCNEGATIVE', 'BC', 'B.C.', 'BCE', 'B.C.E.', 'AD', 'A.D.', 'CE', 'C.E.'
b = 0 - b
-- Option am = 'am' does not mean the hour is AM; it means 'am' or 'pm' is used, depending on the hour,
--   and am = 'pm' has the same meaning.
-- Similarly, era = 'BC' means 'BC' is used if year <= 0.
-- BCMINUS displays a MINUS if year < 0 and the display format does not include %{era}.
-- BCNEGATIVE is similar but displays a hyphen.
local result = { bydefault = {} }
if type(options1) == 'table' then
result.am = options1.am
result.era = options1.era
elseif type(options1) == 'string' then
-- Example: 'am:AM era:BC' or 'am=AM era=BC'.
for item in options1:gmatch('%S+') do
local lhs, rhs = item:match('^(%w+)[:=](.+)$')
if lhs then
result[lhs] = rhs
end
end
if  a > 12 and ( b < 1 or b > 31 ) or
end
b > 12 and ( a < 1 or a > 31 ) then
end
return erreur( 'Date', dateAvantCleanup )
options2 = type(options2) == 'table' and options2 or {}
elseif b < 1 or b > 31 then
local defaults = { am = 'am', era = 'BC' }
mois, annee, masquerAnnee = a, b, sb
for k, v in pairs(defaults) do
elseif a < 1 or a > 31 then
if not result[k] then
annee, mois = a, b
if options2[k] then
elseif b > 12 then
result[k] = options2[k]
return erreur( 'Mois', b )
else
else
jour, mois, masquerMois = a, b, sb
result[k] = v
result.bydefault[k] = true
end
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
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
return result
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 = {
-- validationJourMoisAnnee vérifie que les paramètres correspondent à une date valide.
-- Text for displaying an era with a positive year (after adjusting
-- la date peut être dans les paramètres 1 à 3, ou dans des paramètres jour, mois et annee.
-- by replacing year with 1 - year if date.year <= 0).
-- La fonction retourne true suivi d'une table avec la date en paramètres nommés (sans accent sur année)
-- options.era = { year<=0 , year>0 }
-- ou false suivi d'un message d'erreur.
['BCMINUS']    = { 'BC'    , ''    , isbc = true, sign = MINUS },
function fun.validationJourMoisAnnee( frame )
['BCNEGATIVE'] = { 'BC'    , ''    , isbc = true, sign = '-'   },
local args = Outils.extractArgs( frame )
['BC']        = { 'BC'    , ''   , isbc = true },
local jour, mois, numMois, annee
['B.C.']      = { 'B.C.'  , ''    , isbc = true },
local bjour = args[1] or args['jour'] or ''
['BCE']        = { 'BCE'  , ''    , isbc = true },
local bmois = tostring( args[2] or args['mois'] or '' )
['B.C.E.']     = { 'B.C.E.', ''    , isbc = true },
local bannee = args[3] or args['annee'] or args['année'] or ''
['AD']         = { 'BC'    , 'AD'   },
['A.D.']       = { 'B.C.'  , 'A.D.' },
['CE']         = { 'BCE'   , 'CE'  },
['C.E.']       = { 'B.C.E.', 'C.E.' },
}


local function erreur( periode, valeur )
local function get_era_for_year(era, year)
return false, '<span class="error">' .. periode .. ' invalide (' .. valeur .. ')</span>'
return (era_text[era] or era_text['BC'])[year > 0 and 2 or 1] or ''
end
end


-- on traite l'année
local function strftime(date, format, options)
if Outils.notEmpty( bannee ) then
-- Return date formatted as a string using codes similar to those
annee = tonumber( bannee )
-- in the C strftime library function.
if annee == nil and type( bannee ) == 'string' then
local sformat = string.format
-- test si l'année contient av. J.-C.
local shortcuts = {
annee = bannee:upper():match( '^(%d+) ?[Aa][Vv]%.? ?[Jj][ .-]*[Cc]%.?' )
['%c'] = '%-I:%M %p %-d %B %-Y %{era}',  -- date and time: 2:30 pm 1 April 2016
annee = tonumber( annee )
['%x'] = '%-d %B %-Y %{era}',            -- date:          1 April 2016
if annee then
['%X'] = '%-I:%M %p',                   -- time:          2:30 pm
annee = 0 - annee
}
else
if shortcuts[format] then
return erreur( 'Année', bannee )
format = shortcuts[format]
end
elseif annee == 0 then
return erreur( 'Année', 0 )
end
else
annee = nil
end
end
 
local codes = {
-- on traite le mois
a = { field = 'dayabbr' },
if Outils.notEmpty( bmois ) then
A = { field = 'dayname' },
mois, numMois = fun.determinationMois( bmois )
b = { field = 'monthabbr' },
if mois == nil then
B = { field = 'monthname' },
mois = fun.determinationSaison( bmois )
u = { fmt = '%d'  , field = 'dowiso' },
if mois == nil then
w = { fmt = '%d'  , field = 'dow' },
local mois1, sep, mois2 = bmois:match( '^([^%s%p%d]+[.]?)([-/])([^%s%p%d]+[.]?)$' )
d = { fmt = '%02d', fmt2 = '%d', field = 'day' },
if mois1 then
m = { fmt = '%02d', fmt2 = '%d', field = 'month' },
mois1 = fun.determinationMois( mois1 )
Y = { fmt = '%04d', fmt2 = '%d', field = 'year' },
mois2 = fun.determinationMois( mois2 )
H = { fmt = '%02d', fmt2 = '%d', field = 'hour' },
if mois1 == nil or mois2 == nil then
M = { fmt = '%02d', fmt2 = '%d', field = 'minute' },
return erreur( 'Mois', bmois )
S = { fmt = '%02d', fmt2 = '%d', field = 'second' },
end
j = { fmt = '%03d', fmt2 = '%d', field = 'dayofyear' },
mois = mois1 .. sep .. mois2
I = { fmt = '%02d', fmt2 = '%d', field = 'hour', special = 'hour12' },
else
p = { field = 'hour', special = 'am' },
return erreur( 'Mois', bmois )
}
end
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
end
local value = date[code.field]
-- on traite le jour si présent
if not value then
if Outils.notEmpty( bjour ) then
return nil  -- an undefined field in a partial date
if not numMois then
erreur( 'Date', 'jour avec saison ou plusieurs mois' )
end
end
jour = tonumber( bjour )
local special = code.special
if jour == nil then
if special then
jour = tonumber( fun.nettoyageJour( bjour ) )
if special == 'hour12' then
value = value % 12
value = value == 0 and 12 or value
elseif special == 'am' then
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
end
if jour == nil then
if code.field == 'year' then
return erreur( 'Jour', bjour )
local sign = (era_text[eraopt] or {}).sign
end
if not sign or format:find('%{era}', 1, true) then
-- on valide que le jour est correct
sign = ''
if jour < 1 or jour > 31 then
if value <= 0 then
return erreur( 'Jour', bjour )
value = 1 - value
elseif jour > listeMois[numMois].nJour then
end
return erreur( 'Jour', bjour .. ' ' .. mois )
else
elseif jour == 29 and numMois == 2 and annee and ( math.fmod( annee, 4 ) ~= 0 ) then
if value >= 0 then
-- l'année bisextile sur les siècles est toujours acceptée pour être compatible avec les dates juliennes.
sign = ''
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
else
return erreur( 'Date', 'jour seul : ' .. bjour )
value = -value
end
end
else
return erreur( 'Jour', bjour )
end
end
return spaces .. sign .. sformat(fmt, value)
end
end
return spaces .. (fmt and sformat(fmt, value) or value)
end
end
end
end
 
local function replace_property(spaces, id)
-- vérification de l'absence d'un décalage
if id == 'era' then
if annee and annee < 13 and annee > 0 and not jour and ( tonumber( bmois ) or ( not mois and tonumber( args[4] ) ) ) then
-- Special case so can use local era option.
return false, '<span class="error">année improbable (' .. annee .. ')</span>'
local result = get_era_for_year(eraopt, date.year)
if result == '' then
return ''
end
return (spaces == '' and '' or '&nbsp;') .. result
end
local result = date[id]
if type(result) == 'string' then
return spaces .. result
end
if type(result) == 'number' then
return  spaces .. tostring(result)
end
if type(result) == 'boolean' then
return  spaces .. (result and '1' or '0')
end
-- This occurs if id is an undefined field in a partial date, or is the name of a function.
return nil
end
end
 
local PERCENT = '\127PERCENT\127'
local resultat = {
return (format
jour = jour,
:gsub('%%%%', PERCENT)
mois = mois,
:gsub('(%s*)%%{(%w+)}', replace_property)
numMois = numMois,
:gsub('(%s*)%%(%-?)(%a)', replace_code)
annee = annee,
:gsub(PERCENT, '%%')
masquerAnnee = args.masquerAnnee,
)
masquerMois = args.masquerMois,
}
return true, resultat
end
end


 
local function _date_text(date, fmt, options)
---
-- Return a formatted string representing the given date.
-- émule le modèle {{m|Date}}.
if not is_date(date) then
-- Paramètres :
error('date:text: need a date (use "date:text()" with a colon)', 2)
-- 1 : jour (numéro ou "1er") ou la date complète
end
-- 2 : mois (en toutes lettres) ou spécialité de l'année
if type(fmt) == 'string' and fmt:match('%S') then
-- 3 : année (nombre)
if fmt:find('%', 1, true) then
-- 4 : spécialité de l'année
return strftime(date, fmt, options)
-- 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
end
elseif date.partial then
fmt = date.month and 'my' or 'y'
else
else
local function masquerParam( p )
fmt = 'dmy'
-- sépare le signe moins final éventuel signifiant que le paramètre ne doit pas être affiché.
if date.hastime then
if type( p ) ~= 'string' then
fmt = (date.second > 0 and 'hms ' or 'hm ') .. fmt
return p, nil
end
local value, mask = p:match( '^%s*(.-)(%-?)%s*$' )
return value, ( mask == '-' or nil )
end
end
local cleanArgs = { arg1 or args.jour }
end
cleanArgs[2], cleanArgs.masquerMois = masquerParam( args[2] or args.mois )
local function bad_format()
cleanArgs[3], cleanArgs.masquerAnnee = masquerParam( args[3] or args.annee or args['année'] )
-- For consistency with other format processing, return given format
 
-- (or cleaned format if original was not a string) if invalid.
test, params = fun.validationJourMoisAnnee( cleanArgs )
return mw.text.nowiki(fmt)
if test then
end
params.qualificatif = trim( args[4] )
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
return bad_format()
end
end
return strftime(date, fmt, options)
end
end
 
local function hm_fmt()
-- analyse des paramètres nommés
local plain = make_option_table(options, date.options).bydefault.am
if test then
return plain and '%H:%M' or '%-I:%M %p'
params.agePrefix = args.agePrefix
end
if args.qualificatif and args.qualificatif ~= '' then
local need_time = date.hastime
params.qualificatif = args.qualificatif
local t = collection()
end
for item in fmt:gmatch('%S+') do
 
local f
-- julien peut avoir trois valeurs : inactif, format standard (true), format court
if item == 'hm' then
params.julien = Yesno( args.julien, 'court', false )
f = hm_fmt()
params.avJC = Yesno( args.avJC )
need_time = false
 
elseif item == 'hms' then
if args['républicain'] and args['républicain'] ~= '' then
f = '%H:%M:%S'
if args['républicain'] == 'liens' then
need_time = false
params.republicain = 'liens'
elseif item == 'ymd' then
else
f = '%Y-%m-%d %{era}'
params.republicain = Yesno( args['républicain'], false )
elseif item == 'mdy' then
end
f = '%B %-d, %-Y %{era}'
elseif item == 'dmy' then
f = '%-d %B %-Y %{era}'
else
else
params.republicain = false
return bad_format()
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
end
t:add(f)
end
fmt = t:join(' ')
if need_time then
fmt = hm_fmt() .. ' ' .. fmt
end
return strftime(date, fmt, options)
end


local listeParam = {
local day_info = {
age = 'âge',
-- 0=Sun to 6=Sat
['âge'] = 'âge',
[0] = { 'Sun', 'Sunday' },
naissance = 'naissance',
{ 'Mon', 'Monday' },
mort = 'mort',
{ 'Tue', 'Tuesday' },
['événement'] = 'événement',
{ 'Wed', 'Wednesday' },
evenement = 'evenement',
{ 'Thu', 'Thursday' },
['décès'] = 'mort',
{ 'Fri', 'Friday' },
apJC = 'apJC',
{ 'Sat', 'Saturday' },
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
local month_info = {
if args.debug then
-- 1=Jan to 12=Dec
return params
{ 'Jan', 'January' },
end
{ 'Feb', 'February' },
{ 'Mar', 'March' },
{ 'Apr', 'April' },
{ 'May', 'May' },
{ 'Jun', 'June' },
{ 'Jul', 'July' },
{ 'Aug', 'August' },
{ 'Sep', 'September' },
{ 'Oct', 'October' },
{ 'Nov', 'November' },
{ 'Dec', 'December' },
}


resultat = fun._modeleDate( params )
local function name_to_number(text, translate)
if type(text) == 'string' then
return translate[text:lower()]
end
end


else
local function day_number(text)
local yn_afficherErreurs = Yesno( args.afficherErreurs )
return name_to_number(text, {
if yn_afficherErreurs == nil or yn_afficherErreurs == true then
sun = 0, sunday = 0,
resultat = params
mon = 1, monday = 1,
else
tue = 2, tuesday = 2,
resultat = args[1]
wed = 3, wednesday = 3,
end
thu = 4, thursday = 4,
fri = 5, friday = 5,
sat = 6, saturday = 6,
})
end


local currentTitle = mw.title.getCurrentTitle()
local function month_number(text)
return name_to_number(text, {
jan = 1, january = 1,
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


if currentTitle:inNamespaces( 0, 4, 10, 14, 100 )
local function _list_text(list, fmt)
and not Outils.notEmpty( args.nocat )
-- Return a list of formatted strings from a list of dates.
and not currentTitle.prefixedText:match( '^Modèle:.+/Test$' ) then
if not type(list) == 'table' then
local categorie
error('date:list:text: need "list:text()" with a colon', 2)
local yn_categoriserErreurs = Yesno( args.categoriserErreurs, 'custom', true )
end
if yn_categoriserErreurs == nil or yn_categoriserErreurs == true then
local result = { join = _list_join }
categorie = '[[Catégorie:Page utilisant le modèle date avec une syntaxe erronée]]'
for i, date in ipairs(list) do
elseif yn_categoriserErreurs == false then
result[i] = date:text(fmt)
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
end
 
return result
return resultat or ''
end
end


function fun._modeleDate( args )
local function _date_list(date, spec)
local annee, mois, numMois, jour = args.annee, args.mois, args.numMois, args.jour
-- Return a possibly empty numbered table of dates meeting the specification.
local qualificatif = args.qualificatif
-- Dates in the list are in ascending order (oldest date first).
 
-- The spec should be a string of form "<count> <day> <op>"
if ( annee or mois or jour ) == nil then
-- where each item is optional and
return
--  count = number of items wanted in list
--  day = abbreviation or name such as Mon or Monday
--  op = >, >=, <, <= (default is > meaning after date)
-- If no count is given, the list is for the specified days in date's month.
-- The default day is date's day.
-- The spec can also be a positive or negative number:
--  -5 is equivalent to '5 <'
--  5  is equivalent to '5' which is '5 >'
if not is_date(date) then
error('date:list: need a date (use "date:list()" with a colon)', 2)
end
end
 
local list = { text = _list_text }
-- on traite l'âge, naissance et mort
if date.partial then
local agePrefix = args.agePrefix
return list
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
agePrefix = agePrefix or ''
local count, offset, operation
 
local ops = {
-- on traite le calendrier
['>='] = { before = false, include = true  },
local gannee, gmois, gjour = annee, numMois, jour  -- date suivant le calendrier grégorien pour <time>
['>']  = { before = false, include = false },
local jannee, jmois, jjour = annee, mois, jour      -- date suivant le calendrier julien si necessaire
['<='] = { before = true , include = true  },
local julienDate, julienSup, julienSep              -- servira éventuellement à afficher la date selon le calendrier julien
['<']  = { before = true , include = false },
local gregAprMois, gregAprAn, gregFin              -- message de calendrier grégorien lorsque la date est selon le calendrier julien
}
if annee and jour then
if spec then
local amj = annee * 10000 + numMois * 100 + jour
if type(spec) == 'number' then
if amj < 15821014 then
count = floor(spec + 0.5)
if annee > 0 then
if count < 0 then
gannee, gmois, gjour = fun.julianToGregorian( annee, numMois, jour )
count = -count
else
operation = ops['<']
-- calendrier grégorien proleptique avec année 0.
gannee, gmois, gjour = fun.julianToGregorian( annee + 1, numMois, jour )
end
end
args.julien = false
elseif type(spec) == 'string' then
 
local num, day, op = spec:match('^%s*(%d*)%s*(%a*)%s*([<>=]*)%s*$')
elseif args.julien then
if not num then
gannee, gmois, gjour = fun.julianToGregorian( annee, numMois, jour )
return list
annee, mois, jour = gannee, listeMois[gmois].nom, gjour
if jjour == 1 then
jjour = modelePremier
end
end
if args.compact then
if num ~= '' then
jmois = listeMois[jmois].abrev
count = tonumber(num)
end
end
if args.julien == 'court' then
if day ~= '' then
julienDate = jjour .. ' ' .. jmois .. ' '
local dow = day_number(day:gsub('[sS]$', ''))  -- accept plural days
julienSup = '<sup>[[calendrier julien|jul.]]</sup>'
if not dow then
if jannee == annee then
return list
gregAprMois = '<sup>[[calendrier grégorien|grég.]]</sup>'
else
julienDate = julienDate .. jannee .. ' '
gregAprAn = '<sup>[[calendrier grégorien|grég.]]</sup>'
end
end
julienSep = ' / '
offset = dow - date.dow
else
end
julienDate = jjour .. ' ' .. jmois .. ' ' .. jannee
operation = ops[op]
julienSep = ' ('
else
gregFin = ' [[Passage du calendrier julien au calendrier grégorien|dans le calendrier grégorien]])'
return list
end
end
offset = offset or 0
operation = operation or ops['>']
local datefrom, dayfirst, daylast
if operation.before then
if offset > 0 or (offset == 0 and not operation.include) then
offset = offset - 7
end
if count then
if count > 1 then
offset = offset - 7*(count - 1)
end
end
 
datefrom = date + offset
elseif args.republicain then
else
local DateRep = require 'Module:Date républicaine'
daylast = date.day + offset
local RepSansLiens
dayfirst = daylast % 7
if args.republicain == 'liens' then
if dayfirst == 0 then
RepSansLiens = false
dayfirst = 7
else
RepSansLiens = true
end
end
dateRepublicaine = DateRep._date_republicaine(
RepSansLiens,
{ fun.formatRepCal( fun.do_toRepCal{gannee, gmois, gjour} ) }
)
end
end
else
else
if annee and annee < 0 then
if offset < 0 or (offset == 0 and not operation.include) then
gannee = gannee + 1
offset = offset + 7
end
end
args.julien = false
if count then
args.republicain = false
datefrom = date + offset
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
else
if args.nolinks then
dayfirst = date.day + offset
texteMois = '<abbr class=abbr title="' .. mois .. '">' .. listeMois[mois].abrev .. '</abbr>'
daylast = date.monthdays
else
texteMois = listeMois[mois].abrev
end
end
end
end
end
mois = mois and mois:gsub( 'aout', 'août' )
if not count then
 
if daylast < dayfirst then
local dataQualificatif, dataCat
return list
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
count = floor((daylast - dayfirst)/7) + 1
datefrom = Date(date, {day = dayfirst})
end
end
local function wikiLien( lien, texte )
for i = 1, count do
if lien == texte then
if not datefrom then break end  -- exceeds date limits
return '[[' .. texte .. ']]'
list[i] = datefrom
else
datefrom = datefrom + 7
return '[[' .. lien .. '|' .. texte .. ']]'
end
end
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 })


-- le jour si présent
local function extract_date(newdate, text)
local qualifJour = ''
-- Parse the date/time in text and return n, o where
if jour then
--  n = table of numbers with date/time fields
if args.nolinks then
--  o = table of options for AM/PM or AD/BC or format, if any
if jour == 1 then
-- or return nothing if date is known to be invalid.
jour = modelePremier
-- Caller determines if the values in n are valid.
-- 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
if y <= 0 then
options.era = 'BCE'
end
end
table.insert( wikiListe, jour )
date.year = y
else
m = tonumber(m)
qualifJour = dataQualificatif.jour and dataQualificatif.qualificatif
d = tonumber(d)
or dataCat.jour and dataCat.qualificatif
H = tonumber(H)
or ''
M = tonumber(M)
local texteJour, lien
S = tonumber(S)
if jour == 1 then
if m == 0 then
texteJour = '1<sup>er</sup>'
newdate.partial = true
lien = '1er ' .. mois
return date, options
else
end
texteJour = jour
date.month = m
lien = jour .. ' ' .. mois
if d == 0 then
newdate.partial = true
return date, options
end
end
if qualifJour ~= '' then
date.day = d
lien = lien .. ' ' .. qualifJour
if H > 0 or M > 0 or S > 0 then
date.hour = H
date.minute = M
date.second = S
end
end
-- s'il n'y a pas de lien sur le mois, il sera affiché avec le jour.
return date, options
table.insert( wikiListe, wikiLien( lien, texteJour ) )
table.insert( wikiListe, wikiLien( lien, texteJour .. ' '.. texteMois ) )
end
end
table.insert( iso, 1, string.sub( '0' .. gjour, -2 ) )
return
end
end
 
local function extract_ymd(item)
-- le mois
-- Called when no day or month has been set.
if mois then
local y, m, d = item:match('^(%d%d%d%d)%-(%w+)%-(%d%d?)$')
if #wikiListe == 0 and annee == nil then
if y then
return texteMois
if date.year then
end
return
if args.nolinks then
end
if not args.masquerMois then
if m:match('^%d%d?$') then
table.insert( wikiListe, texteMois )
m = tonumber(m)
else
m = month_number(m)
end
end
else
if m then
local lien
date.year = tonumber(y)
if annee then
date.month = m
if not numMois then
date.day = tonumber(d)
-- mois est autre chose qu'un simple mois : saison, mois-mois... auquel cas, pas de lien
return true
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
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]]'
end
table.remove( wikiListe )
local function extract_day_or_year(item)
if not args.masquerMois then
-- Called when a day would be valid, or
table.insert( wikiListe, wikiLien( lien, texteMois ) )
-- when a year would be valid if no year has been set and partial is set.
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 #wikiListe > 0 then
elseif suffix == '' and newdate.partial and not date.year then
-- sinon on retire le lien affichant 'jour' pour ne garder que le lien 'jour mois'
date.year = n
table.remove( wikiListe, #wikiListe - 1 )
return true
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
if gmois then
end
table.insert( iso, 1, string.sub( '0' .. gmois, -2 ) )
local function extract_month(item)
-- 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
local function extract_time(item)
local h, m, s = item:match('^(%d%d?):(%d%d)(:?%d*)$')
if date.hour or not h then
return
end
if s ~= '' then
s = s:match('^:(%d%d)$')
if not s then
return
end
end
end
table.insert( wikiListe, gregAprMois )
date.hour = tonumber(h)
date.minute = tonumber(m)
date.second = tonumber(s)  -- nil if empty string
return true
end
end
 
local item_count = 0
-- l'année
local index_time
if annee and not (args.julien == true and args.nolinks and jannee == annee ) then
local function set_ampm(item)
if not args.masquerAnnee then
local H = date.hour
local texteAnnee = annee
if H and not options.am and index_time + 1 == item_count then
local lien
options.am = ampm_options[item]  -- caller checked this is not nil
if annee < 0 then
if item:match('^[Aa]') then
local annneeAvJc = 0 - annee
if not (1 <= H and H <= 12) then
lien = lien or ( annneeAvJc .. ' av. J.-C.' )
return
if args.avJC == false then
end
texteAnnee = annneeAvJc
if H == 12 then
else
date.hour = 0
texteAnnee = annneeAvJc .. ' <abbr class="abbr" title="'
.. annneeAvJc .. ' avant Jésus-Christ">av. J.-C.</abbr>'
end
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
else
lien = existDate( dataQualificatif, annee ) or existDate( dataCat, annee ) or lien or annee
if not (1 <= H and H <= 23) then
if mois and #wikiListe == 0 then
return
-- si le mois n'a pas de lien et n'est pas affiché avec le jour, il est affiché avec l'année.
end
texteAnnee = texteMois .. ' ' .. texteAnnee
if H <= 11 then
date.hour = H + 12
end
end
table.insert( wikiListe, wikiLien( lien, texteAnnee ) )
end
end
return true
end
end
end
end
if annee then
for item in text:gsub(',', ' '):gsub('&nbsp;', ' '):gmatch('%S+') do
if gannee > 999 then
item_count = item_count + 1
table.insert( iso, 1, gannee )
if era_text[item] then
elseif gannee > -1 then
-- Era is accepted in peculiar places.
table.insert( iso, 1, string.sub( '000' .. gannee , -4 ) )
if options.era then
elseif gannee > -999 then
return
-- calendrier grégorien proleptique avec année 0.
end
table.insert( iso, 1, 'U-' .. string.sub( '000' .. ( 0 - gannee ), -4 ) )
options.era = item
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
table.insert( iso, 1, 'U' .. gannee )
return
end
end
end
end
table.insert( wikiListe, gregAprAn )
if not date.year or date.year == 0 then
return
end
local era = era_text[options.era]
if era and era.isbc then
date.year = 1 - date.year
end
return date, options
end


-- l'age
local function autofill(date1, date2)
if type( age ) == 'number' and age >= 0 and ( not naissance or age < 120 ) then
-- Fill any missing month or day in each date using the
if age == 0 then
-- corresponding component from the other date, if present,
age = '(' .. agePrefix .. 'moins d’un\194\160an)'
-- or with 1 if both dates are missing the month or day.
elseif age == 1 then
-- This gives a good result for calculating the difference
age = '(' .. agePrefix .. '1\194\160an)'
-- between two partial dates when no range is wanted.
else
-- Return filled date1, date2 (two full dates).
age = '('.. agePrefix .. age .. '\194\160ans)'
local function filled(a, b)
-- 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
if not a.day then
fillday = b.day or 1
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
end
else
return a
age = false
end
end
return filled(date1, date2), filled(date2, date1)
end


 
local function date_add_sub(lhs, rhs, is_sub)
-- compilation du résultat
-- Return a new date from calculating (lhs + rhs) or (lhs - rhs),
local wikiTexte = table.concat( wikiListe, ' ' )
-- or return nothing if invalid.
local isoTexte = table.concat( iso, '-' )
-- The result is nil if the calculated date exceeds allowable limits.
 
-- Caller ensures that lhs is a date; its properties are copied for the new date.
-- On ajoute un peu de sémantique.
if lhs.partial then
local wikiHtml = mw.html.create( '' )
-- Adding to a partial is not supported.
 
-- Can subtract a date or partial from a partial, but this is not called for that.
if julienDate then
return
wikiHtml:tag( 'span')
end
:addClass( 'nowrap' )
local function is_prefix(text, word, minlen)
:attr( 'data-sort-value', isoTexte )
local n = #text
:wikitext( julienDate )
return (minlen or 1) <= n and n <= #word and text == word:sub(1, n)
:node( julienSup )
:done()
:wikitext( julienSep )
end
end
 
local function do_days(n)
local dateHtml = wikiHtml:tag( 'time' )
local forcetime, jd
:wikitext( wikiTexte )
if floor(n) == n then
if wikiTexte:match( ' ' ) then
jd = lhs.jd
dateHtml:addClass( 'nowrap' )
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 isoTexte ~= wikiTexte then
if type(rhs) == 'number' then
dateHtml:attr( 'datetime', isoTexte )
-- Add/subtract days, including fractional days.
:attr( 'data-sort-value', isoTexte )
return do_days(rhs)
end
end
if not args.nolinks then
if type(rhs) == 'string' then
dateHtml:addClass( 'date-lien' )
-- rhs is a single component like '26m' or '26 months' (with optional sign).
-- 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 naissance then
if is_diff(rhs) then
dateHtml:addClass( 'bday' )
local days = rhs.age_days
elseif mort then
if (is_sub or false) ~= (rhs.isnegative or false) then
dateHtml:addClass( 'dday' )
days = -days
end
return lhs + days
end
end
end


wikiHtml:wikitext( gregFin )
local full_date_only = {
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,
}


if args.republicain then
-- Date operators.
wikiHtml:wikitext( ' (', dateRepublicaine, ')' )
local function mt_date_add(lhs, rhs)
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


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


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


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


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


-- analyseDate sépare la date du contenu qui suit, supprime les liens, et retourne si possible une table avec jour mois année
--[[ Examples of syntax to construct a date:
local function analyseDate( d )
Date(y, m, d, 'julian')            default calendar is 'gregorian'
if trim( d ) then
Date(y, m, d, H, M, S, 'julian')
local analyse = d:match( ' ou ') or d:match( 'entre ' ) or d:match( 'vers ' ) or d:match( 'après ' ) or d:match( 'avant ' )
Date('juliandate', jd, 'julian')    if jd contains "." text output includes H:M:S
if analyse then
Date('currentdate')
return d
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
analyse = d:match( 'datetime="([%d-]+)"' ) or d
else
-- sépare la date (avec ses liens) d'une référence ou contenu commençant par un espace)
local num = tonumber(v)
local debut, fin = analyse:match( '(.-%d%d%d%]*%-?)([\127 ].+)' )
if not num and argtype == 'setdate' and numindex == 1 then
if not debut then
num = month_number(v)
-- sépare la date du contenu commençant par <br>
debut, fin = analyse:match( '(.-%d%d%d%]*%-?)(<br ?/?>.+)' )
end
end
analyse = debut or analyse
if num then
-- supprime les liens
if not argtype then
analyse = analyse:gsub(
argtype = 'setdate'
'%[%[([^%[%]|]*)|?([^%[%]]*)%]%]',
end
function ( l, t )
if argtype == 'setdate' and numindex < 6 then
return trim( t ) or l
numindex = numindex + 1
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 d, fin
return
end
end
end
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
if argtype == 'datetext' then
local function prefix( dateString )
if tnums or not set_date_from_numbers(newdate, extract_date(newdate, datetext)) then
if dateString then
return
local datetime = dateString:match( 'datetime="([U%d%-]+)"' )
end
if datetime and datetime:match('%-%d%d%-%d%d') and trim( args['préfixe'] ) then
elseif argtype == 'juliandate' then
return args['préfixe'] .. ' ' .. dateString
newdate.partial = nil
end
newdate.jd = jd_number
if trim( args['préfixe sans jour'] ) then
if not set_date_from_jd(newdate) then
return args['préfixe sans jour'] .. ' ' .. dateString
return
end
end
elseif argtype == 'currentdate' or argtype == 'currentdatetime' then
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
return dateString
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
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 naissance = args[1]:match( '^n' ) == 'n'
local function _diff_age(diff, code, options)
local mort = args[1]:match( '^m' ) or args[1]:match( 'décès' )
-- Return a tuple of integer values from diff as specified by code, except that
local evenement = args[1]:match( '^é' )
-- each integer may be a list of two integers for a diff with a partial date, or
local affichageDate, qualificatif = args[2], args[4]
-- return nil if the code is not supported.
local affichageDateTab, resultatDate, complementDate
-- If want round, the least significant unit is rounded to nearest whole unit.
local dateNaissance, dateMort
-- For a duration, an extra day is added.
if mort or evenement then
local wantround, wantduration, wantrange
affichageDate = args[3]
if type(options) == 'table' then
wantround = options.round
wantduration = options.duration
wantrange = options.range
else
wantround = options
end
end
if not trim( affichageDate ) then
if not is_diff(diff) then
return
local f = wantduration and 'duration' or 'age'
error(f .. ': need a date difference (use "diff:' .. f .. '()" with a colon)', 2)
end
end
if affichageDate:match( '</time>' ) then
if diff.partial 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
-- Ignore wantround, wantduration.
if ( naissance or mort or evenement ) and ( affichageDate:match( 'wikidata%-linkback' )) then
local function choose(v)
dateNaissance = analyseDate( args[2] )
if type(v) == 'table' then
dateMort = analyseDate( args[3] )
if not wantrange or v[1] == v[2] then
resultatDate = affichageDate
-- Example: Date('partial', 2005) - Date('partial', 2001) gives
else
-- diff.years = { 3, 4 } to show the range of possible results.
return prefix( affichageDate )
-- If do not want a range, choose the second value as more expected.
return v[2]
end
end
return v
end
end
else
if code == 'ym' or code == 'ymd' then
affichageDateTab, complementDate = analyseDate( affichageDate )
if not wantrange and diff.iszero then
if type( affichageDateTab ) ~= 'table' then
-- This avoids an unexpected result such as
return affichageDateTab
-- Date('partial', 2001) - Date('partial', 2001)
else
-- giving diff = { years = 0, months = { 0, 11 } }
if naissance then
-- which would be reported as 0 years and 11 months.
dateNaissance = affichageDateTab
return 0, 0
dateMort = analyseDate( args[3] )
elseif mort then
dateNaissance = analyseDate( args[2] )
dateMort = affichageDateTab
else
qualificatif = args[3]
end
end
affichageDateTab.naissance = naissance
return choose(diff.partial.years), choose(diff.partial.months)
affichageDateTab.mort = mort
end
affichageDateTab.evenement = evenement
if code == 'y' then
affichageDateTab.qualificatif = args.qualificatif or qualificatif
return choose(diff.partial.years)
affichageDateTab.nolinks = args.nolinks
end
affichageDateTab.nocat = args.nocat
if code == 'm' or code == 'w' or code == 'd' then
affichageDateTab.julien = args.julien
return choose({ diff.partial.mindiff:age(code), diff.partial.maxdiff:age(code) })
end
end
return nil
end
end
resultatDate = resultatDate or fun.modeleDate( affichageDateTab )
local extra_days = wantduration and 1 or 0
 
if code == 'wd' or code == 'w' or code == 'd' then
local age, prefixAge, suffixAge, calculAge = '', ' <span class="noprint">(', ')</span>', nil
local offset = wantround and 0.5 or 0
if naissance and
local days = diff.age_days + extra_days
dateNaissance and
if code == 'wd' or code == 'd' then
not dateMort and
days = floor(days + offset)
type( dateNaissance ) == 'table'
if code == 'd' then
then
return days
calculAge = fun.age( dateNaissance.annee, dateNaissance.numMois, dateNaissance.jour )
end
if calculAge and calculAge > 120 then
return floor(days/7), days % 7
calculAge = nil
end
end
elseif (mort or evenement) and
return floor(days/7 + offset)
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
if tonumber( calculAge ) then
local H, M, S = diff.hours, diff.minutes, diff.seconds
if calculAge > 1 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
age = prefixAge .. calculAge .. '\194\160ans' .. suffixAge
local days = floor(diff.age_days + extra_days)
elseif calculAge == 1 then
local inc_hour
age = prefixAge .. 'un\194\160an' .. suffixAge
if wantround then
elseif calculAge == 0 then
if code == 'dh' or code == 'h' then
age = prefixAge .. 'moins d’un\194\160an' .. suffixAge
if M >= 30 then
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 complementDate and complementDate:match( 'ans?%)' ) then
if code == 'dh' or code == 'dhm' or code == 'dhms' then
complementDate = ''
if code == 'dh' then
return days, H
elseif code == 'dhm' then
return days, H, M
else
return days, H, M, S
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
end
return hours, M, S
end
end
 
if wantround then
return prefix( resultatDate ) .. ( complementDate or '' ) .. age
local inc_hour
end
if code == 'ymdh' or code == 'ymwdh' then
 
if M >= 30 then
 
inc_hour = true
---
end
-- la fonction dateISO renvoie un date au format aaaa-mm-jj (sans liens)
elseif code == 'ymdhm' or code == 'ymwdhm' then
-- l'année peut être sous la forme 2013 ou [[2013 en litérature|2013]]
if S >= 30 then
-- le mois peut être en lettres ou en chiffres
M = M + 1
-- le jour peut être sous la forme '05', '{{1er}}' ou 'vendredi 13'
if M >= 60 then
function fun.dateISO( frame )
M = 0
local args = Outils.extractArgs( frame )
inc_hour = true
local annee = Outils.notEmpty( args['année'], args.annee, args.year, args.date )
end
-- extraction de l'année
end
if type( annee ) == 'string' then
elseif code == 'ymd' or code == 'ymwd' or code == 'yd' or code == 'md' then
annee = ( tonumber( annee ) -- match '2013'
if H >= 12 then
or string.match ( annee, '%D(%d%d%d%d)%D' ) -- match '[[2013 en musique|2013]]'
extra_days = extra_days + 1
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
jour = tonumber( jour )
end
if jour and jour <= listeMois[numMois].nJour then
if inc_hour then
jour = '-' .. string.sub( '0' .. jour, -2 )
H = H + 1
return annee .. mois .. jour
if H >= 24 then
else
H = 0
return annee .. mois
extra_days = extra_days + 1
end
end
else
return tostring( annee )
end
end
end
end
end
local y, m, d = diff.years, diff.months, diff.days
 
if extra_days > 0 then
---
d = d + extra_days
-- Rang du jour dans l'année
if d > 28 or code == 'yd' then
-- Usage : do_dayRank{année,mois,jour}
-- Recalculate in case have passed a month.
function fun.do_dayRank(arguments)
diff = diff.date1 + extra_days - diff.date2
local yr = tonumber(arguments.year or arguments[1]) or 1
y, m, d = diff.years, diff.months, diff.days
local mt = tonumber(arguments.month or arguments[2]) or 1
end
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
return rank
if code == 'ymd' then
end
return y, m, d
 
elseif code == 'yd' then
-- Nombre de jours entre deux années (du 1er janvier au 1er janvier)
if y > 0 then
-- Suit le calendrier grégorien
-- It is known that diff.date1 > diff.date2.
function fun.do_daysBetween(arguments)
diff = diff.date1 - (diff.date2 + (y .. 'y'))
local yr1 = tonumber(arguments[1]) or 0
end
local yr2 = tonumber(arguments[2]) or 0
return y, floor(diff.age_days)
 
elseif code == 'md' then
return fun.daysSinceOrigin(yr2) - fun.daysSinceOrigin(yr1)
return y * 12 + m, d
end
elseif code == 'ym' or code == 'm' then
 
if wantround then
-- Nombre de jours depuis l'année 1 (du 1er janvier au 1er janvier)
if d >= 16 then
function fun.daysSinceOrigin(year)
m = m + 1
local yr = year-1
if m >= 12 then
return 365*yr + math.floor(yr/4) - math.floor(yr/100) + math.floor(yr/400)
m = 0
end
y = y + 1
 
end
-- Test d'année bissextile (Suit le calendrier grégorien)
end
function fun.isLeapYear(year)
end
local yr = tonumber(year) or 1
if code == 'ym' then
return (yr%4 == 0) and ((yr%100 ~= 0) or (yr%400 == 0))
return y, m
end
end
 
return y * 12 + m
-- Conversion d'un nombre en chiffres romains
elseif code == 'ymw' then
function fun.toRoman(number)
local weeks = floor(d/7)
local n = math.floor(number)
if wantround then
local letters = {"I","V","X","L","C","D","M","",""}
local days = d % 7
local pattern = {"","0","00","000","01","1","10","100","1000","02"}
if days > 3 or (days == 3 and H >= 12) then
local result = ""
weeks = weeks + 1
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
if code == 'y' then
if wantround and m >= 6 then
y = y + 1
end
return y
end
end
return result
return nil
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
end


---
local function _diff_duration(diff, code, options)
-- Formatage d'une date selon le calendrier républicain
if type(options) ~= 'table' then
-- Usage : fun.formatRepCal{année,mois,jour}
options = { round = options }
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
result = result .. " de l'an " .. fun.toRoman(arguments[1])
options.duration = true
return result
return _diff_age(diff, code, options)
end
end


---
-- Metatable for some operations on date differences.
-- Voir Modèle:Âge
diffmt = {  -- for forward declaration above
-- retourne l'âge en fonction de la ou les dates fournies. La valeur retournée est de type 'number'
__concat = function (lhs, rhs)
-- Paramètres :
return tostring(lhs) .. tostring(rhs)
-- 1, 2, 3 : année, mois jour de naissance (supposé dans le calendrier grégorien)
end,
-- 4, 5, 6 : année, mois, jour du calcul (facultatif, par défaut la date UTC courante).
__tostring = function (self)
function fun.age( an, mn, jn, ac, mc, jc )
return tostring(self.age_days)
if ac == nil then
end,
local today = os.date( '!*t' )
__index = function (self, key)
ac = today.year
local value
mc = today.month
if key == 'age_days' then
jc = today.day
if rawget(self, 'partial') then
else
local function jdz(date)
ac = tonumber( ac )
return (date.partial and date.partial.first or date).jdz
mc = tonumber( mc )
end
jc = tonumber( jc )
value = jdz(self.date1) - jdz(self.date2)
end
else
 
value = self.date1.jdz - self.date2.jdz
local an = tonumber( an )
end
local mn = tonumber( mn )
end
local jn = tonumber( jn )
if value ~= nil then
rawset(self, key, value)
return value
end
end,
}


if an == nil or ac == nil or mn == nil or mc == nil then
function DateDiff(date1, date2, options)  -- for forward declaration above
-- pas de message d'erreur qui risque de faire planter la fonction appelante
-- Return a table with the difference between two dates (date1 - date2).
-- à elle de gérer ce retour.
-- The difference is negative if date1 is older than date2.
-- 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
local age = ac - an
if type(options) == 'table' then
if mc == mn then
wantfill = options.fill
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
end
local isnegative = false
 
local iszero = false
function fun.modeleAge( frame )
if date1 < date2 then
local args = Outils.extractArgs( frame )
isnegative = true
local age = fun.age (
date1, date2 = date2, date1
args[1] or args['année'],
elseif date1 == date2 then
args[2] or args['mois'],
iszero = true
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
end
-- It is known that date1 >= date2 (period is from date2 to date1).
 
if date1.partial or date2.partial then
---
-- Two partial dates might have timelines:
-- calcul du jour julien à partir d'une date du calendrier grégorien
---------------------A=================B--- date1 is from A to B inclusive
function fun.julianDay( year, month, day, hour, min, sec )
--------C=======D-------------------------- date2 is from C to D inclusive
local julian
-- date1 > date2 iff A > C (date1.partial.first > date2.partial.first)
julian = math.floor( math.floor( ( year * 12 + month + 57609 ) / 12 - 1 ) * 1461 / 4 )
-- The periods can overlap ('April 2001' - '2001'):
- math.floor( math.floor( ( year * 12 + month + 57609 ) / 12 - 1 ) / 100 )
-------------A===B------------------------- A=2001-04-01  B=2001-04-30
+ math.floor( math.floor( ( year * 12 + month + 57609 ) / 12 - 1 ) / 400 )
--------C=====================D------------ C=2001-01-01 D=2001-12-31
+ math.floor( ( math.fmod( month + 57609, 12 ) + 4 ) * 153 / 5 )
if wantfill then
+ day + ( hour or 12 ) / 24 + ( min or 0 ) / 1440 + ( sec or 0 ) / 86400
date1, date2 = autofill(date1, date2)
- 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
return "[[Heure d'été d'Europe centrale|CEST]]"
local function zdiff(date1, date2)
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
else  -- heure d'hiver (ou autre zone où ça ne s'applique pas)
end
if (opt == "sans lien") then
local y1, m1 = date1.year, date1.month
return "CET"
local y2, m2 = date2.year, date2.month
elseif (opt == "décalage") then
local years = y1 - y2
return "1"
local months = m1 - m2
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
return "[[Heure normale d'Europe centrale|CET]]"
days = dpm - d2 + d1
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 fun
return {
_current = current,
_Date = Date,
_days_in_month = days_in_month,
}

Latest revision as of 08:31, 2 January 2022

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

-- Date functions for use by other modules.
-- I18N and time zones are not supported.

local MINUS = '−'  -- Unicode U+2212 MINUS SIGN
local floor = math.floor

local Date, DateDiff, diffmt  -- forward declarations
local uniq = { 'unique identifier' }

local function is_date(t)
	-- The system used to make a date read-only means there is no unique
	-- metatable that is conveniently accessible to check.
	return type(t) == 'table' and t._id == uniq
end

local function is_diff(t)
	return type(t) == 'table' and getmetatable(t) == diffmt
end

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

local function collection()
	-- Return a table to hold items.
	return {
		n = 0,
		add = function (self, item)
			self.n = self.n + 1
			self[self.n] = item
		end,
		join = _list_join,
	}
end

local function strip_to_nil(text)
	-- If text is a string, return its trimmed content, or nil if empty.
	-- Otherwise return text (convenient when Date fields are provided from
	-- another module which may pass a string, a number, or another type).
	if type(text) == 'string' then
		text = text:match('(%S.-)%s*$')
	end
	return text
end

local function is_leap_year(year, calname)
	-- Return true if year is a leap year.
	if calname == 'Julian' then
		return year % 4 == 0
	end
	return (year % 4 == 0 and year % 100 ~= 0) or year % 400 == 0
end

local function days_in_month(year, month, calname)
	-- Return number of days (1..31) in given month (1..12).
	if month == 2 and is_leap_year(year, calname) then
		return 29
	end
	return ({ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 })[month]
end

local function h_m_s(time)
	-- Return hour, minute, second extracted from fraction of a day.
	time = floor(time * 24 * 3600 + 0.5)  -- number of seconds
	local second = time % 60
	time = floor(time / 60)
	return floor(time / 60), time % 60, second
end

local function hms(date)
	-- Return fraction of a day from date's time, where (0 <= fraction < 1)
	-- if the values are valid, but could be anything if outside range.
	return (date.hour + (date.minute + date.second / 60) / 60) / 24
end

local function julian_date(date)
	-- Return jd, jdz from a Julian or Gregorian calendar date where
	--   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
		offset = floor(y/4) - floor(y/100) + floor(y/400) - 32045
	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

local function set_date_from_jd(date)
	-- Set the fields of table date from its Julian date field.
	-- Return true if date is valid.
	-- http://www.tondering.dk/claus/cal/julperiod.php#formula
	-- This handles the proleptic Julian and Gregorian calendars.
	-- Negative Julian dates are not defined but they work.
	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
		return
	end
	local jd = date.jd
	if not (type(jd) == 'number' and low <= jd and jd <= high) then
		return
	end
	local jdn = floor(jd)
	if date.hastime then
		local time = jd - jdn  -- 0 <= time < 1
		if time >= 0.5 then    -- if at or after midnight of next day
			jdn = jdn + 1
			time = time - 0.5
		else
			time = time + 0.5
		end
		date.hour, date.minute, date.second = h_m_s(time)
	else
		date.second = 0
		date.minute = 0
		date.hour = 0
	end
	local b, c
	if calname == 'Julian' then
		b = 0
		c = jdn + 32082
	else  -- Gregorian
		local a = jdn + 32044
		b = floor((4*a + 3)/146097)
		c = a - floor(146097*b/4)
	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

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.
	-- The result will have valid m, d values if y is valid; caller checks y.
	-- The logic of PHP mktime is followed where m or d can be zero to mean
	-- the previous unit, and -1 is the one before that, etc.
	-- Positive values carry forward.
	local date
	if not (1 <= m and m <= 12) then
		date = Date(y, 1, 1)
		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
		if days_hms or not (1 <= d and d <= days_in_month(y, m, calendar)) then
			date = date or Date(y, m, 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
	numbers.year = y
	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

local function set_date_from_numbers(date, numbers, options)
	-- Set the fields of table date from numeric values.
	-- Return true if date is valid.
	if type(numbers) ~= 'table' then
		return
	end
	local y = numbers.year   or date.year
	local m = numbers.month  or date.month
	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
	elseif y and date.partial then
		if d or not (-9999 <= y and y <= 9999) then
			return
		end
		if m and not (1 <= m and m <= 12) then
			if not date.want_fix then
				return
			end
			need_fix = true
		end
	else
		return
	end
	if date.partial then
		H = nil  -- ignore any time
		M = nil
		S = nil
	else
		if H then
			-- It is not possible to set M or S without also setting H.
			date.hastime = true
		else
			H = 0
		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
				return
			end
		end
	end
	date.want_fix = nil
	if need_fix then
		fix_numbers(numbers, y, m, d, H, M, S, date.partial, date.hastime, date.calendar)
		return set_date_from_numbers(date, numbers, options)
	end
	date.year = y    -- -9999 to 9999 ('n BC' → year = 1 - n)
	date.month = m   -- 1 to 12 (may be nil if partial)
	date.day = d     -- 1 to 31 (* = nil if partial)
	date.hour = H    -- 0 to 59 (*)
	date.minute = M  -- 0 to 59 (*)
	date.second = S  -- 0 to 59 (*)
	if type(options) == 'table' then
		for _, k in ipairs({ 'am', 'era', 'format' }) do
			if options[k] then
				date.options[k] = options[k]
			end
		end
	end
	return true
end

local function make_option_table(options1, options2)
	-- If options1 is a string, return a table with its settings, or
	-- if it is a table, use its settings.
	-- Missing options are set from table options2 or defaults.
	-- If a default is used, a flag is set so caller knows the value was not intentionally set.
	-- Valid option settings are:
	-- am: 'am', 'a.m.', 'AM', 'A.M.'
	--     'pm', 'p.m.', 'PM', 'P.M.' (each has same meaning as corresponding item above)
	-- era: 'BCMINUS', 'BCNEGATIVE', 'BC', 'B.C.', 'BCE', 'B.C.E.', 'AD', 'A.D.', 'CE', 'C.E.'
	-- Option am = 'am' does not mean the hour is AM; it means 'am' or 'pm' is used, depending on the hour,
	--    and am = 'pm' has the same meaning.
	-- Similarly, era = 'BC' means 'BC' is used if year <= 0.
	-- BCMINUS displays a MINUS if year < 0 and the display format does not include %{era}.
	-- BCNEGATIVE is similar but displays a hyphen.
	local result = { bydefault = {} }
	if type(options1) == 'table' then
		result.am = options1.am
		result.era = options1.era
	elseif type(options1) == 'string' then
		-- Example: 'am:AM era:BC' or 'am=AM era=BC'.
		for item in options1:gmatch('%S+') do
			local lhs, rhs = item:match('^(%w+)[:=](.+)$')
			if lhs then
				result[lhs] = rhs
			end
		end
	end
	options2 = type(options2) == 'table' and options2 or {}
	local defaults = { am = 'am', era = 'BC' }
	for k, v in pairs(defaults) do
		if not result[k] then
			if options2[k] then
				result[k] = options2[k]
			else
				result[k] = v
				result.bydefault[k] = true
			end
		end
	end
	return result
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
	-- by replacing year with 1 - year if date.year <= 0).
	-- options.era = { year<=0 , year>0 }
	['BCMINUS']    = { 'BC'    , ''    , isbc = true, sign = MINUS },
	['BCNEGATIVE'] = { 'BC'    , ''    , isbc = true, sign = '-'   },
	['BC']         = { 'BC'    , ''    , isbc = true },
	['B.C.']       = { 'B.C.'  , ''    , isbc = true },
	['BCE']        = { 'BCE'   , ''    , isbc = true },
	['B.C.E.']     = { 'B.C.E.', ''    , isbc = true },
	['AD']         = { 'BC'    , 'AD'   },
	['A.D.']       = { 'B.C.'  , 'A.D.' },
	['CE']         = { 'BCE'   , 'CE'   },
	['C.E.']       = { 'B.C.E.', 'C.E.' },
}

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

local function strftime(date, format, options)
	-- Return date formatted as a string using codes similar to those
	-- in the C strftime library function.
	local sformat = string.format
	local shortcuts = {
		['%c'] = '%-I:%M %p %-d %B %-Y %{era}',  -- date and time: 2:30 pm 1 April 2016
		['%x'] = '%-d %B %-Y %{era}',            -- date:          1 April 2016
		['%X'] = '%-I:%M %p',                    -- time:          2:30 pm
	}
	if shortcuts[format] then
		format = shortcuts[format]
	end
	local codes = {
		a = { field = 'dayabbr' },
		A = { field = 'dayname' },
		b = { field = 'monthabbr' },
		B = { field = 'monthname' },
		u = { fmt = '%d'  , field = 'dowiso' },
		w = { fmt = '%d'  , field = 'dow' },
		d = { fmt = '%02d', fmt2 = '%d', field = 'day' },
		m = { fmt = '%02d', fmt2 = '%d', field = 'month' },
		Y = { fmt = '%04d', fmt2 = '%d', field = 'year' },
		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
			local value = date[code.field]
			if not value then
				return nil  -- an undefined field in a partial date
			end
			local special = code.special
			if special then
				if special == 'hour12' then
					value = value % 12
					value = value == 0 and 12 or value
				elseif special == 'am' then
					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
	local function replace_property(spaces, id)
		if id == 'era' then
			-- Special case so can use local era option.
			local result = get_era_for_year(eraopt, date.year)
			if result == '' then
				return ''
			end
			return (spaces == '' and '' or '&nbsp;') .. result
		end
		local result = date[id]
		if type(result) == 'string' then
			return spaces .. result
		end
		if type(result) == 'number' then
			return  spaces .. tostring(result)
		end
		if type(result) == 'boolean' then
			return  spaces .. (result and '1' or '0')
		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)
	-- Return a formatted string representing the given date.
	if not is_date(date) then
		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
	elseif date.partial then
		fmt = date.month and 'my' or 'y'
	else
		fmt = 'dmy'
		if date.hastime then
			fmt = (date.second > 0 and 'hms ' or 'hm ') .. fmt
		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
			return bad_format()
		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 = {
	-- 0=Sun to 6=Sat
	[0] = { 'Sun', 'Sunday' },
	{ 'Mon', 'Monday' },
	{ 'Tue', 'Tuesday' },
	{ 'Wed', 'Wednesday' },
	{ 'Thu', 'Thursday' },
	{ 'Fri', 'Friday' },
	{ 'Sat', 'Saturday' },
}

local month_info = {
	-- 1=Jan to 12=Dec
	{ 'Jan', 'January' },
	{ 'Feb', 'February' },
	{ 'Mar', 'March' },
	{ 'Apr', 'April' },
	{ 'May', 'May' },
	{ 'Jun', 'June' },
	{ 'Jul', 'July' },
	{ 'Aug', 'August' },
	{ 'Sep', 'September' },
	{ 'Oct', 'October' },
	{ 'Nov', 'November' },
	{ 'Dec', 'December' },
}

local function name_to_number(text, translate)
	if type(text) == 'string' then
		return translate[text:lower()]
	end
end

local function day_number(text)
	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

local function month_number(text)
	return name_to_number(text, {
		jan = 1, january = 1,
		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)
	-- Return a list of formatted strings from a list of dates.
	if not type(list) == 'table' then
		error('date:list:text: need "list:text()" with a colon', 2)
	end
	local result = { join = _list_join }
	for i, date in ipairs(list) do
		result[i] = date:text(fmt)
	end
	return result
end

local function _date_list(date, spec)
	-- Return a possibly empty numbered table of dates meeting the specification.
	-- Dates in the list are in ascending order (oldest date first).
	-- The spec should be a string of form "<count> <day> <op>"
	-- where each item is optional and
	--   count = number of items wanted in list
	--   day = abbreviation or name such as Mon or Monday
	--   op = >, >=, <, <= (default is > meaning after date)
	-- If no count is given, the list is for the specified days in date's month.
	-- The default day is date's day.
	-- The spec can also be a positive or negative number:
	--   -5 is equivalent to '5 <'
	--   5  is equivalent to '5' which is '5 >'
	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
		elseif type(spec) == 'string' then
			local num, day, op = spec:match('^%s*(%d*)%s*(%a*)%s*([<>=]*)%s*$')
			if not num then
				return list
			end
			if num ~= '' then
				count = tonumber(num)
			end
			if day ~= '' then
				local dow = day_number(day:gsub('[sS]$', ''))  -- accept plural days
				if not dow then
					return list
				end
				offset = dow - date.dow
			end
			operation = ops[op]
		else
			return list
		end
	end
	offset = offset or 0
	operation = operation or ops['>']
	local datefrom, dayfirst, daylast
	if operation.before then
		if offset > 0 or (offset == 0 and not operation.include) then
			offset = offset - 7
		end
		if count then
			if count > 1 then
				offset = offset - 7*(count - 1)
			end
			datefrom = date + offset
		else
			daylast = date.day + offset
			dayfirst = daylast % 7
			if dayfirst == 0 then
				dayfirst = 7
			end
		end
	else
		if offset < 0 or (offset == 0 and not operation.include) then
			offset = offset + 7
		end
		if count then
			datefrom = date + offset
		else
			dayfirst = date.day + offset
			daylast = date.monthdays
		end
	end
	if not count then
		if daylast < dayfirst then
			return list
		end
		count = floor((daylast - dayfirst)/7) + 1
		datefrom = Date(date, {day = dayfirst})
	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)
	-- Parse the date/time in text and return n, o where
	--   n = table of numbers with date/time fields
	--   o = table of options for AM/PM or AD/BC or format, if any
	-- or return nothing if date is known to be invalid.
	-- Caller determines if the values in n are valid.
	-- 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
			if y <= 0 then
				options.era = 'BCE'
			end
			date.year = y
			m = tonumber(m)
			d = tonumber(d)
			H = tonumber(H)
			M = tonumber(M)
			S = tonumber(S)
			if m == 0 then
				newdate.partial = true
				return date, options
			end
			date.month = m
			if d == 0 then
				newdate.partial = true
				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
		return
	end
	local function extract_ymd(item)
		-- Called when no day or month has been set.
		local y, m, d = item:match('^(%d%d%d%d)%-(%w+)%-(%d%d?)$')
		if y then
			if date.year then
				return
			end
			if m:match('^%d%d?$') then
				m = tonumber(m)
			else
				m = month_number(m)
			end
			if m then
				date.year = tonumber(y)
				date.month = m
				date.day = tonumber(d)
				return true
			end
		end
	end
	local function extract_day_or_year(item)
		-- Called when a day would be valid, or
		-- when a year would be valid if no year has been set and partial is set.
		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
			elseif suffix == '' and newdate.partial and not date.year then
				date.year = n
				return true
			end
		end
	end
	local function extract_month(item)
		-- 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
	local function extract_time(item)
		local h, m, s = item:match('^(%d%d?):(%d%d)(:?%d*)$')
		if date.hour or not h then
			return
		end
		if s ~= '' then
			s = s:match('^:(%d%d)$')
			if not s then
				return
			end
		end
		date.hour = tonumber(h)
		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
				if not (1 <= H and H <= 23) then
					return
				end
				if H <= 11 then
					date.hour = H + 12
				end
			end
			return true
		end
	end
	for item in text:gsub(',', ' '):gsub('&nbsp;', ' '):gmatch('%S+') do
		item_count = item_count + 1
		if era_text[item] then
			-- Era is accepted in peculiar places.
			if options.era then
				return
			end
			options.era = item
		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
			return
		end
	end
	if not date.year or date.year == 0 then
		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)
	-- Fill any missing month or day in each date using the
	-- corresponding component from the other date, if present,
	-- or with 1 if both dates are missing the month or day.
	-- This gives a good result for calculating the difference
	-- between two partial dates when no range is wanted.
	-- Return filled date1, date2 (two full dates).
	local function filled(a, b)
		-- 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
		if not a.day then
			fillday = b.day or 1
		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
	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),
	-- or return nothing if invalid.
	-- The result is nil if the calculated date exceeds allowable limits.
	-- Caller ensures that lhs is a date; its properties are copied for the new date.
	if lhs.partial then
		-- Adding to a partial is not supported.
		-- Can subtract a date or partial from a partial, but this is not called for that.
		return
	end
	local function is_prefix(text, word, minlen)
		local n = #text
		return (minlen or 1) <= n and n <= #word and text == word:sub(1, n)
	end
	local function do_days(n)
		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
	if type(rhs) == 'number' then
		-- Add/subtract days, including fractional days.
		return do_days(rhs)
	end
	if type(rhs) == 'string' then
		-- rhs is a single component like '26m' or '26 months' (with optional sign).
		-- 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
	if is_diff(rhs) then
		local days = rhs.age_days
		if (is_sub or false) ~= (rhs.isnegative or false) then
			days = -days
		end
		return lhs + days
	end
end

local full_date_only = {
	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.
local function mt_date_add(lhs, rhs)
	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
	return date_add_sub(lhs, rhs)
end

local function mt_date_sub(lhs, rhs)
	if is_date(lhs) then
		if is_date(rhs) then
			return DateDiff(lhs, rhs)
		end
		return date_add_sub(lhs, rhs, true)
	end
end

local function mt_date_concat(lhs, rhs)
	return tostring(lhs) .. tostring(rhs)
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,
	-- Date(-4712, 1, 1, 'Julian') == Date(-4713, 11, 24, 'Gregorian') is true.
	-- This is called only if lhs and rhs have the same type and the same metamethod.
	if lhs.partial or rhs.partial then
		-- One date is partial; the other is a partial or a full date.
		-- The months may both be nil, but must be the same.
		return lhs.year == rhs.year and lhs.month == rhs.month and lhs.calendar == rhs.calendar
	end
	return lhs.jdz == rhs.jdz
end

local function mt_date_lt(lhs, rhs)
	-- Return true if lhs < rhs, for example,
	-- Date('1 Jan 2016') < Date('06:00 1 Jan 2016') is true.
	-- This is called only if lhs and rhs have the same type and the same metamethod.
	if lhs.partial or rhs.partial then
		-- One date is partial; the other is a partial or a full date.
		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
		else
			local num = tonumber(v)
			if not num and argtype == 'setdate' and numindex == 1 then
				num = month_number(v)
			end
			if num then
				if not argtype then
					argtype = 'setdate'
				end
				if argtype == 'setdate' and numindex < 6 then
					numindex = numindex + 1
					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
			else
				return
			end
		end
	end
	if argtype == 'datetext' then
		if tnums or not set_date_from_numbers(newdate, extract_date(newdate, datetext)) then
			return
		end
	elseif argtype == 'juliandate' then
		newdate.partial = nil
		newdate.jd = jd_number
		if not set_date_from_jd(newdate) then
			return
		end
	elseif argtype == 'currentdate' or argtype == 'currentdatetime' then
		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
	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)
	-- Return a tuple of integer values from diff as specified by code, except that
	-- each integer may be a list of two integers for a diff with a partial date, or
	-- return nil if the code is not supported.
	-- If want round, the least significant unit is rounded to nearest whole unit.
	-- For a duration, an extra day is added.
	local wantround, wantduration, wantrange
	if type(options) == 'table' then
		wantround = options.round
		wantduration = options.duration
		wantrange = options.range
	else
		wantround = options
	end
	if not is_diff(diff) then
		local f = wantduration and 'duration' or 'age'
		error(f .. ': need a date difference (use "diff:' .. f .. '()" with a colon)', 2)
	end
	if diff.partial then
		-- Ignore wantround, wantduration.
		local function choose(v)
			if type(v) == 'table' then
				if not wantrange or v[1] == v[2] then
					-- Example: Date('partial', 2005) - Date('partial', 2001) gives
					-- diff.years = { 3, 4 } to show the range of possible results.
					-- If do not want a range, choose the second value as more expected.
					return v[2]
				end
			end
			return v
		end
		if code == 'ym' or code == 'ymd' then
			if not wantrange and diff.iszero then
				-- This avoids an unexpected result such as
				-- Date('partial', 2001) - Date('partial', 2001)
				-- giving diff = { years = 0, months = { 0, 11 } }
				-- which would be reported as 0 years and 11 months.
				return 0, 0
			end
			return choose(diff.partial.years), choose(diff.partial.months)
		end
		if code == 'y' then
			return choose(diff.partial.years)
		end
		if code == 'm' or code == 'w' or code == 'd' then
			return choose({ diff.partial.mindiff:age(code), diff.partial.maxdiff:age(code) })
		end
		return nil
	end
	local extra_days = wantduration and 1 or 0
	if code == 'wd' or code == 'w' or code == 'd' then
		local offset = wantround and 0.5 or 0
		local days = diff.age_days + extra_days
		if code == 'wd' or code == 'd' then
			days = floor(days + offset)
			if code == 'd' then
				return days
			end
			return floor(days/7), days % 7
		end
		return floor(days/7 + offset)
	end
	local H, M, S = diff.hours, diff.minutes, diff.seconds
	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
		local days = floor(diff.age_days + extra_days)
		local inc_hour
		if wantround then
			if code == 'dh' or code == 'h' then
				if M >= 30 then
					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
		if code == 'dh' or code == 'dhm' or code == 'dhms' then
			if code == 'dh' then
				return days, H
			elseif code == 'dhm' then
				return days, H, M
			else
				return days, H, M, S
			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
	if wantround then
		local inc_hour
		if code == 'ymdh' or code == 'ymwdh' then
			if M >= 30 then
				inc_hour = true
			end
		elseif code == 'ymdhm' or code == 'ymwdhm' then
			if S >= 30 then
				M = M + 1
				if M >= 60 then
					M = 0
					inc_hour = true
				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
	end
	local y, m, d = diff.years, diff.months, diff.days
	if extra_days > 0 then
		d = d + extra_days
		if d > 28 or code == 'yd' then
			-- Recalculate in case have passed a month.
			diff = diff.date1 + extra_days - diff.date2
			y, m, d = diff.years, diff.months, diff.days
		end
	end
	if code == 'ymd' then
		return y, m, d
	elseif code == 'yd' then
		if y > 0 then
			-- It is known that diff.date1 > diff.date2.
			diff = diff.date1 - (diff.date2 + (y .. 'y'))
		end
		return y, floor(diff.age_days)
	elseif code == 'md' then
		return y * 12 + m, d
	elseif code == 'ym' or code == 'm' then
		if wantround then
			if d >= 16 then
				m = m + 1
				if m >= 12 then
					m = 0
					y = y + 1
				end
			end
		end
		if code == 'ym' then
			return y, m
		end
		return y * 12 + m
	elseif code == 'ymw' then
		local weeks = floor(d/7)
		if wantround then
			local days = d % 7
			if days > 3 or (days == 3 and H >= 12) then
				weeks = weeks + 1
			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
	if code == 'y' then
		if wantround and m >= 6 then
			y = y + 1
		end
		return y
	end
	return nil
end

local function _diff_duration(diff, code, options)
	if type(options) ~= 'table' then
		options = { round = options }
	end
	options.duration = true
	return _diff_age(diff, code, options)
end

-- Metatable for some operations on date differences.
diffmt = {  -- for forward declaration above
	__concat = function (lhs, rhs)
		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
	-- Return a table with the difference between two dates (date1 - date2).
	-- The difference is negative if date1 is older than date2.
	-- 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
	end
	local wantfill
	if type(options) == 'table' then
		wantfill = options.fill
	end
	local isnegative = false
	local iszero = false
	if date1 < date2 then
		isnegative = true
		date1, date2 = date2, date1
	elseif date1 == date2 then
		iszero = true
	end
	-- It is known that date1 >= date2 (period is from date2 to date1).
	if date1.partial or date2.partial then
		-- Two partial dates might have timelines:
		---------------------A=================B--- date1 is from A to B inclusive
		--------C=======D-------------------------- date2 is from C to D inclusive
		-- date1 > date2 iff A > C (date1.partial.first > date2.partial.first)
		-- The periods can overlap ('April 2001' - '2001'):
		-------------A===B------------------------- A=2001-04-01  B=2001-04-30
		--------C=====================D------------ C=2001-01-01  D=2001-12-31
		if wantfill then
			date1, date2 = autofill(date1, date2)
		else
			local function zdiff(date1, date2)
				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
	local y1, m1 = date1.year, date1.month
	local y2, m2 = date2.year, date2.month
	local years = y1 - y2
	local months = m1 - m2
	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
			days = dpm - d2 + d1
		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

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