Module:Démographie

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

--[[
  Module reprenant les fonctionnalités du modèle Démographie.
--]]

local p = {} -- le module

-- le module chartes (centralisation des styles)
local data = require "Module:Chartes"

-- liste des paramètres reconnus (valeur = nom de la variable)
p.parametres = {
  ["titre"] = "titre",
  ["charte"] = "charte",
  ["colonnes"] = "colonnes",
  ["notes"] = "notes",
  ["source"] = "source",
  ["sources"] = "sources",
  ["wikidata"] = "wikidata",
  ["flottant"] = "flottant",
  ["largeur-tableau"] = "largeur_tableau",
  ["sansdoublescomptes"] = "sansdoublescomptes",
  ["enquêteannuelle"] = "enqueteannuelle",
  ["marge-interlignes"] = "marge_interlignes",
  ["taille-police"] = "taille_police",
  ["hauteur-lignes"] = "hauteur_lignes",
  ["hyperliens-années"] = "hyperliens_annees",
  ["années-fond"] = "annees_fond",
  ["population-fond"] = "population_fond",
  ["notes-fond"] = "notes_fond",
  ["style-notes"] = "style_notes",
  -- pour permettre les paramètres "depuis Lua"
  ["largeur_tableau"] = "largeur_tableau",
  ["enqueteannuelle"] = "enqueteannuelle",
  ["marge_interlignes"] = "marge_interlignes",
  ["taille_police"] = "taille_police",
  ["hauteur_lignes"] = "hauteur_lignes",
  ["hyperliens_annees"] = "hyperliens_annees",
  ["annees_fond"] = "annees_fond",
  ["population_fond"] = "population_fond",
  ["notes_fond"] = "notes_fond",
  ["style_notes"] = "style_notes",
}


-- le nom de la catégorie d'erreur
p.categorie_erreur = "Page avec une erreur d'utilisation du modèle Démographie"

-- le titre par défaut, utilisé en "caption hidden" lorsqu'on ne passe pas de titre à afficher
p.titre_par_defaut = "Évolution démographique"


--[[
  Fonction exportée reprenant le fonctionnement de {{m|Charte de couleur}}

  Fonction devenue inutile (voir Module:Chartes), maintenue pour compatibilité éventuelle
  question : est-il possible de tester un appel à une fonction précise ?
--]]
function p.charte_de_couleur(frame)
    local pframe = frame:getParent()

    -- les deux paramètres
    local nom = mw.ustring.lower(mw.text.trim(pframe.args[1] or ""))
    local code = mw.ustring.lower(mw.text.trim(pframe.args[2] or ""))

    return data.charte_m("geographie", "secondaire", code, "non")
end


--[[
  Insert une catégorie d'erreur
--]]
p.liste_erreurs = {}
p.liste_cats = {}
function p.erreur(message, cle)
    table.insert(p.liste_erreurs, message)
    table.insert(p.liste_cats, cle)
end


--[[
  Fonction de récupération d'un paramètre nommé.
--]]
function p.lit_parametre(nom, pasvide)
    if (type(nom) ~= "string") then
        return nil -- pas un paramètre nommé
    end
    local temp = p.frame.args[nom] or p.pframe.args[nom] -- du modèle, puis de l'article
    if (temp ~= nil) then
        if (pasvide) then
            if (temp == "") then
                return nil
            else
                return temp
            end
        else
            return temp
        end
    else
        return nil
    end
end

local function get_snack_value(t, v, ...)
	if v and type(t) == 'table' then
		return get_snack_value(t[v], ...)
	elseif v then
		return nil
	else
		return t
	end
end

--[[
  Fonction de récupération des données de Wikidata.
--]]
function p.valeur_wikidata(pm, customId)
	local id
	if customId and mw.wikibase.isValidEntityId(customId) then
		id = customId
		if not mw.wikibase.entityExists(id) then
			return
		end
	else
		id = mw.wikibase.getEntityIdForCurrentPage()
		if not id then
			return
		end
	end
	local popProp = mw.wikibase.getAllStatements( id, 'P1082' )
	if not popProp or #popProp == 0 then
		return
	end
	for _, statement in ipairs( popProp ) do
		local pop = get_snack_value(statement, 'mainsnak', 'datavalue', 'value', 'amount')
		local date_table = get_snack_value(statement, 'qualifiers', 'P585', 1, 'datavalue', 'value' )
		if pop and date_table and date_table.precision > 8 and statement.rank ~= 'deprecated' then
			local an = tonumber(date_table.time:match('^%+(%d%d%d%d)'))
			if an and pm[an] == nil then
				pm[an] = pop:match('%d+')
			end
		end
	end
