Модуль:Сартаваньне датаў у табліцы

Дакумэнтацыю да гэтага модуля можна стварыць у Модуль:Сартаваньне датаў у табліцы/Дакумэнтацыя

local yesno = require('Модуль:ТакНе')
local lang = mw.language.getContentLanguage()
local N_YEAR_DIGITS = 12
local MAX_YEAR = 10^N_YEAR_DIGITS - 1

--------------------------------------------------------------------------------
-- Dts class
--------------------------------------------------------------------------------

local Dts = {}
Dts.__index = Dts

Dts.months = {
	"студзень",
	"люты",
	"сакавік",
	"красавік",
	"травень",
	"чэрвень",
	"ліпень",
	"жнівень",
	"верасень",
	"кастрычнік",
	"лістапад",
	"сьнежань"
}

Dts.monthsAbbr = {
	"сту",
	"лют",
	"сак",
	"кра",
	"тра",
	"чэр",
	"ліп",
	"жні",
	"вер",
	"кас",
	"ліс",
	"сьн"
}

function Dts._makeMonthSearch(t)
	local ret = {}
	for i, month in ipairs(t) do
		ret[month:lower()] = i
	end
	return ret
end
Dts.monthSearch = Dts._makeMonthSearch(Dts.months)
Dts.monthSearchAbbr = Dts._makeMonthSearch(Dts.monthsAbbr)
Dts.monthSearchAbbr['вера'] = 9 -- Хай "вера" будзе верасьнем

Dts.formats = {
	dmy = true,
	mdy = true,
	dm = true,
	md = true,
	my = true,
	y = true,
	m = true,
	d = true,
	hide = true
}

function Dts.new(args)
	local self = setmetatable({}, Dts)

	-- Разабраць парамэтры даты.
	-- На гэтым кроку таксама занатуем, дата ў фармаце DMY ці YMD,
	-- і ці назва месяцу скарочаная.
	if args[2] or args[3] or args[4] then
		self:parseDateParts(args[1], args[2], args[3], args[4])
	elseif args[1] then
		self:parseDate(args[1])
	end

	-- Пры няслушных значэньнях вяртаем памылку
	if self.year then
		if self.year == 0 then
			error('год ня можа быць нулявым', 0)
		elseif self.year < -MAX_YEAR then
			error(string.format(
				'год ня можа быць меншы за %s',
				lang:formatNum(-MAX_YEAR)
			), 0)
		elseif self.year > MAX_YEAR then
			error(string.format(
				'год ня можа быць большы за %s',
				lang:formatNum(MAX_YEAR)
			), 0)
		elseif math.floor(self.year) ~= self.year then
			error('год мусіць быць цэлым лікам', 0)
		end
	end
	if self.month and (
		self.month < 1
		or self.month > 12
		or math.floor(self.month) ~= self.month
	) then
		error('месяц мусіць быць цэлым лікам ад 1 да 12', 0)
	end
	if self.day and (
		self.day < 1
		or self.day > 31
		or math.floor(self.day) ~= self.day
	) then
		error('дзень мусіць быць цэлым лікам ад 1 да 31', 0)
	end

	-- Задаць спосаб вываду месяцу, т. б. выводзіць
	-- „студзень“ ці „сту“.
	if args.abbr then
		self.isAbbreviated = args.abbr == 'on' or yesno(args.abbr) or false
	else
		self.isAbbreviated = self.isAbbreviated or false
	end

	-- Задаць радок фармату
	if args.format then
		self.format = args.format
	else
		self.format = self.format or 'dmy'
	end
	if not Dts.formats[self.format] then
		error(string.format(
			"'%s' — хібны фармат",
			tostring(self.format)
		), 0)
	end

	-- Задаць дадатковы ключ. У канцы ключа сартаваньня дадаецца дадатковы,
	-- каб карыстальнікі маглі самастойна адрозьніць аднолькавыя даты.
	if args.addkey then
		self.addkey = tonumber(args.addkey)
		if not self.addkey or
			self.addkey < 0 or
			self.addkey > 9999 or
			math.floor(self.addkey) ~= self.addkey
		then
			error("парамэтар 'addkey' мусіць быць цэлым лікам ад 0 да 9999", 0)
		end
	end

	-- Задаць, абгортваць паказаную дату ці не.
	self.isWrapping = args.nowrap == 'off' or yesno(args.nowrap) == false

	return self
end

function Dts:hasDate()
	return (self.year or self.month or self.day) ~= nil
end

-- Знайсьці нумар месяцу па ягонай назьве і ўсталяваць сьцяг
-- isAbbreviated у патрэбнае значэньне.
function Dts:parseMonthName(s)
	s = s:lower()
	local month = Dts.monthSearch[s]
	if month then
		return month
	else
		month = Dts.monthSearchAbbr[s]
		if month then
			self.isAbbreviated = true
			return month
		end
	end
	return nil
end

