Модуль:Список старейших людей в мире

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

Документацию смотри на странице шаблона {{Список старейших людей в мире}}

require("strict")
local getArgs = require('Модуль:Arguments').getArgs

local displayMax = 500 -- Показать максимальное количество записей
local language = mw.getContentLanguage()
local root = ''
local legend = ''
local legendSpan = ''
local reference = ''

--------------------------------------------------------------------------------
-- Локальные вспомогательные функции
--------------------------------------------------------------------------------

-- Возвращает true, если год является или был високосным
local function isLeapYear(year)
	return year % 4 == 0 and (year % 100 ~= 0 or year % 400 == 0)
end

local function gsd(year, month, day)
	-- Возвращает день по григорианскому календарю (целое число >= 1) для заданной даты,
	-- или возвращает nil, если дата недействительна (проверяется только этот год >= 1).
	-- Это количество дней с 1 года нашей эры (нулевого года нет).
	if year < 1 then
		return nil
	end
	local floor = math.floor
	local days_this_year = (month - 1) * 30.5 + day
	if month > 2 then
		if isLeapYear(year) then
			days_this_year = days_this_year - 1
		else
			days_this_year = days_this_year - 2
		end
		if month > 8 then
			days_this_year = days_this_year + 0.9
		end
	end
	days_this_year = floor(days_this_year + 0.5)
	year = year - 1
	local days_from_past_years = year * 365
		+ floor(year / 4)
		- floor(year / 100)
		+ floor(year / 400)
	return days_from_past_years + days_this_year
end

-- Возвращает величину числа, определяемого как целая часть log10 абсолютного значения числа.
local function magnitude(number)
	if number == 0 then return nil end
	return math.floor(math.log10(math.abs(number)))
end

-- Возвращает ключ сортировки для числа
local function sortableNumber(number)
	local result = ''
	if number > 0 then
		result = result .. '3&' .. tostring(500 + magnitude(number))
	elseif number < 0 then
		result = result .. '1&' .. tostring(500 - magnitude(number))
	else -- 0
		result = result .. '2'
	end
	if number >= 0 then
		result = result .. '&' .. tostring(number)
	else -- negative
		result = result .. '&' .. tostring(100000 * (10 + number / 10 ^ magnitude(number)))
	end
	return '<span data-sort-value="' .. result .. '&">' .. tostring(number) .. '</span>';
end

--------------------------------------------------------------------------------
-- Функции для расчета возраста
--------------------------------------------------------------------------------

-- Возвращает возраст в годах и днях. Входные данные — это две даты в числовой форме, возвращенные из os.time.
local function ageInYearsAndDays(date2N, date1N)
	local date1 = os.date('!*t', date1N)
	local date2 = os.date('!*t', date2N)
	local age = {}

	age.years = date2.year - date1.year;
	if date2.month < date1.month or date2.month == date1.month and date2.day < date1.day then
		age.years = age.years - 1
	end

	local year = date2.year
	if date2.month < date1.month or date2.month == date1.month and date2.day < date1.day then
		year = year - 1
	end
	age.days = gsd(date2.year, date2.month, date2.day) - gsd(year, date1.month, date1.day)

	return age
end

--------------------------------------------------------------------------------
-- Функции для сравнения возрастов
--------------------------------------------------------------------------------

-- Возвращает < 0 если ageA < ageB, 0 если ageA == ageB, > 0 если ageA > ageB
local function compareAges(ageA, ageB)
	if ageA.years ~= ageB.years then
		if ageA.years < ageB.years then return -1 end
		return 1
	end
	if ageA.days ~= ageB.days then
		if ageA.days < ageB.days then return -1 end
		return 1
	end
	return 0 -- equal
end

-- Возвращает true, если ageA == ageB
local function equalAges(ageA, ageB)
	return compareAges(ageA, ageB) == 0
end


--------------------------------------------------------------------------------
-- Функции для отображения возраста
--------------------------------------------------------------------------------

