Modulo:Wikidata
Dokumentado por ĉi tiu modulo povas esti kreata ĉe Modulo:Wikidata/dokumentado
-- 2016-02-04
-- module local variables
local wiki =
{
langcode = mw.language.getContentLanguage().code
}
-- internationalisation
local i18n = {
["errors"] = {
["property-not-found"] = "Trajto ne trovita.",
["entity-not-found"] = "Wikidata-ero ne trovita.",
["unknown-claim-type"] = "Nekonata tipo de deklaro.",
["unknown-entity-type"] = "Nekonata tipo de ento.",
["qualifier-not-found"] = "Kvalifikatoro ne trovita.",
["site-not-found"] = "Wikimedia-projekto ne trovita.",
["image-not-found"] = "Bildo ne difinita en P18 de Vikidatumoj",
},
["datetime"] =
{
-- $1 is a placeholder for the actual number
[0] = "$1 Mrd. da jaroj", -- precision: billion years
[1] = "$100 Mio. da jaroj", -- precision: hundred million years
[2] = "$10 Mio. da jaroj", -- precision: ten million years
[3] = "$1 Mio. da jaroj", -- precision: million years
[4] = "$100.000 jaroj", -- precision: hundred thousand years
[5] = "$10.000 jaroj", -- precision: ten thousand years
[6] = "$1-a jarmilo", -- precision: millenium
[7] = "$1-a jarcento", -- precision: century
[8] = "$1-aj", -- precision: decade
-- the following use the format of #time parser function
[9] = "Y", -- precision: year,
[10] = "F Y", -- precision: month
[11] = 'j"-a de" F Y', -- precision: day
[12] = 'j"-a de" F Y, G "horo"', -- precision: hour
[13] = 'j"-a de" F Y G:i', -- precision: minute
[14] = 'j"-a de" F Y G:i:s', -- precision: second
["beforenow"] = "antaŭ $1", -- how to format negative numbers for precisions 0 to 5
["afternow"] = "en $1", -- how to format positive numbers for precisions 0 to 5
["bc"] = '$1 "a.Kr."', -- how print negative years
["ad"] = "$1" -- how print positive years
},
["monolingualtext"] = '<span lang="%language">%text</span>',
["FETCH_WIKIDATA"] = "ABFRAGE_WIKIDATA"
}
--important properties
local propertyId = {
["starttime"] = "P580",
["endtime"] = "P582"
}
local formatchar = {
[10] = {"n","m","M","F","xg"}, --precision: month
[11] = {"W","j","d","z","D","l","N","w"}, --precision: day
[12] = {"a","A","g","h","G","H"}, --precision: hour
[13] = {"i"}, --precision: minute
[14] = {"s","U"} --precision: second
}
local p = { }
local function printError(code)
return '<span class="error">' .. (i18n.errors[code] or code) .. '</span>'
end
-- exported version of the function
function p.printError(code)
return printError(code)
end
-- the "qualifiers" and "snaks" field have a respective "qualifiers-order" and "snaks-order" field
-- use these as the second parameter and this function instead of the built-in "pairs" function
-- to iterate over all qualifiers and snaks in the intended order.
local function orderedpairs(array, order)
if not order then return pairs(array) end
-- return iterator function
local i = 0
return function()
i = i + 1
if order[i] then
return order[i], array[order[i]]
end
end
end
function p.descriptionIn(frame)
local langcode = frame.args[1]
local id = frame.args[2]
-- return description of a Wikidata entity in the given language or the default language of this Wikipedia site
local entity = mw.wikibase.getEntity(id)
if entity and entity.descriptions then
local desc = entity.descriptions[langcode or wiki.langcode]
if desc then return desc.value end
end
end
function p.labelIn(frame)
local langcode = frame.args[1]
local id = frame.args[2]
-- return label of a Wikidata entity in the given language or the default language of this Wikipedia site
local entity = mw.wikibase.getEntity(id)
if entity and entity.labels then
local label = entity.labels[langcode or wiki.langcode]
if label then return label.value end
end
end
local function printDatavalueCoordinate(data, parameter)
-- data fields: latitude [double], longitude [double], altitude [double], precision [double], globe [wikidata URI, usually http://www.wikidata.org/entity/Q2 [earth]]
if parameter then
if parameter == "globe" then data.globe = mw.ustring.match(data.globe, "Q%d+") end -- extract entity id from the globe URI
return data[parameter]
else
return data.latitude .. "/" .. data.longitude -- combine latitude and longitude, which can be decomposed using the #titleparts wiki function
end
end
local function printDatavalueQuantity(data, parameter)
-- data fields: amount [number], unit [string], upperBound [number], lowerBound [number]
if not parameter or parameter == "amount" then
return tonumber(data.amount)
elseif parameter == "unit" then
return mw.ustring.match(data.unit, "Q%d+")
else
return data[parameter]
end
end
local function normalizeDate(date)
date = mw.text.trim(date, "+")
-- extract year
local yearstr = mw.ustring.match(date, "^\-?%d+")
local year = tonumber(yearstr)
-- remove leading zeros of year
return year .. mw.ustring.sub(date, #yearstr + 1), year
end
-- precision: 0 - billion years, 1 - hundred million years, ..., 6 - millenia, 7 - century, 8 - decade, 9 - year, 10 - month, 11 - day, 12 - hour, 13 - minute, 14 - second
function formatDate(date, precision, timezone, formatstr)
precision = precision or 11
date, year = normalizeDate(date)
date = string.gsub(date, "-00%f[%D]", "-01")
if year == 0 and precision <= 9 then return "" end
-- precision is 10000 years or more
if precision <= 5 then
local factor = 10 ^ ((5 - precision) + 4)
local y2 = math.ceil(math.abs(year) / factor)
local relative = mw.ustring.gsub(i18n.datetime[precision], "$1", tostring(y2))
if year < 0 then
relative = mw.ustring.gsub(i18n.datetime.beforenow, "$1", relative)
else
relative = mw.ustring.gsub(i18n.datetime.afternow, "$1", relative)
end
return relative
end
-- precision is decades, centuries and millenia
local era
if precision == 6 then era = mw.ustring.gsub(i18n.datetime[6], "$1", tostring(math.floor((math.abs(year) - 1) / 1000) + 1)) end
if precision == 7 then era = mw.ustring.gsub(i18n.datetime[7], "$1", tostring(math.floor((math.abs(year) - 1) / 100) + 1)) end
if precision == 8 then era = mw.ustring.gsub(i18n.datetime[8], "$1", tostring(math.floor(math.abs(year) / 10) * 10)) end
if era then
if year < 0 then era = mw.ustring.gsub(mw.ustring.gsub(i18n.datetime.bc, '"', ""), "$1", era)
elseif year > 0 then era = mw.ustring.gsub(mw.ustring.gsub(i18n.datetime.ad, '"', ""), "$1", era) end
return era
end
-- precision is years or less
if precision >= 9 then
--[[ the following code replaces the UTC suffix with the given negated timezone to convert the global time to the given local time
timezone = tonumber(timezone)
if timezone and timezone ~= 0 then
timezone = -timezone
timezone = string.format("%.2d%.2d", timezone / 60, timezone % 60)
if timezone[1] ~= '-' then timezone = "+" .. timezone end
date = mw.text.trim(date, "Z") .. " " .. timezone
end
]]--
if formatstr then
for i=(precision+1), 14 do
for _, ch in pairs(formatchar[i]) do
if formatstr:find(ch) then
formatstr = i18n.datetime[precision]
end
end
end
else
formatstr = i18n.datetime[precision]
end
if year == 0 then formatstr = mw.ustring.gsub(formatstr, i18n.datetime[9], "")
elseif year < 0 then
-- Mediawiki formatDate doesn't support negative years
date = mw.ustring.sub(date, 2)
formatstr = mw.ustring.gsub(formatstr, i18n.datetime[9], mw.ustring.gsub(i18n.datetime.bc, "$1", i18n.datetime[9]))
elseif year > 0 and i18n.datetime.ad ~= "$1" then
formatstr = mw.ustring.gsub(formatstr, i18n.datetime[9], mw.ustring.gsub(i18n.datetime.ad, "$1", i18n.datetime[9]))
end
return mw.language.new(wiki.langcode):formatDate(formatstr, date)
end
end
local function printDatavalueTime(data, parameter)
-- data fields: time [ISO 8601 time], timezone [int in minutes], before [int], after [int], precision [int], calendarmodel [wikidata URI]
-- precision: 0 - billion years, 1 - hundred million years, ..., 6 - millenia, 7 - century, 8 - decade, 9 - year, 10 - month, 11 - day, 12 - hour, 13 - minute, 14 - second
-- calendarmodel: e.g. http://www.wikidata.org/entity/Q1985727 for the proleptic Gregorian calendar or http://www.wikidata.org/wiki/Q11184 for the Julian calendar]
if parameter then
para, formatstr = parameter:match("([^:]+):([^:]+)")
if parameter == "calendarmodel" then
data.calendarmodel = string.match(data.calendarmodel, "Q%d+") -- extract entity id from the calendar model URI
elseif para and para == "time" then
return formatDate(data.time, data.precision, data.timezone,formatstr)
elseif parameter == "time" then
data.time = normalizeDate(data.time)
end
return data[parameter]
else
return formatDate(data.time, data.precision, data.timezone)
end
end
local function printDatavalueEntity(data, parameter)
-- data fields: entity-type [string], numeric-id [int, Wikidata id]
local id
if data["entity-type"] == "item" then id = "Q" .. data["numeric-id"]
elseif data["entity-type"] == "property" then id = "P" .. data["numeric-id"]
else return printError("unknown-entity-type")
end
if parameter then
if parameter == "link" then
local linkTarget = mw.wikibase.sitelink(id)
local linkName = mw.wikibase.label(id)
if linkTarget then
local link = linkTarget
-- if there is a local Wikipedia article linking to it, use the label or the article title
if linkName and (linkName ~= linkTarget) then link = link .. "|" .. linkName end
return "[[" .. link .. "]]"
else
-- if there is no local Wikipedia article output the label or link to the Wikidata object to input a proper label
if linkName then return linkName else return "[[:d:" .. id .. "|" .. id .. "]]" end
end
else
return data[parameter]
end
else
return mw.wikibase.label(id) or id
end
end
local function printDatavalueMonolingualText(data, parameter)
-- data fields: language [string], text [string]
if parameter then
return data[parameter]
else
local result = mw.ustring.gsub(mw.ustring.gsub(i18n.monolingualtext, "%%language", data["language"]), "%%text", data["text"])
return result
end
end
function getSnakValue(snak, parameter)
-- snaks have three types: "novalue" for null/nil, "somevalue" for not null/not nil, or "value" for actual data
if snak.snaktype == "value" then
-- call the respective snak parser
if snak.datavalue.type == "string" then return snak.datavalue.value
elseif snak.datavalue.type == "globecoordinate" then return printDatavalueCoordinate(snak.datavalue.value, parameter)
elseif snak.datavalue.type == "quantity" then return printDatavalueQuantity(snak.datavalue.value, parameter)
elseif snak.datavalue.type == "time" then return printDatavalueTime(snak.datavalue.value, parameter)
elseif snak.datavalue.type == "wikibase-entityid" then return printDatavalueEntity(snak.datavalue.value, parameter)
elseif snak.datavalue.type == "monolingualtext" then return printDatavalueMonolingualText(snak.datavalue.value, parameter)
end
end
return mw.wikibase.renderSnak(snak)
end
function getQualifierSnak(claim, qualifierId)
-- a "snak" is Wikidata terminology for a typed key/value pair
-- a claim consists of a main snak holding the main information of this claim,
-- as well as a list of attribute snaks and a list of references snaks
if qualifierId then
-- search the attribute snak with the given qualifier as key
if claim.qualifiers then
local qualifier = claim.qualifiers[qualifierId]
if qualifier then return qualifier[1] end
end
return nil, printError("qualifier-not-found")
else
-- otherwise return the main snak
return claim.mainsnak
end
end
local function datavalueTimeToDateObject(data)
local sign, year, month, day, hour, minute, second = string.match(data.time, "(.)(%d+)%-(%d+)%-(%d+)T(%d+):(%d+):(%d+)Z")
local result =
{
year = tonumber(year),
month = tonumber(month),
day = tonumber(day),
hour = tonumber(hour),
min = tonumber(minute),
sec = tonumber(second),
timezone = data.timezone,
julian = data.calendarmodel and string.match(data.calendarmodel, "Q11184$")
}
if sign == "-" then result.year = -result.year end
return result
end
function julianDay(dateObject)
local year = dateObject.year
local month = dateObject.month or 0
local day = dateObject.day or 0
if month == 0 then month = 1 end
if day == 0 then day = 1 end
if month <= 2 then
year = year - 1
month = month + 12
end
local time = ((((dateObject.sec or 0) / 60 + (dateObject.min or 0) + (dateObject.timezone or 0)) / 60) + (dateObject.hour or 0)) / 24
local b
if dateObject.julian then b = 0 else
local century = math.floor(year / 100)
b = 2 - century + math.floor(century / 4)
end
return math.floor(365.25 * (year + 4716)) + math.floor(30.6001 * (month + 1)) + day + time + b - 1524.5
end
function getQualifierSortValue(claim, qualifierId)
local snak = getQualifierSnak(claim, qualifierId)
if snak and snak.snaktype == "value" then
if snak.datavalue.type == "time" then
return julianDay(datavalueTimeToDateObject(snak.datavalue.value))
else
return getSnakValue(snak)
end
end
end
function getValueOfClaim(claim, qualifierId, parameter)
local error
local snak
snak, error = getQualifierSnak(claim, qualifierId)
if snak then
return getSnakValue(snak, parameter)
else
return nil, error
end
end
function getReferences(frame, claim)
local result = ""
-- traverse through all references
for ref in pairs(claim.references or {}) do
local refparts
-- traverse through all parts of the current reference
for snakkey, snakval in orderedpairs(claim.references[ref].snaks or {}, claim.references[ref]["snaks-order"]) do
if refparts then refparts = refparts .. ", " else refparts = "" end
-- output the label of the property of the reference part, e.g. "imported from" for P143
refparts = refparts .. tostring(mw.wikibase.label(snakkey)) .. ": "
-- output all values of this reference part, e.g. "German Wikipedia" and "English Wikipedia" if the referenced claim was imported from both sites
for snakidx = 1, #snakval do
if snakidx > 1 then refparts = refparts .. ", " end
refparts = refparts .. getSnakValue(snakval[snakidx])
end
end
if refparts then result = result .. frame:extensionTag("ref", refparts) end
end
return result
end
local function hasqualifier(claim, qualifierproperty)
local negotiate
if string.sub(qualifierproperty, 1, 1) == "!" then negotiate = true else negotiate = false end
if not claim.qualifiers and not negotiate then return false end
if not claim.qualifiers and negotiate then return true end
if qualifierproperty == '' then return true end
if not negotiate and not claim.qualifiers[qualifierproperty] then return false end
if negotiate and claim.qualifiers[string.sub(qualifierproperty, 2)] then return false end
return true
end
local function hassource(claim, sourceproperty)
if not claim.references then return false end
if sourceproperty == '' then return true end
if string.sub(sourceproperty,1,1) ~= "!" then
for _, source in pairs(claim.references) do
if source.snaks[sourceproperty] then return true end
end
return false
else
for _, source in pairs(claim.references) do
for key in pairs(source.snaks) do
if key ~= string.sub(sourceproperty,2) then return true end
end
end
return false
end
end
function atdate(claim, mydate)
local refdate
if not mydate or mydate == "" then
refdate = os.date("!*t")
else
if string.match(mydate, "^%d+$") then
refdate = { year = tonumber(mydate) }
else
refdate = datavalueTimeToDateObject({ time = mw.language.getContentLanguage():formatDate("+Y-m-d\\TH:i:s\\Z", mydate) })
end
end
local refjd = julianDay(refdate)
local mindate = getQualifierSortValue(claim, propertyId["starttime"])
local maxdate = getQualifierSortValue(claim, propertyId["endtime"])
if mindate and mindate > refjd then return false end
if maxdate and maxdate < refjd then return false end
return true
end
--returns a table of claims excluding claims not passed the filters
function filterClaims(frame, claims)
local function filter(condition, filterfunction)
if not frame.args[condition] then
return
end
local newclaims = {}
for i, claim in pairs(claims) do
if filterfunction(claim, frame.args[condition]) then
table.insert(newclaims, claim)
end
end
claims = newclaims
end
filter('hasqualifier', hasqualifier)
filter('hassource', hassource)
filter('atdate', atdate)
return claims
end
function p.claim(frame)
local property = frame.args[1] or ""
local id = frame.args["id"]
local qualifierId = frame.args["qualifier"]
local parameter = frame.args["parameter"]
local language = frame.args["language"]
local list = frame.args["list"]
local includeempty = frame.args["includeempty"]
local references = frame.args["references"]
local sort = frame.args["sort"]
local sortInItem = frame.args["sortInItem"]
local inverse = frame.args["inverse"]
local showerrors = frame.args["showerrors"]
local default = frame.args["default"]
if default then showerrors = nil end
-- get wikidata entity
local entity = mw.wikibase.getEntity(id)
if not entity then
if showerrors then return printError("entity-not-found") else return default end
end
-- fetch the first claim of satisfying the given property
local claims
if entity.claims then claims = entity.claims[mw.wikibase.resolvePropertyId(property)] end
if not claims or not claims[1] then
if showerrors then return printError("property-not-found") else return default end
end
--filter claims
claims = filterClaims(frame, claims)
if not claims[1] then return default end
-- get initial sort indices
local sortindices = {}
for idx in pairs(claims) do
sortindices[#sortindices + 1] = idx
end
local comparator
if sort then
-- sort by time qualifier
comparator = function(a, b)
local timea = getQualifierSortValue(claims[a], sort) or ''
local timeb = getQualifierSortValue(claims[b], sort) or ''
if type(timea) ~= type(timeb) and not (tonumber(timea) and tonumber(timeb)) then
if tonumber(timea) then return true
elseif tonumber(timeb) then return false
elseif tostring(timea) and tostring(timeb) then
if inverse then return tostring(timea) > tostring(timeb) else return tostring(timea) < tostring(timeb) end
else return false end -- different types, neither numbers nor strings, no chance to compare => random result to avoid script error
elseif tonumber(timea) and tonumber(timeb) then
timea = tonumber(timea)
timeb = tonumber(timeb)
end
if inverse then
return timea > timeb
else
return timea < timeb
end
end
elseif sortInItem then
-- fill table sortkeys
local sortkeys = {}
local snakSingle
local sortkeyValueId
local claimContainingValue
for idx, claim in pairs(claims) do
snakSingle = getQualifierSnak(claim)
sortkeyValueId = "Q" .. getSnakValue(snakSingle, "numeric-id")
claimContainingValue = mw.wikibase.getEntity(sortkeyValueId).claims[mw.wikibase.resolvePropertyId(sortInItem)]
if claimContainingValue then
sortkeys[#sortkeys + 1] = getValueOfClaim(claimContainingValue[1])
else
sortkeys[#sortkeys + 1] = ""
end
end
comparator = function(a, b)
if inverse then
return sortkeys[a] > sortkeys [b]
else
return sortkeys[a] < sortkeys [b]
end
end
else
-- sort by claim rank
comparator = function(a, b)
local rankmap = { deprecated = 2, normal = 1, preferred = 0 }
local ranka = rankmap[claims[a].rank or "normal"] .. string.format("%08d", a)
local rankb = rankmap[claims[b].rank or "normal"] .. string.format("%08d", b)
return ranka < rankb
end
end
table.sort(sortindices, comparator)
local result
local error
if list then
list = string.gsub(list, "\\n", "\n") -- if a newline is provided (whose backslash will be escaped) unescape it
local value
-- iterate over all elements and return their value (if existing)
result = {}
for idx in pairs(claims) do
local claim = claims[sortindices[idx]]
value, error = getValueOfClaim(claim, qualifierId, parameter)
if not value and value ~= 0 and showerrors then value = error end
if not value and value ~= 0 and includeempty then value = "" end
if value and references then value = value .. getReferences(frame, claim) end
result[#result + 1] = value
end
result = table.concat(result, list)
else
-- return first element
local claim = claims[sortindices[1]]
if language and claim.mainsnak.datatype == "monolingualtext" then
-- iterate over claims to find adequate language
for idx, claim in pairs(claims) do
if claim.mainsnak.datavalue.value.language == language then
result, error = getValueOfClaim(claim, qualifierId, parameter)
break
end
end
else
result, error = getValueOfClaim(claim, qualifierId, parameter)
end
if result and references then result = result .. getReferences(frame, claim) end
end
if result then return result else
if showerrors then return error else return default end
end
end
function p.getValue(frame)
local param = frame.args[2]
if param == "FETCH_WIKIDATA" or param == i18n["FETCH_WIKIDATA"] then return p.claim(frame) else return param end
end
-- for use in templates only
function p.pageId(frame)
local id = frame.args[1]
return p._pageId(id)
end
-- for use in modules only
function p._pageId(id)
local entity = mw.wikibase.getEntity(id)
if not entity then return nil else return entity.id end
end
function p.labelOf(frame)
local id = frame.args[1]
-- returns the label of the given entity/property id
-- if no id is given, the one from the entity associated with the calling Wikipedia article is used
if not id then
local entity = mw.wikibase.getEntity()
if not entity then return printError("entity-not-found") end
id = entity.id
end
return mw.wikibase.label(id)
end
function p.sitelinkOf(frame)
local id = frame.args[1]
-- returns the Wikipedia article name of the given entity
-- if no id is given, the one from the entity associated with the calling Wikipedia article is used
if not id then
local entity = mw.wikibase.getEntity()
if not entity then return printError("entity-not-found") end
id = entity.id
end
return mw.wikibase.sitelink(id)
end
function p.badges(frame)
local site = frame.args[1]
local id = frame.args[2]
if not site then return printError("site-not-found") end
local entity = mw.wikibase.getEntity(id)
if not entity then return printError("entity-not-found") end
local badges = entity.sitelinks[site].badges
if badges then
local result
for idx = 1, #badges do
if result then result = result .. "/" .. badges[idx] else result = badges[idx] end
end
return result
end
end
function p.sitelinkCount(frame)
local filter = "^.*" .. (frame.args[1] or "") .. "$"
local id = frame.args[2]
local entity = mw.wikibase.getEntity(id)
local count = 0
if entity and entity.sitelinks then
for project, _ in pairs(entity.sitelinks) do
if string.find(project, filter) then count = count + 1 end
end
end
return count
end
-- call this in cases of script errors within a function instead of {{#invoke:Wikidata|<method>|...}} call {{#invoke:Wikidata|debug|<method>|...}}
function p.debug(frame)
local func = frame.args[1]
if func then
-- create new parameter set, where the first parameter with the function name is removed
local newargs = {}
for key, val in pairs(frame.args) do
if type(key) == "number" then
if key > 1 then newargs[key - 1] = val end
else
newargs[key] = val
end
end
frame.args = newargs
local status, result = pcall(p[func], frame)
if status then return result else return '<span class="error">' .. result .. '</span>' end
else
return '<span class="error">invalid parameters</span>'
end
end
function p.printEntity(frame)
local id = frame.args[1]
local entity = mw.wikibase.getEntity(id)
if entity then return "<pre>" .. mw.text.jsonEncode(entity, mw.text.JSON_PRETTY) .. "</pre>" end
end
return p