Модуль:YearMetaCat2

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

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

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

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

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

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

Вызов ключа части света обрабатывает помещение в соответствующую часть света, а также в некоторые надрегионы. Для отдельных стран работает механизм разделения на несколько частей света, когда страна, расположенная одновременно в двух частях света, будет автоматически получать две категории каждой части света. В случае, если один ключей <часть света> используется только в качестве ключа сортировки, а название категории при этом не меняется, то категория публикуется только один раз с первым ключом сортировки.

Механизм проверки существования категорий

  • ? — указывается первым символом перед названием категории, которую необходимо проверять на существование. Если категория существует, она публикуется. Если не существует, ищется замена для публикации на следующей строке, которая должна начинаться с символа ~. В случае неудачи категория не публикуется.
  • ~ — указывается первым символом перед названием категории, которая публикуется (без проверки на существование) только в том случае, если на предыдущей строке категория с ? не создана. Во всех остальных случаях пропускается.

Для проверок работает механизм раздваивания по частям света. Будет проверяться на существование категория с каждой частью света и подбираться замена для соответствующей (замена также должна содержать ключ <части света>).

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

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

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

Примеры:

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

Дополнительные параметры для тестов

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

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

Функция expand

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

Страны и части света функция не обрабатывает.

Пример:

{{#invoke:YearMetaCat2|expand|Мир <год> года}}

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

Мир 100 года до н. э.

Категории отслеживания

См. также

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

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

-- Опции
local year_min = -40000	-- 0 == только н.э.
local year_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 year_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 year_range_error then
			year_range_error = {message = 'Ошибка: год не попадает в заданный диапазон.', details = {}}
			table.insert(error_list, year_range_error)
		end
		table.insert(year_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 = '[[Категория:Википедия:Страницы с некорректным использованием модуля YearMetaCat2]]'
	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_year(t)
	local years = {}
	for year in mw.ustring.gmatch(t, '([0-9]+)%s*год') do
		table.insert(years, tonumber(year))  -- Преобразование строки в число
	end
	if #years == 0 then
		add_error(1)  -- Ошибка "не найден"
		return nil
	elseif #years > 1 then
		add_error(8)  -- Ошибка "обнаружено два"
		return nil
	end
	return years[1]  -- Возврат единственного найденнего значения
end

-- Замена плейсхолдеров (год, десятилетие, век, ключ) на реальные значения
local function do_expand(s)
	-- <год> - год без слова "год"
	-- <ключ> - ключ сортировки, н.э. - номер года,
	-- до н.э. - отрицательное число начиная с -99 (-99 == 1 год до н.э. -98 == 2 год до н.э. и т.д.)
	-- <десятилетие> - десятилетие числом (без окончания -е/-х)
	-- <век> - век римскими цифрами
	local d = math.floor(year/10)*10  -- Определение десятилетия
	local c = toroman(math.floor((year-1)/100)+1)  -- Преобразование века в римские цифры
	-- Обработка для II века (в/во)
	if c == 'II' then
		s = gsub(s, ' в <век>', ' во <век>')
	end
	-- Обработка для 2 года (в/во)
	if year == 2 then
		s = gsub(s, 'в <год> году', 'во <год> году')
	end
	if BC == 1 then
		s = gsub(s, '<год> (год[ау]?)', year..' %1 до н. э.')
		s = gsub(s, '<ключ>', year - 10000)  -- Преобразование ключа для до н.э.
		s = gsub(s, '<десятилетие>(-[ех] год[ыоа][вх]?)', d..'%1 до н. э.')  -- годы/годов/годах
		s = gsub(s, '<век> (век[еа]?)', c..' %1 до н. э.')
	else
		s = gsub(s, '<год>', year)
		s = gsub(s, '<ключ>', year)
		s = gsub(s, '<десятилетие>', d)
		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_year_in_bounds(args)
	args = args or {}
	local country_name = findCountry.findcountryinstring(title)
	local country_data = find_country_in_json(country_name)
	-- Корректировка для до н.э.
	local year_adjusted = (BC == 1) and -year or year
	-- Ручные ограничения min и max
	local manual_min = tonumber(args['min'])
	local manual_max = tonumber(args['max'])
	-- Определение активных границ
	local effective_min = manual_min or (country_data and country_data.min)
	local effective_max = manual_max or (country_data and country_data.max)
	-- Проверка минимального значения
	if effective_min and year_adjusted < effective_min then
		if manual_min then
			-- Если задано вручную
			add_error(2, tostring(manual_min))
		elseif country_data then
			-- Если данные из страны
			add_error(4, string.format("%s: %d", country_name, country_data.min))
		end
	end
	-- Проверка максимального значения
	if effective_max and year_adjusted > effective_max then
		if manual_max then
			-- Если задано вручную
			add_error(3, tostring(manual_max))
		elseif country_data then
			-- Если данные из страны
			add_error(5, string.format("%s: %d", country_name, 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)
	-- Формируем строку-шаблон вида: 'Мир в 99 году до н. э.' -> 'Мир в %s году%s'
	local t
	t, BC = gsub(s, '[0-9]+ (год[ау]?) до н%. э%.', '%%s %1%%s')
	local n = BC
	if BC ~= 1 then
		t, n = gsub(s, '[0-9]+ (год[ау]?)', '%%s %1%%s')
	end
	if n ~= 1 then
		add_error(1)  -- Ошибка, если совпадений нет или их больше одного
	end
	-- Корректировка для "во втором году"
	templ = gsub(t, 'во %%s году', 'в %%s году')
end

-- Форматирование года с учётом до н. э.
local function format(y, wiki)
	local bcs, t
	if y < 1 then
		y = 1 - y
		bcs = ' до н. э.'
		t = '−'..y
	else
		bcs = ''
		t = y
	end
	local s
	if wiki then
		local tt = templ
		-- Корректировка для "во втором году"
		if y == 2 then
			tt = gsub(templ, 'в %%s году', 'во %%s году')
		end
		s = string.format(tt, y, 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
			if category_exists(category_name) then
				-- Категория существует, публикуем её
				process_single_category(category_name .. (sort_key ~= '' and '|' .. sort_key or ''))
			else
				-- Поиск строки с "~" с тем-же типом для замены, если категория не найдена
				local found_replacement = false
				for j = i + 1, #lines do
					local next_arg = lines[j]
					local next_first_char = mw.ustring.sub(next_arg.text, 1, 1)
					local next_typ = next_arg.type
					if next_first_char == '~' and next_typ == typ then
						local replacement_rest_string = mw.ustring.sub(next_arg.text, 2):gsub("^%s+", "")
						process_single_category(replacement_rest_string)
						found_replacement = true
						break  -- Выход из цикла после нахождения замены
					end
				end
				-- Если замена не найдена, ничего не делаем
			end
			i = i + 1
		else
			-- Обрабатываем остальные строки (не начинающиеся с "?")
			if first_char ~= '~' then
				process_single_category(arg.text)
			end
			i = i + 1
		end
	end
	return ret
end

------------------ Навигационный блок ------------------
local function navbox()
	-- Корректировка для до н. э.
	local y = (BC == 1) and 1 - year or year
	local wt = mw.html.create('div'):addClass('ts-module-Индекс_категории hlist')
	local row = wt:tag('ul')
	-- Корректировка min и max для до н. э.
	local adjusted_min = year_min <= 0 and year_min + 1 or year_min
	local adjusted_max = year_max <= 0 and year_max + 1 or year_max
	local country_data = find_country_in_json(findCountry.findcountryinstring(title))
	-- Определение минимального и максимального года для страны
	local country_min_year = math.max(adjusted_min, country_data and country_data.min or adjusted_min)
	local country_max_year = math.min(adjusted_max, country_data and country_data.max or adjusted_max)
	-- Определение стартового и конечного года
	local ystart = math.max(country_min_year, y - range)
	local yend = math.min(country_max_year, y + range)
	-- Если диапазон некорректный, возвращаем пустую строку
	if yend < ystart then return "" end
	-- Добавляем элементы в навигационную полоску
	for i = ystart, yend 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	"[[Категория:Шаблоны, использующие модуль YearMetaCat2]]" ..
				"[[Категория:Шаблоны, использующие индекс категории (автоматический)]]"
	end

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

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

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

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

	-- Проверка, попадает ли год в допустимые границы
	check_year_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
	year = get_year(title)
	if not year then
		return publish_errors()
	end
	BC = mw.ustring.find(title, '[0-9]+ год[ау]? до н%. э%.')
	if BC then
		BC = 1
	else
		BC = 0
	end
	-- в/во
	local tt = args[1]
	if year == 2 then
		tt = mw.ustring.gsub(args[1], 'в <год> году', 'во <год> году')
	end
	return do_expand(tt)
end
return p