Module:Wikidata

-- vim: set noexpandtab ft=lua ts=4 sw=4:

require( 'strict' )

local p =

local debug = false

------------------------------------------------------------------------------

-- module local variables and functions

local wiki =

{

	langcode = mw.language.getContentLanguage.code

}

-- internationalisation

local i18n =

{

	"errors" =

	{

		"property-not-found" = "Property not found.",

		"entity-not-found" = "Wikidata entity not found.",

		"unknown-claim-type" = "Unknown claim type.",

		"unknown-entity-type" = "Unknown entity type.",

		"qualifier-not-found" = "Qualifier not found.",

		"site-not-found" = "Wikimedia project not found.",

		"unknown-datetime-format" = "Unknown datetime format.",

		"local-article-not-found" = "Article is not yet available in this wiki."

	},

	"datetime" =

	{

		-- $1 is a placeholder for the actual number

		0 = "$1 billion years",	-- precision: billion years

		1 = "$100 million years",	-- precision: hundred million years

		2 = "$10 million years",	-- precision: ten million years

		3 = "$1 million years",	-- precision: million years

		4 = "$100,000 years",		-- precision: hundred thousand years

		5 = "$10,000 years",		-- precision: ten thousand years

		6 = "$1 millennium",		-- precision: millennium

		7 = "$1 century",			-- precision: century

		8 = "$1s",				-- precision: decade

		-- the following use the format of #time parser function

		9 = "Y",					-- precision: year,

		10 = "F Y",				-- precision: month

		11 = "F j, Y",			-- precision: day

		12 = "F j, Y ga",			-- precision: hour

		13 = "F j, Y g:ia",		-- precision: minute

		14 = "F j, Y g:i:sa",		-- precision: second

		"beforenow" = "$1 BCE",	-- how to format negative numbers for precisions 0 to 5

		"afternow" = "$1 CE",		-- how to format positive numbers for precisions 0 to 5

		"bc" = '$1 "BCE"',		-- how print negative years

		"ad" = "$1",				-- how print positive years

		-- the following are for function getDateValue and getQualifierDateValue

		"default-format" = "dmy", -- default value of the #3 (getDateValue) or

									-- #4 (getQualifierDateValue) argument

		"default-addon" = "BC",	-- default value of the #4 (getDateValue) or

									-- #5 (getQualifierDateValue) argument

		"prefix-addon" = false,	-- set to true for languages put "BC" in front of the

									-- datetime string; or the addon will be suffixed

		"addon-sep" = " ",		-- separator between datetime string and addon (or inverse)

		"format" =				-- options of the 3rd argument

		{

			"mdy" = "F j, Y",

			"my" = "F Y",

			"y" = "Y",

			"dmy" = "j F Y",

			"ymd" = "Y-m-d",

			"ym" = "Y-m"

		}

	},

	"monolingualtext" = '%text',

	"warnDump" = "",

	"ordinal" =

	{

		1 = "st",

		2 = "nd",

		3 = "rd",

		"default" = "th"

	}

}

--require("Module:i18n").loadI18n("Module:Wikidata/i18n", i18n)

-- got idea from :w:Module:Wd

local module_title; if ... == nil then

	module_title = mw.getCurrentFrame:getTitle

else

	module_title = ...

end

require('Module:i18n').loadI18n(module_title..'-i18n', i18n)

-- this function needs to be internationalised along with the above:

-- takes cardinal numer as a numeric and returns the ordinal as a string

-- we need three exceptions in English for 1st, 2nd, 3rd, 21st, .. 31st, etc.

local function makeOrdinal (cardinal)

	local ordsuffix = i18n.ordinal.default

	if cardinal % 10 == 1 then

		ordsuffix = i18n.ordinal1

	elseif cardinal % 10 == 2 then

		ordsuffix = i18n.ordinal2

	elseif cardinal % 10 == 3 then

		ordsuffix = i18n.ordinal3

	end

	-- In English, 1, 21, 31, etc. use 'st', but 11, 111, etc. use 'th'

	-- similarly for 12 and 13, etc.

	if (cardinal % 100 == 11) or (cardinal % 100 == 12) or (cardinal % 100 == 13) then

		ordsuffix = i18n.ordinal.default

	end

	return tostring(cardinal) .. ordsuffix

