Module:Category series navigation

require('strict')

local p = {}

local horizontal = require('Module:List').horizontal

local rtarget = require('Module:Resolve category redirect').rtarget

--==========================================================================

-- Globals

--==========================================================================

local currtitle = mw.title.getCurrentTitle()

local nexistingcats = 0

local errors = ''

local testcasecolon = ''

local testcases = string.match(currtitle.subpageText, '^testcases')

if testcases then testcasecolon = ':' end

local navborder = true

local followRs = true

local skipgaps = false

local skipgaps_limit = 50

local term_limit = 10

local hgap_limit = 6

local ygap_limit = 5

local listall = false

local 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') 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 -- phab:T369784

currtitle.nsText:gsub('_', ' ') ~= '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' ).exists

end

--Error message handling.

function p.errorclass( msg )

return mw.text.tag( 'span', {class='error mw-ext-cite-error'}, '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] = ''..testcasecolon..'Category:'..cat..''

else

ttrackingcats[key] = ''

end

end

return

end

--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) do

if knownparams[k] == nil then

trackcat(3, 'Category series navigation using unknown parameter')

break

end

end

end

--Check for nav_*() navigational isolation (not necessarily an error).

--Used by all nav_*().

local function isolatedcat()

if nexistingcats == 0 then

trackcat(16, 'Category series navigation isolated')

end

end

--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'] = ,

-- ['rtarget'] = nil,

-- ['navelement'] = ,

-- ['displaytext'] = displaytext,

-- }

-- 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 == 'present' then

disp = y..hyph..ending

elseif ending == '' then

disp = y..hyph..''..y..'' --hidden y to match spacing

end

end

link = ''..disp..''

else

link = ''..disp..''

end

else

link = ''..disp..''

end

if listall and listoverride == nil then

if nilorR then --#R followed

table.insert( tlistall, ':Category:'..cat..' → '..':Category:'..nilorR..' ('..link..')' )

else --no #R

table.insert( tlistall, ':Category:'..cat..' ('..link..')' )

end

end

