Модуль:WDBase

Материал из Википедии — свободной энциклопедии
Это старая версия этой страницы, сохранённая D6194c-1cc (обсуждение | вклад) в 08:20, 11 июня 2024 (Указаны общие требования по части документации). Она может серьёзно отличаться от текущей версии.
Перейти к навигации Перейти к поиску
Документация

Модуль-библиотека с базовыми функциями для работы с Викиданными. Содержит функции для получения утверждений, фильтрации, получения значений утверждений, приведения утверждений к текстовому виду.

Модуль используется другим, более высокоуровневым модулем-библиотекой Модуль:WDCommon. на модуле WDBase основаны модули Модуль:WDBackend и Модуль:WDFormat.

Функции

В функциях используются понятия значения (value) и текстового представления (text). Значение может быть текстом, таблицей с датой или идентификатором элемента Викиданных. Текстовое представление предполагает преобразование значения в текстовый вид, которы уже можно использовать для отображения.

Получаемые из Викиданных данные представлены в специальном сериализованном формате. В этом формате есть понятия утверждения (statement) и снека (snak). Утверждения — это значения для заданного свойства с дополнительными уточняющими данными и источниками. Снеки — часть утверждений, у которых есть только значение. Каждое утверждение может иметь в себе основной снек и квалификаторы. Данные в квалификаторах представлены только лишь снеками.

Работа с элементом Викиданных:

  • statements — получить массив утверждений для указанного свойства.
  • statementsByProperties — получить массив утверждений для указанных свойств, скомбинировав их (учитываются порядковые номера, указанные через квалификаторы).
  • value — получить значение свойства элемента Викиданных (берётся первое утверждение).
  • text — получить текстовое представление свойства элемента Викиданных (берётся первое утверждение).
  • wikilink — получить викиссылку на статью по элементу Викиданных.
  • resolveParent — получить основной элемент Викиданных (издание, на котором основано текущее издание).
  • instanceOf — проверить является ли элемент Викиданных частным случаем одной из указанных сущностей.
  • valueByQualifier — получить значение утверждения, которому соответствует квалификатор с указанным свойством и значением.

Работа с утверждениями:

  • valueByStatement — получить значение из утверждения.
  • textByStatement — привести утверждение к текстовому виду.
  • dataByStatement — получить таблицу с данными в удобном для использования формате из утверждения.
  • statementQualifier — получить снек квалификатора по идентификатору свойства.
  • tryFilterStatementsByLang — попытаться отфильтровать утверждения по языку. Если заданного языка не найдено, возвращает все утверждения. Если передан аргумент forceLang, то не возвращает утверждений, если по указанному языку они не найдены.
  • filterStatementsByUnit — отфильтровать утверждения по QID единицы измерения.

Работа со снеками:

  • valueBySnak — получить значение из снека.
  • dataBySnak — получить таблицу с данными в удобном для использования формате из снека.
  • tryFilterSnaksByLang — попытаться отфильтровать снеки по языку. Если заданного языка не найдено, возвращает все снеки.

Работа с типами:

  • dateFromDatavalue — получить таблицу с распарсенной датой из данных снека.
  • dateToStr — преобразовать таблицу с датой в строку.

Внесение изменений

При исправлении ошибки, пожалуйста, сначала добавьте тест, который будет проваливаться из-за обнаруженной ошибки, и только затем вносите исправление. При внесении исправления проверьте, чтобы все тесты проходили. Вносить исправление можно только, если оно не ломает другие тесты.

Добавление нового функционала рекомендуется делать у себя в песочнице, скопировав в неё модуль. В правке копирования необходимо указать тот факт, что делается копирование, и сделать ссылку на оригинальный модуль в виде викитекста. При добавлении нового функционала сначала желательно добавить тест на этот функционал, затем добавить сам функционал, убедившись, что все тесты при этом проходят.

Тесты

N 1 тест провалился.

Название Ожидается Фактически
N test_dataByStatement Модуль:WDBase/testcases:50: More unit symbols expected.

Failed to assert that false is true

