Documentation for this module may be created at Module:Wikidata/doc

-- module local variables
local wiki = 
{
	langcode = mw.language.getContentLanguage().code
}

-- internationalisation
local i18n = {
	["errors"] = {
		["property-not-found"] = "Eigenschap niet gevonden.",
		["entity-not-found"] = "Wikidata-entiteit niet gevonden.",
		["unknown-claim-type"] = "Onbekend statementtype.",
		["unknown-snak-type"] = "Onbekend snaktype.",
		["unknown-datavalue-type"] = "Onbekend gegevenstype.",
		["unknown-entity-type"] = "Onbekend entiteitstype.",
		["qualifier-not-found"] = "Kwalificatie niet gevonden.",
		["site-not-found"] = "Wikimedia-project niet gevonden."
	},
	["somevalue"] = "",
	["novalue"] = "",
	["datetime"] =
	{
		-- $1 is a placeholder for the actual number
		[0] = "$1 bion aña",		-- precision: billion years
		[1] = "$100 mion aña",		-- precision: hundred million years
		[2] = "$10 miom aña",		-- precision: ten million years
		[3] = "$1 mion aña",		-- precision: million years
		[4] = "$100.000 aña",		-- precision: hundred thousand years
		[5] = "$10.000 aña",		-- precision: ten thousand years
		[6] = "$1e millennium",		-- precision: millennium
		[7] = "[[siglo $1]]",			-- precision: century
		[8] = "dékada  $1",			-- precision: decade
		-- the following use the format of #time parser function
		[9] = "Y",					-- precision: year
		[10] = "F Y",				-- precision: month
		[11] = "j F Y",				-- precision: day
		[12] = 'j F Y, G "ora"',	-- precision: hour
		[13] = "j F Y G:i",			-- precision: minute
		[14] = "j F Y G:i:s",		-- precision: second
		["beforenow"] = "$1 geleden",	-- how to format negative numbers for precisions 0 to 5
		["afternow"] = "over $1",		-- how to format positive numbers for precisions 0 to 5
		["bc"] = '$1 "a.K."',		-- how print negative years
		["bce"] = '$1 "a.K."',		-- how print negative years
		["ad"] = "$1"				-- how print positive years
	},
	["monolingualtext"] = '<span lang="%language">%text</span>'
}

local p = { }

local function printError(code)
	return '<span class="error">' .. i18n.errors[code] .. '</span>'
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

-- getting sitelink of a given wiki
-- source: https://en.wikipedia.org/w/index.php?title=Module:Wikidata&oldid=757775054 line 1102
function p.getSiteLink(frame)
	local siteId = frame.args[1]
	local wikidataId = frame.args["id"]
	local entity = mw.wikibase.getEntity( wikidataId )
	if not entity then
		return
	end
	local link = entity:getSitelink( siteId )
	if not link then
		return
	end
	return link
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
	return mw.wikibase.getEntityObject(id).descriptions[langcode or wiki.langcode].value
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
	return mw.wikibase.getEntityObject(id).labels[langcode or wiki.langcode].value
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 parameter then
		return data[parameter]
	else
		return string.gsub(tonumber(data.amount),"%.",",") -- nl: replace dot decimal separator by comma decimal separator
	end
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
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

function formatDate(date, precision, timezone, calendarmodel)
    precision = precision or 11
    date, year = normalizeDate(date)
    if year == 0 and precision <= 9 then return "" end

    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

    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

    if precision == 9 then
        return year
    end

    if precision > 9 then
        local formatstr = i18n.datetime[precision]
        if year == 0 then formatstr = mw.ustring.gsub(formatstr, i18n.datetime[9], "")
        elseif year < 0 then
            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
        local formattedDate = mw.language.new(wiki.langcode):formatDate(formatstr, date)
        formattedDate = formattedDate:gsub("(%d+) (%a+)", "%1 di %2")
        if (mw.ustring.find(calendarmodel, 'Q1985786', 1, true)) and year > 1582 then
            return formattedDate .. '<sup>&thinsp;[[Kalènder yüliano|jul.]]</sup>'
        else
            return formattedDate
        end
    end
end


