--------------------------------------------------------------------------------
-- Module:Archive
--
-- This module provides classes for easily working with archive pages.
--------------------------------------------------------------------------------
-- Load modules
require('Module:No globals')
-- Modules to be lazily loaded
local exponentialSearch -- [[Module:Exponential search]]
-- Constants
local DEFAULT_NUMBERED_SUBPAGE_FORMAT = 'Archive ${NUMBER}'
local DEFAULT_NUMBERED_ARCHIVE_FORMAT_STRING = '%01d'
local DEFAULT_NUMBERED_ARCHIVE_PATTERN = '(%d+)'
local DEFAULT_DATED_SUBPAGE_FORMAT = '${MONTH} ${YEAR}'
local DEFAULT_DATED_ARCHIVE_FORMAT_STRING = 'F Y'
local DEFAULT_DATED_ARCHIVE_PATTERN = '(%w+ %d%d%d%d)'
--------------------------------------------------------------------------------
-- Helper functions
--------------------------------------------------------------------------------
-- Make a class, with optional constructor and parent class.
local function makeClass(options)
options = options or {}
-- Make the class table. Classes inheriting from a parent class set the
-- parent class table as their metatable.
local class
if options.inheritsFrom then
class = setmetatable({}, options.inheritsFrom)
else
class = {}
end
-- Allow objects and child classes to inherit methods and static properties
-- from the class table.
class.__index = class
-- Make the constructor
function class.new(...)
-- Make the object table. If this is a child class, the object is made
-- from the constructor of the parent class.
local self
if options.inheritsFrom then
self = options.inheritsFrom.new(...)
else
self = {}
end
-- Allow objects to inherit methods and static properties from the class
-- table.
setmetatable(self, class)
-- Apply the constructor to the object.
if options.constructor then
options.constructor(self, ...)
end
return self
end
return class
end
local function validateNamedTitleArg(funcName, key, obj)
local tp = type(obj)
if tp == 'table' and type(obj.getContent) == 'function' then
-- val is a mw.title object
return obj
elseif tp == 'string' then
local title = mw.title.new(obj)
if not title then
error(string.format(
"bad named argument %s to '%s' ('%s' is not a valid title)",
key, funcName, obj
), 3)
end
return title
else
error(string.format(
"bad named argument %s to '%s' (table or string expected, got %s)",
key, funcName, type(obj)
), 3)
end
end
--------------------------------------------------------------------------------
-- ArchivePage class
-- Represents an individual archive page
--------------------------------------------------------------------------------
local ArchivePage = makeClass{constructor = function (self, options)
options = options or {}
self._title = validateNamedTitleArg('ArchivePage.new', 'title', options.title)
end}
function ArchivePage:getTitle()
return self._title
end
function ArchivePage:exists()
return self._title.exists
end
function ArchivePage:__tostring()
return tostring(self._title)
end
-- Not strictly a part of the class, but we keep the __eq metamethod code here
-- as it fits better here than anywhere else. We can't use the same function
-- object for both NumberedArchivePage:__eq and DatedArchivePage:__eq, otherwise
-- NumberedArchivePage objects and DatedArchivePage objects with the same title
-- would be equal to each other, which we don't want.
local function makeArchivePageEqualsMetamethod()
return function (self, obj)
return self._title == obj._title
end
end
--------------------------------------------------------------------------------
-- NumberedArchivePage class
-- Represents an individual numbered archive page
--------------------------------------------------------------------------------
local NumberedArchivePage = makeClass{
inheritsFrom = ArchivePage
}
function NumberedArchivePage:getArchiveNumber()
local pattern = DEFAULT_NUMBERED_SUBPAGE_FORMAT
:gsub('${NUMBER}', 'TEMPNUMBERMAGICWORD')
:gsub('%p', '%%%0')
:gsub(
'TEMPNUMBERMAGICWORD',
DEFAULT_NUMBERED_ARCHIVE_PATTERN:gsub('%%', '%%%%') -- escape % symbols in replacement string
)
pattern = '^' .. pattern .. '$'
local number = self._title.subpageText:match(pattern)
number = tonumber(number)
if number then
return number
else
error('error in getArchiveNumber: could not find number pattern in the subpage text')
end
end
-- Define metamethods
NumberedArchivePage.__eq = makeArchivePageEqualsMetamethod()
NumberedArchivePage.__tostring = ArchivePage.__tostring
--------------------------------------------------------------------------------
-- DatedArchivePage class
-- Represents an individual archive page where the archives are organized by
-- date
--------------------------------------------------------------------------------
local DatedArchivePage = makeClass{
inheritsFrom = ArchivePage
}
function DatedArchivePage:getBaseTitle()
if not self._baseTitle then
self._baseTitle = self._title.basePageTitle
end
return self._baseTitle
end
--[[
function DatedArchivePage:getArchiveCollection(options)
if not self.archiveCollection then
local baseTitle = self:getBaseTitle()
local archiveFormat = self:getArchiveFormat()
local date = self:getDate()
end
return self._archiveCollection
end
--]]
-- Define metamethods
DatedArchivePage.__eq = makeArchivePageEqualsMetamethod()
DatedArchivePage.__tostring = ArchivePage.__tostring
--------------------------------------------------------------------------------
-- ArchiveCollection
-- Represents a collection of archive pages. Should not be used directly, but
-- through one of its subclasses.
--------------------------------------------------------------------------------
local ArchiveCollection = makeClass{constructor = function (self, options)
self._hasArchivePage = options.hasArchivePage
end}
ArchiveCollection._archivePageClass = ArchivePage
function ArchiveCollection:getBaseTitle()
return self._baseTitle
end
function ArchiveCollection:getArchivePage(archive)
local subpage = self._subpageFormat
for variable, methodName in pairs(self._substitutions) do
local pattern = '${' .. variable .. '(:?)(.-)}'
subpage = subpage:gsub(pattern, function (colon, formatString)
if colon == ':' and formatString ~= '' then
return self[methodName](self, formatString, archive)
else
return self[methodName](self, nil, archive)
end
end)
end
return self._archivePageClass.new{title = self._baseTitle.prefixedText .. '/' .. subpage}
end
function ArchiveCollection:_exponentialArchiveSearch(archiveFunc)
exponentialSearch = exponentialSearch or require('Module:Exponential search')
local archiveNumber = exponentialSearch(function (i)
return self:getArchivePage(archiveFunc(i)):exists()
end)
if not archiveNumber then
return nil
end
return self:getArchivePage(archiveFunc(archiveNumber))
end
--------------------------------------------------------------------------------
-- NumberedArchiveCollection
-- Represents a collection of archive pages organized by number
--------------------------------------------------------------------------------
local NumberedArchiveCollection = makeClass{
inheritsFrom = ArchiveCollection,
constructor = function (self, options)
options = options or {}
self._subpageFormat = options.subpageFormat or DEFAULT_NUMBERED_SUBPAGE_FORMAT
self._baseTitle = validateNamedTitleArg(
'NumberedArchiveCollection.new',
'baseTitle',
options.baseTitle
)
end
}
NumberedArchiveCollection._archivePageClass = NumberedArchivePage
NumberedArchiveCollection._substitutions = {
NUMBER = '_formatNumberString',
}
function NumberedArchiveCollection:_formatNumberString(formatString, archive)
formatString = formatString or DEFAULT_NUMBERED_ARCHIVE_FORMAT_STRING
return formatString:format(archive)
end
function NumberedArchiveCollection:getEarliestArchivePage()
if self._hasArchivePage then
local existingArchiveNumber = self._hasArchivePage:getArchiveNumber()
return self:_exponentialArchiveSearch(function (i)
return existingArchiveNumber - i + 1
end)
else
local firstArchive = self:getArchivePage(1)
if firstArchive:exists() then
return firstArchive
else
return nil
end
end
end
function NumberedArchiveCollection:getLatestArchivePage()
return self:_exponentialArchiveSearch(function (i)
return i
end)
end
-----------------------------------------------------------------------------
-- DatedArchiveCollection
-- Represents a collection of archive pages organized by date
--------------------------------------------------------------------------------
local DatedArchiveCollection = makeClass{
inheritsFrom = ArchiveCollection,
constructor = function (self, options)
options = options or {}
self._subpageFormat = options.subpageFormat or DEFAULT_DATED_SUBPAGE_FORMAT
self._baseTitle = validateNamedTitleArg(
'DatedArchiveCollection.new',
'baseTitle',
options.baseTitle
)
self._lang = mw.language.getContentLanguage()
end
}
DatedArchiveCollection._archivePageClass = DatedArchivePage
DatedArchiveCollection._substitutions = {
MONTH = '_formatMonthString',
YEAR = '_formatYearString',
}
function DatedArchiveCollection:_formatDate(formatString, timestamp)
return self._lang:formatDate(formatString, timestamp)
end
function DatedArchiveCollection:_formatDateFragment(formatString, archive, validFragments, default)
local format
if formatString then
for _, s in ipairs(validFragments) do
if formatString == s then
format = formatString
break
end
end
if not format then
format = default
end
else
format = default
end
return self:_formatDate(format, archive)
end
function DatedArchiveCollection:_formatMonthString(formatString, archive)
return self:_formatDateFragment(formatString, archive, {'M', 'F', 'm', 'n', 'xg'}, 'F')
end
function DatedArchiveCollection:_formatYearString(formatString, archive)
return self:_formatDateFragment(formatString, archive, {'Y'}, 'Y')
end
function DatedArchiveCollection:_formatDateString(formatString, archive)
formatString = formatString or DEFAULT_DATED_ARCHIVE_FORMAT_STRING
return self:_formatDate(formatString, archive)
end
function DatedArchiveCollection:getEarliestArchivePage()
-- TODO: use date instead of "now" to avoid bug where March 31st - 1 month is March 3rd
return self:_exponentialArchiveSearch(function (i)
return 'now - ' .. (i - 1) .. ' months'
end)
end
function DatedArchiveCollection:getLatestArchivePage()
return self:getArchivePage('February 2017')
end
--------------------------------------------------------------------------------
-- Exports
--------------------------------------------------------------------------------
return {
NumberedArchivePage = NumberedArchivePage,
NumberedArchiveCollection = NumberedArchiveCollection,
DatedArchivePage = DatedArchivePage,
DatedArchiveCollection = DatedArchiveCollection,
}