-- Возвращает дату в виде сортируемой строки с возрастом в годах и днях.
local function ageInYearsAndDaysFormat(age)
	local result = sortableNumber(age.years) .. ' ' .. mw.getContentLanguage():plural(age.years, 'год', 'года', 'лет')
	return result .. ' ' .. sortableNumber(age.days) .. ' ' .. mw.getContentLanguage():plural(age.days, 'день', 'дня', 'дней')
end

--------------------------------------------------------------------------------
-- ОСНОВНАЯ ЧАСТЬ
--------------------------------------------------------------------------------

-- Возвращает true, если personA старше, чем personB
local function comparePersons(personA, personB)
	local diffAge = compareAges(personA.age, personB.age)
	if diffAge == 0 then -- Ровесники, пусть "выиграет" тот, кто родился первым
		if personA.dateBirth ~= personB.dateBirth then return personA.dateBirth < personB.dateBirth end
	end
	return diffAge > 0
end

local function formatDateIso8061(year, month, day)
	return tostring(year) .. '-' .. string.format('%02d', month) .. '-' .. string.format('%02d', day)
end

local function formatDateSortable(dateN)
	local date = os.date('!*t', dateN)
	local dateIso = formatDateIso8061(date.year, date.month, date.day)
	local result = '<span style="display:none">' .. dateIso .. '</span>'
	result = result .. tostring(date.day) .. ' ' .. language:formatDate('xg', dateIso) .. ' ' .. tostring(date.year)
	return result
end

local p = {}
	
