وحدة:Wd

من موسوعة المزرعة
اذهب إلى التنقل اذهب إلى البحث

يمكن إنشاء صفحة توثيق الوحدة في وحدة:Wd/شرح

-- Original module located at [[:en:Module:Wd]] and [[:en:Module:Wd/i18n]].

local p = {}
local arg = ...
local i18n

local function loadI18n(aliasesP, frame)
	local title

	if frame then
		-- current module invoked by page/template, get its title from frame
		title = frame:getTitle()
	else
		-- current module included by other module, get its title from ...
		title = arg
	end

	if not i18n then
		i18n = require(title .. "/i18n").init(aliasesP)
	end
end

p.claimCommands = {
	property   = "property",
	properties = "properties",
	qualifier  = "qualifier",
	qualifiers = "qualifiers",
	reference  = "reference",
	references = "references"
}

p.generalCommands = {
	label       = "label",
	title       = "title",
	description = "description",
	alias       = "alias",
	aliases     = "aliases",
	badge       = "badge",
	badges      = "badges"
}

p.flags = {
	linked        = "linked",
	short         = "short",
	raw           = "raw",
	multilanguage = "multilanguage",
	unit          = "unit",
	-------------
	preferred     = "preferred",
	normal        = "normal",
	deprecated    = "deprecated",
	best          = "best",
	future        = "future",
	current       = "current",
	former        = "former",
	edit          = "edit",
	editAtEnd     = "edit@end",
	mdy           = "mdy",
	single        = "single",
	sourced       = "sourced"
}

p.args = {
	eid  = "eid",
	page = "page",
	date = "date"
}

local aliasesP = {
	coord                   = "P625",
	-----------------------
	image                   = "P18",
	author                  = "P50",
	publisher               = "P123",
	importedFrom            = "P143",
	statedIn                = "P248",
	pages                   = "P304",
	language                = "P407",
	hasPart                 = "P527",
	publicationDate         = "P577",
	startTime               = "P580",
	endTime                 = "P582",
	chapter                 = "P792",
	retrieved               = "P813",
	referenceURL            = "P854",
	sectionVerseOrParagraph = "P958",
	archiveURL              = "P1065",
	title                   = "P1476",
	formatterURL            = "P1630",
	quote                   = "P1683",
	shortName               = "P1813",
	definingFormula         = "P2534",
	archiveDate             = "P2960",
	inferredFrom            = "P3452",
	typeOfReference         = "P3865",
	column                  = "P3903"
}

local aliasesQ = {
	percentage              = "Q11229",
	prolepticJulianCalendar = "Q1985786",
	citeWeb                 = "Q5637226",
	citeQ                   = "Q22321052"
}

local parameters = {
	property  = "%p",
	qualifier = "%q",
	reference = "%r",
	alias     = "%a",
	badge     = "%b",
	separator = "%s",
	general   = "%x"
}

local formats = {
	property              = "%p[%s][%r]",
	qualifier             = "%q[%s][%r]",
	reference             = "%r",
	propertyWithQualifier = "%p[ <span style=\"font-size:85\\%\">(%q)</span>][%s][%r]",
	alias                 = "%a[%s]",
	badge                 = "%b[%s]"
}

local hookNames = {              -- {level_1, level_2}
	[parameters.property]         = {"getProperty"},
	[parameters.reference]        = {"getReferences", "getReference"},
	[parameters.qualifier]        = {"getAllQualifiers"},
	[parameters.qualifier.."\\d"] = {"getQualifiers", "getQualifier"},
	[parameters.alias]            = {"getAlias"},
	[parameters.badge]            = {"getBadge"}
}

-- default value objects, should NOT be mutated but instead copied
local defaultSeparators = {
	["sep"]      = {" "},
	["sep%s"]    = {","},
	["sep%q"]    = {"; "},
	["sep%q\\d"] = {", "},
	["sep%r"]    = nil,  -- none
	["punc"]     = nil   -- none
}

local rankTable = {
	["preferred"]  = 1,
	["normal"]     = 2,
	["deprecated"] = 3
}

local Config = {}