-- Разьбірае асобныя парамэтры для году, месяцу, дню і эпохі.
function Dts:parseDateParts(year, month, day, bc)
	if year then
		self.year = tonumber(year)
		if not self.year then
			error(string.format(
				"'%s' — няслушны год",
				tostring(year)
			), 0)
		end
	end
	if month then
		if tonumber(month) then
			self.month = tonumber(month)
		elseif type(month) == 'string' then
			self.month = self:parseMonthName(month)
		end
		if not self.month then
			error(string.format(
				"'%s' — няслушны месяц",
				tostring(month)
			), 0)
		end
	end
	if day then
		self.day = tonumber(day)
		if not self.day then
			error(string.format(
				"'%s' — няслушны дзень",
				tostring(day)
			), 0)
		end
	end
	if bc then
		local bcLower = type(bc) == 'string' and bc:lower()
		if bcLower == 'да н. э.' or bcLower == 'да н. хр.' then
			if self.year and self.year > 0 then
				self.year = -self.year
			end
		elseif bcLower ~= 'н. э.' and bcLower ~= 'па н. хр.' then
			error(string.format(
				"'%s' — няслушная эра (магчымыя 'да н. э.', 'да Н. Хр.', 'н. э.' або 'па Н. Хр.')",
				tostring(bc)
			), 0)
		end
	end
end

-- Гэты мэтад разьбірае радок даты. Хоць і благая альтэрнатыва да
-- mw.language:formatDate, але ў выніку зь ім лягчэй разьбіраць дату, чым
-- карыстацца mw.language:formatDate, а пасьля спрабаваць здагадацца,
-- ці месяц у скарочанай форме, і ці фармат даты DMY або MDY.
function Dts:parseDate(date)
	-- Агульнае паведамленьне пра памылку.
	local function dateError()
		error(string.format(
			"'%s' — няслушная дата",
			date
		), 0)
	end

	local function parseDayOrMonth(s)
		if s:find('^%d%d?$') then
			return tonumber(s)
		end
	end

	local function parseYear(s)
		if s:find('^%d%d%d%d?$') then
			return tonumber(s)
		end
	end

	-- Сьпярша апрацоўвае даты толькі з гадоў, бо ў іх прысутнічаюць злучкі,
	-- а пасьля трэба разьдзяліць радок па нялітарных сымбалях, у тым ліку
	-- пераносах. Акрамя таго, няма патрэбы абмяжоўваць гады 3—4 разрадамі,
	-- бо іх асобных ня зблытаць з днём ці нумарам месяцу.
	self.year = tonumber(date)
	if self.year then
		return
	end

	-- Разьдзяліць радок па нялітарных сымбалях.
	date = tostring(date)
	local parts = mw.text.split(date, '%W+')
	local nParts = #parts
	if parts[1] == '' or parts[nParts] == '' or nParts > 3 then
		-- Мы разьбіраем максымум тры элемэнты, таму калі іх болей, тады
		-- вяртаем памылку. Калі першы ці апошні элемэнты пустыя, тады
		-- пачатак ці канец радку быў нялітарным сымбалем, што мы таксама
		-- ўважаем памылкай.
		dateError()
	elseif nParts < 1 then
	 	-- Калі маем менш за адзін элемэнт, то штосьці пайшло абсалютна
	 	-- ня так.
		error(string.format(
			"пры разборы даты '%s' адбылася нечаканая памылка",
			date
		), 0)
	end

	if nParts == 1 then
		-- Гэта можа быць як назва месяцу, так і год.
		self.month = self:parseMonthName(parts[1])
		if not self.month then
			self.year = parseYear(parts[1])
			if not self.year then
				dateError()
			end
		end
	elseif nParts == 2 then
		-- Можа быць адным з гэтых фарматаў:
		-- DD Месяц
		-- Месяц DD
		-- Месяц YYYY
		-- YYYY-MM
		self.month = self:parseMonthName(parts[1])
		if self.month then
			-- Гэта або Месяц DD, або Месяц YYYY.
			self.year = parseYear(parts[2])
			if not self.year then
				-- Гэта Месяц DD.
				self.format = 'mdy'
				self.day = parseDayOrMonth(parts[2])
				if not self.day then
					dateError()
				end
			end
		else
			self.month = self:parseMonthName(parts[2])
			if self.month then
				-- Гэта DD Месяц.
				self.format = 'dmy'
				self.day = parseDayOrMonth(parts[1])
				if not self.day then
					dateError()
				end
			else
				-- Гэта YYYY-MM.
				self.year = parseYear(parts[1])
				self.month = parseDayOrMonth(parts[2])
				if not self.year or not self.month then
					dateError()
				end
			end
		end
	elseif nParts == 3 then
		-- Можа быць адным з гэтых фарматаў:
		-- DD Месяц YYYY
		-- Месяц DD, YYYY
		-- YYYY-MM-DD
		-- DD-MM-YYYY
		self.month = self:parseMonthName(parts[1])
		if self.month then
			-- Гэта Месяц DD, YYYY.
			self.format = 'mdy'
			self.day = parseDayOrMonth(parts[2])
			self.year = parseYear(parts[3])
			if not self.day or not self.year then
				dateError()
			end
		else
			self.day = parseDayOrMonth(parts[1])
			if self.day then
				self.month = self:parseMonthName(parts[2])
				if self.month then
					-- Гэта DD Месяц YYYY.
					self.format = 'dmy'
					self.year = parseYear(parts[3])
					if not self.year then
						dateError()
					end
				else
					-- Гэта Месяц DD-MM-YYYY.
					self.format = 'dmy'
					self.month = parseDayOrMonth(parts[2])
					self.year = parseYear(parts[3])
					if not self.month or not self.year then
						dateError()
					end
				end
			else
				-- Гэта YYYY-MM-DD
				self.year = parseYear(parts[1])
				self.month = parseDayOrMonth(parts[2])
				self.day = parseDayOrMonth(parts[3])
				if not self.year or not self.month or not self.day then
					dateError()
				end
			end
		end
	end
