Difference between revisions of "Module:Current events monthly archive"

From blackwiki
Jump to navigation Jump to search
blackwiki>RossO
(Last day of month still not working)
m (21 revisions imported)
 
(14 intermediate revisions by 2 users not shown)
Line 1: Line 1:
-- This module renders the introductory content on Monthly archive pages for the [[Portal:Current events]].
+
-- This module generates the monthly archives [[Portal:Current events]].
-- Test this against Portal:Current events/September 2011/Sandbox
+
-- See a sample archive at [[Portal:Current events/September 2011]].
--[[
 
Incoming expected variables:
 
frame.args.year = Integer value for year
 
frame.args.month = Integer value for month, 1 based.
 
--]]
 
  
local p = {}
+
--------------------------------------------------------------------------------
 +
-- Helper functions
 +
--------------------------------------------------------------------------------
  
function p.main(frame)
+
-- Return true if num is a positive integer; otherwise return false
local argsDate = nil
+
local function isPositiveInteger(num)
if (frame and frame.args and frame.args.year and frame.args.month) then
+
return num > 0 and num == math.floor(num)
-- If a date is passed in, assume that the display page is an Archive page.
 
-- If no date passed in, assume that the display page is the current Current Events page
 
argsDate = frame.args.year .. "-" .. frame.args.month .. "-01" -- Construct a date, YYY-M-DD format.
 
end
 
local dateStuff = p.getDateStuff(argsDate, frame.args.month, frame.args.year)
 
local dayStrings = p.makeDayStrings(dateStuff)
 
return p.export(dayStrings, dateStuff)
 
 
end
 
end
  
local function makeWikilink(link, display)
+
-- Make an ordinal number from an integer.
if display then
+
local function makeOrdinalNumber(num)
return string.format('[[%s|%s]]', link, display)
+
local suffix
 +
local rem100 = num % 100
 +
if rem100 == 11 or rem100 == 12 or rem100 == 13 then
 +
suffix = 'th'
 
else
 
else
return string.format('[[%s]]', link)
+
local rem10 = num % 10
 +
if rem10 == 1 then
 +
suffix = 'st'
 +
elseif rem10 == 2 then
 +
suffix = 'nd'
 +
elseif rem10 == 3 then
 +
suffix = 'rd'
 +
else
 +
suffix = 'th'
 +
end
 
end
 
end
 +
return tostring(num) .. suffix
 
end
 
end
  
function p.getDateStuff(argsDate, monthNumber, yearNumber)
+
-- Try to parse the year and month from the current title.
 +
-- This template is usually used on pages with titles like
 +
-- [[Portal:Current events/September 2011]], so our general approach will be to
 +
-- pass the subpage name to lang:formatDate and see if we get something that's
 +
-- not an error.
 +
local function parseYearAndMonthFromCurrentTitle()
 +
local title = mw.title.getCurrentTitle()
 +
local lang = mw.language.getContentLanguage()
 +
-- Detect if we are on a sandbox page, and if so, use the base page.
 +
if title.subpageText:find('^[sS]andbox%d*$') then
 +
title = title.basePageTitle
 +
end
 +
-- Try to parse the date.
 +
local success, date = pcall(function ()
 +
-- lang:formatDate throws errors if it gets strange input,
 +
-- so use pcall to catch them, as random subpage names will
 +
-- usually not be well-formed dates.
 +
return lang:formatDate('Y-m', title.subpageText)
 +
end)
 +
if not success then
 +
-- We couldn't parse the date, so return nil.
 +
return nil, nil
 +
end
 +
-- Parse the year and month numbers from the date we got from
 +
-- lang:formatDate. If we can't parse them, then something has gone
 +
-- wrong with either lang:formatDate or our pattern.
 +
local year, month = date:match('^(%d%d%d%d)%-(%d%d)$')
 +
year = tonumber(year)
 +
month = tonumber(month)
 +
if not year or not month then
 +
error('Internal error in [[Module:Current events '
 +
.. 'monthly archive]]: couldn\'t match date '
 +
.. 'from lang:formatDate output'
 +
)
 +
end
 +
return year, month
 +
end
  
--[[
+
--------------------------------------------------------------------------------
Note: This function takes advantage of the formatDate's second argument to
+
-- Date info
create data for the archival calendars. If the second arg (argsDate) is nil,
+
--------------------------------------------------------------------------------
then formatDate assumes the current date/time.
 
Reference: https://www.mediawiki.org/wiki/Help:Extension:ParserFunctions#.23time
 
--]]
 
  
-- Gets date data.
+
-- Get a table of information about the date for the monthly archive.
local dateStuff = {}
+
local function getDateInfo(year, month)
 