-- allows for recursive calls
function Config:new()
	local cfg = {}
	setmetatable(cfg, self)
	self.__index = self

	cfg.separators = {
		-- single value objects wrapped in arrays so that we can pass by reference
		["sep"]   = {copyTable(defaultSeparators["sep"])},
		["sep%s"] = {copyTable(defaultSeparators["sep%s"])},
		["sep%q"] = {copyTable(defaultSeparators["sep%q"])},
		["sep%r"] = {copyTable(defaultSeparators["sep%r"])},
		["punc"]  = {copyTable(defaultSeparators["punc"])}
	}

	cfg.entity = nil
	cfg.entityID = nil
	cfg.propertyID = nil
	cfg.propertyValue = nil
	cfg.qualifierIDs = {}
	cfg.qualifierIDsAndValues = {}

	cfg.bestRank = true
	cfg.ranks = {true, true, false}  -- preferred = true, normal = true, deprecated = false
	cfg.foundRank = #cfg.ranks
	cfg.flagBest = false
	cfg.flagRank = false

	cfg.periods = {true, true, true}  -- future = true, current = true, former = true
	cfg.flagPeriod = false
	cfg.atDate = {parseDate(os.date('!%Y-%m-%d'))}  -- today as {year, month, day}

	cfg.mdyDate = false
	cfg.singleClaim = false
	cfg.sourcedOnly = false
	cfg.editable = false
	cfg.editAtEnd = false

	cfg.inSitelinks = false

	cfg.langCode = mw.language.getContentLanguage().code
	cfg.langName = mw.language.fetchLanguageName(cfg.langCode, cfg.langCode)
	cfg.langObj = mw.language.new(cfg.langCode)

	cfg.siteID = mw.wikibase.getGlobalSiteId()

	cfg.states = {}
	cfg.states.qualifiersCount = 0
	cfg.curState = nil

	cfg.prefetchedRefs = nil

	return cfg
end

local State = {}

function State:new(cfg, type)
	local stt = {}
	setmetatable(stt, self)
	self.__index = self

	stt.conf = cfg
	stt.type = type

	stt.results = {}

	stt.parsedFormat = {}
	stt.separator = {}
	stt.movSeparator = {}
	stt.puncMark = {}

	stt.linked = false
	stt.rawValue = false
	stt.shortName = false
	stt.anyLanguage = false
	stt.unitOnly = false
	stt.singleValue = false

	return stt
end

local function replaceAlias(id)
	if aliasesP[id] then
		id = aliasesP[id]
	end

	return id
end

local function errorText(code, param)
	local text = i18n["errors"][code]
	if param then text = mw.ustring.gsub(text, "$1", param) end
	return text
end

local function throwError(errorMessage, param)
	error(errorText(errorMessage, param))
end

local function replaceDecimalMark(num)
	return mw.ustring.gsub(num, "[.]", i18n['numeric']['decimal-mark'], 1)
end

local function padZeros(num, numDigits)
	local numZeros
	local negative = false

	if num < 0 then
		negative = true
		num = num * -1
	end

	num = tostring(num)
	numZeros = numDigits - num:len()

	for _ = 1, numZeros do
		num = "0"..num
	end

	if negative then
		num = "-"..num
	end

	return num
end

local function replaceSpecialChar(chr)
	if chr == '_' then
		-- replace underscores with spaces
		return ' '
	else
		return chr
	end
end

local function replaceSpecialChars(str)
	local chr
	local esc = false
	local strOut = ""

	for i = 1, #str do
		chr = str:sub(i,i)

		if not esc then
			if chr == '\\' then
				esc = true
			else
				strOut = strOut .. replaceSpecialChar(chr)
			end
		else
			strOut = strOut .. chr
			esc = false
		end
	end

	return strOut
end

local function buildWikilink(target, label)
	if not label or target == label then
		return "[[" .. target .. "]]"
	else
		return "[[" .. target .. "|" .. label .. "]]"
	end
end

-- used to make frame.args mutable, to replace #frame.args (which is always 0)
-- with the actual amount and to simply copy tables
function copyTable(tIn)
	if not tIn then
		return nil
	end

	local tOut = {}

	for i, v in pairs(tIn) do
		tOut[i] = v
	end

	return tOut
end

