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

Модуль:CountryMetaCat

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

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

  • Определяет век и эру (до н.э / н.э.).
  • Создаёт навигационную линейку по векам.
  • Корректно работает с веками до нашей эры.
  • Добавляет категории.

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

{{#invoke:CenturyMetaCat|main
|Мир в <век> веке
|Мир <тысячелетие>-го тысячелетия! <ключ>
|Мир по векам! <ключ>
}}
  • <век> — век римскими цифрами без слова «век»
  • <тысячелетие> — тысячелетие числом (без окончания -е/-м/-го)
  • <ключ> — ключ сортировки, н. э. — номер века числом, до н. э. — отрицательное число начиная с -99 (-99 == I век до н. э. -98 == II век до н. э. и т. д.); нужен для корректной сортировки в категориях

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

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

Категории

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

  • первое — название категории
  • второе — ключ сортировки (необязательно)
  • третье — начиная с какого века добавлять категорию (необязательно)
  • четвертое — каким веком заканчивать добавление категории (необязательно)

Примеры:

  • |Мир по векам! <ключ> — добавлять категорию «Мир по векам» с ключом сортировки <пробел><ключ>
  • |Графы Средних веков!<ключ>!5!15 — добавлять категорию «Графы Средних веков» в категории с V по XV века
  • |Книги в общественном достоянии!<ключ>!!19 — добавлять категорию «Книги в общественном достоянии» во все категории до XIX века включительно
Шаблон:NB

Есть эмпирическое правило: в категории обязательно должен быть или <век> (в названии), или <ключ> (в ключе сортировки), но не оба.

Другие опции

|min = до какого века рисовать линейку слева, по умолчанию -39 (0 — рисовать только века нашей эры)
|max = до какого века рисовать линейку справа, по умолчанию 21
|range = сколько веков в линейке слева и справа, по умолчанию 5
|title = заголовок страницы, используемый вместо текущего (для тестов)

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

expand

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

Пример:

