Aller au contenu

Module:Utilisateur:Maëlan/wd-langue

Une page de Wikipédia, l'encyclopédie libre.

 Documentation[créer] [purger]
-- 2024-06-05
-- un module pour obtenir le nombre de locuteurs de langues depuis Wikidata.

--------------------------------------------------------------------------------

-- les bibliothèques dont on dépend:
Args = require("Module:Arguments")
Wd = require("Module:Wikidata")
FmtNum = require("Module:Conversion")

-- notre module:
local M = {}

-- valeurs configurables:
local arrondi_par_defaut = 2
local inconnu = '<abbr title="nombre inconnu de locuteurs">?</abbr>'

-- arrondit la valeur donnée au nombre de chiffres significatifs demandé
-- (0 signifie pas d’arrondi):
local function arrondir(x, nb_chiffres_significatifs)
  if x == 0 or nb_chiffres_significatifs <= 0 then
    return x
  else
    local p10 = 10 ^ (math.floor(math.log10(math.abs(x))) + 1 - nb_chiffres_significatifs)
    return p10 * math.floor(x / p10 + 0.5)
  end
end

-- génère un message d’erreur en wikicode:
local function erreur(msg)
  local txt = '<strong class="error">Usage erroné de {{nb locuteurs}} : '
  txt = txt .. msg
  txt = txt .. '</strong>'
  return txt
end

-- enveloppe de la fonction `Wd.filterClaims` qui corrige deux choses:
--   (1) ne modifie pas le tableau d’origine, renvoie un nouveau tableau;
--   (2) renvoie un tableau vide plutôt que `nil`.
local function filterClaims(list_of_claims, params)
  local copied_list_of_claims = { unpack(list_of_claims) }
  return Wd.filterClaims(copied_list_of_claims, params) or { }
end