end

local function printError(code)

	return .. (i18n.errorscode or code) ..

end

local function parseDateFormat(f, timestamp, addon, prefixaddon, addonsep)

	local year_suffix

	local tstr = ""

	local lang_obj = mw.language.new(wiki.langcode)

	local f_parts = mw.text.split(f, 'Y', true)

	for idx, fpart in pairs(fparts) do

		year_suffix =

		if string.match(f_part, "xmijkot$") then

			-- for non-Gregorian year

			fpart = fpart .. 'Y'

		elseif idx 0 then era = mw.ustring.gsub(mw.ustring.gsub(i18n.datetime.ad, '"', ""), "$1", era) end

		return era

	end

	-- precision is year

	if precision == 9 then

		return year

	end

	-- precision is less than years

	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 timezone1 ~= '-' then timezone = "+" .. timezone end

			date = mw.text.trim(date, "Z") .. " " .. timezone

		end

		--

		local formatstr = i18n.datetimeprecision

		if year == 0 then formatstr = mw.ustring.gsub(formatstr, i18n.datetime9, "")

		elseif year 0 and i18n.datetime.ad ~= "$1" then

			formatstr = mw.ustring.gsub(formatstr, i18n.datetime9, mw.ustring.gsub(i18n.datetime.ad, "$1", i18n.datetime9))

		end

		return mw.language.new(wiki.langcode):formatDate(formatstr, date)

	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

				-- if there is a local Wikipedia article link to it using the label or the article title

				return "" .. (linkName or linkTarget) .. ""

			else

				-- if there is no local Wikipedia article output the label or link to the Wikidata object to let the user input a proper label

				if linkName then return linkName else return "" .. id .. "" end

			end

		else

			return dataparameter

		end

	else

		return mw.wikibase.label(id) or id

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

		return dataparameter

	else

		return formatDate(data.time, data.precision, data.timezone)

	end

end

local function printDatavalueMonolingualText(data, parameter)

	-- data fields: language string, text string

	if parameter then

		return dataparameter

	else

		local result = mw.ustring.gsub(mw.ustring.gsub(i18n.monolingualtext, "%%language", data"language"), "%%text", data"text")

		return result

	end

end

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

	else

		property = mw.wikibase.resolvePropertyId(property)

		if not property then return end

		return entity.claimsproperty

	end

end

local function getSnakValue(snak, parameter)

	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

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

			if qualifier then return qualifier1 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.referencesref.snaks or , claim.referencesref"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(snakvalsnakidx)

			end

		end

		if refparts then result = result .. frame:extensionTag("ref", refparts) end

	end

	return result

end