end

function Dts:makeSortKey()
	local year, month, day
	local nYearDigits = N_YEAR_DIGITS
	if self:hasDate() then
		year = self.year or os.date("*t").year
		if year < 0 then
			year = -MAX_YEAR - 1 - year
			nYearDigits = nYearDigits + 1 -- Для знаку мінусу
		end
		month = self.month or 1
		day = self.day or 1
	else
		-- Пустыя ўключэньні {{Сартаваньне датаў у табліцы}} павінны
		-- адсартавацца апошнімі.
		year = MAX_YEAR
		month = 99
		day = 99
	end
	return string.format(
		'%0' .. nYearDigits .. 'd-%02d-%02d-%04d',
		year, month, day, self.addkey or 0
	)
end

function Dts:getMonthName()
	if not self.month then
		return ''
	end
	if self.isAbbreviated then
		return self.monthsAbbr[self.month]
	else
		return self.months[self.month]
	end
end

function Dts:makeDisplay()
	if self.format == 'hide' then
		return ''
	end
	local hasYear = self.year and self.format:find('y')
	local hasMonth = self.month and self.format:find('m')
	local hasDay = self.day and self.format:find('d')
	local isMonthFirst = self.format:find('md')
	local ret = {}
	if hasDay and hasMonth and isMonthFirst then
		ret[#ret + 1] = self:getMonthName()
		ret[#ret + 1] = ' '
		ret[#ret + 1] = self.day
		if hasYear then
			ret[#ret + 1] = ','
		end
	elseif hasDay and hasMonth then
		ret[#ret + 1] = self.day
		ret[#ret + 1] = ' '
		ret[#ret + 1] = self:getMonthName()
	elseif hasDay then
		ret[#ret + 1] = self.day
	elseif hasMonth then
		ret[#ret + 1] = self:getMonthName()
	end
	if hasYear then
		if hasDay or hasMonth then
			ret[#ret + 1] = ' '
		end
		local displayYear = math.abs(self.year)
		if displayYear > 9999 then
			displayYear = lang:formatNum(displayYear)
		else
			displayYear = tostring(displayYear)
		end
		ret[#ret + 1] = displayYear
		if self.year < 0 then
			ret[#ret + 1] = '&nbsp;да н. э.'
		end
	end
	return table.concat(ret)
end

function Dts:__tostring()
	local root = mw.html.create()
	local span = root:tag('span')
		:attr('data-sort-value', self:makeSortKey())

	-- Вывад
	if self:hasDate() and self.format ~= 'hide' then
		span:wikitext(self:makeDisplay())
		if not self.isWrapping then
			span:css('white-space', 'nowrap')
		end
	end

	return tostring(root)
end

--------------------------------------------------------------------------------
-- Экспарт
--------------------------------------------------------------------------------

local p = {}

function p._exportClasses()
	return {
		Dts = Dts
	}
end

function p._main(args)
	local success, ret = pcall(function ()
		local dts = Dts.new(args)
		return tostring(dts)
	end)
	if success then
		return ret
	else
		ret = string.format(
			'<strong class="error">Памылка ў шаблёне [[Шаблён:Сартаваньне датаў у табліцы|Сартаваньне датаў у табліцы]]: %s</strong>',
			ret
		)
		if mw.title.getCurrentTitle().namespace == 0 then
			-- Катэгорыя толькі ў асноўнай прасторы
			ret = ret .. '[[Катэгорыя:Вікіпэдыя:Старонкі з памылкамі ў парамэтрах шаблёнаў]]'
		end
		return ret
	end
end

function p.main(frame)
	local args = require('Модуль:Аргумэнты').getArgs(frame, {
		wrappers = 'Шаблён:Сартаваньне датаў у табліцы',
	})
	return p._main(args)
end

return p