Module:Header/year
![]() | This module depends on the following other modules: |
Supporting module for Module:Header. View test cases at Template:Header/testcases.
--[=[
Construct the year span
--]=]
require('strict')
local p = {} --p stands for package
local yesno = require('Module:Yesno')
local TableTools = require('Module:TableTools')
--[=[
Year properties:
{
year = integer,
precision = integer,
circa = boolean,
bce = boolean,
uncertain = boolean,
unknown = boolean,
unrecognised = boolean,
display = string,
start_year = year,
end_year = year,
year_list = year
}
]=]
--[=[ 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 currentyear = {
year = tonumber(os.date('%Y')),
precision = 9,
circa = false,
bce = false,
display = os.date('%Y')
}
local unknownyear = {
unknown = true,
display = 'unknown'
}
local function year_less_than(year1, year2)
if year1 == year2 then
return false
elseif not year1 or not year2 then
return year1 == nil
elseif year1.bce ~= year2.bce then
return year1.bce
end
if year1.bce then
return year1.year > year2.year
else
return year1.year < year2.year
end
end
local function year_greater_than(year1, year2)
if year1 == year2 then
return false
elseif not year1 or not year2 then
return year2 == nil
elseif year1.bce ~= year2.bce then
return year2.bce
end
if year1.bce then
return year1.year < year2.year
else
return year1.year > year2.year
end
end
local function year_equal(year1, year2)
if year1 == year2 then
return true
elseif not year1 or not year2 then
return false
end
year1.bce = year2.bce == true
year1.bce = year2.bce == true
return year1.bce == year2.bce and year1.year == year2.year
end
local function pad_number(n, pad)
n = tostring(n)
return string.rep('0', pad - string.len(n)) .. n
end
local function get_year_text(year, precision, bce)
if not tonumber(year) then
return year
elseif precision < 6 then
return nil
end
year = tonumber(year)
local suffixes = {
[1] = 'st',
[2] = 'nd',
[3] = 'rd',
['default'] = 'th'
}
local bce_text = (bce and ' BCE') or ''
if precision == 6 then
local millennium = (year - year % 1000)/1000 + 1
return millennium .. (suffixes[millennium] or suffixes['default']) .. ' millennium' .. bce_text
elseif precision == 7 then
local century = (year - year % 100)/100 + 1
return century .. (suffixes[century] or suffixes['default']) .. ' century' .. bce_text
elseif precision == 8 then
local decade = year - year % 10
return decade .. 's' .. bce_text
else
return year .. bce_text
end
end
local function format_year_for_display(year)
if not year then
return nil
end
local start_year = year.start_year and TableTools.shallowClone(year.start_year)
local end_year = year.end_year and TableTools.shallowClone(year.end_year)
if year.year then
return (year.circa and 'c. ' or '') .. get_year_text(year.year, year.precision + (year.circa and 1 or 0), year.bce) .. ((year.bce == nil and ' CE') or '')
elseif start_year or end_year then
if start_year and end_year then
if start_year.bce and end_year.bce then
start_year.bce = false
elseif start_year.bce ~= end_year.bce then
start_year.bce = start_year.bce or nil
end_year.bce = end_year.bce or nil
end
if start_year.circa then
end_year.circa = false
end
end
return table.concat({format_year_for_display(start_year) or '', format_year_for_display(end_year) or ''}, '–')
end
return nil
end
local function format_year_list_for_display(years)
local year_displays = {}
for _, year in ipairs(years) do
table.insert(year_displays, year.display or format_year_for_display(year))
end
return table.concat(year_displays, '/')
end
local function substrings_all_equal(years, i)
local substrings = {}
for _, year in ipairs(years) do
table.insert(substrings, string.sub(year, 1, i))
end
return #(TableTools.removeDuplicates(substrings)) <= 1
end
local function get_containing_year_category(years_to_categorize)
local years = {}
for _, year in ipairs(TableTools.shallowClone(years_to_categorize)) do
if not year.year or not year.precision then
return nil
else
table.insert(years, year)
end
end
if #years == 0 then
return nil
end
local bce = years[1].bce
for _, year in ipairs(years) do
year.bce = year.bce == true
if bce ~= year.bce then
return nil
end
end
local precisions = {}
for _, year in ipairs(years) do
table.insert(precisions, year.precision)
end
table.sort(precisions)
local n_length = 0
for _, year in ipairs(years) do
n_length = math.max(n_length, string.len(year.year))
end
local padded_years = {}
for _, year in ipairs(years) do
table.insert(padded_years, pad_number(year.year, n_length))
end
local shared_digits = 1
while shared_digits <= n_length and substrings_all_equal(padded_years, shared_digits) do
shared_digits = shared_digits + 1
end
shared_digits = shared_digits - 1
local range_precision = math.min(precisions[1], 9 - n_length + shared_digits)
return get_year_text(years[1].year, range_precision, years[1].bce)
end
local function categorise_year(args)
local cats = {}
if not args.input_year and args.year then
table.insert(cats, '[[Category:' .. 'Works with year from Wikidata' .. ']]')
elseif not args.input_year then
table.insert(cats, '[[Category:' .. 'Undated works' .. ']]')
elseif args.input_year and not tonumber(args.input_year) then
table.insert(cats, '[[Category:' .. 'Works with non-numeric dates' .. ']]')
end
if args.unrecognised then
table.insert(cats, '[[Category:' .. 'Works with unrecognised dates' .. ']]')
end
local year = args.year or {}
local start_year = year.start_year or (year.end_year ~= nil and unknownyear) or {}
local end_year = year.end_year or (year.start_year ~= nil and currentyear) or {}
start_year.bce = start_year.bce == true
end_year.bce = end_year.bce == true
local year_list = year.year_list or {}
local precisions = {}
table.insert(precisions, year.precision)
table.insert(precisions, start_year.precision)
table.insert(precisions, end_year.precision)
table.sort(precisions)
if year.uncertain or start_year.uncertain or end_year.uncertain or year.circa or start_year.circa or end_year.circa or #(year_list) > 1 or (precisions[1] and precisions[1] < 9) then
table.insert(cats, '[[Category:' .. 'Works of uncertain date' .. ']]')
end
if year.unknown or start_year.unknown or end_year.unknown then
table.insert(cats, '[[Category:' .. 'Works of unknown date' .. ']]')
end
if start_year.year and end_year.year and year_greater_than(start_year, end_year) then
table.insert(cats, '[[Category:' .. 'Works with start and end dates in the wrong order' .. ']]')
end
-- single year
local year_for_cat
if year.year then
year_for_cat = get_year_text(year.year, year.precision, year.bce)
-- date range
elseif start_year.year and end_year.year and start_year.bce == end_year.bce then
year_for_cat = get_containing_year_category({start_year, end_year})
elseif #year_list > 0 then
year_for_cat = get_containing_year_category(year_list)
end
table.insert(cats, year_for_cat and ('[[Category:' .. year_for_cat .. ' works]]'))
return table.concat(cats)
end
local function get_year_from_single_statement(statement)
local snak = statement.mainsnak
if not snak or not snak.datavalue or not snak.datavalue.value or not snak['datavalue']['value']['time'] or not snak.datavalue.value.precision then
return nil
end
local precision = snak.datavalue.value.precision
if precision < 6 then
-- precision is less than a millennium
return {
precision = precision,
display = string.gsub(mw.wikibase.formatValue(statement.mainsnak), '^<span>(.*)</span>$', '%1')
}
end
local start_years = {}
local end_years = {}
local start_year, end_year
local circa = false
if statement.qualifiers then
-- Check if date is approximate
-- P1480 = sourcing circumstances, Q5727902 = circa
if statement.qualifiers.P1480 then
for _, qualifier in ipairs(statement.qualifiers.P1480) do
if qualifier.datavalue and qualifier.datavalue.value.id == 'Q5727902' then
circa = true
break
end
end
end
if circa then
precision = precision - 1
end
-- P580 = start time
if statement.qualifiers.P580 then
for k, v in pairs(statement.qualifiers.P580) do
table.insert(start_years, get_year_from_single_statement({mainsnak = v}))
end
start_years = TableTools.compressSparseArray(TableTools.removeDuplicates(start_years))
table.sort(start_years, year_less_than)
start_year = start_years[1]
if #start_years > 1 then
start_year.uncertain = true
end
end
-- P582 = end time
if statement.qualifiers.P582 then
for k, v in pairs(statement.qualifiers.P582) do
table.insert(end_years, get_year_from_single_statement({mainsnak = v}))
end
end_years = TableTools.compressSparseArray(TableTools.removeDuplicates(end_years))
table.sort(end_years, year_less_than)
end_year = end_years[1]
if #end_years > 1 then
end_year.uncertain = true
end
end
end
-- extract the year from the timestamp
-- example timestamps: +2016-10-05T00:00:00Z, -1752-00-00T00:00:00Z
local year
local bce = false
local split = mw.text.split(snak['datavalue']['value']['time'], '-', true)
if split[1] == '' then
year = tonumber(split[2])
bce = true
else
year = tonumber(split[1])
end
-- malformed timestamp
if not year then
return nil
end
local year_data = {
year = (not start_year and not end_year and year) or nil,
circa = circa,
bce = bce,
uncertain = (start_year and start_year.year ~= year) or circa,
precision = precision,
start_year = start_year,
end_year = end_year
}
year_data.display = format_year_for_display(year_data)
return year_data
end
local function get_wikidata_year(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 = {}
for _, statement in ipairs(statements) do
local year_data = get_year_from_single_statement(statement)
table.insert(years, year_data)
end
years = TableTools.compressSparseArray(TableTools.removeDuplicates(years))
if #years == 0 then
return nil
elseif #years == 1 then
return years[1]
end
return {
uncertain = true,
year_list = years,
display = format_year_list_for_display(years)
}
end
local function parse_year(year)
if not year then
return nil
elseif year == '?' or string.lower(year) == 'unknown' then
return unknownyear
end
local input_year = year
local circa = false
-- Lua patterns can't do ^c(irca)?(%s|%.|/)* because they don't do alternation or apply quantifiers to groups
if string.match(year, '^circa') or string.match(year, '^c%.') or string.match(year, '^c%s*/') then
circa = true
year = string.gsub(string.gsub(string.gsub(year, '^circa%s*', ''), '^c%.%s*', ''), '^c%s*/%s*', '')
end
local bce = false
if string.match(year, 'BC[E]?$') then
bce = true
year = string.gsub(year, '%s*BC[E]?$', '')
end
if string.match(year, '/') then
local year_split = mw.text.split(year, '/')
local years = {}
for _, opt in ipairs(year_split) do
if opt ~= '' then
local y = parse_year(opt)
y.bce = y.bce or bce
y.circa = y.circa or circa
table.insert(years, y)
end
end
if #years > 1 then
return {
uncertain = true,
year_list = years,
display = format_year_list_for_display(years)
}
elseif #years == 1 then
return years[1]
end
end
local start_year, end_year
year = string.gsub(string.gsub(year, '%-', '–'), '—', '–')
if string.match(year, '^[^–]*–[^–]*$') then
local year_range = mw.text.split(year, '–')
start_year = mw.text.trim(year_range[1])
end_year = mw.text.trim(year_range[2])
if start_year == '' then
start_year = nil
else
start_year = parse_year(start_year)
start_year.circa = start_year.circa or circa
start_year.bce = start_year.bce or bce
end
if end_year == '' then
end_year = nil
else
end_year = parse_year(end_year)
end_year.circa = end_year.circa or circa
end_year.bce = end_year.bce or bce
end
end
local precision
if tonumber(year) then
precision = 9
-- Check if it looks like a decade
elseif string.match(year, '^%d*0s$') then
precision = 8
year = string.gsub(year, '^(%d*0)s$', '%1')
-- Or a century
elseif string.match(year, '^%d+[a-z]* century$') then
precision = 7
year = string.gsub(year, '^(%d+)[a-z]* century$', '%1')
year = 100 * (tonumber(year) - 1)
-- Or a millennium
elseif string.match(year, '^%d+[a-z]* millennium$') then
precision = 6
year = string.gsub(year, '^(%d+)[a-z]* millennium', '%1')
year = 1000 * (tonumber(year) - 1)
end
if circa and precision then
precision = precision - 1
end
if tonumber(year) then
local data = {
year = tonumber(year),
bce = bce,
circa = circa,
precision = precision
}
data['display'] = format_year_for_display(data)
return data
elseif start_year or end_year then
return {
start_year = start_year,
end_year = end_year,
display = format_year_for_display({start_year = start_year, end_year = end_year})
}
end
return {
unrecognised = true,
display = input_year
}
end
function p.construct_year(args)
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
if args.testing then
year_args.nocat = false
else
year_args.nocat = (
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
end
local year_data = parse_year(year_args.year) or get_wikidata_year(year_args)
local cats = (year_args.nocat and '') or categorise_year({input_year = year_args.year, year = year_data})
if args.testing then
cats = mw.text.nowiki(cats) .. cats
end
if year_args.noprint or not year_data or not year_data.display then
return cats
end
local year_span = mw.html.create('span')
:addClass('wst-header-year-text')
:wikitext(year_data.display)
:allDone()
return ' (' .. tostring(year_span) .. ')' .. cats
end
return p