✔ test_dateFromDatavalue
✔ test_filterStatementsByUnit
✔ test_instanceOf
✔ test_resolveParent
✔ test_searchStatementByValue
✔ test_statements
✔ test_statementsByProperties
✔ test_text
✔ test_tryFilterSnaksByLang
✔ test_tryFilterStatementsByLang
✔ test_value
✔ test_wikilink


План разработки

-- The documentation for the module should be written in English in the LuaDoc
-- format to ease updating the module between different Wikimedia projects.
-- The localized version of the documentation can be added to the documentation
-- page of the module.

require('strict')

local p = {}

p.P_NAME_WORK_LANG = 'P407'
p.P_EDITION_FOR = 'P629'
p.P_OBJ_NAMED_AS = 'P1932'
p.P_ORDINAL_NUM = 'P1545'

p.Q_MULTILANG = 'Q20923490'

local defaultLangObj = mw.getContentLanguage()
local defaultLang = defaultLangObj:getCode()

local fallbackLang = 'en'

function p.statements(entity, property, cache)
	if cache then
		local entityCache = cache[entity]
		if entityCache then
			local props = entityCache.props or entityCache
			local statements = props[property]
			if statements then
				return statements
			end
		end
	end

	local statements = mw.wikibase.getBestStatements(entity, property)
	if not statements or next(statements) == nil then
		return nil
	end

	return statements
end

function p.statementsByProperties(entity, properties)
	local orderedStatements = {}
	local statements = {}
	for _, property in ipairs(properties) do
		local currStatements = mw.wikibase.getBestStatements(entity, property)
		if currStatements and next(currStatements) ~= nil then
			for _, statement in ipairs(currStatements) do
				local num
				local qualifiers = statement.qualifiers
				if qualifiers then
					local qualifierSnaks = qualifiers[p.P_ORDINAL_NUM]
					if qualifierSnaks then
						local datavalue = qualifierSnaks[1].datavalue
						if datavalue and datavalue.type == 'string' then
							num = tonumber(datavalue.value)
						end
					end
				end
				if num then
					orderedStatements[num] = statement
				else
					table.insert(statements, statement)
				end
			end
		end
	end
	
	if next(orderedStatements) == nil then
		return statements
	elseif next(statements) == nil then
		return orderedStatements
	end

	-- We assume that all statements must be either ordered or unordered, so
	-- all other cases must be fixed in Wikidata, and the code below allowed
	-- to be slow (rare exceptional cases)
	local j = 1
	for i = 1, table.getn(orderedStatements) do
		if orderedStatements[i] == nil then
			orderedStatements[i] = statements[j]
			j = j + 1
			if statements[j] == nil then
				break;
			end
		end
	end
	for i = j, table.getn(statements) do
		table.insert(statements, statements[i])
	end

	return orderedStatements
end

function p.valueBySnak(snak)
	local datavalue = snak.datavalue
	if not datavalue then
		return nil
	end

	if datavalue.type == 'monolingualtext' then
		return datavalue.value.text, datavalue.value.language
	elseif datavalue.type == 'wikibase-entityid' then
		return datavalue.value.id
	elseif datavalue.type == 'string' then
		return datavalue.value
	elseif datavalue.type == 'time' then
		return p.dateFromDatavalue(datavalue)
	elseif datavalue.type == 'quantity' then
		local unitEntity
		if datavalue.value.unit then
			unitEntity = datavalue.value.unit:gsub('^.+/([^/]+)$', '%1')
		end
		return datavalue.value.amount, unitEntity
	end
end

function p.valueByStatement(statement)
	local snak = statement.mainsnak
	if not snak then
		return nil
	end

	return p.valueBySnak(snak)
end

