Permanently protected module

Module:Header/year

From Wikisource
Jump to navigation Jump to search

require('strict')

local p = {} --p stands for package

local yesno = require('Module:Yesno')
local TableTools = require('Module:TableTools')

--[=[
Construct the year span
--]=]

local function formatYear(year, precision)
	if precision == 7 then
		local suffixes = {
			[1] = 'st',
			[2] = 'nd',
			[3] = 'rd',
			default = 'th'
		}
		local century = (year - year % 100)/100 + 1
		return century .. (suffixes[century] or suffixes['default']) .. ' century'
	elseif precision == 8 then
		return year .. 's'
	else
		return year
	end
end

local function getYearFromSingleStatement(statement, args)
	local snak = statement.mainsnak
	if not snak or not snak.datavalue or not snak.datavalue.value or not snak['datavalue']['value']['time'] then
		return nil
	end
	
	local cats = {} -- tracking categories
	
	--[=[ Precision: 
		0 - billion years
		1 - hundred million years,
		2 - ten million years,
		3 - million years,
		4 - hundred thousand years,
		5 - ten thousand years,
		6 - millenia,
		7 - centuries,
		8 - decades,
		9 - years,
		10 - months,
		11 - days
		12 - hours
		13 - minutes
		14 - seconds
	]=]
	local precision = math.min(snak.datavalue.value.precision, 9)
	
	local year
	local bce = ''
	local circa = ''
	
	local start_times = {}
	local end_times = {}
	local start_years = {}
	local end_years = {}
	local start_year
	local end_year
	
	if statement.qualifiers then
		-- Check if date is approximate
		-- P1480 = sourcing circumstances, Q5727902 = circa
		if statement.qualifiers.P1480 then
			for _, qualifier in pairs(statement.qualifiers.P1480) do
				if qualifier.datavalue and qualifier.datavalue.value.id == 'Q5727902' then
					precision = precision - 1
					circa = 'c. '
					break
				end
			end
		end
		
		-- P580 = start time
		if statement.qualifiers.P580 then
			for k, v in pairs(statement.qualifiers.P580) do
				local startt = getYearFromSingleStatement({mainsnak = v}, {noprint = false})
				if startt then
					table.insert(start_times, startt)
				end
			end
			for k, v in pairs(start_times) do
				table.insert(start_years, v.year)
			end
			start_years = TableTools.compressSparseArray(TableTools.removeDuplicates(start_years))
			table.sort(start_years)
			if #start_years > 1 then
				table.insert(cats, '[[Category:' .. 'Works of uncertain date' .. ']]')
			end
			start_year = start_years[1]
		end
		
		-- P582 = end time
		if statement.qualifiers.P582 then
			for k, v in pairs(statement.qualifiers.P582) do
				local endt = getYearFromSingleStatement({mainsnak = v}, {noprint = false})
				if endt then
					table.insert(end_times, endt)
				end
			end
			for k, v in pairs(end_times) do
				table.insert(end_years, v.year)
			end
			end_years = TableTools.compressSparseArray(TableTools.removeDuplicates(end_years))
			table.sort(end_years)
			if #end_years > 1 then
				table.insert(cats, '[[Category:' .. 'Works of uncertain date' .. ']]')
			end
			end_year = end_years[1]
		end
	end
	
	-- precision is less than a year
	if precision < 9 then
		table.insert(cats, '[[Category:' .. 'Works of uncertain date' .. ']]')
	end
	
	-- precision is less than a century
	if precision < 7 then
		if args['noprint'] then
			year = ''
		else
			year = string.gsub(string.gsub(mw.wikibase.formatValue(statement.mainsnak), '^<span>', ''), '</span>$', '')
		end
		return {year = year, printed_year = year, cats = table.concat(cats)}
	end
	
	-- extract the year from the timestamp
	-- example timestamps: +2016-10-05T00:00:00Z, -1752-00-00T00:00:00Z
	local split = mw.text.split(snak['datavalue']['value']['time'], '-', true)
	if split[1] == '' then
		year = tonumber(split[2])
		bce = ' BCE'
	else
		year = tonumber(split[1])
	end
	
	-- malformed timestamp
	if not year then
		table.insert(cats, '[[Category:' .. 'Works of uncertain date' .. ']]')
		return {cats = table.concat(cats)}
	end
	
	-- approximate year, precision 8: year - year % 10 for decade
	-- approximate decade, precision 7: year - year % 100 for century
	if circa ~= '' then
		year = year - (year % math.pow(10, 9 - precision))
	end
	
	year = formatYear(year, precision)
	
	if start_year and start_year ~= year then
		table.insert(cats, '[[Category:' .. 'Works of uncertain date' .. ']]')
	end
	
	local printed_year = circa .. year .. bce
	
	if (start_year or end_year) and start_year ~= end_year then
		printed_year = circa .. (start_year or '') .. '–' .. (end_year or '') .. bce
	end
	if start_year and end_year and start_year ~= end_year then
		local start_decade = start_year - start_year % 10
		local end_decade = end_year - end_year % 10
		
		local start_century = start_year - start_year % 100
		local end_century = end_year - end_year % 100
		
		if start_decade == end_decade then
			table.insert(cats, '[[Category:' .. start_decade .. 's' .. bce .. ' works' .. ']]')
		elseif start_century == end_century then
			table.insert(cats, '[[Category:' .. formatYear(end_year, 7) .. bce .. ' works' .. ']]')
		end
	else
		table.insert(cats, '[[Category:' .. year .. bce .. ' works' .. ']]')
	end
	
	cats = TableTools.compressSparseArray(TableTools.removeDuplicates(cats))
	
	return {year = year, printed_year = printed_year, circa = circa, bce = bce, cats = table.concat(cats)}
