Documentation for this module may be created at ମଡ୍ୟୁଲ:Author/doc

-- Global variables.
dateModule = require( "Module:Date" )
tableToolsModule = require( "Module:TableTools" )
categories = {} -- List of categories to add page to.

--------------------------------------------------------------------------------
-- Get a given or family name property. This concatenates (with spaces) all
-- statements of the given property in order of the series ordinal (P1545)
-- qualifier. @TODO fix this.
function getNameFromWikidata( item, property )
	local statements = item:getBestStatements( property )
	local out = {}
	if statements[1] ~= nil and statements[1].mainsnak.datavalue ~= nil then
		local itemId = statements[1].mainsnak.datavalue.value.id
		table.insert( out, mw.wikibase.label( itemId ) or '' )
	end
	return table.concat( out, ' ' )
end

--------------------------------------------------------------------------------
function getPropertyValue( item, property )
	local statements = item:getBestStatements( property )
	if statements[1] ~= nil and statements[1].mainsnak.datavalue ~= nil then
		return statements[1].mainsnak.datavalue.value
	end	
end

--------------------------------------------------------------------------------
-- The 'Wikisource' format for a birth or death year is as follows:
--     "?" or empty for unknown (or still alive)
--     Use BCE for years before year 1
--     Approximate dates:
--         Decades or centuries: "1930s" or "20th century"
--         Circa: "c/1930" or "c. 1930" or "ca 1930" or "circa 1930"
--         Tenuous year: "1932/?"
--         Choice of two or more years: "1932/1933"
-- This is a slightly overly-complicated function, but one day will be able to be deleted.
-- @param string type Either 'birth' or 'death'
-- @return string The year to display
function formatWikisourceYear( year, type )
	if year == nil or year == '' then
		return ''
	end
	local yearParts = mw.text.split( year, '/', true )
	-- Ends in a question mark.
	if yearParts[2] == '?' then
		addCategory( 'Authors with unknown ' .. type .. ' dates' )
		if tonumber( yearParts[1] ) == nil then
			addCategory( 'Authors with non-numeric ' .. type .. ' dates' )
		else
			addCategory( dateModule.era( yearParts[1] ) .. ' authors' )
			addCategory( yearParts[1] .. ' ' .. type .. 's' )
		end
		return yearParts[1] .. '?'
	end
	-- Starts with one of the 'circa' abbreviations
	local circaNames = { 'c', 'c.', 'ca', 'ca.', 'circa' }
	for _, circaName in pairs( circaNames ) do
		if yearParts[1] == circaName then
			addCategory( 'Authors with approximate ' .. type .. ' dates' )
			local out = 'c. ' .. yearParts[2]
			if tonumber( yearParts[2] ) == nil then
				addCategory( 'Authors with non-numeric ' .. type .. ' dates' )
			else
				addCategory( dateModule.era( yearParts[2] ) .. ' authors' )
				addCategory( yearParts[2] .. ' ' .. type .. 's' )
			end
			return out
		end
	end
	-- If there is more than one year part, and they're all numbers, add categories.
	local allPartsAreNumeric = true
	if #yearParts > 1 then
		for _, yearPart in pairs( yearParts ) do
			if tonumber( yearPart ) ~= nil then
				addCategory( yearPart .. ' ' .. type .. 's' )
				addCategory( dateModule.era( yearPart ) .. ' authors' )
			else
				allPartsAreNumeric = false
			end
		end
		if allPartsAreNumeric then
			addCategory( 'Authors with approximate birth dates' )
		end
	end
	-- Otherwise, just use whatever's been given
	if #yearParts == 1 and tonumber( year ) == nil then
		addCategory( 'Authors with non-numeric ' .. type .. ' dates' )
	end
	if #yearParts == 1 or allPartsAreNumeric == false then
		addCategory( year .. ' ' .. type .. 's' )
	end
	return year
end

