Module:Wikidata

--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("") 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 '' 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 or #values == 0 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 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 if type(entity) == 'table' then link = entity:getSitelink(lang .. 'wiki') else link = mw.wikibase.getSitelink(entity, lang .. 'wiki') end 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			elseif entity.type == 'lexeme' then return 'd:L:' .. entity.id			else return 'd:' .. entity.id			end else if string.sub(entity, 1, 1) == 'P' then return 'd:P:' .. entity elseif string.sub(entity, 1, 1) == 'L' then return 'd:L:' .. 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 '' .. label .. ' (' .. id .. ') ' end

function p.getDatavalue(snak, params) if isSpecial(snak) then return nil end

if not params then params = {} end

local displayformat = params.displayformat local valuetype = snak.datavalue.type local value = snak.datavalue.value

if valuetype == '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 valuetype == 'string' then local showntext = params.showntext if displayformat == 'weblink' then if showntext then return '[' .. value .. ' ' .. showntext .. ']'			else return value end end if ({['math'] = 1, ['musical-notation'] = 1})[snak.datatype] == 1 and displayformat ~= 'raw' then value = mw.wikibase.formatValue(snak) 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 valuetype == 'time' then -- format example: +00000001809-02-12T00:00:00Z if displayformat == 'raw' then return value.time else return objectToText(dateObject(value), params) end

elseif valuetype == '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 valuetype == '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 valuetype == 'monolingualtext' then return ' ' .. value.text .. ' '	else return formatError( 'unknown-datavalue-type', valuetype ) 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 property = string.upper(args.property) local allClaims local entity = args.entity if type(entity) == 'table' then allClaims = (entity and entity.claims and entity.claims[property]) or {} else allClaims = mw.wikibase.getAllStatements(entity, property) end if #allClaims == 0 then return nil end

if not args.rank then args.rank = 'best' end local claims = {} for _, statement in ipairs(allClaims) 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, params.conjtype) 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 .. ' ' .. formatteddate ..' '		end	end

if args.showsource and statement.references then local cite = require 'Module:Cite' local frame = mw.getCurrentFrame local sourcestring = '' local s		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 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) 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  .. 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 } 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, 8) == 'Lexeme:L' then entity = string.sub(entity, 8) elseif not ({L = 1, P = 1, Q = 1})[string.sub(entity, 1, 1)] 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 = '' local new for _, cl in ipairs(claims) do		new = p.getFormattedQualifiers(cl, 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 _, cl in ipairs(claims) do		table.insert(formattedvalues, p.getFormattedDate(cl)) 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 } 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

-- https://www.wikidata.org/w/index.php?title=Wikidata:Project_chat&oldid=1439583623#Check_if_redirect -- Not applicable outside Wikidata function p.isRedirect(frame) return mw.title.new(frame.args[1]).isRedirect end

return p