-- fonction principale:
function M._nb_locuteurs(args)

  -- paramètre obligatoire 1:
  -- détermine le type de locuteurs (L1 | L2 | L1+L2):
  local L1 = false -- langue maternelle
  local L2 = false -- langue seconde
  local LE = false -- langue étrangère
  if args[1] == "L1" then
    L1 = true
  elseif args[1] == "L2" then
    L2 = true
  elseif args[1] == "LE" then
    LE = true
  elseif args[1] == "L1+L2" then
    L1 = true
    L2 = true
  elseif args[1] == "L1+L2+LE" then
    L1 = true
    L2 = true
    LE = true
  else
    return erreur("le type de locuteurs doit être L1, L2, LE, L1+L2 ou L1+L2+LE")
  end

  -- paramètre optionnel «arrondi»:
  -- détermine le nombre de chiffres significatifs;
  -- 0 signifie pas d’arrondi:
  local arrondi = nil
  if args.arrondi ~= nil then
    arrondi = tonumber(args.arrondi)
  end
  if arrondi == nil or arrondi < 0 then
    arrondi = arrondi_par_defaut
  end

  -- on va sommer le nombre de locuteurs de toutes les langues demandées:
  local somme = 0

  -- paramètre 2:
  -- liste de langues à additionner, séparées par "+";
  -- les langues, en tant qu’entités Wikidata, sont identifiées par le titre de
  -- leur page associée dans un certain wiki (ne tolère pas les redirections),
  -- par exemple:
  --     "enwiki:French language"
  --     "frwiki:français"
  -- en l’absence d’indication de wiki, c’est le wiki courant qui est utilisé
  -- (c’est-à-dire ici la Wikipédia francophone).

  -- cas limite où le paramètre n’est pas fourni:
  -- on renvoie 0 (somme de zéro langue) plutôt qu’une erreur:
  if args[2] == nil or args[2] == "" then
    return "0"
  end

  -- pour chaque langue demandée…
  for titre_langue in mw.text.gsplit(args[2], "+", --[[plain=]]true) do
    titre_langue = mw.text.trim(titre_langue)

    -- obtient son identifiant Wikidata à partir de son titre wiki associé:
    local id_entite = nil
    local i = mw.ustring.find(titre_langue, ":", --[[init=]]1, --[[plain=]]true)
    if i ~= nil then -- wiki spécifié
      local site = mw.ustring.sub(titre_langue, 1, i-1)
      local titre_langue_dans_site = mw.ustring.sub(titre_langue, i+1)
      id_entite = mw.wikibase.getEntityIdForTitle(titre_langue_dans_site, site)
    else -- wiki par défaut
      id_entite = mw.wikibase.getEntityIdForTitle(titre_langue)
    end
    if id_entite == nil then
      return erreur("langue inconnue : [[" .. titre_langue .. "]]")
    end

    -- télécharge toutes les affirmations de cette entité
    -- à propos du nombre de locuteurs (propriété P1098):
    local toutes_affirmations_P1098 = mw.wikibase.getAllStatements(id_entite, "P1098")
    -- s’il n’y a aucune telle affirmation, on obtient une liste vide;
    -- inutile de traiter ce cas ici, il est pris en charge par ce qui suit.
    --if #toutes_affirmations_P1098 == 0 then ... end

    -- Dans la suite, on suppose qu’une affirmation de nombre de locuteurs
    -- sans qualificatif «langue maternelle/seconde/étrangère» dans Wikidata
    -- correspond à un nombre de locuteurs L1 (plutôt que L1+L2).

    -- obtient le nombre de locuteurs L1 si nécessaire:
    local locuteurs_L1 = nil
    if L1 then
      -- choisit la meilleure affirmation portant le qualificatif «langue maternelle»:
      local affirmations_L1 = filterClaims(toutes_affirmations_P1098, {
          -- seulement «langue maternelle»:
          qualifier="P518",  qualifiervalue="Q36870",
          -- exclut les affirmations «hypothétiques»:
          excludequalifier="P1480",  excludequalifiervalue="Q18603603",
          -- seulement les affirmations «préférées»; à défaut, les «normales»:
          rank="best",
          -- s’il reste plusieurs affirmations, ne garde que la plus récente:
          sorttype="inverted",  numval=1,
        })
      -- si aucune affirmation ne correspond,
      -- recommence sans le critère «langue maternelle»
      -- (une valeur est parfois fournie sans être qualifiée comme telle):
      if #affirmations_L1 == 0 then
        affirmations_L1 = filterClaims(toutes_affirmations_P1098, {
            excludequalifier="P1480",  excludequalifiervalue="Q18603603",
            rank="best",  sorttype="inverted",  numval=1,
          })
        -- s’il n’y a vraiment rien, renvoie un nombre inconnu de locuteurs
        -- (inutile de traiter les autres langues, la somme serait faussée)
        if #affirmations_L1 == 0 then
          return inconnu
        end
      end
      -- transforme l’affirmation Wikidata en nombre brut:
      locuteurs_L1 = Wd.formatStatement(affirmations_L1[1], { displayformat="raw" })
      locuteurs_L1 = tonumber(locuteurs_L1)
    end

    -- obtient le nombre de locuteurs L2 si nécessaire:
    local locuteurs_L2 = nil
    if L2 then
      -- choisit la meilleure affirmation portant le qualificatif «langue seconde»:
      local affirmations_L2 = filterClaims(toutes_affirmations_P1098, {
          -- seulement «langue seconde»:
          qualifier="P518",  qualifiervalue="Q125421",
          excludequalifier="P1480",  excludequalifiervalue="Q18603603",
          rank="best",  sorttype="inverted",  numval=1,
        })
      -- si aucune affirmation ne correspond,
      -- considère que le nombre de locuteurs L2 est zéro (plutôt qu’inconnu):
      if #affirmations_L2 == 0 then
        locuteurs_L2 = 0
      else
        -- transforme l’affirmation Wikidata en nombre brut:
        locuteurs_L2 = Wd.formatStatement(affirmations_L2[1], { displayformat="raw", default=0 })
        locuteurs_L2 = tonumber(locuteurs_L2)
      end
    end

    -- obtient le nombre de locuteurs LE si nécessaire:
    local locuteurs_LE = nil
    if LE then
      -- choisit la meilleure affirmation portant le qualificatif «langue seconde»:
      local affirmations_LE = filterClaims(toutes_affirmations_P1098, {
          -- seulement «langue étrangère»:
          qualifier="P518",  qualifiervalue="Q150352",
          excludequalifier="P1480",  excludequalifiervalue="Q18603603",
          rank="best",  sorttype="inverted",  numval=1,
        })
      -- si aucune affirmation ne correspond,
      -- renvoie un nombre inconnu de locuteurs
      -- (inutile de traiter les autres langues, la somme serait faussée)
      if #affirmations_LE == 0 then
        return inconnu
      else
        -- transforme l’affirmation Wikidata en nombre brut:
        locuteurs_LE = Wd.formatStatement(affirmations_LE[1], { displayformat="raw" })
        locuteurs_LE = tonumber(locuteurs_LE)
      end
    end

    -- ajoute tous les nombres:
    if locuteurs_L1 ~= nil then
      somme = somme + locuteurs_L1
    end
    if locuteurs_L2 ~= nil then
      somme = somme + locuteurs_L2
    end
    if locuteurs_LE ~= nil then
      somme = somme + locuteurs_LE
    end

  end -- /boucle sur chaque langue

  -- arrondit la valeur au nombre de chiffres significatifs demandé,
  -- la formate joliment en HTML et la renvoie:
  --
  -- NOTE: `FmtNum.displayvalue` présente 3 bugs :
  --   (1) la fonctionnalité qui abrège les nombres au moyen des suffixes
  --       "million (M), milliard (G)" arrondit à 1 chiffre après la virgule
  --       (donc à 100_000 près dans le cas de "million"
  --       et à 100_millions près dans le cas de "milliard")
  --       même si on a demandé une précision + grande via le paramètre `rounding`
  --       => tant pis, il faudrait recoder la fonction…
  --   (2) si on utilise sa fonctionnalité d’arrondi (paramètre `rounding`),
  --       alors le résultat n’est pas abrégé en "million, milliard"
  --       => on fait l’arrondi nous-même
  local somme_arrondie = arrondir(somme, arrondi)
  --   (3) si le nombre est exactement 10^6 ou 10^9,
  --       alors le résultat n’est pas abrégé en "million, milliard"
  --       => on contourne le problème en faisant +1 (qui disparaitra avec l’arrondi, cf (1))
  if somme_arrondie == 1000000 or somme_arrondie == 1000000000 then
    somme_arrondie = somme_arrondie + 1
  end
  -- formate le nombre lisiblement pour un humain:
  local txt = FmtNum.displayvalue(somme_arrondie, "", {
      --rounding=arrondi-1-math.floor(math.log10(somme)),
      showunit="short",
    })
  -- utilise des espaces insécables fines pour séparer les milliers
  -- (remplace les espaces insécables non-fines):
  txt = mw.ustring.gsub(txt, " ", "&#x202F;") -- ici le motif est une NBSP
  txt = mw.ustring.gsub(txt, "&nbsp;", "&#x202F;")
  --txt = mw.ustring.gsub(txt, "([0-9]) %f[0-9]", "%1&#x202F;") -- ici le motif est une espace normale entourée de chiffres
  -- ajoute une clé de tri (requis pour un tri correct dans un tableau triable):
  txt = '<span data-sort-value="' .. tostring(somme) .. '">' .. txt .. '</span>'
  return txt
