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

From blackwiki
Jump to navigation Jump to search
blackwiki>RossO
(HTMLified the Wikimarkup)
m (21 revisions imported)
 
(18 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]].
 +
-- See a sample archive at [[Portal:Current events/September 2011]].
  
--[[
+
--------------------------------------------------------------------------------
Incoming expected variables:
+
-- Helper functions
frame.args.year = Integer value for year
+
--------------------------------------------------------------------------------
frame.args.month = Integer value for month, 1 based.
 
--]]
 
  
local p = {}
+
-- Return true if num is a positive integer; otherwise return false
 +
local function isPositiveInteger(num)
 +
return num > 0 and num == math.floor(num)
 +
end
  
function p.main(frame)
+
-- Make an ordinal number from an integer.
local argsDate = nil
+
local function makeOrdinalNumber(num)
if (frame and frame.args and frame.args.year and frame.args.month) then
+
local suffix
-- If a date is passed in, assume that the display page is an Archive page.
+
local rem100 = num % 100
-- If no date passed in, assume that the display page is the current Current Events page
+
if rem100 == 11 or rem100 == 12 or rem100 == 13 then
argsDate = frame.args.year .. "-" .. frame.args.month .. "-01" -- Construct a date, YYY-M-DD format.
+
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
 
end
local dateStuff = p.getDateStuff(argsDate)
+
return tostring(num) .. suffix
local dayStrings = p.makeDayStrings(dateStuff)
 
return p.export(dayStrings, dateStuff)
 
 
end
 
end
  
local function makeWikilink(link, display)
+
-- Try to parse the year and month from the current title.
if display then
+
-- This template is usually used on pages with titles like
return string.format('[[%s|%s]]', link, display)
+
-- [[Portal:Current events/September 2011]], so our general approach will be to
else
+
-- pass the subpage name to lang:formatDate and see if we get something that's
return string.format('[[%s]]', link)
+
-- 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
 
end
 +
return year, month
 
end
 
end
  
function p.getDateStuff(argsDate)
+
--------------------------------------------------------------------------------
 +
-- 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()
Note: This function takes advantage of the formatDate's second argument to
+
-- The ordinal month as an English word (string)
create data for the archival calendars. If the second arg (argsDate) is nil,
+
local ordinals = {
then formatDate assumes the current date/time.
+
"first",  "second",  "third",
--]]
+
"fourth",  "fifth",    "sixth",
 +
"seventh", "eighth",  "ninth",
 +
"tenth",  "eleventh", "twelfth and final",
 +
}
 +
return ordinals[dateInfo.monthNumber]
 +
end
  
-- Gets date data.
+
function dateFuncs.beVerb()
local dateStuff = {}
+
-- If the month is the current month or a month in the future, then this
local lang = mw.language.getContentLanguage()
+
-- is the string "is"; otherwise, "was" (string)
dateStuff.argsDate = argsDate
+
if dateInfo.year > dateInfo.currentYear
--Year
+
or (
local year = lang:formatDate('Y', argsDate)
+
dateInfo.year == dateInfo.currentYear
year = tonumber(year)
+
and dateInfo.monthNumber >= dateInfo.currentMonthNumber
dateStuff.year = year
+
)
-- Month
+
then
local month = lang:formatDate('F', argsDate)
+
return 'is'
dateStuff.month = month
+
else
-- Month and year
+
return 'was'
local monthAndYear = lang:formatDate('F Y', argsDate)
+
end
local firstOfMonth = lang:formatDate('01-m-Y', argsDate)
+
end
dateStuff.monthAndYear = monthAndYear
 
-- Previous month and year
 
dateStuff.previousMonthAndYear = lang:formatDate('F Y', firstOfMonth .. ' -1 month')
 
-- Next month and year
 
dateStuff.nextMonthAndYear = lang:formatDate('F Y', firstOfMonth .. ' +1 month')
 
-- Day
 
local day = lang:formatDate('j', argsDate)
 
day = tonumber(day)
 
dateStuff.day = day
 
-- Days in month
 
local daysInMonth = lang:formatDate('j', firstOfMonth .. ' +1 month -1 day')
 
daysInMonth = tonumber(daysInMonth)
 
dateStuff.daysInMonth = daysInMonth
 
-- Weekday of the first day of the month
 
local firstWeekday = lang:formatDate('w', firstOfMonth) -- Sunday = 0, Saturday = 6
 
firstWeekday = tonumber(firstWeekday)
 
firstWeekday = firstWeekday + 1 -- Make compatible with Lua tables. Sunday = 1, Saturday = 7.
 
dateStuff.firstWeekday = firstWeekday
 
return dateStuff
 
end
 
  
function p.makeDayStrings(dateStuff)
+
function dateFuncs.leapDesc()
local calStrings = {}
+
-- The year's leap year status; either "common", "leap" or
local currentDay = dateStuff.day
+
-- "century leap" (string)
local isLinkworthy = p.isLinkworthy
+
local isLeapYear = tonumber(lang:formatDate('L', dateInfo.date)) == 1
local currentMonth = dateStuff.month
+
if isLeapYear and dateInfo.year % 400 == 0 then
local currentYear = dateStuff.year
+
return 'century leap'
local makeDayLink = p.makeDayLink
+
elseif isLeapYear then
for day = 1, dateStuff.daysInMonth do
+
return 'leap'
if dateStuff.argsDate or isLinkworthy(day, currentDay) then
 
calStrings[#calStrings + 1] = makeDayLink(day, currentMonth, currentYear)
 
 
else
 
else
calStrings[#calStrings + 1] = tostring(day)
+
return 'common'
 
end
 
end
 
end
 
end
return calStrings
+
 
 +
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
 
end
  
function p.isLinkworthy(day, currentDay)
+
--------------------------------------------------------------------------------
-- Returns true if the calendar day should be linked, and false if not.
+
-- Exports
-- Days should be linked if they are the current day or if they are within the six
+
--------------------------------------------------------------------------------
-- preceding days, as that is the number of items on the current events page.
+
 
if currentDay - 6 <= day and day <= currentDay then
+
local p = {}
return true
+
 
else
+
function p.main(frame)
return false
+
-- 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
 
end
end
 
  
function p.makeDayLink(day, month, year)
+
-- Convert the dateInfo table values into arguments to pass to the current
return string.format("'''[[#%d %s %d|&nbsp;&nbsp;%d&nbsp;&nbsp;]]'''", year, month, day, day)
+
-- 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.
-- Generates the calendar HTML.
+
p.getDateInfo = getDateInfo
local monthAndYear = dateStuff.monthAndYear
 
local root = mw.html.create('table')
 
-- The next two lines help to make the table-layout-based Archive pages look good. When the
 
-- Archives have been converted to a grid-based layout, this logic can be removed, and the
 
-- corressponding CSS margin attribute can be simplified.
 
local temporaryMarginAdjustment = "auto !important"
 
if dateStuff.argsDate then temporaryMarginAdjustment = "8px 0 0 8px" end
 
root
 
-- Intro paragraph
 
:tag('p')
 
:wikitext("'''[[" .. dateStuff.month .. "]] [[" .. dateStuff.year .. "]]''' was the ninth month of that common year. The month, which began on a [[Thursday]], ended on a [[Friday]] 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