-- Ожидает таблицу персон в качестве входных данных.
-- Возвращает строку с отсортированным и отформатированным выводом (в виде сортируемой таблицы)
function p.displaySortedTable(persons, args)
	if #persons == 0 then return '' end

	table.sort(persons, comparePersons) -- Сортировка по возрасту и дате рождения
	
	local dateNow = os.date('!*t')
	local dateIsoNow = formatDateIso8061(dateNow.year, dateNow.month, dateNow.day)
	local root = p.createTable()
		p.captionTable()
	root:node('<tr>') --начало tr
		p.cellTh('№')
		p.cellTh('Имя')
	if args['Безпола'] == nil then
		p.cellTh('Пол') end
		p.cellTh('Дата рождения')
		p.cellTh('Дата смерти')
		p.cellTh('Достигнутый возраст<br><small>на ' .. language:formatDate('j xg Y', dateIsoNow) .. '</small>')
	if persons[1]['dateAssignment'] ~= nil then
		p.cellTh('Дата присвоения<br>статуса') end
	if persons[1]['placeOfBirth'] ~= nil or persons[1]['placeOfDeath'] ~= nil or persons[1]['placeOfResidence'] ~= nil then
		p.cellTh('Место рождения')
		p.cellTh('Место смерти<br>или<br>проживания') 
	else
		p.cellTh('Страна')
	end
	root:node('</tr>')  --конец tr
	
	local lastAge = nil
	local rank, keyLast = 0, 0
	local numberOfVerifiedGRG, numberOfVerifiedESO, numberOfVerifiedGWR = 0, 0, 0
	local numberOfContested, numberOfLiving = 0, 0
	local row, rankCell = nil, nil
	
	for key, person in ipairs(persons) do
		if lastAge == nil or not equalAges(lastAge, person.age) then -- Новый век: повышение ранга
			if rankCell ~= nil and key > rank then
				rankCell:attr('rowspan', tostring(key - rank))
			end
			rankCell = nil
			if key > displayMax then break end -- Показать максимальное количество записей
			rank = key
			lastAge = person.age
			row = root:tag('tr')
			rankCell = row:tag('th'):wikitext(tostring(rank))
		else
			row = root:tag('tr')
		end
		keyLast = key
		row:attr('vertical-align', 'top')

		local color = nil
		if not person.dateDeath then -- Not dead
			numberOfLiving = numberOfLiving + 1
			if args['Безцвета'] == nil then color = '#99FF99' end -- Цвет живых
		end
		if person.contested then
			numberOfContested = numberOfContested + 1
			color = '#e9b06b'
		end
		if person.verifiedGRG then
			numberOfVerifiedGRG = numberOfVerifiedGRG + 1
			color = '#f9f9f9'
		end
		if person.verifiedESO then
			numberOfVerifiedESO = numberOfVerifiedESO + 1
			color = '#ccff00'
		end
		if person.verifiedGWR then
			numberOfVerifiedGWR = numberOfVerifiedGWR + 1
			color = '#f0dc82'
		end
		row:tag('td'):wikitext(person.name)
		row:attr('bgcolor', color)
		local sexFix = { ['м']='муж.', ['ж']='жен.' }
		local sexCell = mw.ustring.sub(person.sex, 1, 1)
		local sexCell = sexFix[mw.ustring.lower(sexCell)] or ''
		if args['Безпола'] == nil then
			row:tag('td'):wikitext(sexCell)
		end
		row:tag('td'):wikitext(formatDateSortable(person.dateBirth))
		if person.dateDeath ~= nil then -- Если мертв или мертва
			row:tag('td'):wikitext(formatDateSortable(person.dateDeath))
		else
			-- Если жив или жива
			if string.match(sexCell, 'жен.')  == 'жен.' then
				row:tag('td'):wikitext('Жива')
			elseif string.match(sexCell, 'муж.') == 'муж.' then
				row:tag('td'):wikitext('Жив')
			else
				row:tag('td'):wikitext('Жив(а)')
			end
		end
		row:tag('td'):wikitext(ageInYearsAndDaysFormat(person.age))
		if person.dateAssignment ~= nil then
			row:tag('td'):wikitext(formatDateSortable(person.dateAssignment))
		end
		if person.placeOfBirth ~= nil or person.placeOfDeath ~= nil or person.placeOfResidence ~= nil then
			row:tag('td'):wikitext(person.placeOfBirth or '')
			if person.placeOfResidence ~= nil then
				row:tag('td'):wikitext(person.placeOfResidence or '')
			else
				row:tag('td'):wikitext(person.placeOfDeath or '')
			end
		else
			row:tag('td'):wikitext(person.nation or '')
		end
	end
	if rankCell ~= nil and keyLast > rank then
		rankCell:attr('rowspan', tostring(keyLast - rank + 1))
	end
	root:allDone()

	-- Легенда
	if numberOfVerifiedGRG > 0 then
		legend = legend .. p.legendTable('#f9f9f9', 'Верифицированы GRG:', numberOfVerifiedGRG)
	end
	if numberOfVerifiedESO > 0 then
		legend = legend .. p.legendTable('#ccff00', 'Верифицированы только ESO:', numberOfVerifiedESO)
	end
	if numberOfVerifiedGWR > 0 then
		legend = legend .. p.legendTable('#f0dc82', 'Верифицированы Книгой рекордов Гиннесса:', numberOfVerifiedGWR)
	end
	if numberOfContested > 0 then
		legend = legend .. p.legendTable('#E9B06B', 'Оспариваемые:', numberOfContested)
	end
	if numberOfLiving > 0 then
		legend = legend .. p.legendTable('#99FF99', 'Ныне живущие:', numberOfLiving)
	end
	if keyLast - numberOfLiving > 0 then
		legend = legend .. p.legendTable('#F9F9F9', 'Умершие:', keyLast - numberOfLiving)
	end
		legend = legend .. p.legendTable('#eaecf0', 'Всего:', keyLast)
		
	return tostring(legend) .. tostring(root)
end

--------------------------------------------------------------------------------
	
	-- Ячейка заголовка
function p.cellTh(text)
	-- Ячейка
	root
		:tag('th') --начало th
			:attr('scope', 'col')
			:wikitext(text)
	
	-- Сноска на примечание
	if reference ~= nil and reference ~= '' then
		root:wikitext("&#8202;" .. p.reference(reference))
	end
	
		root:done() --конец th
	
	return tostring(root)