local function formatDateLink(time, precision, calendarmodel)
    precision = precision or 11
    local year, m, d = mw.ustring.match(time, "(%-?%d+)%-(%d+)%-(%d+)T")
    local maanden = {'yanüari', 'febrüari', 'mart', 'aprel', 'mei', 'yüni', 'yüli', 'ougùstùs', 'sèptèmber', 'òktober', 'novèmber', 'desèmber'}
    m = tonumber(m)
    d = tonumber(d)
    year = tonumber(year)

    -- 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

    if precision == 9 then
        return '[[' .. year .. ']]'
    end

    if precision == 10 then
        return '[[' .. d .. ' di ' .. maanden[m] .. ']] [[' .. year .. ']]'
    end

    if precision > 10 then
        local formattedDate = '[[' .. d .. ' di ' .. maanden[m] .. ']] [[' .. year .. ']]'
        if (mw.ustring.find(calendarmodel, 'Q1985786', 1, true)) and year > 1582 then
            return formattedDate .. '<sup>&thinsp;[[Kalènder yüliano|jul.]]</sup>'
        else
            return formattedDate
        end
    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
		if parameter == "calendarmodel" then data.calendarmodel = mw.ustring.match(data.calendarmodel, "Q%d+") -- extract entity id from the calendar model URI
		elseif parameter == "time" then data.time = normalizeDate(data.time) end
		if parameter == "link" then
			return formatDateLink(data.time, data.precision, data.calendarmodel)
		end
		return data[parameter]
	else
		return formatDate(data.time, data.precision, data.timezone, data.calendarmodel)
	end
end

local function printDatavalueEntity(data, parameter)
	-- data fields: entity-type [string], numeric-id [int, Wikidata id]
	local id = "Q" .. data["numeric-id"]
	if parameter then
		if parameter == "link" then
			return "[[" .. (mw.wikibase.sitelink(id) or (":d:" .. id)) .. "|" .. (mw.wikibase.label(id) or id) .. "]]"
		else
			return data[parameter]
		end
	else
		if data["entity-type"] == "item" then return mw.wikibase.label("Q" .. data["numeric-id"]) or id else printError("unknown-entity-type") end
	end
end

local function printDatavalueMonolingualText(data, parameter)
	-- data fields: language [string], text [string]
	if parameter then
		return data[parameter]
	else
		return mw.ustring.gsub(mw.ustring.gsub(i18n.monolingualtext, "%%language", data["language"]), "%%text", data["text"])
	end
end

function findClaims(entity, property)
	if not property or not entity or not entity.claims then return end

	if mw.ustring.match(property, "^P%d+$") then
		-- if the property is given by an id (P..) access the claim list by this id
		return entity.claims[property]
	else
		property = mw.wikibase.resolvePropertyId(property)
		if not property then return end

		return entity.claims[property]
	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 == "novalue" then return i18n["novalue"]
	elseif snak.snaktype == "somevalue" then return i18n["somevalue"]
	elseif snak.snaktype ~= "value" then return nil, printError("unknown-snak-type")
	end

	-- 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)
	else return nil, printError("unknown-datavalue-type")
	end
end

local 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 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

local 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

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 list = frame.args["list"]
	local references = frame.args["references"]
	local showerrors = frame.args["showerrors"]
	local default = frame.args["default"]
	if default then showerrors = nil end

	-- get wikidata entity
	local entity = mw.wikibase.getEntityObject(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 = findClaims(entity, property)
	if not claims or not claims[1] then
		if showerrors then return printError("property-not-found") else return default end
	end

	-- get initial sort indices
	local sortindices = {}
	for idx in pairs(claims) do
		sortindices[#sortindices + 1] = idx
	end
	-- sort by claim rank
	local 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
	table.sort(sortindices, comparator)

	local result
	local error
	if list then
		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 showerrors then value = error end
			if value and references then value = value .. getReferences(frame, claim) end
			result[#result + 1] = value
		end
		result = table.concat(result, list)
	elseif parameter == "count" then
		result = #claims
	else
		-- return first element
		local claim = claims[sortindices[1]]
		result, error = getValueOfClaim(claim, qualifierId, parameter)
		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" then return p.claim(frame) else return param end
end

function p.pageId(frame)
	local entity = mw.wikibase.getEntityObject()
	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.getEntityObject()
		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.getEntityObject()
		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.getEntityObject(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

-- 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

return p