Modiwl:Author
Documentation for this module may be created at Modiwl:Author/doc
require( "strict" )
-- Local variables.
local dateModule = require( "Module:Era" )
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( '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+)%-' )
local 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 ~= 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
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
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:Author/data' )
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 )
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( '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 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;
}