end
	
	--Ячейка ряда строки
function p.cellTd(text)
	-- Ячейка
	root
		:tag('td') --начало td
			:wikitext(text)
	
	-- Сноска на примечание
	if reference ~= nil and reference ~= '' then
		root:wikitext("&#8202;" .. p.reference(reference))
	end
	
	root:done() --конец td
	
	return tostring(root)
end

	--Создание ссылки
function p.reference(reference)
	local refspan = mw.html.create('span')
			:wikitext(reference)
			:css('background-color', 'transparent')
			:css('color', 'black')
			:css('padding','1px')
			:css('display','inline-block')
			:css('line-height','50%')
	
	return tostring(refspan)
end

	--Легенда таблицы
function p.legendTable(color, text, number)
	legendSpan = mw.html.create('span') --начало span 1
	legendSpan
		:css('font-size', '90%')
		:css('margin', '0px 0px 1px 0px')
		:css('display', 'block')
			:tag('span') --начало span 2
				:css('vertical-align', 'top')
				:css('border', '1px solid #a2a9b1')
				:css('background', color or '')
				:css('text-align', 'center')
				:attr('title', color or '')
				:wikitext('&nbsp;&nbsp;&nbsp;&nbsp;')
				:done() --конец span 2
		:wikitext('&nbsp;' .. text or '')
			:tag('span') --начало span 3
				:css('font-weight', 'bold')
				:wikitext('&nbsp;' .. number or '')
				:done() --конец span 3
		:done() --конец span 1
	return tostring(legendSpan)
end

	--Создание таблица
function p.createTable()
	root = mw.html.create('table')
	root
		:addClass('wikitable sortable')
	return root
end
	
	--Заголовок таблицы
function p.captionTable()
	local title = mw.title.getCurrentTitle()
	local pageTitle = title.text
	root
		:tag('caption') --начало caption
			:tag('span') --начало span
				:addClass('noprint purgelink')
				:attr('data-pagename', pageTitle)
				:css('float', 'right')
				:css('clear', 'right')
				:css('font-style', 'italic')
				:css('font-size', 'xx-small')
				:wikitext('[[Special:Purge/' .. pageTitle .. '|Очистить кеш сервера]]')
				:done() --конец span
			:done() --конец caption
	return tostring(root)
end

	--Создание флага
function p.createFlag(args)
	local strFlag = ''
	local argsFlag = mw.text.trim(args) or ''
	strFlag = '<span class="nowrap">'
		.. mw.getCurrentFrame():expandTemplate{
			title = 'Флаг',
			args = { argsFlag }
			}
		.. ' [[' .. argsFlag .. ']]</span>'
	return tostring(strFlag)
end

--------------------------------------------------------------------------------

