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 | ||
TV season | Futurama season 1 episodes | – | ||
Office term (regular) | MEPs 2004–2009 | No | ||
Office term (irregular) | Wales AMs 2003–2007 | No | ||
Numerical range | Taxonbars with 30–34 taxon IDs | – | ||
Decade | 1990s in Scotland | BC | ||
Year | 1999 in Scotland | BC(E) | ||
Year (auto-condensed) | Candidates in the 2000 US presidential election | – | ||
Year (|skip-gaps=yes ) | Amusement parks opened in 1880 | – | ||
Ordinal (temporal) | 2nd-century rabbis | BC(E) | ||
Ordinal (numeric) | 9th Lok Sabha | – | ||
Ordinal (word) | First Dynasty of Egypt | – | ||
Roman numeral | Deputies of Legislature X of the Kingdom of Italy | – | ||
Mixed decade | 1760s in the Province of Quebec (1763–1791) | – | ||
Mixed year | 1778 establishments in the Province of Quebec (1763–1791) | – |
Searching behavior
Most multi-year seasons/office terms/numerical ranges are acceptable, as long as the season duration/term length/range size is <= 10, and the gap between seasons is <= 6. 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 10 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 6 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 5 years, and defaults to 1. To skip gaps of up to 30 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 presidential categories only, due to their consistency; use
|skip-gaps=yes
as desired elsewhere.
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 {{Succeeding category}}, {{Preceding category}}, {{Category pair}}, 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.
Related CfDs
- Wikipedia:Categories for discussion/Log/2019 June 8#Category:Northern Ireland MLAs 2016–17
- Wikipedia:Categories for discussion/Log/2019 May 29#Category:MEPs 1952–58
- Wikipedia:Categories for discussion/Log/2019 April 19#Category:Aircraft piston engines 1900–1909
Usage
- Typical usage
- 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=2025}}
- 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)|min=1760}}
→
{{Category series navigation|testcase=1770s in the Province of Quebec (1763–1791)|max=1790s}}
→
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:
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 (1)
- Category:Category series navigation redirection error (0)
- Category:Category series navigation range abbreviated (MOS) (0)
- Category:Category series navigation range redirected (MOS) (0)
- Category:Category series navigation range ends (blank, MOS) (0)
- Category:Category series navigation range not using en dash (0)
- Category:Category series navigation in mainspace (0)
Maintenance possible
- Category:Category series navigation isolated (1,363)
- Category:Category series navigation default season gap size (122)
- Category:Category series navigation using cat parameter (482)
- 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) (256)
- Category:Category series navigation range redirected (var change) (0)
- Category:Category series navigation range redirected (end) (0)
- Category:Category series navigation range gaps (2,810)
- Category:Category series navigation range irregular (618)
- Category:Category series navigation range irregular, 0-length (1,514)
- Category:Category series navigation range ends (present) (3)
- Category:Category series navigation TV season redirected (0)
- Category:Category series navigation decade redirected (7,285)
- Category:Category series navigation year redirected (base change) (2,513)
- Category:Category series navigation year redirected (var change) (45)
- Category:Category series navigation roman numeral redirected (0)
- Category:Category series navigation nordinal redirected (3,510)
- Category:Category series navigation wordinal redirected (9)
- Category:Category series navigation using skip-gaps parameter (286,076)
- Category:Category series navigation year and range (528)
- Category:Category series navigation year and decade (287,677)
- Category:Category series navigation decade and century (46,408)
See also
- {{Irregular category series navigation}}—for use on categories
- {{Irregular series navigation}}—for use outside categories
- {{Year by category}}
- {{R from category navigation}}
- {{Category TOC custom}}
require('strict')local p = {}local horizontal = require('Module:List').horizontal--[[==========================================================================]]--[[ Globals ]]--[[==========================================================================]]local currtitle = mw.title.getCurrentTitle()local nexistingcats = 0local errors = ''local testcasecolon = ''local testcases = string.match(currtitle.subpageText, '^testcases')if testcases then testcasecolon = ':' endlocal navborder = truelocal followRs = truelocal skipgaps = falselocal skipgaps_limit = 30local term_limit = 10local hgap_limit = 6local ygap_limit = 5local listall = falselocal tlistall = {}local tlistallbwd = {}local tlistallfwd = {}local ttrackingcats = { --when reindexing, Ctrl+H 'trackcat(13,' & 'ttrackingcats[16]''', -- [1] placeholder for [[Category:Category series navigation using cat parameter]]'', -- [2] placeholder for [[Category:Category series navigation using testcase parameter]]'', -- [3] placeholder for [[Category:Category series navigation using unknown parameter]]'', -- [4] placeholder for [[Category:Category series navigation range not using en dash]]'', -- [5] placeholder for [[Category:Category series navigation range abbreviated (MOS)]]'', -- [6] placeholder for [[Category:Category series navigation range redirected (base change)]]'', -- [7] placeholder for [[Category:Category series navigation range redirected (var change)]]'', -- [8] placeholder for [[Category:Category series navigation range redirected (end)]]'', -- [9] placeholder for [[Category:Category series navigation range redirected (MOS)]]'', --[10] placeholder for [[Category:Category series navigation range redirected (other)]]'', --[11] placeholder for [[Category:Category series navigation range gaps]]'', --[12] placeholder for [[Category:Category series navigation range irregular]]'', --[13] placeholder for [[Category:Category series navigation range irregular, 0-length]]'', --[14] placeholder for [[Category:Category series navigation range ends (present)]]'', --[15] placeholder for [[Category:Category series navigation range ends (blank, MOS)]]'', --[16] placeholder for [[Category:Category series navigation isolated]]'', --[17] placeholder for [[Category:Category series navigation default season gap size]]'', --[18] placeholder for [[Category:Category series navigation decade redirected]]'', --[19] placeholder for [[Category:Category series navigation year redirected (base change)]]'', --[20] placeholder for [[Category:Category series navigation year redirected (var change)]]'', --[21] placeholder for [[Category:Category series navigation year redirected (other)]]'', --[22] placeholder for [[Category:Category series navigation roman numeral redirected]]'', --[23] placeholder for [[Category:Category series navigation nordinal redirected]]'', --[24] placeholder for [[Category:Category series navigation wordinal redirected]]'', --[25] placeholder for [[Category:Category series navigation TV season redirected]]'', --[26] placeholder for [[Category:Category series navigation using skip-gaps parameter]]'', --[27] placeholder for [[Category:Category series navigation year and range]]'', --[28] placeholder for [[Category:Category series navigation year and decade]]'', --[29] placeholder for [[Category:Category series navigation decade and century]]'', --[30] placeholder for [[Category:Category series navigation in mainspace]]'', --[31] placeholder for [[Category:Category series navigation redirection error]]}local avoidself = (not string.match(currtitle.text, 'Category series navigation with') andnot string.match(currtitle.text, 'Category series navigation.*/doc') andnot string.match(currtitle.text, 'Category series navigation.*/sandbox') andcurrtitle.text ~= 'Category series navigation' andcurrtitle.nsText ~= 'User_talk' andcurrtitle.nsText ~= 'Template_talk' and(currtitle.nsText ~= 'Template' or testcases)) --avoid nested transclusion errors (i.e. {{Infilmdecade}})--[[==========================================================================]]--[[ Utility & category functions ]]--[[==========================================================================]]--Determine if a category exists (in a function for easier localization).local function catexists( title )return mw.title.new( title, 'Category' ).existsend--Error message handling.function p.errorclass( msg )return mw.text.tag( 'span', {class='error mw-ext-cite-error'}, '<b>Error!</b> '..string.gsub(msg, '&#', '&#') )end--Failure handling.function p.failedcat( errors, sortkey )if avoidself thenreturn (errors or '')..'***Category series navigation failed to generate navbox***'.. '[['..testcasecolon..'Category:Category series navigation failed to generate navbox|'..(sortkey or 'O')..']]\n'endreturn ''end--Tracking cat handling.--key: 15 (when reindexing ttrackingcats{}, Ctrl+H 'trackcat(13,' & 'ttrackingcats[16]')--cat: 'Category series navigation isolated'; '' to remove--Used by main, all nav_*(), & several utility functions.local function trackcat( key, cat )if avoidself and key and cat thenif cat ~= '' thenttrackingcats[key] = '[['..testcasecolon..'Category:'..cat..']]'elsettrackingcats[key] = ''endendreturnend--Check for unknown parameters.--Used by main only.local function checkforunknownparams( tbl )local knownparams = { --parameter whitelist['min'] = 'min',['max'] = 'max',['cat'] = 'cat',['show'] = 'show',['testcase'] = 'testcase',['testcasegap'] = 'testcasegap',['skip-gaps'] = 'skip-gaps',['list-all-links'] = 'list-all-links',['follow-redirects'] = 'follow-redirects',}for k, _ in pairs (tbl) doif knownparams[k] == nil thentrackcat(3, 'Category series navigation using unknown parameter')breakendendend--Check for nav_*() navigational isolation (not necessarily an error).--Used by all nav_*().local function isolatedcat()if nexistingcats == 0 thentrackcat(16, 'Category series navigation isolated')endend--Returns the target of {{Category redirect}}, if it exists, else returns the original cat.--{{Title year}}, etc., if found, are evaluated.--Used by catlinkfollowr(), and so indirectly by all nav_*().local function rtarget( frame, cat )local catcontent = mw.title.new( cat or '', 'Category' ):getContent()if string.match( catcontent or '', '{{ *[Cc]at' ) then --prelim testlocal getRegex = require('Module:Template redirect regex').mainlocal tregex = getRegex('Category redirect')for _, v in pairs (tregex) dolocal rtarget = mw.ustring.match( catcontent, v..'%s*|%s*([^|}]+)' )if rtarget thenif string.match(rtarget, '{{') then --{{Title year}}, etc., exists; evaluatelocal regex_ty = '%s*|%s*([^{}]*{{([^{|}]+)}}[^{}]-)%s*}}' --eval null-param templates only; expanded if/as neededlocal rtarget_orig, ty = mw.ustring.match( catcontent, v..regex_ty )if rtarget_orig thenlocal ty_eval = frame:expandTemplate{ title = ty, args = { page = cat } } --frame:newChild doesn't work, use 'page' param insteadlocal rtarget_eval = mw.ustring.gsub(rtarget_orig, '{{%s*'..ty..'%s*}}', ty_eval )return rtarget_evalelse --sub-parameters present; track & return defaulttrackcat(31, 'Category series navigation redirection error')endendrtarget = mw.ustring.gsub(rtarget, '^1%s*=%s*', '')rtarget = string.gsub(rtarget, '^[Cc]ategory:', '')return rtargetendend --forend --ifreturn catend--Similar to {{LinkCatIfExists2}}: make a piped link to a category, if it exists;--if it doesn't exist, just display the greyed link title without linking.--Follows {{Category redirect}}s.--Returns {--['cat'] = cat,--['catexists'] = true,--['rtarget'] = <#R target>,--['navelement'] = <#R target navelement>,--['displaytext'] = displaytext,-- }-- if #R followed;--returns {--['cat'] = cat,--['catexists'] = <true|false>,--['rtarget'] = nil,--['navelement'] = <cat navelement>,--['displaytext'] = displaytext,-- }-- otherwise.--Used by all nav_*().local function catlinkfollowr( frame, cat, displaytext, displayend )cat = mw.text.trim(cat or '')displaytext = mw.text.trim(displaytext or '')displayend = displayend or false --bool flag to override displaytext IIF the cat/target is terminal (e.g. "2021–present" or "2021–")local disp = catif displaytext ~= '' then --use 'displaytext' parameter if presentdisp = mw.ustring.gsub(displaytext, '%s+%(.+$', ''); --strip any trailing disambiguatorendlocal link, nilorRlocal exists = catexists(cat)if exists thennexistingcats = nexistingcats + 1if followRs thenlocal R = rtarget(frame, cat) --find & follow #Rif R ~= cat then --#R followednilorR = Rendif displayend thenlocal y, hyph, ending = mw.ustring.match(R, '^.-(%d+)([–-])(.*)$')if ending == 'present' thendisp = y..hyph..endingelseif ending == '' thendisp = y..hyph..'<span style="visibility:hidden">'..y..'</span>' --hidden y to match spacingendendlink = '[[:Category:'..R..'|'..disp..']]'elselink = '[[:Category:'..cat..'|'..disp..']]'endelselink = '<span class="categorySeriesNavigation-item-inactive">'..disp..'</span>'endif listall thenif nilorR then --#R followedtable.insert( tlistall, '[[:Category:'..cat..']] → '..'[[:Category:'..nilorR..']] ('..link..')' )else --no #Rtable.insert( tlistall, '[[:Category:'..cat..']] ('..link..')' )endendreturn {['cat'] = cat,['catexists'] = exists,['rtarget'] = nilorR,['navelement'] = link,['displaytext'] = disp,}end--Returns a numbered list of all {{Category redirect}}s followed by catlinkfollowr() -> rtarget().--For a nav_hyphen() cat, also returns a formatted list of all cats searched for & found, & all loop indices.--Used by all nav_*().local function listalllinks()local nl = '\n# 'local out = ''if currtitle.nsText == 'Category' thenerrors = p.errorclass('The <b><code>|list-all-links=yes</code></b> parameter/utility '..'should not be saved in category space, only previewed.')out = p.failedcat(errors, 'Z')endlocal bwd, fwd = '', ''if tlistallbwd[1] thenbwd = '\n\nbackward search:'..nl..table.concat(tlistallbwd, nl)endif tlistallfwd[1] thenfwd = '\n\nforward search:'..nl..table.concat(tlistallfwd, nl)endif tlistall[1] thenreturn out..nl..table.concat(tlistall, nl)..bwd..fwdelsereturn out..nl..'No links found!?'..bwd..fwdendend--Returns the difference b/w 2 ints separated by endash|hyphen, nil if error.--Used by nav_hyphen() only.local function find_duration( cat )local from, to = mw.ustring.match(cat, '(%d+)[–-](%d+)')if from and to thenif to == '00' then return nil end --doesn't follow MOS:DATERANGEif (#from == 4) and (#to == 2) then --1900-01to = string.match(from, '(%d%d)%d%d')..to --1900-1901elseif (#from == 2) and (#to == 4) then -- 01-1902from = string.match(to, '(%d%d)%d%d')..from --1901-1902endreturn (tonumber(to) - tonumber(from))endreturn 0end--Returns the ending of a terminal cat, and sets the appropriate tracking cat, else nil.--Used by nav_hyphen() only.local function find_terminaltxt( cat )local terminaltxt = nilif mw.ustring.match(cat, '%d+[–-]present$') thenterminaltxt = 'present'trackcat(14, 'Category series navigation range ends (present)')elseif mw.ustring.match(cat, '%d+[–-]$') thenterminaltxt = ''trackcat(15, 'Category series navigation range ends (blank, MOS)')endreturn terminaltxtend--Returns an unsigned string of the 1-4 digit decade ending in "0", else nil.--Used by nav_decade() only.local function sterilizedec( decade )if decade == nil or decade == '' thenreturn nilendlocal dec = string.match(decade, '^[-%+]?(%d?%d?%d?0)$') orstring.match(decade, '^[-%+]?(%d?%d?%d?0)%D')if dec thenreturn decelse--fix 2-4 digit decadelocal decade_fixed234 = string.match(decade, '^[-%+]?(%d%d?%d?)%d$') orstring.match(decade, '^[-%+]?(%d%d?%d?)%d%D')if decade_fixed234 thenreturn decade_fixed234..'0'end--fix 1-digit decadelocal decade_fixed1 = string.match(decade, '^[-%+]?(%d)$') orstring.match(decade, '^[-%+]?(%d)%D')if decade_fixed1 thenreturn '0'end--unfixablereturn nilendend--Check for nav_hyphen default gap size + isolatedcat() (not necessarily an error).--Used by nav_hyphen() only.local function defaultgapcat( bool )if bool and nexistingcats == 0 then--using "nexistingcats > 0" isn't as useful, since the default gap size obviously workedtrackcat(17, 'Category series navigation default season gap size')endend--12 -> 12th, etc.--Used by nav_nordinal() & nav_wordinal().function p.addord( i )if tonumber(i) thenlocal s = tostring(i)local tens = string.match(s, '1%d$')if tens then return s..'th' endlocal ones = string.match(s, '%d$')if ones == '1' then return s..'st'elseif ones == '2' then return s..'nd'elseif ones == '3' then return s..'rd' endreturn s..'th'endreturn iend--Returns the properly formatted central nav element.--Expects an integer i, and a catlinkfollowr() table.--Used by nav_decade() & nav_ordinal() only.local function navcenter( i, catlink )if i == 0 then --center nav elementif navborder == true thenreturn '<b>'..catlink.displaytext..'</b>'elsereturn '<b>'..catlink.navelement..'</b>'endelsereturn catlink.navelementendend--Wrap one or two navs in a <div> with ARIA attributes; add TemplateStyles--before it. This also aligns the navs in case some floating element (like a--portal box) breaks their alignment.--Used by main only.local function wrap( nav1, nav2 )local templatestyles = require("Module:TemplateStyles")("Module:Category series navigation/styles.css")local prepare = function (nav)if nav thennav = '\n'..navelsenav = ''endreturn navendreturn templatestyles..'<div class="categorySeriesNavigation" role="navigation" aria-label="Range">'..prepare(nav1)..prepare(nav2)..'\n</div>'end--[[==========================================================================]]--[[ Formerly separated templates/modules ]]--[[==========================================================================]]--[[==========================={{ nav_hyphen }}=============================]]local function nav_hyphen( frame, start, hyph, finish, firstpart, lastpart, minseas, maxseas, testgap )--Expects a PAGENAME of the form "Some sequential 2015–16 example cat", where--start = 2015--hyph = –--finish = 16 (sequential years can be abbreviated, but others should be full year, e.g. "2001–2005")--firstpart = Some sequential--lastpart = example cat--minseas = 1800 ('min' starting season shown; optional; defaults to -9999)--maxseas = 2000 ('max' starting season shown; optional; defaults to 9999; 2000 will show 2000-01)--testgap = 0 (testcasegap parameter for easier testing; optional)--sterilize startif string.match(start or '', '^%d%d?%d?%d?$') == nil then --1-4 digits, AD onlylocal start_fixed = mw.ustring.match(start or '', '^%s*(%d%d?%d?%d?)%D')if start_fixed thenstart = start_fixedelseerrors = p.errorclass('Function nav_hyphen can\'t recognize the number "'..(start or '')..'" '.. 'in the first part of the "season" that was passed to it. '.. 'For e.g. "2015–16", "2015" is expected via "|2015|–|16|".')return p.failedcat(errors, 'H')endendlocal nstart = tonumber(start)--en dash checkif hyph ~= '–' thentrackcat(4, 'Category series navigation range not using en dash') --nav still processable, but trackend--sterilize finish & check for weird parentslocal tgaps = {} --table of gap sizes found b/w terms { [<gap size found>] = 1 }local ttlens = {} --table of term lengths found w/i terms { [<term length found>] = 1 }local tirregs = {} --table of ir/regular-term-length cats' "from"s & "to"s foundlocal regularparent = trueif (finish == -1) or --"Members of the Scottish Parliament 2021–present" (finish == 0) --"Members of the Scottish Parliament 2021–"thenregularparent = falseif maxseas == nil or maxseas == '' thenmaxseas = start --hide subsequent rangesendif finish == -1 then trackcat(14, 'Category series navigation range ends (present)')else trackcat(15, 'Category series navigation range ends (blank, MOS)') endelseif (start == finish) and (ttrackingcats[16] ~= '') --nav_year found isolated; check for surrounding hyphenated terms (e.g. UK MPs 1974)thentrackcat(16, '') --reset for another check latertrackcat(13, 'Category series navigation range irregular, 0-length')ttlens[0] = 1 --calc ttlens for std cases belowregularparent = 'isolated'endif (string.match(finish or '', '^%d+$') == nil) and (string.match(finish or '', '^%-%d+$') == nil)thenlocal finish_fixed = mw.ustring.match(finish or '', '^%s*(%d%d?%d?%d?)%D')if finish_fixed thenfinish = finish_fixedelseerrors = p.errorclass('Function nav_hyphen can\'t recognize "'..(finish or '')..'" '.. 'in the second part of the "season" that was passed to it. '.. 'For e.g. "2015–16", "16" is expected via "|2015|–|16|".')return p.failedcat(errors, 'I')endelseif string.len(finish) >= 5 thenerrors = p.errorclass('The second part of the season passed to function nav_hyphen should only be four or fewer digits, not "'..(finish or '')..'". '.. 'See [[MOS:DATERANGE]] for details.')return p.failedcat(errors, 'J')endendlocal nfinish = tonumber(finish)--save sterilized parent range for easier lookup latertirregs['from0'] = nstarttirregs['to0'] = nfinish--sterilize min/maxlocal nminseas_default = -9999local nmaxseas_default = 9999local nminseas = tonumber(minseas) or nminseas_default --same behavior as nav_yearlocal nmaxseas = tonumber(maxseas) or nmaxseas_default --same behavior as nav_yearif nminseas > nstart then nminseas = nstart endif nmaxseas < nstart then nmaxseas = nstart endlocal lspace = ' ' --assume a leading space (most common)local tspace = ' ' --assume a trailing space (most common)if string.match(firstpart, '%($') then lspace = '' end --DNE for "Madrid city councillors (2007–2011)"-type catsif string.match(lastpart, '^%)') then tspace = '' end --DNE for "Madrid city councillors (2007–2011)"-type cats--calculate term length/intRAseason size & finishing yearlocal t = 1while t <= term_limit and regularparent == true dolocal nish = nstart + t --use switchADBC to flip this sign to work for years BC, if/when the time comesif (nish == nfinish) or (string.match(nish, '%d?%d$') == finish) thenttlens[t] = 1breakendif t == term_limit thenerrors = p.errorclass('Function nav_hyphen can\'t determine a reasonable term length for "'..start..hyph..finish..'".')return p.failedcat(errors, 'K')endt = t + 1end--apply MOS:DATERANGE to parentlocal lenstart = string.len(start)local lenfinish = string.len(finish)if lenstart == 4 and regularparent == true then --"2001–..."if t == 1 then --"2001–02" & "2001–2002" both allowedif lenfinish ~= 2 and lenfinish ~= 4 thenerrors = p.errorclass('The second part of the season passed to function nav_hyphen should be two or four digits, not "'..finish..'".')return p.failedcat(errors, 'L')endelse --"2001–2005" is required for t > 1; track "2001–05"; anything else = errorif lenfinish == 2 thentrackcat(5, 'Category series navigation range abbreviated (MOS)')elseif lenfinish ~= 4 thenerrors = p.errorclass('The second part of the season passed to function nav_hyphen should be four digits, not "'..finish..'".')return p.failedcat(errors, 'M')endendif finish == '00' then --full year required regardless of term lengthtrackcat(5, 'Category series navigation range abbreviated (MOS)')endend--calculate intERseason gap sizelocal hgap_default = 0 --assume & start at the most common case: 2001–02 -> 2002–03, etc.local hgap_limit_reg = hgap_limit --less expensive per-increment (inc x 4)local hgap_limit_irreg = hgap_limit --more expensive per-increment (inc x 23 = inc x (k_bwd + k_fwd) = inc x (12 + 11))local hgap_success = falselocal hgap = hgap_defaultwhile hgap <= hgap_limit_reg and regularparent == true do --verifylocal prevseason2 = firstpart..lspace..(nstart-t-hgap)..hyph..string.match(nstart-hgap, '%d?%d$') ..tspace..lastpartlocal nextseason2 = firstpart..lspace..(nstart+t+hgap)..hyph..string.match(nstart+2*t+hgap, '%d?%d$')..tspace..lastpartlocal prevseason4 = firstpart..lspace..(nstart-t-hgap)..hyph..(nstart-hgap) ..tspace..lastpartlocal nextseason4 = firstpart..lspace..(nstart+t+hgap)..hyph..(nstart+2*t+hgap)..tspace..lastpartif t == 1 then --test abbreviated range first, then full range, to be frugal with expensive functionsif catexists(prevseason2) or --use 'or', in case we're at the edge of the cat structure, catexists(nextseason2) or --or we hit a "–00"/"–2000" situation on one side catexists(prevseason4) or catexists(nextseason4)thenhgap_success = truebreakendelseif t > 1 then --test full range first, then abbreviated range, to be frugal with expensive functionsif catexists(prevseason4) or --use 'or', in case we're at the edge of the cat structure, catexists(nextseason4) or --or we hit a "–00"/"–2000" situation on one side catexists(prevseason2) or catexists(nextseason2)thenhgap_success = truebreakendendhgap = hgap + 1endif hgap_success == false thenhgap = tonumber(testgap) or hgap_default --tracked via defaultgapcat()end--preliminary scan to determine ir/regular spacing of nearby cats;--to limit expensive function calls, MOS:DATERANGE-violating cats are ignored;--an irregular-term-length series should follow "YYYY..hyph..YYYY" throughoutif hgap <= hgap_limit_reg then --also to isolate temp vars--find # of nav-visible ir/regular-term-length catslocal bwanchor = nstart --backward anchor/common yearlocal fwanchor = bwanchor + t --forward anchor/common yearif regularparent == 'isolated' thenfwanchor = bwanchorendlocal spangreen = '[<span style="color:green">j, g, k = ' --used for/when debugging via list-all-links=yeslocal spanblue = '<span style="color:blue">'local spanred = ' (<span style="color:red">'local span = '</span>'local lastg = nil --to check for run-on searcheslocal lastk = nil --to check for run-on searcheslocal endfound = false --switch used to stop searching forwardlocal iirregs = 0 --index of tirregs[] for j < 0, since search starts from parentlocal j = -3 --index of tirregs[] for j > 0 & pseudo nav positionwhile j <= 3 doif j < 0 then --search backward from parentlocal gbreak = false --switch used to break out of g-looplocal g = 0 --gap sizewhile g <= hgap_limit_irreg dolocal k = 0 --term length: 0 = "0-length", 1+ = normalwhile k <= term_limit dolocal from = bwanchor - k - glocal to = bwanchor - glocal full = mw.text.trim( firstpart..lspace..from..hyph..to..tspace..lastpart )if k == 0 thenif regularparent ~= 'isolated' then --+restrict to g == 0 if repeating year problems ariseto = '0-length'full = mw.text.trim( firstpart..lspace..from..tspace..lastpart )if catlinkfollowr( frame, full ).rtarget ~= nil then --#R followedtable.insert( tlistallbwd, spangreen..j..', '..g..', '..k..span..'] '..full..spanred..'#R ignored'..span..')' )full, to = '', '' --don't use/follow 0-length cat #Rs from nav_hyphen(); otherwise gets messyendendendif (k >= 1) or --the normal case; only continue k = 0 if 0-length found (to == '0-length') --ghetto "continue" (thx Lua) to avoid expensive searches for "UK MPs 1974-1974", etc.thentable.insert( tlistallbwd, spangreen..j..', '..g..', '..k..span..'] '..full )if (k == 1) and-- (g == 0 or g == 1) and --commented to match j>0 case ("1995–96 in Federal Republic of Yugoslavia basketball") (catexists(full) == false)then --allow bare-bones MOS:DATERANGE alternation, in case we're on a 0|1-gap, 1-year term serieslocal to2 = string.match(to, '%d%d$')if to2 and to2 ~= '00' then --and not at a century transition (i.e. 1999–2000)to = to2full = mw.text.trim( firstpart..lspace..from..hyph..to..tspace..lastpart )table.insert( tlistallbwd, spangreen..j..', '..g..', '..k..span..'] '..full )endendif catexists(full) thenif to == '0-length' thentrackcat(13, 'Category series navigation range irregular, 0-length')endtlistallbwd[#tlistallbwd] = spanblue..tlistallbwd[#tlistallbwd]..span..' (found)'ttlens[ find_duration(full) ] = 1tgaps[g] = 1iirregs = iirregs + 1tirregs['from-'..iirregs] = fromtirregs['to-'..iirregs] = tobwanchor = from --ratchet downif to ~= '0-length' thengbreak = truebreakelseg = 0 --soft-reset g, to keep stepping thru kj = j + 1 --save, but keep searching thru kif j > 0 then --(restore "> 3" if acts up) lest we keep searching bwd & finding 0-length cats ("MEPs for the Republic of Ireland 1973" & down)j = -1 --allow a normal, full search fwd after breakgbreak = truebreakendendelseif (j >= 0) and (lastg and lastk) and ((lastg >= hgap_limit_irreg) or(lastk >= term_limit))then --bwd search exhausted and/or done (runaway bwd search on "2018–19 FIA World Endurance Championship season")j = -1 --allow a normal, full search fwd after breakgbreak = truebreakendend --ghetto "continue"k = k + 1lastk = kend --while kif gbreak == true then break endg = g + 1lastg = gend --while gend --if j < 0if j > 0 and endfound == false then --search forward from parentlocal gbreak = false --switch used to break out of g-looplocal g = 0 --gap sizewhile g <= hgap_limit_irreg dolocal k = -2 --term length: -2 = "0-length", -1 = "2020–present", 0 = "2020–", 1+ = normalwhile k <= term_limit dolocal from = fwanchor + glocal to4 = fwanchor + k + g--override carefullylocal to2 = nil--last 2 digits of to4, IIF existsif k == -1 then to4 = 'present'--see if end-cat exists (present)elseif k == 0 then to4 = '' end--see if end-cat exists (blank)local full = mw.text.trim( firstpart..lspace..from..hyph..to4..tspace..lastpart )if k == -2 thenif regularparent ~= 'isolated' then --+restrict to g == 0 if repeating year problems ariseto4 = '0-length' --see if 0-length cat existsfull = mw.text.trim( firstpart..lspace..from..tspace..lastpart )if catlinkfollowr( frame, full ).rtarget ~= nil then --#R followedtable.insert( tlistallfwd, spangreen..j..', '..g..', '..k..span..'] '..full..spanred..'#R ignored'..span..')' )full, to4 = '', '' --don't use/follow 0-length cat #Rs from nav_hyphen(); otherwise gets messyendendendif (k >= -1) or --only continue k = -2 if 0-length found (to4 == '0-length') --ghetto "continue" (thx Lua) to avoid expensive searches for "UK MPs 1974-1974", etc.thentable.insert( tlistallfwd, spangreen..j..', '..g..', '..k..span..'] '..full )if (k == 1) and-- (g == 0 or g == 1) and --commented to let "2002–03 in Scottish women's football" find "2008–09 in Scottish women's football" (catexists(full) == false)then --allow bare-bones MOS:DATERANGE alternation, in case we're on a 0|1-gap, 1-year term seriesto2 = string.match(to4, '%d%d$')if to2 and to2 ~= '00' then --and not at a century transition (i.e. 1999–2000)full = mw.text.trim( firstpart..lspace..from..hyph..to2..tspace..lastpart )table.insert( tlistallfwd, spangreen..j..', '..g..', '..k..span..'] '..full )endendif catexists(full) thenif to4 == '0-length' thenif rtarget(frame, full) == full then --only use 0-length cats that don't #Rtrackcat(13, 'Category series navigation range irregular, 0-length')endendtirregs['from'..j] = fromtirregs['to'..j] = (to2 or to4)if (k == -1) or (k == 0) thenendfound = true --tentativeelse --k == { -2, > 0 }tlistallfwd[#tlistallfwd] = spanblue..tlistallfwd[#tlistallfwd]..span..' (found)'ttlens[ find_duration(full) ] = 1tgaps[g] = 1endfound = falseif to4 ~= '0-length' then --k > 0fwanchor = to4 --ratchet upgbreak = truebreak --only break on k > 0 b/c old end-cat #Rs still exist like "Members of the Scottish Parliament 2011–"else --k == -2j = j + 1 --save, but keep searching k's, in case "1974" → "1974-1979"if j > 3 then --lest we keep searching & finding 0-length cats ("2018 CONCACAF Champions League" & up)gbreak = truebreakendendendendend --ghetto "continue"k = k + 1lastk = kend --while kif gbreak == true then break endg = g + 1lastg = gend --while gend --if j > 0if (lastg and lastk) and (lastg > hgap_limit_irreg) and (lastk > term_limit)then --search exhaustedif j < 0 then j = 0 --bwd search exhausted; continue fwdelseif j > 0 then break end --fwd search exhaustedendj = j + 1end --while j <= 3end --if hgap <= hgap_limit_reg--begin navhyphenlocal navh = '<div class="toccolours categorySeriesNavigation-range">\n'local navlist = {}local terminalcat = false --switch used to hide future catslocal terminaltxt = nillocal i = -3 --nav positionwhile i <= 3 dolocal from = nstart + i*(t+hgap) --the logical, but not necessarily correct, 'from'if tirregs['from'..i] then --prefer the irregular term tablefrom = tonumber(tirregs['from'..i])else --fallback to lazy/naive 'from'if i > 0 and tirregs['from'..(i-1)] and tirregs['from'..(i-1)] >= fromthen --end of the line: avoid dups/past, and create reasonable grey'd rangeslocal greyto = tonumber(tirregs['to' .. (i-1)]) or -9999local greyfrom = tonumber(tirregs['from'..(i-1)]) or -9999local grey = greyto --prefer 'to'if greyfrom > greyto then grey = greyfrom end --'from' fallback, in case "1995–96", "1995-present", etc.if grey > -9999 thenif grey ~= greyto thenfrom = grey + t + hgap --account for missing/incomplete 'to'elsefrom = grey + hgapendtirregs['from'..i] = from --remembertirregs['to' .. i] = from + tendelseif i < 0 thenlocal greyfromlocal ii = 0while ii < 3 doii = ii + 1greyfrom = tonumber(tirregs['from'..(i+ii)])if greyfrom then break endendfrom = greyfrom - ii*(t+hgap)tirregs['from'..i] = from --remembertirregs['to' .. i] = from + tendendlocal from2 = string.match(from, '%d?%d$')local to = tostring(from+t)--the logical, naive range, butif tirregs['to'..i] then--prefer irregular term tableto = tirregs['to'..i]elseif regularparent == false and tirregs and i > 0 thento = tirregs['to-1']--special treatment for parent terminal cats, since they have no natural 'to'endlocal to2 = string.match(to, '%d?%d$')local tofinal = (to2 or '') --assume t=1 and abbreviated 'to' (the most common case)if t > 1 or --per MOS:DATERANGE (e.g. 1999-2004) (from2 - (to2 or from2)) > 0 --century transition exception (e.g. 1999–2000)thentofinal = (to or '') --default to the MOS-correct format, in case no fallbacks foundendif to == '0-length' thentofinal = toend--check existance of 4-digit, MOS-correct range, with abbreviation fallbackif tofinal ~= '0-length' thenif t > 1 and string.len(from) == 4 then --e.g. 1999-2004--determine which link exists (full or abbr)local full = firstpart..lspace..from..hyph..tofinal..tspace..lastpartif not catexists(full) thenlocal abbr = firstpart..lspace..from..hyph..to2..tspace..lastpartif catexists(abbr) thentofinal = (to2 or '') --rv to MOS-incorrect format; if full AND abbr DNE, then tofinal is still in its MOS-correct formatendendelseif t == 1 then --full-year consecutive ranges are also allowedlocal abbr = firstpart..lspace..from..hyph..tofinal..tspace..lastpart --assume tofinal is in abbr formatif not catexists(abbr) and tofinal ~= to thenlocal full = firstpart..lspace..from..hyph..to..tspace..lastpartif catexists(full) thentofinal = (to or '') --if abbr AND full DNE, then tofinal is still in its abbr format (unless it's a century transition)endendendend--populate navhif i ~= 0 then --left/right navhlocal orig = firstpart..lspace..from..hyph..tofinal..tspace..lastpartlocal disp = from..hyph..tofinalif tofinal == '0-length' thenorig = firstpart..lspace..from..tspace..lastpartdisp = fromendlocal catlink = catlinkfollowr(frame, orig, disp, true) --force terminal cat displayif terminalcat == false thenterminaltxt = find_terminaltxt( disp ) --also sets tracking catsterminalcat = (terminaltxt ~= nil)endif catlink.rtarget and avoidself then --a {{Category redirect}} was followed, figure out why--determine new term length & gap sizettlens[ find_duration( catlink.rtarget ) ] = 1if i > -3 thenlocal lastto = tirregs['to'..(i-1)]if lastto == nil thenlocal lastfrom = nstart + (i-1)*(t+hgap)lastto = lastfrom+t --use last logical 'from' to calc lasttoendif lastto thenlocal gapcat = lastto..'-'..from --dummy cat to calc withlocal gap = find_duration(gapcat) or -1--in case of nil,tgaps[ gap ] = 1--tgaps[-1] is ignoredendend--display/tracking handlinglocal base_regex = '%d+[–-]%d+'local origbase = mw.ustring.gsub(orig, base_regex, '')local rtarbase, rtarbase_success = mw.ustring.gsub(catlink.rtarget, base_regex, '')if rtarbase_success == 0 thenlocal base_regex_lax = '%d%d%d%d' --in case rtarget is a year catrtarbase, rtarbase_success = mw.ustring.gsub(catlink.rtarget, base_regex_lax, '')endlocal terminal_regex = '%d+[–-]'..(terminaltxt or '')..'$' --more manual ORs bc Lua regex suxif mw.ustring.match(orig, terminal_regex) thenorigbase = mw.ustring.gsub(orig, terminal_regex, '')endif mw.ustring.match(catlink.rtarget, terminal_regex) then--finagle/overload terminalcat type to set nmaxseas on 1st occurence onlyif terminalcat == false then terminalcat = 1 endlocal dummy = find_terminaltxt( catlink.rtarget ) --also sets tracking catsrtarbase = mw.ustring.gsub(catlink.rtarget, terminal_regex, '')endorigbase = mw.text.trim(origbase)rtarbase = mw.text.trim(rtarbase)if origbase ~= rtarbase thentrackcat(6, 'Category series navigation range redirected (base change)')elseif terminalcat == 1 thentrackcat(8, 'Category series navigation range redirected (end)')else --origbase == rtarbaselocal all4s_regex = '%d%d%d%d[–-]%d%d%d%d'local orig_all4s = mw.ustring.match(orig, all4s_regex)local rtar_all4s = mw.ustring.match(catlink.rtarget, all4s_regex)if orig_all4s and rtar_all4s thentrackcat(10, 'Category series navigation range redirected (other)')elselocal year_regex1 = '%d%d%d%d$'local year_regex2 = '%d%d%d%d[%s%)]'local year_rtar = mw.ustring.match(catlink.rtarget, year_regex1) or mw.ustring.match(catlink.rtarget, year_regex2)if orig_all4s and year_rtar thentrackcat(7, 'Category series navigation range redirected (var change)')elsetrackcat(9, 'Category series navigation range redirected (MOS)')endendendendif terminalcat then --true or 1if type(terminalcat) ~= 'boolean' then nmaxseas = from end --only want to do this onceterminalcat = true --done finagling/overloadingendif (from >= 0) and (nminseas <= from) and (from <= nmaxseas) thentable.insert(navlist, catlink.navelement)if terminalcat then nmaxseas = nminseas_default end --prevent display of future rangeselselocal hidden = '<span style="visibility:hidden">'..disp..'</span>'table.insert(navlist, hidden)if listall thentlistall[#tlistall] = tlistall[#tlistall]..' ('..hidden..')'endendelse --center navhif finish == -1 then finish = 'present'elseif finish == 0 then finish = '<span style="visibility:hidden">'..start..'</span>' endlocal disp = start..hyph..finishif regularparent == 'isolated' then disp = start endtable.insert(navlist, '<b>'..disp..'</b>')endi = i + 1end-- add the listnavh = navh..horizontal(navlist)..'\n'--tracking cats & finalizeif avoidself thenlocal igaps = 0 --# of diff gap sizes > 0 foundlocal itlens = 0 --# of diff term lengths foundfor s = 1, hgap_limit_reg do --must loop; #tgaps, #ttlens unreliableigaps = igaps + (tgaps[s] or 0)endfor s = 0, term_limit doitlens = itlens + (ttlens[s] or 0)endif igaps > 0 then trackcat(11, 'Category series navigation range gaps') endif itlens > 1 and ttrackingcats[13] == '' then --avoid duplication in "Category series navigation range irregular, 0-length"trackcat(12, 'Category series navigation range irregular')endendisolatedcat()defaultgapcat(not hgap_success)if listall thenreturn listalllinks()elsereturn navh..'</div>'endend--[[=========================={{ nav_tvseason }}============================]]local function nav_tvseason( frame, firstpart, tv, lastpart, maximumtv )--Expects a PAGENAME of the form "Futurama (season 1) episodes", where--firstpart = Futurama (season--tv = 1--lastpart = ) episodes--maximumtv = 7 ('max' tv season parameter; optional; defaults to 9999)tv = tonumber(tv)if tv == nil thenerrors = p.errorclass('Function nav_tvseason can\'t recognize the TV season number sent to its 2nd parameter.')return p.failedcat(errors, 'T')end--"(season 1) episodes" -> "season 1 episodes" following March 2024 RfC:--[[Wikipedia talk:Naming conventions (television)#Follow-up RfC on TV season article titles]]-- [[Special:Permalink/1216885280#Follow-up RfC on TV season article titles]]local tspace = ' ' --"season 1 episodes"local parenth_check = string.match(lastpart, '^%)')if parenth_check then tspace = '' end --accommodate old style "(season 1) episodes" just in caselocal maxtv = tonumber(maximumtv) or 9999 --allow +/- qualifierif maxtv < tv then maxtv = tv end --input error; maxtv should be >= parent--begin navtvseasonlocal navt = '<div class="toccolours categorySeriesNavigation-range">\n'local navlist = {}local i = -5 --nav positionwhile i <= 5 dolocal t = tv + iif i ~= 0 then --left/right navtlocal catlink = catlinkfollowr( frame, firstpart..' '..t..tspace..lastpart, t )if (t >= 1 and t <= maxtv) then --hardcode mintvif catlink.rtarget then --a {{Category redirect}} was followedtrackcat(25, 'Category series navigation TV season redirected')endtable.insert(navlist, catlink.navelement)elselocal hidden = '<span style="visibility:hidden">'..'0'..'</span>' --'0' to maintain dot spacingtable.insert(navlist, hidden)if listall thentlistall[#tlistall] = tlistall[#tlistall]..' ('..hidden..')'endendelse --center navttable.insert(navlist, '<b>'..tv..'</b>')endi = i + 1end-- add the listnavt = navt..horizontal(navlist)..'\n'isolatedcat()if listall thenreturn listalllinks()elsereturn navt..'</div>'endend--[[==========================={{ nav_decade }}=============================]]local function nav_decade( frame, firstpart, decade, lastpart, mindecade, maxdecade )--Expects a PAGENAME of the form "Some sequential 2000 example cat", where--firstpart = Some sequential--decade = 2000--lastpart = example cat--mindecade = 1800 ('min' decade parameter; optional; defaults to -9999)--maxdecade = 2020 ('max' decade parameter; optional; defaults to 9999)--sterilize declocal dec = sterilizedec(decade)if dec == nil thenerrors = p.errorclass('Function nav_decade was sent "'..(decade or '')..'" as its 2nd parameter, '..'but expects a 1 to 4-digit year ending in "0".')return p.failedcat(errors, 'D')endlocal ndec = tonumber(dec)--sterilize mindecade & determine AD/BClocal mindefault = '-9999'local mindec = sterilizedec(mindecade) --returns a tostring(unsigned int), or nilif mindec thenif string.match(mindecade, '-%d') or string.match(mindecade, 'BC')thenmindec = '-'..mindec --better +/-0 behavior with strings (0-initialized int == "-0" string...)endelseif mindec == nil and mindecade and mindecade ~= '' thenerrors = p.errorclass('Function nav_decade was sent "'..(mindecade or '')..'" as its 4th parameter, '..'but expects a 1 to 4-digit year ending in "0", the earliest decade to be shown.')return p.failedcat(errors, 'E')else --mindec == nilmindec = mindefault --tonumber() later, after error checksend--sterilize maxdecade & determine AD/BClocal maxdefault = '9999'local maxdec = sterilizedec(maxdecade) --returns a tostring(unsigned int), or nil + errorif maxdec thenif string.match(maxdecade, '-%d') or string.match(maxdecade, 'BC')then --better +/-0 behavior with strings (0-initialized int == "-0" string...),maxdec = '-'..maxdec --but a "-0" string -> tonumber() -> tostring() = "-0",end --and a "0" string -> tonumber() -> tostring() = "0"elseif maxdec == nil and maxdecade and maxdecade ~= '' thenerrors = p.errorclass('Function nav_decade was sent "'..(maxdecade or '')..'" as its 5th parameter, '..'but expects a 1 to 4-digit year ending in "0", the highest decade to be shown.')return p.failedcat(errors, 'F')else --maxdec == nilmaxdec = maxdefaultendlocal tspace = ' ' --assume trailing space for "1950s in X"-type catsif string.match(lastpart, '^-') then tspace = '' end --DNE for "1970s-related"-type cats--AD/BC switches & varslocal parentBC = string.match(lastpart, '^BC') --following the "0s BC" convention for all years BClastpart = mw.ustring.gsub(lastpart, '^BC%s*', '') --handle BC separately; AD never used--TODO?: handle BCE, but only if it exists in the wildlocal dec0to40AD = (ndec >= 0 and ndec <= 40 and not parentBC) --special behavior in this rangelocal switchADBC = 1 -- 1=AD parentif parentBC then switchADBC = -1 end -- -1=BC parent; possibly adjusted laterlocal BCdisp = ''local D = -math.huge --secondary switch & iterator for AD/BC transition--check non-default min/max more carefullyif mindec ~= mindefault thenif tonumber(mindec) > ndec*switchADBC thenmindec = tostring(ndec*switchADBC) --input error; mindec should be <= parentendendif maxdec ~= maxdefault thenif tonumber(maxdec) < ndec*switchADBC thenmaxdec = tostring(ndec*switchADBC) --input error; maxdec should be >= parentendendlocal nmindec = tonumber(mindec) --similar behavior to nav_year & nav_nordinallocal nmaxdec = tonumber(maxdec) --similar behavior to nav_nordinal--begin navdecadelocal bnb = '' --border/no borderif navborder == false then --for Category series navigation year and decadebnb = 'categorySeriesNavigation-range-transparent'endlocal navd = '<div class="toccolours categorySeriesNavigation-range '..bnb..'">\n'local navlist = {}local i = -50 --nav position x 10while i <= 50 dolocal d = ndec + i*switchADBClocal BC = ''BCdisp = ''if dec0to40AD thenif D < -10 thend = math.abs(d + 10) --b/c 2 "0s" decades exist: "0s BC" & "0s" (AD)BC = 'BC 'if d == 0 thenD = -10 --track 1st d = 0 use (BC)endelseif D >= -10 thenD = D + 10 --now iterate from 0s ADd = D --2nd d = 0 useendelseif parentBC thenif switchADBC == -1 then --parentBC looking at the BC side (the common case)BC = 'BC 'if d == 0 then --prepare to switch to the AD side on the next iterationswitchADBC = 1 --1st d = 0 use (BC)D = -10 --prependelseif switchADBC == 1 then --switched to the AD sideD = D + 10 --now iterate from 0s ADd = D --2nd d = 0 use (on first use)endendif BC ~= '' and ndec <= 50 thenBCdisp = ' BC' --show BC for all BC decades whenever a "0s" is displayed on the navend--determine target catlocal disp = d..'s'..BCdisplocal catlink = catlinkfollowr( frame, firstpart..' '..d..'s'..tspace..BC..lastpart, disp )if catlink.rtarget then --a {{Category redirect}} was followedtrackcat(18, 'Category series navigation decade redirected')end--populate left/right navdlocal shown = navcenter(i, catlink)local hidden = '<span style="visibility:hidden">'..disp..'</span>'local dsign = d --use d for display & dsign for logicif BC ~= '' then dsign = -dsign endif (nmindec <= dsign) and (dsign <= nmaxdec) thenif dsign == 0 and (nmindec == 0 or nmaxdec == 0) then --distinguish b/w -0 (BC) & 0 (AD)--"zoom in" on +/- 0 and turn dsign/min/max temporarily into +/- 1 for easier processinglocal zsign, zmin, zmax = 1, nmindec, nmaxdecif BC ~= '' then zsign = -1 endif mindec == '-0' then zmin = -1elseif mindec == '0' then zmin = 1 endif maxdec == '-0' then zmax = -1elseif maxdec == '0' then zmax = 1 endif (zmin <= zsign) and (zsign <= zmax) thentable.insert(navlist, shown)hidden = nilelsetable.insert(navlist, hidden)endelsetable.insert(navlist, shown)--the common casehidden = nilendelsetable.insert(navlist, hidden)endif listall and hidden thentlistall[#tlistall] = tlistall[#tlistall]..' ('..hidden..')'endi = i + 10end-- add the listnavd = navd..horizontal(navlist)..'\n'isolatedcat()if listall thenreturn listalllinks()elsereturn navd..'</div>'endend--[[============================{{ nav_year }}==============================]]local function nav_year( frame, firstpart, year, lastpart, minimumyear, maximumyear )--Expects a PAGENAME of the form "Some sequential 1760 example cat", where--firstpart = Some sequential--year = 1760--lastpart = example cat--minimumyear = 1758 ('min' year parameter; optional)--maximumyear = 1800 ('max' year parameter; optional)local minyear_default = -9999local maxyear_default = 9999year = tonumber(year) or tonumber(mw.ustring.match(year or '', '^%s*(%d*)'))local minyear = tonumber(string.match(minimumyear or '', '-?%d+')) or minyear_default --allow +/- qualifierlocal maxyear = tonumber(string.match(maximumyear or '', '-?%d+')) or maxyear_default --allow +/- qualifierif string.match(minimumyear or '', 'BC') then minyear = -math.abs(minyear) end --allow BC qualifier (AD otherwise assumed)if string.match(maximumyear or '', 'BC') then maxyear = -math.abs(maxyear) end --allow BC qualifier (AD otherwise assumed)if year == nil thenerrors = p.errorclass('Function nav_year can\'t recognize the year sent to its 2nd parameter.')return p.failedcat(errors, 'Y')end--AD/BC switches & varslocal yearBCElastparts = { --needed for parent = AD 1-5, when the BC/E format is unknown--"BCE" removed to match both AD & BCE cats; easier & faster than multiple string.match()s['example_Hebrew people_example'] = 'BCE', --example entry format; add to & adjust as needed}local parentAD = string.match(firstpart, 'AD$') --following the "AD 1" convention from AD 1 to AD 10local parentBC = string.match(lastpart, '^BCE?') --following the "1 BC" convention for all years BCfirstpart = mw.ustring.gsub(firstpart, '%s*AD$', '') --handle AD/BC separately for easier & faster accountinglastpart = mw.ustring.gsub(lastpart, '^BCE?%s*', '')local BCe = parentBC or yearBCElastparts[lastpart] or 'BC' --"BC" defaultlocal year1to10 = (year >= 1 and year <= 10)local year1to10ADBC = year1to10 and (parentBC or parentAD) --special behavior 1-10 for low-# non-year serieslocal year1to15AD = (year >= 1 and year <= 15 and not parentBC) --special behavior 1-15 for AD/BC displaylocal switchADBC = 1 -- 1=AD parentif parentBC then switchADBC = -1 end -- -1=BC parent; possibly adjusted laterlocal Y = 0 --secondary iterator for AD-on-a-BC-parentif minyear > year*switchADBC then minyear = year*switchADBC end --input error; minyear should be <= parentif maxyear < year*switchADBC then maxyear = year*switchADBC end --input error; maxyear should be >= parentlocal lspace = ' ' --leading space before year, after firstpartif string.match(firstpart, '[%-VW]$') thenlspace = '' --e.g. "Straight-8 engines"endlocal tspace = ' ' --trailing space after year, before lastpartif string.match(lastpart, '^-') thentspace = '' --e.g. "2018-related timelines"end--determine interyear gap size to condense special category types, if possiblelocal ygapdefault = 1 --assume/start at the most common case: 2001, 2002, etc.local ygap = ygapdefaultif string.match(lastpart, 'presidential') thenlocal ygap1, ygap2 = ygapdefault, ygapdefault --need to determine previous & next year gaps indepedentlylocal ygap1_success, ygap2_success = false, falselocal prevseason = nilwhile ygap1 <= ygap_limit do --Czech Republic, Poland, Sri Lanka, etc. have 5-year termsprevseason = firstpart..lspace..(year-ygap1)..tspace..lastpartif catexists(prevseason) thenygap1_success = truebreakendygap1 = ygap1 + 1endlocal nextseason = nilwhile ygap2 <= ygap_limit do --Czech Republic, Poland, Sri Lanka, etc. have 5-year termsnextseason = firstpart..lspace..(year+ygap2)..tspace..lastpartif catexists(nextseason) thenygap2_success = truebreakendygap2 = ygap2 + 1endif ygap1_success and ygap2_success thenif ygap1 == ygap2 then ygap = ygap1 endelseif ygap1_success then ygap = ygap1elseif ygap2_success then ygap = ygap2endend--skip non-existing years, if requestedlocal ynogaps = {} --populate with existing years in the range, at most, [year - (skipgaps_limit * 5), year + (skipgaps_limit * 5)]if skipgaps thenif minyear == minyear_default thenminyear = 0 --automatically set minyear to 0, as AD/BC not supported anywayendif (year > 70) or --add support for AD/BC (<= AD 10) if/when needed (minyear >= 0 and --must be a non-year series like "AC with 0 elements" not parentAD and not parentBC)thenlocal yskipped = {} --track skipped y's to avoid double-checkinglocal cat, found, Yeary --populate nav element queue outwards positively from the parentlocal Year = year --to save/ratchet progressionlocal i = 1while i <= 5 dolocal y = 1while y <= skipgaps_limit dofound = falseYeary = Year + yif yskipped[Yeary] == nil thenyskipped[Yeary] = Yearycat = firstpart..lspace..Yeary..tspace..lastpartfound = catexists(cat)if found then break endendy = y + 1endif found then Year = Yearyelse Year = Year + 1 endynogaps[i] = Yeari = i + 1endynogaps[0] = year --the parent--populate nav element queue outwards negatively from the parentYear = year --reset ratcheti = -1while i >= -5 dolocal y = -1while y >= -skipgaps_limit dofound = falseYeary = Year + yif yskipped[Yeary] == nil thenyskipped[Yeary] = Yearycat = firstpart..lspace..Yeary..tspace..lastpartfound = catexists(cat)if found then break endendy = y - 1endif found then Year = Yearyelse Year = Year - 1 endynogaps[i] = Yeari = i - 1endelseskipgaps = false --TODO: AD/BC support, then lift BC restrictions @ [[Template:Establishment category BC]] & [[Template:Year category header/core]]endend--begin navyearslocal navy = '<div class="toccolours categorySeriesNavigation-range">\n'local navlist = {}local ylocal j = 0 --decrementor for special cases "2021 World Rugby Sevens Series" -> "2021–2022"local i = -5 --nav positionwhile i <= 5 doif skipgaps theny = ynogaps[i]elsey = year + i*ygap*switchADBC - jendlocal BCdisp = ''if i ~= 0 then --left/right navylocal AD = ''local BC = ''if year1to15AD and not (year1to10 and not year1to10ADBC) --don't AD/BC 1-10's if parents don't contain AD/BCthenif year >= 11 then --parent = AD 11-15if y <= 10 then --prepend AD on y = 1-10 cats only, per existing catsAD = 'AD 'endelseif year >= 1 then --parent = AD 1-10if y <= 0 thenBC = BCe..' 'y = math.abs(y - 1) --skip y = 0 (DNE)elseif y >= 1 and y <= 10 then --prepend AD on y = 1-10 cats only, per existing catsAD = 'AD 'endendelseif parentBC thenif switchADBC == -1 then --displayed y is in the BC regimeif y >= 1 then --the common caseBC = BCe..' 'elseif y == 0 then --switch from BC to AD regimeswitchADBC = 1endendif switchADBC == 1 then --displayed y is now in the AD regimeY = Y + 1 --skip y = 0 (DNE)y = Y --easiest solution: start another iterator for these AD y's displayed on a BC year parentAD = 'AD 'endendif BC ~= '' and year <= 5 then --only show 'BC' for parent years <= 5: saves room, easier to read,BCdisp = ' '..BCe --and 6 is the first/last nav year that doesn't need a disambiguator;end --the center/parent year will always show BC, so no need to show it another 10x--populate left/right navylocal ysign = y --use y for display & ysign for logiclocal disp = y..BCdispif BC ~= '' then ysign = -ysign endlocal firsttry = firstpart..lspace..AD..y..tspace..BC..lastpartif (minyear <= ysign) and (ysign <= maxyear) thenlocal catlinkAD = catlinkfollowr( frame, firsttry, disp ) --try ADlocal catlink = catlinkAD --tentative winnerif AD ~= '' then --for "ACArt with 5 suppressed elements"-type catslocal catlinkNoAD = catlinkfollowr( frame, firstpart..lspace..y..tspace..BC..lastpart, disp ) --try !ADif catlinkNoAD.catexists == true thencatlink = catlinkNoAD --usurpelseif listall thentlistall[#tlistall] = tlistall[#tlistall]..' (tried; not displayed)<sup>1</sup>'endendif (AD..BC == '') and (catlink.catexists == false) and (y >= 1000) then --!ADBC & DNE; 4-digit only, to be frugal--try basic hyphenated cats: 1-year, endash, MOS-correct only, no #Rslocal yHyph_4 = y..'–'..(y+1) --try 2010–2011 type catslocal catlinkHyph_4 = catlinkfollowr( frame, firstpart..lspace..yHyph_4..tspace..BC..lastpart, yHyph_4 )if catlinkHyph_4.catexists and catlinkHyph_4.rtarget == nil then --exists & no #Rscatlink = catlinkHyph_4 --usurptrackcat(27, 'Category series navigation year and range')elseif listall thentlistall[#tlistall] = tlistall[#tlistall]..' (tried; not displayed)<sup>2</sup>'endlocal yHyph_2 = y..'–'..string.match(y+1, '%d%d$') --try 2010–11 type catsif i == 1 thenlocal yHyph_2_special = (y-1)..'–'..string.match(y, '%d%d$') --try special case 2021 -> 2021–22local catlinkHyph_2_special = catlinkfollowr( frame, firstpart..lspace..yHyph_2_special..tspace..BC..lastpart, yHyph_2_special )if catlinkHyph_2_special.catexists and catlinkHyph_2_special.rtarget == nil then --exists & no #Rscatlink = catlinkHyph_2_special --usurptrackcat(27, 'Category series navigation year and range')j = 1elseif listall thentlistall[#tlistall] = tlistall[#tlistall]..' (tried; not displayed)<sup>3</sup>'endendif not (i == 1 and j == 1) thenlocal catlinkHyph_2 = catlinkfollowr( frame, firstpart..lspace..yHyph_2..tspace..BC..lastpart, yHyph_2 )if catlinkHyph_2.catexists and catlinkHyph_2.rtarget == nil then --exists & no #Rscatlink = catlinkHyph_2 --usurptrackcat(27, 'Category series navigation year and range')elseif listall thentlistall[#tlistall] = tlistall[#tlistall]..' (tried; not displayed)<sup>4</sup>'endendendendif catlink.rtarget then --#R followed; determine whylocal r = catlink.rtargetlocal c = catlink.catlocal year_regex = '%d%d%d%d[–-]?%d?%d?%d?%d?' --prioritize year/range stripping, e.g. for "2006 Super 14 season"local hyph_regex = '%d%d%d%d[–-]%d+' --stricterlocal num_regex = '%d+' --strip any number otherwiselocal final_regex = nil --best choice goes hereif mw.ustring.match(r, year_regex) and mw.ustring.match(c, year_regex) thenfinal_regex = year_regexelseif mw.ustring.match(r, num_regex) and mw.ustring.match(c, num_regex) thenfinal_regex = num_regexendif final_regex thenlocal r_base = mw.ustring.gsub(r, final_regex, '')local c_base = mw.ustring.gsub(c, final_regex, '')if r_base ~= c_base thentrackcat(19, 'Category series navigation year redirected (base change)') --acceptable #R targetelseif mw.ustring.match(r, hyph_regex) thentrackcat(20, 'Category series navigation year redirected (var change)') --e.g. "2008 in Scottish women's football" to "2008–09"elsetrackcat(21, 'Category series navigation year redirected (other)') --exceptions go hereendelsetrackcat(20, 'Category series navigation year redirected (var change)') --e.g. "V2 engines" to "V-twin engines"endendtable.insert(navlist, catlink.navelement)else --OOB vs min/maxlocal hidden = '<span style="visibility:hidden">'..disp..'</span>'table.insert(navlist, hidden)if listall thenlocal dummy = catlinkfollowr( frame, firsttry, disp )tlistall[#tlistall] = tlistall[#tlistall]..' ('..hidden..')'endendelse --center navyif parentBC then BCdisp = ' '..BCe endtable.insert(navlist, '<b>'..year..BCdisp..'</b>')endi = i + 1end--add the listnavy = navy..horizontal(navlist)..'\n'isolatedcat()if listall thenreturn listalllinks()elsereturn navy..'</div>'endend--[[==========================={{ nav_roman }}==============================]]local function nav_roman( frame, firstpart, roman, lastpart, minimumrom, maximumrom )local toarabic = require('Module:ConvertNumeric').roman_to_numerallocal toroman = require('Module:Roman').main--sterilize/convert rom/numlocal num = tonumber(toarabic(roman))local rom = toroman({ [1] = num })if num == nil or rom == nil then --out of range or some other errorerrors = p.errorclass('Function nav_roman can\'t recognize one or more of "'..(num or 'nil')..'" & "'..(rom or 'nil')..'" in category "'..firstpart..' '..roman..' '..lastpart..'".')return p.failedcat(errors, 'R')end--sterilize min/maxlocal minrom = tonumber(minimumrom or '') or tonumber(toarabic(minimumrom or ''))local maxrom = tonumber(maximumrom or '') or tonumber(toarabic(maximumrom or ''))if minrom < 1 then minrom = 1 end --toarabic() returns -1 on errorif maxrom < 1 then maxrom = 9999 end --toarabic() returns -1 on errorif minrom > num then minrom = num endif maxrom < num then maxrom = num end--begin navromanlocal navr = '<div class="toccolours categorySeriesNavigation-range">\n'local navlist = {}local i = -5 --nav positionwhile i <= 5 dolocal n = num + iif n >= 1 thenlocal r = toroman({ [1] = n })if i ~= 0 then --left/right navrlocal catlink = catlinkfollowr( frame, firstpart..' '..r..' '..lastpart, r )if minrom <= n and n <= maxrom thenif catlink.rtarget then --a {{Category redirect}} was followedtrackcat(22, 'Category series navigation roman numeral redirected')endtable.insert(navlist, catlink.navelement)elselocal hidden = '<span style="visibility:hidden">'..r..'</span>'table.insert(navlist, hidden)if listall thentlistall[#tlistall] = tlistall[#tlistall]..' ('..hidden..')'endendelse --center navrtable.insert(navlist, '<b>'..r..'</b>')endelsetable.insert(navlist, '<span style="visibility:hidden">I</span>')endi = i + 1end-- add the listnavr = navr..horizontal(navlist)..'\n'isolatedcat()if listall thenreturn listalllinks()elsereturn navr..'</div>'endend--[[=========================={{ nav_nordinal }}============================]]local function nav_nordinal( frame, firstpart, ord, lastpart, minimumord, maximumord )local nord = tonumber(ord)local minord = tonumber(string.match(minimumord or '', '(-?%d+)[snrt]?[tdh]?')) or -9999 --allow full ord & +/- qualifierlocal maxord = tonumber(string.match(maximumord or '', '(-?%d+)[snrt]?[tdh]?')) or 9999 --allow full ord & +/- qualifierif string.match(minimumord or '', 'BC') then minord = -math.abs(minord) end --allow BC qualifier (AD otherwise assumed)if string.match(maximumord or '', 'BC') then maxord = -math.abs(maxord) end --allow BC qualifier (AD otherwise assumed)local temporal = string.match(lastpart, 'century') or string.match(lastpart, 'millennium')local tspace = ' ' --assume a trailing space after ordinalif string.match(lastpart, '^-') then tspace = '' end --DNE for "19th-century"-type cats--AD/BC switches & varslocal ordBCElastparts = { --needed for parent = AD 1-5, when the BC/E format is unknown--lists the lastpart of valid BCE cats--"BCE" removed to match both AD & BCE cats; easier & faster than multiple string.match()s['-century Hebrew people'] = 'BCE', --WP:CFD/Log/2016 June 21#Category:11th-century BC Hebrew people['-century Jews'] = 'BCE', --co-nominated['-century Judaism'] = 'BCE', --co-nominated['-century rabbis'] = 'BCE', --co-nominated['-century High Priests of Israel'] = 'BCE',}local parentBC = mw.ustring.match(lastpart, '%s(BCE?)') --"1st-century BC" formatlocal lastpartNoBC = mw.ustring.gsub(lastpart, '%sBCE?', '') --easier than splitting lastpart up in 2; AD never usedlocal BCe = parentBC or ordBCElastparts[lastpartNoBC] or 'BC' --"BC" defaultlocal switchADBC = 1 -- 1=AD parentif parentBC then switchADBC = -1 end -- -1=BC parent; possibly adjusted laterlocal O = 0 --secondary iterator for AD-on-a-BC-parentif not temporal and minord < 1 then minord = 1 end --nothing before "1st parliament", etc.if minord > nord*switchADBC then minord = nord*switchADBC end --input error; minord should be <= parentif maxord < nord*switchADBC then maxord = nord*switchADBC end --input error; maxord should be >= parent--begin navnordinallocal bnb = '' --border/no borderif navborder == false then --for Category series navigation decade and centurybnb = 'categorySeriesNavigation-range-transparent'endlocal navo = '<div class="toccolours categorySeriesNavigation-range '..bnb..'">\n'local navlist = {}local i = -5 --nav positionwhile i <= 5 dolocal o = nord + i*switchADBClocal BC = ''local BCdisp = ''if parentBC thenif switchADBC == -1 then --parentBC looking at the BC sideif o >= 1 then --the common caseBC = ' '..BCeelseif o == 0 then --switch to the AD sideBC = ''switchADBC = 1endendif switchADBC == 1 then --displayed o is now in the AD regimeO = O + 1 --skip o = 0 (DNE)o = O --easiest solution: start another iterator for these AD o's displayed on a BC year parentendelseif o <= 0 then --parentAD looking at BC sideBC = ' '..BCeo = math.abs(o - 1) --skip o = 0 (DNE)endif BC ~= '' and nord <= 5 then --only show 'BC' for parent ords <= 5: saves room, easier to read,BCdisp = ' '..BCe --and 6 is the first/last nav ord that doesn't need a disambiguator;end --the center/parent ord will always show BC, so no need to show it another 10x--populate left/right navolocal oth = p.addord(o)local osign = o --use o for display & osign for logicif BC ~= '' then osign = -osign endlocal hidden = '<span style="visibility:hidden">'..oth..'</span>'if temporal then --e.g. "3rd-century BC"local lastpart = lastpartNoBC --lest we recursively add multiple "BC"sif BC ~= '' thenlastpart = string.gsub(lastpart, temporal, temporal..BC) --replace BC if neededendlocal catlink = catlinkfollowr( frame, firstpart..' '..oth..tspace..lastpart, oth..BCdisp )if (minord <= osign) and (osign <= maxord) thenif catlink.rtarget then --a {{Category redirect}} was followedtrackcat(23, 'Category series navigation nordinal redirected')endtable.insert(navlist, navcenter(i, catlink))elsetable.insert(navlist, hidden)if listall thentlistall[#tlistall] = tlistall[#tlistall]..' ('..hidden..')'endendelseif BC == '' and minord <= osign and osign <= maxord then --e.g. >= "1st parliament"local catlink = catlinkfollowr( frame, firstpart..' '..oth..tspace..lastpart, oth )if catlink.rtarget then --a {{Category redirect}} was followedtrackcat(23, 'Category series navigation nordinal redirected')endtable.insert(navlist, navcenter(i, catlink))else --either out-of-range (hide), or non-temporal + BC = something might be wrong (2nd X parliament BC?); handle exceptions if/as they arisetable.insert(navlist, hidden)endi = i + 1endnavo = navo..horizontal(navlist)..'\n'isolatedcat()if listall thenreturn listalllinks()elsereturn navo..'</div>'endend--[[========================={{ nav_wordinal }}=============================]]local function nav_wordinal( frame, firstpart, word, lastpart, minimumword, maximumword, ordinal, frame )--Module:ConvertNumeric.spell_number2() args:-- ordinal == true : 'second' is output instead of 'two'-- ordinal == false: 'two' is output instead of 'second'local ord2eng = require('Module:ConvertNumeric').spell_number2local eng2ord = require('Module:ConvertNumeric').english_to_ordinallocal th = 'th'if not ordinal thenth = ''eng2ord = require('Module:ConvertNumeric').english_to_numeralendlocal capitalize = nil ~= string.match(word, '^%u') --determine capitalizationlocal nord = eng2ord(string.lower(word)) --operate on/with lowercase, and restore any capitalization laterlocal lspace = ' ' --assume a leading space (most common)local tspace = ' ' --assume a trailing space (most common)if string.match(firstpart, '[%-%(]$') then lspace = '' end --DNE for "Straight-eight engines"-type catsif string.match(lastpart, '^[%-%)]' ) then tspace = '' end --DNE for "Nine-cylinder engines"-type cats--sterilize min/maxlocal minword = 1local maxword = 99if minimumword thenlocal num = tonumber(minimumword)if num and 0 < num and num < maxword thenminword = numelselocal ord = eng2ord(minimumword)if 0 < ord and ord < maxword thenminword = ordendendendif maximumword thenlocal num = tonumber(maximumword)if num and 0 < num and num < maxword thenmaxword = numelselocal ord = eng2ord(maximumword)if 0 < ord and ord < maxword thenmaxword = ordendendendif minword > nord then minword = nord endif maxword < nord then maxword = nord end--begin navwordinallocal navw = '<div class="toccolours categorySeriesNavigation-range">\n'local navlist = {}local i = -5 --nav positionwhile i <= 5 dolocal n = nord + iif n >= 1 thenlocal nth = p.addord(n)if not ordinal then nth = n endif i ~= 0 then --left/right navwlocal w = ord2eng{ num = n, ordinal = ordinal, capitalize = capitalize }local catlink = catlinkfollowr( frame, firstpart..lspace..w..tspace..lastpart, nth )if minword <= n and n <= maxword thenif catlink.rtarget then --a {{Category redirect}} was followedtrackcat(24, 'Category series navigation wordinal redirected')endtable.insert(navlist, catlink.navelement)elselocal hidden = '<span style="visibility:hidden">'..nth..'</span>'table.insert(navlist, hidden)if listall thentlistall[#tlistall] = tlistall[#tlistall]..' ('..hidden..')'endendelse --center navwtable.insert(navlist, '<b>'..nth..'</b>')endelsetable.insert(navlist, '<span style="visibility:hidden">'..'0'..th..'</span>')endi = i + 1end-- Add the listnavw = navw..horizontal(navlist)..'\n'isolatedcat()if listall thenreturn listalllinks()elsereturn navw..'</div>'endend--[[==========================={{ find_var }}===============================]]local function find_var( pn )--Extracts the variable text (e.g. 2015, 2015–16, 2000s, 3rd, III, etc.) from a string,--and returns { ['vtype'] = <'year'|'season'|etc.>, <v> = <2015|2015–16|etc.> }local pagename = currtitle.textif pn and pn ~= '' thenpagename = pnendlocal cpagename = 'Category:'..pagename --limited-Lua-regex workaroundlocal d_season = mw.ustring.match(cpagename, ':(%d+s).+%(%d+[–-]%d+%)') --i.e. "1760s in the Province of Quebec (1763–1791)"local y_season = mw.ustring.match(cpagename, ':(%d+) .+%(%d+[–-]%d+%)') --i.e. "1763 establishments in the Province of Quebec (1763–1791)"local e_season = mw.ustring.match(cpagename, '%s(%d+[–-])$') or --irreg; ending unknown, e.g. "Members of the Scottish Parliament 2021–" mw.ustring.match(cpagename, '%s(%d+[–-]present)$') --e.g. "UK MPs 2019–present"local season = mw.ustring.match(cpagename, '[:%s%(](%d+[–-]%d+)[%)%s]') or --split in 2 b/c you can't frontier '$'/eos? mw.ustring.match(cpagename, '[:%s](%d+[–-]%d+)$')local tvseason = mw.ustring.match(cpagename, 'season (%d+)') or mw.ustring.match(cpagename, 'series (%d+)')local nordinal = mw.ustring.match(cpagename, '[:%s](%d+[snrt][tdh])[-%s]') or mw.ustring.match(cpagename, '[:%s](%d+[snrt][tdh])$')local decade = mw.ustring.match(cpagename, '[:%s](%d+s)[%s-]') or mw.ustring.match(cpagename, '[:%s](%d+s)$')local year = mw.ustring.match(cpagename, '[:%s](%d%d%d%d)%s') or --prioritize 4-digit years mw.ustring.match(cpagename, '[:%s](%d%d%d%d)$') or mw.ustring.match(cpagename, '[:%s](%d+)%s') or mw.ustring.match(cpagename, '[:%s](%d+)$') or --expand/combine exceptions below as needed mw.ustring.match(cpagename, '[:%s](%d+)-related') or mw.ustring.match(cpagename, '[:%s](%d+)-cylinder') or mw.ustring.match(cpagename, '[:%-VW](%d+)%s') --e.g. "Straight-8 engines"local roman = mw.ustring.match(cpagename, '%s([IVXLCDM]+)%s')local found = d_season or y_season or e_season or season or tvseason or nordinal or decade or year or romanif found thenif string.match(found, '%d%d%d%d%d') == nil then--return in order of decreasing complexity/least chance for duplicationif nordinal and season --i.e. "18th-century establishments in the Province of Quebec (1763–1791)"then return { ['vtype'] = 'nordinal', ['v'] = nordinal } endif d_season then return { ['vtype'] = 'decade', ['v'] = d_season } endif y_season then return { ['vtype'] = 'year', ['v'] = y_season } endif e_season then return { ['vtype'] = 'ending', ['v'] = e_season } endif season then return { ['vtype'] = 'season', ['v'] = season } endif tvseason then return { ['vtype'] = 'tvseason', ['v'] = tvseason } endif nordinal then return { ['vtype'] = 'nordinal', ['v'] = nordinal } endif decade then return { ['vtype'] = 'decade', ['v'] = decade } endif year then return { ['vtype'] = 'year', ['v'] = year } endif roman then return { ['vtype'] = 'roman', ['v'] = roman } endendelse--try wordinals ('zeroth' to 'ninety-ninth' only)local eng2ord = require('Module:ConvertNumeric').english_to_ordinallocal split = mw.text.split(pagename, ' ')for i=1, #split doif eng2ord(split[i]) > -1 thenreturn { ['vtype'] = 'wordinal', ['v'] = split[i] }endend--try English numerics ('one'/'single' to 'ninety-nine' only)local eng2num = require('Module:ConvertNumeric').english_to_numerallocal split = mw.text.split(pagename, '[%s%-]') --e.g. "Nine-cylinder engines"for i=1, #split doif eng2num(split[i]) > -1 thenreturn { ['vtype'] = 'enumeric', ['v'] = split[i] }endendenderrors = p.errorclass('Function find_var can\'t find the variable text in category "'..pagename..'".')return { ['vtype'] = 'error', ['v'] = p.failedcat(errors, 'V') }end--[[==========================================================================]]--[[ Main ]]--[[==========================================================================]]function p.csn( frame )--arg checks & handlinglocal args = frame:getParent().argscheckforunknownparams(args) --for template argscheckforunknownparams(frame.args) --for #invoke'd argslocal cat = args['cat'] --'testcase' alias for catspacelocal list = args['list-all-links'] --debugging utility to output all links & followed #Rslocal follow = args['follow-redirects'] --default 'yes'local testcase = args['testcase']local testcasegap = args['testcasegap']local minimum = args['min']local maximum = args['max']local skip_gaps = args['skip-gaps']local show = args['show']if show and show ~= '' thenif show == 'skip-gaps' then return skipgaps_limitelseif show == 'term-limit' then return term_limitelseif show == 'hgap-limit' then return hgap_limitelseif show == 'ygap-limit' then return ygap_limit endend--apply argslocal pagename = testcase or cat or currtitle.textlocal testcaseindent = ''if testcasecolon == ':' then testcaseindent = '\n::' endif follow and follow == 'no' then followRs = false endif list and list == 'yes' then listall = true endif skip_gaps and skip_gaps == 'yes' thenskipgaps = truetrackcat(26, 'Category series navigation using skip-gaps parameter')end--ns checksif currtitle.nsText == 'Category' thenif cat and cat ~= '' thentrackcat(1, 'Category series navigation using cat parameter')endif testcase and testcase ~= '' thentrackcat(2, 'Category series navigation using testcase parameter')endelseif currtitle.nsText == '' thentrackcat(30, 'Category series navigation in mainspace')end--find the variable parts of pagenamelocal findvar = find_var(pagename)if findvar.vtype == 'error' then --basic format error checking in find_var()return findvar.v..table.concat(ttrackingcats)endlocal start = string.match(findvar.v, '^%d+')--the rest is staticlocal findvar_escaped = string.gsub( findvar.v, '%-', '%%%-')local firstpart, lastpart = string.match(pagename, '^(.-)'..findvar_escaped..'(.*)$')if findvar.vtype == 'tvseason' then --double check for cases like "30 Rock (season 3) episodes"firstpart, lastpart = string.match(pagename, '^(.-season )'..findvar_escaped..'(.*)$')if firstpart == nil thenfirstpart, lastpart = string.match(pagename, '^(.-series )'..findvar_escaped..'(.*)$')endendfirstpart = mw.text.trim(firstpart or '')lastpart = mw.text.trim(lastpart or '')--call the appropriate nav function, in order of decreasing popularityif findvar.vtype == 'year' then --e.g. "500", "2001"; nav_year..nav_decade; ~75% of catslocal nav1 = nav_year( frame, firstpart, start, lastpart, minimum, maximum )..testcaseindent..table.concat(ttrackingcats)local dec = math.floor(findvar.v/10)local decadecat = nillocal firstpart_dec = firstpartif firstpart_dec ~= '' thenfirstpart_dec = firstpart_dec..' the'elseif firstpart_dec == 'AD' and dec <= 1 thenfirstpart_dec = ''if dec == 0 then dec = '' endendlocal decade = dec..'0s 'decadecat = mw.text.trim( firstpart_dec..' '..decade..lastpart )local exists = catexists(decadecat)if exists thennavborder = falsetrackcat(28, 'Category series navigation year and decade')local nav2 = nav_decade( frame, firstpart_dec, decade, lastpart, minimum, maximum )..testcaseindent..table.concat(ttrackingcats)return wrap( nav1, nav2 )elseif ttrackingcats[16] ~= '' then --nav_year isolated; check nav_hyphen (e.g. UK MPs 1974, Moldovan MPs 2009, etc.)local hyphen = '–'local finish = startlocal nav2 = nav_hyphen( frame, start, hyphen, finish, firstpart, lastpart, minimum, maximum, testcasegap )..testcaseindent..table.concat(ttrackingcats)if ttrackingcats[16] ~= '' then return wrap( nav1 ) --still isolated; rv to nav_yearelse return wrap( nav2 ) endelse --regular nav_yearreturn wrap( nav1 )endelseif findvar.vtype == 'decade' then --e.g. "0s", "2010s"; nav_decade..nav_nordinal; ~12% of catslocal nav1 = nav_decade( frame, firstpart, start, lastpart, minimum, maximum )..testcaseindent..table.concat(ttrackingcats)local decade = tonumber(string.match(findvar.v, '^(%d+)s'))local century = math.floor( ((decade-1)/100) + 1 ) --from {{CENTURY}}if century == 0 then century = 1 end --no 0th centuryif string.match(decade, '00$') thencentury = century + 1 --'2000' is in the 20th, but the rest of the 2000s is in the 21stendlocal clastpart = ' century '..lastpartlocal centurycat = mw.text.trim( firstpart..' '..p.addord(century)..clastpart )local exists = catexists(centurycat)if not exists then --check for hyphenated centuryclastpart = '-century '..lastpartcenturycat = mw.text.trim( firstpart..' '..p.addord(century)..clastpart )exists = catexists(centurycat)endif exists thennavborder = falsetrackcat(29, 'Category series navigation decade and century')local nav2 = nav_nordinal( frame, firstpart, century, clastpart, minimum, maximum )..testcaseindent..table.concat(ttrackingcats)return wrap( nav1, nav2 )elsereturn wrap( nav1 )endelseif findvar.vtype == 'nordinal' then --e.g. "1st", "99th"; ~7.5% of catsreturn wrap( nav_nordinal( frame, firstpart, start, lastpart, minimum, maximum )..testcaseindent..table.concat(ttrackingcats) )elseif findvar.vtype == 'season' then --e.g. "1–4", "1999–2000", "2001–02", "2001–2002", "2005–2010", etc.; ~5.25%local hyphen, finish = mw.ustring.match(findvar.v, '%d([–-])(%d+)') --ascii 150 & 45 (ndash & keyboard hyphen); mw req'dreturn wrap( nav_hyphen( frame, start, hyphen, finish, firstpart, lastpart, minimum, maximum, testcasegap )..testcaseindent..table.concat(ttrackingcats) )elseif findvar.vtype == 'tvseason' then --e.g. "1", "15" but preceded with "season" or "series"; <1% of catsreturn wrap( nav_tvseason( frame, firstpart, start, lastpart, maximum )..testcaseindent..table.concat(ttrackingcats) ) --"minimum" defaults to 1elseif findvar.vtype == 'wordinal' then --e.g. "first", "ninety-ninth"; <<1% of catslocal ordinal = truereturn wrap( nav_wordinal( frame, firstpart, findvar.v, lastpart, minimum, maximum, ordinal, frame )..testcaseindent..table.concat(ttrackingcats) )elseif findvar.vtype == 'enumeric' then --e.g. "one", "ninety-nine"; <<1% of catslocal ordinal = falsereturn wrap( nav_wordinal( frame, firstpart, findvar.v, lastpart, minimum, maximum, ordinal, frame )..testcaseindent..table.concat(ttrackingcats) )elseif findvar.vtype == 'roman' then --e.g. "I", "XXVIII"; <<1% of catsreturn wrap( nav_roman( frame, firstpart, findvar.v, lastpart, minimum, maximum )..testcaseindent..table.concat(ttrackingcats) )elseif findvar.vtype == 'ending' then --e.g. "2021–" (irregular; ending unknown); <<<1% of catslocal hyphen, finish = mw.ustring.match(findvar.v, '%d([–-])present$'), -1 --ascii 150 & 45 (ndash & keyboard hyphen); mw req'dif hyphen == nil thenhyphen, finish = mw.ustring.match(findvar.v, '%d([–-])$'), 0 --0/-1 are hardcoded switches for nav_hyphen()endreturn wrap( nav_hyphen( frame, start, hyphen, finish, firstpart, lastpart, minimum, maximum, testcasegap )..testcaseindent..table.concat(ttrackingcats) )else --malformederrors = p.errorclass('Failed to determine the appropriate nav function from malformed season "'..findvar.v..'". ')return p.failedcat(errors, 'N')..table.concat(ttrackingcats)endendreturn p