end

-- fonction d’entrée, invoquée par [[Modèle:nb locuteurs]]:
function M.nb_locuteurs(frame)
  return M._nb_locuteurs(Args.getArgs(frame))
end

-- publication de notre module:
return M

--------------------------------------------------------------------------------

-- OPTIONS UTILES DU [[Module:Wikidata]]:
--     entity=....
--     property="P1098" -- nombre de locuteurs
--   filtrage:
--     qualifier="P518",  qualifiervalue="Q36870"  -- seulement langue maternelle
--     qualifier="P518",  qualifiervalue="Q125421" -- seulement langue seconde
--     qualifier="P518",  qualifiervalue="Q150352" -- seulement langue étrangère
--     excludequalifier="P1480",  excludequalifiervalue="Q18912752,Q18603603"
--       --^ exclut les assertions resp. «controversées» ou «hypothétiques»,
--       --  ce dernier qualificatif indiquant une projection dans le futur
--     sorttype="inverted" -- ou "chronological"
--       --^ tri des valeurs par ordre chrono décroissant ou croissant
--     numval=1 -- au plus une seule valeur
--   formatage:
--     default=0
--     displayformat="raw"   -- nombre brut
--     rounding=N-1-math.floor(math.log10(X))
--       --^ arrondit X à N chiffres significatifs (option du [[Module:Conversion]])
--     showunit="short"      -- abréger "million" en "M" (option du [[Module:Conversion]])
--     showqualifiers="P518" -- indicateur (langue maternelle/seconde/étrangère)
--     showdate="yes"