-- Входная строка dateStr ожидается в формате «ГГГГ-ММ-ДД».
-- Возвращает числовое представление даты или nil
local function decodeDate(dateStr)
	if dateStr == nil or (#dateStr < 8 and #dateStr > 10) then return nil end

	local strings = mw.text.split(dateStr, '-', true)
	if strings == nil or #strings ~= 3 then return nil end

	local date = {}
	date.year = tonumber(strings[1]) or 0
	if tonumber(strings[2]) < 1 then
		date.month = 1
	elseif tonumber(strings[2]) > 12 then
		date.month = 12
	else
		date.month = tonumber(strings[2]) or 1
	end
	date.day = tonumber(strings[3]) or 1
	return os.time(date);
end

--[[ Ввод ожидается в виде таблицы аргументов.
	Ожидается запись, разделенная точкой с запятой, для каждого человека в формате:
		«Имя и т. д.; Пол; Дата рождения; Дата смерти; Ссылки и т. д.»
			Имя и Дата рождения являются обязательными, остальное не являются обязательными.
			Даты должны быть в формате «ГГГГ-ММ-ДД». ]]
-- Возвращает таблицу с персонами
local function decodeArgs(args)
	local dateNow = os.time()
	local persons = {}
	for name, value in pairs(args) do
		local strings = mw.text.split(value, ';', true)
		if strings ~= nil and #strings >= 2 then -- Нужно хотя бы имя и дату рождения
			local person = {}
			person.name = mw.text.trim(strings[1]) or ''
			person.sex = mw.text.trim(strings[2]) or ''
			person.dateBirth = decodeDate(strings[3]) -- Дата рождения
			if person.dateBirth ~= nil then
				person.dateDeath = decodeDate(strings[4]) -- Дата смерти
				if person.dateDeath ~= nil or person.dateDeath ~= '' then -- Мертвый
					person.age = ageInYearsAndDays(person.dateDeath, person.dateBirth)
				else -- Still alive
					person.age = ageInYearsAndDays(dateNow, person.dateBirth)
				end
				if #strings == 6 then strings[5] = mw.text.trim(strings[5]) end
				if strings[5] ~= nil  and strings[5] ~= '' then
					if string.match(strings[5], ',',1) ~= nil then
						local country = mw.text.split(strings[5], ',', true)
						person.nation = ''
						for k=1, #country do
							if country[k] == nil then break end
							if country[k]:find('МестоРождения:') then
								person.placeOfBirth = '[[' .. country[k]:gsub('МестоРождения:','',1) .. ']]'
							elseif country[k]:find('МестоСмерти:') then
								person.placeOfDeath = '[[' .. country[k]:gsub('МестоСмерти:','',1) .. ']]'
							elseif country[k]:find('МестоПроживания:') then
								person.placeOfResidence = '[[' .. country[k]:gsub('МестоПроживания:','',1) .. ']]'
							else
								person.nation = person.nation .. (k >= 2 and ' — ' or '') .. p.createFlag(country[k])
							end
						end
					else
						if strings[5]:find('МестоРождения:') then
								person.placeOfBirth = '[[' .. strings[5]:gsub('МестоРождения:','',1) .. ']]'
						elseif strings[5]:find('МестоСмерти:') then
								person.placeOfDeath = '[[' .. strings[5]:gsub('МестоСмерти:','',1) .. ']]'
						elseif strings[5]:find('МестоПроживания:') then
								person.placeOfResidence = '[[' .. strings[5]:gsub('МестоПроживания:','',1) .. ']]'
						else
							person.nation = p.createFlag(strings[5]) or ''
						end
					end
				end
				if #strings == 6  and strings[6] ~= nil and strings[6] ~= '' then
					if strings[6]:find('GRG') then
						person.verifiedGRG = true
						strings[6] = strings[6]:gsub('GRG','',1)
					elseif strings[6]:find('ESO') then
						person.verifiedESO = true
						strings[6] = strings[6]:gsub('ESO','',1)
					elseif strings[6]:find('GWR') then
						person.verifiedGWR = true
						strings[6] = strings[6]:gsub('GWR','',1)
					elseif strings[6]:find('спорный') then
						person.contested = true
						strings[6]	= strings[6]:gsub('спорный','',1)
					end
					strings[6]	= mw.text.trim(strings[6]:gsub(',',''))
					if strings[6] ~= '' then
						person.dateAssignment = decodeDate(strings[6]) or '' -- Дата присвоения статуса
					end
				end
				table.insert(persons, person)
			end
		end
	end
	return persons
end

-- Декодирует аргументы из фрейма и отображает список
local function displaySorted_(args)
	local args = getArgs(args)
	local persons = decodeArgs(args)
	return p.displaySortedTable(persons, args) -- Отображать в виде сортируемой таблицы
end

-- Для использования через шаблон. Использует аргументы от родителя к шаблону
function p.displaySorted(frame)
	local args = getArgs(frame, {
		--trim = false,
		--removeBlanks = false,
		parentOnly = true,
		wrappers = 'Шаблон:Список старейших людей в мире',
	})
	return displaySorted_(args)
end

-- Для использования непосредственно с #invoke
function p.displaySorted0(frame)
	return displaySorted_(frame.args)
end

return p