Modul:Navseasoncats

Revisi sejak 3 Desember 2023 16.53 oleh Baloo Official (bicara | kontrib)
(beda) ← Revisi sebelumnya | Revisi terkini (beda) | Revisi selanjutnya → (beda)

Galat Lua: too many expensive function calls.

require [[strict]]
local p = {}
local horizontal = require("Modul:List").horizontal

--[[==========================================================================]]
--[[                                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 = 25
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, "Navseasoncats with") and not string.match(currtitle.text, "Navseasoncats.*/doc") and
    not string.match(currtitle.text, "Navseasoncats.*/sandbox") and
    currtitle.text ~= "Navseasoncats" and
    currtitle.nsText ~= "User_talk" and
    currtitle.nsText ~= "Template_talk" and
    (currtitle.nsText ~= "Template" or testcases)) --avoid nested transclusion errors (i.e. {{Infilmdecade}})

--[[==========================================================================]]
--[[                      Utility & category functions                        ]]
--[[==========================================================================]]
--Determine if a category exists (in a function for easier localization).
local function catexists(title)
    return mw.title.new(title, "Kategori").exists
end

--Error message handling.
function p.errorclass(msg)
    return mw.text.tag(
        "span",
        {class = "error mw-ext-cite-error"},
        "<b>Galat!</b> " .. string.gsub(msg, "&#", "&amp;#")
    )
end

--Failure handling.
function p.failedcat(errors, sortkey)
    if avoidself then
        return (errors or "") ..
            "&#42;&#42;&#42;Navseasoncats failed to generate navbox***" ..
                "[[" ..
                    testcasecolon .. "Category:Navseasoncats 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: 'Navseasoncats 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 .. "Kategori:" .. 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, "Navseasoncats dengan parameter tidak dikenal")
            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, "Navseasoncats terisolasi")
    end
end