local lang = mw.language.getContentLanguage()
 
local lang = mw.language.getContentLanguage()
dateStuff.argsDate = argsDate
+
local dateFuncs = {}
 +
local dateInfo = setmetatable({}, {
 +
__index = function (t, key)
 +
-- Memoize values so we only have to calculate them once.
 +
if dateFuncs[key] then
 +
local val = dateFuncs[key]()
 +
t[key] = val
 +
return val
 +
end
 +
end
 +
})
 +
 
 +
function dateFuncs.currentYear()
 +
-- The current year (number)
 +
return tonumber(os.date('%Y'))
 +
end
 +
 
 +
function dateFuncs.currentMonthNumber()
 +
-- The current month (number)
 +
return tonumber(os.date('%m'))
 +
end
  
--Year
+
function dateFuncs.year()
local year = lang:formatDate('Y', argsDate)
+
-- The year (number)
year = tonumber(year)
+
return tonumber(year) or dateInfo.currentYear
dateStuff.year = year
+
end
--Leap Year
+
 
local leapBool = tonumber(lang:formatDate('L', argsDate))
+
function dateFuncs.monthNumber()
dateStuff.leapBool = leapBool
+
-- The month (number)
local leapDesc = "common"
+
return tonumber(month) or dateInfo.currentMonthNumber
if (leapBool == 1) then
+
end
leapDesc = "leap"
+
 
 +
function dateFuncs.monthNumberZeroPadded()
 +
-- The month, zero-padded to two digits (string)
 +
return string.format('%02d', dateInfo.monthNumber)
 +
end
 +
 
 +
function dateFuncs.date()
 +
-- The date in YYYY-MM-DD format (string)
 +
return string.format(
 +
'%04d-%02d-01',
 +
dateInfo.year, dateInfo.monthNumber
 +
)
 +
end
 +
 
 +
function dateFuncs.monthName()
 +
-- The month name, e.g. "September" (string)
 +
return lang:formatDate('F', dateInfo.date)
 +
end
 +
 
 +
function dateFuncs.monthOrdinal()
 +
-- The ordinal month as an English word (string)
 +
local ordinals = {
 +
"first",  "second",  "third",
 +
"fourth",  "fifth",    "sixth",
 +
"seventh", "eighth",  "ninth",
 +
"tenth",  "eleventh", "twelfth and final",
 +
}
 +
return ordinals[dateInfo.monthNumber]
 +
end
 +
 
 +
function dateFuncs.beVerb()
 +
-- If the month is the current month or a month in the future, then this
 +
-- is the string "is"; otherwise, "was" (string)
 +
if dateInfo.year > dateInfo.currentYear
 +
or (
 +
dateInfo.year == dateInfo.currentYear
 +
and dateInfo.monthNumber >= dateInfo.currentMonthNumber
 +
)
 +
then
 +
return 'is'
 +
else
 +
return 'was'
 +
end
 +
end
 +
 
 +
function dateFuncs.leapDesc()
 +
-- The year's leap year status; either "common", "leap" or
 +
-- "century leap" (string)
 +
local isLeapYear = tonumber(lang:formatDate('L', dateInfo.date)) == 1
 +
if isLeapYear and dateInfo.year % 400 == 0 then
 +
return 'century leap'
 +
elseif isLeapYear then
 +
return 'leap'
 +
else
 +
return 'common'
 +
end
 
end
 
end
dateStuff.leapDesc = leapDesc
 
  
-- Month and Name
+
function dateFuncs.decadeNote()
local monthName = lang:formatDate('F', argsDate)
+
-- If the month is the first or last of a decade, century, or
dateStuff.monthName = monthName
+
-- millennium, a note to that effect; otherwise the empty string
dateStuff.monthNumber = tonumber(monthNumber)
+
-- (string)
 +
local function getMillennium(year)
 +
return math.floor((year - 1) / 1000) + 1 -- Fenceposts
 +
end
  
