Module:Wikidata date

    From Commons

    Documentation for this module may be created at Module:Wikidata date/doc

    --[[  
      __  __           _       _      __        ___ _    _     _       _              _       _       
     |  \/  | ___   __| |_   _| | ___ \ \      / (_) | _(_) __| | __ _| |_ __ _    __| | __ _| |_ ___ 
     | |\/| |/ _ \ / _` | | | | |/ _ (_) \ /\ / /| | |/ / |/ _` |/ _` | __/ _` |  / _` |/ _` | __/ _ \
     | |  | | (_) | (_| | |_| | |  __/_ \ 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 then
    				data.from.adj = 'from'
    				dateStr = formatDate(data.from.calendar, data.from, 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 then
    				data.after.adj = 'after'
    				dateStr = formatDate(data.after.calendar, data.after, nulDate, certainty, lang)
    			elseif data.before then
    				data.before.adj = 'before'
    				dateStr = formatDate(data.before.calendar, 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