return {

['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' then

errors = p.errorclass('The |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

end

end

--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 == '00' then return nil end --doesn't follow MOS:DATERANGE

if (#from == 4) and (#to == 2) then --1900-01

to = string.match(from, '(%d%d)%d%d')..to --1900-1901

elseif (#from == 2) and (#to == 4) then -- 01-1902

from = string.match(to, '(%d%d)%d%d')..from --1901-1902

end

return (tonumber(to) - tonumber(from))

end

return 0

end

--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 terminaltxt

end

--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 == '' 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

end

end

--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 worked

trackcat(17, 'Category series navigation default season gap size')

end

end

--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 == '1' then return s..'st'

elseif ones == '2' then return s..'nd'

elseif ones == '3' then return s..'rd' end

return s..'th'

end

return i

end

--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 element

if navborder == true then

return ''..catlink.displaytext..''

else

return ''..catlink.navelement..''

end

else

return catlink.navelement

end

end

--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..

'

'

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 start

if string.match(start or '', '^%d%d?%d?%d?$') == nil then --1-4 digits, AD only

local start_fixed = mw.ustring.match(start or '', '^%s*(%d%d?%d?%d?)%D')

if start_fixed then

start = start_fixed

else

errors = 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')

end

end

local nstart = tonumber(start)

--en dash check

if hyph ~= '–' then

trackcat(4, 'Category series navigation range not using en dash') --nav still processable, but track

end

--sterilize finish & check for weird parents

local tgaps = {} --table of gap sizes found b/w terms { [] = 1 } for -3 <= j <= 3

local tgapsj4 = {} --table of gap sizes found b/w terms { [] = 1 } for j = { -4, 4 }

local ttlens = {} --table of term lengths found w/i terms { [] = 1 }

local tirregs = {} --table of ir/regular-term-length cats' "from"s & "to"s found

local regularparent = true

if (finish == -1) or --"Members of the Scottish Parliament 2021–present"

(finish == 0) --"Members of the Scottish Parliament 2021–"

then

regularparent = false

if maxseas == nil or maxseas == '' then

maxseas = start --hide subsequent ranges

end

if finish == -1 then trackcat(14, 'Category series navigation range ends (present)')

else trackcat(15, 'Category series navigation range ends (blank, MOS)') end

elseif (start == finish) and

(ttrackingcats[16] ~= '') --nav_year found isolated; check for surrounding hyphenated terms (e.g. UK MPs 1974)

then

trackcat(16, '') --reset for another check later

trackcat(13, 'Category series navigation range irregular, 0-length')

ttlens[0] = 1 --calc ttlens for std cases below

regularparent = 'isolated'

end

if (string.match(finish or '', '^%d+$') == nil) and

(string.match(finish or '', '^%-%d+$') == nil)

then

local finish_fixed = mw.ustring.match(finish or '', '^%s*(%d%d?%d?%d?)%D')

if finish_fixed then

finish = finish_fixed

else

errors = 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')

end

else

if string.len(finish) >= 5 then

errors = 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')

end

end

local nfinish = tonumber(finish)

--save sterilized parent range for easier lookup later

tirregs['from0'] = nstart

tirregs['to0'] = nfinish

--sterilize min/max

local nminseas_default = -9999

local nmaxseas_default = 9999

local nminseas = tonumber(minseas) or nminseas_default --same behavior as nav_year

local nmaxseas = tonumber(maxseas) or nmaxseas_default --same behavior as nav_year

if nminseas > nstart then nminseas = nstart end

if nmaxseas < nstart then nmaxseas = nstart end

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 "Madrid city councillors (2007–2011)"-type cats

if string.match(lastpart, '^%)') then tspace = '' end --DNE for "Madrid city councillors (2007–2011)"-type cats

--calculate term length/intRAseason size & finishing year

local t = 1

while t <= term_limit and regularparent == true do

local nish = nstart + t --use switchADBC to flip this sign to work for years BC, if/when the time comes

if (nish == nfinish) or (string.match(nish, '%d?%d$') == finish) then

ttlens[t] = 1

break

end

if t == term_limit then

errors = p.errorclass('Function nav_hyphen can\'t determine a reasonable term length for "'..start..hyph..finish..'".')

return p.failedcat(errors, 'K')

end

t = t + 1

end

--apply MOS:DATERANGE to parent

local 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 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 == 2 then

trackcat(5, 'Category series navigation range abbreviated (MOS)')

elseif lenfinish ~= 4 then

errors = p.errorclass('The second part of the season passed to function nav_hyphen should be four digits, not "'..finish..'".')

return p.failedcat(errors, 'M')

end

end

if finish == '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 == true do --verify

local prevseason2 = firstpart..lspace..(nstart-t-hgap)..hyph..string.match(nstart-hgap, '%d?%d$') ..tspace..lastpart

local nextseason2 = firstpart..lspace..(nstart+t+hgap)..hyph..string.match(nstart+2*t+hgap, '%d?%d$')..tspace..lastpart

local prevseason4 = firstpart..lspace..(nstart-t-hgap)..hyph..(nstart-hgap) ..tspace..lastpart

local nextseason4 = firstpart..lspace..(nstart+t+hgap)..hyph..(nstart+2*t+hgap)..tspace..lastpart

if t == 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 == false then

hgap = 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" throughout

local jlimit = 4 --4-a-side if all YYYY-YY, 3-a-side if all YYYY-YYYY, with some threshold in between

if hgap <= hgap_limit_reg then --also to isolate temp vars

--find # of nav-visible ir/regular-term-length cats

local bwanchor = nstart --backward anchor/common year

local fwanchor = bwanchor + t --forward anchor/common year

if regularparent == 'isolated' then

fwanchor = bwanchor

end

local spangreen = '[j, g, k = ' --used for/when debugging via list-all-links=yes

local spanblue = ''

local spanred = ' ('

local 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 then

if regularparent ~= 'isolated' then --+restrict to g == 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 == '0-length') --ghetto "continue" (thx Lua) to avoid expensive searches for "UK MPs 1974-1974", etc.

then

table.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 series

local to2 = string.match(to, '%d%d$')

if to2 and to2 ~= '00' then --and not at a century transition (i.e. 1999–2000)

to = to2

full = mw.text.trim( firstpart..lspace..from..hyph..to..tspace..lastpart )

table.insert( tlistallbwd, spangreen..j..', '..g..', '..k..span..'] '..full )

end

end

if catexists(full) then

if to == '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 == -1 then tgapsj4[g] = 1 -- -1 since bwd search starts from parent @ -4 and ends at -1

else tgaps[g] = 1 end

iirregs = iirregs + 1

tirregs['from-'..iirregs] = from

tirregs['to-'..iirregs] = to

bwanchor = from --ratchet down

if to ~= '0-length' then

gbreak = true

break

else

g = 0 --soft-reset g, to keep stepping thru k

j = j + 1 --save, but keep searching thru k

if 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 break

gbreak = true

break

end

end

elseif (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 break

gbreak = true

break

end

end --ghetto "continue"

k = k + 1

lastk = k

end --while k <= term_limit do

if gbreak == 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 == false then --search forward 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 = -2 --term length: -2 = "0-length", -1 = "2020–present", 0 = "2020–", 1+ = normal

while k <= term_limit do

local from = fwanchor + g

local to4 = fwanchor + k + g --override carefully

local to2 = nil --last 2 digits of to4, IIF exists

if 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 then

if regularparent ~= 'isolated' then --+restrict to g == 0 if repeating year problems arise

to4 = '0-length' --see if 0-length cat exists

full = mw.text.trim( firstpart..lspace..from..tspace..lastpart )

if catlinkfollowr( frame, full ).rtarget ~= nil then --#R followed

table.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 messy

end

end

end

if (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.

then

table.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 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 == '0-length' then

if rtarget(full, frame) == 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 == -1) or (k == 0) then

endfound = true --tentative

else --k == { -2, > 0 }

tlistallfwd[#tlistallfwd] = spanblue..tlistallfwd[#tlistallfwd]..span..' (found)'

ttlens[ find_duration(full) ] = 1

if j == 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 == -2

j = j + 1 --save, but keep searching k's, in case "1974" → "1974-1979"

if j > jlimit then --lest we keep searching & finding 0-length cats ("2018 CONCACAF Champions League" & up)

gbreak = true

break

elseif g == 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 == true then break end

g = g + 1

lastg = g

end --while g <= hgap_limit_irreg do

end --if j > 0 and endfound == 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 == false and tirregs and i > 0 then

to = tirregs['to-1'] --special treatment for parent terminal cats, since they have no natural 'to'

end

local 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)

then

tofinal = (to or '') --default to the MOS-correct format, in case no fallbacks found

end

if to == '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) == 4 then --e.g. 1999-2004

--determine which link exists (full or abbr)

local full = firstpart..lspace..from..hyph..tofinal..tspace..lastpart

if not catexists(full) then

local abbr = firstpart..lspace..from..hyph..to2..tspace..lastpart

if catexists(abbr) then

tofinal = (to2 or '') --rv to MOS-incorrect format; if full AND abbr DNE, then tofinal is still in its MOS-correct format

end

end

elseif t == 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 == '0-length' then

orig = firstpart..lspace..from..tspace..lastpart

disp = from

end

local catlink = catlinkfollowr(frame, orig, disp, true) --force terminal cat display

if terminalcat == false then

terminaltxt = find_terminaltxt( disp ) --also sets tracking cats

terminalcat = (terminaltxt ~= nil)

end

if catlink.rtarget and avoidself then --a {{Category redirect}} 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 == nil then

local lastfrom = nstart + (i-1)*(t+hgap)

lastto = lastfrom+t --use last logical 'from' to calc lastto

end

if lastto then

local gapcat = lastto..'-'..from --dummy cat to calc with

local gap = find_duration(gapcat) or -1 --in case of nil,

if iwidth == 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 == 0 then

local base_regex_lax = '%d%d%d%d' --in case rtarget is a year cat

rtarbase, rtarbase_success = mw.ustring.gsub(catlink.rtarget, base_regex_lax, '')

end

local terminal_regex = '%d+[–-]'..(terminaltxt or '')..'$' --more manual ORs bc Lua regex sux

if mw.ustring.match(orig, terminal_regex) then

origbase = mw.ustring.gsub(orig, terminal_regex, '')

end

if mw.ustring.match(catlink.rtarget, terminal_regex) then

--finagle/overload terminalcat type to set nmaxseas on 1st occurence only

if terminalcat == 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 == 1 then

trackcat(8, 'Category series navigation range redirected (end)')

else --origbase == 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 = ''..disp..''

table.insert(navlist, hidden)

if listall then

tlistall[#tlistall] = tlistall[#tlistall]..' ('..hidden..')'

end

end

else --center navh

if finish == -1 then finish = 'present'

elseif finish == 0 then finish = ''..start..'' end

local disp = start..hyph..finish

if regularparent == 'isolated' then disp = start end

table.insert(navlist, ''..disp..'')

end

i = i + 1

end

-- add the list

navh = navh..horizontal(navlist)..'\n'

--tracking cats & finalize

if avoidself then

local igaps = 0 --# of diff gap sizes > 0 found

local itlens = 0 --# of diff term lengths found

for s = 1, hgap_limit_reg do --must loop; #tgaps, #ttlens unreliable

igaps = igaps + (tgaps[s] or 0)

end

if iwidth == 4 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] == '' then --avoid duplication in "Category series navigation range irregular, 0-length"

trackcat(12, 'Category series navigation range irregular')

end

end

isolatedcat()

defaultgapcat(not hgap_success)

if listall then

return listalllinks()

else

return navh..'

'

end

end

--=========================={{ 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 then

errors = p.errorclass('Function nav_tvseason can\'t recognize the TV season number sent to its 3rd 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 case

local maxtv_default = 9999

local maxtv = tonumber(maximumtv) or maxtv_default --allow +/- qualifier

if maxtv < tv then maxtv = tv end --input error; maxtv should be >= parent

--begin navtvseason

local navt = '

\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 {{Category redirect}} 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 = ' • '..t..''

navlist[#navlist] = (navlist[#navlist] or '')..postpad

if listall then tlistall[#tlistall] = tlistall[#tlistall]..' ('..postpad..')' end

end

elseif t < 1 then

prepad = prepad..' • '..'0'..''

if listall then tlistall[#tlistall] = (tlistall[#tlistall] or '')..' (x)' end

else --t > maxtv

local postpad = ' • '..t..''

navlist[#navlist] = (navlist[#navlist] or '')..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..'

'

end

end

--==========================={{ 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 dec

local dec = sterilizedec(decade)

if dec == nil then

errors = 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')

end

local ndec = tonumber(dec)

--sterilize mindecade & determine AD/BC

local mindefault = '-9999'

local mindec = sterilizedec(mindecade) --returns a tostring(unsigned int), or nil

if mindec then

if string.match(mindecade, '-%d') or

string.match(mindecade, 'BC')

then

mindec = '-'..mindec --better +/-0 behavior with strings (0-initialized int == "-0" string...)

end

elseif mindec == nil and mindecade and mindecade ~= '' then

errors = 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 == 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 == "-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 ~= '' 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 == nil

maxdec = maxdefault

end

local tspace = ' ' --assume trailing space for "1950s in X"-type cats

if string.match(lastpart, '^-') then tspace = '' end --DNE for "1970s-related"-type cats

--AD/BC switches & vars

local parentBC = string.match(lastpart, '^BC') --following the "0s BC" convention for all years BC

lastpart = mw.ustring.gsub(lastpart, '^BC%s*', '') --handle BC separately; AD never used

--TODO?: handle BCE, but only if it exists in the wild

local dec0to40AD = (ndec >= 0 and ndec <= 40 and not parentBC) --special behavior in this range

local switchADBC = 1 -- 1=AD parent

if parentBC then switchADBC = -1 end -- -1=BC parent; possibly adjusted later

local BCdisp = ''

local D = -math.huge --secondary switch & iterator for AD/BC transition

--check non-default min/max more carefully

if mindec ~= mindefault then

if tonumber(mindec) > ndec*switchADBC then

mindec = tostring(ndec*switchADBC) --input error; mindec should be <= parent

end

end

if maxdec ~= maxdefault then

if tonumber(maxdec) < ndec*switchADBC then

maxdec = tostring(ndec*switchADBC) --input error; maxdec should be >= parent

end

end

local nmindec = tonumber(mindec) --similar behavior to nav_year & nav_nordinal

local nmaxdec = tonumber(maxdec) --similar behavior to nav_nordinal

--begin navdecade

local bnb = '' --border/no border

if navborder == 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 == 0 then

D = -10 --track 1st d = 0 use (BC)

end

elseif D >= -10 then

D = D + 10 --now iterate from 0s AD

d = D --2nd d = 0 use

end

elseif parentBC then

if 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 iteration

switchADBC = 1 --1st d = 0 use (BC)

D = -10 --prep

end

elseif switchADBC == 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 {{Category redirect}} was followed

trackcat(18, 'Category series navigation decade redirected')

end

--populate left/right navd

local shown = navcenter(i, catlink)

local hidden = ''..disp..''

local dsign = d --use d for display & dsign for logic

if BC ~= '' then dsign = -dsign end

if (nmindec <= dsign) and (dsign <= nmaxdec) then

if 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 processing

local zsign, zmin, zmax = 1, nmindec, nmaxdec

if BC ~= '' then zsign = -1 end

if mindec == '-0' then zmin = -1

elseif mindec == '0' then zmin = 1 end

if maxdec == '-0' then zmax = -1

elseif maxdec == '0' then zmax = 1 end

if (zmin <= zsign) and (zsign <= zmax) then

table.insert(navlist, shown)

hidden = nil

else

table.insert(navlist, hidden)

end

else

table.insert(navlist, shown)--the common case

hidden = nil

end

else

table.insert(navlist, hidden)

end

if listall and hidden then

tlistall[#tlistall] = tlistall[#tlistall]..' ('..hidden..')'

end

i = i + 10

end

-- add the list

navd = navd..horizontal(navlist)..'\n'

isolatedcat()

if listall then

return listalllinks()

else

return navd..'

'

end

end

--============================{{ 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 = -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 == nil then

errors = p.errorclass('Function nav_year can\'t recognize the year sent to its 3rd parameter.')

return p.failedcat(errors, 'Y')

end

--AD/BC switches & vars

local 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 10

local parentBC = string.match(lastpart, '^BCE?') --following the "1 BC" convention for all years BC

firstpart = mw.ustring.gsub(firstpart, '%s*AD$', '') --handle AD/BC separately for easier & faster accounting

lastpart = mw.ustring.gsub(lastpart, '^BCE?%s*', '')

local BCe = parentBC or yearBCElastparts[lastpart] or 'BC' --"BC" default

local year1to10 = (year >= 1 and year <= 10)

local year1to10ADBC = year1to10 and (parentBC or parentAD) --special behavior 1-10 for low-# non-year series

local year1to15AD = (year >= 1 and year <= 15 and not parentBC) --special behavior 1-15 for AD/BC display

local switchADBC = 1 -- 1=AD parent

if parentBC then switchADBC = -1 end -- -1=BC parent; possibly adjusted later

local Y = 0 --secondary iterator for AD-on-a-BC-parent

if minyear > year*switchADBC then minyear = year*switchADBC end --input error; minyear should be <= parent

if maxyear < year*switchADBC then maxyear = year*switchADBC end --input error; maxyear should be >= parent

local lspace = ' ' --leading space before year, after firstpart

if string.match(firstpart, '[%-VW]$') then

lspace = '' --e.g. "Straight-8 engines"

end

local tspace = ' ' --trailing space after year, before lastpart

if string.match(lastpart, '^-') then

tspace = '' --e.g. "2018-related timelines"

end

--determine interyear gap size to condense special category types, if possible

local ygapdefault = 1 --assume/start at the most common case: 2001, 2002, etc.

local ygap = ygapdefault

if string.match(lastpart, 'presidential') then

local ygap1, ygap2 = ygapdefault, ygapdefault --need to determine previous & next year gaps indepedently

local ygap1_success, ygap2_success = false, false

local prevseason = nil

while ygap1 <= ygap_limit do --Czech Republic, Poland, Sri Lanka, etc. have 5-year terms

prevseason = firstpart..lspace..(year-ygap1)..tspace..lastpart

if catexists(prevseason) then

ygap1_success = true

break

end

ygap1 = ygap1 + 1

end

local nextseason = nil

while ygap2 <= ygap_limit do --Czech Republic, Poland, Sri Lanka, etc. have 5-year terms

nextseason = firstpart..lspace..(year+ygap2)..tspace..lastpart

if catexists(nextseason) then

ygap2_success = true

break

end

ygap2 = ygap2 + 1

end

if ygap1_success and ygap2_success then

if ygap1 == 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 == minyear_default then

minyear = 0 --automatically set minyear to 0, as AD/BC not supported anyway

end

if (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)

then

local yskipped = {} --track skipped y's to avoid double-checking

local cat, found, Yeary

--populate nav element queue outwards positively from the parent

local Year = year --to save/ratchet progression

local i = 1

while i <= 5 do

local y = 1

while y <= skipgaps_limit do

found = false

Yeary = Year + y

if yskipped[Yeary] == 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] == 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

else

skipgaps = false --TODO: AD/BC support, then lift BC restrictions @ Template:Establishment category BC & Template:Year category header/core

end

end

--begin navyears

local navy = '

\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 == -1 then --displayed y is in the BC regime

if y >= 1 then --the common case

BC = BCe..' '

elseif y == 0 then --switch from BC to AD regime

switchADBC = 1

end

end

if switchADBC == 1 then --displayed y is now in the AD regime

Y = Y + 1 --skip y = 0 (DNE)

y = Y --easiest solution: start another iterator for these AD y's displayed on a BC year parent

AD = 'AD '

end

end

if 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 navy

local ysign = y --use y for display & ysign for logic

local disp = y..BCdisp

if BC ~= '' then ysign = -ysign end

local firsttry = firstpart..lspace..AD..y..tspace..BC..lastpart

if (minyear <= ysign) and (ysign <= maxyear) then

local catlinkAD = catlinkfollowr( frame, firsttry, disp ) --try AD

local catlink = catlinkAD --tentative winner

if AD ~= '' then --for "ACArt with 5 suppressed elements"-type cats

local catlinkNoAD = catlinkfollowr( frame, firstpart..lspace..y..tspace..BC..lastpart, disp ) --try !AD

if catlinkNoAD.catexists == true then

catlink = catlinkNoAD --usurp

elseif listall then

tlistall[#tlistall] = tlistall[#tlistall]..' (tried; not displayed)1'

end

end

if (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 #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 == nil then --exists & no #Rs

catlink = catlinkHyph_4 --usurp

trackcat(27, 'Category series navigation year and range')

else

if listall then

tlistall[#tlistall] = tlistall[#tlistall]..' (tried; not displayed)2'

end

local yHyph_2 = y..'–'..string.match(y+1, '%d%d$') --try 2010–11 type cats

if i == 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 == nil then --exists & no #Rs

catlink = catlinkHyph_2_special --usurp

trackcat(27, 'Category series navigation year and range')

j = 1

elseif listall then

tlistall[#tlistall] = tlistall[#tlistall]..' (tried; not displayed)3'

end

end

if not (i == 1 and j == 1) then

local 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 #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 = ''..disp..''

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..'

'

end

end

--==========================={{ nav_roman }}==============================

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({ [1] = num })

if num == nil or rom == 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({ [1] = n })

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 {{Category redirect}} was followed

trackcat(22, 'Category series navigation roman numeral redirected')

end

table.insert(navlist, catlink.navelement)

else

local hidden = ''..r..''

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, 'I')

end

i = i + 1

end

-- add the list

navr = navr..horizontal(navlist)..'\n'

isolatedcat()

if listall then

return listalllinks()

else

return navr..'

'

end

end

--=========================={{ 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 & +/- 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 = { --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" 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 == false then --for Category series navigation decade and century

bnb = 'categorySeriesNavigation-range-transparent'

end

local navo = '

\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 == -1 then --parentBC looking at the BC side

if o >= 1 then --the common case

BC = ' '..BCe

elseif o == 0 then --switch to the AD side

BC = ''

switchADBC = 1

end

end

if switchADBC == 1 then --displayed o is now in the AD regime

O = O + 1 --skip o = 0 (DNE)

o = O --easiest solution: start another iterator for these AD o's displayed on a BC year parent

end

elseif o <= 0 then --parentAD looking at BC side

BC = ' '..BCe

o = math.abs(o - 1) --skip o = 0 (DNE)

end

if 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 navo

local oth = p.addord(o)

local osign = o --use o for display & osign for logic

if BC ~= '' then osign = -osign end

local hidden = ''..oth..''

if temporal then --e.g. "3rd-century BC"

local lastpart = lastpartNoBC --lest we recursively add multiple "BC"s

if BC ~= '' then

lastpart = string.gsub(lastpart, temporal, temporal..BC) --replace BC if needed

end

local catlink = catlinkfollowr( frame, firstpart..' '..oth..tspace..lastpart, oth..BCdisp )

if (minord <= osign) and (osign <= maxord) then

if catlink.rtarget then --a {{Category redirect}} was followed

trackcat(23, 'Category series navigation nordinal redirected')

end

table.insert(navlist, navcenter(i, catlink))

else

table.insert(navlist, hidden)

if listall then

tlistall[#tlistall] = tlistall[#tlistall]..' ('..hidden..')'

end

end

elseif 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 followed

trackcat(23, 'Category series navigation nordinal redirected')

end

table.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 arise

table.insert(navlist, hidden)

end

i = i + 1

end

navo = navo..horizontal(navlist)..'\n'

isolatedcat()

if listall then

return listalllinks()

else

return navo..'

'

end

end

--========================={{ 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_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{ num = n, ordinal = ordinal, capitalize = capitalize }

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{ num = n, ordinal = ordinal, capitalize = capitalize }

local catlink = catlinkfollowr( frame, firstpart..lspace..w..tspace..lastpart, nth )

if minword <= n and n <= maxword then

if catlink.rtarget then --a {{Category redirect}} 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 = ' • '..nth..''

navlist[#navlist] = (navlist[#navlist] or '')..postpad

if listall then tlistall[#tlistall] = tlistall[#tlistall]..' ('..postpad..')' end

end

else

local postpad = ' • '..nth..''

navlist[#navlist] = (navlist[#navlist] or '')..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..' • '..'0'..th..''

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..'

'

end

end

--==========================={{ 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.>, = <2015|2015–16|etc.> }

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+)') or

mw.ustring.match(cpagename, 'meetup (%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') == nil then

--return in order of decreasing complexity/chance for duplication

if nordinal and season --i.e. "18th-century establishments in the Province of Quebec (1763–1791)"

then return { ['vtype'] = 'nordinal', ['v'] = nordinal } end

if d_season then return { ['vtype'] = 'decade', ['v'] = d_season } end

if y_season then return { ['vtype'] = 'year', ['v'] = y_season } end

if e_season then return { ['vtype'] = 'ending', ['v'] = e_season } end

if season then return { ['vtype'] = 'season', ['v'] = season } end

if tvseason then return { ['vtype'] = 'tvseason', ['v'] = tvseason } end

if nordinal then return { ['vtype'] = 'nordinal', ['v'] = nordinal } end

if decade then return { ['vtype'] = 'decade', ['v'] = decade } end

if year then return { ['vtype'] = 'year', ['v'] = year } end

if roman then return { ['vtype'] = 'roman', ['v'] = roman } end

end

else

--try wordinals ('zeroth' to 'ninety-ninth' only)

local eng2ord = require('Module:ConvertNumeric').english_to_ordinal

local split = mw.text.split(pagename, ' ')

for i=1, #split do

if eng2ord(split[i]) > -1 then

return { ['vtype'] = 'wordinal', ['v'] = split[i] }

end

end

--try English numerics ('one'/'single' to 'ninety-nine' only)

local eng2num = require('Module:ConvertNumeric').english_to_numeral

local split = mw.text.split(pagename, '[%s%-]') --e.g. "Nine-cylinder engines"

for i=1, #split do

if eng2num(split[i]) > -1 then

return { ['vtype'] = 'enumeric', ['v'] = split[i] }

end

end

end

errors = 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 & 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 == 'skip-gaps' then return skipgaps_limit

elseif show == 'term-limit' then return term_limit

elseif show == 'hgap-limit' then return hgap_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 == ':' then testcaseindent = '\n::' end

if follow and follow == 'no' then followRs = false end

if list and list == 'yes' then listall = true end

if skip_gaps and skip_gaps == 'yes' then

skipgaps = true

trackcat(26, 'Category series navigation using skip-gaps parameter')

end

--ns checks

if currtitle.nsText == 'Category' then

if cat and cat ~= '' then

trackcat(1, 'Category series navigation using cat parameter')

end

if testcase and testcase ~= '' then

trackcat(2, 'Category series navigation using testcase parameter')

end

elseif 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 == 'error' then --basic format error checking in find_var()

return findvar.v..table.concat(ttrackingcats)

end

local start = string.match(findvar.v, '^%d+')

--the rest is static

local 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 then

firstpart, lastpart = string.match(pagename, '^(.-series )'..findvar_escaped..'(.*)$')

end

if firstpart == nil then

firstpart, lastpart = string.match(pagename, '^(.-meetup )'..findvar_escaped..'(.*)$')

end

end

firstpart = mw.text.trim(firstpart or '')

lastpart = mw.text.trim(lastpart or '')

--call the appropriate nav function, in order of decreasing popularity

if findvar.vtype == 'year' then --e.g. "500", "2001"; nav_year..nav_decade; ~75% of cats

local nav1 = nav_year( frame, firstpart, start, lastpart, minimum, maximum )..testcaseindent..table.concat(ttrackingcats)

local dec = math.floor(findvar.v/10)

local decadecat = nil

local firstpart_dec = firstpart

if firstpart_dec ~= '' then

firstpart_dec = firstpart_dec..' the'

elseif firstpart_dec == 'AD' and dec <= 1 then

firstpart_dec = ''

if dec == 0 then dec = '' end

end

local decade = dec..'0s '

decadecat = mw.text.trim( firstpart_dec..' '..decade..lastpart )

local exists = catexists(decadecat)

if exists then

navborder = false

trackcat(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 = start

local 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_year

else return wrap( nav2 ) end

else --regular nav_year

return wrap( nav1 )

end

elseif findvar.vtype == '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 {{CENTURY}}

if century == 0 then century = 1 end --no 0th century

if string.match(decade, '00$') then

century = century + 1 --'2000' is in the 20th, but the rest of the 2000s is in the 21st

end

local clastpart = ' century '..lastpart

local centurycat = mw.text.trim( firstpart..' '..p.addord(century)..clastpart )

local exists = catexists(centurycat)

if not exists then --check for hyphenated century

clastpart = '-century '..lastpart

centurycat = mw.text.trim( firstpart..' '..p.addord(century)..clastpart )

exists = catexists(centurycat)

end

if exists then

navborder = false

trackcat(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 )

else

return wrap( nav1 )

end

elseif findvar.vtype == '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 == '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'd

return 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 cats

return wrap( nav_tvseason( frame, firstpart, start, lastpart, maximum )..testcaseindent..table.concat(ttrackingcats) ) --"minimum" defaults to 1

elseif findvar.vtype == 'wordinal' then --e.g. "first", "ninety-ninth"; <<1% of cats

local ordinal = true

return 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 cats

local ordinal = false

return 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 cats

return 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 cats

local hyphen, finish = mw.ustring.match(findvar.v, '%d([–-])present$'), -1 --ascii 150 & 45 (ndash & keyboard hyphen); mw req'd

if hyphen == nil then

hyphen, finish = mw.ustring.match(findvar.v, '%d([–-])$'), 0 --0/-1 are hardcoded switches for nav_hyphen()

end

return wrap( nav_hyphen( frame, start, hyphen, finish, firstpart, lastpart, minimum, maximum, testcasegap )..testcaseindent..table.concat(ttrackingcats) )

else --malformed

errors = p.errorclass('Failed to determine the appropriate nav function from malformed season "'..findvar.v..'". ')

return p.failedcat(errors, 'N')..table.concat(ttrackingcats)

end

end

return p