function p.textByStatement(statement, lang)
	local datavalue = statement.mainsnak.datavalue
	if not datavalue then
		return nil
	end

	if datavalue.type == 'monolingualtext' then
		return datavalue.value.text, datavalue.value.language
	elseif datavalue.type == 'wikibase-entityid' then
		local entity = datavalue.value.id
		if lang then
			return mw.wikibase.getLabelByLang(entity, lang), lang
		else
			return mw.wikibase.getLabelWithLang(entity)
		end
	elseif datavalue.type == 'string' then
		return datavalue.value
	elseif datavalue.type == 'time' then
		return p.dateToStr(p.dateFromDatavalue(datavalue))
	elseif datavalue.value.amount == 'quantity' then
		local unit
		local valueLang
		if lang then
			unit = mw.wikibase.getLabelByLang(entity, lang)
			valueLang = lang
		else
			unit, valueLang = mw.wikibase.getLabelWithLang(entity)
		end
		return datavalue.value.amount .. ' ' .. datavalue.value.unit, valueLang
	end
end

function p.value(entity, property)
	local statements = p.statements(entity, property)
	if not statements then
		return nil
	end

	return p.valueByStatement(statements[1])
end

function p.text(entity, property, lang)
	local statements = p.statements(entity, property)
	if not statements then
		return nil
	end

	return p.textByStatement(statements[1], lang)
end

function p.valueByQualifier(entity, property, qualifier, qualifierValue)
	local statements = p.statements(entity, property)
	if not statements then
		return nil
	end

	for _, statement in ipairs(statements) do
		if statement.mainsnak and statement.qualifiers then
			local currQualifiers = statement.qualifiers[qualifier]
			if currQualifiers and currQualifiers[1] then
				local currQualifier = currQualifiers[1]
				local value = p.valueBySnak(currQualifier)
				if value == qualifierValue then
					return p.valueByStatement(statement)
				end
			end
		end
	end

	return nil
end

function p.tryFilterSnaksByLang(snaks, lang)
	if not lang then
		lang = defaultLang
	end

	local suitableValueInCurrLang = {}
	local suitableValueInFallbackLang = {}
	local suitableValue = {}
	for _, snak in ipairs(snaks) do
		local datavalue = snak.datavalue
		if datavalue.type == 'monolingualtext' then
			if datavalue.value.language == lang then
				table.insert(suitableValue, snak)
			elseif not suitableValueInCurrLang then
				if datavalue.value.language == fallbackLang then
					table.insert(suitableValueInCurrLang, snak)
				end
			elseif not suitableValueInFallbackLang then
				if datavalue.value.language == fallbackLang then
					table.insert(suitableValueInFallbackLang, snak)
				end
			end
		elseif datavalue.type == 'string' and not suitableValue then
			table.insert(suitableValue, snak)
		end
	end
		
	if next(suitableValue) ~= nil then
		return suitableValue
	elseif next(suitableValueInCurrLang) ~= nil then
		return suitableValueInCurrLang
	elseif next(suitableValueInFallbackLang) ~= nil then
		return suitableValueInFallbackLang
	else
		return snaks
	end
end

function p.tryFilterStatementsByLang(statements, lang, forceLang)
	if not lang then
		lang = defaultLang
	end

	local suitableValueInCurrLang = {}
	local suitableValueInFallbackLang = {}
	local suitableValue = {}
	for _, statement in ipairs(statements) do
		local datavalue = statement.mainsnak.datavalue
		if datavalue.type == 'monolingualtext' then
			if datavalue.value.language == lang then
				table.insert(suitableValue, statement)
			elseif not forceLang then
				if not suitableValueInCurrLang then
					if datavalue.value.language == fallbackLang then
						table.insert(suitableValueInCurrLang, statement)
					end
				elseif not suitableValueInFallbackLang then
					if datavalue.value.language == fallbackLang then
						table.insert(suitableValueInFallbackLang, statement)
					end
				end
			end
		elseif datavalue.type == 'string' and not suitableValue then
			table.insert(suitableValue, statement)
		end
	end
		
	if next(suitableValue) ~= nil then
		return suitableValue
	elseif next(suitableValueInCurrLang) ~= nil then
		return suitableValueInCurrLang
	elseif next(suitableValueInFallbackLang) ~= nil then
		return suitableValueInFallbackLang
	elseif not forceLang then
		return statements
	else
		return nil
	end
end

