Module:Dts
From Timelines
Revision as of 00:00, 3 July 2015 by Mr. Stradivarius (talk) (actually, don't parse pure year dates the same as years in other dates, as it will break transclusions)
Documentation for this module may be created at Module:Dts/doc
local lang = mw.language.getContentLanguage() -------------------------------------------------------------------------------- -- 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 ret[month:lower()] = i end return ret end Dts.monthSearch = Dts._makeMonthSearch(Dts.months) Dts.monthSearchAbbr = Dts._makeMonthSearch(Dts.monthsAbbr) Dts.formats = { dmy = true, mdy = true, dm = true, md = true, my = true, m = true } function Dts.new(args) local self = setmetatable({}, Dts) if args[2] or args[3] or args[4] then -- YMD parameters are specified individually. if args[1] then self.year = tonumber(args[1]) if not self.year then error(string.format( "'%s' is not a valid year", tostring(args[1]) ), 3) end end if args[2] then if tonumber(args[2]) then self.month = tonumber(args[2]) elseif type(args[2]) == 'string' then self.month = self:parseMonthName(args[2]) end if not self.month then error(string.format( "'%s' is not a valid month", tostring(args[2]) ), 3) end end if args[3] then self.day = tonumber(args[3]) if not self.day then error(string.format( "'%s' is not a valid day", tostring(args[3]) ), 3) end end if args[4] then local bc = type(args[4]) == 'string' and args[4]:lower() if bc == 'bc' or bc == 'bce' then if self.year and self.year > 0 then self.year = -self.year end elseif bc ~= 'ad' or bc ~= 'ce' then error(string.format( "'%s' is not a valid era code (expected 'BC', 'BCE', 'AD' or 'CE')", tostring(args[4]) ), 3) end end elseif args[1] then -- args[1] is the entire date that we need to parse. self:parseDate(tostring(args[1])) else error('no date parameters detected', 3) end -- Raise an error on invalid values if self.year then if self.year == 0 then error('years cannot be zero', 3) elseif math.floor(self.year) ~= self.year then error('years must be an integer', 3) end end if self.month and ( self.month < 1 or self.month > 12 or math.floor(self.month) ~= self.month ) then error('months must be an integer between 1 and 12', 3) end if self.day and ( self.day < 1 or self.day > 31 or math.floor(self.month) ~= self.month ) then error('months must be an integer between 1 and 31', 3) end -- Whether to output abbreviated month names if args.abbr then self.abbr = args.abbr == 'on' else self.abbr = self.abbr 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( "'%s' is not a valid format", tostring(self.format) ), 3) end return self end -- Find the month number for a month name, and set the abbr 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.abbr = true return month end end return nil 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 ), 5) end local function parseDayOrMonth(s) if s:find('^%d%d?$') then return tonumber(s) end end local function parseYear(s) if #s == 4 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 four 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. local parts = mw.text.split(date, '%W+') local nParts = #parts if parts[1] == '' or parts[nParts] == '' or nParts > 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 < 1 then -- If we have less than one element, then something has gone horribly -- wrong. error(string.format( "an unknown error occurred while parsing the date '%s'", date ), 4) 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 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 -- This is DD Month. self.format = 'dmy' self.day = parseDayOrMonth(parts[1]) self.month = self:parseMonthName(parts[2]) if not self.day or not self.month then dateError() end end elseif nParts == 3 then -- This can be any of the following formats: -- DD Month YYYY -- Month DD, YYYY -- YYYY-MM-DD 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 -- This is DD Month YYYY. self.format = 'dmy' self.month = self:parseMonthName(parts[2]) self.year = parseYear(parts[3]) if not self.month or not self.year then dateError() 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 = self.year or os.date("*t").year year = year > 0 and year or -10000 - year return string.format( "%05d-%02d-%02d-%02d%02d", year, self.month or 1, self.day or 1, 0, 0 ) end function Dts:getMonthName() if not self.month then return '' end if self.abbr then return self.monthsAbbr[self.month] else return self.months[self.month] end end function Dts:makeDisplay() 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 ret[#ret + 1] = math.abs(self.year) if self.year < 0 then ret[#ret + 1] = ' BC' end end return table.concat(ret) end function Dts:__tostring() local root = mw.html.create() root :tag('span') :addClass('sortkey') :css('display', 'none') :css('speak', 'none') :wikitext(self:makeSortKey()) :done() :tag('span') :css('white-space', 'nowrap') :wikitext(self:makeDisplay()) return tostring(root) end -------------------------------------------------------------------------------- -- Exports -------------------------------------------------------------------------------- local p = {} function p._main(args) local dts = Dts.new(args) return tostring(dts) end function p.main(frame) local args = getArgs(frame, { wrappers = 'Template:Dts', removeBlanks = false }) return p._main(args) end return p