end

local function parse_wikidata_year_and_categorise(args)
	-- Fetch entity object for Wikidata item connected to the current page
	-- Let manually-specified Wikidata ID override if given and valid
	if not (args.wikidata and mw.wikibase.isValidEntityId(args.wikidata)) then
		args.wikidata = mw.wikibase.getEntityIdForCurrentPage()
	end
	if not args.wikidata then
		return nil
	end
	local item = mw.wikibase.getEntity(args.wikidata)
	if not item then
		return nil
	end
	
	local statements = item:getBestStatements('P577') -- publication date
	if #statements == 0 then
		return nil
	end
	
	local years = {}
	local cats = {}
	for _, statement in pairs(statements) do
		local year_data = getYearFromSingleStatement(statement, args)
		if year_data then
			table.insert(years, year_data.printed_year)
			table.insert(cats, year_data.cats)
		end
	end
	
	years = TableTools.compressSparseArray(TableTools.removeDuplicates(years))
	cats = TableTools.compressSparseArray(TableTools.removeDuplicates(cats))
	if #years == 0 then
		return nil
	elseif #years > 1 then
		table.insert(cats, '[[Category:' .. 'Works of uncertain date' .. ']]')
	end
	return {year = table.concat(years, '/'), cats = table.concat(cats)}
end