-- used to merge output arrays together;
-- note that it currently mutates the first input array
local function mergeArrays(a1, a2)
	for i = 1, #a2 do
		a1[#a1 + 1] = a2[i]
	end

	return a1
end

local function split(str, del)
	local out = {}
	local i, j = str:find(del)

	if i and j then
		out[1] = str:sub(1, i - 1)
		out[2] = str:sub(j + 1)
	else
		out[1] = str
	end

	return out
end

local function parseWikidataURL(url)
	local id

	if url:match('^http[s]?://') then
		id = split(url, "Q")

		if id[2] then
			return "Q" .. id[2]
		end
	end

	return nil
end

function parseDate(dateStr, precision)
	precision = precision or "d"

	local i, j, index, ptr
	local parts = {nil, nil, nil}

	if dateStr == nil then
		return parts[1], parts[2], parts[3]  -- year, month, day
	end

	-- 'T' for snak values, '/' for outputs with '/Julian' attached
	i, j = dateStr:find("[T/]")

	if i then
		dateStr = dateStr:sub(1, i-1)
	end

	local from = 1

	if dateStr:sub(1,1) == "-" then
		-- this is a negative number, look further ahead
		from = 2
	end

	index = 1
	ptr = 1

	i, j = dateStr:find("-", from)

	if i then
		-- year
		parts[index] = tonumber(mw.ustring.gsub(dateStr:sub(ptr, i-1), "^\+(.+)$", "%1"), 10)  -- remove '+' sign (explicitly give base 10 to prevent error)

		if parts[index] == -0 then
			parts[index] = tonumber("0")  -- for some reason, 'parts[index] = 0' may actually store '-0', so parse from string instead
		end

		if precision == "y" then
			-- we're done
			return parts[1], parts[2], parts[3]  -- year, month, day
		end

		index = index + 1
		ptr = i + 1

		i, j = dateStr:find("-", ptr)

		if i then
			-- month
			parts[index] = tonumber(dateStr:sub(ptr, i-1), 10)

			if precision == "m" then
				-- we're done
				return parts[1], parts[2], parts[3]  -- year, month, day
			end

			index = index + 1
			ptr = i + 1
		end
	end

	if dateStr:sub(ptr) ~= "" then
		-- day if we have month, month if we have year, or year
		parts[index] = tonumber(dateStr:sub(ptr), 10)
	end

	return parts[1], parts[2], parts[3]  -- year, month, day
end

local function datePrecedesDate(aY, aM, aD, bY, bM, bD)
	if aY == nil or bY == nil then
		return nil
	end
	aM = aM or 1
	aD = aD or 1
	bM = bM or 1
	bD = bD or 1

	if aY < bY then
		return true
	end

	if aY > bY then
		return false
	end

	if aM < bM then
		return true
	end

	if aM > bM then
		return false
	end

	if aD < bD then
		return true
	end

	return false
end

local function getHookName(param, index)
	if hookNames[param] then
		return hookNames[param][index]
	elseif param:len() > 2 then
		return hookNames[param:sub(1, 2).."\\d"][index]
	else
		return nil
	end
end

local function alwaysTrue()
	return true
end

-- The following function parses a format string.
--
-- The example below shows how a parsed string is structured in memory.
-- Variables other than 'str' and 'child' are left out for clarity's sake.
--
-- Example:
-- "A %p B [%s[%q1]] C [%r] D"
--
-- Structure:
-- [
--   {
--     str = "A "
--   },
--   {
--     str = "%p"
--   },
--   {
--     str = " B ",
--     child =
--     [
--       {
--         str = "%s",
--         child =
--         [
--           {
--             str = "%q1"
--           }
--         ]
--       }
--     ]
--   },
--   {
--     str = " C ",
--     child =
--     [
--       {
--         str = "%r"
--       }
--     ]
--   },
--   {
--     str = " D"
--   }
-- ]
--
local function parseFormat(str)
	local chr, esc, param, root, cur, prev, new
	local params = {}

	local function newObject(array)
		local obj = {}  -- new object
		obj.str = ""

		array[#array + 1] = obj  -- array{object}
		obj.parent = array

		return obj
	end

	local function endParam()
		if param > 0 then
			if cur.str ~= "" then
				cur.str = "%"..cur.str
				cur.param = true
				params[cur.str] = true
				cur.parent.req[cur.str] = true
				prev = cur
				cur = newObject(cur.parent)
			end
			param = 0
		end
	end

	root = {}  -- array
	root.req = {}
	cur = newObject(root)
	prev = nil

	esc = false
	param = 0

	for i = 1, #str do
		chr = str:sub(i,i)

		if not esc then
			if chr == '\\' then
				endParam()
				esc = true
			elseif chr == '%' then
				endParam()
				if cur.str ~= "" then
					cur = newObject(cur.parent)
				end
				param = 2
			elseif chr == '[' then
				endParam()
				if prev and cur.str == "" then
					table.remove(cur.parent)
					cur = prev
				end
				cur.child = {}  -- new array
				cur.child.req = {}
				cur.child.parent = cur
				cur = newObject(cur.child)
			elseif chr == ']' then
				endParam()
				if cur.parent.parent then
					new = newObject(cur.parent.parent.parent)
					if cur.str == "" then
						table.remove(cur.parent)
					end
					cur = new
				end
			else
				if param > 1 then
					param = param - 1
				elseif param == 1 then
					if not chr:match('%d') then
						endParam()
					end
				end

				cur.str = cur.str .. replaceSpecialChar(chr)
			end
		else
			cur.str = cur.str .. chr
			esc = false
		end

		prev = nil
	end

	endParam()

	-- make sure that at least one required parameter has been defined
	if not next(root.req) then
		throwError("missing-required-parameter")
	end

	-- make sure that the separator parameter "%s" is not amongst the required parameters
	if root.req[parameters.separator] then
		throwError("extra-required-parameter", parameters.separator)
	end

	return root, params
end

local function sortOnRank(claims)
	local rankPos
	local ranks = {{}, {}, {}, {}}  -- preferred, normal, deprecated, (default)
	local sorted = {}

	for _, v in ipairs(claims) do
		rankPos = rankTable[v.rank] or 4
		ranks[rankPos][#ranks[rankPos] + 1] = v
	end

	sorted = ranks[1]
	sorted = mergeArrays(sorted, ranks[2])
	sorted = mergeArrays(sorted, ranks[3])

	return sorted
end

-- if id == nil then item connected to current page is used
function Config:getLabel(id, raw, link, short)
	local label = nil
	local title = nil
	local prefix= ""

	if not id then
		id = mw.wikibase.getEntityIdForCurrentPage()

		if not id then
			return ""
		end
	end

	id = id:upper()  -- just to be sure

	if raw then
		-- check if given id actually exists
		if mw.wikibase.isValidEntityId(id) and mw.wikibase.entityExists(id) then
			label = id

			if id:sub(1,1) == "P" then
				prefix = "Property:"
			end
		end

		prefix = "d:" .. prefix

		title = label  -- may be nil
	else
		-- try short name first if requested
		if short then
			label = p._property{aliasesP.shortName, [p.args.eid] = id}  -- get short name

			if label == "" then
				label = nil
			end
		end

		-- get label
		if not label then
			label = mw.wikibase.getLabelByLang(id, self.langCode)
		end
	end

	if not label then
		label = ""
	elseif link then
		-- build a link if requested
		if not title then
			if id:sub(1,1) == "Q" then
				title = mw.wikibase.getSitelink(id)
			elseif id:sub(1,1) == "P" then
				-- properties have no sitelink, link to Wikidata instead
				title = id
				prefix = "d:Property:"
			end
		end

		if title then
			label = buildWikilink(prefix .. title, label)
		end
	end

	return label
end

function Config:getEditIcon()
	local value = ""
	local prefix = ""
	local front = "&nbsp;"
	local back = ""

	if self.entityID:sub(1,1) == "P" then
		prefix = "Property:"
	end

	if self.editAtEnd then
		front = '<span style="float:'

		if self.langObj:isRTL() then
			front = front .. 'left'
		else
			front = front .. 'right'
		end

		front = front .. '">'
		back = '</span>'
	end

	value = "[[File:OOjs UI icon edit-ltr-progressive.svg|frameless|text-top|10px|alt=" .. i18n['info']['edit-on-wikidata'] .. "|link=https://www.wikidata.org/wiki/" .. prefix .. self.entityID .. "?uselang=" .. self.langCode

	if self.propertyID then
		value = value .. "#" .. self.propertyID
	elseif self.inSitelinks then
		value = value .. "#sitelinks-wikipedia"
	end

	value = value .. "|" .. i18n['info']['edit-on-wikidata'] .. "]]"

	return front .. value .. back
end

-- used to create the final output string when it's all done, so that for references the
-- function extensionTag("ref", ...) is only called when they really ended up in the final output
function Config:concatValues(valuesArray)
	local outString = ""
	local j, skip

	for i = 1, #valuesArray do
		-- check if this is a reference
		if valuesArray[i].refHash then
			j = i - 1
			skip = false

			-- skip this reference if it is part of a continuous row of references that already contains the exact same reference
			while valuesArray[j] and valuesArray[j].refHash do
				if valuesArray[i].refHash == valuesArray[j].refHash then
					skip = true
					break
				end
				j = j - 1
			end

			if not skip then
				-- add <ref> tag with the reference's hash as its name (to deduplicate references)
				outString = outString .. mw.getCurrentFrame():extensionTag("ref", valuesArray[i][1], {name = valuesArray[i].refHash})
			end
		else
			outString = outString .. valuesArray[i][1]
		end
	end

	return outString
end
function Config:convertUnit(unit, raw, link, short, unitOnly)
	local space = " "
	local label = ""
	local itemID

	if unit == "" or unit == "1" then
		return nil
	end

	if unitOnly then
		space = ""
	end

	itemID = parseWikidataURL(unit)

	if itemID then
		if itemID == aliasesQ.percentage then
			return "%"
		else
			label = self:getLabel(itemID, raw, link, short)

			if label ~= "" then
				return space .. label
			end
		end
	end

	return ""
end

function State:getValue(snak)
	return self.conf:getValue(snak, self.rawValue, self.linked, self.shortName, self.anyLanguage, self.unitOnly, false, self.type:sub(1,2))
end

function Config:getValue(snak, raw, link, short, anyLang, unitOnly, noSpecial, type)
	if snak.snaktype == 'value' then
		local datatype = snak.datavalue.type
		local subtype = snak.datatype
		local datavalue = snak.datavalue.value

		if datatype == 'string' then
			if subtype == 'url' and link then
				-- create link explicitly
				if raw then
					-- will render as a linked number like [1]
					return "[" .. datavalue .. "]"
				else
					return "[" .. datavalue .. " " .. datavalue .. "]"
				end
			elseif subtype == 'commonsMedia' then
				if link then
					return buildWikilink("c:File:" .. datavalue, datavalue)
				elseif not raw then
					return "[[File:" .. datavalue .. "]]"
				else
					return datavalue
				end
			elseif subtype == 'geo-shape' and link then
				return buildWikilink("c:" .. datavalue, datavalue)
			elseif subtype == 'math' and not raw then
				local attribute = nil

				if (type == parameters.property or (type == parameters.qualifier and self.propertyID == aliasesP.hasPart)) and snak.property == aliasesP.definingFormula then
					attribute = {qid = self.entityID}
				end

				return mw.getCurrentFrame():extensionTag("math", datavalue, attribute)
			elseif subtype == 'external-id' and link then
				local url = p._property{aliasesP.formatterURL, [p.args.eid] = snak.property}  -- get formatter URL

				if url ~= "" then
					url = mw.ustring.gsub(url, "$1", datavalue)
					return "[" .. url .. " " .. datavalue .. "]"
				else
					return datavalue
				end
			else
				return datavalue
			end
		elseif datatype == 'monolingualtext' then
			if anyLang or datavalue['language'] == self.langCode then
				return datavalue['text']
			else
				return nil
			end
		elseif datatype == 'quantity' then
			local value = ""
			local unit

			if not unitOnly then
				-- get value and strip + signs from front
				value = mw.ustring.gsub(datavalue['amount'], "^\+(.+)$", "%1")

				if raw then
					return value
				end

				-- replace decimal mark based on locale
				value = replaceDecimalMark(value)

				-- add delimiters for readability
				value = i18n.addDelimiters(value)
			end

			unit = self:convertUnit(datavalue['unit'], raw, link, short, unitOnly)

			if unit then
				value = value .. unit
			end

			return value
		elseif datatype == 'time' then
			local y, m, d, p, yDiv, yRound, yFull, value, calendarID, dateStr
			local yFactor = 1
			local sign = 1
			local prefix = ""
			local suffix = ""
			local mayAddCalendar = false
			local calendar = ""
			local precision = datavalue['precision']

			if precision == 11 then
				p = "d"
			elseif precision == 10 then
				p = "m"
			else
				p = "y"
				yFactor = 10^(9-precision)
			end

			y, m, d = parseDate(datavalue['time'], p)

			if y < 0 then
				sign = -1
				y = y * sign
			end

			-- if precision is tens/hundreds/thousands/millions/billions of years
			if precision <= 8 then
				yDiv = y / yFactor

				-- if precision is tens/hundreds/thousands of years
				if precision >= 6 then
					mayAddCalendar = true

					if precision <= 7 then
						-- round centuries/millenniums up (e.g. 20th century or 3rd millennium)
						yRound = math.ceil(yDiv)

						if not raw then
							if precision == 6 then
								suffix = i18n['datetime']['suffixes']['millennium']
							else
								suffix = i18n['datetime']['suffixes']['century']
							end

							suffix = i18n.getOrdinalSuffix(yRound) .. suffix
						else
							-- if not verbose, take the first year of the century/millennium
							-- (e.g. 1901 for 20th century or 2001 for 3rd millennium)
							yRound = (yRound - 1) * yFactor + 1
						end
					else
						-- precision == 8
						-- round decades down (e.g. 2010s)
						yRound = math.floor(yDiv) * yFactor

						if not raw then
							prefix = i18n['datetime']['prefixes']['decade-period']
							suffix = i18n['datetime']['suffixes']['decade-period']
						end
					end

					if raw and sign < 0 then
						-- if BCE then compensate for "counting backwards"
						-- (e.g. -2019 for 2010s BCE, -2000 for 20th century BCE or -3000 for 3rd millennium BCE)
						yRound = yRound + yFactor - 1
					end
				else
					local yReFactor, yReDiv, yReRound

					-- round to nearest for tens of thousands of years or more
					yRound = math.floor(yDiv + 0.5)

					if yRound == 0 then
						if precision <= 2 and y ~= 0 then
							yReFactor = 1e6
							yReDiv = y / yReFactor
							yReRound = math.floor(yReDiv + 0.5)

							if yReDiv == yReRound then
								-- change precision to millions of years only if we have a whole number of them
								precision = 3
								yFactor = yReFactor
								yRound = yReRound
							end
						end

						if yRound == 0 then
							-- otherwise, take the unrounded (original) number of years
							precision = 5
							yFactor = 1
							yRound = y
							mayAddCalendar = true
						end
					end

					if precision >= 1 and y ~= 0 then
						yFull = yRound * yFactor

						yReFactor = 1e9
						yReDiv = yFull / yReFactor
						yReRound = math.floor(yReDiv + 0.5)

						if yReDiv == yReRound then
							-- change precision to billions of years if we're in that range
							precision = 0
							yFactor = yReFactor
							yRound = yReRound
						else
							yReFactor = 1e6
							yReDiv = yFull / yReFactor
							yReRound = math.floor(yReDiv + 0.5)

							if yReDiv == yReRound then
								-- change precision to millions of years if we're in that range
								precision = 3
								yFactor = yReFactor
								yRound = yReRound
							end
						end
					end

					if not raw then
						if precision == 3 then
							suffix = i18n['datetime']['suffixes']['million-years']
						elseif precision == 0 then
							suffix = i18n['datetime']['suffixes']['billion-years']
						else
							yRound = yRound * yFactor
							if yRound == 1 then
								suffix = i18n['datetime']['suffixes']['year']
							else
								suffix = i18n['datetime']['suffixes']['years']
							end
						end
					else
						yRound = yRound * yFactor
					end
				end
			else
				yRound = y
				mayAddCalendar = true
			end

			if mayAddCalendar then
				calendarID = parseWikidataURL(datavalue['calendarmodel'])

				if calendarID and calendarID == aliasesQ.prolepticJulianCalendar then
					if not raw then
						if link then
							calendar = " ("..buildWikilink(i18n['datetime']['julian-calendar'], i18n['datetime']['julian'])..")"
						else
							calendar = " ("..i18n['datetime']['julian']..")"
						end
					else
						calendar = "/"..i18n['datetime']['julian']
					end
				end
			end

			if not raw then
				local ce = nil

				if sign < 0 then
					ce = i18n['datetime']['BCE']
				elseif precision <= 5 then
					ce = i18n['datetime']['CE']
				end

				if ce then
					if link then
						ce = buildWikilink(i18n['datetime']['common-era'], ce)
					end
					suffix = suffix .. " " .. ce
				end

				value = tostring(yRound)

				if m then
					dateStr = self.langObj:formatDate("F", "1-"..m.."-1")

					if d then
						if self.mdyDate then
							dateStr = dateStr .. " " .. d .. ","
						else
							dateStr = d .. " " .. dateStr
						end
					end

					value = dateStr .. " " .. value
				end

				value = prefix .. value .. suffix .. calendar
			else
				value = padZeros(yRound * sign, 4)

				if m then
					value = value .. "-" .. padZeros(m, 2)

					if d then
						value = value .. "-" .. padZeros(d, 2)
					end
				end

				value = value .. calendar
			end

			return value
		elseif datatype == 'globecoordinate' then
			-- logic from https://github.com/DataValues/Geo (v4.0.1)

			local precision, unitsPerDegree, numDigits, strFormat, value, globe
			local latitude, latConv, latValue, latLink
			local longitude, lonConv, lonValue, lonLink
			local latDirection, latDirectionN, latDirectionS, latDirectionEN
			local lonDirection, lonDirectionE, lonDirectionW, lonDirectionEN
			local degSymbol, minSymbol, secSymbol, separator

			local latDegrees = nil
			local latMinutes = nil
			local latSeconds = nil
			local lonDegrees = nil
			local lonMinutes = nil
			local lonSeconds = nil

			local latDegSym = ""
			local latMinSym = ""
			local latSecSym = ""
			local lonDegSym = ""
			local lonMinSym = ""
			local lonSecSym = ""

			local latDirectionEN_N = "N"
			local latDirectionEN_S = "S"
			local lonDirectionEN_E = "E"
			local lonDirectionEN_W = "W"

			if not raw then
				latDirectionN = i18n['coord']['latitude-north']
				latDirectionS = i18n['coord']['latitude-south']
				lonDirectionE = i18n['coord']['longitude-east']
				lonDirectionW = i18n['coord']['longitude-west']

				degSymbol = i18n['coord']['degrees']
				minSymbol = i18n['coord']['minutes']
				secSymbol = i18n['coord']['seconds']
				separator = i18n['coord']['separator']
			else
				latDirectionN = latDirectionEN_N
				latDirectionS = latDirectionEN_S
				lonDirectionE = lonDirectionEN_E
				lonDirectionW = lonDirectionEN_W

				degSymbol = "/"
				minSymbol = "/"
				secSymbol = "/"
				separator = "/"
			end

			latitude = datavalue['latitude']
			longitude = datavalue['longitude']

			if latitude < 0 then
				latDirection = latDirectionS
				latDirectionEN = latDirectionEN_S
				latitude = math.abs(latitude)
			else
				latDirection = latDirectionN
				latDirectionEN = latDirectionEN_N
			end

			if longitude < 0 then
				lonDirection = lonDirectionW
				lonDirectionEN = lonDirectionEN_W
				longitude = math.abs(longitude)
			else
				lonDirection = lonDirectionE
				lonDirectionEN = lonDirectionEN_E
			end

			precision = datavalue['precision']

			if not precision or precision <= 0 then
				precision = 1 / 3600  -- precision not set (correctly), set to arcsecond
			end

			-- remove insignificant detail
			latitude = math.floor(latitude / precision + 0.5) * precision
			longitude = math.floor(longitude / precision + 0.5) * precision

			if precision >= 1 - (1 / 60) and precision < 1 then
				precision = 1
			elseif precision >= (1 / 60) - (1 / 3600) and precision < (1 / 60) then
				precision = 1 / 60
			end

			if precision >= 1 then
				unitsPerDegree = 1
			elseif precision >= (1 / 60)  then
				unitsPerDegree = 60
			else
				unitsPerDegree = 3600
			end

			numDigits = math.ceil(-math.log10(unitsPerDegree * precision))

			if numDigits <= 0 then
				numDigits = tonumber("0")  -- for some reason, 'numDigits = 0' may actually store '-0', so parse from string instead
			end

			strFormat = "%." .. numDigits .. "f"

			if precision >= 1 then
				latDegrees = strFormat:format(latitude)
				lonDegrees = strFormat:format(longitude)

				if not raw then
					latDegSym = replaceDecimalMark(latDegrees) .. degSymbol
					lonDegSym = replaceDecimalMark(lonDegrees) .. degSymbol
				else
					latDegSym = latDegrees .. degSymbol
					lonDegSym = lonDegrees .. degSymbol
				end
			else
				latConv = math.floor(latitude * unitsPerDegree * 10^numDigits + 0.5) / 10^numDigits
				lonConv = math.floor(longitude * unitsPerDegree * 10^numDigits + 0.5) / 10^numDigits

				if precision >= (1 / 60) then
					latMinutes = latConv
					lonMinutes = lonConv
				else
					latSeconds = latConv
					lonSeconds = lonConv

					latMinutes = math.floor(latSeconds / 60)
					lonMinutes = math.floor(lonSeconds / 60)

					latSeconds = strFormat:format(latSeconds - (latMinutes * 60))
					lonSeconds = strFormat:format(lonSeconds - (lonMinutes * 60))

					if not raw then
						latSecSym = replaceDecimalMark(latSeconds) .. secSymbol
						lonSecSym = replaceDecimalMark(lonSeconds) .. secSymbol
					else
						latSecSym = latSeconds .. secSymbol
						lonSecSym = lonSeconds .. secSymbol
					end
				end

				latDegrees = math.floor(latMinutes / 60)
				lonDegrees = math.floor(lonMinutes / 60)

				latDegSym = latDegrees .. degSymbol
				lonDegSym = lonDegrees .. degSymbol

				latMinutes = latMinutes - (latDegrees * 60)
				lonMinutes = lonMinutes - (lonDegrees * 60)

				if precision >= (1 / 60) then
					latMinutes = strFormat:format(latMinutes)
					lonMinutes = strFormat:format(lonMinutes)

					if not raw then
						latMinSym = replaceDecimalMark(latMinutes) .. minSymbol
						lonMinSym = replaceDecimalMark(lonMinutes) .. minSymbol
					else
						latMinSym = latMinutes .. minSymbol
						lonMinSym = lonMinutes .. minSymbol
					end
				else
					latMinSym = latMinutes .. minSymbol
					lonMinSym = lonMinutes .. minSymbol
				end
			end

			latValue = latDegSym .. latMinSym .. latSecSym .. latDirection
			lonValue = lonDegSym .. lonMinSym .. lonSecSym .. lonDirection

			value = latValue .. separator .. lonValue

			if link then
				globe = parseWikidataURL(datavalue['globe'])

				if globe then
					globe = mw.wikibase.getLabelByLang(globe, "en"):lower()
				else
					globe = "earth"
				end

				latLink = table.concat({latDegrees, latMinutes, latSeconds}, "_")
				lonLink = table.concat({lonDegrees, lonMinutes, lonSeconds}, "_")

				value = "[https://tools.wmflabs.org/geohack/geohack.php?language="..self.langCode.."&params="..latLink.."_"..latDirectionEN.."_"..lonLink.."_"..lonDirectionEN.."_globe:"..globe.." "..value.."]"
			end

			return value
		elseif datatype == 'wikibase-entityid' then
			local label
			local itemID = datavalue['numeric-id']

			if subtype == 'wikibase-item' then
				itemID = "Q" .. itemID
			elseif subtype == 'wikibase-property' then
				itemID = "P" .. itemID
			else
				return '<strong class="error">' .. errorText('unknown-data-type', subtype) .. '</strong>'
			end

			label = self:getLabel(itemID, raw, link, short)

			if label == "" then
				label = nil
			end

			return label
		else
			return '<strong class="error">' .. errorText('unknown-data-type', datatype) .. '</strong>'
		end
	elseif snak.snaktype == 'somevalue' and not noSpecial then
		if raw then
			return " "  -- single space represents 'somevalue'
		else
			return i18n['values']['unknown']
		end
	elseif snak.snaktype == 'novalue' and not noSpecial then
		if raw then
			return ""  -- empty string represents 'novalue'
		else
			return i18n['values']['none']
		end
	else
		return nil
	end
end

function Config:getSingleRawQualifier(claim, qualifierID)
	local qualifiers

	if claim.qualifiers then qualifiers = claim.qualifiers[qualifierID] end

	if qualifiers and qualifiers[1] then
		return self:getValue(qualifiers[1], true)  -- raw = true
	else
		return nil
	end
end

return p