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

From blackwiki
Jump to navigation Jump to search
blackwiki>RossO
(Initial code for Structural injection)
m (21 revisions imported)
 
(4 intermediate revisions by 2 users not shown)
Line 1: Line 1:
 +
-- This module generates the monthly archives [[Portal:Current events]].
 +
-- See a sample archive at [[Portal:Current events/September 2011]].
  
-- This module renders the introductory content on Monthly archive pages for the [[Portal:Current events]].
+
--------------------------------------------------------------------------------
 +
-- 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
  
Incoming expected variables:
+
-- Make an ordinal number from an integer.
frame.args.year = Integer value for year
+
local function makeOrdinalNumber(num)
frame.args.month = Integer value for month, 1 based.
+
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
  
Preview this against:
+
-- Try to parse the year and month from the current title.
Portal:Current events/September 2011/Sandbox
+
-- This template is usually used on pages with titles like
User:RossO/sandbox/Current events monthly intro
+
-- [[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
 +
--------------------------------------------------------------------------------
  
local p = {}
+
-- 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 p.main(frame)
+
function dateFuncs.currentYear()
local isArchive = false
+
-- The current year (number)
if (frame and frame.args and frame.args.year and frame.args.month) then
+
return tonumber(os.date('%Y'))
isArchive = true
 
 
end
 
end
local argsYear = tonumber(frame.args.year or os.date( "%Y" ))
 
local argsMonth = tonumber(frame.args.month or os.date( "%m" ))
 
  
local dateStuff = p.getDateStuff(argsMonth, argsYear, isArchive)
+
function dateFuncs.currentMonthNumber()
return p.exportPageIntro(dateStuff)
+
-- The current month (number)
end
+
return tonumber(os.date('%m'))
 +
end
  
function p.archiveNote(frame)
+
function dateFuncs.year()
local isArchive = false
+
-- The year (number)
if (frame and frame.args and frame.args.year and frame.args.month) then
+
return tonumber(year) or dateInfo.currentYear
isArchive = true
 
 
end
 
end
local argsYear = tonumber(frame.args.year or os.date( "%Y" ))
 
local argsMonth = tonumber(frame.args.month or os.date( "%m" ))
 
  
local dateStuff = p.getDateStuff(argsMonth, argsYear, isArchive)
+
function dateFuncs.monthNumber()
return p.exportArchiveNote(dateStuff)
+
-- The month (number)
end
+
return tonumber(month) or dateInfo.currentMonthNumber
 +
end
  
function p.startPrimarySection(frame)
+
function dateFuncs.monthNumberZeroPadded()
local root = "<div style='display: flex;'>"
+
-- The month, zero-padded to two digits (string)
root = root .. "<div style='flex: auto; outline: green 3px dotted;'>"
+
return string.format('%02d', dateInfo.monthNumber)
return root
+
end
end
 
function p.endPrimarySection(frame)
 
local root = "</div>"
 
return root
 
end
 
function p.startSecondarySection(frame)
 
local root = "<div style='flex: 0 0 280px; outline: green 3px dotted;'>"
 
return root
 
end
 
function p.endSecondarySection(frame)
 
local root = "</div>"
 
root = root .. "</div>"
 
return root
 
end
 
  
--[[
+
function dateFuncs.date()
Purpose: Construct the dateStuff object. This object is just properties, used by multiple functions.
+
-- The date in YYYY-MM-DD format (string)
Reference: https://www.mediawiki.org/wiki/Help:Extension:ParserFunctions#.23time
+
return string.format(
NOTE: HTML/Wikitext string construction will always end with a blank space.
+
'%04d-%02d-01',
--]]
+
dateInfo.year, dateInfo.monthNumber
 +
)
 +
end
  
function p.getDateStuff(monthNumber, yearNumber, isArchive)
+
function dateFuncs.monthName()
 +
-- The month name, e.g. "September" (string)
 +
return lang:formatDate('F', dateInfo.date)
 +
end
  
-- Gets date data.
+
function dateFuncs.monthOrdinal()
local dateStuff = {}
+
-- The ordinal month as an English word (string)
local constructedDate = yearNumber .. "-" .. monthNumber .. "-01" -- Construct a date, YYY-M-DD format.
+
local ordinals = {
local lang = mw.language.getContentLanguage()
+
"first",  "second",  "third",
 +
"fourth",  "fifth",    "sixth",
 +
"seventh", "eighth",  "ninth",
 +
"tenth",  "eleventh", "twelfth and final",
 +
}
 +
return ordinals[dateInfo.monthNumber]
 +
end
  
local beVerb = "is"
+
function dateFuncs.beVerb()
if (isArchive) then
+
-- If the month is the current month or a month in the future, then this
beVerb = "was"
+
-- 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
 
end
-- Support for future tense 'will be' can go here.
 
dateStuff.beVerb = beVerb
 
  
--Year and Leap Year
+
function dateFuncs.leapDesc()
dateStuff.year = tonumber(yearNumber)
+
-- The year's leap year status; either "common", "leap" or
local leapBool = tonumber(lang:formatDate('L', constructedDate))
+
-- "century leap" (string)
dateStuff.leapBool = leapBool
+
local isLeapYear = tonumber(lang:formatDate('L', dateInfo.date)) == 1
local leapDesc = "common"
+
if isLeapYear and dateInfo.year % 400 == 0 then
if (leapBool == 1) then
+
return 'century leap'
leapDesc = "leap"
+
elseif isLeapYear then
if (math.fmod(yearNumber, 400) == 0) then
+
return 'leap'
leapDesc = "century leap"
+
else
 +
return 'common'
 
end
 
end
 
end
 
end
dateStuff.leapDesc = leapDesc
 
  
-- Month and Name
+
function dateFuncs.decadeNote()
local monthName = lang:formatDate('F', constructedDate)
+
-- 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
 +
 
 +
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"
  
--Decade/Century/Millennium Note
+
if year % 1000 == 0 and month == 12
local decadeFlag = false
+
or year % 1000 == 1 and month == 1
local decadeNote = ""
+
then
local decadeNumber = math.floor(yearNumber / 10) * 10
+
local millennium = makeOrdinalNumber(getMillennium(year))
local centuryCount = math.floor((yearNumber - 1) / 100) + 1 -- Fenceposts
+
local century = makeOrdinalNumber(getCentury(year))
local millenniumCount = math.floor((yearNumber - 1) / 1000) + 1 -- Fenceposts
+
return string.format(
local notePrefix = "It " .. beVerb .. " the "
+
--Millenniums always overlap centuries.
local noteInfix = "month of the "
+
"It %s the %s month of the [[%s millennium]] and the [[%s century]].",
local noteFirstOrLast = "first "
+
dateInfo.beVerb, firstOrLast, millennium, century
if (monthNumber == 12) then
+
)
noteFirstOrLast = "last "
+
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
 
end
local ordinalSuffix = {
+
 
"1st", "2nd", "3rd", "4th", "5th", "6th", "7th", "8th", "9th", "10th",
+
function dateFuncs.moonNote()
"11th", "12th", "13th", "14th", "15th", "16th", "17th", "18th", "19th", "20th",
+
-- If the month had no full moon, a note to that effect; otherwise the
"21st", "22nd", "23rd", "24th", "25th", "26th", "27th", "28th", "29th", "30th"
+
-- empty string (string)
}
+
if dateInfo.monthNumber == 2 then
if (math.fmod(yearNumber, 1000) == 0 and monthNumber == 12) then
+
-- https://www.quora.com/When-was-the-last-time-the-entire-month-of-February-passed-without-a-Full-Moon/answer/Alan-Marble
decadeNote = "[[" .. ordinalSuffix[millenniumCount] .. " millennium]] "
+
local year = dateInfo.year
decadeNote = decadeNote .. "and the " --Millenniums always overlap centuries.
+
if year == 1961
decadeFlag = true
+
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
 
end
if (math.fmod(yearNumber, 1000) == 1 and monthNumber == 1) then
+
 
decadeNote = "[[" .. ordinalSuffix[millenniumCount] .. " millennium]] "
+
function dateFuncs.firstDayOfMonth()
decadeNote = decadeNote .. "and the " --Millenniums always overlap centuries.
+
-- Weekday of the first day of the month, e.g. "Tuesday" (string)
decadeFlag = true
+
return lang:formatDate('l', dateInfo.date)
end
 
if (math.fmod(yearNumber, 100) == 0 and monthNumber == 12) then
 
decadeNote = decadeNote .. "[[" .. ordinalSuffix[centuryCount] .. " century]]. "
 
decadeFlag = true
 
end
 
if (math.fmod(yearNumber, 100) == 1 and monthNumber == 1) then
 
decadeNote = decadeNote .. "[[" .. ordinalSuffix[centuryCount] .. " century]]. "
 
decadeFlag = true
 
 
end
 
end
  
if (math.fmod(yearNumber, 10) == 9 and monthNumber == 12) then
+
function dateFuncs.lastDayOfMonth()
decadeNote = "[[" .. decadeNumber .. "s]] decade. "
+
-- Weekday of the last day of the month, e.g. "Thursday" (string)
decadeFlag = true
+
return lang:formatDate('l', dateInfo.date .. ' +1 month -1 day')
end
 
if (math.fmod(yearNumber, 10) == 0 and monthNumber == 1) then
 
decadeNote = "[[" .. decadeNumber .. "s]] decade. "
 
decadeFlag = true
 
 
end
 
end
  
dateStuff.decadeNote = notePrefix .. noteFirstOrLast .. noteInfix .. decadeNote
+
function dateFuncs.daysInMonth()
if (decadeFlag == false) then
+
-- Number of days in the month (number)
dateStuff.decadeNote = ""
+
return tonumber(lang:formatDate(
 +
'j',
 +
dateInfo.date .. ' +1 month -1 day')
 +
)
 
end
 
end
  
 
+
function dateFuncs.mainContent()
--Moon Note
+
-- The rendered content of all the current events portal pages for the
local moonNote = ""
+
-- month (string)
if (monthNumber == 2) then
+
local ret = {}
-- https://www.quora.com/When-was-the-last-time-the-entire-month-of-February-passed-without-a-Full-Moon/answer/Alan-Marble
+
local frame = mw.getCurrentFrame()
if (yearNumber == 1961 or yearNumber == 1999 or yearNumber == 2018 or yearNumber == 2037 or yearNumber == 2067 or yearNumber == 2094) then
+
local year = dateInfo.year
moonNote = "This month had no full moon. "
+
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
 
end
 +
return table.concat(ret, '\n')
 
end
 
end
dateStuff.moonNote = moonNote
 
  
-- First Day of the Month
+
return dateInfo
local firstOfMonth = lang:formatDate('01-m-Y', constructedDate)
+
end
  
-- Weekday of the first day of the month
+
--------------------------------------------------------------------------------
local firstDayOfMonthName = lang:formatDate('l', firstOfMonth)
+
-- Exports
dateStuff.firstDayOfMonthName = firstDayOfMonthName
+
--------------------------------------------------------------------------------
  
-- Days in month
+
local p = {}
local daysInMonth = lang:formatDate('j', firstOfMonth .. ' +1 month -1 day')
 
dateStuff.daysInMonth = tonumber(daysInMonth)
 
  
-- Weekday of the last day of the month
+
function p.main(frame)
local lastDayOfMonthName = lang:formatDate('l', firstOfMonth .. ' +1 month -1 day')
+
-- Get the arguments
dateStuff.lastDayOfMonthName = lastDayOfMonthName
+
local args = require('Module:Arguments').getArgs(frame, {
 +
wrappers = 'Template:Current events monthly archive',
 +
})
 +
local year = tonumber(args.year)
 +
local month = tonumber(args.month)
  
return dateStuff
+
-- Validate the arguments
end
+
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
  
function p.exportPageIntro(dateStuff)
+
-- 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
Purpose: Construct WikiMarkup for the Intro paragraph.
+
-- events monthly archive display template
Reference: https://www.mediawiki.org/wiki/Help:Extension:ParserFunctions#.23time
+
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
  
local monthOrdinal = {"first", "second", "third", "fourth", "fifth", "sixth", "seventh", "eighth", "ninth", "tenth", "eleventh", "twelfth and final"}
+
-- Expand the display template with the arguments from dateInfo, and return
local root = mw.html.create('div')
+
-- it
:tag('p')
+
return frame:expandTemplate{
:wikitext("'''''[[" .. dateStuff.monthName .. "]] [[" .. dateStuff.year .. "]]''' " .. dateStuff.beVerb .. " the " .. monthOrdinal[dateStuff.monthNumber] .. " month of the " .. dateStuff.leapDesc .. " year. ")
+
title = 'Current events monthly archive/display',
:wikitext("" .. dateStuff.moonNote .. "" .. dateStuff.decadeNote .. "")
+
args = displayArgs,
:wikitext("The calendar for the month begins on a [[" .. dateStuff.firstDayOfMonthName .. "]] and ends on a [[" .. dateStuff.lastDayOfMonthName .. "]] after " .. dateStuff.daysInMonth .. " days.''")
+
}
:done()
 
 
return tostring(root)
 
 
end
 
end
  
function p.exportArchiveNote(dateStuff)
+
-- Export getDateInfo so that we can use it in unit tests.
 
+
p.getDateInfo = getDateInfo
--[[
 
Purpose: Construct WikiMarkup for the Archive Note heading and paragraph.
 
--]]
 
 
 
local root = mw.html.create('div')
 
: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 " .. dateStuff.monthName .. " " .. dateStuff.year .. ".''")
 
: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