-- Month and year
+
local function getCentury(year)
local monthAndYear = lang:formatDate('F Y', argsDate)
+
return math.floor((year - 1) / 100) + 1 -- Fenceposts
local firstOfMonth = lang:formatDate('01-m-Y', argsDate)
+
end
dateStuff.monthAndYear = monthAndYear
 
  
-- Previous month and year
+
local year = dateInfo.year
dateStuff.previousMonthAndYear = lang:formatDate('F Y', firstOfMonth .. ' -1 month')
+
local month = dateInfo.monthNumber
-- Next month and year
+
local firstOrLast = month == 12 and "last" or "first"
dateStuff.nextMonthAndYear = lang:formatDate('F Y', firstOfMonth .. ' +1 month')
 
  
-- Day
+
if year % 1000 == 0 and month == 12
local dayNumber = lang:formatDate('j', argsDate)
+
or year % 1000 == 1 and month == 1
dayNumber = tonumber(dayNumber)
+
then
dateStuff.dayNumber = dayNumber
+
local millennium = makeOrdinalNumber(getMillennium(year))
 +
local century = makeOrdinalNumber(getCentury(year))
 +
return string.format(
 +
--Millenniums always overlap centuries.
 +
"It %s the %s month of the [[%s millennium]] and the [[%s century]].",
 +
dateInfo.beVerb, firstOrLast, millennium, century
 +
)
 +
elseif year % 100 == 0 and month == 12
 +
or year % 100 == 1 and month == 1
 +
then
 +
local century = makeOrdinalNumber(getCentury(year))
 +
return string.format(
 +
"It %s the %s month of the [[%s century]].",
 +
dateInfo.beVerb, firstOrLast, century
 +
)
 +
elseif year % 10 == 9 and month == 12
 +
or year % 10 == 0 and month == 1
 +
then
 +
local decadeNumber = math.floor(dateInfo.year / 10) * 10
 +
return string.format(
 +
"It %s the %s month of the [[%ds]] decade.",
 +
dateInfo.beVerb, firstOrLast, decadeNumber
 +
)
 +
end
  
-- Weekday of the first day of the month
+
return ''
local firstWeekday = lang:formatDate('w', firstOfMonth) -- Sunday = 0, Saturday = 6
+
end
firstWeekday = tonumber(firstWeekday)
 
firstWeekday = firstWeekday + 1 -- Make compatible with Lua tables. Sunday = 1, Saturday = 7.
 
dateStuff.firstWeekday = firstWeekday
 
  
-- Days in month
+
function dateFuncs.moonNote()
local daysInMonth = lang:formatDate('j', firstOfMonth .. ' +1 month -1 day')
+
-- If the month had no full moon, a note to that effect; otherwise the
daysInMonth = tonumber(daysInMonth)
+
-- empty string (string)
dateStuff.daysInMonth = daysInMonth
+
if dateInfo.monthNumber == 2 then
 +
-- https://www.quora.com/When-was-the-last-time-the-entire-month-of-February-passed-without-a-Full-Moon/answer/Alan-Marble
 +
local year = dateInfo.year
 +
if year == 1961
 +
or year == 1999
 +
or year == 2018
 +
or year == 2037
 +
or year == 2067
 +
or year == 2094
 +
then
 +
return 'This month had no full moon.'
 +
end
 +
end
 +
 
 +
return ''
 +
end
 +
 
 +
function dateFuncs.firstDayOfMonth()
 +
-- Weekday of the first day of the month, e.g. "Tuesday" (string)
 +
return lang:formatDate('l', dateInfo.date)
 +
end
 +
 
 +
function dateFuncs.lastDayOfMonth()
 +
-- Weekday of the last day of the month, e.g. "Thursday" (string)
 +
return lang:formatDate('l', dateInfo.date .. ' +1 month -1 day')
 +
end
  
-- Weekday of the last day of the month
+
function dateFuncs.daysInMonth()
local lastOfMonthDayNumber = lang:formatDate('w', argsDate .. ' +' .. daysInMonth .. ' days')
+
-- Number of days in the month (number)
local lastOfMonthDayName = lang:formatDate('l - Y/M/d', argsDate .. ' +' .. daysInMonth .. ' days')
+
return tonumber(lang:formatDate(
dateStuff.lastOfMonthDayNumber = lastOfMonthDayNumber
+
'j',
dateStuff.lastOfMonthDayName = lastOfMonthDayName
+
dateInfo.date .. ' +1 month -1 day')
 +
)
 +
end
  
