Mô đun:Category series navigation/sandbox

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
, -- [2] placeholder for
, -- [3] placeholder for
, -- [4] placeholder for
, -- [5] placeholder for
, -- [6] placeholder for
, -- [7] placeholder for
, -- [8] placeholder for
, -- [9] placeholder for
, --[10] placeholder for
, --[11] placeholder for
, --[12] placeholder for
, --[13] placeholder for
, --[14] placeholder for
, --[15] placeholder for
, --[16] placeholder for
, --[17] placeholder for
, --[18] placeholder for
, --[19] placeholder for
, --[20] placeholder for
, --[21] placeholder for
, --[22] placeholder for
, --[23] placeholder for
, --[24] placeholder for
, --[25] placeholder for
, --[26] placeholder for
, --[27] placeholder for
, --[28] placeholder for
, --[29] placeholder for
, --[30] placeholder for
, --[31] placeholder for
}
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('_', ' ') ~= 'Thảo luận Thành viên' and -- phab:T369784
currtitle.nsText:gsub('_', ' ') ~= 'Thảo luận Bản mẫu' and
(currtitle.nsText ~= 'Bản_mẫu' 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, 'Thể_loại' ).exists
end

--Error message handling.
function p.errorclass( msg )
return mw.text.tag( 'span', {class='error mw-ext-cite-error'}, 'Lỗi! '..string.gsub(msg, '&', '&amp;') )
end