--Returns the target of {{Category redirect}}, if it exists, else returns the original cat.
--{{Title year}}, etc., if found, are evaluated.
--Used by catlinkfollowr(), and so indirectly by all nav_*().
local function rtarget(frame, cat)
    local catcontent = mw.title.new(cat or "", "Kategori"):getContent()
    if string.match(catcontent or "", "{{ *[Cc]at") then --prelim test
        local regex = {
            --the following 11 pages (6 condensed) redirect to [[Template:Category redirect]], in descending order, as of 9/2022:
            "{{ *[Cc]ate?g?o?r?y?[ _]*[rR]edirect", --505+312+243+1 transclusions
            "{{ *[Cc]atr", --21
            "{{ *[Cc]at[ _]*[rR]edir", --5+3
            "{{ *[Cc]at[ _]*[rR]ed", --3+2
            "{{ *[Cc]at[ _]*[mM]ove", --1
            "{{ *[Cc]ategory[ _]*[mM]ove" --0
        }
        for _, v in pairs(regex) do
            local rtarget = mw.ustring.match(catcontent, v .. "%s*|%s*([^|}]+)")
            if rtarget then
                if string.match(rtarget, "{{") then --{{Title year}}, etc., exists; evaluate
                    local regex_ty = "%s*|%s*([^{}]*{{([^{|}]+)}}[^{}]-)%s*}}" --eval null-param templates only; expanded if/as needed
                    local rtarget_orig, ty = mw.ustring.match(catcontent, v .. regex_ty)
                    if rtarget_orig then
                        local ty_eval = frame:expandTemplate {title = ty, args = {page = cat}} --frame:newChild doesn't work, use 'page' param instead
                        local rtarget_eval = mw.ustring.gsub(rtarget_orig, "{{%s*" .. ty .. "%s*}}", ty_eval)
                        return rtarget_eval
                    else --sub-parameters present; track & return default
                        trackcat(31, "Navseasoncats redirection error")
                    end
                end
                rtarget = mw.ustring.gsub(rtarget, "^1%s*=%s*", "")
                rtarget = string.gsub(rtarget, "^[Cc]ategory:", "")
                return rtarget
            end
        end --for
    end --if
    return cat
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'] = <true|false>,
--			['rtarget'] = nil,
--			['navelement'] = <cat navelement>,
--			['displaytext'] = displaytext,
--		  } otherwise.
--Used by all nav_*().
local function catlinkfollowr(frame, cat, displaytext, displayend)
    cat = mw.text.trim(cat or "")
    displaytext = mw.text.trim(displaytext or "")
    displayend = displayend or false --bool flag to override displaytext IIF the cat/target is terminal (e.g. "2021–present" or "2021–")

    local grey = "#888"
    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(frame, cat) --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 == "sekarang" then
                    disp = y .. hyph .. ending
                elseif ending == "" then
                    disp = y .. hyph .. '<span style="visibility:hidden">' .. y .. "</span>" --hidden y to match spacing
                end
            end

            link = "[[:Category:" .. R .. "|" .. disp .. "]]"
        else
            link = "[[:Category:" .. cat .. "|" .. disp .. "]]"
        end
    else
        link = '<span style="color:' .. grey .. '">' .. disp .. "</span>"
    end

    if listall 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 == "Kategori" then
        errors =
            p.errorclass(
            "The <b><code>|list-all-links=yes</code></b> parameter/utility " ..
                "should not be saved in category space, only previewed."
        )
        out = p.failedcat(errors, "Z")
    end

    local bwd, fwd = "", ""
    if tlistallbwd[1] then
        bwd = "\n\nbackward search:\n# " .. table.concat(tlistallbwd, nl)
    end
    if tlistallfwd[1] then
        fwd = "\n\nforward search:\n# " .. 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+[–-]sekarang$") then
        terminaltxt = "sekarang"
        trackcat(14, "Navseasoncats range ends (sekarang)")
    elseif mw.ustring.match(cat, "%d+[–-]$") then
        terminaltxt = ""
        trackcat(15, "Navseasoncats 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, "Navseasoncats 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 "*<b>" .. catlink.displaytext .. "</b>\n"
        else
            return "*<b>" .. catlink.navelement .. "</b>\n"
        end
    else
        return "*" .. catlink.navelement .. "\n"
    end
end

--Return conditionally aligned stacked navs.
--Used by main only.
local function nav1nav2(nav1, nav2)
    if avoidself then
        local forcealign = '<div style="display:block !important; max-width: calc(100% - 25em);">'
        return forcealign .. "\n" .. nav1 .. "\n" .. nav2 .. "\n</div>"
    else
        return nav1 .. "\n" .. nav2
    end
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, "Navseasoncats 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    { [<gap size found>]    = 1 }
    local ttlens = {} --table of term lengths found w/i terms { [<term length found>] = 1 }
    local tirregs = {} --table of ir/regular-term-length cats' "from"s & "to"s found
    local regularparent = true
    if
        (finish == -1) or (finish == 0) --"Members of the Scottish Parliament 2021–present"
     then --"Members of the Scottish Parliament 2021–"
        regularparent = false
        if maxseas == nil or maxseas == "" then
            maxseas = start --hide subsequent ranges
        end
        if finish == -1 then
            trackcat(14, "Navseasoncats range ends (present)")
        else
            trackcat(15, "Navseasoncats range ends (blank, MOS)")
        end
    elseif (start == finish) and (ttrackingcats[16] ~= "") then --nav_year found isolated; check for surrounding hyphenated terms (e.g. UK MPs 1974)
        trackcat(16, "") --reset for another check later
        trackcat(13, "Navseasoncats 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 term_limit = 10
    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, "Navseasoncats 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, "Navseasoncats 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 = 6 --less expensive per-increment (inc x 4)
    local hgap_limit_irreg = 6 --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
    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 = '[<span style="color:green">j, g, k = ' --used for/when debugging via list-all-links=yes
        local spanblue = '<span style="color:blue">'
        local spanred = ' (<span style="color:red">'
        local span = "</span>"
        local lastg = nil --to check for run-on searches
        local lastk = nil --to check for run-on searches
        local endfound = false --switch used to stop searching forward
        local iirregs = 0 --index of tirregs[] for j < 0, since search starts from parent
        local j = -3 --index of tirregs[] for j > 0 & pseudo nav position
        while j <= 3 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 (to == "0-length") --the normal case; only continue k = 0 if 0-length found
                         then --ghetto "continue" (thx Lua) to avoid expensive searches for "UK MPs 1974-1974", etc.
                            table.insert(tlistallbwd, spangreen .. j .. ", " .. g .. ", " .. k .. span .. "] " .. full)
                            if (k == 1) and (g == 0 or g == 1) and (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, "Navseasoncats range irregular, 0-length")
                                end
                                tlistallbwd[#tlistallbwd] = spanblue .. tlistallbwd[#tlistallbwd] .. span .. " (found)"
                                ttlens[find_duration(full)] = 1
                                tgaps[g] = 1
                                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 > 3 then --lest we keep searching & finding 0-length cats ("MEPs for the Republic of Ireland 1973" & down)
                                        gbreak = true
                                        break
                                    end
                                end
                            end
                        end --ghetto "continue"
                        k = k + 1
                        lastk = k
                    end --while k
                    if gbreak == true then
                        break
                    end
                    g = g + 1
                    lastg = g
                end --while g
            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 (to4 == "0-length") --only continue k = -2 if 0-length found
                         then --ghetto "continue" (thx Lua) to avoid expensive searches for "UK MPs 1974-1974", etc.
                            table.insert(tlistallfwd, spangreen .. j .. ", " .. g .. ", " .. k .. span .. "] " .. full)
                            if (k == 1) and (g == 0 or g == 1) and (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(frame, full) == full then --only use 0-length cats that don't #R
                                        trackcat(13, "Navseasoncats 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
                                    tgaps[g] = 1
                                    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 > 3 then --lest we keep searching & finding 0-length cats ("2018 CONCACAF Champions League" & up)
                                            gbreak = true
                                            break
                                        end
                                    end
                                end
                            end
                        end --ghetto "continue"
                        k = k + 1
                        lastk = k
                    end --while k
                    if gbreak == true then
                        break
                    end
                    g = g + 1
                    lastg = g
                end --while g
            end --if j > 0

            if (lastg == (hgap_limit_irreg + 1)) and (lastk == (term_limit + 1)) 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 <= 3
    end --if hgap <= hgap_limit_reg

    --begin navhyphen
    local navh = '{| class="toccolours hlist" style="text-align: center; margin: auto;"\n' .. "|\n"

    local terminalcat = false --switch used to hide future cats
    local terminaltxt = nil
    local i = -3 --nav position
    while i <= 3 do
        local from = nstart + i * (t + hgap) --the logical, but not necessarily correct, 'from'
        if tirregs["from" .. i] then
            from = tonumber(tirregs["from" .. i])
        end --prefer irregular term table
        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 (from2 - (to2 or from2)) > 0 --per MOS:DATERANGE (e.g. 1999-2004)
         then --century transition exception (e.g. 1999–2000)
            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 > -3 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,
                        tgaps[gap] = 1 --tgaps[-1] is ignored
                    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, "Navseasoncats range redirected (base change)")
                elseif terminalcat == 1 then
                    trackcat(8, "Navseasoncats 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, "Navseasoncats 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, "Navseasoncats range redirected (var change)")
                        else
                            trackcat(9, "Navseasoncats 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
                navh = navh .. "*" .. catlink.navelement .. "\n"
                if terminalcat then
                    nmaxseas = nminseas_default
                end --prevent display of future ranges
            else
                local hidden = '<span style="visibility:hidden">' .. disp .. "</span>"
                navh = navh .. "*" .. hidden .. "\n"
                if listall then
                    tlistall[#tlistall] = tlistall[#tlistall] .. " (" .. hidden .. ")"
                end
            end
        else --center navh
            if finish == -1 then
                finish = "sekarang"
            elseif finish == 0 then
                finish = '<span style="visibility:hidden">' .. start .. "</span>"
            end
            local disp = start .. hyph .. finish
            if regularparent == "isolated" then
                disp = start
            end
            navh = navh .. "*<b>" .. disp .. "</b>\n"
        end

        i = i + 1
    end

    --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
        for s = 0, term_limit do
            itlens = itlens + (ttlens[s] or 0)
        end
        if igaps > 0 then
            trackcat(11, "Navseasoncats range gaps")
        end
        if itlens > 1 and ttrackingcats[13] == "" then --avoid duplication in "Navseasoncats range irregular, 0-length"
            trackcat(12, "Navseasoncats 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 2nd parameter.")
        return p.failedcat(errors, "T")
    end

    local maxtv = tonumber(maximumtv) or 9999 --allow +/- qualifier
    if maxtv < tv then
        maxtv = tv
    end --input error; maxtv should be >= parent

    --begin navtvseason
    local navt = '{| class="toccolours hlist" style="text-align: center; margin: auto;"\n' .. "|\n"

    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 .. lastpart, t)
            if (t >= 1 and t <= maxtv) then --hardcode mintv
                if catlink.rtarget then --a {{Category redirect}} was followed
                    trackcat(25, "Navseasoncats TV season redirected")
                end
                navt = navt .. "*" .. catlink.navelement .. "\n"
            else
                local hidden = '<span style="visibility:hidden">' .. "0" .. "</span>" --'0' to maintain dot spacing
                navt = navt .. "*" .. hidden .. "\n"
                if listall then
                    tlistall[#tlistall] = tlistall[#tlistall] .. " (" .. hidden .. ")"
                end
            end
        else --center navt
            navt = navt .. "*<b>" .. tv .. "</b>\n"
        end

        i = i + 1
    end

    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, "SM") 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, "SM") 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, "^SM") --following the "0s BC" convention for all years BC
    lastpart = mw.ustring.gsub(lastpart, "^SM%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 function navdecade(frame, ndec, navborder, mindec, maxdec, switchADBC, parentBC, dec0to40AD)
        local bnb = "" --border/no border
        if not navborder then --for Category series navigation year and decade
            bnb = " border-style: none; background-color: transparent;"
        end
        local navd = '{| class="toccolours" style="text-align: center; margin: auto;' .. bnb .. '"\n|\n'

        local navlist = {}
        local i = -50 --nav position x 10
        while i <= 50 do
            local d = ndec + i * switchADBC

            local BC = ""
            local BCdisp = ""
            if dec0to40AD then
                if d < -10 then
                    d = math.abs(d + 10) --b/c 2 "0s" decades exist: "0s SM" & "0s" (M)
                    BC = "SM " -- 'SM' for 'Sebelum Masehi', equivalent to 'BC'
                    if d == 0 then
                        d = -10 --track 1st d = 0 use (SM)
                    end
                else
                    d = d + 10 --now iterate from 0s M
                end
            elseif parentBC then
                if switchADBC == -1 then --parentBC looking at the SM side (the common case)
                    BC = "SM "
                    if d == 0 then --prepare to switch to the M side on the next iteration
                        switchADBC = 1 --1st d = 0 use (SM)
                        d = -10 --prep
                    end
                elseif switchADBC == 1 then --switched to the M side
                    d = d + 10 --now iterate from 0s M
                end
            end
            if BC ~= "" and ndec <= 50 then
                BCdisp = " SM" --show SM for all SM decades whenever a "0s" is displayed on the nav
            end

            --determine target cat
            local disp = d .. "-an" .. BCdisp
            local catlink = catlinkfollowr(frame, "Indonesia dalam tahun " .. d .. "-an" .. BC, disp) -- Adjust category naming
            -- Check if category redirect was followed
            if catlink.rtarget then
                trackcat(18, "Navigasi seri kategori dekade dialihkan") -- Translated message
            end

            --populate left/right navd
            local shown = navcenter(i, catlink)
            local hidden = '<span style="visibility:hidden">' .. disp .. "</span>"
            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
        -- Concatenate navlist into a string and return
        return navd .. table.concat(navlist, "\n|\n") .. "\n|}"
    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 "", "SM") then
        minyear = -math.abs(minyear)
    end --allow BC qualifier (AD otherwise assumed)
    if string.match(maximumyear or "", "SM") 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 2nd 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"] = "SM" --example entry format; add to & adjust as needed
    }
    local parentAD = string.match(firstpart, "M$") --following the "AD 1" convention from AD 1 to AD 10
    local parentBC = string.match(lastpart, "^SM?") --following the "1 BC" convention for all years BC
    firstpart = mw.ustring.gsub(firstpart, "%s*M$", "") --handle AD/BC separately for easier & faster accounting
    lastpart = mw.ustring.gsub(lastpart, "^SM?%s*", "")
    local BCe = parentBC or yearBCElastparts[lastpart] or "SM" --"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, "kepresidenan") 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 <= 5 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 <= 5 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)]
    local skipgaps_limit = 25
    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 --AD/BC not supported
        end
    end

    --begin navyears
    local navy = '{| class="toccolours hlist" style="text-align: center; margin: auto;"\n' .. "|\n"

    local y
    local i = -5 --nav position
    while i <= 5 do
        if skipgaps then
            y = ynogaps[i]
        else
            y = year + i * ygap * switchADBC
        end
        local BCdisp = ""
        if i ~= 0 then --left/right navy
            local AD = ""
            local BC = ""
            if year1to15AD and not (year1to10 and not year1to10ADBC) then --don't AD/BC 1-10's if parents don't contain AD/BC
                if year >= 11 then --parent = AD 11-15
                    if y <= 10 then --prepend AD on y = 1-10 cats only, per existing cats
                        AD = "M "
                    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 = "M "
                    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 = "M "
                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)"
                    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, "Navseasoncats year and range")
                    else
                        if listall then
                            tlistall[#tlistall] = tlistall[#tlistall] .. " (tried; not displayed)"
                        end
                        local yHyph_2 = y .. "–" .. string.match(y + 1, "%d%d$") --try 2010–11 type cats
                        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, "Navseasoncats year and range")
                        elseif listall then
                            tlistall[#tlistall] = tlistall[#tlistall] .. " (tried; not displayed)"
                        end
                    end
                end
                if catlink.rtarget then --#R followed; determine why
                    local r = catlink.rtarget
                    local c = catlink.cat
                    local regex_year = "%d%d%d%d[–-]?%d?%d?%d?%d?" --prioritize year/range stripping, e.g. for "2006 Super 14 season"
                    local regex_hyph = "%d%d%d%d[–-]%d+"
                    local regex_num = "%d+" --strip any number otherwise
                    local regex_final = nil --best choice goes here
                    if mw.ustring.match(r, regex_year) and mw.ustring.match(c, regex_year) then
                        regex_final = regex_year
                    elseif mw.ustring.match(r, regex_num) and mw.ustring.match(c, regex_num) then
                        regex_final = regex_num
                    end
                    if regex_final then
                        local r_base = mw.ustring.gsub(r, regex_final, "")
                        local c_base = mw.ustring.gsub(c, regex_final, "")
                        if r_base ~= c_base then
                            trackcat(19, "Navseasoncats year redirected (base change)") --acceptable #R target
                        elseif mw.ustring.match(r, regex_hyph) then
                            trackcat(20, "Navseasoncats year redirected (var change)") --e.g. "2008 in Scottish women's football" to "2008–09"
                        else
                            trackcat(21, "Navseasoncats year redirected (other)") --exceptions go here
                        end
                    else
                        trackcat(20, "Navseasoncats year redirected (var change)") --e.g. "V2 engines" to "V-twin engines"
                    end
                end
                navy = navy .. "*" .. catlink.navelement .. "\n"
            else --OOB vs min/max
                local hidden = '<span style="visibility:hidden">' .. disp .. "</span>"
                navy = navy .. "*" .. hidden .. "\n"
                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
            navy = navy .. "*<b>" .. year .. BCdisp .. "</b>\n"
        end

        i = i + 1
    end

    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("Modul:ConvertNumeric").roman_to_numeral
    local toroman = require("Modul: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") .. '" dalam kategori "' .. 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 = '{| class="toccolours hlist" style="text-align: center; margin: auto;"\n' .. "|\n"

    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, "Navseasoncats roman numeral redirected")
                    end
                    navr = navr .. "*" .. catlink.navelement .. "\n"
                else
                    local hidden = '<span style="visibility:hidden">' .. r .. "</span>"
                    navr = navr .. "*" .. hidden .. "\n"
                    if listall then
                        tlistall[#tlistall] = tlistall[#tlistall] .. " (" .. hidden .. ")"
                    end
                end
            else --center navr
                navr = navr .. "*<b>" .. r .. "</b>\n"
            end
        else
            navr = navr .. '*<span style="visibility:hidden">' .. "I" .. "</span>\n"
        end

        i = i + 1
    end

    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 "", "SM") then
        minord = -math.abs(minord)
    end --allow BC qualifier (AD otherwise assumed)
    if string.match(maximumord or "", "SM") then
        maxord = -math.abs(maxord)
    end --allow BC qualifier (AD otherwise assumed)

    local temporal = string.match(lastpart, "abad") or string.match(lastpart, "milenium")

    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"] = "SM", --WP:CFD/Log/2016 June 21#Category:11th-century BC Hebrew people
        ["-century Jews"] = "SM", --co-nominated
        ["-century Judaism"] = "SM", --co-nominated
        ["-century rabbis"] = "SM", --co-nominated
        ["-century High Priests of Israel"] = "SM"
    }
    local parentBC = mw.ustring.match(lastpart, "%s(SM?)") --"1st-century BC" format
    local lastpartNoBC = mw.ustring.gsub(lastpart, "%sSM?", "") --easier than splitting lastpart up in 2; AD never used
    local BCe = parentBC or ordBCElastparts[lastpartNoBC] or "SM" --"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 Navseasoncats decade and century
        bnb = " border-style: none; background-color: transparent;"
    end
    local navo = '{| class="toccolours hlist" style="text-align: center; margin: auto;' .. bnb .. '"\n' .. "|\n"

    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 = '<span style="visibility:hidden">' .. oth .. "</span>"
        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, "Navseasoncats nordinal redirected")
                end
                navo = navo .. navcenter(i, catlink)
            else
                navo = navo .. "*" .. hidden .. "\n"
                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, "Navseasoncats nordinal redirected")
            end
            navo = navo .. 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
            navo = navo .. "*" .. hidden .. "\n"
        end

        i = i + 1
    end

    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.numeral_to_indonesian() flags:
    --   ordinal == 'on': 'second' is output instead of 'two'
    --   ordinal ~= 'on': 'two' is output instead of 'second'
    local ord2ind = require("Modul:ConvertNumeric").numeral_to_indonesian
    local ind2ord = require("Modul:ConvertNumeric").indonesian_to_ordinal
    local th = ""
    if ordinal ~= "on" then
        th = ""
        ind2ord = require("Modul:ConvertNumeric").indonesian_to_numeral
    end
    local sc = string.match(word, "^%u") --sentence-case check
    local lc = string.lower(word) --operate on/with lc, and restore any sc later
    local nord = ind2ord(lc)
    local case = nil
    if sc then
        case = "U"
    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 "Straight-eight engines"-type cats
    if string.match(lastpart, "^[%-%)]") then
        tspace = ""
    end --DNE for "Nine-cylinder engines"-type cats

    --sterilize min/max
    local minword = 1
    local maxword = 99
    if minimumword then
        local num = tonumber(minimumword)
        if num and 0 < num and num < maxword then
            minword = num
        else
            local ord = ind2ord(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 = ind2ord(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

    --begin navwordinal
    local navw = '{| class="toccolours hlist" style="text-align: center; margin: auto;"\n' .. "|\n"

    local i = -5 --nav position
    while i <= 5 do
        local n = nord + i

        if n >= 1 then
            local nth = p.addord(n)
            if ordinal ~= "on" then
                nth = n
            end
            if i ~= 0 then --left/right navw
                local frame_args = frame:newChild {args = {n, ord = ordinal, case = case}} --easier to do this than modify Module:ConvertNumeric
                local w = ord2eng(frame_args)
                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, "Navseasoncats wordinal redirected")
                    end
                    navw = navw .. "*" .. catlink.navelement .. "\n"
                else
                    local hidden = '<span style="visibility:hidden">' .. nth .. "</span>"
                    navw = navw .. "*" .. hidden .. "\n"
                    if listall then
                        tlistall[#tlistall] = tlistall[#tlistall] .. " (" .. hidden .. ")"
                    end
                end
            else --center navw
                navw = navw .. "*<b>" .. nth .. "</b>\n"
            end
        else
            navw = navw .. '*<span style="visibility:hidden">' .. "0" .. th .. "</span>\n"
        end

        i = i + 1
    end

    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.>, <v> = <2015|2015–16|etc.> }
    local pagename = currtitle.text
    if pn and pn ~= "" then
        pagename = pn
    end

    local cpagename = "Kategori:" .. pagename --limited-Lua-regex workaround

    local d_season = mw.ustring.match(cpagename, ":(%d+%-an).+%(%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 mw.ustring.match(cpagename, "%s(%d+[–-]sekarang)$") --irreg; ending unknown, e.g. "Members of the Scottish Parliament 2021–" --e.g. "UK MPs 2019–present"

    local season =
        mw.ustring.match(cpagename, "[:%s%(](%d+[–-]%d+)[%)%s]") or mw.ustring.match(cpagename, "[:%s](%d+[–-]%d+)$") --split in 2 b/c you can't frontier '$'/eos?

    local tvseason = mw.ustring.match(cpagename, "musim (%d+)") or mw.ustring.match(cpagename, "seri (%d+)")

    local nordinal = mw.ustring.match(cpagename, "ke%-%d+") or mw.ustring.match(cpagename, "ke%-%d+$")

    local decade = mw.ustring.match(cpagename, "%d+%-an") or mw.ustring.match(cpagename, "%d+%-an$")

    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/least chance for duplication
            if nordinal and season then --i.e. "18th-century establishments in the Province of Quebec (1763–1791)"
                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 ind2ord = require("Modul:ConvertNumeric").indonesian_to_ordinal
        local split = mw.text.split(pagename, " ")
        for i = 1, #split do
            if ind2ord(split[i]) > -1 then
                return {["vtype"] = "wordinal", ["v"] = split[i]}
            end
        end

        --try English numerics ('one'/'single' to 'ninety-nine' only)
        local eng2num = require("Modul:ConvertNumeric").indonesian_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.navseasoncats(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"]

    --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, "Navseasoncats dengan parameter skip-gaps")
    end

    --ns checks
    if currtitle.nsText == "Kategori" then
        if cat and cat ~= "" then
            trackcat(1, "Navseasoncats using cat parameter")
        end
        if testcase and testcase ~= "" then
            trackcat(2, "Navseasoncats using testcase parameter")
        end
    elseif currtitle.nsText == "" then
        trackcat(30, "Navseasoncats 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, "^(.-musim )" .. findvar_escaped .. "(.*)$")
        if firstpart == nil then
            firstpart, lastpart = string.match(pagename, "^(.-seri )" .. 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 .. " "
        elseif firstpart_dec == "M" and dec <= 1 then
            firstpart_dec = ""
            if dec == 0 then
                dec = ""
            end
        end
        local decade = dec .. "0%-an "
        decadecat = mw.text.trim(firstpart_dec .. " " .. decade .. lastpart)
        local exists = catexists(decadecat)
        if exists then
            navborder = false
            trackcat(28, "Navseasoncats year and decade")
            local nav2 =
                nav_decade(frame, firstpart_dec, decade, lastpart, minimum, maximum) ..
                testcaseindent .. table.concat(ttrackingcats)
            return nav1nav2(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 nav1 --still isolated; rv to nav_year
            else
                return nav2
            end
        else --regular nav_year
            return 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+)%-an"))
        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 = " abad " .. lastpart
        local centurycat = mw.text.trim(firstpart .. " " .. p.addord(century) .. clastpart)
        local exists = catexists(centurycat)
        if not exists then --check for hyphenated century
            clastpart = "-abad " .. lastpart
            centurycat = mw.text.trim(firstpart .. " " .. p.addord(century) .. clastpart)
            exists = catexists(centurycat)
        end
        if exists then
            navborder = false
            trackcat(29, "Navseasoncats decade and century")
            local nav2 =
                nav_nordinal(frame, firstpart, century, clastpart, minimum, maximum) ..
                testcaseindent .. table.concat(ttrackingcats)
            return nav1nav2(nav1, nav2)
        else
            return nav1
        end
    elseif findvar.vtype == "nordinal" then --e.g. "1st", "99th"; ~7.5% of cats
        return 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 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 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 = "on"
        return 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 = "off"
        return 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 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([–-])sekarang$"), -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 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