--------------------------------------------------------------------------------
-- Get a formatted year of the given property and add to the relevant categories.
--   P569   date of birth
--   P570   date of death
--   P1317  floruit
function formatWikidataYear( item, property )
	-- Check sanity of inputs.
	if item == nil or string.sub( property, 1, 1 ) ~= 'P' then
		return ''
	end
	local type = 'birth'
	if property == 'P570' then
		type = 'death'
	end
	-- Get this property's statements.
	local statements = item:getBestStatements( property )
	if #statements == 0 then
		-- If there are no statements of this type, add to 'missing' category.
		if type == 'birth' or type == 'death' then
			addCategory( 'Authors with missing ' .. type .. ' dates' )
		end
		local isHuman = item:formatPropertyValues( 'P31' ).value == 'human'
		if type == 'death' and isHuman then
			-- If no statements about death, assume to be alive.
			addCategory( 'Living authors' )
		end
	end

	-- Compile a list of years, one from each statement.
	local years = {}
	for _, statement in pairs( statements ) do
		local year = getYearStringFromSingleStatement( statement, type )
		table.insert( years, year )
	end
	years = tableToolsModule.removeDuplicates( tableToolsModule.compressSparseArray( years ) )

	-- If no year found yet, try for a floruit date.
	if #years == 0 or table.concat( years, '/' ) == '?' then
		floruitStatements = item:getBestStatements( 'P1317' )
		for _, statement in pairs( floruitStatements ) do
			-- If all we've got so far is 'unknown', replace it.
			if table.concat( years, '/' ) == '?' then
				years = {}
			end
			addCategory( 'Authors with floruit dates' )
			year = getYearStringFromSingleStatement( statement, 'floruit' )
			table.insert( years, year )
		end
	end
	years = tableToolsModule.removeDuplicates( tableToolsModule.compressSparseArray( years ) )

	-- table.sort( years );
	return table.concat( years, '/' )
end

--------------------------------------------------------------------------------
-- Take a statement of a given property and make a human-readable year string
-- out of it, adding the relevant categories as we go.
-- @param table statement The statement.
-- @param string type One of 'birth' or 'death'.
function getYearStringFromSingleStatement( statement, type )
	local snak = statement.mainsnak
	-- We're not using mw.wikibase.formatValue because we only want years.

	-- No value. This is invalid for birth dates (should be 'somevalue'
	-- instead), and indicates 'still alive' for death dates.
	if snak.snaktype == 'novalue' and type == 'birth' then
		addCategory( 'Authors with missing birth dates' )
		return ''
	end
	if snak.snaktype == 'novalue' and type == 'death' then
		addCategory( 'Living authors' )
		return ''
	end

	-- Unknown value.
	if snak.snaktype == 'somevalue' then
		addCategory( 'Authors with unknown ' .. type .. ' dates' )
		return '?'
	end

	-- Extract year from the time value.
	local _,_, extractedYear = string.find( snak.datavalue.value.time, '([%+%-]%d%d%d+)%-' )
	year = math.abs( tonumber( extractedYear ) )
	addCategory( dateModule.era( extractedYear ) .. ' authors' )
	 -- Century & millennium precision.
	if snak.datavalue.value.precision == 6 or snak.datavalue.value.precision == 7 then
		local ceilfactor = 100
		local precisionName = 'century'
		if snak.datavalue.value.precision == 6 then
			ceilfactor = 1000
			precisionName = 'millennium'
		end
		local cent = math.max( math.ceil( year / ceilfactor ), 1 )
		-- @TODO: extract this to use something like [[en:wikipedia:Module:Ordinal]]
		local suffix = 'th'
		if string.sub( cent, -1 ) == '1' and string.sub( cent, -2 ) ~= '11' then
			suffix = 'st'
		elseif string.sub( cent, -1 ) == '2' and string.sub( cent, -2 ) ~= '12' then
			suffix = 'nd'
		elseif string.sub( cent, -1 ) == '3' and string.sub( cent, -2 ) ~= '13' then
			suffix = 'rd'
		end
		year = cent .. suffix .. ' ' .. precisionName
		addCategory( 'Authors with approximate ' .. type .. ' dates' )
	end
	if snak.datavalue.value.precision == 8 then -- decade precision
		year = math.floor( tonumber( year ) / 10 ) * 10 .. 's'
		addCategory( 'Authors with approximate ' .. type .. ' dates' )
	end
	if tonumber( extractedYear ) < 0 then
		year = year .. ' BCE'
	end

	-- Remove from 'Living authors' if that's not possible.
	if tonumber( extractedYear ) < tonumber( os.date( '%Y' ) - 110 ) then
		removeCategory( 'Living authors' )
	end

	-- Add to e.g. 'YYYY births' category (before we add 'c.' or 'fl.' prefixes).
	if type == 'birth' or type == 'death' then
		addCategory( year .. ' ' .. type .. 's' )
	end

	-- Extract circa (P1480 = sourcing circumstances, Q5727902 = circa)
	if statement.qualifiers ~= nil and statement.qualifiers.P1480 ~= nil then
		for _,qualifier in pairs(statement.qualifiers.P1480) do
			if qualifier.datavalue.value.id == 'Q5727902' then
				addCategory( 'Authors with approximate ' .. type .. ' dates' )
				year = 'c. ' .. year
			end
		end
	end

	-- Add floruit abbreviation.
	if type == 'floruit' then
		year = 'fl. ' .. year
	end

	return year
