モジュール: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.months = {
	"January",
	"February",
	"March",
	"April",
	"May",
	"June",
	"July",
	"August",
	"September",
	"October",
	"November",
	"December"
}
Dts.monthsAbbr = {
	"Jan",
	"Feb",
	"Mar",
	"Apr",
	"May",
	"Jun",
	"Jul",
	"Aug",
	"Sep",
	"Oct",
	"Nov",
	"Dec"
}
function Dts._makeMonthSearch(t)
	local ret = {}
	for i, month in ipairs(t) do
		retmonth:lower() = i
	end
	return ret
end
Dts.monthSearch = Dts._makeMonthSearch(Dts.months)
Dts.monthSearchAbbr = Dts._makeMonthSearch(Dts.monthsAbbr)
Dts.monthSearchAbbr'sept' = 9 -- Allow "Sept" to match September
Dts.formats = {
	ymd = true,
	dmy = true,
	mdy = true,
	ym = 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 args2 or args3 or args4 then
		self:parseDateParts(args1, args2, args3, args4)
	elseif args1 then
		self:parseDate(args1)
	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  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  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  31
		or math.floor(self.day) ~= self.day
	) then
		error('days must be an integer between 1 and 31', 0)
	end
	-- Set debug mode
	if args.debug then
		self.isdebug = args.debug
	end
	-- Set the format string
	if args.format then
		self.format = args.format
	else
		self.format = self.format or 'ymd'
	end
	if not Dts.formatsself.format then
		error(string.format(
			"'%s' is not a valid format",
			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  9999 or
			math.floor(self.addkey) ~= self.addkey
		then
			error("the 'addkey' parameter must be an integer between 0 and 9999", 0)
		end
	end
	-- Set whether the displayed date is allowed to wrap or not.
	self.isWrapping = args.nowrap == 'off' or yesno(args.nowrap) == false
	-- Set whether the abbreviated or not.
	self.isAbbreviated = args.abbr == 'on' or yesno(args.abbr) == true
	-- Check for deprecated parameters.
	if args.link then
		self.hasDeprecatedParameters = true
	end
	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.monthSearchs
	if month then
		return month
	else
		month = Dts.monthSearchAbbrs
		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(
				"'%s' is not a valid year",
				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' is not a valid month",
				tostring(month)
			), 0)
		end
	end
	if day then
		self.day = tonumber(day)
		if not self.day then
			error(string.format(
				"'%s' is not a valid day",
				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 > 0 then
				self.year = -self.year
			end
		elseif bcLower ~= 'ad' and bcLower ~= 'ce' then
			error(string.format(
				"'%s' is not a valid era code (expected 'BC', 'BCE', 'AD' or 'CE')",
				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(
			"'%s' is an invalid date",
			date
		), 0)
	end
	local function parseDayOrMonth(s)
		if s:find('^%d%d?$') then
			return tonumber(s)
		end
	end
	local function parseMonth(s)
		if s:find('^%d%d?$') and tonumber(s) >=1 and tonumber(s) =1 and tonumber(s)  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  9999 and lang:formatNum(displayYear) or tostring(displayYear)
		ret#ret + 1 = displayYear
		ret#ret + 1 = '年'
	end
	if hasMonth then
		ret#ret + 1 = self.month
		ret#ret + 1 = '月'
	end
	if hasDay then
		ret#ret + 1 = self.day
		ret#ret + 1 = '日'
	end
	return table.concat(ret)
end
function Dts:makeDisplayAbbr()
	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 ret = {}
	if hasYear then
		if self.year  9999 and lang:formatNum(displayYear) or tostring(displayYear)
		ret#ret + 1 = displayYear
		if hasMonth or hasDay then
			ret#ret + 1 = '/'
		end
	end
	if hasMonth then
		ret#ret + 1 = self.month
		if hasDay then
			ret#ret + 1 = '/'
		end
	end
	if hasDay then
		ret#ret + 1 = self.day
	end
	return table.concat(ret)
end
function Dts:makeDisplayAbbrOrNoAbbr()
	if self.isAbbreviated then
		return self:makeDisplayAbbr()
	else
		return self:makeDisplay()
	end
end
function Dts:renderTrackingCategories()
	if self.hasDeprecatedParameters then
		return 
	else
		return 
	end
end
function Dts:__tostring()
	local root = mw.html.create()
	-- Sort key
	if self.isdebug then
		root:tag('span')
			:css('border', 'dotted 1px')
			:wikitext(self:makeSortKey())
	else
		root:tag('span')
			:addClass('sortkey')
			:css('display', 'none')
			:css('speak', 'none')
			:wikitext(self:makeSortKey())
	end
	
	-- Display
	if self:hasDate() then
		if self.isWrapping then
			root:wikitext(self:makeDisplayAbbrOrNoAbbr())
		else
			root:tag('span')
				:css('white-space', 'nowrap')
				:wikitext(self:makeDisplayAbbrOrNoAbbr())
		end
	end
	-- Tracking categories
	root:wikitext(self:renderTrackingCategories())
	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(
			'Error in Template:Dts: %s',
			ret
		)
		if mw.title.getCurrentTitle().namespace == 0 then
			-- Only categorise in the main namespace
			ret = ret .. 
		end
		return ret
	end
end
function p.main(frame)
	local args = require('Module:Arguments').getArgs(frame, {
		wrappers = 'Template:Dts',
	})
	return p._main(args)
end
return p