local lastWeekday = 0 --lang:formatDate('w', lastOfMonthDayNumber) -- Sunday = 0, Saturday = 6
+
function dateFuncs.mainContent()
dateStuff.lastWeekdayDate = lastWeekday
+
-- The rendered content of all the current events portal pages for the
lastWeekday = tonumber(lastWeekday)
+
-- month (string)
lastWeekday = lastWeekday + 1 -- Make compatible with Lua tables. Sunday = 1, Saturday = 7.
+
local ret = {}
dateStuff.lastWeekday = lastWeekday
+
local frame = mw.getCurrentFrame()
 +
local year = dateInfo.year
 +
local monthName = dateInfo.monthName
 +
for date = 1, 31 do
 +
local portalTitle = mw.title.new(string.format(
 +
'Portal:Current events/%d %s %d',
 +
year, monthName, date
 +
))
 +
if portalTitle.exists then
 +
table.insert(
 +
ret,
 +
frame:expandTemplate{title = portalTitle.prefixedText}
 +
)
 +
end
 +
end
 +
return table.concat(ret, '\n')
 +
end
  
return dateStuff
+
return dateInfo
 
end
 
end
  
function p.makeDayStrings(dateStuff)
+
--------------------------------------------------------------------------------
local calStrings = {}
+
-- Exports
local currentDay = dateStuff.dayNumber
+
--------------------------------------------------------------------------------
local currentMonth = dateStuff.monthName
+
 
local currentYear = dateStuff.year
+
local p = {}
local makeDayLink = p.makeDayLink
+
 
