require('strict')local p = local horizontal = require('Module:List').horizontallocal rtarget = require('Module:Resolve category redirect').rtarget
----Globals --
local currtitle = mw.title.getCurrentTitlelocal 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 = 50local term_limit = 10local hgap_limit = 6local ygap_limit = 5local listall = falselocal tlistall = local tlistallbwd = local tlistallfwd = local ttrackingcats = local avoidself = (not string.match(currtitle.text, 'Category series navigation with') and not string.match(currtitle.text, 'Category series navigation.*/doc') and not string.match(currtitle.text, 'Category series navigation.*/sandbox') and currtitle.text ~= 'Category series navigation' and currtitle.nsText:gsub('_', ' ') ~= 'User talk' and -- currtitle.nsText:gsub('_', ' ') ~= 'Template talk' and (currtitle.nsText ~= 'Template' or testcases)) --avoid nested transclusion errors (i.e.)
----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',, 'Error! '..string.gsub(msg, '&#', '&#'))end
--Failure handling.function p.failedcat(errors, sortkey) if avoidself then return (errors or )..'***Category series navigation failed to generate navbox***'.. ''..(sortkey or 'O')..'\n' end return 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 then if cat ~= then ttrackingcats[key] = '' else ttrackingcats[key] = end end returnend
--Check for unknown parameters.--Used by main only.local function checkforunknownparams(tbl) local knownparams = for k, _ in pairs (tbl) do if knownparams[k]
--Check for nav_* navigational isolation (not necessarily an error).--Used by all nav_*.local function isolatedcat if nexistingcats
--Similar to : make a piped link to a category, if it exists;--if it doesn't exist, just display the greyed link title without linking.--Follows s.--Returns -- if #R followed;--returns -- otherwise.--Used by all nav_*.local function catlinkfollowr(frame, cat, displaytext, displayend, listoverride) 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 = cat if displaytext ~= then --use 'displaytext' parameter if present disp = mw.ustring.gsub(displaytext, '%s+%(.+$', ); --strip any trailing disambiguator end local link, nilorR local exists = catexists(cat) if exists then nexistingcats = nexistingcats + 1 if followRs then local R = rtarget(cat, frame) --find & follow #R if R ~= cat then --#R followed nilorR = R end if displayend then local y, hyph, ending = mw.ustring.match(R, '^.-(%d+)([–-])(.*)$') if ending
then disp = y..hyph..'
' --hidden y to match spacing end end link = ''..disp..'' else link = ''..disp..'' end else link = ' ' end if listall and listoverride--Returns a numbered list of all 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
|list-all-links=yes
parameter/utility '.. 'should not be saved in category space, only previewed.') out = p.failedcat(errors, 'Z') end local bwd, fwd = , if tlistallbwd[1] then bwd = '\n\nbackward search:'..nl..table.concat(tlistallbwd, nl) end if tlistallfwd[1] then fwd = '\n\nforward search:'..nl..table.concat(tlistallfwd, nl) end if tlistall[1] then return out..nl..table.concat(tlistall, nl)..bwd..fwd else return out..nl..'No links found!?'..bwd..fwd endend--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 then if to
4) and (#to
2) and (#to
--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 = nil if mw.ustring.match(cat, '%d+[–-]present$') then terminaltxt = 'present' trackcat(14, 'Category series navigation range ends (present)') elseif mw.ustring.match(cat, '%d+[–-]$') then terminaltxt = trackcat(15, 'Category series navigation range ends (blank, MOS)') end return 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
then return nil end local dec = string.match(decade, '^[-%+]?(%d?%d?%d?0)$') or string.match(decade, '^[-%+]?(%d?%d?%d?0)%D') if dec then return dec else --fix 2-4 digit decade local decade_fixed234 = string.match(decade, '^[-%+]?(%d%d?%d?)%d$') or string.match(decade, '^[-%+]?(%d%d?%d?)%d%D') if decade_fixed234 then return decade_fixed234..'0' end --fix 1-digit decade local decade_fixed1 = string.match(decade, '^[-%+]?(%d)$') or string.match(decade, '^[-%+]?(%d)%D') if decade_fixed1 then return '0' end --unfixable return nil endend
--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
--12 -> 12th, etc.--Used by nav_nordinal & nav_wordinal.function p.addord(i) if tonumber(i) then local s = tostring(i) local tens = string.match(s, '1%d$') if tens then return s..'th' end local ones = string.match(s, '%d$') if ones
'2' then return s..'nd' elseif ones
--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
true then return ''..catlink.displaytext..'' else return ''..catlink.navelement..'' end else return catlink.navelement endend
--Wrap one or two navs in a
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 then nav = '\n'..nav else nav = end return nav end return templatestyles.. '
'.. prepare(nav1)..prepare(nav2).. '\n
'end
----Formerly separated templates/modules --
--
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 start if string.match(start or , '^%d%d?%d?%d?$')
-1) or --"Members of the Scottish Parliament 2021–present" (finish
nil or maxseas
-1 then trackcat(14, 'Category series navigation range ends (present)') else trackcat(15, 'Category series navigation range ends (blank, MOS)') end elseif (start
nil) and (string.match(finish or , '^%-%d+$')
true do local nish = nstart + t --use switchADBC to flip this sign to work for years BC, if/when the time comes if (nish
finish) then ttlens[t] = 1 break end if t
4 and regularparent
1 then --"2001–02" & "2001–2002" both allowed if lenfinish ~= 2 and lenfinish ~= 4 then errors = 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') end else --"2001–2005" is required for t > 1; track "2001–05"; anything else = error if lenfinish
'00' then --full year required regardless of term length trackcat(5, 'Category series navigation range abbreviated (MOS)') end end --calculate intERseason gap size local 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 = false local hgap = hgap_default while hgap <= hgap_limit_reg and regularparent
1 then --test abbreviated range first, then full range, to be frugal with expensive functions if 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) then hgap_success = true break end elseif t > 1 then --test full range first, then abbreviated range, to be frugal with expensive functions if 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) then hgap_success = true break end end hgap = hgap + 1 end if hgap_success
'isolated' then fwanchor = bwanchor end local spangreen = '[<span style="color:green">j, g, k = ' --used for/when debugging via list-all-links=yes local spanblue = '<span style="color:blue">' local spanred = ' (<span style="color:red">' local span = '</span>' local lastg = nil --to check for run-on searches local lastk = nil --to check for run-on searches local endfound = false --switch used to stop searching forward local iirregs = 0 --index of tirregs[] for j < 0, since search starts from parent local j = -jlimit --index of tirregs[] for j > 0 & pseudo navh position while j <= jlimit do if j < 0 then --search backward from parent local gbreak = false --switch used to break out of g-loop local g = 0 --gap size while g <= hgap_limit_irreg do local k = 0 --term length: 0 = "0-length", 1+ = normal while k <= term_limit do local from = bwanchor - k - g local to = bwanchor - g local full = mw.text.trim(firstpart..lspace..from..hyph..to..tspace..lastpart) if k
0 if repeating year problems arise to = '0-length' full = mw.text.trim(firstpart..lspace..from..tspace..lastpart) if catlinkfollowr(frame, full).rtarget ~= nil then --#R followed table.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 messy end end end if (k >= 1) or --the normal case; only continue k = 0 if 0-length found (to
1) and-- (g
1) and --commented to match j>0 case ("1995–96 in Federal Republic of Yugoslavia basketball") (catexists(full)
'0-length' then trackcat(13, 'Category series navigation range irregular, 0-length') end tlistallbwd[#tlistallbwd] = spanblue..tlistallbwd[#tlistallbwd]..span..' (found)' ttlens[find_duration(full) ] = 1 if j
true then break end g = g + 1 lastg = g end --while g <= hgap_limit_irreg do end --if j < 0 if j > 0 and endfound
-1 then to4 = 'present' --see if end-cat exists (present) elseif k
-2 then if regularparent ~= 'isolated' then --+restrict to g
'0-length') --ghetto "continue" (thx Lua) to avoid expensive searches for "UK MPs 1974-1974", etc. then table.insert(tlistallfwd, spangreen..j..', '..g..', '..k..span..'] '..full) if (k
0 or g
false) then --allow bare-bones MOS:DATERANGE alternation, in case we're on a 0|1-gap, 1-year term series to2 = 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) end end if catexists(full) then if to4
full then --only use 0-length cats that don't #R trackcat(13, 'Category series navigation range irregular, 0-length') end end tirregs['from'..j] = from tirregs['to'..j] = (to2 or to4) if (k
0) then endfound = true --tentative else --k
4 then tgapsj4[g] = 1 else tgaps[g] = 1 end endfound = false if to4 ~= '0-length' then --k > 0 fwanchor = to4 --ratchet up gbreak = true break --only break on k > 0 b/c old end-cat #Rs still exist like "Members of the Scottish Parliament 2011–" else --k
hgap_limit_irreg then --keep searching, since not a runaway, just far away ("American soccer clubs 1958–59 season") hgap_limit_irreg = hgap_limit_irreg + 1 end end end end end --ghetto "continue" k = k + 1 lastk = k end --while k <= term_limit do if gbreak
false then if (lastg and lastk) and (lastg > hgap_limit_irreg) and (lastk > term_limit) then --search exhausted if j < 0 then j = 0 --bwd search exhausted; continue fwd elseif j > 0 then break end --fwd search exhausted end j = j + 1 end --while j <= jlimit end --if hgap <= hgap_limit_reg --determine # of displayed navh elements based on "YYYY-YY" vs. "YYYY-YYYY" counts local Ythreshold = 3.3 --((YYYY-YY x 7) + (YYYY-YYYY x 2))/18 = 3.222; ((YYYY-YY x 6) + (YYYY-YYYY x 3))/18 = 3.333 local Ycount = 0 --"Y" count local ycount = 0 --tirregs counter; # of contiguous #s for k, v in pairs (tirregs) do local dummy, dunce = mw.ustring.gsub(tostring(v), '%d', ) --why can't gsub just return a table?? Ycount = Ycount + dunce ycount = ycount + 1 end local ycount_limit = ((jlimit * 2) + 1) * 2 --i.e. ((4 * 2) + 1) * 2 = 18 if ycount < ycount_limit then --fill in the blanks with Ycount_parent, since hidden/dne cats aren't in tirregs local dummy_finish = finish if not regularparent then dummy_finish = start end local dummy, dunce_from = mw.ustring.gsub(start, '%d', ) local dummy, dunce_to = mw.ustring.gsub(dummy_finish, '%d', ) local Ycount_parent_avg = (dunce_from + dunce_to)/2 --"YYYY-YYYY" = 4; "YYYY-YY" = 3 Ycount = Ycount + (Ycount_parent_avg * (ycount_limit - ycount)) ycount = ycount_limit end local iwidth = 3 --default to 3-a-side, 7 total local Y_per_y = Ycount / ycount --normalized range: [3-4] if Y_per_y < Ythreshold then iwidth = 4 --extend to 4-a-side, 9 total end --begin navhyphen local navh = '
\n' local navlist = local terminalcat = false --switch used to hide future cats local terminaltxt = nil local i = -iwidth --nav position while i <= iwidth do local from = nstart + i*(t+hgap) --the logical, but not necessarily correct, 'from' if tirregs['from'..i] then --prefer the irregular term table from = tonumber(tirregs['from'..i]) else --fallback to lazy/naive 'from' if i > 0 and tirregs['from'..(i-1)] and tirregs['from'..(i-1)] >= from then --end of the line: avoid dups/past, and create reasonable grey'd ranges local greyto = tonumber(tirregs['to' .. (i-1)]) or -9999 local greyfrom = tonumber(tirregs['from'..(i-1)]) or -9999 local grey = greyto --prefer 'to' if greyfrom > greyto then grey = greyfrom end --'from' fallback, in case "1995–96", "1995-present", etc. if grey > -9999 then if grey ~= greyto then from = grey + t + hgap --account for missing/incomplete 'to' else from = grey + hgap end tirregs['from'..i] = from --remember tirregs['to' .. i] = from + t end elseif i < 0 then local greyfrom local ii = 0 while ii < 3 do ii = ii + 1 greyfrom = tonumber(tirregs['from'..(i+ii)]) if greyfrom then break end end from = (greyfrom or nstart) - ii*(t+hgap) tirregs['from'..i] = from --remember tirregs['to' .. i] = from + t end end local from2 = string.match(from, '%d?%d$') local to = tostring(from+t) --the logical, naive range, but if tirregs['to'..i] then --prefer irregular term table to = tirregs['to'..i] elseif regularparent
'0-length' then tofinal = to end --check existance of 4-digit, MOS-correct range, with abbreviation fallback if tofinal ~= '0-length' then if t > 1 and string.len(from)
1 then --full-year consecutive ranges are also allowed local abbr = firstpart..lspace..from..hyph..tofinal..tspace..lastpart --assume tofinal is in abbr format if not catexists(abbr) and tofinal ~= to then local full = firstpart..lspace..from..hyph..to..tspace..lastpart if catexists(full) then tofinal = (to or ) --if abbr AND full DNE, then tofinal is still in its abbr format (unless it's a century transition) end end end end --populate navh if i ~= 0 then --left/right navh local orig = firstpart..lspace..from..hyph..tofinal..tspace..lastpart local disp = from..hyph..tofinal if tofinal
false then terminaltxt = find_terminaltxt(disp) --also sets tracking cats terminalcat = (terminaltxt ~= nil) end if catlink.rtarget and avoidself then --a was followed, figure out why --determine new term length & gap size ttlens[find_duration(catlink.rtarget) ] = 1 if i > -iwidth then local lastto = tirregs['to'..(i-1)] if lastto
4 then tgapsj4[gap ] = 1 --tgapsj4[-1] are ignored later else tgaps[gap ] = 1 --tgaps[-1] are ignored later end end end --display/tracking handling local 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
false then terminalcat = 1 end local dummy = find_terminaltxt(catlink.rtarget) --also sets tracking cats rtarbase = mw.ustring.gsub(catlink.rtarget, terminal_regex, ) end origbase = mw.text.trim(origbase) rtarbase = mw.text.trim(rtarbase) if origbase ~= rtarbase then trackcat(6, 'Category series navigation range redirected (base change)') elseif terminalcat
rtarbase local 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 then trackcat(10, 'Category series navigation range redirected (other)') else local 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 then trackcat(7, 'Category series navigation range redirected (var change)') else trackcat(9, 'Category series navigation range redirected (MOS)') end end end end if terminalcat then --true or 1 if type(terminalcat) ~= 'boolean' then nmaxseas = from end --only want to do this once terminalcat = true --done finagling/overloading end if (from >= 0) and (nminseas <= from) and (from <= nmaxseas) then table.insert(navlist, catlink.navelement) if terminalcat then nmaxseas = nminseas_default end --prevent display of future ranges else local hidden = '
' table.insert(navlist, hidden) if listall then tlistall[#tlistall] = tlistall[#tlistall]..' ('..hidden..')' end end else --center navh if finish0 then finish = '
' end local disp = start..hyph..finish if regularparent4 then --only count gaps if they were displayed ("Karnataka MLAs 1957–1962") for s = 1, hgap_limit_reg do igaps = igaps + (tgapsj4[s] or 0) end end for s = 0, term_limit do itlens = itlens + (ttlens[s] or 0) end if igaps > 0 then trackcat(11, 'Category series navigation range gaps') end if itlens > 1 and ttrackingcats[13]
' endend
--
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
\n' local navlist = local prepad = local i = -5 --nav position while i <= 5 do local t = tv + i if i ~= 0 then --left/right navt local catlink = catlinkfollowr(frame, firstpart..' '..t..tspace..lastpart, t) if t >= 1 and t <= maxtv then --hardcode mintv if catlink.rtarget then --a was followed trackcat(25, 'Category series navigation TV season redirected') end if catlink.catexists or (maxtv ~= maxtv_default and t <= maxtv) then table.insert(navlist, prepad..catlink.navelement) --display normally prepad = else local postpad = '
' navlist[#navlist] = navlist[#navlist]..postpad if listall then tlistall[#tlistall] = tlistall[#tlistall]..' ('..postpad..')' end end elseif t < 1 then prepad = prepad..' ' if listall then tlistall[#tlistall] = (tlistall[#tlistall] or )..' (x)' end else --t > maxtv local postpad = ' ' navlist[#navlist] = navlist[#navlist]..postpad if listall then tlistall[#tlistall] = tlistall[#tlistall]..' ('..postpad..')' end end else --center navt table.insert(navlist, prepad..''..tv..'') prepad = end i = i + 1 end -- add the list navt = navt..horizontal(navlist)..'\n' isolatedcat if listall then return listalllinks else return navt..'' endend
--
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 dec local dec = sterilizedec(decade) if dec
"-0" string...) end elseif mindec
nil mindec = mindefault --tonumber later, after error checks end --sterilize maxdecade & determine AD/BC local maxdefault = '9999' local maxdec = sterilizedec(maxdecade) --returns a tostring(unsigned int), or nil + error if maxdec then if string.match(maxdecade, '-%d') or string.match(maxdecade, 'BC') then --better +/-0 behavior with strings (0-initialized int
nil and maxdecade and maxdecade ~= then errors = 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
false then --for Category series navigation year and decade bnb = 'categorySeriesNavigation-range-transparent' end local navd = '
\n' local navlist = local i = -50 --nav position x 10 while i <= 50 do local d = ndec + i*switchADBC local BC = BCdisp = if dec0to40AD then if D < -10 then d = math.abs(d + 10) --b/c 2 "0s" decades exist: "0s BC" & "0s" (AD) BC = 'BC ' if d
-1 then --parentBC looking at the BC side (the common case) BC = 'BC ' if d
1 then --switched to the AD side D = D + 10 --now iterate from 0s AD d = D --2nd d = 0 use (on first use) end end if BC ~= and ndec <= 50 then BCdisp = ' BC' --show BC for all BC decades whenever a "0s" is displayed on the nav end --determine target cat local disp = d..'s'..BCdisp local catlink = catlinkfollowr(frame, firstpart..' '..d..'s'..tspace..BC..lastpart, disp) if catlink.rtarget then --a was followed trackcat(18, 'Category series navigation decade redirected') end --populate left/right navd local shown = navcenter(i, catlink) local hidden = '
' local dsign = d --use d for display & dsign for logic if BC ~= then dsign = -dsign end if (nmindec <= dsign) and (dsign <= nmaxdec) then if dsign0 or nmaxdec
'-0' then zmin = -1 elseif mindec
'-0' then zmax = -1 elseif maxdec
' endend
--
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 = -9999 local maxyear_default = 9999 year = tonumber(year) or tonumber(mw.ustring.match(year or , '^%s*(%d*)')) local minyear = tonumber(string.match(minimumyear or , '-?%d+')) or minyear_default --allow +/- qualifier local maxyear = tonumber(string.match(maximumyear or , '-?%d+')) or maxyear_default --allow +/- qualifier if 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
ygap2 then ygap = ygap1 end elseif ygap1_success then ygap = ygap1 elseif ygap2_success then ygap = ygap2 end end --skip non-existing years, if requested local ynogaps = --populate with existing years in the range, at most, [year - (skipgaps_limit * 5), year + (skipgaps_limit * 5)] if skipgaps then if minyear
nil then yskipped[Yeary] = Yeary cat = firstpart..lspace..Yeary..tspace..lastpart found = catexists(cat) if found then break end end y = y + 1 end if found then Year = Yeary else Year = Year + 1 end ynogaps[i] = Year i = i + 1 end ynogaps[0] = year --the parent --populate nav element queue outwards negatively from the parent Year = year --reset ratchet i = -1 while i >= -5 do local y = -1 while y >= -skipgaps_limit do found = false Yeary = Year + y if yskipped[Yeary]
\n' local navlist = local y local j = 0 --decrementor for special cases "2021 World Rugby Sevens Series" -> "2021–2022" local i = -5 --nav position while i <= 5 do if skipgaps then y = ynogaps[i] else y = year + i*ygap*switchADBC - j end local BCdisp = if i ~= 0 then --left/right navy local AD = local BC = if year1to15AD and not (year1to10 and not year1to10ADBC) --don't AD/BC 1-10's if parents don't contain AD/BC then if year >= 11 then --parent = AD 11-15 if y <= 10 then --prepend AD on y = 1-10 cats only, per existing cats AD = 'AD ' end elseif year >= 1 then --parent = AD 1-10 if y <= 0 then BC = 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 cats AD = 'AD ' end end elseif parentBC then if switchADBC
0 then --switch from BC to AD regime switchADBC = 1 end end if switchADBC
true then catlink = catlinkNoAD --usurp elseif listall then tlistall[#tlistall] = tlistall[#tlistall]..' (tried; not displayed)1' end end if (AD..BC
false) and (y >= 1000) then --!ADBC & DNE; 4-digit only, to be frugal --try basic hyphenated cats: 1-year, endash, MOS-correct only, no #Rs local yHyph_4 = y..'–'..(y+1) --try 2010–2011 type cats local catlinkHyph_4 = catlinkfollowr(frame, firstpart..lspace..yHyph_4..tspace..BC..lastpart, yHyph_4) if catlinkHyph_4.catexists and catlinkHyph_4.rtarget
1 then local yHyph_2_special = (y-1)..'–'..string.match(y, '%d%d$') --try special case 2021 -> 2021–22 local 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
1 and j
nil then --exists & no #Rs catlink = catlinkHyph_2 --usurp trackcat(27, 'Category series navigation year and range') elseif listall then tlistall[#tlistall] = tlistall[#tlistall]..' (tried; not displayed)4' end end end end if catlink.rtarget then --#R followed; determine why local r = catlink.rtarget local c = catlink.cat local 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+' --stricter local num_regex = '%d+' --strip any number otherwise local final_regex = nil --best choice goes here if mw.ustring.match(r, year_regex) and mw.ustring.match(c, year_regex) then final_regex = year_regex elseif mw.ustring.match(r, num_regex) and mw.ustring.match(c, num_regex) then final_regex = num_regex end if final_regex then local r_base = mw.ustring.gsub(r, final_regex, ) local c_base = mw.ustring.gsub(c, final_regex, ) if r_base ~= c_base then trackcat(19, 'Category series navigation year redirected (base change)') --acceptable #R target elseif mw.ustring.match(r, hyph_regex) then trackcat(20, 'Category series navigation year redirected (var change)') --e.g. "2008 in Scottish women's football" to "2008–09" else trackcat(21, 'Category series navigation year redirected (other)') --exceptions go here end else trackcat(20, 'Category series navigation year redirected (var change)') --e.g. "V2 engines" to "V-twin engines" end end table.insert(navlist, catlink.navelement) else --OOB vs min/max local hidden = '
' table.insert(navlist, hidden) if listall then local dummy = catlinkfollowr(frame, firsttry, disp) tlistall[#tlistall] = tlistall[#tlistall]..' ('..hidden..')' end end else --center navy if parentBC then BCdisp = ' '..BCe end table.insert(navlist, ''..year..BCdisp..'') end i = i + 1 end --add the list navy = navy..horizontal(navlist)..'\n' isolatedcat if listall then return listalllinks else return navy..'' endend
--
local function nav_roman(frame, firstpart, roman, lastpart, minimumrom, maximumrom) local toarabic = require('Module:ConvertNumeric').roman_to_numeral local toroman = require('Module:Roman').main --sterilize/convert rom/num local num = tonumber(toarabic(roman)) local rom = toroman if num
nil then --out of range or some other error errors = 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/max local 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 error if maxrom < 1 then maxrom = 9999 end --toarabic returns -1 on error if minrom > num then minrom = num end if maxrom < num then maxrom = num end --begin navroman local navr = '
\n' local navlist = local i = -5 --nav position while i <= 5 do local n = num + i if n >= 1 then local r = toroman if i ~= 0 then --left/right navr local catlink = catlinkfollowr(frame, firstpart..' '..r..' '..lastpart, r) if minrom <= n and n <= maxrom then if catlink.rtarget then --a was followed trackcat(22, 'Category series navigation roman numeral redirected') end table.insert(navlist, catlink.navelement) else local hidden = '
' table.insert(navlist, hidden) if listall then tlistall[#tlistall] = tlistall[#tlistall]..' ('..hidden..')' end end else --center navr table.insert(navlist, ''..r..'') end else table.insert(navlist, ' ') end i = i + 1 end -- add the list navr = navr..horizontal(navlist)..'\n' isolatedcat if listall then return listalllinks else return navr..'' endend
--
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 & +/- qualifier local maxord = tonumber(string.match(maximumord or , '(-?%d+)[snrt]?[tdh]?')) or 9999 --allow full ord & +/- qualifier if 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 ordinal if string.match(lastpart, '^-') then tspace = end --DNE for "19th-century"-type cats --AD/BC switches & vars local ordBCElastparts = local parentBC = mw.ustring.match(lastpart, '%s(BCE?)') --"1st-century BC" format local lastpartNoBC = mw.ustring.gsub(lastpart, '%sBCE?', ) --easier than splitting lastpart up in 2; AD never used local BCe = parentBC or ordBCElastparts[lastpartNoBC] or 'BC' --"BC" default local switchADBC = 1 -- 1=AD parent if parentBC then switchADBC = -1 end -- -1=BC parent; possibly adjusted later local O = 0 --secondary iterator for AD-on-a-BC-parent if 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 <= parent if maxord < nord*switchADBC then maxord = nord*switchADBC end --input error; maxord should be >= parent --begin navnordinal local bnb = --border/no border if navborder
\n' local navlist = local i = -5 --nav position while i <= 5 do local o = nord + i*switchADBC local BC = local BCdisp = if parentBC then if switchADBC
0 then --switch to the AD side BC = switchADBC = 1 end end if switchADBC
' endend
--
local function nav_wordinal(frame, firstpart, word, lastpart, minimumword, maximumword, ordinal, frame) --Module:ConvertNumeric.spell_number2 args: -- ordinal
false: 'two' is output instead of 'second' local ord2eng = require('Module:ConvertNumeric').spell_number2 local eng2ord = require('Module:ConvertNumeric').english_to_ordinal local th = 'th' if not ordinal then th = eng2ord = require('Module:ConvertNumeric').english_to_numeral end local capitalize = nil ~= string.match(word, '^%u') --determine capitalization local nord = eng2ord(string.lower(word)) --operate on/with lowercase, and restore any capitalization later local 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 cats if string.match(lastpart, '^[%-%)]') then tspace = end --DNE for "Nine-cylinder engines"-type cats --sterilize min/max local maxword_default = 99 local maxword = maxword_default local minword = 1 if minimumword then local num = tonumber(minimumword) if num and 0 < num and num < maxword then minword = num else local ord = eng2ord(minimumword) if 0 < ord and ord < maxword then minword = ord end end end if maximumword then local num = tonumber(maximumword) if num and 0 < num and num < maxword then maxword = num else local ord = eng2ord(maximumword) if 0 < ord and ord < maxword then maxword = ord end end end if minword > nord then minword = nord end if maxword < nord then maxword = nord end --determine max existing cat local listoverride = true local n_max = nord local m = 1 while m <= 5 do local n = nord + m local nth = p.addord(n) if not ordinal then nth = n end local w = ord2eng local catlink = catlinkfollowr(frame, firstpart..lspace..w..tspace..lastpart, nth, nil, listoverride) if catlink.catexists then n_max = n end m = m + 1 end --begin navwordinal local navw = '
\n' local navlist = local prepad = local i = -5 --nav position while i <= 5 do local n = nord + i if n >= 1 then local nth = p.addord(n) if not ordinal then nth = n end if i ~= 0 then --left/right navw local w = ord2eng local catlink = catlinkfollowr(frame, firstpart..lspace..w..tspace..lastpart, nth) if minword <= n and n <= maxword then if catlink.rtarget then --a was followed trackcat(24, 'Category series navigation wordinal redirected') end if n <= n_max or maxword ~= maxword_default then table.insert(navlist, prepad..catlink.navelement) --display normally prepad = else local postpad = '
' navlist[#navlist] = (navlist[#navlist] or )..postpad if listall then tlistall[#tlistall] = tlistall[#tlistall]..' ('..postpad..')' end end else local postpad = ' ' navlist[#navlist] = navlist[#navlist]..postpad if listall then tlistall[#tlistall] = tlistall[#tlistall]..' ('..postpad..')' end end else --center navw table.insert(navlist, prepad..''..nth..'') prepad = end else --n < 1 prepad = prepad..' ' if listall then tlistall[#tlistall] = (tlistall[#tlistall] or )..' (x)' end end i = i + 1 end -- Add the list navw = navw..horizontal(navlist)..'\n' isolatedcat if listall then return listalllinks else return navw..'' endend
--
=
local function find_var(pn) --Extracts the variable text (e.g. 2015, 2015–16, 2000s, 3rd, III, etc.) from a string, --and returns local pagename = currtitle.text if pn and pn ~= then pagename = pn end local cpagename = 'Category:'..pagename --limited-Lua-regex workaround local 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 roman if found then if string.match(found, '%d%d%d%d%d')
----Main --
function p.csn(frame) --arg checks & handling local args = frame:getParent.args checkforunknownparams(args) --for template args checkforunknownparams(frame.args) --for #invoke'd args local cat = args['cat'] --'testcase' alias for catspace local list = args['list-all-links'] --debugging utility to output all links & followed #Rs local 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 ~= then if show
'term-limit' then return term_limit elseif show
'ygap-limit' then return ygap_limit end end --apply args local pagename = testcase or cat or currtitle.text local testcaseindent = if testcasecolon
'no' then followRs = false end if list and list
'yes' then skipgaps = true trackcat(26, 'Category series navigation using skip-gaps parameter') end --ns checks if currtitle.nsText
then trackcat(30, 'Category series navigation in mainspace') end --find the variable parts of pagename local findvar = find_var(pagename) 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
--call the appropriate nav function, in order of decreasing popularity if findvar.vtype
'AD' and dec <= 1 then firstpart_dec = if dec
'decade' then --e.g. "0s", "2010s"; nav_decade..nav_nordinal; ~12% of cats local 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 if century
'nordinal' then --e.g. "1st", "99th"; ~7.5% of cats return wrap(nav_nordinal(frame, firstpart, start, lastpart, minimum, maximum)..testcaseindent..table.concat(ttrackingcats)) elseif findvar.vtype
'tvseason' then --e.g. "1", "15" but preceded with "season" or "series"; <1% of cats return wrap(nav_tvseason(frame, firstpart, start, lastpart, maximum)..testcaseindent..table.concat(ttrackingcats)) --"minimum" defaults to 1 elseif findvar.vtype
'enumeric' then --e.g. "one", "ninety-nine"; <<1% of cats local ordinal = false return wrap(nav_wordinal(frame, firstpart, findvar.v, lastpart, minimum, maximum, ordinal, frame)..testcaseindent..table.concat(ttrackingcats)) elseif findvar.vtype
'ending' then --e.g. "2021–" (irregular; ending unknown); <<<1% of cats local hyphen, finish = mw.ustring.match(findvar.v, '%d([–-])present$'), -1 --ascii 150 & 45 (ndash & keyboard hyphen); mw req'd if hyphen
return p