Module:Category series navigation

(Redirected from Module:Navseasoncats)

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, '&#', '&amp;#') )end--Failure handling.function p.failedcat( errors, sortkey )if avoidself thenreturn (errors or '')..'&#42;&#42;&#42;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