Modul:Wikidata

Aus ÖsterreichWiki
Zur Navigation springen Zur Suche springen

Die Dokumentation für dieses Modul kann unter Modul:Wikidata/Doku erstellt werden

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

-- internationalisation
local i18n =
{
	["errors"] =
	{
		["property-not-found"] = "Eigenschaft nicht gefunden.",
		["entity-not-found"] = "Wikidata-Eintrag nicht gefunden.",
		["entity-not-valid"] = "Die an die Wikidata-Schnittstelle übergebene Item-ID ist nicht gültig.",
		["unknown-claim-type"] = "Unbekannter Aussagentyp.",
		["unknown-entity-type"] = "Unbekannter Entity-Typ.",
		["qualifier-not-found"] = "Qualifikator nicht gefunden.",
		["site-not-found"] = "Wikimedia-Projekt nicht gefunden.",
		["invalid-parameters"] = "Ungültige Parameter.",
		["module-not-loaded"] = "Loading of additional module failed."
	},
	["maintenance-pages"] =
	{
		["entity-not-found"] = "Wikidata/Wartung/Fehlendes Datenobjekt",
		["entity-not-valid"] = "Wikidata/Wartung/Ungültige Datenobjekt-Identifikationsnummer",
		["property-not-existing"] = "Wikidata/Wartung/Eigenschaft existiert nicht"
	},
	["datetime"] =
	{
		-- $1 is a placeholder for the actual number
		[0] = "$1 Mrd. Jahren",		-- precision: billion years
		[1] = "$100 Mio. Jahren",	-- precision: hundred million years
		[2] = "$10 Mio. Jahren",	-- precision: ten million years
		[3] = "$1 Mio. Jahren",		-- precision: million years
		[4] = "$100.000 Jahren",	-- precision: hundred thousand years
		[5] = "$10.000 Jahren",		-- precision: ten thousand years
		[6] = "$1. Jahrtausend", 	-- precision: millenium
		[7] = "$1. Jahrhundert",	-- precision: century
		[8] = "$1er",				-- 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 "Uhr"',	-- precision: hour
		[13] = "j. F Y G:i",		-- precision: minute
		[14] = "j. F Y G:i:s",		-- precision: second
		["beforenow"] = "vor $1",	-- how to format negative numbers for precisions 0 to 5
		["afternow"] = "in $1",		-- how to format positive numbers for precisions 0 to 5
		["bc"] = '$1 "v.Chr."',		-- 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 function printError(code)
	return '<span class="error">' .. (i18n.errors[code] or 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

-- Function to check whether a certain item is a parent of a given item.
-- If pExitItem is reached without finding the searched parent item, the search stops.
-- A parent is connected via P31 or P279.
-- Attention: very intensive function, use carefully!
local function isParent(pItem, pParent, pExitItem, pMaxDepth, pDepth)
	if not pDepth then pDepth = 0 end

	if type(pItem) == "number" then pItem = "Q" .. pItem end

	local entity = mw.wikibase.getEntity(pItem)
	if not entity then return false end

	local claims31
	local claims279
	if entity.claims then
		claims31 = entity.claims[mw.wikibase.resolvePropertyId('P31')]
		claims279 = entity.claims[mw.wikibase.resolvePropertyId('P279')]
	else
		return false
	end
	if not claims31 and not claims279 then return false end

	local parentIds = {}
	if claims31 and #claims31 > 0 then
		for i, v in ipairs(claims31) do parentIds[#parentIds+1] = getSnakValue(v.mainsnak, "numeric-id") end
	end
	if claims279 and #claims279 > 0 then
		for i, v in ipairs(claims279) do parentIds[#parentIds+1] = getSnakValue(v.mainsnak, "numeric-id") end
	end

	-- check if searched parent or exit item is reached or do recursive call
	if not parentIds[1] or #parentIds == 0 then return false end
	local itemString = ""
	local result = nil
	for i, v in ipairs(parentIds) do
		if not v then return false end
		itemString = "Q" .. v

		if itemString == pParent then
			-- successful!
			return true
		elseif itemString == pExitItem or itemString == "Q35120" then
			-- exit if either "exit item" or node item (Q35120) is reached
			return false
		else
			if pDepth+1 < pMaxDepth then
				result = isParent(itemString, pParent, pExitItem, pMaxDepth, pDepth+1)
			else return false end

			if result == true then return result end
		end
	end
	do return false 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 and 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 formatReference(ref)
	-- "imported from"-references are useless, skip them:
	if ref["P143"] or ref["P4656"] then return nil end
	
	-- load [[Modul:Zitation]]
	local ZitationSuccess, r = pcall(require, "Modul:Zitation")
	if type(r) == "table" then
		Zitation = r.Zitation()
		-- clear Zitation state from previous invocations
		Zitation.o = nil
	end
	-- assert (ZitationSuccess, i18n["errors"]["module-not-loaded"])
	
	-- assignments of Wikidata properties to Zitation parameters
	local wdZmap = {
		P1433 = {"bas", "Werk"},
		P248  = {"bas", "Werk"},
		P1476 = {"bas", "Titel"},
		P1680 = {"bas", "TitelErg"},
		P407  = {"bas", "Sprache"},
		P364  = {"bas", "Sprache"},
		P2439 = {"bas", "Sprache"},
		P123  = {"bas", "Verlag"},
		P577  = {"bas", "Datum"},
		P98   = {"bas", "Hrsg"},
		P2093 = {"bas", "Autor"},
		P50   = {"bas", "Autor"},
		P1683 = {"bas", "Zitat"},
		P854  = {"www", "URL"},
		P813  = {"www", "Abruf"},
		P1065 = {"www", "ArchivURL"},
		P2960 = {"www", "ArchivDatum"},
		P2701 = {"www", "Format"},
		P393  = {"print", "Auflage"},
		P291  = {"print", "Ort"},
		P304  = {"fragment", "Seiten"},
		P792  = {"fragment", "Kapitel"},
		P629  = {"orig", "Titel"}
	}
	
	for prop, value in pairs(ref) do
		if wdZmap[prop] then
			if type(value) == "table" then
				-- More snaks with same property, we concatenate using a comma
				value = table.concat(value, ", ")
			end
			-- value should be string now, so we can call Zitation
			if type(value) == "string" and string.len(value) > 0 then
				Zitation.fill(wdZmap[prop][1], wdZmap[prop][2], value, prop)	
			end
		end
	end
	-- if no title on Wikidata, try to use the URL as title
	if (not ref["P1476"]) and ref["P854"] then
		local URLutil = Zitation.fetch("URLutil")
		Zitation.fill("bas", "Titel", URLutil.getHost(ref["P854"]))
	end
	refFormatted, f = Zitation.format()
	return refFormatted, f
end

function getReferences(frame, claim)
	local result = ""
	-- traverse through all references
	for ref in pairs(claim.references or {}) do
		local refTable = {}
		for snakkey, snakval in orderedpairs(claim.references[ref].snaks or {}, claim.references[ref]["snaks-order"]) do
			if #snakval == 1 then
				refTable[snakkey] = getSnakValue(snakval[1])
			else
				--
				multival = {}
				for snakidx = 1, #snakval do
						table.insert(multival, getSnakValue(snakval[snakidx]))
				end
				refTable[snakkey] = multival
			end
		end
		local formattedRef, f = formatReference(refTable)
		-- log errors that occur during formatting
		if f then
			mw.log(f)
		end
		if formattedRef and formattedRef ~= "" then
			local hash = mw.hash.hashValue('fnv164', formattedRef)
			result = result .. frame:extensionTag("ref", formattedRef, { name = '_' .. hash })
		end
	end
	return result
end

local function hasqualifier(claim, qualifierproperty)
	local invert
	if string.sub(qualifierproperty, 1, 1) == "!" then invert = true else invert = false end
	if not claim.qualifiers and not invert then return false end
	if not claim.qualifiers and invert then return true end
	if qualifierproperty == '' then return true end
	if not invert and not claim.qualifiers[qualifierproperty] then return false end
	if invert and claim.qualifiers[string.sub(qualifierproperty, 2)] then return false end
	return true
end

local function qualifierhasvalue(claim, property, value)
	-- TODO: not yet documented!
	if not claim.qualifiers then return false end
	if not claim.qualifiers[property] then return false end
	for key, snak in pairs(claim.qualifiers[property]) do
		if snak.snaktype == "value" then
			if snak.datavalue.type == "wikibase-entityid" then
				if snak.datavalue.value.id == value then
					return true
				end
			--TODO: elseif other types
			end
		end
	end
	return false
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

local function notdeprecated(claim, sourceproperty)
  return not (claim.rank == "deprecated")
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)
	if not frame.args.includedeprecated then
		frame.args.notdeprecated = true
    	filter('notdeprecated', notdeprecated)
    end
	
	-- use additional unnamed parameters as qualifier conditions (in pairs)
	-- not yet documented!
	-- TODO: not sure if this is good approach. Maybe use named parameter that has pairs split by semicolon
	for key, val in pairs(frame.args) do
		if type(key) == "number" and key > 2 and key % 2 == 1 then
			-- key = 3, 5, 7 and so on
			local newclaims = {}
			for i, claim in pairs(claims) do
				if qualifierhasvalue(claim, frame.args[key - 1], frame.args[key]) then
					table.insert(newclaims, claim)
				end
			end
			claims = newclaims
		end
	end
	
	return claims
end

local p = {}

function p.isSubclass(frame)
	if not frame.args["parent"] then return "" end

	local maxDepth
	maxDepth = frame.args["maxDepth"] or 5
	if not type(maxDepth) == "number" then maxDepth = 5 end

	local result
	result = isParent(frame.args["id"], frame.args["parent"], frame.args["exitItem"], maxDepth)
	
	if frame.args["returnInt"] then
		if result == true then return 1 else return "" end
	else
		if result then return result else return false 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
	else
		return "";
	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
	else
		return "";
	end
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 listMaxItems = tonumber(frame.args["listMaxItems"]) or 0
	local references = frame.args["references"]
	local sort = frame.args["sort"]
	local sortEmptiesFirst = frame.args["sortEmptiesFirst"]
	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
	if id then
		if not mw.wikibase.isValidEntityId(id) then
			if showerrors then 
				return printError("entity-not-valid") 
			else 
				local temp = mw.title.new(i18n["maintenance-pages"]["entity-not-valid"], "Modul").exists
				return default 
			end
		elseif not mw.wikibase.entityExists(id) then
			if showerrors then
				return printError("entity-not-found") 
			else 
				local temp = mw.title.new(i18n["maintenance-pages"]["entity-not-found"], "Modul").exists
				return default 
			end
		end
	end
	
	local entity = mw.wikibase.getEntity(id)
	if not entity then
		if showerrors then return printError("entity-not-found") else return default end
	end

	-- check if property exists
	local realProp = mw.wikibase.resolvePropertyId(property)
	if not realProp then
		local temp = mw.title.new(i18n["maintenance-pages"]["property-not-existing"], "Modul").exists
	end

	-- fetch the first claim of satisfying the given property
	local claims
	if entity.claims then claims = entity.claims[realProp] 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
		comparator = function(a, b) --comparator function for sorting statements based on qualifier value
			-- load qualifier values
			local QualifierSortValueA = getQualifierSortValue(claims[a], sort)
			local QualifierSortValueB = getQualifierSortValue(claims[b], sort)
			
			-- if either of the two statements does not have this qualifer: 
			---- if sortEmptiesFirst=true: sort it to the beginning 
			---- else: always sort it to the end
			if not QualifierSortValueB then 
				if not QualifierSortValueA then
					-- if neither of the two statements has this qualifier, arbitrarily but consistently return a < b
					return a < b
				elseif sortEmptiesFirst then
					return false
				else
					return true
				end
			elseif not QualifierSortValueA then 
				if sortEmptiesFirst then return true else return false end
			end
			
			if type(QualifierSortValueA) ~= type(QualifierSortValueB) and not (tonumber(QualifierSortValueA) and tonumber(QualifierSortValueB)) then
				if tonumber(QualifierSortValueA) then return true
				elseif tonumber(QualifierSortValueB) then return false
				elseif tostring(QualifierSortValueA) and tostring(QualifierSortValueB) then
					if inverse then return tostring(QualifierSortValueA) > tostring(QualifierSortValueB) else return tostring(QualifierSortValueA) < tostring(QualifierSortValueB) end
				else return false end -- different types, neither numbers nor strings, no chance to compare => random result to avoid script error
			elseif tonumber(QualifierSortValueA) and tonumber(QualifierSortValueB) then
				QualifierSortValueA = tonumber(QualifierSortValueA)
				QualifierSortValueB = tonumber(QualifierSortValueB)
			end
			if inverse then
				return QualifierSortValueA > QualifierSortValueB
			else
				return QualifierSortValueA < QualifierSortValueB
			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
		if listMaxItems and listMaxItems > 0 then
			result = table.concat(result, list, 1, math.min(table.getn(result), listMaxItems))
		else
			result = table.concat(result, list)
		end
	else
		-- return first element

		local claim = claims[sortindices[1]]
		if language == "Q" then
			result, error = "Q" .. getSnakValue(getQualifierSnak(claim), "numeric-id")
		elseif 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 references == "only" then 
			result = getReferences(frame, claim)
		elseif 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

function p.pageId(frame)
	local id = frame.args[1]
	local entity = mw.wikibase.getEntity(id)
	if not entity then return "" 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 tostring(result) or "" else return '<span class="error">' .. result .. '</span>' end -- revert
		if status then return result else return '<span class="error">' .. result .. '</span>' end
	else
		return printError("invalid-parameters")
	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

-- formfill Template:Coordinate (NS, EW, name from WikidataEntity) and expand it
-- füllt Vorlage:Coordinate (NS, EW, name mit Wikidata-Werten) + expandiert sie
--  1st frame.arg                            .. Q prefixed entity id (mandatory)
--  named frame.arg "type", "region", "text" .. see doc of 'Coordinate' template
function p.ffCoordinate(frame)
	local f = frame
	local id = f.args[1] or f.args.Q
	local name = f.args.name or p.labelIn{ args = { nil, id, id = id }}
	
	local coord = mw.text.split(p.claim{ args = { "P625", id, id = id }}, '/')
	coord[1] = tonumber(coord[1])
	coord[2] = tonumber(coord[2])

	local t, r = f.args.type, f.args.region
	if not t
	then t = p.claim{ args = { "P31", id, id = id, language = "Q" }}
		 t = t and t:gsub("Q.*", {
			Q8502  = "mountain",
			Q54050 = "landmark"
		 })
		 if not t or t and t:find("Q", 1, true)
		 then t="" -- no default, let Coordinate warn about unset type= param
		 end
	end
	if not r
	then r = p.claim{ args = { "P17", id, id = id, language = "Q" }}
		 r = r and p.claim{ args = { "P297", r, id = r }}
		 if not r
		 then r="" -- no default, let Coordinate warn about unset region= param
		 end
	end
	
	return ('<span data-sort-value="%010.6f"></span>'):format((f.args.sortkey
		or "EW"):find("EW", 1, true) and coord[2]+360.0 or coord[1]+180.0
	) .. f:expandTemplate{ title = 'Coordinate', args = {
		NS = coord[1], EW = coord[2], type = t, region = r,
		text = f.args.text or (f.args.maplink and "ICON0" or "/"),
		name = name, simple = f.args.simple
	}} .. (not f.args.maplink and "" or (" " ..
		--f:callParserFunction{ name="#statements", args={ "P625", from = id } }
		f:callParserFunction{ name="#tag:maplink", args={ "",
			class = "no-icon", text = f.args.mlname and name,
			zoom = 12, latitude = coord[1], longitude = coord[2]
		}}
	))
end

function p.ffCoordinateAndLatLonMaplink(frame)
	frame.args.maplink = 1
	--frame.args.mlname = nil
	return p.ffCoordinate(frame)
end

function p.ffCoordinateAndMaplink(frame)
	frame.args.maplink = 1
	frame.args.mlname = 1
	return p.ffCoordinate(frame)
end


return p