Difference between revisions of "Module:Dts"
From Timelines
(detect invalid numbers) |
(make our own alternative to mw.language:formatDate) |
||
Line 68: | Line 68: | ||
self.month = tonumber(args[2]) | self.month = tonumber(args[2]) | ||
elseif type(args[2]) == 'string' then | elseif type(args[2]) == 'string' then | ||
− | + | self.month = self:parseMonthName(args[2]) | |
− | self.month = | ||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
end | end | ||
if not self.month then | if not self.month then | ||
Line 108: | Line 101: | ||
elseif args[1] then | elseif args[1] then | ||
-- args[1] is the entire date that we need to parse. | -- args[1] is the entire date that we need to parse. | ||
− | + | self:parseDate(tostring(args[1])) | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
else | else | ||
error('no date parameters detected', 3) | error('no date parameters detected', 3) | ||
Line 192: | Line 139: | ||
end | end | ||
− | function Dts | + | -- Find the month number for a month name, and set the abbr flag as appropriate. |
− | local | + | function Dts:parseMonthName(s) |
− | if | + | s = s:lower() |
− | return | + | 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 | end | ||
+ | return nil | ||
end | end | ||
− | function Dts: | + | -- 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 | end | ||
− | local | + | |
− | + | local function parseDayOrMonth(s) | |
− | + | if s:find('^%d%d?$') then | |
− | + | return tonumber(s) | |
+ | end | ||
end | end | ||
− | + | ||
− | if | + | local function parseYear(s) |
− | + | if #s == 4 then | |
− | + | return tonumber(s) | |
− | |||
− | |||
− | |||
− | |||
− | |||
end | end | ||
end | end | ||
− | |||
− | |||
− | + | -- Deal with things that can have hyphens in first, as later we need to | |
− | + | -- split the string by all non-word characters, including hyphens. | |
− | if | + | if tonumber(date) then |
− | + | self.year = tonumber(date) | |
+ | return | ||
end | 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 | end | ||
− | if | + | |
− | + | 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 | ||
− | if self.year then | + | end |
− | self.year = | + | 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 | end | ||
else | else | ||
− | if self: | + | self.day = parseDayOrMonth(parts[1]) |
− | self. | + | if self.day then |
− | self.year | + | -- 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 | ||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
end | end | ||
end | end | ||
Line 285: | Line 288: | ||
0 | 0 | ||
) | ) | ||
+ | end | ||
+ | |||
+ | function Dts:getMonthName() | ||
+ | if not self.month then | ||
+ | return '' | ||
+ | end | ||
+ | if self.abbr then | ||
+ | return self.monthsAbr[self.month] | ||
+ | else | ||
+ | return self.months[self.month] | ||
+ | end | ||
end | end | ||
Line 291: | Line 305: | ||
if self.day then | if self.day then | ||
if self.format == "mdy" then | if self.format == "mdy" then | ||
− | ret[#ret + 1] = self: | + | ret[#ret + 1] = self:getMonthName() |
ret[#ret + 1] = ' ' | ret[#ret + 1] = ' ' | ||
ret[#ret + 1] = self.day | ret[#ret + 1] = self.day | ||
Line 300: | Line 314: | ||
ret[#ret + 1] = self.day | ret[#ret + 1] = self.day | ||
ret[#ret + 1] = ' ' | ret[#ret + 1] = ' ' | ||
− | ret[#ret + 1] = self: | + | ret[#ret + 1] = self:getMonthName() |
end | end | ||
elseif self.month then | elseif self.month then | ||
− | ret[#ret + 1] = self: | + | ret[#ret + 1] = self:getMonthName() |
end | end | ||
if self.year then | if self.year then |
Revision as of 23:58, 1 July 2015
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) function Dts.new(args) local self = setmetatable({}, Dts) self.format = args.format or "mdy" self.abbr = false -- Default 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 -- Whether to output abbreviated month names if args.abbr then self.abbr = args.abbr == 'on' else self.abbr = self.abbr or false 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 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 things that can have hyphens in first, as later we need to -- split the string by all non-word characters, including hyphens. if tonumber(date) then self.year = tonumber(date) 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.monthsAbr[self.month] else return self.months[self.month] end end function Dts:makeDisplay() local ret = {} if self.day then if self.format == "mdy" then ret[#ret + 1] = self:getMonthName() ret[#ret + 1] = ' ' ret[#ret + 1] = self.day if self.year then ret[#ret + 1] = ',' end else ret[#ret + 1] = self.day ret[#ret + 1] = ' ' ret[#ret + 1] = self:getMonthName() end elseif self.month then ret[#ret + 1] = self:getMonthName() end if self.year then if self.month 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