Module:Category series navigation
![]() | This Lua module is used on approximately 535,000 pages, or roughly 1% of all pages. To avoid major disruption and server load, any changes should be tested in the module's /sandbox or /testcases subpages, or in your own module sandbox. The tested changes can be added to this page in a single edit. Consider discussing changes on the talk page before implementing them. |
![]() | This module depends on the following other modules: |
Related pages |
---|
About
{{Category series navigation}} is intended to be a minimal-input, near-universal template for automatically navigating most numerically adjacent categories.
Type | Example category | BC(E)? | Example output |
---|---|---|---|
Season | 2001–02 FA Cup | No | Script error: The function "csn" does not exist. |
TV season | Futurama season 1 episodes | – | Script error: The function "csn" does not exist. |
Office term (regular) | MEPs 2004–2009 | No | Script error: The function "csn" does not exist. |
Office term (irregular) | Wales AMs 2003–2007 | No | Script error: The function "csn" does not exist. |
Numerical range | Taxonbars with 30–34 taxon IDs | – | |
Decade | 1990s in Scotland | BC | Script error: The function "csn" does not exist. |
Year | 1999 in Scotland | BC(E) | Script error: The function "csn" does not exist. |
Year (auto-condensed) | Candidates in the 2000 US presidential election | – | Script error: The function "csn" does not exist. |
Year (|skip-gaps=yes ) |
Amusement parks opened in 1877 | – | Script error: The function "csn" does not exist. |
Ordinal (temporal) | 2nd-century rabbis | BC(E) | Script error: The function "csn" does not exist. |
Ordinal (numeric) | 9th Lok Sabha | – | Script error: The function "csn" does not exist. |
Ordinal (word) | First Dynasty of Egypt | – | Script error: The function "csn" does not exist. |
Roman numeral | Deputies of Legislature X of the Kingdom of Italy | – | Script error: The function "csn" does not exist. |
Mixed decade | 1760s in the Province of Quebec (1763–1791) | – | Script error: The function "csn" does not exist. |
Mixed year | 1778 establishments in the Province of Quebec (1763–1791) | – | Script error: The function "csn" does not exist. |
Searching behavior
Most multi-year seasons/office terms/numerical ranges are acceptable, as long as the season duration/term length/range size is <= Script error: The function "csn" does not exist., and the gap between seasons is <= Script error: The function "csn" does not exist.. For series exceeding either of these criteria, see/use {{Irregular category series navigation}}.
The length of each season is automatically determined from the originating category name, up to and including Script error: The function "csn" does not exist. years. MOS:DATERANGE compliance is preferred, but some deviation is allowed and tracked for regular series with seasons > 1 year long. {{Category redirect}}s are followed, and tracked for either MOS contravention (to be corrected) or for navigational aid (no error). The gap size between successive seasons is also automatically determined, up to and including Script error: The function "csn" does not exist. years, and defaults to 0 (e.g. 1995–96 → 1996–97).
Automatically condensed years are supported for presidential categories only (but can be easily expanded as needed), for gaps up to and including Script error: The function "csn" does not exist. years, and defaults to 1. To skip gaps of up to Script error: The function "csn" does not exist. years in any year categories, use |skip-gaps=yes
.
Limitations
Numerical limitations and AD/BC/E
- Season/office term categories do not work for any years BC, which will be hidden, because no working examples were found.
- Decade categories recognize BC, but not BCE, because no working examples were found.
- Ordinal & numeral words do not work above the ninety-ninth & ninety-nine, because no working examples were found.
Condensation
- Automatically condensed Olympics display is not supported due to peculiarities; use {{Winter Olympics by year category navigation}}, etc., instead.
- Automatically condensed years are supported for US presidential categories only, due to their consistency; use
|skip-gaps=yes
as desired on other year categories. |skip-gaps=yes
currently only works when starting on a year category, and is not intended to find all hyphenated ranges, which allows it to span much larger gaps.
Work-arounds
- Base-name changes: create at least 2 logically numbered {{R from category navigation}} (1 backward & 1 forward), to join both related series.
- Unaccounted-for name+number conventions: where a fixed number is part of the prefix or suffix text, e.g. Chapter 11 bankruptcies, a non-breaking space may force the template to work. See this fix, where {{title year}} skipped over 11 as part of a word rather than a discrete number. (This case has been accounted for and is no longer required in this example.)
- General: for large, permanent gaps† between successive categories, use {{Preceding category}}, {{Category pair}}, {{Succeeding category}}, as needed, in addition to {{Category series navigation}} on both sides, or in the middle, of the gap. Even if {{Category series navigation}} is isolated, it has the benefit of confirming the absence of nearby categories to the reader or maintainer.
†Permanent gaps, where there is a confirmed permanent absence of data, and not just a temporary, yet to be filled, gap on Wikipedia. |skip-gaps=
: create {{R from category navigation}} from an appropriate year to the hyphenated category that was not found.
Related CfDs
- Wikipedia:Categories for discussion/Log/2019 April 19#Category:Aircraft piston engines 1900–1909
- Wikipedia:Categories for discussion/Log/2019 May 29#Category:MEPs 1952–58
- Wikipedia:Categories for discussion/Log/2019 June 8#Category:Northern Ireland MLAs 2016–17
Usage
- Typical usage
{{Category series navigation}}
- Specify a minimum and/or maximum year to display
{{Category series navigation|min=-100}}
{{Category series navigation|min=100 BC}}
{{Category series navigation|min=1753|max=1810}}
{{Category series navigation|max=2030}}
- To skip gaps in year categories
{{Category series navigation|skip-gaps=yes}}
- To not automatically follow {{Category redirect}}s
{{Category series navigation|follow-redirects=no}}
- Exceptional cases
{{Category series navigation|cat=2010s albums}}
— to behave as if placed on|cat=
; consider using {{Category pair}} instead of|cat=
Testing & debugging
To test the output of the template on a particular category name, use the |testcase=
parameter, and |testcasegap=
if necessary:
{{Category series navigation|testcase=1770s in the Province of Quebec (1763–1791)}}
→
Script error: The function "csn" does not exist.
To see all links produced and/or tested, and what effect each has on their display, use |list-all-links=yes
:
{{Category series navigation|testcase=Nations at the 2013 World Athletics Championships|min=2008|skip-gaps=yes|list-all-links=yes}}
→
- Category:Nations at the 2006 World Athletics Championships (2006) ( )
- Category:Nations at the 2007 World Athletics Championships (2007) ( )
- Category:Nations at the 2008 World Athletics Championships (2008)
- Category:Nations at the 2008–2009 World Athletics Championships (2008–2009) (tried; not displayed)2
- Category:Nations at the 2008–09 World Athletics Championships (2008–09) (tried; not displayed)4
- Category:Nations at the 2009 World Athletics Championships → Category:Nations at the 2009 World Championships in Athletics (2009)
- Category:Nations at the 2011 World Athletics Championships → Category:Nations at the 2011 World Championships in Athletics (2011)
- Category:Nations at the 2015 World Athletics Championships → Category:Nations at the 2015 World Championships in Athletics (2015)
- Category:Nations at the 2017 World Athletics Championships → Category:Nations at the 2017 World Championships in Athletics (2017)
- Category:Nations at the 2019 World Athletics Championships (2019)
- Category:Nations at the 2020 World Athletics Championships (2020)
- Category:Nations at the 2020–2021 World Athletics Championships (2020–2021) (tried; not displayed)2
- Category:Nations at the 2020–21 World Athletics Championships (2020–21) (tried; not displayed)4
- Category:Nations at the 2021 World Athletics Championships (2021)
- Category:Nations at the 2021–2022 World Athletics Championships (2021–2022) (tried; not displayed)2
- Category:Nations at the 2021–22 World Athletics Championships (2021–22) (tried; not displayed)4
- All possible element types are shown above (blue, red/grey, hidden, and redirect), and would otherwise display as:
If |list-all-links=yes
is used on a hyphenated category, then all tested categories will also be shown:
{{Category series navigation|cat=2018–19 NHL season|list-all-links=yes}}
→
Extended content
|
---|
Tracking categories
If the template encounters an issue, it displays an error message and/or places the category into one or more of the following tracking categories:
Maintenance required
- Category:Category series navigation failed to generate navbox (3)
- Category:Category series navigation redirection error (0)
- Category:Category series navigation range abbreviated (MOS) (0)
- Category:Category series navigation range redirected (MOS) (3)
- Category:Category series navigation range ends (blank, MOS) (4)
- Category:Category series navigation range not using en dash (0)
- Category:Category series navigation in mainspace (1)
Maintenance possible
- Category:Category series navigation isolated (1,889)
- Category:Category series navigation default season gap size (145)
- Category:Category series navigation using cat parameter (494)
- Category:Category series navigation using testcase parameter (0)
- Category:Category series navigation using unknown parameter (0)
Module maintenance possible
Tracking only
- Category:Category series navigation range redirected (base change) (368)
- Category:Category series navigation range redirected (var change) (0)
- Category:Category series navigation range redirected (end) (0)
- Category:Category series navigation range gaps (3,755)
- Category:Category series navigation range irregular (828)
- Category:Category series navigation range irregular, 0-length (2,149)
- Category:Category series navigation range ends (present) (9)
- Category:Category series navigation TV season redirected (0)
- Category:Category series navigation decade redirected (8,211)
- Category:Category series navigation year redirected (base change) (2,892)
- Category:Category series navigation year redirected (var change) (100)
- Category:Category series navigation roman numeral redirected (0)
- Category:Category series navigation nordinal redirected (3,887)
- Category:Category series navigation wordinal redirected (15)
- Category:Category series navigation using skip-gaps parameter (305,750)
- Category:Category series navigation year and range (635)
- Category:Category series navigation year and decade (290,995)
- Category:Category series navigation decade and century (47,425)
See also
- {{Irregular category series navigation}}—for use on categories
- {{Irregular series navigation}}—for use outside categories
- {{R from category navigation}}
- {{Category TOC custom}}
local p = {}
--[[==========================================================================]]
--[[ Utility functions ]]
--[[==========================================================================]]
--Standardized error handling
function errorclass(msg)
return mw.text.tag( 'span', {class='error mw-ext-cite-error'}, '<b>Error!</b> '..msg )
end
--Make a piped link to a category, if it exists
--If it doesn't exist, just display the greyed the link title without linking
function makeCatLink(catname, disp)
local displaytext = nil
local greyLinkColor = '#888'
if (disp ~= '') and (disp ~= nil) then
--use 'disp' parameter, but strip any trailing disambiguator
displaytext = mw.ustring.gsub(disp, '%s+%(.+$', '');
else
displaytext = catname
end
local fmtlink
local catPage = mw.title.new( catname, 'Category' )
if (catPage.exists) then
fmtlink = '[[:Category:'..catname..'|'..displaytext..']]'
else
fmtlink = '<span style="color:'..greyLinkColor..'">'..displaytext..'span>'
end
return fmtlink
end
--[[==========================================================================]]
--[[ Formerly separated templates/modules ]]
--[[==========================================================================]]
--[[============================={{ navyear }}==============================]]
function navyear(arg1, arg2, arg3, arg4, arg5, frame)
--Expects a PAGENAME of the form "Some sequential 1760 example cat", where
-- {{{1}}}=Some sequential
-- {{{2}}}=1760
-- {{{3}}}=example cat
-- {{{4}}}=1758 ('min' year parameter; optional)
-- {{{5}}}=1800 ('max' year parameter; optional)
arg2 = tonumber(arg2)
arg4 = tonumber(arg4)
arg5 = tonumber(arg5)
if arg4 == nil then arg4 = -9999 end
if arg5 == nil then arg5 = 9999 end
local navyear = '{| class="toccolours hlist" style="text-align: center; margin: auto;"\n'..'|\n'
local i = -5
while i <= 5 do
local year = arg2 + i
if i ~= 0 then
if (year >= arg4) and (year <= arg5) then -- ex: 1758, 1759, 1761, 1762, 1763, 1764, 1765
navyear = navyear..'*'..makeCatLink( arg1..' '..year..' '..arg3, year )..'\n'
else -- ex: 1755, 1756, 1757
navyear = navyear..'*<span style="visibility:hidden">'..year..'</span>\n'
end
else -- ex: 1760
navyear = navyear..'*<b>'..arg2..'</b>\n'
end
i = i + 1
end
return navyear..'|}'
end
--[[============================{{ navdecade }}=============================]]
function navdecade(arg1, arg2, arg3, arg4, frame)
--Expects a PAGENAME of the form "Some sequential 2015 example cat", where
-- {{{1}}}=Some sequential
-- {{{2}}}=2015
-- {{{3}}}=example cat
--and
-- {{{4}}}=2010
--the penultimate decade to be shown, normally the current 4-digit decade.
local narg2 = tonumber(arg2)
local narg4 = tonumber(arg4)
local errors = '' --TODO: use local regex for these checks instead of template-expands
if string.len(arg2) ~= 4 then --expecting e.g. yyyy
errors = errorclass('Decade module at Navseasoncats/navdecade has been sent "'..arg2..'" but is expecting a 4-digit year ending in zero, of the form yyy0 - there should be 4 digits only. '..
'So e.g. the 2010s should be cut down to 2010.')
elseif string.len(arg4) ~= 4 then --expecting e.g. yyyy
errors = errorclass('Decade module at Navseasoncats/navdecade can\'t recognise a number in the "year" '..arg4..' that has been passed to it as the penultimate decade to be shown. '..
'It expects to be sent e.g. 2010.')
else
local strnumber = frame:expandTemplate{ title = 'Str number/trim', args = { arg2 } }
if string.len(strnumber) < string.len(arg2) then --if 4-digit but non-numeric, e.g. 201t
errors = errorclass('Decade module at Navseasoncats/navdecade can\'t recognise a number in the "year" '..arg2..' that has been passed to it. '..
'It expects to be sent e.g. 2010.')
if narg2 == nil then narg2 = tonumber(strnumber) end
else
local strrightc = frame:expandTemplate{ title = 'Str rightc', args = { arg4, 1 } }
if strrightc ~= '0' then --expecting 2010
errors = errorclass('Decade module at Navseasoncats/navdecade has been sent "'..arg4..'" but is expecting a 4-digit year ending in zero, of the form yyy0 - there should be no trailing s. '..
'So e.g. the 1990s should be cut down to 1990.')
end
if narg4 == nil then narg4 = tonumber( frame:expandTemplate{ title = 'Str number/trim', args = { arg4 } } ) end
end
end
if errors ~= '' then errors = errors..'\n' end
local nav = '{| class="toccolours" style="text-align: center; margin: auto;"\n'
local i = 0
while i <= 20 do
nav = nav..'|'
if narg2 > (narg4 - (1+i)) then
nav = nav..frame:expandTemplate{ title = 'LinkCatIfExists2', args = { arg1..' '..(narg2 - (70-i))..'s '..arg3, (narg2 - (70-i))..'s' } }..' •'
end
nav = nav..'\n'
i = i + 10
end
local j = 0
while j <= 30 do
nav = nav..'|'..frame:expandTemplate{ title = 'LinkCatIfExists2', args = { arg1..' '..(narg2 - (40-j))..'s '..arg3, (narg2 - (40-j))..'s' } }..' •\n'
j = j + 10
end
nav = nav..'|<b>'..arg2..'s</b>\n'..
'\n'..
'| • '..frame:expandTemplate{ title = 'LinkCatIfExists2', args = { arg1..' '..(narg2 + 10)..'s '..arg3, (narg2 + 10)..'s' } }..'\n'
local k = 0
while k <= 20 do
nav = nav..'|'
if narg2 < (narg4 - (1+k)) then
nav = nav..'• '..frame:expandTemplate{ title = 'LinkCatIfExists2', args = { arg1..' '..(narg2 + (20+k))..'s '..arg3, (narg2 + (20+k))..'s' } }
end
nav = nav..'\n'
k = k + 10
end
return errors..nav..'|}'
end
--[[============================{{ navhyphen }}=============================]]
function navhyphen(arg1, arg2, arg3, arg4, arg5, frame)
--Expects a PAGENAME of the form "Some sequential 2015–16 example cat", where
-- {{{1}}}=2015
-- {{{2}}}=–
-- {{{3}}}=16
-- {{{4}}}=Some sequential
-- {{{5}}}=example cat
-- For "1999-2000", "2000" must be truncated so {{{3}}} is "00".
local narg1 = tonumber(arg1)
local narg3 = tonumber(arg3)
local errors = '' --TODO: use local regex for these checks instead of template-expands
if string.len(arg3) > 2 then --expecting 16
errors = errorclass('Second part of season passed to Navseasoncats/navhyphen should only be two digits, not '..arg3..'. '..
'Also, e.g. 1999-2000 should be cut down to 1999-00.')
else
local strnumber = frame:expandTemplate{ title = 'Str number/trim', args = { arg1 } }
if string.len(strnumber) < string.len(arg1) then --if 4-digit but non-numeric, e.g. 201t
errors = errorclass('Navseasoncats/navhyphen can\'t recognise the number '..arg1..' in the first part of the "season" that has been passed to it. '..
'For e.g. 2015–16, 16 is expected via |2015|–|16|.')
if narg1 == nil then narg1 = tonumber(strnumber) end
else
local strnumber = frame:expandTemplate{ title = 'Str number/trim', args = { arg3 } }
if string.len(strnumber) < string.len(arg3) then --if 2-digit but non-numeric, e.g. 1O
errors = errorclass('Navseasoncats/navhyphen can\'t recognise the number '..arg3..' in the second part of the "season" that has been passed to it. '..
'For e.g. 2015–16, 16 is expected via |2015|–|16|.')
end
end
end
if narg1 == nil then narg1 = tonumber( frame:expandTemplate{ title = 'Str number/trim', args = { arg1 } } ) end
if narg3 == nil then narg3 = tonumber( frame:expandTemplate{ title = 'Str number/trim', args = { arg3 } } ) end
if errors ~= '' then errors = errors..'\n' end
local nav = '{| class="toccolours" style="text-align: center; margin: auto;"\n'
local i = 0
while i <= 2 do
local zero1 = ''
local cent1 = ''
if narg3 > (3-i) and narg3 < (13-i) then zero1 = '0' end
if narg3 > (3-i) then cent1 = tostring(narg3 - (3-i))
elseif narg3 == (3-i) then cent1 = mw.ustring.match(arg1, '^%s*(%d%d)')..'00'
else cent1 = tostring(narg3 + (97+i))
end
local zero2 = ''
local cent2 = ''
if narg3 > (2-i) and narg3 < (13-i) then zero2 = '0' end
if narg3 > (2-i) then cent2 = tostring(narg3 - (3-i))
else cent2 = tostring(narg3 + (97+i))
end
nav = nav..'|'..
frame:expandTemplate{
title = 'LinkCatIfExists2',
args = {
arg4..' '..(narg1 - (3-i))..arg2..zero1..cent1..' '..arg5,
(narg1 - (3-i))..arg2..zero2..cent2
}
}..' •'
nav = nav..'\n\n'
i = i + 1
end
nav = nav..'|<b>'..arg1..arg2..arg3..'</b> • \n\n'
local j = 0
while j <= 2 do
local zero1 = ''
local cent1 = ''
if narg3 < (9-j) or narg3 > (99-j) then zero1 = '0' end
if narg3 < (99-j) then cent1 = tostring(narg3 + (1+j))
elseif narg3 == (99-j) then
cent1 = tostring(tonumber(mw.ustring.match(arg1, '^%s*(%d%d)')) + 1)..'00'
else cent1 = tostring(narg3 - (99-j))
end
local zero2 = ''
local cent2 = ''
if narg3 < (9-j) or narg3 > (98-j) then zero2 = '0' end
if narg3 < (99-j) then cent2 = tostring(narg3 + (1+j))
else cent2 = tostring(narg3 - (99-j))
end
local bull = ''
if j < 2 then bull = ' •' end
nav = nav..'|'..
frame:expandTemplate{
title = 'LinkCatIfExists2',
args = {
arg4..' '..(narg1 + (1+j))..arg2..zero1..cent1..' '..arg5,
(narg1 + (1+j))..arg2..zero2..cent2
}
}..bull
nav = nav..'\n\n'
j = j + 1
end
return errors..nav..'|}'
end
--[[============================{{ var_season }}============================]]
function var_season(testcase, frame)
--Extracts e.g. 2015–16 or 2015 out of a string
local pagename, titleyear = nil
if testcase and testcase ~= '' then
pagename = testcase
titleyear = frame:expandTemplate{ title = 'Title year', args = { page = pagename } }
else
pagename = mw.title.getCurrentTitle().text
titleyear = frame:expandTemplate{ title = 'Title year', args = { pagename } }
end
local strleft4 = string.match(pagename, '^[%d%D]?[%d%D]?[%d%D]?[%d%D]?')
if titleyear == strleft4 then
local firstword = frame:expandTemplate{ title = 'First word', args = { pagename } }
return firstword
else
local pos = mw.ustring.find( pagename, titleyear, 1, true ) or 0
local posm1 = pos - 1
local strright = frame:expandTemplate{ title = 'Str right', args = { pagename, posm1 } }
local firstword = frame:expandTemplate{ title = 'First word', args = { strright } }
return firstword
end
end
--[[=========================={{ var_firsthalf }}===========================]]
function var_firsthalf(arg, frame)
--Extracts the part of the string before the year
local pagename, titleyear = nil
if arg then arg = mw.text.trim(arg) end
if arg and arg ~= '' then
pagename = arg
titleyear = frame:expandTemplate{ title = 'Title year', args = { page = pagename } }
else
pagename = mw.title.getCurrentTitle().text
titleyear = frame:expandTemplate{ title = 'Title year', args = { pagename } }
end
local strleft4 = string.match(pagename, '^[%d%D]?[%d%D]?[%d%D]?[%d%D]?')
if strleft4 == titleyear then
return ''
else
local pos = mw.ustring.find( pagename, titleyear, 1, true ) or 0
local posm2 = pos - 2
local out = frame:expandTemplate{ title = 'Str left', args = { pagename, posm2 } }
return mw.text.trim(out)
end
end
--[[==========================={{ var_lasthalf }}===========================]]
function var_lasthalf(arg, frame)
--Extracts the part of the string after the word with the year
local pagename, titleyear = nil
if arg then arg = mw.text.trim(arg) end
if arg and arg ~= '' then
pagename = arg
titleyear = frame:expandTemplate{ title = 'Title year', args = { page = pagename } }
else
pagename = mw.title.getCurrentTitle().text
titleyear = frame:expandTemplate{ title = 'Title year', args = { pagename } }
end
local pos = mw.ustring.find( pagename, titleyear, 1, true ) or 0
local posm1 = pos - 1
local strright = frame:expandTemplate{ title = 'Str right', args = { pagename, posm1 } }
local removefirstword = frame:expandTemplate{ title = 'Remove first word', args = { strright } }
return removefirstword
end
--[[==========================================================================]]
--[[ Main ]]
--[[==========================================================================]]
function p.navseasoncats(frame)
local testcase = frame.args['testcase'] or ''
local minyear = frame.args['min']
local maxyear = frame.args['max']
local varseason = var_season(testcase, frame)
local varfirsthalf = var_firsthalf(testcase, frame)
local varlasthalf = var_lasthalf(testcase, frame)
local hyphen = mw.ustring.match(varseason, '[–-]') --ascii 150 & 45 (ndash & keyboard hyphen)
local strleft = frame:expandTemplate{ title = 'Str left', args = { varseason, 4 } }
if string.len(varseason) > 5 and string.len(varseason) < 10 or hyphen then
local strmid = frame:expandTemplate{ title = 'Str mid', args = { varseason, 5, 1 } }
local strrightc = frame:expandTemplate{ title = 'Str rightc', args = { varseason, 2 } }
return navhyphen( strleft, strmid, strrightc, varfirsthalf, varlasthalf, frame )
elseif string.len(varseason) == 4 then
return navyear( varfirsthalf, strleft, varlasthalf, minyear, maxyear, frame )
else
local strmid = frame:expandTemplate{ title = 'Str mid', args = { varseason, 4, 2 } }
if string.len(varseason) == 5 and strmid == '0s' then
local year = mw.getContentLanguage():formatDate( 'Y' )
local decade = mw.ustring.match(year, '^(%d%d%d)')..'0'
return navdecade( varfirsthalf, strleft, varlasthalf, decade )
else
return '<nowiki>***Navseasoncats failed to generate navbox***</nowiki>[[Category:Navseasoncats failed to generate navbox]]'
end
end
end
return p