پودمان:Header/year
ظاهر
توضیحات این پودمان میتواند در پودمان:Header/year/توضیحات قرار گیرد.
--[=[
Construct the year span
Accepts an optional |calendar= parameter: 'Miladi' (Gregorian),
'Khorshidi' (Solar Hijri, default), or 'Qamari' (Lunar Hijri). Only affects
how a manually-typed |year= is labelled; Wikidata-derived years are always
forced to Gregorian regardless of this parameter/default, since Wikidata
only stores proleptic Gregorian dates.
--]=]
require('strict')
local p = {} --p stands for package
local yesno = require('پودمان:Yesno')
local TableTools = require('پودمان: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
]=]
-- Convert Latin digits to Persian digits; only used for final display
local function to_persian_digits(n)
local digit_map = {
['0'] = '۰', ['1'] = '۱', ['2'] = '۲', ['3'] = '۳', ['4'] = '۴',
['5'] = '۵', ['6'] = '۶', ['7'] = '۷', ['8'] = '۸', ['9'] = '۹'
}
return (tostring(n):gsub('%d', digit_map))
end
-- Convert Persian digits to Latin digits, so editors who type the year using Persian numerals are also supported
local function from_persian_digits(str)
if not str then
return str
end
local digit_map = {
['۰'] = '0', ['۱'] = '1', ['۲'] = '2', ['۳'] = '3', ['۴'] = '4',
['۵'] = '5', ['۶'] = '6', ['۷'] = '7', ['۸'] = '8', ['۹'] = '9'
}
local result = str
for persian, latin in pairs(digit_map) do
result = string.gsub(result, persian, latin)
end
return result
end
local currentyear = {
year = tonumber(os.date('%Y')),
precision = 9,
circa = false,
bce = false,
display = to_persian_digits(os.date('%Y'))
}
local unknownyear = {
unknown = true,
display = 'نامشخص'
}
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
-- Validate the calendar parameter. Returns (normalised_value, was_invalid).
-- Falls back to Solar Hijri (Khorshidi) if unset, blank, or unrecognised.
-- Note: Wikidata-derived years always force Gregorian regardless of this default;
-- see the 'effective_calendar' handling in p.construct_year.
local function normalize_calendar(calendar)
local valid_calendars = {
['میلادی'] = true,
['خورشیدی'] = true,
['قمری'] = true
}
if not calendar or mw.text.trim(calendar) == '' then
return 'خورشیدی', false
end
calendar = mw.text.trim(calendar)
if valid_calendars[calendar] then
return calendar, false
end
return 'خورشیدی', true
end
-- Abbreviated era/calendar marker shown next to the year in the header display.
local calendar_display_abbrev = {
['میلادی'] = ' م.',
['خورشیدی'] = ' ه.ش',
['قمری'] = ' ه.ق'
}
-- Era/calendar marker shown next to the year in the header display.
-- All three calendars are always marked explicitly now (no more implicit/silent
-- default), so a range only needs to suppress the marker on its first half when
-- both halves share the exact same era - see the suppress_era flag below.
local function display_era_suffix(bce, calendar)
if bce then
return ' پیش از میلاد'
else
return calendar_display_abbrev[calendar] or ''
end
end
-- Era/calendar marker shown in the year category name; unlike the display
-- version, the calendar name is always spelled out, including for Gregorian
local function category_era_suffix(bce, calendar)
if bce then
return ' پیش از میلاد'
else
return ' (' .. (calendar or 'میلادی') .. ')'
end
end
local function get_year_text(year, precision)
if not tonumber(year) then
return year
elseif precision < 6 then
return nil
end
year = tonumber(year)
if precision == 6 then
local millennium = (year - year % 1000)/1000 + 1
return 'هزاره ' .. to_persian_digits(millennium)
elseif precision == 7 then
local century = (year - year % 100)/100 + 1
return 'سده ' .. to_persian_digits(century)
elseif precision == 8 then
local decade = year - year % 10
return 'دهه ' .. to_persian_digits(decade)
else
return to_persian_digits(year)
end
end
local function format_year_for_display(year, calendar)
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
local suffix = (not year.suppress_era) and display_era_suffix(year.bce, calendar) or ''
return (year.circa and 'حدود ' or '') .. get_year_text(year.year, year.precision + (year.circa and 1 or 0)) .. suffix
elseif start_year or end_year then
if start_year and end_year then
-- Same era on both sides (e.g. both Gregorian, or both BCE): show the
-- marker only once, on the end year. Different eras: show both, to disambiguate.
if start_year.bce == end_year.bce then
start_year.suppress_era = true
end
if start_year.circa then
end_year.circa = false
end
end
return table.concat({format_year_for_display(start_year, calendar) or '', format_year_for_display(end_year, calendar) or ''}, '–')
end
return nil
end
local function format_year_list_for_display(years, calendar)
local year_displays = {}
for _, year in ipairs(years) do
table.insert(year_displays, year.display or format_year_for_display(year, calendar))
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)
end
local function categorise_year(args)
local cats = {}
if not args.input_year and args.year then
table.insert(cats, '[[رده:' .. 'آثار با سال از ویکیداده' .. ']]')
elseif not args.input_year then
table.insert(cats, '[[رده:' .. 'آثار بیتاریخ' .. ']]')
elseif args.input_year and not tonumber(from_persian_digits(args.input_year)) then
table.insert(cats, '[[رده:' .. 'آثار با تاریخهای غیرعددی' .. ']]')
end
if args.unrecognised then
table.insert(cats, '[[رده:' .. 'آثار با قالب تاریخ نامعتبر' .. ']]')
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, '[[رده:' .. 'آثار با تاریخ نامعین' .. ']]')
end
if year.unknown or start_year.unknown or end_year.unknown then
table.insert(cats, '[[رده:' .. 'آثار با تاریخ نامشخص' .. ']]')
end
if start_year.year and end_year.year and year_greater_than(start_year, end_year) then
table.insert(cats, '[[رده:' .. 'آثار با ترتیب نادرست تاریخ آغاز و پایان' .. ']]')
end
-- single year
local year_for_cat
local year_for_cat_bce
if year.year then
year_for_cat = get_year_text(year.year, year.precision)
year_for_cat_bce = 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})
year_for_cat_bce = start_year.bce
elseif #year_list > 0 then
year_for_cat = get_containing_year_category(year_list)
year_for_cat_bce = year_list[1] and year_list[1].bce
end
if year_for_cat then
table.insert(cats, '[[رده:' .. 'آثار ' .. year_for_cat .. category_era_suffix(year_for_cat_bce, args.calendar) .. ']]')
end
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
}
-- Wikidata dates are always Gregorian
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,
-- Wikidata dates are always Gregorian
display = format_year_list_for_display(years, 'میلادی')
}
end
local function parse_year(year, calendar)
if not year then
return nil
elseif year == '?' or year == '؟' or string.lower(year) == 'unknown' or year == 'نامشخص' or year == 'ناشناس' then
return unknownyear
end
local input_year = year
year = from_persian_digits(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*/') or string.match(year, '^حدود') then
circa = true
year = string.gsub(string.gsub(string.gsub(string.gsub(year, '^circa%s*', ''), '^c%.%s*', ''), '^c%s*/%s*', ''), '^حدود%s*', '')
end
local bce = false
if string.match(year, 'BC[E]?$') or string.match(year, 'پیش از میلاد$') then
bce = true
year = string.gsub(string.gsub(year, '%s*BC[E]?$', ''), '%s*پیش از میلاد$', '')
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, calendar)
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, calendar)
}
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, calendar)
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, calendar)
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$') or string.match(year, '^دهه%s*%d+$') then
precision = 8
year = string.gsub(string.gsub(year, '^(%d*0)s$', '%1'), '^دهه%s*(%d+)$', '%1')
-- Or a century
elseif string.match(year, '^%d+[a-z]* century$') or string.match(year, '^سده%s*%d+$') or string.match(year, '^قرن%s*%d+$') then
precision = 7
year = string.gsub(string.gsub(string.gsub(year, '^(%d+)[a-z]* century$', '%1'), '^سده%s*(%d+)$', '%1'), '^قرن%s*(%d+)$', '%1')
year = 100 * (tonumber(year) - 1)
-- Or a millennium
elseif string.match(year, '^%d+[a-z]* millennium$') or string.match(year, '^هزاره%s*%d+$') then
precision = 6
year = string.gsub(string.gsub(year, '^(%d+)[a-z]* millennium', '%1'), '^هزاره%s*(%d+)$', '%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, calendar)
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}, calendar)
}
end
return {
unrecognised = true,
display = input_year
}
end
function p.construct_year(args)
local current_title = mw.title.getCurrentTitle()
local calendar, invalid_calendar = normalize_calendar(args.calendar)
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
-- calendar only applies to a manually-typed |year=; Wikidata-derived years are forced to Gregorian
local year_data = parse_year(year_args.year, calendar)
local effective_calendar = calendar
if not year_data then
year_data = get_wikidata_year(year_args)
effective_calendar = 'میلادی'
end
local cats = ''
if not year_args.nocat then
cats = categorise_year({input_year = year_args.year, year = year_data, calendar = effective_calendar})
if invalid_calendar then
cats = cats .. '[[رده:' .. 'صفحههای دارای پارامتر calendar نامعتبر' .. ']]'
end
end
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