پرش به محتوا

پودمان: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) -- 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