Модуль:DecadeMetaCat

Материал из Википедии — свободной энциклопедии
Перейти к навигации Перейти к поиску
Документация

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

Возможности

  • Определяет десятилетие, век и эру (до н. э. / н. э.).
  • Работает со странами
    • Определяет название страны из заголовка в любом падеже.
    • В категориях меняет падеж страны на нужный.
    • Определяет, в какой части света (континенте) расположена страна и публикует её/их в нужном падеже.
  • Позволяет проверить существование категории или опубликовать замену для неё.
  • Добавляет {{автоиндекс}} (появляется от 200 статей, расширенный индекс от 1200 статей).
  • Создаёт навигационную линейку по десятилетиям, с возможностями:
    • Задавать min/max десятилетия.
    • Указывать количество элементов в линейке.
    • Автоматически отслеживает годы существования отдельных стран и выдаёт ошибку при выходе за ограничение.
  • Добавляет категории.

Используемые списки:

Использование

{{#invoke:DecadeMetaCat|main
|Мир <десятилетие>-х годов по странам
|Мир <век> века <страны>!<ключ>
|Мир по десятилетиям <в стране>!<ключ>
|Мир <десятилетие>-х годов <в части света>!<страна>
}}

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

Переменные

  • <десятилетие> — десятилетие числом (без окончания -е/-х)
  • <век> — век римскими цифрами без слова «век»
  • <ключ> — ключ сортировки, н. э. — год, до н. э. — отрицательное число начиная с −10000 (-10000 == 0-е годы до н. э. −9990 == 10-е годы до н. э. и т. д.); нужен для корректной сортировки в категориях
  • <страна><страны>, <в стране> — страна в именительном, родительном или предложном падеже; для последнего автоматически выбирается и ставится предлог «в/во/на».
  • <часть света><части света>, <в части света> — часть света в именительном, родительном или предложном падеже; для последнего автоматически ставится предлог «в».

Следующие символы, указанные перед названием категории, осуществляют механизм проверки на существование категорий:

  • ? — категория публикуется только если она существует.
  • ~ — является заменой для несуществующей категории ?. Обязательно должна следовать сразу за ней на следующей строке, иначе игнорируется. Замены публикуются без проверок на существование. Для одной проверяемой категории может указываться несколько замен подряд.

Для отдельных стран, расположенных на двух частях света, и при условии наличия названия категории с переменной части света, выполняется механизм раздваивания на две категории. Если переменная указана лишь в качестве ключа сортировки, то категория публикуется только один раз (с первой найденной частью света в качестве ключа сортировки). Проверка на существование категорий с такими частями света также осуществляется для каждой из них отдельно. Если одна из категорий не существует, то будет опубликована замена для соответствующей части света.

Для указания государств, в которые входит или входила страна в определённых промежуток времени, используйте шаблон {{в государстве по времени}} с параметром decade.

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

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

Дополнительные параметры:

|title = заголовок страницы, используемый вместо текущего (для тестов)
|noindex = 1 (указывается, если необходимо отключить добавления шаблона индекса)
|nonav = 1 (указывается, если необходимо отключить добавления навигационной линейки)

Дополнительные функции

Функция 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 error_list = {}
local decade_range_error = nil	-- Переменная для хранения ошибки диапазонов
local country_error_flag = false

------------------ Ошибки ------------------
-- Сбор и обработка ошибок
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
		table.insert(error_list, {message = '<span class="error">' .. error_specific[error_code] .. '</span>'})
	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, '<ключ>', dec - 10000)  -- Преобразование ключа для до н.э.
		s = gsub(s, '<век> (век[еа]?)', c..' %1 до н. э.')
	else
		s = gsub(s, '<десятилетие>', dec)
		s = gsub(s, '<ключ>', dec)
		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  -- Возврат true, если найден хотя бы одно
		end
	end
	return false
end