function p.filterStatementsByUnit(statements, unit)
	local filteredStatements = {}
	for _, statement in ipairs(statements) do
		local snak = statement.mainsnak
		if snak then
			local datavalue = snak.datavalue
			if datavalue and datavalue.type == 'quantity' then
				local currUnit = datavalue.value.unit
				if currUnit then
					currUnit = currUnit:match('^.*/([^/]+)')
					if currUnit == unit then
						table.insert(filteredStatements, statement)
					end
				end
			end
		end
	end
		
	if next(filteredStatements) == nil then
		return nil
	end
	
	return filteredStatements
end

function p.tryTextByLang(entity, property, lang)
	local statements = p.statements(entity, property)
	if not statements then
		return nil
	end

	statements = p.tryFilterStatementsByLang(statements, lang, false)
	return p.textByStatement(statements[1])
end

function p.textByLang(entity, property, lang)
	local statements = p.statements(entity, property)
	if not statements then
		return nil
	end

	statements = p.tryFilterStatementsByLang(statements, lang, true)
	if not statements then
		return nil
	end
	return p.textByStatement(statements[1])
end

function p.searchStatementByValue(statements, value)
	for _, statement in ipairs(statements) do
		local datavalue = statement.mainsnak.datavalue
		if datavalue.type == 'string' then
			if datavalue.value == value then
				return statement
			end
		elseif datavalue.type == 'monolingualtext' then
			if datavalue.value.text == value then
				return statement
			end
		elseif datavalue.type == 'wikibase-entityid' then
			if datavalue.value.id == value then
				return statement
			end
		elseif datavalue.type == 'quantity' then
			if tonumber(datavalue.value.amount) == value then
				return statement
			end
		end
	end
end

function p.statementByValue(entity, property, value)
	local statements = p.statements(entity, property)
	if not statements then
		return nil
	end

	return p.searchStatementByValue(statements, value)
end

function p.statementQualifier(statement, quailiferProperty)
	if not statement.qualifiers then
		return nil
	end

	local qualifiers = statement.qualifiers[quailiferProperty]
	if not qualifiers then
		return nil
	end

	return p.valueBySnak(qualifiers[1])
end

function p.resolveParent(entity)
	local statements = p.statements(entity, p.P_EDITION_FOR)
	if not statements then
		return nil
	end

	local datavalue = statements[1].mainsnak.datavalue
	if datavalue.type == 'wikibase-entityid' then
		return datavalue.value.id
	end

	return nil
end

--- Get a wikilink to a page describing the given Wikidata entity
-- The function returns a wikilink to a page corresponding to the Wikidata
-- entity. The Wikilink will lead to the page in the local wiki project.
-- If the text of the wikilink is not specified, then the label
-- of the Wikidata entity will be used.
-- @param entity The QID of the Wikidata entity.
-- @text text The text of the resulting wikilink (optional).
-- @returns The code of the resulting Wikilink.
-- 
function p.wikilink(entity, text)
	local title = mw.wikibase.getSitelink(entity)
	if not title then
		return nil
	end

	local wikilink
	if text and text ~= title then
		wikilink = '[[' .. title .. '|' .. text .. ']]'
	else
		wikilink = '[[' .. title .. ']]'
	end
	return wikilink, entity
end

function p.dateFromDatavalue(datavalue)
	if datavalue.type ~= 'time' then
		return nil
	end

	local precision = datavalue.value.precision

	local date = {}
	date.timestamp = datavalue.value.time
	local sign
	sign, date.year, date.month, date.day, date.hour, date.minute, date.second = string.match(date.timestamp, '([+-])(%d+)-(%d+)-(%d+)T(%d+):(%d+):(%d+)')
	date.year = tonumber(date.year)
	date.month = tonumber(date.month)
	date.day = tonumber(date.day)
	date.hour = tonumber(date.hour)
	date.minute = tonumber(date.minute)
	date.second = tonumber(date.second)
	if sign == '-' then
		date.year = -date.year
	end

	if precision < 14 then
		date.second = nil
	end
	if precision < 13 then
		date.minute = nil
	end
	if precision < 12 then
		date.hour = nil
	end
	if precision < 11 then
		date.day = nil
	end
	if precision < 10 then
		date.month = nil
	end
	if precision == 8 then
		date.decade = date.year
	end
	if precision == 7 then
		date.century = date.year / 100
	end
	if precision == 6 then
		date.millenium = date.year / 1000
	end

	return date
