Module:Date table sorting

(Redirected from Module:Dts)

local yesno = require('Module:Yesno')local lang = mw.language.getContentLanguage()local N_YEAR_DIGITS = 12local MAX_YEAR = 10^N_YEAR_DIGITS - 1---------------------------------------------------------------------------------- Dts class--------------------------------------------------------------------------------local Dts = {}Dts.__index = DtsDts.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) doret[month:lower()] = iendreturn retendDts.monthSearch = Dts._makeMonthSearch(Dts.months)Dts.monthSearchAbbr = Dts._makeMonthSearch(Dts.monthsAbbr)Dts.monthSearchAbbr['sept'] = 9 -- Allow "Sept" to match SeptemberDts.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] thenself:parseDateParts(args[1], args[2], args[3], args[4])elseif args[1] thenself:parseDate(args[1])end-- Raise an error on invalid valuesif self.year thenif self.year == 0 thenerror('years cannot be zero', 0)elseif self.year < -MAX_YEAR thenerror(string.format('years cannot be less than %s',lang:formatNum(-MAX_YEAR)), 0)elseif self.year > MAX_YEAR thenerror(string.format('years cannot be greater than %s',lang:formatNum(MAX_YEAR)), 0)elseif math.floor(self.year) ~= self.year thenerror('years must be an integer', 0)endendif self.month and (self.month < 1or self.month > 12or math.floor(self.month) ~= self.month) thenerror('months must be an integer between 1 and 12', 0)endif self.day and (self.day < 1or self.day > 31or math.floor(self.day) ~= self.day) thenerror('days must be an integer between 1 and 31', 0)end-- Set month abbreviation behaviour, i.e. whether we are outputting-- "January" or "Jan".if args.abbr thenself.isAbbreviated = args.abbr == 'on' or yesno(args.abbr) or falseelseself.isAbbreviated = self.isAbbreviated or falseend-- Set the format stringif args.format thenself.format = args.formatelseself.format = self.format or 'mdy'endif not Dts.formats[self.format] thenerror(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 thenself.addkey = tonumber(args.addkey)if not self.addkey orself.addkey < 0 orself.addkey > 9999 ormath.floor(self.addkey) ~= self.addkeythenerror("the 'addkey' parameter must be an integer between 0 and 9999", 0)endend-- Set whether the displayed date is allowed to wrap or not.self.isWrapping = args.nowrap == 'off' or yesno(args.nowrap) == falsereturn selfendfunction Dts:hasDate()return (self.year or self.month or self.day) ~= nilend-- 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 thenreturn monthelsemonth = Dts.monthSearchAbbr[s]if month thenself.isAbbreviated = truereturn monthendendreturn nilend-- Parses separate parameters for year, month, day, and era.function Dts:parseDateParts(year, month, day, bc)if year thenself.year = tonumber(year)if not self.year thenerror(string.format("'%s' is not a valid year",tostring(year)), 0)endendif month thenif tonumber(month) thenself.month = tonumber(month)elseif type(month) == 'string' thenself.month = self:parseMonthName(month)endif not self.month thenerror(string.format("'%s' is not a valid month",tostring(month)), 0)endendif day thenself.day = tonumber(day)if not self.day thenerror(string.format("'%s' is not a valid day",tostring(day)), 0)endendif bc thenlocal bcLower = type(bc) == 'string' and bc:lower()if bcLower == 'bc' or bcLower == 'bce' thenif self.year and self.year > 0 thenself.year = -self.yearendelseif bcLower ~= 'ad' and bcLower ~= 'ce' thenerror(string.format("'%s' is not a valid era code (expected 'BC', 'BCE', 'AD' or 'CE')",tostring(bc)), 0)endendend-- 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)endlocal function parseDayOrMonth(s)if s:find('^%d%d?$') thenreturn tonumber(s)endendlocal function parseYear(s)if s:find('^%d%d%d%d?$') thenreturn tonumber(s)endend-- 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 thenreturnend-- Split the string using non-word characters as boundaries.date = tostring(date)local parts = mw.text.split(date, '%W+')local nParts = #partsif 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), 0)endif nParts == 1 then-- This can be either a month name or a year.self.month = self:parseMonthName(parts[1])if not self.month thenself.year = parseYear(parts[1])if not self.year thendateError()endendelseif nParts == 2 then-- This can be any of the following formats:-- DD Month-- Month DD-- Month YYYY-- YYYY-MMself.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 thendateError()endendelseself.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 thendateError()endelse-- This is YYYY-MM.self.year = parseYear(parts[1])self.month = parseDayOrMonth(parts[2])if not self.year or not self.month thendateError()endendendelseif nParts == 3 then-- This can be any of the following formats:-- DD Month YYYY-- Month DD, YYYY-- YYYY-MM-DD-- DD-MM-YYYYself.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 thendateError()endelseself.day = parseDayOrMonth(parts[1])if self.day thenself.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 thendateError()endelse-- 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 thendateError()endendelse-- This is YYYY-MM-DDself.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 thendateError()endendendendendfunction Dts:makeSortKey()local year, month, daylocal nYearDigits = N_YEAR_DIGITSif self:hasDate() thenyear = self.year or os.date("*t").yearif year < 0 thenyear = -MAX_YEAR - 1 - yearnYearDigits = nYearDigits + 1 -- For the minus signendmonth = self.month or 1day = self.day or 1else-- Blank {{dts}} transclusions should sort last.year = MAX_YEARmonth = 99day = 99endreturn string.format('%0' .. nYearDigits .. 'd-%02d-%02d-%04d',year, month, day, self.addkey or 0)endfunction Dts:getMonthName()if not self.month thenreturn ''endif self.isAbbreviated thenreturn self.monthsAbbr[self.month]elsereturn self.months[self.month]endendfunction Dts:makeDisplay()if self.format == 'hide' thenreturn ''endlocal 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 thenret[#ret + 1] = self:getMonthName()ret[#ret + 1] = ' 'ret[#ret + 1] = self.dayif hasYear thenret[#ret + 1] = ','endelseif hasDay and hasMonth thenret[#ret + 1] = self.dayret[#ret + 1] = ' 'ret[#ret + 1] = self:getMonthName()elseif hasDay thenret[#ret + 1] = self.dayelseif hasMonth thenret[#ret + 1] = self:getMonthName()endif hasYear thenif hasDay or hasMonth thenret[#ret + 1] = ' 'endlocal displayYear = math.abs(self.year)if displayYear > 9999 thendisplayYear = lang:formatNum(displayYear)elsedisplayYear = tostring(displayYear)endret[#ret + 1] = displayYearif self.year < 0 thenret[#ret + 1] = '&nbsp;BC'endendreturn table.concat(ret)endfunction Dts:__tostring()local root = mw.html.create()local span = root:tag('span'):attr('data-sort-value', self:makeSortKey())-- Displayif self:hasDate() and self.format ~= 'hide' thenspan:wikitext(self:makeDisplay())if not self.isWrapping thenspan:css('white-space', 'nowrap')endendreturn tostring(root)end---------------------------------------------------------------------------------- Exports--------------------------------------------------------------------------------local p = {}function p._exportClasses()return {Dts = Dts}endfunction p._main(args)local success, ret = pcall(function ()local dts = Dts.new(args)return tostring(dts)end)if success thenreturn retelseret = string.format('<strong class="error">Error in [[Template:Date table sorting]]: %s</strong>',ret)if mw.title.getCurrentTitle().namespace == 0 then-- Only categorise in the main namespaceret = ret .. '[[Category:Date table sorting templates with errors]]'endreturn retendendfunction p.main(frame)local args = require('Module:Arguments').getArgs(frame, {wrappers = 'Template:Date table sorting',})return p._main(args)endreturn p