end

--[[
  Supprime le premier retour à la ligne (éventuel) de forme <br/>
--]]
function p.sans_nl(texte)
    if (texte == nil or texte == "" or type(texte) ~= "string") then
        return texte
    end
    -- parenthèses car gsub() retourne plusieurs valeurs, et on veut retourner uniquement la première valeur
    return ( mw.ustring.gsub(texte, "[<][bB][rR][ ]*[/]?[>]", "", 1) )
end

--[[
  Fonction principale
  reçoit une table des paramètres (pm) issus de l'appel
--]]
function p.demographie_m(pm)
    -- valeur marge interlignes
    if (pm.marge_interlignes == nil) then
        pm.marge_interlignes = "5px"
    else
        -- les valeurs trop petites
        if (pm.marge_interlignes == "0" or pm.marge_interlignes == "0em" or pm.marge_interlignes == "0.1em" or pm.marge_interlignes == "0px" or
             pm.marge_interlignes == "1px" or pm.marge_interlignes == "2px" or pm.marge_interlignes == "3px" or pm.marge_interlignes == "4px") then
            pm.marge_interlignes = "5px"
        end
    end

    -- valeur effective du flottant
    local vflottant = 'margin: 0 auto' -- valeur par défaut
    if (pm.flottant and pm.flottant:lower() == "gauche") then
        vflottant = 'float:left; margin: 0 1em 1em 0'
    elseif (pm.flottant and pm.flottant:lower() == "droite") then
        vflottant = 'float:right; margin: 0 0 1em 1em'
    end

    if (pm.hauteur_lignes == nil) then
        pm.hauteur_lignes = ""
    else
        pm.hauteur_lignes = "line-height:" .. pm.hauteur_lignes .. ";"
    end
    pm.taille_police = (pm.taille_police or "100%") -- valeur par défaut taille police
    if (pm.notes_fond ~= nil) then
        pm.notes_fond = "background: " .. pm.notes_fond .. ";"
    else
        pm.notes_fond = ""
    end
    local parenthese = false
    if (pm.style_notes == "gauche") then
        pm.style_notes = 'border: 1px solid #aaa; text-align:left;'
    else
        pm.style_notes = 'border: 0; border-width: 0;'
        parenthese = true
    end

    -- valeur par défaut lien
    if (pm.hyperliens_annees == nil) then
        pm.hyperliens_annees = false
    else
       -- validation valeur
        if (pm.hyperliens_annees == "on" or pm.hyperliens_annees == "oui") then
            pm.hyperliens_annees = true
        else
            pm.hyperliens_annees = false -- toute valeur autre que "on" = "off"
        end
    end
    -- valeurs par défaut des colonnes
    local colonnes_par_defaut
    if (pm.colonnes == nil) then
        colonnes_par_defaut = true
        pm.colonnes = 9
    else
        pm.colonnes = tonumber(pm.colonnes) -- pour que ce soit un nombre
    end
    -- on valide les colonnes
    if (type(pm.colonnes) ~= "number" or pm.colonnes < 1) then
        -- colonne erronée : erreur
        p.erreur("La valeur du paramètre ''colonnes'' (" .. (pm.colonnes or "<pas un nombre>") .. ") n'est pas valide", "nombre de colonnes")
        pm.colonnes = 9
    end
    -- largeur par défaut : 5.4em * colonnes
    local largeur_tableau_par_defaut
    if (pm.largeur_tableau == nil) then
        largeur_tableau_par_defaut = true
        pm.largeur_tableau = pm.colonnes*5.4 .. "em"
    end
    if (pm.charte == nil) then
        pm.charte = "défaut"
    else
        -- on valide la charte
        pm.charte = mw.ustring.lower(pm.charte)
    end
    -- on récupère les couleurs de la charte sauf si indiquées
    local coul_annees = (pm.annees_fond or data.charte_m("geographie", "secondaire", pm.charte , "oui"))
    local coul_valeurs = (pm.population_fond or nil) -- valeur par défaut = rien

    if (coul_valeurs == nil) then
        coul_valeurs = ""
    else
        coul_valeurs = 'style="background:' .. coul_valeurs .. ';"'
    end

    -- obtient les données de wikidata, mais sans écraser les données locales
    -- * définir à une valeur "truly" pour activer (désactivé par défaut)
    -- * définir à un id wikidata pour récupérer les informations depuis une autre entité
    if pm.wikidata ~= nil then
        local testParam = require('Module:Yesno')(pm.wikidata, 'custom_id')
        if testParam == true then
            p.valeur_wikidata(pm)
        elseif testParam == 'custom_id' then
            p.valeur_wikidata(pm, pm.wikidata)
        end
    end

    -- extraction des éléments de la table, rangés dans une "vraie" table pour les trier
    local tbl = {}
    for annee, valeur in pairs(pm) do
        -- il y a aussi les paramètres nommés dans cette table, qu'on laisse
        if type(annee) == "number" then
            if annee == 1 then -- protection : un paramètre non nommé sera à l'index "1"
                p.erreur("Présence d'un paramètre non nommé", "paramètre inconnu")
            else
                -- nettoyage de la valeur
                local v = mw.text.trim(valeur or "")
                table.insert(tbl, {annee, v})
            end
        else
            -- on profite de cette boucle pour vérifier les paramètres qui n'existent pas
            if p.parametres[annee] == nil then
                -- cas particulier : les paramètres sous la forme "XXXX notes" et "XXXX unité" sont acceptés
                if not (annee:match("^[0-9]+ notes$") or annee:match("^[0-9]+ unité$") or annee:match("^[0-9]+ affichage$")) then
                    -- pas un paramètre connu ni XXXX notes → erreur
                    p.erreur("Le paramètre ''>>" .. annee .. "<<'' est inconnu", "paramètre inconnu")
                    -- on ignore simplement ce champs
                end
            end
        end
    end
    -- tri de la table
    table.sort(tbl, function (el1, el2)
        return el1[1] < el2[1]
    end)

    -- cette fois on parcours la structure des infos
    local ret = ""
    local odebug = "" -- sortie de debug

    -- on parcours les données (années) pour générer la table structurée
    local col = 0
    local ligne = 0
    local struct = {}
    local total = 0 -- compte du nombre total
    for pos = 1, #tbl do
        -- colonne d'insertion
        col = col + 1
        -- fin de la ligne ? on retourne à la première colonne
        if (col > pm.colonnes) then
            col = 1
        end
        -- première colonne ? on crée une nouvelle ligne
        if (col == 1) then
            ligne = ligne + 1
            table.insert(struct, {})
        end
        -- on insert dans la ligne
        table.insert(struct[ligne], tbl[pos])
        total = total + 1
    end
    -- aucune entrée ? erreur.
    if (total == 0) then
        p.erreur("Aucune année fournie au modèle", "absence d'années")
    end
    -- on traite la largeur
    if (colonnes_par_defaut == true and ligne == 1 and total < pm.colonnes) then
        pm.colonnes = total  -- restriction du nombre de colonnes au nombre réel d'éléments
        -- il faut aussi recalculer la largeur totale : on fait le rapport entre 9 (ancien) et le nouveau nombre de colonnes
        if (largeur_tableau_par_defaut == true) then
            -- uniquement si l'utilisateur n'a pas fixé la taille
            pm.largeur_tableau = pm.colonnes*5.4 .. "em"
        end
    end

    -- on récupère le "langage" courant pour utiliser formatnum
    local lang = mw.language.getContentLanguage()

    -- on récupère le namespace
    local ttl = mw.title.getCurrentTitle().namespace


    -- création du div principal
    ret = ret .. '<div style="overflow:hidden; width: ' .. pm.largeur_tableau .. '; ' .. vflottant .. '; padding:0 1px; text-align:center; font-family: Liberation Sans, Arial, sans-serif;">'

    -- boucle sur les lignes
    for ligne = 1, #struct do
        local structLigne = struct[ligne]
        -- une ligne à faire, on crée le tableau
        if (ligne == 1) then
            ret = ret .. '<table class="wikitable" style="table-layout:fixed;width:100%;text-align:center;margin-top: 1px; ' .. pm.hauteur_lignes .. 'margin-bottom: 0;font-size:' .. pm.taille_police .. ';">\n'
        else
            ret = ret .. '<table class="wikitable" style="table-layout:fixed;width:100%;text-align:center;margin-top:' .. pm.marge_interlignes .. '; ' .. pm.hauteur_lignes .. 'margin-bottom: 0;font-size:' .. pm.taille_police .. ';">\n'
        end
        -- si titre présent on l'ajoute : visible si 1ère ligne, caché sinon
        if (pm.titre ~= nil) then
            if (ligne == 1) then
                ret = ret .. '<caption style="margin-bottom:' .. pm.marge_interlignes .. ';">' .. pm.titre .. '</caption>'
            else
                ret = ret .. '<caption class="hidden">' .. pm.titre .. ", suite (" .. ligne-1 .. ')</caption>'
            end
        else
            -- titre par défaut, caché
            ret = ret .. '<caption class="hidden">' .. p.titre_par_defaut .. ' (ligne ' .. ligne .. ')</caption>'
        end
        -- parcours des colonnes pour insérer les années
        ret = ret .. "<tr>\n"
        for col = 1, #structLigne do
            local structCol = structLigne[col]
            -- présence de AAAA affichage ?
            local temp = pm[structCol[1] .. " affichage"]
            if (temp ~= nil) then
                -- on affiche l'élément indiqué à la place
                ret = ret .. '<th scope="col" style="background-color: ' .. coul_annees .. ';">' .. temp .. '</th>\n'
            else
                if (pm.hyperliens_annees) then
                    ret = ret .. '<th scope="col" style="background-color: ' .. coul_annees .. ';">[[' .. structCol[1] .. ']]</th>\n'
                else
                    ret = ret .. '<th scope="col" style="background-color: ' .. coul_annees .. ';">' .. structCol[1] .. '</th>\n'
                end
            end
        end
        -- si on n'a pas terminé les colonnes on termine avec du vide
        if #structLigne < pm.colonnes then
            for col = #structLigne + 1, pm.colonnes do
                ret = ret .. '<th scope="col" style="background-color: ' .. coul_annees .. ';">-</th>\n'
            end
        end
        ret = ret .. "</tr>\n"
        -- parcours des colonnes pour insérer les valeurs
        ret = ret .. "<tr>\n"
        for col = 1, #structLigne do
            local structCol = structLigne[col]
            if (structCol[2] == "" or structCol[2] == nil) then
                ret = ret .. '<td ' .. coul_valeurs .. '>-</td>'
            else
                local tmp = ""
                -- on récupère la partie numérique au début
                local pdeb = ""
                do
                    local fStart, fEnd, match = structCol[2]:find("^([0-9]+)")
                    while fEnd do
                        pdeb = pdeb .. match
                        -- dans l'éventualité où la valeur est un nombre déjà formaté
                        fStart, fEnd, match = structCol[2]:find("^(\194\160[0-9]+)", fEnd + 1)
                    end
                end
                if (pdeb ~= "") then
                    -- si le nombre est déjà formaté on l'ajoute tel quel, sinon on lui applique un formatnum
                    -- (attention à ne pas modifier la variable pdeb, pour ne pas modifier sa longueur)
                    if (pdeb:find("\194\160")) then
                        tmp = tmp .. pdeb
                    else
                        tmp = tmp .. lang:formatNum(tonumber(pdeb))
                    end
                end
                -- on ajoute la suite (éventuelle)
                local pfin = structCol[2]:sub(#pdeb + 1)
                if (pfin ~= "") then
                    tmp = tmp .. pfin
                end
                -- si un paramètre "XXXX unité" existe on l'insert
                local unite = pm[structCol[1] .. " unité"]
                if (unite ~= nil) then
                    tmp = tmp .. " " .. unite
                end
                -- si un paramètre "XXXX notes" existe on l'insert en tant que note
                local note = pm[structCol[1] .. " notes"]
                if (note ~= nil) then
                    -- test : on regarde si la note est déjà une ref (pour insérer une espace ou pas avant)
                    local estref = mw.text.unstrip(note)
                    -- si 'estref' == '' la note ne contient qu'un strip marker (ref, pre, gallery). Probablement une ref.
                    if (estref ~= '') then
                        tmp = tmp .. " "
                    end
                    tmp = tmp .. note
                end
                ret = ret .. '<td ' .. coul_valeurs .. '>' .. tmp .. '</td>'
            end
        end
        -- si on n'a pas terminé les colonnes on termine avec du vide
        if #structLigne < pm.colonnes then
            for col = #structLigne + 1, pm.colonnes do
                ret = ret .. '<td ' .. coul_valeurs .. '>-</td>'
            end
        end
        ret = ret .. "</tr>\n"
        -- fermeture table
        ret = ret .. "</table>\n"
    end

    -- si présence d'erreur on l'ajoute aux notes
    local erreurs = nil
    if (p.liste_erreurs[1] ~= nil) then
        erreurs = "<span class=\"error\" style=\"font-size: 0.9em;\">Liste des erreurs :"
        for i = 1, #p.liste_erreurs do
            erreurs = erreurs .. "<br>• " .. p.liste_erreurs[i]
        end
        erreurs = erreurs .. "</span>"
    end


    -- gestion des notes et sources
    if (pm.notes ~= nil or pm.source ~= nil or pm.sources ~= nil or pm.sansdoublescomptes ~= nil or pm.enqueteannuelle ~= nil or erreurs ~= nil) then
        local pred = false
        ret = ret .. '<div style="padding: 0.3em; margin: 6px 0; line-height: 150%; font-size: 0.9em; ' .. pm.notes_fond .. ' ' .. pm.style_notes .. '">'
        -- le double-compte si présent
        if (pm.sansdoublescomptes ~= nil) then
           ret = ret .. "Nombre retenu à partir de [[" .. pm.sansdoublescomptes .. "]] : [[Chiffres de population de la France|population sans doubles comptes]]."
           pred = true
        end
        if (pm.enqueteannuelle ~= nil) then
            if (pred) then
                ret = ret .. "<br/>"
            end
            ret = ret .. "[[" .. pm.enqueteannuelle .. "]] : Population provisoire (enquête annuelle)."
            pred = true
        end
        -- on ajoute les notes si présentes
        if (pm.notes ~= nil) then
            if (pred) then
                ret = ret .. "<br/>"
            end
            -- si présent on retire le saut de ligne
            pm.notes = p.sans_nl(pm.notes)
            ret = ret .. pm.notes
            pred = true
        end
        -- sources si présentes
        if (pm.source ~= nil or pm.sources ~= nil) then
            if (pred) then
                ret = ret .. "<br/>"
            end
            pred = true
            -- si on a source et sources on met tout dans sources
            if (pm.source ~= nil and pm.sources ~= nil) then
                pm.sources = pm.source .. " " .. pm.sources
                pm.source = nil
            end
            if (pm.sources ~= nil) then
                -- si présent on retire le saut de ligne
                pm.sources = p.sans_nl(pm.sources)
            end
            if (pm.source ~= nil) then
                -- si présent on retire le saut de ligne
                pm.source = p.sans_nl(pm.source)
            end
            local tmp
            if (pm.sources ~= nil) then
                tmp = "Sources : " .. pm.sources
            else
                tmp = "Source : " .. pm.source
            end
            if (parenthese) then
                ret = ret .. "(" .. tmp .. ")"
            else
                ret = ret .. tmp
            end
        end

        -- on ajoute les erreurs si présentes
        if (erreurs ~= nil) then
            if (pred) then
                ret = ret .. "<br/>"
            end
            ret = ret .. erreurs
        end
        -- on ferme la div des notes
        ret = ret .. '</div>'
    end

    -- on ferme la div principale
    ret = ret .. "</div>"

    -- si namespace encyclo (ttl = 0) on insert les catégories d'erreur
    if (ttl == 0) then
        for i = 1, #p.liste_cats do
            ret = ret .. "[[Catégorie:" .. p.categorie_erreur .. "|" .. p.liste_cats[i] .. "]]"
        end
    end

    -- on retourne le résultat
    return ret
end

--[[
  Fonction appelable depuis un modèle. Se contente d'appeler demographie_m() qui fait le
    traitement et est également appelable depuis un autre module
--]]
function p.demographie(frame)
    -- pour simplifier on stocke la frame et la pframe
    p.frame = frame
    p.pframe = frame:getParent()

    -- pm est la table des parametres → on lit tous les paramètres référencés
    local pm = {}
    for k, v in pairs(p.parametres) do
        local value = p.lit_parametre(k, true)
        if value then
            pm[v] = value
        end
    end
    -- les paramètres numériques maintenant (les années)
    for k, v in pairs(p.pframe.args) do
        if type(k) == "number" then
            pm[k] = mw.text.trim(v)
        elseif p.parametres[k] == nil and k:sub(1, 1) ~= "_" then
            pm[k] = v
        end
    end
    for k, v in pairs(p.frame.args) do
        if type(k) == "number" then
            pm[k] = mw.text.trim(v)
        elseif p.parametres[k] == nil and k:sub(1, 1) ~= "_" then
            pm[k] = v
        end
    end

    -- on appelle (et on retourne) la fonction principale
    return p.demographie_m(pm)
end


return p -- on retourne le module