local function parse_year_and_categorise(args)
	if args['noprint'] and args['nocat'] then
		return nil
	end
	
	local year = args['year']
	
	-- Use Wikidata if year param is empty
	if not year then
		return parse_wikidata_year_and_categorise(args)
	end
	
	local cats = {}
	local bce = ''
	local circa = ''
	
	-- Extract common era info to make it easier to process
	if string.match(year, "BC[E]?$") then
		bce = ' BCE'
		year = string.gsub(year, '%s*BC[E]?', '')
		-- Also tag it as a non-numeric year
		table.insert(cats, '[[Category:' .. 'Works with non-numeric dates' .. ']]')
	end
	
	-- If the year provided is a plain year (all digits)
	if tonumber(year) then
		table.insert(cats, '[[Category:' .. year .. bce .. ' works]]')
		return {year = year .. bce, cats = table.concat(cats)}
	end
	
	-- Add tracking category for all non-numeric dates
	table.insert(cats, '[[Category:' .. 'Works with non-numeric dates' .. ']]')
	
	-- Explicitly tagged as being of unknown date
	if year == '?' or string.lower(year) == 'unknown' then
		table.insert(cats, '[[Category:' .. 'Works of unknown date' .. ']]')
		return {year = 'unknown', cats = table.concat(cats)}
	end
	
	-- Now figure out a complex date
	
	-- Year ranges
	year = string.gsub(string.gsub(year, '%-', '–'), '—', '–')
	
	-- Approximate years
	-- Lua patterns can't do ^c(irca)?( |%.|/)* because they don't do alternation or apply quantifiers to groups
	if string.match(year, '^circa') or string.match(year, '^c%s*%.') or string.match(year, '^c%s*/') then
		circa = 'c. '
		year = string.gsub(string.gsub(string.gsub(year, '^circa%s*', ''), '^c%s*%.%s*', ''), '^c%s*/%s*', '')
		table.insert(cats, '[[Category:' .. 'Works of uncertain date' .. ']]')
		
		-- circa a specific year
		if tonumber(year) then
			year = tonumber(year)
			local decade = (year - year % 10) .. 's'
			table.insert(cats, '[[Category:' .. decade .. bce .. ' works]]')
			return {year = circa .. year .. bce, cats = table.concat(cats)}
		end
	end
	
	-- Check if it looks like a decade
	if string.match(year, '^%d*0s$') then
		table.insert(cats, '[[Category:' .. 'Works of uncertain date' .. ']]')
		table.insert(cats, '[[Category:' .. year .. bce .. ' works]]')
		return {year = circa .. year .. bce, cats = table.concat(cats)}
	end
	
	-- Or a century
	if string.match(year, '^%d+[a-z]* century$') then
		year = string.gsub(year, '^(%d+)[a-z]* century$', '%1')
		year = formatYear(100 * (tonumber(year) - 1), 7)
		table.insert(cats, '[[Category:' .. 'Works of uncertain date' .. ']]')
		table.insert(cats, '[[Category:' .. year .. bce .. ' works]]')
		return {year = circa .. year .. bce, cats = table.concat(cats)}
	end
	
	-- Or a range of years
	local start_year, end_year
	if string.match(year, '^%d*–%d*$') then
		start_year, end_year = string.match(year, '^(%d*)–(%d*)$')
	elseif string.match(year, '^%d*/%d*$') then
		start_year, end_year = string.match(year, '^(%d*)/(%d*)$')
		table.insert(cats, '[[Category:' .. 'Works of uncertain date' .. ']]')
	end
	
	if start_year or end_year then
		start_year = tonumber(start_year)
		end_year = tonumber(end_year)
		
		if start_year and end_year and start_year == end_year and circa == '' then
			table.insert(cats, '[[Category:' .. start_year .. bce .. ' works' .. ']]')
			return {year = circa .. start_year .. bce, cats = table.concat(cats)}
		elseif start_year and end_year then
			local start_decade = start_year - start_year % 10
			local end_decade = end_year - end_year % 10
			
			local start_century = start_year - start_year % 100
			local end_century = end_year - end_year % 100
			
			if start_decade == end_decade then
				table.insert(cats, '[[Category:' .. start_decade .. 's' .. bce .. ' works' .. ']]')
			elseif start_century == end_century then
				table.insert(cats, '[[Category:' .. formatYear(end_year, 7) .. bce .. ' works' .. ']]')
			end
		elseif end_year then
			table.insert(cats, '[[Category:' .. 'Works of uncertain date' .. ']]')
		end
		
		-- check isn't redundant since the year might just be –
		if start_year or end_year then
			return {year = circa .. year .. bce, cats = table.concat(cats)}
		end
	end
	
	-- If we're here we didn't manage to parse it.
	table.insert(cats, '[[Category:' .. 'Works with unrecognised dates' .. ']]')
	return {year = circa .. year .. bce, cats = table.concat(cats)}
end

function p.construct_year(args)
	local year = mw.html.create('span'):attr('id', 'header-year-text')
	
	local current_title = mw.title.getCurrentTitle()
	
	local year_args = {
		['year'] = args['year'],
		noprint = yesno(args['noyear']) or false,
		wikidata = args.wikidata
	}
	year_args['nocat'] = yesno(args['noyearcat'])
	if year_args['nocat'] == nil then
		year_args['nocat'] = (
			not args.testing -- Testing pages always categorise
		) and (
			yesno(args['disambiguation']) -- Disambiguations never categorise
			or not current_title:inNamespaces(0, 114) -- Only categorise in mainspace and Translation
			or current_title.isSubpage -- Only categorise if this is a base page
		)
	end
	local year_data = parse_year_and_categorise(year_args)
	
	if year_data then
		local cats = (not year_args['nocat'] and year_data.cats) or ''
		if year_args['noprint'] or not year_data.year then
			return cats
		else
			year:wikitext(year_data.year)
			return ' (' .. tostring(year) .. ')' .. cats
		end
	else
		return (year_args['nocat'] and '') or '[[Category:' .. 'Undated works' .. ']]'
	end
end

return p