-- Замена плейсхолдеров на страны и части света
local function process_country_placeholders(s, title)
	if type(s) ~= 'string' then return {}, nil end
	local lines = mw.text.split(s, '\n')
	local result_lines = {}
	local added_categories = {}
	local c = require('Module:CountryMetaCat')
	local function add_category(category, typ)
		-- Проверка уникальности категории перед добавлением
		if category ~= '' and not added_categories[category] then
			table.insert(result_lines, {text = category, type = typ})
			added_categories[category] = true
		end
	end
	for _, line in ipairs(lines) do
		if has_country_placeholders(line) then
			local args = {line, title = title}
			local country_result = c.resolve_country(args)
			if country_result then
				local main_category = country_result.result or ''  -- основная категория или 1 часть света
				local extra_category = country_result.extra_result or ''  -- 2 часть света
				local error_code = country_result.error or 0
				-- Добавление основной категории
				add_category(main_category, "main")
				-- Удаление ключей сортировки у категорий для сравнения названий
				local main_base_category = mw.ustring.match(main_category, "^(.-)!") or main_category
				local extra_base_category = mw.ustring.match(extra_category, "^(.-)!") or extra_category
				-- Добавление extra категории если отличаются
				if extra_base_category ~= main_base_category then
					add_category(extra_category, "extra")
				end
				-- Ошибка, если страна или часть света не найдены
				if error_code > 0 and not country_error_flag then
					add_error(error_code == 1 and 6 or 7)
					country_error_flag = true
				end
			elseif not country_error_flag then
				add_error(6)
				country_error_flag = true
			end
		else
			-- Если строка не содержит плейсхолдеров, добавляем её как основную
			table.insert(result_lines, {text = line, type = "main"})
		end
	end
	return result_lines, nil
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 = {}
	-- Считывание строк в массив
	for i, arg in sparseIpairs(args) do
		if type(arg) == "string" and arg ~= "" then
			local result = process_country_placeholders(arg, title)
			for _, line in ipairs(result) do
				table.insert(lines, {text = do_expand(line.text), type = line.type})
			end
		end
	end
	local function process_single_category(category_string)
		category_string = mw.ustring.gsub(category_string, "!", "|")
		-- Разделение строки на основную категорию и ключ сортировки
		local categories = mw.text.split(category_string, "|")
		local base_category = categories[1]
		local sort_key = categories[2] or ""  -- Ключ сортировки (если есть)
		if not added_categories[base_category] then
			if sort_key ~= "" then
				ret = ret .. string.format('[[Категория:%s|%s]]', base_category, sort_key)
			else
				ret = ret .. string.format('[[Категория:%s]]', base_category)
			end
			added_categories[base_category] = true
		end
	end
	local i = 1
	while i <= #lines do
		local arg = lines[i]
		local first_char = mw.ustring.sub(arg.text, 1, 1)
		local rest_string = mw.ustring.sub(arg.text, 2):gsub("^%s+", "")  -- Удаление пробелов после символов
		local typ = arg.type  -- Получаем тип строки ("main" или "extra")
		if first_char == '?' then
			local category_name = mw.ustring.match(rest_string, "^(.-)!") or rest_string
			local sort_key = mw.ustring.match(rest_string, "!(.-)$") or ''
			category_name = mw.ustring.match(category_name, "^[?]*(.*)") or category_name
			-- Ищем парную строку с другим типом
			local next_line = i + 1 <= #lines and lines[i + 1] or nil
			local is_paired = next_line and next_line.type ~= typ and 
							 mw.ustring.sub(next_line.text, 1, 1) == '?'
			-- Проверяем существование категории
			if category_exists(category_name) then
				process_single_category(category_name .. (sort_key ~= '' and '|' .. sort_key or ''))
			else
				-- Ищем все замены с "~" для текущего типа
				local j = i + 1
				while j <= #lines do
					local next_arg = lines[j]
					-- Прерываем поиск, если достигли конца или встретили не "~"
					if not next_arg or mw.ustring.sub(next_arg.text, 1, 1) ~= '~' then
						break
					end
					-- Проверяем тип только если это не парное раздвоение
					if not is_paired or (is_paired and next_arg.type == typ) then
						local replacement_rest_string = mw.ustring.sub(next_arg.text, 2):gsub("^%s+", "")
						process_single_category(replacement_rest_string)
					end
					j = j + 1
				end
			end
			-- Если есть парная строка, обрабатываем её следом
			if is_paired then
				i = i + 1
				-- Обработка парной строки происходит на следующей итерации
			else
				i = i + 1
			end
		else
			-- Обработка обычных строк (не начинающихся с ? или ~)
			if first_char ~= '~' then
				process_single_category(arg.text)
			end
			i = i + 1
		end
	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