-- WIKIDATA CHEATSHEET:
--[[
  -- 1: find the entity id (such as "Q150") associated to a given wikipedia page:
  local entity_id = mw.wikibase.getEntityIdForTitle("French Language", "enwiki")
      -- => returns nil if not found

  -- 2: fetch this entity’s data (that’s the costly step):
  local entity = mw.wikibase.getEntity(entity_id) -- Wd.getEntity(entity_id) is just a proxy

  -- 3: select property P1098 (number of speakers):
  local all_claims_about_P1098 = entity.claims.P1098
      -- => fails if no such claim

  -- shortcut for 2+3:
  local all_claims_about_P1098 = mw.wikibase.getAllStatements(entity_id, "p1098")
      -- => returns empty array if no such claim

  -- 4: filter claims by “preferred” and such:
  local filtered_claims = Wd.filterClaims(all_claims_about_P1098, { })
      --^ insert additional filtering options between {}
      -- => returns nil(!) if there is no claim left
      --    also, beware that it modifies the array of claims in place(!)

  -- shortcut for 2+3+4, but does not allow additional filtering options:
  local best_claims = mw.wikibase.getBestStatements(entity_id, "P1098")

  -- 5: format every claim as wikicode text:
  for _, claim in pairs(filtered_claims) do
    print(Wd.formatStatement(claim, { })) --< insert additional formatting options between {}
  end

  -- shortcut for 3+4+5: filter, format and concatenate claims,
  -- starting from the whole entity’s data:
  print(Wd.formatStatements({ entity=entity, property="P1098" }))
      --^ insert additional filtering and formatting options between {}
      -- => returns nil if no such claim
--]]

-- BASIC LUA SYNTAX REMAINDER:
--[[
  -- table (array/dictionary):
  local my_dict = {
    ["key1"] = { "", "x", "y" },
    ["keya"] = { aa="", bb="z" },
  }
  -- `mydict.key` is sugar for `mydict["key"]`

  -- debugging tip: print the structure of any object
  -- (arbitrary nest of Lua “tables“):
  print(mw.logObject(my_object))

  -- the table datastructure is used to encode both arrays and dictionaries;
  -- arrays are indexed by successive integers starting at 1 (!! not 0 !!).

  -- get the length of the array portion of table:
  -- (i.e. the initial integer-indexed segment of a given table):
  print(#my_dict)

  -- remove the 1st element of an array (shifts the following elements by 1):
  table.remove(args, 1)

  -- loop over a table (all keys):
  local my_list = {}
  for my_key, _ in pairs(my_dict) do
    table.insert(my_list, my_dict)
  end

  -- loop over the array portion of a table:
  for i, my_elt in ipairs(my_list) do
    print(i, my_elt)
  end

  -- conditions:
  if not args[2] == nil then ... end
  if args[2] ~= nil then ... end

  -- substring:
  my_sub = my_strA:sub(1, -#my_strB-1) -- or rather use mw.ustring.sub() for correct UTF-8 support
--]]