end

--------------------------------------------------------------------------------
-- Add a category to the current list of categories. Do not include the Category prefix.
function addCategory( category )
	for _, cat in pairs( categories ) do
    	if cat == category then
    		-- Already present
			return
    	end
  	end
	table.insert( categories, category )
end

--------------------------------------------------------------------------------
-- Remove a category. Do not include the Category prefix.
function removeCategory( category )
	for catPos, cat in pairs( categories ) do
    	if cat == category then
    		table.remove( categories, catPos )
    	end
  	end
end

--------------------------------------------------------------------------------
-- Get wikitext for all categories added using addCategory.
function getCategories()
	table.sort( categories )
	local out = ''
	for _, cat in pairs( categories ) do
		out = out .. '[[Category:' .. cat .. ']]'
	end
	return out
end

--------------------------------------------------------------------------------
-- Check a given title as having the appropriate dates as a disambiguating suffix.
function checkTitleDatesAgainstWikidata( title, wikidata_id )
	-- All disambiguated author pages have parentheses in their titles.
	local titleHasParentheses = string.find( tostring( title ), '%d%)' )
	if titleHasParentheses == nil then
		return
	end

	-- The title should end with years in the same format as is used in the page
	-- header but with a normal hyphen instead of an en dash.
	local birthYear = date( { type = 'birth'; wikidata_id = wikidata_id } )
	local deathYear = date( { type = 'death'; wikidata_id = wikidata_id } )
	local dates = '(' .. birthYear .. '-' .. deathYear .. ')'
	if string.sub( tostring( title ), -string.len( dates ) ) ~= dates then 
		addCategory( 'Authors with title-date mismatches' )
	end
end

