Module:කර්තෘ

From Wikisource
Jump to navigation Jump to search

This module handles logic for pages in the Author namespace and in portals about individual people. It uses Wikidata where possible, and allows local override of all parameters. It is used by the {{කර්තෘ}} template.

Unit and integration tests are at Module:කර්තෘ/testcases and their results can be viewed at Module talk:කර්තෘ/testcases.

Function: dates[edit]

Get the author-page date string, with categories.

Usage
Common usage: {{#invoke|කර්තෘ|dates}}
All parameters: {{#invoke|කර්තෘ|dates|birthyear=|deathyear=|dates=|wikidata_id=}}
Parameters
  • dates — if supplied will be used as-is for the date display (however, birthyear and deathyear can still be specified for categorization purposes)
  • birthyear and deathyear — the years of birth and death, in this format:
    • a numeric year
    • "?" 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"
  • wikidata_id — the Wikidata identifier to use. Will default to the current page if not supplied.
  • pagetitle — a page title to use instead of the current page (only used for testing purposes).
Returns
This function returns the author's birth and death years, wrapped in parentheses and separated by an en dash. The return string is prefixed with a <br />, and suffixed with the list of appropriate categories (see below).

ප්‍රවර්ග[edit]

මෙමගින් පිටු පහත ප්‍රවර්ග වලට ඇතුළු කිරීමද සිදු කරයි:

  1. In all cases (where applicable):
  2. Where manual birth and/or death dates are supplied:

Function: date[edit]

Get a single formatted date, with no categories.

Usage
Common usage: {{#invoke|කර්තෘ|date|type=}}
All parameters: {{#invoke|කර්තෘ|date|type=|year=|wikidata_id=}}
Parameters
  • type — either birth or death.
  • year — the year to display, following the same format as birthyear in the dates function above.
  • wikidata_id — the Wikidata Q-identifier of the author to use.
Returns
A simple string with no categories or leading or trailing line breaks.

Function: categories[edit]

Get a list of categories for occupations and nationalities

Usage
All parameters: {{#invoke|කර්තෘ|categories|wikidata_id=}}
Returns
A string containing categories

Function: lastInitial[edit]

Get a link to the alphabetical author index page (e.g. Wikisource:Authors-A).

Usage
Common usage: {{#invoke|කර්තෘ|lastInitial |wikidata_id= }}
All parameters: {{#invoke|කර්තෘ|lastInitial |wikidata_id= |last_initial= }}
Parameters
  • |wikidata_id= — the Wikidata Q-identifier of the author to use.
  • |last_initial= — manually-specified override for the initial letters. Means that |wikidata_id= will be ignored.
Returns
A wikitext string containing a link (if appropriate) and a category.

require( "strict" )
-- Local variables.
local dateModule = require( "Module:යුගය" )
local tableToolsModule = require( "Module:TableTools" )
local categories = {} -- List of categories to add page to.

local PROP_FAMILY_NAME = 'P734'

--------------------------------------------------------------------------------
-- Get the actual parentheses-enclosed HTML string that shows the dates.
local 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

--------------------------------------------------------------------------------
-- Add a category to the current list of categories. Do not include the Category prefix.
local 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.
local 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.
local function getCategories()
	table.sort( categories )
	local out = ''
	for _, cat in pairs( categories ) do
		out = out .. '[[Category:' .. cat .. ']]'
	end
	return out
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'.
local 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( 'උපන් දිනය පිළිබඳ තොරතුරු සපයා නොමැති කතුවරු' )
		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+)%-' )
	local year = math.abs( tonumber( extractedYear ) )
	addCategory( dateModule.era( extractedYear ) .. ' කතුවරු' )
	 -- 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 ~= nil and 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

--------------------------------------------------------------------------------
-- 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.
local 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

--------------------------------------------------------------------------------
local 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
local 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
local 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
			local vargaya = 'උපන්'
			if type == 'death' then
				vargaya = 'වියෝග'
			end
			addCategory( ' ' .. vargaya .. ' දිනය පිළිබඳ තොරතුරු සපයා නොමැති කතුවරු‎' )
		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
		local 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' )
			local year = getYearStringFromSingleStatement( statement, 'floruit' )
			table.insert( years, year )
		end
	end
	years = tableToolsModule.removeDuplicates( tableToolsModule.compressSparseArray( years ) )

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

--[=[
Get categories for nationality, occupations, etc.

Returns a list of category names
]=]
local function categoriesList( entity )
	-- bail out on junk inputs
	if entity == nil or entity == "" then
		return {}	
	end

	local DATA = mw.loadData( 'Module:කර්තෘ/දත්ත' )

	local function addCategoriesFromClaims( item, cats, pId, knownCategories )
		-- the item is completely empty (?), maybe this could happen
		if not item.claims then
			return
		end

		-- should never happen, but makes development easier as it won't break as
		-- easily if the /data module is out of sync
		if not knownCategories then
			return
		end

		local statements = item.claims[ pId ]

		-- there aren't any
		if not statements then
			return
		end

		-- for each statement in the item
		for _, v in pairs( statements ) do
			-- Sometimes the property exists on item but has no value, or it has an
			-- unknown value, so in the output from mw.wikibase.getEntity()
			-- .mainsnak's .datavalue will be nil.		
			if v.mainsnak.snaktype == "value" then
				local valueId = v.mainsnak.datavalue.value.id
				-- add the category if we know it
				local knownCat = knownCategories[ valueId ]
				if knownCat then
					table.insert( cats, knownCat )
				end
			end
		end
	end

	local cats = {}
	
	-- add known categories from certain properties
	addCategoriesFromClaims( entity, cats, 'P27', DATA.categories.nationalities )
	addCategoriesFromClaims( entity, cats, 'P106', DATA.categories.occupations )
	addCategoriesFromClaims( entity, cats, 'P140', DATA.categories.religions ) -- note P8929 is listed for deletion
	addCategoriesFromClaims( entity, cats, 'P135', DATA.categories.movements )
	addCategoriesFromClaims( entity, cats, 'P1142', DATA.categories.ideologies )
	addCategoriesFromClaims( entity, cats, 'P108', DATA.categories.employer )
	addCategoriesFromClaims( entity, cats, 'P39', DATA.categories.positionheld )
	addCategoriesFromClaims( entity, cats, 'P166', DATA.categories.awardreceived )
	addCategoriesFromClaims( entity, cats, 'P463', DATA.categories.memberof )
	addCategoriesFromClaims( entity, cats, 'P411', DATA.categories.canonizationstatus )
	addCategoriesFromClaims( entity, cats, 'P3919', DATA.categories.contributedto )
	addCategoriesFromClaims( entity, cats, 'P3716', DATA.categories.socialclassification )
	addCategoriesFromClaims( entity, cats, 'P1303', DATA.categories.instrument )
	
	return cats
end

--------------------------------------------------------------------------------
-- Get a single formatted date, with no categories.
-- args.year, args.type, args.wikidata_id
local function date( args )
	if args.type == nil or args.type == '' then
		args.type = 'birth'
	end
	if args.year == nil or args.year == '' then
		local item = nil
		if args.wikidata_id ~= nil and args.wikidata_id ~= '' then
			item = mw.wikibase.getEntity( args.wikidata_id )
		else
			item = mw.wikibase.getEntity()
		end
		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 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 />).
local 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 a given title as having the appropriate dates as a disambiguating suffix.
	local 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

	-- 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( 'උපන් දිනය පිළිබඳ තොරතුරු සපයා නොමැති කතුවරු' )
	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 categories for nationality, occupations, etc.

Returns a string of wikicode
]=]
local function constructCategories( args )
	local item
	if args.wikidata_id ~= nil and args.wikidata_id ~= '' then
		-- This check required because getEntity can't cope with empty strings.
		item = mw.wikibase.getEntity( args.wikidata_id )
	else
		item = mw.wikibase.getEntity()
	end
	
	if item == nil or item == "" then
		-- .getEntity() failed, possibly because the page is not connected to
		-- Wikidata (the author is unknown and with only a partial name, e.g.)
		return
	end
	
	-- defer to the core function
	local cats = categoriesList( item )
	
	local out = ''
	-- and construct a list of wikitext categories
	for _, cat in pairs( cats ) do
		out = out .. '[[Category:' .. cat .. ']]\n'
	end
	return out
end

--------------------------------------------------------------------------------
-- Output link and category for initial letters of family name.
--
-- Debugging 1: =p.lastInitial({args={last_initial='Qx'}})
-- Debugging 2: =p.lastInitial({args={wikidata_id='Q1107985'}})
-- Debugging 1: =p.lastInitial({args={lastname='Qqxxx'}})
-- Debugging 3: =p.lastInitial({args={last_initial='Qx', wikidata_id='Q1107985'}})
local function lastInitial( args )
	local initials = nil
	
	-- Allow manual override of initials.
	if args.last_initial ~= nil and args.last_initial ~= '' then
		initials = args.last_initial
	end

	-- Handle special override, used by the {{disambiguation}} template.
	if initials == '!NO_INITIALS' then
		return ''
	end

	-- If a lastname is provided, get the initials from that.
	if initials == nil and args.lastname ~= nil and args.lastname ~= '' then
		initials =  mw.ustring.sub( args.lastname, 1, 2 )
	end

	-- Fetch from Wikidata.
	if initials == nil then
		local item = nil
		if args.wikidata_id ~= nil and args.wikidata_id ~= '' then
			-- Make it possible to pass a Wikidata ID, for easier testing.
			item = mw.wikibase.getEntity( args.wikidata_id )
		else
			item = mw.wikibase.getEntity()
		end
		if item then
			-- Get the first family name statement.
			local familyNames = item:getBestStatements( PROP_FAMILY_NAME )
			if #familyNames > 0 then
				local familyNameId = familyNames[1].mainsnak.datavalue.value.id
				local familyName = mw.wikibase.getEntity( familyNameId )
				if familyName.labels ~= nil and familyName.labels.en ~= nil then
					-- Take the first two characters of the English label
					-- (this avoids issues with 'navive label P1705' and is fine for English Wikisource).
					initials = mw.ustring.sub( familyName.labels.en.value, 1, 2 ) 
				end
			end
		end
	end

	-- Put it all together and output.
	local out = ''
	if initials ~= nil then
		local authorIndex = '[[Wikisource:Authors-' .. initials .. '|Author Index: ' .. initials .. ']]'
		local authorCategory = mw.title.new('Authors-' .. initials, 'Category')
		out = authorIndex .. '[[' .. authorCategory.prefixedText .. ']]'
		if authorCategory.exists ~= true then
			local missingAuthorCat = mw.title.new('Author pages with missing initials category', 'Category')
			out = out .. '[[' .. missingAuthorCat.prefixedText .. ']]'
		end
	else
		out = '[[:Category:Authors without initials|Authors without initials]][[Category:Authors without initials]]'
	end

	return out
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;
	categories = function( frame ) return constructCategories( frame.args ) end;
	categoriesList = categoriesList;
	lastInitial = function( frame ) return lastInitial( frame.args ) end;
}