local function parseInput(frame)

	local qid = frame.args.qid

	if qid and (#qid == 0) then qid = nil end

	local propertyID = mw.text.trim(frame.args1 or "")

	local input_parm = mw.text.trim(frame.args2 or "")

	if inputparm ~= "FETCHWIKIDATA" then

		return false, input_parm, nil, nil

	end

	local entity = mw.wikibase.getEntityObject(qid)

	local claims

	if entity and entity.claims then

		claims = entity.claimspropertyID

		if not claims then

			return false, "", nil, nil

		end

	else

		return false, "", nil, nil

	end

	return true, entity, claims, propertyID

end

local function isType(claims, type)

	return claims1 and claims1.mainsnak.snaktype == "value" and claims1.mainsnak.datavalue.type == type

end

local function getValue(entity, claims, propertyID, delim, labelHook)

	if labelHook == nil then

		labelHook = function (qnumber)

			return nil;

		end

	end

	if isType(claims, "wikibase-entityid") then

		local out =

		for k, v in pairs(claims) do

			local qnumber = "Q" .. v.mainsnak.datavalue.value"numeric-id"

			local sitelink = mw.wikibase.sitelink(qnumber)

			local label = labelHook(qnumber) or mw.wikibase.label(qnumber) or qnumber

			if sitelink then

				out#out + 1 = "" .. label .. ""

			else

				out#out + 1 = "" .. label .. "*"

			end

		end

		return table.concat(out, delim)

	else

		-- just return best values

		return entity:formatPropertyValues(propertyID).value

	end

end

------------------------------------------------------------------------------

-- module global functions

if debug then

	function p.inspectI18n(frame)

		local val = i18n

		for _, key in pairs(frame.args) do

			key = mw.text.trim(key)

			val = valkey

		end

		return val

	end

end

function p.descriptionIn(frame)

	local langcode = frame.args1

	local id = frame.args2

	-- return description of a Wikidata entity in the given language or the default language of this Wikipedia site

	return mw.wikibase.getEntityObject(id).descriptionslangcode or wiki.langcode.value

end

function p.labelIn(frame)

	local langcode = frame.args1

	local id = frame.args2

	-- return label of a Wikidata entity in the given language or the default language of this Wikipedia site

	return mw.wikibase.getEntityObject(id).labelslangcode or wiki.langcode.value

end

-- This is used to get a value, or a comma separated list of them if multiple values exist

p.getValue = function(frame)

	local delimdefault = "、" -- internationalise later*	local delim = frame.args.delimiter or ""

	delim = string.gsub(delim, '"', )

	if #delim == 0 then

		delim = delimdefault

	end

	local go, errorOrentity, claims, propertyID = parseInput(frame)

	if not go then

		return errorOrentity

	end

	return getValue(errorOrentity, claims, propertyID, delim)

end

-- Same as above, but uses the short name property for label if available.

p.getValueShortName = function(frame)

	local go, errorOrentity, claims, propertyID = parseInput(frame)

	if not go then

		return errorOrentity

	end

	local entity = errorOrentity

	-- if wiki-linked value output as link if possible

	local function labelHook (qnumber)

		local label

		local claimEntity = mw.wikibase.getEntity(qnumber)

		if claimEntity ~= nil then

			if claimEntity.claims.P1813 then

				for k2, v2 in pairs(claimEntity.claims.P1813) do

					for _, variant in ipairs{"zh", "zh-hans", "zh-hant", "zh-cn", "zh-hk", "zh-mo", "zh-sg", "zh-tw"} do

						if v2.mainsnak.datavalue.value.language == variant then

							label = v2.mainsnak.datavalue.value.text

						end

					end

				end

			end

		end

		if label == nil or label == "" then return nil end

		return label

	end

	return getValue(errorOrentity, claims, propertyID, ", ", labelHook);

end

-- This is used to get a value, or a comma separated list of them if multiple values exist

-- from an arbitrary entry by using its QID.

-- Use :

-- E.g.: - to fetch value of 'spouse' (P26) from 'Richard Burton' (Q151973)

-- Please use sparingly - this is an expensive call.

p.getValueFromID = function(frame)

	local itemID = mw.text.trim(frame.args1 or "")

	local propertyID = mw.text.trim(frame.args2 or "")

	local input_parm = mw.text.trim(frame.args3 or "")

	if inputparm == "FETCHWIKIDATA" then

		local entity = mw.wikibase.getEntity(itemID)

		local claims

		if entity and entity.claims then

			claims = entity.claimspropertyID

		end

		if claims then

			return getValue(entity, claims, propertyID, ", ")

		else

			return ""

		end

	else

		return input_parm

	end

end

local function getQualifier(frame, outputHook)

	local propertyID = mw.text.trim(frame.args1 or "")

	local qualifierID = mw.text.trim(frame.args2 or "")

	local input_parm = mw.text.trim(frame.args3 or "")

	if inputparm == "FETCHWIKIDATA" then

		local entity = mw.wikibase.getEntityObject

		if entity.claimspropertyID ~= nil then

			local out =

			for k, v in pairs(entity.claimspropertyID) do

				for k2, v2 in pairs(v.qualifiersqualifierID) do

					if v2.snaktype == 'value' then

						out#out + 1 = outputHook(v2);

					end

				end

			end

			return table.concat(out, "、"), true

		else

			return "", false

		end

	else

		return input_parm, false

	end

end

p.getQualifierValue = function(frame)

	local function outputValue(value)

		local qnumber = "Q" .. value.datavalue.value"numeric-id"

		if (mw.wikibase.sitelink(qnumber)) then

			return "" .. mw.wikibase.sitelink(qnumber) .. ""

		else

			return "" ..qnumber .. "*"

		end

	end

	return (getQualifier(frame, outputValue))

end

-- This is used to get a value like 'male' (for property p21) which won't be linked and numbers without the thousand separators

p.getRawValue = function(frame)

	local go, errorOrentity, claims, propertyID = parseInput(frame)

	if not go then

		return errorOrentity

	end

	local entity = errorOrentity

	local result = entity:formatPropertyValues(propertyID, mw.wikibase.entity.claimRanks).value

	-- if number type: remove thousand separators, bounds and units

	if isType(claims, "quantity") then

		result = mw.ustring.gsub(result, "(%d),(%d)", "%1%2")

		result = mw.ustring.gsub(result, "(%d)±.*", "%1")

	end

	return result

end

-- This is used to get the unit name for the numeric value returned by getRawValue

p.getUnits = function(frame)

	local go, errorOrentity, claims, propertyID = parseInput(frame)

	if not go then

		return errorOrentity

	end

	local entity = errorOrentity

	local result = entity:formatPropertyValues(propertyID, mw.wikibase.entity.claimRanks).value

	if isType(claims, "quantity") then

		result = mw.ustring.sub(result, mw.ustring.find(result, " ")+1, -1)

	end

	return result

end

-- This is used to get the unit's QID to use with the numeric value returned by getRawValue

p.getUnitID = function(frame)

	local go, errorOrentity, claims = parseInput(frame)

	if not go then

		return errorOrentity

	end

	local entity = errorOrentity

	local result

	if isType(claims, "quantity") then

		-- get the url for the unit entry on Wikidata:

		result = claims1.mainsnak.datavalue.value.unit

		-- and just reurn the last bit from "Q" to the end (which is the QID):

		result = mw.ustring.sub(result, mw.ustring.find(result, "Q"), -1)

	end

	return result

end

p.getRawQualifierValue = function(frame)

	local function outputHook(value)

		if value.datavalue.value"numeric-id" then

			return mw.wikibase.label("Q" .. value.datavalue.value"numeric-id")

		else

			return value.datavalue.value

		end

	end

	local ret, gotData = getQualifier(frame, outputHook)

	if gotData then

		ret = string.upper(string.sub(ret, 1, 1)) .. string.sub(ret, 2)

	end

	return ret

end

-- This is used to get a date value for dateofbirth (P569), etc. which won't be linked

-- Dates and times are stored in ISO 8601 format (sort of).

-- At present the local formatDate(date, precision, timezone) function doesn't handle timezone

-- So I'll just supply "Z" in the call to formatDate below:

p.getDateValue = function(frame)

	local date_format = mw.text.trim(frame.args3 or i18n"datetime""default-format")

	local date_addon = mw.text.trim(frame.args4 or i18n"datetime""default-addon")

	local go, errorOrentity, claims = parseInput(frame)

	if not go then

		return errorOrentity

	end

	local entity = errorOrentity

	local out =

	for k, v in pairs(claims) do

		if v.mainsnak.datavalue.type == 'time' then

			local timestamp = v.mainsnak.datavalue.value.time

			local dateprecision = v.mainsnak.datavalue.value.precision

			-- A year can be stored like this: "+1872-00-00T00:00:00Z",

			-- which is processed here as if it were the day before "+1872-01-01T00:00:00Z",

			-- and that's the last day of 1871, so the year is wrong.

			-- So fix the month 0, day 0 timestamp to become 1 January instead:

			timestamp = timestamp:gsub("%-00%-00T", "-01-01T")

			out#out + 1 = parseDateFull(timestamp, dateprecision, dateformat, dateaddon)

		end

	end

	return table.concat(out, "、")

end

p.getQualifierDateValue = function(frame)

	local date_format = mw.text.trim(frame.args4 or i18n"datetime""default-format")

	local date_addon = mw.text.trim(frame.args5 or i18n"datetime""default-addon")

	local function outputHook(value)

		local timestamp = value.datavalue.value.time

		return parseDateValue(timestamp, dateformat, dateaddon)

	end

	return (getQualifier(frame, outputHook))

end

-- This is used to fetch all of the images with a particular property, e.g. image (P18), Gene Atlas Image (P692), etc.

-- Parameters are | propertyID | value / FETCH_WIKIDATA / nil | separator (default=space) | size (default=frameless)

-- It will return a standard wiki-markup for each image with a selectable size and separator (which may be html)

-- e.g.

-- e.g.

-- If a property is chosen that is not of type "commonsMedia", it will return empty text.

p.getImages = function(frame)

	local sep = mw.text.trim(frame.args3 or " ")

	local imgsize = mw.text.trim(frame.args4 or "frameless")

	local go, errorOrentity, claims = parseInput(frame)

	if not go then

		return errorOrentity

	end

	local entity = errorOrentity

	if (claims1 and claims1.mainsnak.datatype == "commonsMedia") then

		local out =

		for k, v in pairs(claims) do

			local filename = v.mainsnak.datavalue.value

			out#out + 1 = ""

		end

		return table.concat(out, sep)

	else

		return ""

	end

end

-- This is used to get the TA98 (Terminologia Anatomica first edition 1998) values like 'A01.1.00.005' (property P1323)

-- which are then linked to http://www.unifr.ch/ifaa/Public/EntryPage/TA98%20Tree/Entity%20TA98%20EN/01.1.00.005%20Entity%20TA98%20EN.htm

-- uses the newer mw.wikibase calls instead of directly using the snaks

-- formatPropertyValues returns a table with the P1323 values concatenated with ", " so we have to split them out into a table in order to construct the return string

p.getTAValue = function(frame)

	local ent = mw.wikibase.getEntityObject

	local props = ent:formatPropertyValues('P1323')

	local out =

	local t =

	for k, v in pairs(props) do

		if k == 'value' then

			t = mw.text.split( v, ", ")

			for k2, v2 in pairs(t) do

				out#out + 1 = " .. string.sub(v2, 2) .. "%20Entity%20TA98%20EN.htm " .. v2 .. ""

			end

		end

	end

	local ret = table.concat(out, " ")

	if #ret == 0 then

		ret = "Invalid TA"

	end

	return ret

end

--

This is used to return an image legend from Wikidata

image is property P18

image legend is property P2096

Call as

Returns PARAMETER, unless it is equal to "FETCH_WIKIDATA", from Item QID (expensive call)

If QID is omitted or blank, the current article is used (not an expensive call)

If lang is omitted, it uses the local wiki language, otherwise it uses the provided ISO-639 language code

ISO-639: https://docs.oracle.com/cd/E13214_01/wli/docs92/xref/xqisocodes.html#wp1252447

Ranks are: 'preferred' > 'normal'

This returns the label from the first image with 'preferred' rank

Or the label from the first image with 'normal' rank if preferred returns nothing

Ranks: https://www.mediawiki.org/wiki/Extension:Wikibase_Client/Lua

p.getImageLegend = function(frame)

	-- look for named parameter id; if it's blank make it nil

	local id = frame.args.id

	if id and (#id == 0) then

		id = nil

	end

	-- look for named parameter lang

	-- it should contain a two-character ISO-639 language code

	-- if it's blank fetch the language of the local wiki

	local lang = frame.args.lang

	if (not lang) or (#lang "..mw.dumpObject(data).."".. i18n.warnDump

		end

		data = dataindex or datatonumber(index)

		if not data then

			return i18n.warnDump

		end

		i = i + 1

	end

end

return p