--------------------------------------------------------------------------------
-- Get a formatted string of the years that this author lived,
-- and categorise in the appropriate categories.
-- The returned string starts with a line break (<br />).
function dates( args )
	local item = mw.wikibase.getEntity()
	if args.wikidata_id ~= nil and args.wikidata_id ~= '' then
		-- This check required because getEntity can't copy with empty strings.
		item = mw.wikibase.getEntity( args.wikidata_id )
	end
	local outHtml = mw.html.create()
	
	-- Check disambiguated page titles for accuracy.
	checkTitleDatesAgainstWikidata( args.pagetitle or mw.title.getCurrentTitle(), args.wikidata_id )

	-- Get the dates (do death first, so birth can override categories if required):
	-- Death.
	local wikidataDeathyear = formatWikidataYear( item, 'P570' ) -- P570 Date of death
	local wikisourceDeathyear = formatWikisourceYear( args.deathyear, 'death' )
	if args.deathyear == nil or args.deathyear == '' then
		args.deathyear = wikidataDeathyear
	else
		-- For Wikisource-supplied death dates.
		args.deathyear = wikisourceDeathyear
		addCategory( 'Authors with override death dates' )
		if item ~= nil and wikisourceDeathyear ~= wikidataDeathyear then
			addCategory( 'Authors with death dates differing from Wikidata' )
		end
		if tonumber( args.deathyear ) ~= nil then
			addCategory( dateModule.era( args.deathyear ) .. ' authors' )
		end
	end
	if args.deathyear == '' and item == nil then
		addCategory( 'Authors with missing death dates' )
	end
	-- Birth.
	local wikidataBirthyear = formatWikidataYear( item, 'P569' ) -- P569 Date of birth
	local wikisourceBirthyear = formatWikisourceYear( args.birthyear, 'birth' )
	if args.birthyear == nil or args.birthyear == '' then
		args.birthyear = wikidataBirthyear
	else
		-- For Wikisource-supplied birth dates.
		args.birthyear = wikisourceBirthyear
		addCategory( 'Authors with override birth dates' )
		if item ~= nil and wikisourceBirthyear ~= wikidataBirthyear then
			addCategory( 'Authors with birth dates differing from Wikidata' )
		end
		if tonumber( args.birthyear ) ~= nil then
			addCategory( dateModule.era( args.birthyear ) .. ' authors' )
		end
	end
	if args.birthyear == '' then
		addCategory( 'Authors with missing birth dates' )
	end

	-- Put all the output together, including manual override of the dates.
	local dates = ''
	if args.dates ~= nil and args.dates ~= '' then
		-- The parentheses are repeated here and in getFormattedDates()
		addCategory( 'Authors with override dates' )
		dates = '<br />(' .. args.dates .. ')'
	else
		dates = getFormattedDates( args.birthyear, args.deathyear )
	end
	outHtml:wikitext( dates .. getCategories() )
	return tostring( outHtml )
end

--------------------------------------------------------------------------------
-- Get a single formatted date, with no categories.
-- args.year, args.type, args.wikidata_id
function date( args )
	if args.type == nil then
		args.type = 'birth'
	end
	if args.year == nil or args.year == '' then
		if args.wikidata_id == '' then
			-- Nillify if not given.
			args.wikidata_id = nil
		end
		local item = mw.wikibase.getEntity( args.wikidata_id )
		local property = 'P570' -- P570 Date of death
		if args.type == 'birth' then
			property = 'P569' -- P569 Date of birth
		end
		return formatWikidataYear( item, property )
	else
		return formatWikisourceYear( args.year, args.type )
	end
end

--------------------------------------------------------------------------------
-- Get the actual parentheses-enclosed HTML string that shows the dates.
function getFormattedDates( birthyear, deathyear )
	local dates = ''
	if birthyear ~= '' or deathyear ~= '' then
		dates = dates .. '<br />('
	end
	if birthyear ~= '' then
		dates = dates .. birthyear
	end
	if ( birthyear ~= '' or deathyear ~= '' ) and birthyear ~= deathyear then
		-- Add spaces if there are spaces in either of the dates.
		local spaces = ''
		if string.match( birthyear .. deathyear, ' ' ) then
			spaces = ' '
		end
		dates = dates .. spaces .. '–' .. spaces
	end
	if deathyear ~= '' and birthyear ~= deathyear then
		dates = dates .. deathyear
	end
	if birthyear ~= '' or deathyear ~= '' then
		dates = dates .. ')'
	end
	return dates
end

--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Export all public functions.
return {
	header = function( frame ) return header( frame.args ) end;
	dates = function( frame ) return dates( frame.args ) end;
	date = function( frame ) return date( frame.args ) end;
}