Jump to content

Module:Wikidata date

Permanently protected module
From Wikisource

--[[  
  __  __           _       _      __        ___ _    _     _       _              _       _       
 |  \/  | ___   __| |_   _| | ___ \ \      / (_) | _(_) __| | __ _| |_ __ _    __| | __ _| |_ ___ 
 | |\/| |/ _ \ / _` | | | | |/ _ (_) \ /\ / /| | |/ / |/ _` |/ _` | __/ _` |  / _` |/ _` | __/ _ \
 | |  | | (_) | (_| | |_| | |  __/_ \ 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