Module:Wikidata
From BDI Data
Documentation for this module may be created at Module:Wikidata/doc
--script that retrieves basic data stored in Wikidata, for the datamodel, see https://www.mediawiki.org/wiki/Extension:Wikibase_Client/Lua local p = {} local linguistic = require('Module:Linguistic') --local formatDate = require('Module:Complex date') only loaded when needed to save memory in large pages like Wikidata:List of properties/all local fb = require('Module:Fallback') local i18nmessages = mw.loadData('Module:i18n/wikidata') -- Wiki-specific parameters local defaultlang = mw.getCurrentFrame():preprocess("{{int:lang}}") local defaultlink = 'wikidata' local function i18n(str) local message = i18nmessages[str] if type(message) == 'string' then return message end return fb._langSwitch(message, defaultlang) .. '' end local function formatError( key, text ) return error(i18n(key) .. (text or '')) end local function addTrackingCat(prop, cat) if not prop and not cat then return error("no property provided") end if not cat then cat = i18nmessages.trackingcat .. '/' .. string.upper(prop) end return '[[Category:' .. cat .. ']]' end local function removeBlanks(args) for i, j in pairs(args) do -- does not work ?? if (j == '') or (j == '-') then args[i] = nil end end return args end local function formatTheUnknown() -- voir si on peut accorder/adapter l'usage de "inconnu" return i18n('somevalue') end local function isSpecial(snak) return snak.snaktype ~= 'value' end local function sameValue(snak, target) return not isSpecial(snak) and p.getRawvalue(snak) == target end local function showLang(statement, str) -- TODO (not yet in proper format) --adds a lang indication at the start of the string, based on data in statement local mainsnak = statement.mainsnak if isSpecial(mainsnak) then return str end local langlist = {} if mainsnak.datavalue.type == 'monolingualtext' then langlist = {mainsnak.datavalue.value.language} elseif statement.qualifiers and statement.qualifiers.P407 then local convertlangcode = mw.loadData('Module:Dictionary/lang codes') for i, j in pairs( statement.qualifiers.P407 ) do if not isSpecial(j) then local val = convertlangcode[j.datavalue.value['numeric-id']] table.insert(langlist, val) end end end if #langlist == 0 then return str else return '(' .. table.concat(langlist) .. ')' .. str end end function p.getEntity( val ) if type(val) == 'table' then return val end return mw.wikibase.getEntityObject(val) end -- DATE FUNCTIONS local function splitTimestamp(timestamp, calendar) local pattern = "(%W)(%d+)%-(%d+)%-(%d+)" local era, year, month, day = timestamp:match(pattern) if calendar == 'julian' then --todo year, month, day = formatdate.gregorianToJulian( era .. year, month, day ) end return {day = day, month = month, year = year, era = era, timestamp = timestamp, type = 'dateobject'} end local function rangeObject(begin, ending) local timestamp if begin then timestamp = begin.timestamp elseif ending then timestamp = ending.timestamp end return {begin = begin, ending = ending, timestamp = timestamp, type = 'rangeobject'} end local function dateObject(orig, params) -- transforme un snak en un nouvel objet utilisable par Module:Date complexe if not params then params = {} end local newobj = splitTimestamp(orig.time, orig.calendar) -- initalise l'object en mettant la valeur des dates newobj.precision = params.precision or orig.precision newobj.type = 'dateobject' return newobj end local function formatDatepoint(obj, params) -- TO IMPROVE if not obj then return nil end local formatDate = require('Module:Complex date') local lang = params.lang or defaultlang local precision = math.min(obj.precision, params.precision or 15) -- if we don't want to show the value to its full detail if precision >= 11 then return formatDate.complex_date{args={date1 = obj.year .. '-' .. obj.month .. '-' .. obj.day, lang= lang}} elseif precision == 10 then return formatDate.complex_date{args={date1 = obj.year .. '-' .. obj.month, lang= lang}} elseif precision == 9 then return formatDate.complex_date{args={date1 = tostring(obj.year), lang= lang}} elseif precision == 8 then return formatDate.complex_date{args={date1 = string.sub(tostring(obj.year), 1, 3) .. '0', lang = lang, precision = 'decade'}} elseif precision == 7 then return formatDate.complex_date{args={date1 = string.sub(tostring(obj.year + 100), 1, 2), lang = lang, precision = 'century'}} end return nil end local function formatDaterange(obj, params) --TODO local begin = formatDatepoint(obj.begin, params) or '' local ending = formatDatepoint(obj.ending, params) or '' return begin .. '-' .. ending end local function objectToText(obj, params) if obj.type == 'dateobject' then return formatDatepoint(obj, params) elseif obj.type == 'rangeobject' then return formatDaterange(obj, params) end return nil end local function tableToText(values, params) -- takes a list of already formatted values and make them a text if not values then return nil end return linguistic.conj(values, params.lang or defaultlang, params.conjtype)--linguistic.conj( values, params.lang, params.conjtype ) end function p.getDate(obj) --[[ returns an object containing a timestamp for easy sorting, and other data possible types of object: dateobject {timestamp = string, year = number, month = number, day = number, calendar = string} rangeobject {timestamp = string, begin = dateobject, ending = dateobject} ]]-- if not obj then return nil end if type(obj) == 'string' then obj = p.getEntity(obj) end -- if obj is a statement with date, get it if obj.mainsnak and not isSpecial(obj.mainsnak) and obj.mainsnak.datatype == 'time' then return dateObject(obj.mainsnak.datavalue.value) end -- else preload relevant data local qualifs = obj.qualifiers -- when obj is a statement, look in qualifiers local claims = obj.claims -- when obj is an item, look in claims local pointprop = {'P585', 'P571'} -- dates corresponding to a punctual fact local beginprop = {'P580', 'P569'} -- start date, birth date == start of a date range local endingprop = {'P582', 'P570'} local function getval(prop) local val if claims and claims[prop] and not isSpecial(claims[prop][1].mainsnak) then val = claims[prop][1].mainsnak.datavalue.value elseif qualifs and qualifs[prop] and not isSpecial(qualifs[prop][1]) then val = qualifs[prop][1].datavalue.value end if val then return dateObject(val) end return nil end for i, prop in pairs(pointprop) do local val = getval(prop) if val then return val end end --if no date has not been found, look for startdate or enddate local begin, ending for i, prop in pairs(beginprop) do begin = getval(prop) if begin then break end end for i, prop in pairs(endingprop) do ending = getval(prop) if ending then break end end if begin or ending then return rangeObject(begin, ending) end return nil end function p.getFormattedDate(statement, params) local datetable = p.getDate(statement) if not datetable then return nil end return objectToText(datetable, params) end local function hasTargetValue(claim, target) if target == nil then return true end return sameValue(claim.mainsnak, target) end local function hasRank(claim, target) if target == 'valid' then return hasRank(claim, 'preferred') or hasRank(claim, 'normal') else return claim.rank == target end end local function bestRanked(claims) if not claims then return nil end local preferred, normal = {}, {} for _, j in ipairs(claims) do if j.rank == 'preferred' then table.insert(preferred, j) elseif j.rank == 'normal' then table.insert(normal, j) end end if #preferred > 0 then return preferred else return normal end end local function hasQualifier(claim, qualifier, qualifiervalues) if not qualifier then -- si aucun qualificatif est demandé, ça passe return true end qualifier = string.upper(qualifier) if not claim.qualifiers or not claim.qualifiers[qualifier] then return false end if type(qualifiervalues) == 'string' then qualifiervalues = mw.text.split(qualifiervalues, ',') end if (not qualifiervalues) or (qualifiervalues == {}) then return true -- si aucune valeur spécifique n'est exigée end for _, j in ipairs(claim.qualifiers[qualifier]) do for _, l in ipairs(qualifiervalues) do if p.sameValue(j, l) then return true end end end return false end local function hasSource(statement, source, sourceproperty) if not statement.references then return false end sourceproperty = string.upper(sourceproperty or 'P248') local sourcevalue = string.upper(source or '') for _, ref in ipairs(statement.references) do for prop, content in pairs(ref.snaks) do if prop == sourceproperty then if sourcevalue == '' then return true else for _, k in ipairs(content) do if sameValue(k, source) then return true end end end end end end return false end local function hasDate(statement) if not statement.qualifiers then return false end local dateprops = {'P580', 'P585', 'P582'} for i, prop in pairs(dateprops) do if statement.qualifiers[prop] then return true end end return false end local function isInLanguage(snak, lang) -- ne fonctionne que pour les monolingualtext / étendre aux autres types en utilisant les qualifiers ? return not isSpecial(snak) and snak.datavalue.type == 'monolingualtext' and snak.datavalue.value.language == lang end local function numval(claims, numval) -- retourn les numval premières valeurs de la table claims local numval = tonumber(numval) or 0 -- raise an error if numval is not a positive integer ? if #claims <= numval then return claims end local newclaims = {} while #newclaims < numval do table.insert(newclaims, claims[#newclaims + 1]) end return newclaims end local function wikipediaLink(entity, lang) local link = entity:getSitelink(lang .. 'wiki') if link then return ':' .. lang .. ':' .. link end return nil end local function getLink(entity, typelink, lang) if typelink == 'wikidata' then if type(entity) == 'table' then if entity.type == 'property' then return 'd:P:' .. entity.id else return 'd:' .. entity.id end else if string.sub(entity, 1, 1) == 'P' then return 'd:P:' .. entity else return 'd:' .. entity end end elseif typelink == 'wikipedia' then return wikipediaLink(entity, lang or defaultlang) elseif typelink == 'anywikipedia' then for _, lg in ipairs(fb.fblist(lang or defaultlang, true)) do local link = wikipediaLink(entity, lg) if link then return link end end end return nil end function p.comparedate(a, b) -- returns true if a is earlier than B or if a has a date but not b if a and b then return a.timestamp < b.timestamp elseif a then return true end return false end function p.chronosort(objs, inverted) table.sort(objs, function(a, b) local timeA = p.getDate(a) local timeB = p.getDate(b) if inverted then return p.comparedate(timeB, timeA) else return p.comparedate(timeA, timeB) end end) return objs end function p.sortclaims(claims, sorttype) if type(sorttype) == 'function' then table.sort(claims, sorttype) elseif sorttype == 'chronological' then return p.chronosort(claims) elseif sorttype == 'inverted' then return p.chronosort(claims, true) end return claims end function p.getRawvalue(snak) return p.getDatavalue(snak, { displayformat = 'raw' }) end function p.showentity(entity, lang) if not entity then return nil end local label, link, id = p._getLabel(entity, lang), getLink(entity, 'wikidata') if type(entity) == 'table' then id = entity.id else id = entity end return '[[' .. link .. '|' .. label .. ']] <small>(' .. id .. ')</small>' end function p.getDatavalue(snak, params) if isSpecial(snak) then return nil end if not params then params = {} end local displayformat = params.displayformat local datatype = snak.datavalue.type local value = snak.datavalue.value if datatype == 'wikibase-entityid' then if type(displayformat) == 'function' then return displayformat(snak, params) end local id = snak.datavalue.value.id if displayformat == 'raw' then return id elseif displayformat == 'wikidatastyle' then return p.showentity(id, params.lang) else return p.formatEntity(id, params) end elseif datatype == 'string' then local showntext = params.showntext if displayformat == 'weblink' then if showntext then return '[' .. value .. ' ' .. showntext .. ']' else return value end end if snak.datatype == 'math' and displayformat ~= 'raw' then value = mw.getCurrentFrame():extensionTag('math', value) else if params.urlpattern then showntext = mw.text.nowiki(showntext or value) value = mw.ustring.gsub(value, '%%', '%%%%') -- escape '%' value = '[' .. mw.ustring.gsub(mw.ustring.gsub(params.urlpattern, '$1', value), ' ', '%%20') .. ' ' .. showntext .. ']' elseif params.pattern then local pattern = mw.ustring.gsub(params.pattern, '%%', '%%%%') value = mw.ustring.gsub(value, '%%', '%%%%') value = mw.ustring.gsub(pattern, '$1', value) else if displayformat ~= 'raw' then value = mw.text.nowiki(value) end end end return value elseif datatype == 'time' then -- format example: +00000001809-02-12T00:00:00Z if displayformat == 'raw' then return value.time else return objectToText(dateObject(value), params) end elseif datatype == 'globecoordinate' then -- retourne une table avec clés latitude, longitude, précision et globe à formater par un autre module (à changer ?) if displayformat == 'latitude' then return value.latitude elseif displayformat == 'longitude' then return value.longitude elseif displayformat == 'qualifier' then local coord = require 'Module:Coordinates' value.globe = mw.loadData('Module:Wikidata/Globes')[value.globe] value.precision = nil return coord._coord(value) else value.globe = mw.loadData('Module:Wikidata/Globes')[value.globe] -- transforme l'ID du globe en nom anglais utilisable par geohack return value -- note : les coordonnées Wikidata peuvent être utilisée depuis Module:Coordinates. Faut-il aussi autoriser à appeler Module:Coordiantes ici ? end elseif datatype == 'quantity' then -- todo : gérer les paramètre précision if displayformat == 'raw' then return tonumber(value.amount) else local formatNum = require 'Module:Formatnum' local number = formatNum.formatNum(value.amount, params.lang) local unit = mw.ustring.match(value.unit, '(Q%d+)') if unit then number = number .. ' ' .. p.formatEntity(unit, params) end return number end elseif datatype == 'monolingualtext' then return '<span lang="' .. value.language .. '">' .. value.text .. '</span>' else return formatError( 'unknown-datavalue-type', datatype ) end end local function getMultipleClaims(args) local newargs = args local claims = {} for i, j in pairs(args.property) do newargs.property = j local newclaims = p.getClaims(args) if newclaims then for k, l in pairs(newclaims) do table.insert(claims, l) end end end return claims end function p.getClaims( args ) -- returns a table of the claims matching some conditions given in args args = removeBlanks(args) if not args.property then return formatError( 'property-param-not-provided' ) end if type(args.property) == 'table' then return getMultipleClaims(args) end --Get entity if args.item then -- synonyms args.entity = args.item end local entity = p.getEntity(args.entity) local property = string.upper(args.property) if not entity or not entity.claims or not entity.claims[property] then return nil end if not args.rank then args.rank = 'best' end local claims = {} -- ~= '' lorsque le paramètre est écrit mais laissé blanc dans une fonction frame for i, statement in pairs(entity.claims[property]) do if ( not args.excludespecial or not (isSpecial(statement.mainsnak)) ) and ( not args.targetvalue or hasTargetValue(statement, args.targetvalue) ) and ( not args.qualifier or hasQualifier(statement, args.qualifier, args.qualifiervalues or args.qualifiervalue) ) and ( not args.withsource or args.withsource == '-' or hasSource(statement, args.withsource, args.sourceproperty) ) and ( not args.isinlanguage or isInLanguage(statement.mainsnak, args.isinlanguage) ) and ( args.rank == 'best' -- rank == best est traité à a fin or hasRank(statement, args.rank) ) then table.insert(claims, statement) end end if #claims == 0 then return nil end if args.rank == 'best' then claims = bestRanked(claims) end if args.sorttype then claims = p.sortclaims(claims, args.sorttype) end if args.numval then return numval(claims, args.numval) end return claims end function p.formatClaimList(claims, args) if not claims then return nil end for i, j in pairs(claims) do claims[i] = p.formatStatement(j, args) end return claims end function p.stringTable(args) -- like getClaims, but get a list of string rather than a list of snaks, for easier manipulation local claims = p.getClaims(args) return p.formatClaimList(claims, args) end local function getQualifiers(statement, qualifs, params) if not statement.qualifiers then return nil end local vals = {} for i, j in pairs(qualifs) do j = string.upper(j) if statement.qualifiers[j] then local inserted = false if statement.qualifiers[j][1].datatype == 'monolingualtext' then local in_preferred_lang for _, language in ipairs(fb.fblist(params.lang or defaultlang, true)) do for _, snak in ipairs(statement.qualifiers[j]) do if isInLanguage(snak, language) then in_preferred_lang = snak break end end if in_preferred_lang then break end end if in_preferred_lang then table.insert(vals, in_preferred_lang) inserted = true end end if not inserted then for _, snak in pairs(statement.qualifiers[j]) do table.insert(vals, snak) end end end end if #vals == 0 then return nil end return vals end function p.getFormattedQualifiers(statement, qualifs, params) if not params then params = {} end local qualiftable = getQualifiers(statement, qualifs, params) if not qualiftable then return nil end for i, j in pairs(qualiftable) do local params = params if j.datatype == 'globe-coordinate' then params.displayformat = 'qualifier' end qualiftable[i] = p.formatSnak(j, params) end return linguistic.conj(qualiftable, params.lang or defaultlang) end function p.formatStatement( statement, args ) if not statement.type or statement.type ~= 'statement' then return formatError( 'unknown-claim-type', statement.type ) end if not args then args = {} end local lang = args.lang or defaultlang local str = p.formatSnak( statement.mainsnak, args ) if args.showlang == true then str = showLang(statement, str) end local qualifs = args.showqualifiers if qualifs then if type(qualifs) == 'string' then qualifs = mw.text.split(qualifs, ',') end local foundvalues = p.getFormattedQualifiers(statement, qualifs, args) if foundvalues then if args.delimiter then str = str .. args.delimiter .. foundvalues else str = str .. linguistic.inparentheses(foundvalues, lang) end end end if args.showdate then -- when "showdate and p.chronosort are both set, date retrieval is performed twice local timedata = p.getDate(statement) if timedata then local formatteddate = objectToText(timedata, args) formatteddate = linguistic.inparentheses(formatteddate, lang) str = str .. '<small>' .. formatteddate ..'</small>' end end if args.showsource and statement.references then local cite = require 'Module:Cite' local frame = mw.getCurrentFrame() local sourcestring = '' for _, ref in ipairs(statement.references) do if ref.snaks.P248 then for j, source in pairs(ref.snaks.P248) do if not isSpecial(source) then local page if ref.snaks.P304 and not isSpecial(ref.snaks.P304[1]) then page = ref.snaks.P304[1].datavalue.value end local s = cite.citeitem(source.datavalue.value.id, lang, page) s = frame:extensionTag( 'ref', s ) sourcestring = sourcestring .. s end end elseif ref.snaks.P854 and not isSpecial(ref.snaks.P854[1]) then s = frame:extensionTag( 'ref', p.getDatavalue(ref.snaks.P854[1]) ) sourcestring = sourcestring .. s end end str = str .. sourcestring end return str end function p.getmainid(claim) if claim and not isSpecial(claim.mainsnak) then return claim.mainsnak.datavalue.value.id end return nil end function p.formatSnak(snak, params) --local params = params or {} pour faciliter l'appel depuis d'autres modules if snak.snaktype == 'value' then return p.getDatavalue(snak, params) elseif snak.snaktype == 'somevalue' then return formatTheUnknown() elseif snak.snaktype == 'novalue' then return i18n('novalue') --todo else return formatError( 'unknown-snak-type', snak.snaktype ) end end local function defaultLabel(entity, displayformat) -- label when no label is available if displayformat == 'id' then if type(entity) ~= 'table' then return entity else return entity.id end end return i18n('no-label') end function p._getLabel(entity, lang, default, fallback) if not entity then return nil end if not lang then lang = defaultlang end if type(entity) ~= 'table' and lang == defaultlang then local label, lg = mw.wikibase.getLabelWithLang(entity) if label and (fallback ~= '-' or lg == lang) then return label end else entity = p.getEntity(entity) if entity and entity.labels then if fallback ~= '-' then for _, lg in ipairs(fb.fblist(lang, true)) do if entity.labels[lg] then return entity.labels[lg].value end end else if entity.labels[lang] then return entity.labels[lang].value end end end end return defaultLabel(entity, default) end function p._getDescription(entity, lang, fallback) if not entity then return i18n('no description') end if not lang then lang = defaultlang end if type(entity) ~= 'table' and lang == defaultlang then local description, lg = mw.wikibase.getDescriptionWithLang(entity) if description and (fallback ~= '-' or lg == lang) then return description end else entity = p.getEntity(entity) if entity and entity.descriptions then if fallback ~= '-' then for _, lg in ipairs(fb.fblist(lang, true)) do if entity.descriptions[lg] then return entity.descriptions[lg].value end end else if entity.descriptions[lang] then return entity.descriptions[lang].value end end end end return i18n('no description') end local function formattedLabel(label, entity, args) if not args then args = {} end local link = getLink(entity, args.link, args.lang) if not link then link = getLink(entity, defaultlink, args.lang) end if not link then return label else return '[[' .. link .. '|' .. label .. ']]' end end function p.formatEntity( entity, args ) if not entity then return nil end if not args then args = {} end local label = p._getLabel(entity, args.lang, 'id', args.fallback) return formattedLabel(label, entity, args) end function p.getLabel(frame) -- simple for simple templates like {{Q|}}} local args = frame.args local entity = args.entity local lang = args.lang if not entity then return i18n('invalid-id') end if string.sub(entity, 1, 10) == 'Property:P' then entity = string.sub(entity, 10) elseif (string.sub(entity, 1, 1) ~= 'P' and string.sub(entity, 1, 1) ~= 'Q') or (not tonumber(string.sub(entity, 2))) then return i18n('invalid-id') end if not args.link or args.link == '' or args.link == '-' then -- by default: no link if lang == '' then lang = defaultlang end return p._getLabel(entity, lang, args.default, args.fallback) else return p.formatEntity(entity, args) end end function p._formatStatements( args )--Format statements and concat them cleanly if args.value == '-' then return nil end --If a value is already set, use it if args.value and args.value ~= '' then return args.value end local valuetable = p.stringTable(args) return tableToText(valuetable, args) end function p.showQualifier( args ) local qualifs = args.qualifiers or args.qualifier if type(qualifs) == 'string' then qualifs = mw.text.split(qualifs, ',') end if not qualifs then return formatError( 'property-param-not-provided' ) end local claims = p.getClaims(args) if not claims then return nil end local str = '' for i, j in pairs(claims) do local new = p.getFormattedQualifiers(j, qualifs, args) or '' str = str .. new end return str end function p._formatAndCat(args) local val = p._formatStatements(args) if val then return val .. addTrackingCat(args.property) end return nil end function p.getTheDate(args) local claims = p.getClaims(args) if not claims then return nil end local formattedvalues = {} for i, j in pairs(claims) do table.insert(formattedvalues, p.getFormattedDate(j)) end local val = linguistic.conj(formattedvalues) if val and args.addcat == true then return val .. addTrackingCat(args.property) else return val end end ---FONCTIONS depuis le FRAME function p.getaDate(frame) return p.getTheDate(frame.args) end function p.getQualifier(frame) return p.showQualifier(frame.args) end function p.getDescription(frame) -- simple for simple templates like {{Q|}}} local entity = frame.args.entity if not entity then return i18n('invalid-id') end local lang = frame.args.lang local fallback = frame.args.fallback return p._getDescription(entity, lang, fallback) end function p.formatStatements( args ) return p._formatStatements( args ) end function p.formatStatementsE(frame) local args = {} if frame == mw.getCurrentFrame() then args = frame:getParent().args -- paramètres du modèle appelant (est-ce vraiment une bonne idée ?) for k, v in pairs(frame.args) do args[k] = v end else args = frame end return p._formatStatements( args ) end function p.formatAndCat(frame) local args = {} if frame == mw.getCurrentFrame() then args = frame:getParent().args -- paramètres du modèle appelant (est-ce vraiment une bonne idée ?) for k, v in pairs(frame.args) do args[k] = v end else args = frame end return p._formatAndCat( args ) end function p.getEntityFromId(id) return p.getEntity(id) end return p