end

function p.dateToStr(date, lang)
	local langObj = mw.getLanguage(lang)
	if date.second then
		return langObj:formatDate('d xg Y H:i:s', date.timestamp)
	elseif date.minute then
		return langObj:formatDate('d xg Y H:i', date.timestamp)
	elseif date.hour then
		return langObj:formatDate('d xg Y H:00', date.timestamp)
	elseif date.day then
		return langObj:formatDate('d xg Y', date.timestamp)
	elseif date.month then
		return langObj:formatDate('F Y', date.timestamp)
	elseif date.year then
		return langObj:formatDate('Y', date.timestamp)
	end
	return date.timestamp
end

function p.instanceOf(entity, property, ofEntities)
	local statements = p.statements(entity, property)
	if not statements then
		return nil
	end

	for i=1, table.getn(statements) do
		local statement = statements[i]
		if statement.mainsnak then
			local datavalue = statement.mainsnak.datavalue
			if datavalue and datavalue.type == 'wikibase-entityid' then
				local currEntity = datavalue.value.id
				for j=1, table.getn(ofEntities) do
					if currEntity == ofEntities[i] then
						return currEntity
					end
				end
			end
		end
	end

	return nil
end

function p.dataBySnak(snak, lang, cache)
	local datavalue = snak.datavalue
	if not datavalue then
		return nil
	end

	local data = {}

	if datavalue.type == 'wikibase-entityid' then
		data.entity = datavalue.value.id
		if lang then
			local cachedFound = false
			if cache then
				local cached = cache[data.entity]
				if cached and cached.label then
					local label = cached.label[self.lang]
					if label then
						cachedFound = true
						data.value = label
					end
				end
			end
			if not cachedFound then
				data.value = mw.wikibase.getLabelByLang(data.entity, lang)
			end
			data.lang = lang
			data.fromLabel = true
		else
			data.value, data.lang = mw.wikibase.getLabelWithLang(data.entity)
			data.fromLabel = true
		end
	elseif datavalue.type == 'monolingualtext' then
		data.value = datavalue.value.text
		data.lang = datavalue.value.language
	elseif datavalue.type == 'time' then
		data.value = p.dateFromDatavalue(datavalue)
	elseif datavalue.type == 'quantity' then
		data.value = tonumber(datavalue.value.amount)
		data.unitEntity = datavalue.value.unit
		if data.unitEntity then
			data.unitEntity = data.unitEntity:match('^.*/([^/]+)')
		end
	else
		data.value = datavalue.value
	end

	return data
end

function p.dataByStatement(statement, lang, cache, novalueQualifier)
	local snak = statement.mainsnak
	if not snak then
		return nil
	end
	if not snak.datavalue then
		if not novalueQualifier then
			novalueQualifier = p.P_OBJ_NAMED_AS
		end
		if statement.qualifiers then
			local qualifierSnaks = statement.qualifiers[novalueQualifier]
			if qualifierSnaks then
				return p.dataBySnak(qualifierSnaks[1], lang, cache)
			end
		end
		return nil
	end

	return p.dataBySnak(snak, lang, cache)
end

function p.dataByEntity(entity, lang, cache)
	local value, valueLang
	if lang then
		local cachedFound = false
		if cache then
			local cached = cache[entity]
			if cached and cached.label then
				local label = cached.label[lang]
				if label then
					cachedFound = true
					value = label
				end
			end
		end
		if not cachedFound then
			value = mw.wikibase.getLabelByLang(entity, lang)
		end
		valueLang = lang
	else
		value, valueLang = mw.wikibase.getLabelWithLang(entity)
	end

	return { value = value, lang = valueLang, entity = entity, fromLabel = true }
end

return p