--Failure handling.
function p.failedcat( errors, sortkey )
if avoidself then
return (errors or )..'&42;&42;&42;Category series navigation tạo navbox không thành công'..
..(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..'Thể loại:'..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 sử dụng tham số không rõ')
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 cô lập')
end
end

--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 {
-- ['cat'] = cat,
-- ['catexists'] = true,
-- ['rtarget'] = ,
-- ['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 == 'nay' 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, ':Thể loại:'..cat..' → '..':Thể loại:'..nilorR..' ('..link..')' )
else --no R
table.insert( tlistall, ':Thể loại:'..cat..' ('..link..')' )
end
end

return {
['cat'] = cat,
['catexists'] = exists,
['rtarget'] = nilorR,
['navelement'] = link,
['displaytext'] = disp,
}
end

--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 == 'Thể_loại' then
errors = p.errorclass('Tham số/công cụ |list-all-links=yes '..
'không nên được lưu trong không gian thể loại, mà chỉ có thể xem trước.')
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..'Không tìm thấy liên kết!?'..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 CNBS: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+[–-]nay$') then
terminaltxt = 'nay'
trackcat(14, 'Category series navigation có khoảng kết thúc (hiện tại)')
elseif mw.ustring.match(cat, '%d+[–-]$') then
terminaltxt =
trackcat(15, 'Category series navigation có khoảng kết thúc (trắng, CNBS)')
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 có khoảng cách thời gian mặc định')
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 end

local ones = string.match(s, '%d$')
if ones '1' then return s
elseif ones '2' then return s
elseif ones '3' then return s end

return s
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..
..
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?$') == 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('Hàm nav_hyphen không thể nhận ra số "'..(start or )..'" '..
'trong phần đầu của tham số "season" được đưa vào nó. '..
'V.d. "2015–16", "2015" lẽ ra phải được nhận ra bằng "|2015|–|16|".')
return p.failedcat(errors, 'H')
end
end
local nstart = tonumber(start)

--en dash check
if hyph ~= '–' then
trackcat(4, 'Category series navigation có khoảng không sử dụng 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 ] = 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 có khoảng kết thúc (hiện tại)')
else trackcat(15, 'Category series navigation có khoảng kết thúc (trắng, CNBS)') 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 có khoảng bất thường, độ dài 0')
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('Hàm nav_hyphen không thể nhận ra số "'..(finish or )..'" '..
'trong phần thứ hai của tham số "season" được đưa vào nó. '..
'V.d. "2015–16", "16" lẽ ra phải được nhận ra bằng "|2015|–|16|".')
return p.failedcat(errors, 'I')
end
else
if string.len(finish) >= 5 then
errors = p.errorclass('Phần thứ hai của tham số season đưa vào hàm nav_hyphen chỉ nên có bốn số hoặc ít hơn, không phải "'..(finish or )..'". '..
'Xem CNBS:DATERANGE để biết thêm thông tin.')
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 1; track "2001–05"; anything else = error
if lenfinish == 2 then
trackcat(5, 'Category series navigation có khoảng viết tắt (CNBS)')
elseif lenfinish ~= 4 then
errors = p.errorclass('Phần thứ hai của tham số season đưa vào hàm nav_hyphen chỉ nên có bốn số, không phải "'..finish..'".')
return p.failedcat(errors, 'M')
end
end
if finish == '00' then --full year required regardless of term length
trackcat(5, 'Category series navigation có khoảng viết tắt (CNBS)')
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 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, CNBS: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 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 & pseudo navh position
while j = 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 CNBS: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 có khoảng bất thường, độ dài 0')
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 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 = -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 CNBS: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 có khoảng bất thường, độ dài 0')
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 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 break end --fwd search exhausted
end

j = j + 1
end --while j \n'

local navlist = {}
local terminalcat = false --switch used to hide future cats
local terminaltxt = nil
local i = -iwidth --nav position
while 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
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 CNBS:DATERANGE (e.g. 1999-2004)
(from2 - (to2 or from2)) > 0 --century transition exception (e.g. 1999–2000)
then
tofinal = (to or ) --default to the CNBS-correct format, in case no fallbacks found
end
if to == '0-length' then
tofinal = to
end

--check existance of 4-digit, CNBS-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 CNBS-incorrect format; if full AND abbr DNE, then tofinal is still in its CNBS-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 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 có khoảng là đổi hướng (thay đổi gốc)')
elseif terminalcat 1 then
trackcat(8, 'Category series navigation có khoảng là đổi hướng (kết thúc)')
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 có khoảng là đổi hướng (khác)')
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 có khoảng là đổi hướng (thay đổi var)')
else
trackcat(9, 'Category series navigation có khoảng là đổi hướng (CNBS)')
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 '..disp..
table.insert(navlist, hidden)
if listall then
tlistall[tlistall] = tlistall[tlistall]..' ('..hidden..')'
end
end
else --center navh
if finish == -1 then finish = 'nay'
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 có khoảng với chu kỳ') end
if itlens > 1 and ttrackingcats[13] then --avoid duplication in "Category series navigation range irregular, 0-length"
trackcat(12, 'Category series navigation có khoảng bất thường')
end
end
isolatedcat()
defaultgapcat(not hgap_success)
if listall then
return listalllinks()
else
return navh..
end
end

--

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('Hàm nav_tvseason không thể nhận ra số mùa truyền hình được đưa vào tham số thứ 3 của nó.')
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/1216885280Follow-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 = parent

--begin navtvseason
local navt = '\n'

local navlist = {}
local prepad =
local i = -5 --nav position
while i = 1 and t • '..t..
navlist[navlist] = (navlist[navlist] or )..postpad
if listall then tlistall[tlistall] = tlistall[tlistall]..' ('..postpad..')' end
end
elseif t • '..'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

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

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('Hàm nav_decade đã được gửi "'..(decade or )..'" như là tham số thứ 2, '..
'nhưng nó cần năm từ 1 đến 4 số kết thúc bằng số "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, 'TCN')
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('Hàm nav_decade đã được gửi "'..(mindecade or )..'" như là tham số thứ 4, '..
'nhưng nó cần năm từ 1 đến 4 số kết thúc bằng số "0", chỉ thập niên đầu tiên cần được hiển thị.')
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, 'TCN')
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('Hàm nav_decade đã được gửi "'..(maxdecade or )..'" như là tham số thứ 5, '..
'nhưng nó cần năm từ 1 đến 4 số kết thúc bằng số "0", chỉ thập niên cuối cùng cần được hiển thị.')
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, '^TCN') --following the "0s BC" convention for all years BC
lastpart = mw.ustring.gsub(lastpart, '^TCN%s', ) --handle BC separately; AD never used
--TODO?: handle BCE, but only if it exists in the wild

local dec0to40AD = (ndec >= 0 and ndec ndecswitchADBC then
mindec = tostring(ndecswitchADBC) --input error; mindec 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 = -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 = 'TCN '
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 '..disp..
local dsign = d --use d for display & dsign for logic
if BC ~= then dsign = -dsign end
if (nmindec '
end
end

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

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 , 'TCN') then minyear = -math.abs(minyear) end --allow BC qualifier (AD otherwise assumed)
if string.match(maximumyear or , 'TCN') then maxyear = -math.abs(maxyear) end --allow BC qualifier (AD otherwise assumed)

if year == nil then
errors = p.errorclass('Hàm nav_year không thể nhận ra năm được đưa vào tham số thứ 2 của nó.')
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 = nil --following the "AD 1" convention from AD 1 to AD 10
local parentBC = string.match(lastpart, '^TCN?') --following the "1 BC" convention for all years BC
firstpart = mw.ustring.gsub(firstpart, '%sSCN$', ) --handle AD/BC separately for easier & faster accounting
lastpart = mw.ustring.gsub(lastpart, '^TCN?%s', )
local BCe = parentBC or yearBCElastparts[lastpart] or 'TCN' --"TCN" default

local year1to10 = (year >= 1 and year = 1 and year yearswitchADBC then minyear = yearswitchADBC end --input error; minyear 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"
elseif lastpart == then
tspace =
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 70) or --add support for AD/BC (= 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
mw.log("Checking cat: '" .. cat .. "'...")
found = catexists(cat)
mw.log(found)
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 = 11 then --parent = AD 11-15
if y = 1 then --parent = AD 1-10
if y = 1 and 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 =
end
end
if BC ~= and year 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, CNBS-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 năm và khoảng')
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 năm và khoảng')
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 năm và khoảng')
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 có năm đã đổi hướng (thay đổi gốc)') --acceptable R target
elseif mw.ustring.match(r, hyph_regex) then
trackcat(20, 'Category series navigation có năm đã đổi hướng (thay đổi var)') --e.g. "2008 in Scottish women's football" to "2008–09"
else
trackcat(21, 'Category series navigation có năm đã đổi hướng (khác)') --exceptions go here
end
else
trackcat(20, 'Category series navigation có năm đã đổi hướng (thay đổi var)') --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

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

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('Hàm nav_roman không thể nhận ra một hoặc nhiều "'..(num or 'nil')..'" & "'..
(rom or 'nil')..'" trong thể loại "'..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 num then minrom = num end
if maxrom \n'

local navlist = {}
local i = -5 --nav position
while i = 1 then
local r = toroman({ [1] = n })
if i ~= 0 then --left/right navr
local catlink = catlinkfollowr( frame, firstpart..' '..r..' '..lastpart, r )
if minrom '..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

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

local function nav_nordinal( frame, firstpart, ord, lastpart, minimumord, maximumord )
local nord = tonumber(ord)
local minord = tonumber(string.match(minimumord or , '(-?%d+)')) or -9999 --allow full ord & +/- qualifier
local maxord = tonumber(string.match(maximumord or , '(-?%d+)')) or 9999 --allow full ord & +/- qualifier
if string.match(minimumord or , 'TCN') then minord = -math.abs(minord) end --allow BC qualifier (AD otherwise assumed)
if string.match(maximumord or , 'TCN') then maxord = -math.abs(maxord) end --allow BC qualifier (AD otherwise assumed)

local temporal = string.match(lastpart, 'thế kỷ') or
string.match(lastpart, 'thiên niên kỷ')

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'] = 'TCN', --WP:CFD/Log/2016 June 21Category:11th-century BC Hebrew people
['-century Jews'] = 'TCN', --co-nominated
['-century Judaism'] = 'TCN', --co-nominated
['-century rabbis'] = 'TCN', --co-nominated
['-century High Priests of Israel'] = 'TCN',
}
local parentBC = mw.ustring.match(lastpart, '%s(TCN?)') --"1st-century BC" format
local lastpartNoBC = mw.ustring.gsub(lastpart, '%sTCN?', ) --easier than splitting lastpart up in 2; AD never used
local BCe = parentBC or ordBCElastparts[lastpartNoBC] or 'TCN' --"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 nordswitchADBC then minord = nordswitchADBC end --input error; minord 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 = 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 '..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 = "1st parliament"
local catlink = catlinkfollowr( frame, firstpart..' '..oth..tspace..lastpart, oth )
if catlink.rtarget then --a was followed
trackcat(23, 'Category series navigation có số thứ tự đã đổi hướng')
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

--

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 ord2viet = require('Module:ConvertNumeric').spell_number2
local viet2ord = require('Module:ConvertNumeric').vietnamese_to_numeral
local th = 'th'
if not ordinal then
th =
viet2ord = require('Module:ConvertNumeric').vietnamese_to_numeral
end
local capitalize = nil ~= string.match(word, '^%u') --determine capitalization
local nord = viet2ord(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 nord then minword = nord end
if maxword \n'

local navlist = {}
local prepad =
local i = -5 --nav position
while i = 1 then
local nth = p.addord(n)
if not ordinal then nth = n end
if i ~= 0 then --left/right navw
local w = ord2viet{ num = n, ordinal = ordinal, capitalize = capitalize }
local catlink = catlinkfollowr( frame, firstpart..lspace..w..tspace..lastpart, nth )
if minword • '..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 • '..'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

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

local function find_var( pn )
--Extracts the variable text (e.g. 2015, 2015–16, 2000s, 3rd, III, etc.) from a string,
--and returns { ['vtype'] = , = }
local pagename = currtitle.text
if pn and pn ~= then
pagename = pn
end

local cpagename = 'Thể loại:'..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+[–-]nay)$') --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, 'mùa (%d+)') or
mw.ustring.match(cpagename, 'series (%d+)')

local nordinal = mw.ustring.match(cpagename, '[:%s](%d+)[-%s]') or
mw.ustring.match(cpagename, '[:%s](%d+)$')

local decade = mw.ustring.match(cpagename, '[:%s][Tt]hập niên (%d+)[%s-]') or
mw.ustring.match(cpagename, '[:%s][Tt]hập niên (%d+)$')

local year = mw.ustring.match(cpagename, '[:%s][Nn]ăm (%d%d%d%d)%s') or --prioritize 4-digit years
mw.ustring.match(cpagename, '[:%s][Nn]ăm (%d%d%d%d)$') or
mw.ustring.match(cpagename, '[:%s][Nn]ăm (%d+)%s') or
mw.ustring.match(cpagename, '[:%s][Nn]ăm (%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
--giữ nguyên thứ tự decade, year, nordinal
if decade then return { ['vtype'] = 'decade', ['v'] = decade } end
if year then return { ['vtype'] = 'year', ['v'] = year } end
if nordinal then return { ['vtype'] = 'nordinal', ['v'] = nordinal } end
if roman then return { ['vtype'] = 'roman', ['v'] = roman } end
end
else
--try wordinals ('zeroth' to 'ninety-ninth' only)
--TODO: sửa hàm vietnamese_to_numeral và viết thêm hàm vietnamese_to_ordinal
local viet2ord = require('Module:ConvertNumeric').vietnamese_to_numeral
local split = mw.text.split(pagename, ' ')
for i=1, split do
local compound
--'hai mươi mốt đến chín mươi chín'
if i -1 then
return { ['vtype'] = 'wordinal', ['v'] = compound }
end
end
--'mười một' đến 'mười chín'
if i -1 then
return { ['vtype'] = 'wordinal', ['v'] = compound }
end
end
if viet2ord(split[i]) > -1 then
return { ['vtype'] = 'wordinal', ['v'] = split[i] }
end
end

--try Vietnamese numerics ('one'/'single' to 'ninety-nine' only)
local viet2num = require('Module:ConvertNumeric').vietnamese_to_numeral
local split = mw.text.split(pagename, '[%s%-]') --e.g. "Nine-cylinder engines"
for i=1, split do
if viet2num(split[i]) > -1 then
return { ['vtype'] = 'enumeric', ['v'] = split[i] }
end
end
end

errors = p.errorclass('Hàm find_var không thể tìm văn bản biến trong thể loại "'..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 sử dụng tham số skip-gaps')
end

--ns checks
if currtitle.nsText == 'Thể_loại' then
if cat and cat ~= then
trackcat(1, 'Category series navigation sử dụng tham số cat')
end
if testcase and testcase ~= then
trackcat(2, 'Category series navigation sử dụng tham số testcase')
end
elseif currtitle.nsText == then
trackcat(30, 'Category series navigation trong không gian tên chính')
end

--find the variable parts of pagename
local findvar = find_var(pagename)
if skipgaps then
findvar.vtype = 'year' -- skip-gaps chỉ được dùng cho đơn vị là năm
end
mw.log("findvar.vtype: " .. findvar.vtype)
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
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:gsub('[Nn]ăm', 'thập niên')
if firstpart_dec == 'SCN' and dec <= 1 then
firstpart_dec =
if dec == 0 then dec = end
end
local decade = dec..'0 '
decadecat = mw.text.trim( firstpart_dec..' '..decade..lastpart )
local exists = catexists(decadecat)
if exists then
navborder = false
trackcat(28, 'Category series navigation năm và thập niên')
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+)'))
local century = math.floor( ((decade-1)/100) + 1 ) --from
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
firstpart = firstpart:gsub('([Tt])hập niên', '%1hế kỷ')
local clastpart = lastpart
local centurycat = mw.text.trim( firstpart..' '..p.addord(century)..' '..clastpart )
local exists = catexists(centurycat)
if exists then
navborder = false
trackcat(29, 'Category series navigation thập niên và thế kỷ')
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([–-])nay$'), -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('Không thể xác định hàm nav phù hợp từ khoảng thời gian không đúng định dạng "'..findvar.v..'". ')
return p.failedcat(errors, 'N')..table.concat(ttrackingcats)
end
end

return p