Module:Current events monthly archive/sandbox
Appearance
This is the module sandbox page for Module:Current events monthly archive (diff). See also the companion subpage for test cases (run). |
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.
local function parseYearAndMonthFromCurrentTitle()
-- This template is used on archive pages that look like
-- [[Portal:Current events/September 2011]], so just try to use the
-- subpage name as a date.
local title = mw.title.getCurrentTitle()
local lang = mw.language.getContentLanguage()
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