Перейти к содержанию

Модуль:DecadeMetaCat

Материал из энциклопедии Руниверсалис

Модуль используется для навигации и категоризации категорий по десятилетиям (для категорий с заголовком, включающим «<число кратное 10>-е/-х годы/годов/годах»).

  • Определяет десятилетие и эру (до н.э. / н.э.).
  • Создаёт навигационную линейку по десятилетиям.
  • Корректно работает с десятилетиями до нашей эры.
  • Добавляет категории.
Использование
{{#invoke:DecadeMetaCat|main
|Мир <десятилетие>-х годов
|Мир <век> века! <ключ>
|Мир по десятилетиям! <ключ>
}}
  • <десятилетие> — десятилетие числом (без окончания -е/-х)
  • <век> — век римскими цифрами без слова «век»
  • <ключ> — ключ сортировки, н. э. — год, до н. э. — отрицательное число начиная с -10000 (-10000 == 0-е годы до н. э. -9990 == 10-е годы до н. э. и т. д.); нужен для корректной сортировки в категориях

Полная версия

{{#invoke:DecadeMetaCat|main
|Категория 1![ключ сортировки]
...
|Категория N[...]
|min = до какого года рисовать линейку слева, по умолчанию -40000 (0 — рисовать только года нашей эры)
|max = до какого года рисовать линейку справа, по умолчанию 2100
|range = сколько десятилетий в линейке слева и справа, по умолчанию 5
|title = заголовок страницы, используемый вместо текущего (для тестов)
}}

Категория состоит из 2-х полей, разделенных ! (восклицательным знаком):

Примеры:

  • |Мир по десятилетиям! <ключ> — добавлять категорию «Мир по десятилетиям» с ключом сортировки <пробел><ключ>
Дополнительные функции

Функция expand

  • заменяет <десятилетие> на текущее, по необходимости добавив «до н. э.»
  • заменяет <век> на текущий, по необходимости добавив «до н. э.»
  • заменяет <ключ> на ключ сортировки

Пример:

{{#invoke:DecadeMetaCat|expand|Мир <десятилетие>-х годов}}

на странице «К:Земля в 100-е годы до н. э.» вернёт:

Мир 100-х годов до н. э.
См. также

---*- mode: lua; coding: utf-8; -*-
local p = {}

-- Переменные
local dec	-- десятилетие, положительное число
local BC	-- 0 == н.э. 1 == до н.э.
local templ	-- строка-шаблон вида 'Мир в %s-е годы%s'
local title = mw.title.getCurrentTitle().text

-- Опции
local dec_min = -40000	-- 0 == только н.э.
local dec_max = 2100	-- XXI
local range = 5

-- Импортируемые функции
local getArgs = require('Module:Arguments').getArgs
local sparseIpairs = require('Module:TableTools').sparseIpairs
local toroman = require('Module:Roman').convert
local getStyles = require('Модуль:Индекс категории').getStyles
local gsub = mw.ustring.gsub
local findCountry = require('Модуль:Find country')
local countryModule = require('Модуль:CountryMetaCat')

-- Инициализация трекера для ошибок
local error_list = {}
local decade_range_error = nil
local country_error_flag = false
local unique_errors = {}

------------------ Ошибки ------------------
-- Сбор и обработка ошибок
local function add_error(error_code, additional_info)
	local error_specific = {
		[1] = 'Ошибка: десятилетие не найдено.',
		[2] = 'Минимальное десятилетие, ограниченное шаблоном: ' .. (additional_info or "") .. '.',
		[3] = 'Максимальное десятилетие, ограниченное шаблоном: ' .. (additional_info or "") .. '.',
		[4] = 'Минимальное десятилетие для ' .. (additional_info or "") .. '.',
		[5] = 'Максимальное десятилетие для ' .. (additional_info or "") .. '.',
		[6] = 'Ошибка: страна не найдена.',
		[7] = 'Ошибка: часть света для страны не найдена.',
		[8] = 'Ошибка: обнаружено два десятилетия.'
	}
	if error_code >= 2 and error_code <= 5 then
		if not decade_range_error then
			decade_range_error = {message = 'Ошибка: десятилетие не попадает в заданный диапазон.', details = {}}
			table.insert(error_list, decade_range_error)
		end
		table.insert(decade_range_error.details, error_specific[error_code])
	else
		local error_message = '<span class="error">' .. error_specific[error_code] .. '</span>'
		if not unique_errors[error_message] then
			unique_errors[error_message] = true
			table.insert(error_list, {message = error_message})
		end
	end
end

-- Публикация всех ошибок в едином блоке
local function publish_errors()
	local error_category = '[[Категория:Википедия:Страницы с некорректным использованием модуля DecadeMetaCat]]'
	if #error_list == 0 then
		return ''
	end
	local result = '<div class="error-list">'
	for _, err in ipairs(error_list) do
		if err.details then
			result = result .. '<span class="error">' .. err.message
			for _, detail in ipairs(err.details) do
				result = result .. ' ' .. detail
			end
			result = result .. '</span>'
		else
			result = result .. err.message
		end
	end
	result = result .. '</div>'
	result = result .. error_category
	return result
end

--------------- Считывание и обработка десятилетий ---------------
-- Считывание десятилетия из строки
local function get_dec(t)
	local decades = {}
	for dec in mw.ustring.gmatch(t, '([0-9]*0)-[ех] год') do
		table.insert(decades, tonumber(dec))  -- Преобразование строки в число
	end
	if #decades == 0 then
		add_error(1)  -- Ошибка "не найден"
		return nil
	elseif #decades > 1 then
		add_error(8)  -- Ошибка "обнаружено два"
		return nil
	end
	return decades[1]  -- Возврат единственного найденнего значения
end

-- Замена плейсхолдеров (десятилетие, век, ключ) на реальные значения
local function do_expand(s)
	-- <десятилетие> - десятилетие числом (без окончания -е/-х)
	-- <ключ> - ключ сортировки, н.э. - номер года,
	-- до н.э. - отрицательное число начиная с -10000 (-10000 == 0-е годы до н.э. -9990 == 10-е годы до н.э. -9980 == 20-е годы до н.э. и т.д.)
	-- <век> - век римскими цифрами
	local c = toroman(math.floor(dec / 100) + 1)
	-- Обработка для II века (в/во)
	if c == 'II' then
		s = gsub(s, ' в <век>', ' во <век>')
	end
	if BC == 1 then
		s = gsub(s, '<десятилетие>(-[ех] год[ыоа][вх]?)', dec..'%1 до н. э.')  -- годы/годов/годах
		s = gsub(s, '<ключ>', '0' .. (dec - 10000))  -- Преобразование ключа для до н.э.
		s = gsub(s, '<век> (век[еа]?)', c..' %1 до н. э.')
	else
		s = gsub(s, '<десятилетие>', dec)
		if dec == 0 then
			s = gsub(s, '<ключ>', '5')  -- Ключ 5 для 0-х н. э.
		else
			s = gsub(s, '<ключ>', dec)
		end
		s = gsub(s, '<век>', c)
	end
	return s
end

--------------- Обработка min/max ---------------
-- Поиск данных о стране в JSON-файле по названию или алиасу
local function find_country_in_json(country_name)
	local country_data = mw.loadJsonData('Модуль:YearMetaCat2/country-years.json')
	for _, country in ipairs(country_data.countries) do
		if country.name == country_name then
			return country
		end
		if country.aliases then
			for _, alias in ipairs(country.aliases) do
				if alias == country_name then
					return country
				end
			end
		end
	end
	return nil
end

-- Проверка, попадает ли десятилетие в диапазон страны или вручную заданные значения
local function check_decade_in_bounds(args)
	args = args or {}
	local country_name = findCountry.findcountryinstring(title)
	local country_data = find_country_in_json(country_name)
	-- Корректировка для до н.э.
	local dec_adjusted = (BC == 1) and -dec or dec
	-- Ручные ограничения min и max (только для десятилетий)
	local manual_min = tonumber(args['min'])
	local manual_max = tonumber(args['max'])
	-- Преобразуем годы из JSON в десятилетия, если данные найдены
	local country_min = country_data and country_data.min and math.floor(country_data.min / 10) * 10 or nil
	local country_max = country_data and country_data.max and math.floor(country_data.max / 10) * 10 or nil
	-- Определение активных границ
	local effective_min = manual_min or country_min
	local effective_max = manual_max or country_max
	-- Проверка минимального значения
	if effective_min and dec_adjusted < effective_min then
		if manual_min then
			-- Если задано вручную
			add_error(2, string.format("%d-е", manual_min))
		elseif country_min then
			-- Если данные из страны
			add_error(4, string.format("%s: %d-е (минимальный год: %d)", country_name, country_min, country_data.min))
		end
	end
	-- Проверка максимального значения
	if effective_max and dec_adjusted > effective_max then
		if manual_max then
			-- Если задано вручную
			add_error(3, string.format("%d-е", manual_max))
		elseif country_max then
			-- Если данные из страны
			add_error(5, string.format("%s: %d-е (максимальный год: %d)", country_name, country_max, country_data.max))
		end
	end
end

--------------- Считывание и обработка стран ---------------
-- Проверка на наличие плейсхолдеров, связанных со странами
local function has_country_placeholders(s)
	local placeholders = {
		'<страна>', '<страны>', '<в стране>',
		'<часть света>', '<части света>', '<в части света>',
		'<государство>', '<государства>', '<в государстве>'
	}
	-- Проверка на стандартные плейсхолдеры
	for _, placeholder in ipairs(placeholders) do
		if s:find(placeholder, 1, true) then
			return true
		end
	end
	-- Проверка на плейсхолдеры с указанием названия страны (например, <государство:Название страны>)
	local complex_placeholders = {
		'<государство:[^>]+>',
		'<государства:[^>]+>',
		'<в государстве:[^>]+>'
	}
	for _, pattern in ipairs(complex_placeholders) do
		if s:find(pattern) then
			return true
		end
	end
	return false
end

-- Обработка стран, частей света стран или государств
local function process_country_placeholders(s, title, current_dec)
	if type(s) ~= 'string' then return {}, nil end
	local result_lines = {}
	local added_categories = {}
	-- Вызываем resolve_country с нужными параметрами
	local adjusted_time = (BC == 1) and (-current_dec - 10) or current_dec
	local country_result = countryModule.resolve_country({
		[1] = s,
		title = title,
		type = "decade",
		time = adjusted_time
	})
	if country_result then
		-- Обработка основного результата
		if country_result.result and country_result.result ~= "" then
			table.insert(result_lines, {text = country_result.result, type = "main"})
		end
		-- Обработка дополнительного результата
		if country_result.extra_result and country_result.extra_result ~= "" then
			table.insert(result_lines, {text = country_result.extra_result, type = "extra"})
		end
		-- Обработка ошибок от country_module
		if country_result.error and country_result.error > 0 then
			add_error(country_result.error == 1 and 6 or 7)
			country_error_flag = true
		end
	end
	return result_lines
end

--------------- Форматирование строк ---------------
-- Формирование шаблона строки для отображения года с учётом до н. э.
local function get_templ(s)
	-- Формируем строку-шаблон вида: 'Мир в 90-е годы до н. э.' -> 'Мир в %s-е годы%s'
	local t
	t, BC = gsub(s, '[0-9]*0(-[ех] год[ыоа][вх]?) до н%. э.', '%%s%1%%s')
	local n = BC
	if BC ~= 1 then
		t, n = gsub(s, '[0-9]*0(-[ех] год[ыоа][вх]?)', '%%s%1%%s')
	end
	if n ~= 1 then  -- Ошибка, если совпадений нет или их больше одного
		add_error(1)
	end
	templ = t
end

-- Форматирование года с учётом до н. э.
local function format(d, wiki)
	local bcs, t
	if d < 0 then
		d = -d - 10
		bcs = ' до н. э.'
		t = '−'..d
	else
		bcs = ''
		t = d
	end
	local s
	if wiki then
		s = string.format(templ, d, bcs)
		s = string.format('[[:К:%s|%s]]', s, t)
	else
		s = t
	end
	return s
end

--------------- Список категорий ---------------
-- Проверка на существование категории
local function category_exists(category_name)
	if not category_name or category_name == '' then return false end
	-- Удаление символов ? ~ вначале или ! с текстом вконце
	category_name = mw.ustring.match(category_name, "^[%?~]*(.-)!") or category_name
	local title = mw.title.new('Категория:' .. category_name)
	return title and title.exists
end

-- Основная обработка категорий
local function cats(args)
	local ret = ''
	local added_categories = {}
	local lines = {}
	
	-- Вспомогательная функция для добавления категории
	local function add_category(text)
		local processed = do_expand(text:gsub("!", "|"))
		local categories = mw.text.split(processed, "|")
		local cat_name = categories[1]
		if not added_categories[cat_name] then
			ret = ret .. string.format('[[Категория:%s%s]]',
				cat_name,
				categories[2] and ('|' .. categories[2]) or ''
			)
			added_categories[cat_name] = true
			return true
		end
		return false
	end
	
	-- Обработка входных аргументов и заполнение lines
	for i, arg in sparseIpairs(args) do
		if type(arg) == "string" and arg ~= "" then
			if has_country_placeholders(arg) then
				local result = process_country_placeholders(arg, title, dec)
				lines[i] = {
					original = arg,
					results = result or {},
					is_placeholder = true
				}
			else
				local text = do_expand(arg)
				lines[i] = {
					original = arg,
					results = {
						{text = text, type = "main"},
						{text = text, type = "extra"}
					},
					is_placeholder = false
				}
			end
		end
	end

	local i = 1
	while i <= #lines do
		local line = lines[i]
		if line then
			local first_char = mw.ustring.sub(line.original, 1, 1)
			if first_char == '?' then
				local exists = {main = false, extra = false}
				local questions = {main = {}, extra = {}}
				
				-- Проверяем существование категорий
				for _, result in ipairs(line.results) do
					local text = result.text:sub(2):gsub("^%s+", "")
					local cat_name = do_expand(text:match("^(.-)!") or text)
					questions[result.type][cat_name] = text
					if category_exists(cat_name) then
						exists[result.type] = true
						add_category(result.text:sub(2))
					end
				end

				local j = i + 1
				while j <= #lines and mw.ustring.sub(lines[j].original, 1, 1) == '~' do
					for _, result in ipairs(lines[j].results) do
						if not exists[result.type] and next(questions[result.type]) then
							add_category(result.text:sub(2))
						end
					end
					j = j + 1
				end
				i = j - 1
				
			elseif first_char ~= '~' then
				for _, result in ipairs(line.results) do
					if result.text and result.text ~= "" then
						add_category(result.text)
					end
				end
			end
		end
		i = i + 1
	end
	
	return ret
end

--------------- Навигационный блок ---------------
local function navbox()
	-- Корректировка для до н. э.
	local d = (BC == 1) and (-dec - 10) or dec
	local wt = mw.html.create('div'):addClass('ts-module-Индекс_категории hlist')
	local row = wt:tag('ul')
	-- Корректировка min и max для до н. э.
	dec_min = (dec_min < 0) and (dec_min - 10) or dec_min
	dec_max = (dec_max < 0) and (dec_max - 10) or dec_max
	local country_data = find_country_in_json(findCountry.findcountryinstring(title))
	-- Определение минимального и максимального десятилетий для страны
	local country_min_decade = country_data and country_data.min and 
		math.max(dec_min, math.floor(country_data.min / 10) * 10) or dec_min
	local country_max_decade = country_data and country_data.max and 
		math.min(dec_max, math.floor(country_data.max / 10) * 10 + 9) or dec_max
	-- Определение стартового и конечного десятилетий
	local dstart = math.max(country_min_decade, d - range * 10)
	local dend = math.min(country_max_decade, d + range * 10)
	-- Если диапазон некорректный, возвращаем пустую строку
	if dend < dstart then return "" end
	-- Добавляем элементы в навигационную полоску
	for i = dstart, dend, 10 do
		row:tag('li'):wikitext(format(i, true))
	end
	return getStyles() .. tostring(wt)
end

--------------- Вывод ---------------
function p.main(frame)
	local args = getArgs(frame)
	title = args['title'] or title
	range = tonumber(args['range'] or range)

	if mw.title.getCurrentTitle().namespace == 10 then  -- проверка пространства шаблонов
		return	"[[Категория:Шаблоны, использующие модуль DecadeMetaCat]]" ..
				"[[Категория:Шаблоны, использующие индекс категории (автоматический)]]"
	end

	-- Обработка вручную заданных min и max
	dec_min = tonumber(args['min'] or dec_min)
	dec_max = tonumber(args['max'] or dec_max)

	-- Нахождение десятилетия по заголовку страницы
	dec = get_dec(title)
	if not dec then
		return publish_errors()  -- Возврат ошибок и прекращаем выполнение, если десятилетие не найдено
	end

	-- Создание шаблона-строки
	get_templ(title)

	-- Стандартная категоризация
	local categories = cats(args)

	-- Проверка, попадает ли год в допустимые границы
	check_decade_in_bounds(args)

	local output = ""

	-- Навигационная полоска с отключением
	if args['nonav'] ~= "1" then
		output = output .. navbox(title)
	end

	-- Автоиндекс с отключением
	if args['noindex'] ~= "1" then
		output = output .. mw.getCurrentFrame():preprocess('{{индекс категории (автоматический)}}\n')
	end

	-- Преобразование таблицы категорий в строку, если это таблица
	if type(categories) == "table" then
		local flat_categories = {}
		for _, value in ipairs(categories) do
			table.insert(flat_categories, value.text)
		end
		categories = table.concat(flat_categories, '')
	end

	output = output .. publish_errors()
	return output .. (categories or "")
end

-- Вспомогательная функция для развёртывания
function p.expand(frame)
	local args = getArgs(frame)
	title = args['title'] or title
	dec = get_dec(title)
	if not dec then
		return publish_errors()
	end
	BC = mw.ustring.find(title, '[0-9]*0-[ех] год[ыоа][вх]? до н%. э%.')
	if BC then
		BC = 1
	else
		BC = 0
	end
	return do_expand(args[1])
end
return p