for dayCount = 1, dateStuff.daysInMonth do
+
function p.main(frame)
if dateStuff.argsDate then
+
-- Get the arguments
calStrings[#calStrings + 1] = makeDayLink(dayCount, currentMonth, currentYear)
+
local args = require('Module:Arguments').getArgs(frame, {
else
+
wrappers = 'Template:Current events monthly archive',
calStrings[#calStrings + 1] = tostring(dayCount)
+
})
 +
local year = tonumber(args.year)
 +
local month = tonumber(args.month)
 +
 
 +
-- Validate the arguments
 +
if year and not isPositiveInteger(year) then
 +
error('invalid year argument (must be a positive integer)', 2)
 +
end
 +
if month then
 +
if not isPositiveInteger(month) then
 +
error('invalid month argument (must be a positive integer)', 2)
 +
elseif month > 12 then
 +
error('invalid month argument (must be 12 or less)', 2)
 
end
 
end
 
end
 
end
return calStrings
 
end
 
  
function p.makeDayLink(day, month, year)
+
-- If we weren't passed a month or a year, try to get them from the
return string.format("'''[[#%d %s %d|  %d  ]]'''", year, month, day, day)
+
-- page title.
 +
if not year and not month then
 +
year, month = parseYearAndMonthFromCurrentTitle()
 +
end
 +
 
 +
-- Convert the dateInfo table values into arguments to pass to the current
 +
-- events monthly archive display template
 +
local dateInfo = getDateInfo(year, month)
 +
local displayArgs = {}
 +
displayArgs['year']                    = dateInfo.year
 +
displayArgs['month-name']              = dateInfo.monthName
 +
displayArgs['month-number']            = dateInfo.monthNumber
 +
displayArgs['month-number-zero-padded'] = dateInfo.monthNumberZeroPadded
 +
displayArgs['be-verb']                  = dateInfo.beVerb
 +
displayArgs['month-ordinal']            = dateInfo.monthOrdinal
 +
displayArgs['leap-desc']                = dateInfo.leapDesc
 +
displayArgs['moon-note']                = dateInfo.moonNote
 +
displayArgs['decade-note']              = dateInfo.decadeNote
 +
displayArgs['first-day-of-month']       = dateInfo.firstDayOfMonth
 +
displayArgs['last-day-of-month']       = dateInfo.lastDayOfMonth
 +
displayArgs['days-in-month']            = dateInfo.daysInMonth
 +
displayArgs['main-content']            = dateInfo.mainContent
 +
 
 +
-- Expand the display template with the arguments from dateInfo, and return
 +
-- it
 +
return frame:expandTemplate{
 +
title = 'Current events monthly archive/display',
 +
args = displayArgs,
 +
}
 
end
 
end
  
function p.export(dayStrings, dateStuff)
+
-- Export getDateInfo so that we can use it in unit tests.
-- Intro paragraph
+
p.getDateInfo = getDateInfo
local monthAndYear = dateStuff.monthAndYear
 
local ordinal = {"first", "second", "third", "fourth", "fifth", "sixth", "seventh", "eighth", "ninth", "tenth", "eleventh", "twelfth and final"}
 
local daysOfWeek = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}
 
local root = mw.html.create('div')
 
root
 
:tag('p')
 
:wikitext("'''[[" .. dateStuff.monthName .. "]] [[" .. dateStuff.year .. "]]''' was the " .. ordinal[dateStuff.monthNumber] .. " month of that " .. dateStuff.leapDesc .. " year. ")
 
:wikitext("The month, which began on a [[" .. daysOfWeek[dateStuff.firstWeekday] .. "]], ended on a ")
 
:wikitext("[[" .. dateStuff.lastOfMonthDayName .. " : " .. dateStuff.lastOfMonthDayNumber .. " : " .. dateStuff.lastWeekday .. " : " .. daysOfWeek[dateStuff.lastWeekday] .. "]] after " .. dateStuff.daysInMonth .. " days.")
 
:done()
 
:tag('h2')
 
:wikitext("[[Portal:Current events]]")
 
:done()
 
:tag('p')
 
:wikitext("''This is an [[Portal:Current events/How to archive the portal|archived version]] of Wikipedia's [[Portal:Current events|Current events Portal]] from September 2011.''")
 
:done()
 
:tag('div')
 
:css{border='solid 1px black',display='flex'}
 
:tag('div')
 
:css{border='solid 1px black',flex='100%'}
 
:tag('p')
 
:wikitext("{{Portal:Current events/Month Inclusion|2011 September}}")
 
:done()
 
:done()
 
:tag('div')
 
:css{border='solid 1px black',flex='300px'}
 
:tag('div')
 
:wikitext("{{Portal:Current events/September 2011/Calendar}}")
 
:done()
 
:tag('div')
 
:wikitext("{{Portal:Current events/September 2011/Sidebar}}")
 
:done()
 
:done()
 
:done()
 
:tag('h2')
 
:wikitext("References")
 
:done()
 
:tag('div')
 
:wikitext("{{reflist}}")
 
:done()
 
:tag('div')
 
:wikitext("{{commons category|September 2011}}")
 
:done()
 
:tag('div')
 
:wikitext("{{Portal:Current events/Events by month}}")
 
:done()
 
:tag('div')
 
:wikitext("[[Category:September|2011]] [[Category:2011|*2011-09]] [[Category:Current events archives]]")
 
:done()
 
 
return tostring(root)
 
end
 
  
 
return p
 
return p

Latest revision as of 16:00, 26 September 2020

Usage

{{#invoke:Current events monthly archive|main}}

{{#invoke:Current events monthly archive|main|year=1996|month=12}}

This will eventually be extended to support the layout components for the Portal:Current events archive pages.



-- This module generates the monthly archives [[Portal:Current events]].
-- See a sample archive at [[Portal:Current events/September 2011]].

--------------------------------------------------------------------------------
-- Helper functions
--------------------------------------------------------------------------------

-- Return true if num is a positive integer; otherwise return false
local function isPositiveInteger(num)
	return num > 0 and num == math.floor(num)
end

-- Make an ordinal number from an integer.
local function makeOrdinalNumber(num)
	local suffix
	local rem100 = num % 100
	if rem100 == 11 or rem100 == 12 or rem100 == 13 then
		suffix = 'th'
	else
		local rem10 = num % 10
		if rem10 == 1 then
			suffix = 'st'
		elseif rem10 == 2 then
			suffix = 'nd'
		elseif rem10 == 3 then
			suffix = 'rd'
		else
			suffix = 'th'
		end
	end
	return tostring(num) .. suffix
end

-- Try to parse the year and month from the current title.
-- This template is usually used on pages with titles like
-- [[Portal:Current events/September 2011]], so our general approach will be to
-- pass the subpage name to lang:formatDate and see if we get something that's
-- not an error.
local function parseYearAndMonthFromCurrentTitle()
	local title = mw.title.getCurrentTitle()
	local lang = mw.language.getContentLanguage()
	-- Detect if we are on a sandbox page, and if so, use the base page.
	if title.subpageText:find('^[sS]andbox%d*$') then
		title = title.basePageTitle
	end
	-- Try to parse the date.
	local success, date = pcall(function ()
		-- lang:formatDate throws errors if it gets strange input,
		-- so use pcall to catch them, as random subpage names will
		-- usually not be well-formed dates.
		return lang:formatDate('Y-m', title.subpageText)
	end)
	if not success then
		-- We couldn't parse the date, so return nil.
		return nil, nil
	end
	-- Parse the year and month numbers from the date we got from
	-- lang:formatDate. If we can't parse them, then something has gone
	-- wrong with either lang:formatDate or our pattern.
	local year, month = date:match('^(%d%d%d%d)%-(%d%d)$')
	year = tonumber(year)
	month = tonumber(month)
	if not year or not month then
		error('Internal error in [[Module:Current events '
			.. 'monthly archive]]: couldn\'t match date '
			.. 'from lang:formatDate output'
		)
	end
	return year, month
end

--------------------------------------------------------------------------------
-- Date info
--------------------------------------------------------------------------------

-- Get a table of information about the date for the monthly archive.
local function getDateInfo(year, month)
	local lang = mw.language.getContentLanguage()
	local dateFuncs = {}
	local dateInfo = setmetatable({}, {
		__index = function (t, key)
			-- Memoize values so we only have to calculate them once.
			if dateFuncs[key] then
				local val = dateFuncs[key]()
				t[key] = val
				return val
			end
		end
	})

	function dateFuncs.currentYear()
		-- The current year (number)
		return tonumber(os.date('%Y'))
	end

	function dateFuncs.currentMonthNumber()
		-- The current month (number)
		return tonumber(os.date('%m'))
	end

	function dateFuncs.year()
		-- The year (number)
		return tonumber(year) or dateInfo.currentYear
	end

	function dateFuncs.monthNumber()
		-- The month (number)
		return tonumber(month) or dateInfo.currentMonthNumber
	end

	function dateFuncs.monthNumberZeroPadded()
		-- The month, zero-padded to two digits (string)
		return string.format('%02d', dateInfo.monthNumber)
	end

	function dateFuncs.date()
		-- The date in YYYY-MM-DD format (string)
		return string.format(
			'%04d-%02d-01',
			dateInfo.year, dateInfo.monthNumber
		)
	end

	function dateFuncs.monthName()
		-- The month name, e.g. "September" (string)
		return lang:formatDate('F', dateInfo.date)
	end

	function dateFuncs.monthOrdinal()
		-- The ordinal month as an English word (string)
		local ordinals = {
			"first",   "second",   "third",
			"fourth",  "fifth",    "sixth",
			"seventh", "eighth",   "ninth",
			"tenth",   "eleventh", "twelfth and final",
		}
		return ordinals[dateInfo.monthNumber]
	end

	function dateFuncs.beVerb()
		-- If the month is the current month or a month in the future, then this
		-- is the string "is"; otherwise, "was" (string)
		if dateInfo.year > dateInfo.currentYear
			or (
				dateInfo.year == dateInfo.currentYear
				and dateInfo.monthNumber >= dateInfo.currentMonthNumber
			)
		then
			return 'is'
		else
			return 'was'
		end
	end

	function dateFuncs.leapDesc()
		-- The year's leap year status; either "common", "leap" or
		-- "century leap" (string)
		local isLeapYear = tonumber(lang:formatDate('L', dateInfo.date)) == 1
		if isLeapYear and dateInfo.year % 400 == 0 then
			return 'century leap'
		elseif isLeapYear then
			return 'leap'
		else
			return 'common'
		end
	end

	function dateFuncs.decadeNote()
		-- If the month is the first or last of a decade, century, or
		-- millennium, a note to that effect; otherwise the empty string
		-- (string)
		local function getMillennium(year)
			return math.floor((year - 1) / 1000) + 1 -- Fenceposts
		end

		local function getCentury(year)
			return math.floor((year - 1) / 100) + 1 -- Fenceposts
		end

		local year = dateInfo.year
		local month = dateInfo.monthNumber
		local firstOrLast = month == 12 and "last" or "first"

		if year % 1000 == 0 and month == 12
			or year % 1000 == 1 and month == 1
		then
			local millennium = makeOrdinalNumber(getMillennium(year))
			local century = makeOrdinalNumber(getCentury(year))
			return string.format(
				--Millenniums always overlap centuries.
				"It %s the %s month of the [[%s millennium]] and the [[%s century]].",
				dateInfo.beVerb, firstOrLast, millennium, century
			)
		elseif year % 100 == 0 and month == 12
			or year % 100 == 1 and month == 1
		then
			local century = makeOrdinalNumber(getCentury(year))
			return string.format(
				"It %s the %s month of the [[%s century]].",
				dateInfo.beVerb, firstOrLast, century
			)
		elseif year % 10 == 9 and month == 12
			or year % 10 == 0 and month == 1
		then
			local decadeNumber = math.floor(dateInfo.year / 10) * 10
			return string.format(
				"It %s the %s month of the [[%ds]] decade.",
				dateInfo.beVerb, firstOrLast, decadeNumber
			)
		end

		return ''
	end

	function dateFuncs.moonNote()
		-- If the month had no full moon, a note to that effect; otherwise the
		-- empty string (string)
		if dateInfo.monthNumber == 2 then
			-- https://www.quora.com/When-was-the-last-time-the-entire-month-of-February-passed-without-a-Full-Moon/answer/Alan-Marble
			local year = dateInfo.year
			if year == 1961
				or year == 1999
				or year == 2018
				or year == 2037
				or year == 2067
				or year == 2094
			then
				return 'This month had no full moon.'
			end
		end

		return ''
	end

	function dateFuncs.firstDayOfMonth()
		-- Weekday of the first day of the month, e.g. "Tuesday" (string)
		return lang:formatDate('l', dateInfo.date)
	end

	function dateFuncs.lastDayOfMonth()
		-- Weekday of the last day of the month, e.g. "Thursday" (string)
		return lang:formatDate('l', dateInfo.date .. ' +1 month -1 day')
	end

	function dateFuncs.daysInMonth()
		-- Number of days in the month (number)
		return tonumber(lang:formatDate(
			'j',
			dateInfo.date .. ' +1 month -1 day')
		)
	end

	function dateFuncs.mainContent()
		-- The rendered content of all the current events portal pages for the
		-- month (string)
		local ret = {}
		local frame = mw.getCurrentFrame()
		local year = dateInfo.year
		local monthName = dateInfo.monthName
		for date = 1, 31 do
			local portalTitle = mw.title.new(string.format(
				'Portal:Current events/%d %s %d',
				year, monthName, date
			))
			if portalTitle.exists then
				table.insert(
					ret,
					frame:expandTemplate{title = portalTitle.prefixedText}
				)
			end
		end
		return table.concat(ret, '\n')
	end

	return dateInfo
end

--------------------------------------------------------------------------------
-- Exports
--------------------------------------------------------------------------------

local p = {}

function p.main(frame)
	-- Get the arguments
	local args = require('Module:Arguments').getArgs(frame, {
		wrappers = 'Template:Current events monthly archive',
	})
	local year = tonumber(args.year)
	local month = tonumber(args.month)

	-- Validate the arguments
	if year and not isPositiveInteger(year) then
		error('invalid year argument (must be a positive integer)', 2)
	end
	if month then
		if not isPositiveInteger(month) then
			error('invalid month argument (must be a positive integer)', 2)
		elseif month > 12 then
			error('invalid month argument (must be 12 or less)', 2)
		end
	end

	-- If we weren't passed a month or a year, try to get them from the
	-- page title.
	if not year and not month then
		year, month = parseYearAndMonthFromCurrentTitle()
	end

	-- Convert the dateInfo table values into arguments to pass to the current
	-- events monthly archive display template
	local dateInfo = getDateInfo(year, month)
	local displayArgs = {}
	displayArgs['year']                     = dateInfo.year
	displayArgs['month-name']               = dateInfo.monthName
	displayArgs['month-number']             = dateInfo.monthNumber
	displayArgs['month-number-zero-padded'] = dateInfo.monthNumberZeroPadded
	displayArgs['be-verb']                  = dateInfo.beVerb
	displayArgs['month-ordinal']            = dateInfo.monthOrdinal
	displayArgs['leap-desc']                = dateInfo.leapDesc
	displayArgs['moon-note']                = dateInfo.moonNote
	displayArgs['decade-note']              = dateInfo.decadeNote
	displayArgs['first-day-of-month']       = dateInfo.firstDayOfMonth
	displayArgs['last-day-of-month']        = dateInfo.lastDayOfMonth
	displayArgs['days-in-month']            = dateInfo.daysInMonth
	displayArgs['main-content']             = dateInfo.mainContent

	-- Expand the display template with the arguments from dateInfo, and return
	-- it
	return frame:expandTemplate{
		title = 'Current events monthly archive/display',
		args = displayArgs,
	}
end

-- Export getDateInfo so that we can use it in unit tests.
p.getDateInfo = getDateInfo

return p