{{#invoke:CenturyMetaCat|expand|Мир в <век> веке}}

на странице «К:Земля I века до н. э.» вернёт:

Мир в I веке до н. э.

century_from_title

Возвращает век из заголовка числом, для веков до н.э. с минусом.

См. также


local p = {}
local getArgs = require('Модуль:Arguments').getArgs
local findCountry = require('Модуль:Find country')
local error_category = '[[Категория:Википедия:Страницы с некорректным использованием модуля CountryMetaCat]]'

local errors = {}

--------------- Отслеживание ошибок ---------------
-- Добавляет сообщение об ошибке в список с указанием кода ошибки
local function add_error(error_code, additional_info)
	local error_specific = {
		[1] = 'Ошибка: страна не найдена.',
		[2] = 'Ошибка: часть света не найдена для страны ' .. (additional_info or '') .. '.'
	}

	local error_text = error_specific[error_code]
	if error_text then
		table.insert(errors, '<span class="error">' .. error_text .. '</span>')
	end
end

-- Возвращает строку с ошибками и очищает список ошибок
local function get_errors()
	local result = table.concat(errors)
	if result ~= "" then
		result = result .. error_category
	end
	errors = {} -- Очищаем ошибки после получения
	return result
end

---------------- Обработка государств и частей света ----------------
-- Проверяет наличие указанных плейсхолдеров в аргументах
local function has_placeholders(args, placeholders)
	for _, value in pairs(args) do
		if type(value) == "string" then
			for _, placeholder in ipairs(placeholders) do
				if mw.ustring.find(value, placeholder, 1, true) then
					return true
				end
			end
		end
	end
	return false
end

-- Проверяет наличие плейсхолдеров для частей света
local function has_continent_placeholders(args)
	return has_placeholders(args, {'<часть света>', '<части света>', '<в части света>'})
end

-- Проверяет наличие плейсхолдеров для государств
local function has_state_placeholders(args)
	for _, value in pairs(args) do
		if type(value) == "string" then
			if	mw.ustring.find(value, '<государство[^>]*>') or
				mw.ustring.find(value, '<государства[^>]*>') or
				mw.ustring.find(value, '<в государстве[^>]*>') then
				return true
			end
		end
	end
	return false
end

-- Загрузка данных о государствах из JSON
local function load_states_data()
	return mw.loadJsonData('Модуль:CountryMetaCat/state-data.json')
end

-- Проверяет, попадает ли заданный год в указанный период (век, десятилетие, год)
local function check_time_period(start_year, end_year, type, time)
	local year = tonumber(time)
	if not year then return false end

	if type == "century" then
		local century_start = (year - 1) * 100 + 1
		local century_end = year * 100
		return	(not end_year and start_year <= century_end) or
				(start_year <= century_end and end_year and end_year >= century_start)
	elseif type == "decade" then
		local decade_start = math.floor(year / 10) * 10
		local decade_end = decade_start + 9
		return	(not end_year and start_year <= decade_end) or
				(start_year <= decade_end and end_year and end_year >= decade_start)
	else -- year
		return	(not end_year and start_year <= year) or
				(start_year <= year and end_year and end_year >= year)
	end
end

-- Безопасно возвращает название страны в нужном падеже
local function get_safe_case(country, case)
	if not country then return "" end
	local result = findCountry.findcountryinstring(country, case)
	if not result or result:match("^Ошибка") then result = "" end
	return result
end

-- Находит государства по временному периоду, и сортирует их
local function find_states_by_time(country, type, time)
	local states = {}
	local states_data = load_states_data()
	local country_data = states_data.country[country]

	if not country_data then return states end

	-- Создаем массив с годами для сортировки
	local states_with_years = {}
	for state_name, years in pairs(country_data) do
		local start_year = tonumber(years[1])
		local end_year = years[2] and tonumber(years[2]) or nil

		if check_time_period(start_year, end_year, type, time) then
			table.insert(states_with_years, {
				name = state_name,
				start_year = start_year,
				end_year = end_year,
				cases = {
					['именительный'] = get_safe_case(state_name, 'именительный'),
					['родительный'] = get_safe_case(state_name, 'родительный'),
					['предлог'] = get_safe_case(state_name, 'предлог')
				}
			})
		end
	end

	-- Сортируем по начальному году (по возрастанию)
	table.sort(states_with_years, function(a, b)
		return a.start_year < b.start_year
	end)

	-- При запросе со временем возвращаем все подходящие государства для дальнейшей обработки
	return states_with_years
end

-- Проверяет соответствие названия государства из плейсхолдера с указанным государством
local function check_state_match(state, placeholder)
	if not state then return false end

	local required_state_name = mw.ustring.match(placeholder, ":([^>]+)>")
	if not required_state_name then return true end -- если нет конкретного требования, разрешаем любое государство

	-- Нормализуем названия для корректного сравнения
	required_state_name = mw.ustring.gsub(required_state_name, "%s+", " ")
	local state_name = mw.ustring.gsub(state.name or "", "%s+", " ")

	-- Проверяем на исключения (формат ^Страна1^Страна2^...)
	if mw.ustring.match(required_state_name, "^%^") then
		-- Разбиваем строку исключений на отдельные страны
		local excluded_states = {}
		for excluded_state in mw.ustring.gmatch(required_state_name, "%^([^%^]+)") do
			excluded_states[mw.ustring.gsub(excluded_state, "%s+", " ")] = true
		end
		-- Возвращаем true, если государство НЕ в списке исключений
		return not excluded_states[state_name]
	else
		-- Обычное сравнение для включения конкретной страны
		return state_name == required_state_name
	end
end

-- Заменяет плейсхолдеры в тексте на значения из таблицы
local function replace_placeholders(text, replacements)
	for placeholder, value in pairs(replacements) do
		text = mw.ustring.gsub(text, placeholder, value)
	end
	return text
end

-- Удаляет неиспользованные плейсхолдеры из текста
local function remove_unused_placeholders(text, placeholders)
	for _, placeholder in ipairs(placeholders) do
		text = mw.ustring.gsub(text, placeholder, '')
	end
	return text
end

-- Вспомогательная функция для замены плейсхолдера государства в нужном падеже
local function replace_state_placeholder(result, pattern, case, state, is_question)
	return result:gsub(pattern, function(state_name)
		if check_state_match(state, pattern:gsub(":([^>]+)>", ":" .. state_name .. ">")) then
			return state.cases[case] or ""
		end
		return is_question and (pattern:gsub("([^>]+)>", state_name .. ">")) or ""
	end)
end

--------------- Обработка всех плейсхолдеров ---------------
-- Основная обработка всех плейсхолдеров с заменой
local function process_placeholders(s, country, continent, state, is_question)
	if not s then return "" end

	local result = s
	local has_exclamation_space = mw.ustring.find(s, "! ")

	-- Проверка на пустой возврат для не "?"
	if not is_question then
		if not state and result:match('<[^>]*государств[^>]*>') then
			return ""
		end
		if state and result:match('<[^>]+:[^>]+>') then
			local found_match = false
			for pattern in result:gmatch('<[^>]+:[^>]+>') do
				if check_state_match(state, pattern) then
					found_match = true
					break
				end
			end
			if not found_match then return "" end
		end
	end

	-- Обработка плейсхолдеров государства с использованием вспомогательной функции
	if state then
		result = replace_placeholders(result, {
			['<государство>'] = state.cases['именительный'],
			['<государства>'] = state.cases['родительный'],
			['<в государстве>'] = state.cases['предлог']
		})

		result = replace_state_placeholder(result, '<государство:([^>]+)>', 'именительный', state, is_question)
		result = replace_state_placeholder(result, '<государства:([^>]+)>', 'родительный', state, is_question)
		result = replace_state_placeholder(result, '<в государстве:([^>]+)>', 'предлог', state, is_question)
	end

	-- Обработка плейсхолдеров частей света (без изменений)
	if result:match('<[^>]*част[^>]*света[^>]*>') then
		if continent and continent.cases then
			result = replace_placeholders(result, {
				['<часть света>'] = continent.cases['именительный'] or '',
				['<части света>'] = continent.cases['родительный'] or '',
				['<в части света>'] = continent.cases['предложный'] and ('в ' .. continent.cases['предложный']) or ''
			})
		else
			add_error(2, country)
			result = remove_unused_placeholders(result, {
				'<часть света>', '<части света>', '<в части света>'
			})
		end
	end

	-- Обработка плейсхолдеров страны (без изменений)
	if country then
		result = replace_placeholders(result, {
			['<страна>'] = get_safe_case(country, 'именительный') or "",
			['<страны>'] = get_safe_case(country, 'родительный') or "",
			['<в стране>'] = get_safe_case(country, 'предлог') or ""
		})
	elseif not is_question then
		result = remove_unused_placeholders(result, {
			'<страна>', '<страны>', '<в стране>'
		})
	end

	-- Финальная обработка (без изменений)
	result = mw.ustring.gsub(result, " !", "!")
	result = mw.ustring.gsub(result, "[%!%s]+$", "")
	if has_exclamation_space and not mw.ustring.find(result, "!") then
		result = result .. "! "
	end

	return result
end

--------------- Обработка и публикация категорий ---------------
-- Генерирует комбинации частей света и государств
local function combinations_categories(country, args)
	local combinations = {}
	local continents = {}
	local states = {}

	-- Загрузка данных о частях света
	if has_continent_placeholders(args) then
		local continents_data = mw.loadJsonData('Модуль:CountryMetaCat/country-continents.json')
		for _, continent in ipairs(continents_data.continents) do
			if continent.countries then
				for _, c in ipairs(continent.countries) do
					if c == country then
						table.insert(continents, continent)
						break
					end
				end
			end
		end
	end

	-- Получение государств
	if has_state_placeholders(args) then
		states = find_states_by_time(country, args.type or "year", args.time or os.date("%Y"))
	end

	-- Вспомогательная функция для добавления комбинаций
	local function add_combinations(continents_list, states_list)
		if #continents_list == 0 and #states_list == 0 then
			table.insert(combinations, { continent = nil, state = nil })
		else
			for _, continent in ipairs(continents_list) do
				for _, state in ipairs(states_list) do
					table.insert(combinations, { continent = continent, state = state })
				end
			end
			-- Добавляем оставшиеся комбинации, если один из списков пуст
			if #continents_list > 0 and #states_list == 0 then
				for _, continent in ipairs(continents_list) do
					table.insert(combinations, { continent = continent, state = nil })
				end
			elseif #continents_list == 0 and #states_list > 0 then
				for _, state in ipairs(states_list) do
					table.insert(combinations, { continent = nil, state = state })
				end
			end
		end
	end

	-- Добавляем комбинации частей света и государств
	add_combinations(continents, states)

	return combinations
end

-- Проверка существования категории
local function category_exists(category_name)
	if not category_name or category_name == '' then return false end
	local clean_category_name = mw.ustring.match(category_name, "^(.-)!")
	clean_category_name = clean_category_name or category_name
	local title = mw.title.new('Категория:' .. clean_category_name)
	return title and title.exists
end

-- Обрабатывает и добавляет уникальные категории на основе комбинаций частей света и государств
local function process_and_add_categories(text, country, combinations, results, added_categories, check_exists)
	for _, combination in ipairs(combinations) do
		if not combination.state or check_state_match(combination.state, text) then
			local processed = process_placeholders(text, country, combination.continent, combination.state, check_exists)
			if processed ~= "" then
				local category_name = mw.ustring.match(processed, "^(.-)!") or processed
				if not added_categories[category_name] then
					table.insert(results, string.format('[[Категория:%s]]',
						mw.ustring.gsub(processed, "!", "|")))
					added_categories[category_name] = true
				end
			end
		end
	end
end

-- Создание категорий на основе аргументов и комбинаций
local function create_categories(args, country, combinations)
	local results = {}
	local added_categories = {}

	if #combinations == 0 and has_continent_placeholders(args) then
		add_error(2, country)
	end

	local ordered_args = {}
	for i, arg in pairs(args) do
		if type(arg) == "string" and arg ~= "" and type(i) == "number" then
			table.insert(ordered_args, {index = i, value = arg})
		end
	end
	table.sort(ordered_args, function(a, b) return a.index < b.index end)

	local i = 1
	while i <= #ordered_args do
		local arg_data = ordered_args[i]
		local arg = arg_data.value

		if type(arg) == "string" and arg ~= "" then
			local first_char = mw.ustring.sub(arg, 1, 1)
			local rest_string = mw.ustring.sub(arg, 2):gsub("^%s+", "")

			if first_char == '?' then
				local replacements = {}
				local j = i + 1
				while j <= #ordered_args and mw.ustring.sub(ordered_args[j].value, 1, 1) == '~' do
					local replacement_text = mw.ustring.sub(ordered_args[j].value, 2):gsub("^%s+", "")
					table.insert(replacements, replacement_text)
					j = j + 1
				end
				i = j - 1

				-- Обрабатываем каждую комбинацию отдельно
				for _, combination in ipairs(combinations) do
					local processed = process_placeholders(rest_string, country, combination.continent, combination.state, true)
					if processed ~= "" then
						local category_name = mw.ustring.match(processed, "^(.-)!") or processed

						if category_exists(category_name) then
							-- Если категория существует, добавляем её
							if not added_categories[category_name] then
								table.insert(results, string.format('[[Категория:%s]]',
									mw.ustring.gsub(processed, "!", "|")))
								added_categories[category_name] = true
							end
						else
							-- Если категория не существует, обрабатываем замены для этой комбинации
							for _, replacement in ipairs(replacements) do
								local replacement_processed = process_placeholders(replacement, country,
									combination.continent, combination.state, false)
								if replacement_processed ~= "" then
									local replacement_category = mw.ustring.match(replacement_processed, "^(.-)!") or replacement_processed
									if not added_categories[replacement_category] then
										table.insert(results, string.format('[[Категория:%s]]',
											mw.ustring.gsub(replacement_processed, "!", "|")))
										added_categories[replacement_category] = true
									end
								end
							end
						end
					end
				end
			elseif first_char ~= '~' then
				-- Обработка обычных категорий без проверки существования
				process_and_add_categories(arg, country, combinations, results, added_categories, false)
			end
		end
		i = i + 1
	end

	return table.concat(results)
end

-- Основная функция модуля
function p.main(frame)
	local args = getArgs(frame)
	local title = args.title or mw.title.getCurrentTitle().text

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

	local country = findCountry.findcountryinstring(title, 'именительный')
	if not country or country == "" then
		add_error(1)
		return get_errors()
	end

	local combinations = combinations_categories(country, args)

	local result = create_categories(args, country, combinations)

	if args.noindex ~= "1" then
		result = mw.getCurrentFrame():preprocess('{{индекс категории (автоматический)}}') .. result
	end

	return result .. get_errors()
end

-- Функция для внешней обработки стран
function p._resolve_country(args)
	local title = args.title or mw.title.getCurrentTitle().text
	local country = findCountry.findcountryinstring(title, 'именительный')
	local text = args[1] or ""
	local is_question = text:match("^%?")
	local has_state = text:match('<[^>]*государств[^>]*>')
	local has_specific_state = text:match('<[^>]+:[^>]+>')
	
	local result = {
		result = "",
		extra_result = nil,
		error = country and country ~= "" and 0 or 1
	}
	
	-- Получаем комбинации, если страна найдена
	local combinations = country and country ~= "" and combinations_categories(country, args) or {}
	
	-- Ранний выход, если это ? с названием государства, но комбинаций нет
	if is_question and has_state and #combinations == 0 then
		result.result = process_placeholders(text, country, nil, nil, true)
		return result
	end
	
	-- Проверка на ошибку части света
	if #combinations > 0 and has_continent_placeholders(args) and not combinations[1].continent then
		result.error = 2
	end
	
	-- Сбор допустимых результатов
	local valid_results = {}
	for _, combo in ipairs(combinations) do
		local should_process = is_question and
			(not has_specific_state or (combo.state and check_state_match(combo.state, text))) or
			(not has_state or (combo.state and check_state_match(combo.state, text)))
			
		if should_process then
			local processed = process_placeholders(text, country, combo.continent, combo.state, is_question)
			if processed ~= "" then
				table.insert(valid_results, processed)
				if #valid_results >= 2 then break end -- We only need max 2 results
			end
		end
	end
	
	-- Результаты
	if #valid_results > 0 then
		result.result = valid_results[1]
		result.extra_result = valid_results[2]
	elseif is_question and has_state then
		result.result = process_placeholders(text, country, 
			combinations[1] and combinations[1].continent, nil, true)
	else
		result.result = process_placeholders(text, country, nil, nil, is_question)
	end
	
	return result
end

function p.resolve_country(frame)
	local args = getArgs(frame)
	return p._resolve_country(args)
end

return {
	main = p.main,
	resolve_country = p.resolve_country
}