Module:Wikidata date

From Wikimedia Commons, the free media repository
Jump to navigation Jump to search
Lua
CodeDiscussionEditHistoryLinksLink count Subpages:DocumentationTestsResultsSandboxLive code All modules

This Module handles pulling data type properties from Wikidata. It was mainly intended for calls from other Lua modules, but it can be also called directly from templates. The module can read and parse and display dates in all precision's (year, decade, century, millennium, ettc.) and calendars (Gregorian and Julian) used on Wikidata. It can also interpret all qualifiers used for wider range of dates. Dates are localized (displayed in the language of the user) using:

Properties

Module recognizes several qualifiers, by themselves and in groups:

Calling from templates

{{#invoke:Wikidata date|date|item=Q5600|property=P569|lang=en}}

Inputs
  • item - wikidata item ID
  • property - property to capture
  • lang - what language to display it in. If skipped than date will be shown in the language of the user.

Code

--[[  
  __  __           _       _      __        ___ _    _     _       _              _       _       
 |  \/  | ___   __| |_   _| | ___ \ \      / (_) | _(_) __| | __ _| |_ __ _    __| | __ _| |_ ___ 
 | |\/| |/ _ \ / _` | | | | |/ _ (_) \ /\ / /| | |/ / |/ _` |/ _` | __/ _` |  / _` |/ _` | __/ _ \
 | |  | | (_) | (_| | |_| | |  __/_ \ V  V / | |   <| | (_| | (_| | || (_| | | (_| | (_| | ||  __/
 |_|  |_|\___/ \__,_|\__,_|_|\___(_) \_/\_/  |_|_|\_\_|\__,_|\__,_|\__\__,_|  \__,_|\__,_|\__\___|
 
 This module displays content of wikidata "time" properties, with special  
emphasis on complex dates. Dates are localized using Module:Complex_date
 
Please do not modify this code without applying the changes first 
at Module:Wikidata date/sandbox and testing at Module:Wikidata date/sandbox/testcases.
 
Authors and maintainers:
* User:Jarekt -  original version 
]]

local cDate    = require("Module:Complex date") -- used for internationalization of dates
local ISOdate  = require('Module:ISOdate')._ISOdate
local date2jdn = require('Module:Calendar')._date2jdn

-- ==================================================
-- === local helper functions =======================
-- ==================================================

local function processFrame(frame)
	-- inputs in any upper or lower case
	local args = {}
	for name, value in pairs( frame.args ) do 
		if value ~= '' then -- nuke empty strings
			args[string.lower(name)] = value
		end
	end
	args.item = args.item or args.wikidata 
	if not (args.lang and mw.language.isSupportedLanguage(args.lang)) then 
		args.lang = frame:callParserFunction( "int", "lang" ) -- get user's chosen language 
	end
	return args
end

local function formatDate(conj, date1, date2, certainty, lang)
	return cDate._complex_date_cer(conj, date1.adj, date1.date, date1.units, date1.era, 
	                                       date2.adj, date2.date, date2.units, date2.era, certainty, lang)
end

local function parse_item_snak(snak)
	if (snak.snaktype == "value") then 
		return snak.datavalue.value.id 
	end
end

local function parse_time_snak(snak)
-- Converts a "time" snak into structure with obj.calendar, obj.date, obj.precision,  and obj.era
-- fields. Converts a "wikibase-item" snak into a string with q-code
	local obj = { date='', debug='' }
	if (snak.snaktype == "value" and snak.datavalue.type == 'time') then 
		local units = {[6]='millennium', [7]='century', [8]='decade'} -- precision to units conversion
		local calendars = { Q1985727='gregorian', Q1985786='julian'} 
		local v = snak.datavalue.value
		local calendar = calendars[string.gsub(v.calendarmodel, 'http://www.wikidata.org/entity/', '')]
		obj.units = units[v.precision]
		obj.debug = string.format(" (time=%s/%i, calendar=%s)", v.time, v.precision, calendar) -- string used for debuging	
		obj.timestamp = v.time
		local year = tonumber(string.sub( v.time, 1, string.find( string.sub(v.time,2), '-') ) )
		if year<0 then
			obj.era  = 'BC'
		elseif year<100 then
			obj.era  = 'AD'
		end
		if calendar == 'julian' and year>1583 and year<1923 then 
			obj.calendar = 'julian' -- if julian calenar in a period of time usually associated with gregorian calendar
		end
		if v.precision >= 9 then -- assign year if precission higher than a decade
			obj.year = year;
		end
		local den = math.pow(10,9-v.precision)
		year = math.floor((math.abs(year)-1)/den)+1
		if v.precision >= 11 then                -- day
			obj.date = string.sub(v.time,2,11)     -- date in YYYY-MM-DD format
		elseif v.precision == 10 then            -- month
			obj.date = string.sub(v.time,2,8)      -- date in YYYY-MM format
		elseif v.precision == 9 then             -- year
			obj.date = string.sub(v.time,2,5)      -- date in YYYY format
		elseif v.precision == 8 then             -- decade
			obj.date = string.sub(v.time,2,4)..'0' -- date in YYY0 format
		elseif v.precision == 7 then             -- century 
			obj.date = tostring(year) 
		elseif v.precision == 6 then             -- millennium
			obj.date = tostring(year) 
		elseif v.precision <= 5 then             -- millions of years
			obj.date = tostring(year*den) 
		end
		return obj
	end

	return nil
end

-- ==================================================
-- === External functions ===========================
-- ==================================================
local p = {}

-- ===========================================================================
-- === Version of the function to be called from other LUA codes
-- ===========================================================================
function p._qualifierDate(snak, lang)
	local date1 = parse_time_snak(snak)
	local gregorian = 1
	if date1.calendar=='julian' then 
		gregorian = 0
	end
	local jdn = date2jdn(date1.timestamp, gregorian) or 0
	local dateStr
	if (date1.calendar or date1.era or date1.units ) then -- check the main statement
		dateStr = formatDate(date1.calendar, date1, { date='', debug='' }, '', lang)
	else
		dateStr = ISOdate(date1.date, lang)
	end
	return {str=dateStr, year=date1.year, jdn=jdn}
end

function p._date(item, prop, lang)
  -- Interpret date stored in "item"'s "prop" property and display it using [[Module:Complex date]] 
	-- module using language "lang". 
	local str, iso, year, year2return, iso2return, entity
	local dateTable = {}  -- table to store QuickStatements 
	
	-- Step 1: clean up the input parameters
	if type(item)=='string' then -- "item" is a q-code
		entity = mw.wikibase.getEntity(item); 
	else
		entity = item            -- "item" is the entity
	end
	lang = string.lower(lang) or 'en' -- lang comming from p.date(frame) will be clean, others might not be
	
	-- Step 2: parse all the statements in property "prop" and call Module:Complex_data
	if entity and entity.claims and entity.claims[prop] then -- if we have wikidata item and item has the property
		for _, statement in pairs( entity:getBestStatements( prop )) do
			-- harvest few date-type qualifiers 
			local data = {}
			
			-- parse time datatype properties
			local qualifiers = {['from']='P580', ['until_']='P582', ['after']='P1319', ['before']='P1326'}
			for field,qual in pairs( qualifiers ) do
				if statement.qualifiers and statement.qualifiers[qual] then
					data[field] = parse_time_snak(statement.qualifiers[qual][1])
				end
			end
			
			-- parse item datatype properties
			local qualifiers = {sourcing='P1480', refine='P4241', validity='P5102'}
			for field,qual in pairs( qualifiers ) do
				if statement.qualifiers and statement.qualifiers[qual] then
				  -- only one P1480 qualifier per date so no "presumably circa" dates, etc.
					data[field] = parse_item_snak(statement.qualifiers[qual][1])
				end
			end
						
			-- check on P4241 ("refine date") and P1480 ("sourcing circumstances") qualifiers
			local LUT = {              Q40719727='early'     , Q40719748='mid',      Q40719766='late',
				Q40690303='1quarter' , Q40719649='2quarter'  , Q40719662='3quarter', Q40719674='4quarter',
				Q40720559='spring'   , Q40720564='summer'    , Q40720568='autumn'  , Q40720553='winter',
				Q40719687='firsthalf', Q40719707='secondhalf', Q5727902='circa',
				Q56644435='probably',  Q18122778='presumably', Q30230067='possibly' }
			local adj       = LUT[data.refine]    -- check on P4241 ("refine date") item-type qualifier
			local certainty = LUT[data.sourcing] or LUT[data.validity] -- check on P1480 ("sourcing circumstances") item-type qualifier
			if data.sourcing and not certainty then
				certainty = 'uncertain' 
			end

			-- initialize
			local nulDate = { date='', debug='' } -- nul parameter to pass to formatDate
			local dateStr = nil
			
			-- check 'P580' ("start time" aka "from" "since") and 'P582' ("end time" aka "until") qualifiers:
			if data.from and data.until_ then
				dateStr = formatDate('from-until', data.from, data.until_, certainty, lang)
				if data.from.year==data.until_.year then
					year = data.from.year
				end
			elseif data.from and not data.from.calendar then
				data.from.adj = adj
				dateStr = formatDate('from', data.from, nulDate, certainty, lang)
			elseif data.from then
				data.from.adj = 'from'
				dateStr = formatDate(data.from.calendar, data.from, nulDate, certainty, lang)
			elseif data.until_ and not data.until_.calendar then
				data.until_.adj = adj
				dateStr = formatDate('until', data.until_, nulDate, certainty, lang)
			elseif data.until_ then
				data.until_.adj = 'until'
				dateStr = formatDate(data.until_.calendar, data.until_, nulDate, certainty, lang)			
			end
			
			-- check 'P1319' ("earliest date" aka "after this date") and 'P1326' ("latest date" aka "before this date") qualifiers:
			if data.after and data.before and certainty=='circa' then
				dateStr = formatDate('circa', data.after, data.before, '', lang) --module:Complex_date has custom 2-date "circa" option based on "between" option
				if data.after.year==data.before.year then
					year = data.before.year
				end
			elseif data.after and data.before then
				dateStr = formatDate('between', data.after, data.before, certainty, lang)
				if data.after.year==data.before.year then
					year = data.before.year
				end
			elseif data.after and data.after.calendar then
				data.after.adj = 'after'
				dateStr = formatDate(data.after.calendar, data.after, nulDate, certainty, lang)
			elseif data.after then
				data.after.adj = adj
				dateStr = formatDate('after', data.after, nulDate, certainty, lang)
			elseif data.before and data.before.calendar then
				data.before.adj = 'before'
				dateStr = formatDate(data.before.calendar, data.before, nulDate, certainty, lang)
			elseif data.before then
				data.before.adj = adj
				dateStr = formatDate('before', data.before, nulDate, certainty, lang)
			end
			
			-- if no above qualifiers than look at the main snack
			if not dateStr then
				data.main = parse_time_snak(statement.mainsnak)
				if data.main then
					year = data.main.year
					if (data.main.calendar or adj or data.main.era or data.main.units or certainty ) then -- check the main statement
						data.main.adj = adj
						dateStr = formatDate(data.main.calendar, data.main, nulDate, certainty, lang)
					else
						iso     = data.main.date
						dateStr = ISOdate(iso, lang)
					end
				end	
			end
	
			table.insert( dateTable, dateStr)
			if not year2return then
				year2return = year
			elseif year2return and year2return~=year then
				year2return = nil -- if years conflict than nul
			end
			if not iso2return then
				iso2return = iso
			elseif iso2return then
				iso2return = nil -- if date conflict than nul
			end

		end -- for loop
	end -- if entity then
	
	local dateStr = mw.text.trim(table.concat( dateTable, ' / '))
	if dateStr=='' then dateStr=nil; end
	return {str=dateStr, year=year2return, iso=iso2return}
end

-- ===========================================================================
-- === Functions to be called from template namespace
-- ===========================================================================
function p.date(frame)
	local args = processFrame(frame)
	local result = p._date(args.item, args.property, args.lang)
	return result.str or ''
end

function p.year(frame)  -- return only year string
	local args = processFrame(frame)
	local result = p._date(args.item, args.property, args.lang)
	return tostring(result.year) or ''
end

function p.isoDate(frame)  -- return only year string
	local args = processFrame(frame)
	local result = p._date(args.item, args.property, args.lang)
	return result.iso or 'nil'
end

function p.timestamp(frame) 
  -- debuging function which might go away
	local entity = mw.wikibase.getEntity(frame.args.item); 
	local dateTable = {}  -- table to store QuickStatements 
	if entity and entity.claims and entity.claims[frame.args.property] then -- if we have wikidata item and item has the property
		for _, statement in pairs( entity:getBestStatements( frame.args.property )) do
			local snak = statement.mainsnak
			if (snak.snaktype == "value" and snak.datavalue.type == 'time') then 
				local v = snak.datavalue.value
				table.insert( dateTable, v.time ..'/' .. v.precision)
			end
		end -- for loop
	end -- if entity then
	return table.concat( dateTable, ' / ') or ''
end

return p