Module:Dts
local yesno = require('Module:Yesno')
 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.Tamilmonths = {
 	&quot;சனவரி&quot;,
 	&quot;பெப்ரவரி&quot;,
 	&quot;மார்ச்சு&quot;,
 	&quot;ஏப்ரல்&quot;,
 	&quot;மே&quot;,
 	&quot;சூன்&quot;,
 	&quot;சூலை&quot;,
 	&quot;ஆகத்து&quot;,
 	&quot;செப்டம்பர்&quot;,
 	&quot;அக்டோபர்&quot;,
 	&quot;நவம்பர்&quot;,
 	&quot;திசம்பர்&quot;
 }
 
 Dts.TamilmonthsAbbr = {
 	&quot;ஜன&quot;,
 	&quot;பிப்&quot;,
 	&quot;மார்&quot;,
 	&quot;ஏப்&quot;,
 	&quot;மே&quot;,
 	&quot;ஜூன்&quot;,
 	&quot;ஜூலை&quot;,
 	&quot;ஆக&quot;,
 	&quot;செப்&quot;,
 	&quot;அக்&quot;,
 	&quot;நவ&quot;,
 	&quot;டிச&quot;
 }
 
 Dts.months = {
 	&quot;January&quot;,
 	&quot;February&quot;,
 	&quot;March&quot;,
 	&quot;April&quot;,
 	&quot;May&quot;,
 	&quot;June&quot;,
 	&quot;July&quot;,
 	&quot;August&quot;,
 	&quot;September&quot;,
 	&quot;October&quot;,
 	&quot;November&quot;,
 	&quot;December&quot;
 }
 
 Dts.monthsAbbr = {
 	&quot;Jan&quot;,
 	&quot;Feb&quot;,
 	&quot;Mar&quot;,
 	&quot;Apr&quot;,
 	&quot;May&quot;,
 	&quot;Jun&quot;,
 	&quot;Jul&quot;,
 	&quot;Aug&quot;,
 	&quot;Sep&quot;,
 	&quot;Oct&quot;,
 	&quot;Nov&quot;,
 	&quot;Dec&quot;
 }
 
 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['sept'] = 9 -- Allow &quot;Sept&quot; to match September
 
 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)
 
 	-- Parse date parameters.
 	-- In this step we also record whether the date was in DMY or YMD format,
 	-- and whether the month name was abbreviated.
 	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
 
 	-- Raise an error on invalid values
 	if self.year then
 		if self.year == 0 then
 			error('years cannot be zero', 0)
 		elseif self.year &lt; -MAX_YEAR then
 			error(string.format(
 				'years cannot be less than %s',
 				lang:formatNum(-MAX_YEAR)
 			), 0)
 		elseif self.year &gt; MAX_YEAR then
 			error(string.format(
 				'years cannot be greater than %s',
 				lang:formatNum(MAX_YEAR)
 			), 0)
 		elseif math.floor(self.year) ~= self.year then
 			error('years must be an integer', 0)
 		end
 	end
 	if self.month and (
 		self.month &lt; 1
 		or self.month &gt; 12
 		or math.floor(self.month) ~= self.month
 	) then
 		error('months must be an integer between 1 and 12', 0)
 	end
 	if self.day and (
 		self.day &lt; 1
 		or self.day &gt; 31
 		or math.floor(self.day) ~= self.day
 	) then
 		error('days must be an integer between 1 and 31', 0)
 	end
 
 	-- Set month abbreviation behaviour, i.e. whether we are outputting
 	-- &quot;January&quot; or &quot;Jan&quot;.
 	if args.abbr then
 		self.isAbbreviated = args.abbr == 'on' or yesno(args.abbr) or false
 	else
 		self.isAbbreviated = self.isAbbreviated or false
 	end
 
 	-- Set the format string
 	if args.format then
 		self.format = args.format
 	else
 		self.format = self.format or 'mdy'
 	end
 	if not Dts.formats[self.format] then
 		error(string.format(
 			&quot;'%s' is not a valid format&quot;,
 			tostring(self.format)
 		), 0)
 	end
 
 	-- Set addkey. This adds a value at the end of the sort key, allowing users
 	-- to manually distinguish between identical dates.
 	if args.addkey then
 		self.addkey = tonumber(args.addkey)
 		if not self.addkey or
 			self.addkey &lt; 0 or
 			self.addkey &gt; 9999 or
 			math.floor(self.addkey) ~= self.addkey
 		then
 			error(&quot;the 'addkey' parameter must be an integer between 0 and 9999&quot;, 0)
 		end
 	end
 
 	-- Set whether the displayed date is allowed to wrap or not.
 	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
 
 -- Find the month number for a month name, and set the isAbbreviated flag as
 -- appropriate.
 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
 
 -- Parses separate parameters for year, month, day, and era.
 function Dts:parseDateParts(year, month, day, bc)
 	if year then
 		self.year = tonumber(year)
 		if not self.year then
 			error(string.format(
 				&quot;'%s' is not a valid year&quot;,
 				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(
 				&quot;'%s' is not a valid month&quot;,
 				tostring(month)
 			), 0)
 		end
 	end
 	if day then
 		self.day = tonumber(day)
 		if not self.day then
 			error(string.format(
 				&quot;'%s' is not a valid day&quot;,
 				tostring(day)
 			), 0)
 		end
 	end
 	if bc then
 		local bcLower = type(bc) == 'string' and bc:lower()
 		if bcLower == 'bc' or bcLower == 'bce' then
 			if self.year and self.year &gt; 0 then
 				self.year = -self.year
 			end
 		elseif bcLower ~= 'ad' and bcLower ~= 'ce' then
 			error(string.format(
 				&quot;'%s' is not a valid era code (expected 'BC', 'BCE', 'AD' or 'CE')&quot;,
 				tostring(bc)
 			), 0)
 		end
 	end
 end
 
 -- This method parses date strings. This is a poor man's alternative to
 -- mw.language:formatDate, but it ends up being easier for us to parse the date
 -- here than to use mw.language:formatDate and then try to figure out after the
 -- fact whether the month was abbreviated and whether we were DMY or MDY.
 function Dts:parseDate(date)
 	-- Generic error message.
 	local function dateError()
 		error(string.format(
 			&quot;'%s' is an invalid date&quot;,
 			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
 
 	-- Deal with year-only dates first, as they can have hyphens in, and later
 	-- we need to split the string by all non-word characters, including
 	-- hyphens. Also, we don't need to restrict years to 3 or 4 digits, as on
 	-- their own they can't be confused as a day or a month number.
 	self.year = tonumber(date)
 	if self.year then
 		return
 	end
 
 	-- Split the string using non-word characters as boundaries.
 	date = tostring(date)
 	local parts = mw.text.split(date, '%W+')
 	local nParts = #parts
 	if parts[1] == '' or parts[nParts] == '' or nParts &gt; 3 then
 		-- We are parsing a maximum of three elements, so raise an error if we
 		-- have more. If the first or last elements were blank, then the start
 		-- or end of the string was a non-word character, which we will also
 		-- treat as an error.
 		dateError()
 	elseif nParts &lt; 1 then
 	 	-- If we have less than one element, then something has gone horribly
 	 	-- wrong.
 		error(string.format(
 			&quot;an unknown error occurred while parsing the date '%s'&quot;,
 			date
 		), 0)
 	end
 
 	if nParts == 1 then
 		-- This can be either a month name or a year.
 		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
 		-- This can be any of the following formats:
 		-- DD Month
 		-- Month DD
 		-- Month YYYY
 		-- YYYY-MM
 		self.month = self:parseMonthName(parts[1])
 		if self.month then
 			-- This is either Month DD or Month YYYY.
 			self.year = parseYear(parts[2])
 			if not self.year then
 				-- This is Month 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
 				-- This is DD Month.
 				self.format = 'dmy'
 				self.day = parseDayOrMonth(parts[1])
 				if not self.day then
 					dateError()
 				end
 			else
 				-- This is 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
 		-- This can be any of the following formats:
 		-- DD Month YYYY
 		-- Month DD, YYYY
 		-- YYYY-MM-DD
 		-- DD-MM-YYYY
 		self.month = self:parseMonthName(parts[1])
 		if self.month then
 			-- This is Month 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
 					-- This is DD Month YYYY.
 					self.format = 'dmy'
 					self.year = parseYear(parts[3])
 					if not self.year then
 						dateError()
 					end
 				else
 					-- This is 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
 				-- This is 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(&quot;*t&quot;).year
 		if year &lt; 0 then
 			year = -MAX_YEAR - 1 - year
 			nYearDigits = nYearDigits + 1 -- For the minus sign
 		end
 		month = self.month or 1
 		day = self.day or 1
 	else
 		-- Blank {{dts}} transclusions should sort last.
 		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.TamilmonthsAbbr[self.month]
 	else
 		return self.Tamilmonths[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 &gt; 9999 then
 			displayYear = lang:formatNum(displayYear)
 		else
 			displayYear = tostring(displayYear)
 		end
 		ret[#ret + 1] = displayYear
 		if self.year &lt; 0 then
 			ret[#ret + 1] = '&amp;nbsp;BC'
 		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())
 
 	-- Display
 	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
 
 --------------------------------------------------------------------------------
 -- Exports
 --------------------------------------------------------------------------------
 
 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(
 			'&lt;strong class=&quot;error&quot;&gt;Error in [[Template:Dts]]: %s&lt;/strong&gt;',
 			ret
 		)
 		if mw.title.getCurrentTitle().namespace == 0 then
 			-- Only categorise in the main namespace
 			ret = ret .. '[[Category:Dts templates with errors]]'
 		end
 		return ret
 	end
 end
 
 function p.main(frame)
 	local args = require('Module:Arguments').getArgs(frame, {
 		wrappers = 'Template:Dts',
 	})
 	return p._main(args)
 end