Lompat ke isi

Modul:Navseasoncats

Dari Wikipedia bahasa Indonesia, ensiklopedia bebas

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)
    -- Mengharapkan PAGENAME dengan format "Beberapa contoh kucing 2000 berurutan", dimana
    --  firstpart = Beberapa contoh kucing
    --  decade    = 2000
    --  lastpart  = berurutan
    --  mindecade = 1800 (parameter dekade minimal; opsional; defaultnya -9999)
    --  maxdecade = 2020 (parameter dekade maksimal; opsional; defaultnya 9999)
    
    -- Mensterilkan dekade
    local dec = sterilizedec(decade)
    if dec == nil then
        errors = p.errorclass('Fungsi nav_decade menerima "'..(decade or '')..'" sebagai parameter ke-2, '..
                            'tetapi mengharapkan tahun berakhir dengan "0" sebanyak 1 hingga 4 digit.')
        return p.failedcat(errors, 'D')
    end
    local ndec = tonumber(dec)
    
    -- Mensterilkan mindecade & menentukan SM/M
    local mindefault = '-9999'
    local mindec = sterilizedec(mindecade) -- mengembalikan tostring(unsigned int), atau nil
    if mindec then
        if string.match(mindecade, '-%d') or
           string.match(mindecade, 'SM')
        then
            mindec = '-'..mindec -- penanganan string dengan +/-0
        end
    elseif mindec == nil and mindecade and mindecade ~= '' then
        errors = p.errorclass('Fungsi nav_decade menerima "'..(mindecade or '')..'" sebagai parameter ke-4, '..
                            'tetapi mengharapkan tahun berakhir dengan "0" sebanyak 1 hingga 4 digit, dekade terawal yang ditampilkan.')
        return p.failedcat(errors, 'E')
    else -- mindec == nil
        mindec = mindefault -- tonumber() nanti, setelah pengecekan error
    end
    
    -- Mensterilkan maxdecade & menentukan SM/M
    local maxdefault = '9999'
    local maxdec = sterilizedec(maxdecade) -- mengembalikan tostring(unsigned int), atau nil + error
    if maxdec then
        if string.match(maxdecade, '-%d') or
           string.match(maxdecade, 'SM')
        then
            maxdec = '-'..maxdec -- penanganan string dengan +/-0
        end
    elseif maxdec == nil and maxdecade and maxdecade ~= '' then
        errors = p.errorclass('Fungsi nav_decade menerima "'..(maxdecade or '')..'" sebagai parameter ke-5, '..
                            'tetapi mengharapkan tahun berakhir dengan "0" sebanyak 1 hingga 4 digit, dekade tertinggi yang ditampilkan.')
        return p.failedcat(errors, 'F')
    else -- maxdec == nil
        maxdec = maxdefault
    end
    
    local tspace = ' ' -- asumsikan spasi di akhir untuk kategori "1950-an di X"
    if string.match(lastpart, '^-') then tspace = '' end -- tidak ada spasi untuk kategori tipe "terkait 1970-an"
    
    -- Pengaturan SM/M dan variabel terkait
    
    local parentSM = string.match(lastpart, '^SM') -- mengikuti konvensi "0-an SM" untuk semua tahun SM
    lastpart = mw.ustring.gsub(lastpart, '^SM%s*', '') -- tangani SM secara terpisah; M tidak pernah digunakan
    --TODO?: tangani BCE, tetapi hanya jika ada dalam praktik
    
    local dec0to40M = (ndec >= 0 and ndec <= 40 and not parentSM) -- perilaku khusus dalam rentang ini
    local switchSM = 1                 --  1=M parent
    if parentSM then switchSM = -1 end -- -1=SM parent; mungkin disesuaikan nanti
    local SMdisp = ''
    local D = -math.huge -- pengalih sekunder & iterator untuk transisi M/SM
    
    -- Periksa min/max non-default dengan lebih teliti
    if mindec ~= mindefault then
        if tonumber(mindec) > ndec * switchSM then
            mindec = tostring(ndec * switchSM) -- kesalahan input; mindec harus <= parent
        end
    end
    if maxdec ~= maxdefault then
        if tonumber(maxdec) < ndec * switchSM then
            maxdec = tostring(ndec * switchSM) -- kesalahan input; maxdec harus >= parent
        end
    end
    local nmindec = tonumber(mindec) -- perilaku serupa dengan nav_year & nav_nordinal
    local nmaxdec = tonumber(maxdec) -- perilaku serupa dengan nav_nordinal
    
    -- Memulai nav_decade
    local bnb = '' -- border/tanpa border
    if navborder == false then -- untuk navigasi seri kategori tahun dan dekade
        bnb = 'categorySeriesNavigation-range-transparent'
    end
    local navd = '<div class="toccolours categorySeriesNavigation-range '..bnb..'">\n'
    
    local navlist = {}
    local i = -50 -- posisi navigasi x 10
    while i <= 50 do
        local d = ndec + i * switchSM
        
        local SM = ''
        SMdisp = ''
        if dec0to40M then
            if D < -10 then
                d = math.abs(d + 10) -- karena ada 2 dekade "0-an": "0-an SM" & "0-an" (M)
                SM = 'SM '
                if d == 0 then
                    D = -10 -- lacak penggunaan d = 0 pertama kali (SM)
                end
            elseif D >= -10 then
                D = D + 10 -- sekarang iterasi dari 0-an M
                d = D      -- penggunaan d = 0 kedua
            end
        elseif parentSM then
            if switchSM == -1 then -- parentSM melihat sisi SM (kasus umum)
                SM = 'SM '
                if d == 0 then     -- persiapan untuk beralih ke sisi M pada iterasi berikutnya
                    switchSM = 1 -- penggunaan d = 0 pertama kali (SM)
                    D = -10        -- persiapan
                end
            elseif switchSM == 1 then -- telah beralih ke sisi M
                D = D + 10 -- sekarang iterasi dari 0-an M
                d = D      -- penggunaan d = 0 kedua (pada penggunaan pertama)
            end
        end
        if SM ~= '' and ndec <= 50 then
            SMdisp = ' SM' -- tampilkan SM untuk semua dekade SM ketika "0-an" ditampilkan di navigasi
        end
        
        -- Menentukan kategori target
        local disp = d..'-an'..SMdisp
        local catlink = catlinkfollowr(frame, firstpart..' '..d..'-an'..tspace..SM..lastpart, disp)
        if catlink.rtarget then -- sebuah {{Category redirect}} diikuti
            trackcat(18, 'Navigasi seri kategori dekade dialihkan')
        end
        
        -- Mengisi navigasi kiri/kanan
        local shown = navcenter(i, catlink)
        local hidden = '<span style="visibility:hidden">'..disp..'</span>'
        local dsign = d -- gunakan d untuk tampilan & dsign untuk logika
        if SM ~= '' then dsign = -dsign end
        if (nmindec <= dsign) and (dsign <= nmaxdec) then
            if dsign == 0 and (nmindec == 0 or nmaxdec == 0) then -- membedakan antara -0 (SM) & 0 (M)
                -- "zoom in" pada +/- 0 dan ubah dsign/min/max sementara menjadi +/- 1 untuk pemrosesan yang lebih mudah
                local zsign, zmin, zmax = 1, nmindec, nmaxdec
                if SM ~= '' 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) -- kasus umum
                hidden = nil
            end
        else
            table.insert(navlist, hidden)
        end
        if listall and hidden then
            tlistall[#tlistall] = tlistall[#tlistall]..' ('..hidden..')'
        end
        
        i = i + 10
    end
    -- Menambahkan daftar
    navd = navd..horizontal(navlist)..'\n'
    isolatedcat()
    if listall then
        return listalllinks()
    else
        return navd..'</div>'
    end
end

--[[============================{{  nav_year  }}==============================]]

local function nav_year(frame, firstpart, year, lastpart, minimumyear, maximumyear)
    -- Mengharapkan PAGENAME dengan format "Beberapa contoh kucing 1760 berurutan", dimana
    --  firstpart     = Beberapa contoh kucing
    --  year          = 1760
    --  lastpart      = berurutan
    --  minimumyear   = 1758 (parameter tahun minimal; opsional)
    --  maximumyear   = 1800 (parameter tahun maksimal; opsional)
    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 -- mengizinkan qualifier +/- 
    local maxyear = tonumber(string.match(maximumyear or '', '-?%d+')) or maxyear_default -- mengizinkan qualifier +/-
    if string.match(minimumyear or '', 'SM') then minyear = -math.abs(minyear) end -- mengizinkan qualifier SM (AD diasumsikan)
    if string.match(maximumyear or '', 'SM') then maxyear = -math.abs(maxyear) end -- mengizinkan qualifier SM (AD diasumsikan)
    
    if year == nil then
        errors = p.errorclass('Fungsi nav_year tidak dapat mengenali tahun yang dikirimkan ke parameter ke-3.')
        return p.failedcat(errors, 'Y')
    end
    
    -- Pengaturan SM/M dan variabel terkait
    
    local yearSMElastparts = { -- diperlukan untuk parent = M 1-5, ketika format SM/E tidak diketahui
        -- "SME" dihapus untuk mencocokkan baik kategori M & SM; lebih mudah & lebih cepat daripada beberapa string.match()
        ['contoh_peoples_Hebrew_example'] = 'SME', -- format entri contoh; tambahkan & sesuaikan sesuai kebutuhan
    }
    local parentM = string.match(firstpart, 'M$')  -- mengikuti konvensi "M 1" dari M 1 hingga M 10
    local parentSM = string.match(lastpart, '^SME?') -- mengikuti konvensi "1 SM" untuk semua tahun SM
    firstpart = mw.ustring.gsub(firstpart, '%s*M$', '') -- tangani M/SM secara terpisah untuk akuntansi yang lebih mudah & cepat
    lastpart  = mw.ustring.gsub(lastpart,  '^SME?%s*', '')
    local SMe = parentSM or yearSMElastparts[lastpart] or 'SM' -- default "SM"
    
    local year1to10 = (year >= 1 and year <= 10)
    local year1to10SM = year1to10 and (parentSM or parentM) -- perilaku khusus 1-10 untuk seri non-tahun dengan angka rendah
    local year1to15M = (year >= 1 and year <= 15 and not parentSM) -- perilaku khusus 1-15 untuk tampilan M/SM
    local switchMSM = 1                 --  1=M parent
    if parentSM then switchMSM = -1 end -- -1=SM parent; mungkin disesuaikan nanti
    local Y = 0 -- iterator sekunder untuk parent SM yang menampilkan M
    
    if minyear > year * switchMSM then minyear = year * switchMSM end -- kesalahan input; minyear harus <= parent
    if maxyear < year * switchMSM then maxyear = year * switchMSM end -- kesalahan input; maxyear harus >= parent
    
    local lspace = ' ' -- spasi di depan tahun, setelah firstpart
    if string.match(firstpart, '[%-VW]$') then
        lspace = '' -- misalnya "Mesin Straight-8"
    end
    
    local tspace = ' ' -- spasi di akhir tahun, sebelum lastpart
    if string.match(lastpart, '^-') then
        tspace = '' -- misalnya "timeline terkait-2018"
    end
    
    -- menentukan ukuran jarak antar tahun untuk mengkondensasi tipe kategori khusus, jika memungkinkan
    local ygapdefault = 1 -- asumsi/mulai dari kasus paling umum: 2001, 2002, dll.
    local ygap = ygapdefault
    if string.match(lastpart, 'presidential') then
        local ygap1, ygap2 = ygapdefault, ygapdefault -- perlu menentukan jarak tahun sebelumnya & berikutnya secara independen
        local ygap1_success, ygap2_success = false, false
        
        local prevseason = nil
        while ygap1 <= ygap_limit do -- Republik Ceko, Polandia, Sri Lanka, dll. memiliki masa jabatan 5 tahun
            prevseason = firstpart..lspace..(year - ygap1)..tspace..lastpart
            if catexists(prevseason) then
                ygap1_success = true
                break
            end
            ygap1 = ygap1 + 1
        end
        
        local nextseason = nil
        while ygap2 <= ygap_limit do -- Republik Ceko, Polandia, Sri Lanka, dll. memiliki masa jabatan 5 tahun
            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
    
    -- lewati tahun yang tidak ada, jika diminta
    local ynogaps = {} -- isi dengan tahun yang ada dalam rentang, paling banyak, [year - (skipgaps_limit * 5), year + (skipgaps_limit * 5)]
    if skipgaps then
        if minyear == minyear_default then
            minyear = 0 -- otomatis set minyear ke 0, karena AD/SM tidak didukung
        end
        if (year > 70) or -- tambahkan dukungan untuk AD/SM (<= M 10) jika diperlukan
           (minyear >= 0 and -- harus merupakan seri non-tahun seperti "AC dengan 0 elemen"
            not parentM and not parentSM)
        then
            local yskipped = {} -- lacak y yang dilewati untuk menghindari pengecekan ganda
            local cat, found, Yeary
            
            -- isi antrian elemen navigasi ke luar secara positif dari parent
            local Year = year -- untuk menyimpan/mengatur progresi
            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 -- parent
            
            -- isi antrian elemen navigasi ke luar secara negatif dari parent
            Year = year -- reset progresi
            i = -1
            while i >= -5 do
                local y = -1
                while y >= -skipgaps_limit do
                    found = false
                    Yeary = Year + y
                    if yskipped[Yeary] == nil then
                        yskipped[Yeary] = Yeary
                        cat = firstpart..lspace..Yeary..tspace..lastpart
                        found = catexists(cat)
                        if found then break end
                    end
                    y = y - 1
                end
                if found then Year = Yeary
                else          Year = Year - 1 end
                ynogaps[i] =  Year
                i = i - 1
            end
        else
            skipgaps = false -- TODO: dukungan AD/SM, kemudian angkat pembatasan SM @ [[Template:Establishment category SM]] & [[Template:Year category header/core]]
        end
    end
    
    -- memulai nav_years
    local navy = '<div class="toccolours categorySeriesNavigation-range">\n'
    
    local navlist = {}
    local y
    local j = 0 -- decrementor untuk kasus khusus "2021 World Rugby Sevens Series" -> "2021–2022"
    local i = -5 -- posisi navigasi
    while i <= 5 do
        if skipgaps then
            y = ynogaps[i]
        else
            y = year + i * ygap * switchMSM - j
        end
        local SMdisp = ''
        if i ~= 0 then -- navigasi kiri/kanan
            
            local M = ''
            local SM = ''
            if year1to15M and not
               (year1to10 and not year1to10SM) -- jangan tampilkan M/SM untuk tahun 1-10 jika parent tidak mengandung M/SM
            then
                if year >= 11 then -- parent = M 11-15
                    if y <= 10 then -- tambahkan "M" hanya pada kategori y = 1-10, sesuai kategori yang ada
                        M = 'M '
                    end
                    
                elseif year >= 1 then -- parent = M 1-10
                    if y <= 0 then
                        SM = SMe..' '
                        y = math.abs(y - 1) -- lewati y = 0 (tidak ada)
                    elseif y >= 1 and y <= 10 then -- tambahkan "M" hanya pada kategori y = 1-10, sesuai kategori yang ada
                        M = 'M '
                    end
                end
                
            elseif parentSM then
                if switchMSM == -1 then -- y yang ditampilkan berada di rezim SM
                    if y >= 1 then     -- kasus umum
                        SM = SMe..' '
                    elseif y == 0 then -- beralih dari SM ke rezim M pada iterasi berikutnya
                        switchMSM = 1 -- penggunaan y = 0 pertama kali (SM)
                    end
                end
                if switchMSM == 1 then -- y yang ditampilkan sekarang berada di rezim M
                    Y = Y + 1 -- lewati y = 0 (tidak ada)
                    y = Y     -- solusi termudah: mulai iterator lain untuk y M yang ditampilkan pada parent tahun SM
                    M = 'M '
                end
            end
            if SM ~= '' and year <= 5 then -- hanya tampilkan 'SM' untuk parent tahun <= 5: menghemat ruang, lebih mudah dibaca,
                SMdisp = ' '..SMe          -- dan 6 adalah tahun navigasi pertama/terakhir yang tidak perlu disambiguasi;
            end                            -- tahun center/parent akan selalu menampilkan SM, jadi tidak perlu menampilkannya lagi
            
            -- mengisi navigasi kiri/kanan
            local ysign = y -- gunakan y untuk tampilan & ysign untuk logika
            local disp = y..SMdisp
            if SM ~= '' then ysign = -ysign end
            local firsttry = firstpart..lspace..M..y..tspace..SM..lastpart
            if (minyear <= ysign) and (ysign <= maxyear) then
                local catlinkM = catlinkfollowr(frame, firsttry, disp ) -- coba M
                local catlink = catlinkM -- kandidat awal
                if M ~= '' then -- untuk kategori tipe "ACArt dengan 5 elemen yang ditekan"
                    local catlinkNoM = catlinkfollowr(frame, firstpart..lspace..y..tspace..SM..lastpart, disp ) -- coba tanpa M
                    if catlinkNoM.catexists == true then
                        catlink = catlinkNoM -- ganti dengan yang tanpa M
                    elseif listall then
                        tlistall[#tlistall] = tlistall[#tlistall]..' (dicoba; tidak ditampilkan)<sup>1</sup>'
                    end
                end
                if (M..SM == '') and (catlink.catexists == false) and (y >= 1000) then -- tanpa M/SM & tidak ada; hanya 4 digit, untuk hemat
                    -- coba kategori dengan tanda hubung dasar: 1-tahun, en-dash, hanya MOS yang benar, tanpa #Rs
                    local yHyph_4 = y..'–'..(y + 1) -- coba kategori tipe 2010–2011
                    local catlinkHyph_4 = catlinkfollowr(frame, firstpart..lspace..yHyph_4..tspace..SM..lastpart, yHyph_4 )
                    if catlinkHyph_4.catexists and catlinkHyph_4.rtarget == nil then -- ada & tanpa #Rs
                        catlink = catlinkHyph_4 -- ganti dengan yang ini
                        trackcat(27, 'Navigasi seri kategori tahun dan rentang')
                    else
                        if listall then
                            tlistall[#tlistall] = tlistall[#tlistall]..' (dicoba; tidak ditampilkan)<sup>2</sup>'
                        end
                        local yHyph_2 = y..'–'..string.match(y + 1, '%d%d$') -- coba kategori tipe 2010–11
                        if i == 1 then
                            local yHyph_2_special = (y - 1)..'–'..string.match(y, '%d%d$') -- coba kasus khusus 2021 -> 2021–22
                            local catlinkHyph_2_special = catlinkfollowr(frame, firstpart..lspace..yHyph_2_special..tspace..SM..lastpart, yHyph_2_special )
                            if catlinkHyph_2_special.catexists and catlinkHyph_2_special.rtarget == nil then -- ada & tanpa #Rs
                                catlink = catlinkHyph_2_special -- ganti dengan yang ini
                                trackcat(27, 'Navigasi seri kategori tahun dan rentang')
                                j = 1
                            elseif listall then
                                tlistall[#tlistall] = tlistall[#tlistall]..' (dicoba; tidak ditampilkan)<sup>3</sup>'
                            end
                        end
                        if not (i == 1 and j == 1) then
                            local catlinkHyph_2 = catlinkfollowr(frame, firstpart..lspace..yHyph_2..tspace..SM..lastpart, yHyph_2 )
                            if catlinkHyph_2.catexists and catlinkHyph_2.rtarget == nil then -- ada & tanpa #Rs
                                catlink = catlinkHyph_2 -- ganti dengan yang ini
                                trackcat(27, 'Navigasi seri kategori tahun dan rentang')
                            elseif listall then
                                tlistall[#tlistall] = tlistall[#tlistall]..' (dicoba; tidak ditampilkan)<sup>4</sup>'
                            end
                        end
                    end
                end
                if catlink.rtarget then -- #R diikuti; tentukan alasannya
                    local r = catlink.rtarget
                    local c = catlink.cat
                    local year_regex  = '%d%d%d%d[–-]?%d?%d?%d?%d?' -- prioritaskan penghilangan tahun/rentang, mis. "2006 Super 14 season"
                    local hyph_regex  = '%d%d%d%d[–-]%d+' -- lebih ketat
                    local num_regex   = '%d+' -- hapus angka apa pun selain itu
                    local final_regex = nil   -- pilihan terbaik akan ditempatkan di sini
                    if mw.ustring.match(r, year_regex) and mw.ustring.match(c, year_regex) then
                        final_regex = year_regex
                    elseif mw.ustring.match(r, num_regex) and mw.ustring.match(c, num_regex) then
                        final_regex = num_regex
                    end
                    if final_regex then
                        local r_base = mw.ustring.gsub(r, final_regex, '')
                        local c_base = mw.ustring.gsub(c, final_regex, '')
                        if r_base ~= c_base then
                            trackcat(19, 'Navigasi seri kategori tahun dialihkan (perubahan dasar)') -- target #R yang dapat diterima
                        elseif mw.ustring.match(r, hyph_regex) then
                            trackcat(20, 'Navigasi seri kategori tahun dialihkan (perubahan var)') -- mis. "2008 dalam sepak bola wanita Skotlandia" ke "2008–09"
                        else
                            trackcat(21, 'Navigasi seri kategori tahun dialihkan (lainnya)') -- pengecualian berada di sini
                        end
                    else
                        trackcat(20, 'Navigasi seri kategori tahun dialihkan (perubahan var)') -- mis. "V2 engines" ke "V-twin engines"
                    end
                end
                table.insert(navlist, catlink.navelement)
            else -- di luar batas vs min/max
                local hidden = '<span style="visibility:hidden">'..disp..'</span>'
                table.insert(navlist, hidden)
                if listall then
                    local dummy = catlinkfollowr(frame, firsttry, disp )
                    tlistall[#tlistall] = tlistall[#tlistall]..' ('..hidden..')'
                end
            end
        else -- navy center
            if parentSM then SMdisp = ' '..SMe end
            table.insert(navlist, '<b>'..year..SMdisp..'</b>')
        end
        
        i = i + 1
    end
    
    -- menambahkan daftar
    navy = navy..horizontal(navlist)..'\n'
    
    isolatedcat()
    if listall then
        return listalllinks()
    else
        return navy..'</div>'
    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)
    -- Mengonversi ordinal (1st, 2nd, 3rd, dst.) menjadi angka dan mensterilkan nilai minimum/maksimum
    local nord = tonumber(ord)
    local minord = tonumber(string.match(minimumord or "", "(-?%d+)[snrt]?[tdh]?")) or -9999
    local maxord = tonumber(string.match(maximumord or "", "(-?%d+)[snrt]?[tdh]?")) or 9999
    
    -- Mengizinkan penulisan "SM" untuk menunjukkan "Sebelum Masehi" (setara "BC")
    if string.match(minimumord or "", "SM") then
        minord = -math.abs(minord)
    end
    if string.match(maximumord or "", "SM") then
        maxord = -math.abs(maxord)
    end

    -- Mendeteksi konteks temporal (misalnya "abad", "milenium") pada lastpart
    local temporal = string.match(lastpart, "abad") or string.match(lastpart, "milenium")

    -- Spasi di akhir ordinal
    local tspace = " "
    -- Jika lastpart berawalan "-", tidak perlu spasi (misal "19th-century" -> "19th-century" tanpa spasi)
    if string.match(lastpart, "^-") then
        tspace = ""
    end

    ----------------------------------------------------------------------------
    -- Pengaturan SM/M dan variabel terkait
    ----------------------------------------------------------------------------
    
    -- Daftar lastpart yang valid untuk SM (dahulu "BC"/"BCE" di versi bahasa Inggris)
    local ordSMElastparts = {
        ["-century Hebrew people"]       = "SM",
        ["-century Jews"]               = "SM",
        ["-century Judaism"]            = "SM",
        ["-century rabbis"]             = "SM",
        ["-century High Priests of Israel"] = "SM"
    }
    
    -- parentSM mendeteksi apakah lastpart mengandung " SM" (misal "1st-century SM")
    local parentSM = mw.ustring.match(lastpart, "%s(SM?)") -- Mendeteksi " SM" atau " S"
    -- lastpartNoSM menghapus " SM" agar lebih mudah diolah
    local lastpartNoSM = mw.ustring.gsub(lastpart, "%sSM?", "")
    -- BCe dulunya adalah label default "BC", sekarang disesuaikan menjadi "SM"
    local SMe = parentSM or ordSMElastparts[lastpartNoSM] or "SM"

    -- switchMSM:  1 = M (Masehi), -1 = SM (Sebelum Masehi)
    local switchMSM = 1
    if parentSM then
        switchMSM = -1
    end
    
    -- O adalah iterator sekunder apabila kita beralih dari SM ke M
    local O = 0

    -- Jika konteks bukan "temporal" (bukan abad atau milenium), maka minimal ordinal tidak boleh < 1
    if not temporal and minord < 1 then
        minord = 1
    end

    -- Validasi minord & maxord agar tidak melebihi ordinal induk
    if minord > nord * switchMSM then
        minord = nord * switchMSM
    end
    if maxord < nord * switchMSM then
        maxord = nord * switchMSM
    end

    ----------------------------------------------------------------------------
    -- Memulai nav_nordinal
    ----------------------------------------------------------------------------
    
    local bnb = ""
    -- Jika navborder == false, kita hilangkan border agar sesuai untuk beberapa navigasi
    if navborder == false then
        bnb = " border-style: none; background-color: transparent;"
    end
    
    -- Gunakan table HTML untuk meletakkan daftar tautan ordinal
    local navo = '{| class="toccolours hlist" style="text-align: center; margin: auto;' .. bnb .. '"\n' ..
                 '|-\n'

    local i = -5 -- posisi navigasi (5 ordinal ke kiri)
    while i <= 5 do -- hingga 5 ordinal ke kanan
        local o = nord + i * switchMSM
        local SM = ""
        local SMdisp = ""

        -- Jika parentSM terdeteksi, artinya induknya SM
        if parentSM then
            if switchMSM == -1 then -- Masih di sisi SM
                if o >= 1 then
                    -- Kasus umum SM (misal "3rd-century SM")
                    SM = " " .. SMe
                elseif o == 0 then
                    -- Beralih dari SM ke M
                    SM = ""
                    switchMSM = 1
                end
            end
            if switchMSM == 1 then
                -- Sekarang berada di rezim M (Masehi) setelah SM
                O = O + 1
                o = O
            end
        else
            -- Jika induknya M (Masehi), tetapi ordinal bernilai <= 0, berarti kita menampilkan sisi SM
            if o <= 0 then
                SM = " " .. SMe
                -- Lewati ordinal 0 karena tidak masuk akal ("0th century" dsb.)
                o = math.abs(o - 1)
            end
        end

        -- Hanya tampilkan "SM" jika induknya <= 5 untuk menghindari pemakaian label SM yang berlebihan
        if SM ~= "" and nord <= 5 then
            SMdisp = " " .. SMe
        end

        -- Bangun ordinal (misal "1st", "2nd" dsb.) lewat pemanggilan `p.addord`
        local oth = p.addord(o)  
        local osign = o
        if SM ~= "" then
            osign = -osign -- Logika internal untuk membedakan SM
        end

        -- Elemen tersembunyi jika di luar jangkauan atau jika bernilai 0
        local hidden = '<span style="visibility:hidden">' .. oth .. "</span>"

        ----------------------------------------------------------------------------
        -- Penanganan Kategori Temporal (abad, milenium)
        ----------------------------------------------------------------------------
        if temporal then
            -- Salin lastpart tanpa SM (agar tidak berulang menambah SM)
            local newlastpart = lastpartNoSM

            -- Jika kita butuh menambahkan label SM ke "abad" / "milenium"
            if SM ~= "" then
                newlastpart = string.gsub(newlastpart, temporal, temporal .. SM)
            end

            local catlink = catlinkfollowr(frame,
                firstpart .. " " .. oth .. tspace .. newlastpart,
                oth .. SMdisp
            )
            -- Tampilkan tautan hanya jika berada dalam rentang minord-maxord
            if (minord <= osign) and (osign <= maxord) then
                if catlink.rtarget then
                    trackcat(23, "Navseasoncats nordinal dialihkan")
                end
                navo = navo .. navcenter(i, catlink)
            else
                navo = navo .. "*" .. hidden .. "\n"
                if listall then
                    tlistall[#tlistall] = tlistall[#tlistall] .. " (" .. hidden .. ")"
                end
            end
        ----------------------------------------------------------------------------
        -- Penanganan Kategori Non-Temporal (misal "1st parliament", dsb.)
        ----------------------------------------------------------------------------
        elseif SM == "" and (minord <= osign) and (osign <= maxord) then
            local catlink = catlinkfollowr(frame,
                firstpart .. " " .. oth .. tspace .. lastpart,
                oth
            )
            if catlink.rtarget then
                trackcat(23, "Navseasoncats nordinal dialihkan")
            end
            navo = navo .. navcenter(i, catlink)
        else
            -- Di luar jangkauan, atau penanganan aneh (misal "2nd parliament SM") yang tidak lazim
            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)
    -- Paramater:
    -- 1) firstpart    : Bagian teks di depan kata bilangan (word).
    -- 2) word         : Kata bilangan (misalnya "second" atau "two"), masih menggunakan bahasa Inggris.
    -- 3) lastpart     : Bagian teks setelah kata bilangan.
    -- 4) minimumword  : Kata bilangan (atau angka) minimal, opsional.
    -- 5) maximumword  : Kata bilangan (atau angka) maksimal, opsional.
    -- 6) ordinal      : true untuk menghasilkan bentuk ordinal ('second'), false untuk bentuk kardinal ('two').
    -- 7) frame        : Objek frame Lua (untuk pemanggilan modul & fungsi bantu lainnya).

    -- Dalam Module:ConvertNumeric.spell_number2(), argumen:
    --   ordinal == true  => 'second' dihasilkan alih-alih 'two'
    --   ordinal == false => 'two' dihasilkan alih-alih 'second'

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

    -- Jika bukan bentuk ordinal, maka suffix 'th' dihilangkan (karena pakai bentuk kardinal).
    if not ordinal then
        th = ''
        eng2ord = require('Module:ConvertNumeric').english_to_numeral
    end

    -- Memeriksa huruf kapital (uppercase) pada kata awal (word).
    local capitalize = nil ~= string.match(word, '^%u')  -- Apakah huruf pertama kapital?
    -- Konversi kata bilangan Inggris ke angka (mis. "two" -> 2, "second" -> 2).
    local nord = eng2ord(string.lower(word))

    -- Mengatur spasi di depan (lspace) dan di belakang (tspace) kata bilangan.
    local lspace = ' ' -- Asumsikan ada spasi di depan (kasus umum).
    local tspace = ' ' -- Asumsikan ada spasi di belakang (kasus umum).
    if string.match(firstpart, '[%-%(]$') then
        lspace = ''  -- Tanpa spasi, misalnya "Straight-eight engines"-type cats.
    end
    if string.match(lastpart, '^[%-%)]') then
        tspace = ''  -- Tanpa spasi, misalnya "Nine-cylinder engines"-type cats.
    end

    ----------------------------------------------------------------------------
    -- Mensterilkan nilai minimum / maksimum (minword / maxword).
    ----------------------------------------------------------------------------

    local maxword_default = 99
    local maxword = maxword_default
    local minword = 1

    if minimumword then
        local num = tonumber(minimumword)
        if num and 0 < num and num < maxword then
            minword = num
        else
            local ord = eng2ord(minimumword)
            if 0 < ord and ord < maxword then
                minword = ord
            end
        end
    end

    if maximumword then
        local num = tonumber(maximumword)
        if num and 0 < num and num < maxword then
            maxword = num
        else
            local ord = eng2ord(maximumword)
            if 0 < ord and ord < maxword then
                maxword = ord
            end
        end
    end

    -- Pastikan minword <= nord <= maxword agar sesuai dengan kategori induk.
    if minword > nord then
        minword = nord
    end
    if maxword < nord then
        maxword = nord
    end

    ----------------------------------------------------------------------------
    -- Menentukan kategori terakhir (n_max) yang benar-benar ada (catexists).
    -- Logika ini memeriksa hingga 5 kata bilangan setelah (nord + m).
    ----------------------------------------------------------------------------

    local listoverride = true
    local n_max = nord
    local m = 1
    while m <= 5 do
        local n = nord + m
        local nth = p.addord(n)
        if not ordinal then
            nth = n -- Jika bukan ordinal, pakai angka biasa.
        end
        -- Konversi balik ke kata bilangan Inggris (mis. 2 -> "two" / "second").
        local w = ord2eng{
            num = n,
            ordinal = ordinal,
            capitalize = capitalize
        }
        local catlink = catlinkfollowr(
            frame,
            firstpart .. lspace .. w .. tspace .. lastpart,
            nth,
            nil,
            listoverride
        )
        -- Jika kategori ada, perbarui n_max
        if catlink.catexists then
            n_max = n
        end
        m = m + 1
    end

    ----------------------------------------------------------------------------
    -- Memulai nav_wordinal (navigasi kata bilangan).
    ----------------------------------------------------------------------------

    local navw = '<div class="toccolours categorySeriesNavigation-range">\n'
    local navlist = {}
    local prepad = ''

    local i = -5 -- Menampilkan 5 kata bilangan sebelum induk
    while i <= 5 do -- Hingga 5 kata bilangan setelah induk
        local n = nord + i

        if n >= 1 then
            local nth = p.addord(n)
            if not ordinal then
                nth = n
            end

            if i ~= 0 then
                -- Bagian kiri/kanan dari navigasi
                local w = ord2eng{
                    num = n,
                    ordinal = ordinal,
                    capitalize = capitalize
                }
                local catlink = catlinkfollowr(
                    frame,
                    firstpart .. lspace .. w .. tspace .. lastpart,
                    nth
                )
                if minword <= n and n <= maxword then
                    if catlink.rtarget then
                        -- Sebuah {{Category redirect}} diikuti
                        trackcat(24, 'Navigasi seri kategori wordinal dialihkan')
                    end
                    -- Tampilkan normal jika masih dalam jangkauan n_max atau maxword bukan default
                    if n <= n_max or maxword ~= maxword_default then
                        table.insert(navlist, prepad .. catlink.navelement)
                        prepad = ''
                    else
                        -- Jika melebihi n_max, tampilkan sebagai teks tak terlihat
                        local postpad = '<span style="visibility:hidden"> • ' .. nth .. '</span>'
                        navlist[#navlist] = (navlist[#navlist] or '') .. postpad
                        if listall then
                            tlistall[#tlistall] = tlistall[#tlistall] .. ' (' .. postpad .. ')'
                        end
                    end
                else
                    -- Di luar rentang minword / maxword
                    local postpad = '<span style="visibility:hidden"> • ' .. nth .. '</span>'
                    navlist[#navlist] = (navlist[#navlist] or '') .. postpad
                    if listall then
                        tlistall[#tlistall] = tlistall[#tlistall] .. ' (' .. postpad .. ')'
                    end
                end
            else
                -- Bagian tengah (center) dari navigasi (kata bilangan induk)
                table.insert(navlist, prepad .. '<b>' .. nth .. '</b>')
                prepad = ''
            end
        else
            -- Jika n < 1, tampilkan placeholder agar tata letak tetap rapi
            prepad = prepad .. '<span style="visibility:hidden"> • ' .. '0' .. th .. '</span>'
            if listall then
                tlistall[#tlistall] = (tlistall[#tlistall] or '') .. ' (x)'
            end
        end

        i = i + 1
    end

    -- Menambahkan daftar ke string hasil
    navw = navw .. horizontal(navlist) .. '\n'
    
    isolatedcat()
    if listall then
        return listalllinks()
    else
        return navw .. '</div>'
    end
end


--[[==========================={{  find_var  }}===============================]]

local function find_var( pn )
    -- Mengekstrak teks variabel (misalnya tahun, musim, dll.) dari sebuah string,
    -- dan mengembalikan { ['vtype'] = <'tahun'|'musim'|dll.>, <v> = <2023|2023–24|dll.> }
    local pagename = currtitle.text
    if pn and pn ~= '' then
        pagename = pn
    end
    
    local cpagename = 'Kategori:'..pagename -- workaround regex Lua terbatas
    
    -- Pola untuk dekade, misalnya "1990-an di Indonesia (1990–1999)"
    local d_season = mw.ustring.match(cpagename, ':(%d-an).+%(%d+[–-]%d+%)')
    
    -- Pola untuk tahun dengan penambahan, misalnya "Pendiri tahun 2020 di Jakarta (2020–2024)"
    local y_season = mw.ustring.match(cpagename, ':(%d+) .+%(%d+[–-]%d+%)')
    
    -- Pola untuk tahun yang belum selesai, misalnya "Anggota Parlemen Indonesia 2021–"
    local e_season = mw.ustring.match(cpagename, '%s(%d+[–-])$') or
                     mw.ustring.match(cpagename, '%s(%d+[–-]sekarang)$')
    
    -- Pola untuk musim atau periode, misalnya "Periode 2023–2024"
    local season   = mw.ustring.match(cpagename, '[:%s%(](%d+[–-]%d+)[%)%s]') or
                     mw.ustring.match(cpagename, '[:%s](%d+[–-]%d+)$')
    
    -- Pola untuk musim TV, misalnya "musim 3"
    local tvseason = mw.ustring.match(cpagename, 'musim (%d+)') or
                     mw.ustring.match(cpagename, 'seri (%d+)')
    
    -- Pola untuk ordinal dalam Bahasa Indonesia, misalnya "ke-3"
    local nordinal = mw.ustring.match(cpagename, '[:%s](ke%-%d+)[%s-]') or
                     mw.ustring.match(cpagename, '[:%s](ke%-%d+)$')
    
    -- Pola untuk dekade, misalnya "1990-an"
    local decade   = mw.ustring.match(cpagename, '[:%s](%d+%-an)[%s-]') or
                     mw.ustring.match(cpagename, '[:%s](%d+%-an)$')
    
    -- Pola untuk tahun, prioritas pada tahun 4 digit
    local year     = mw.ustring.match(cpagename, '[:%s](%d%d%d%d)%s') or
                     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
                     -- Ekspansi/kombinasi pengecualian jika diperlukan
                     mw.ustring.match(cpagename, '[:%s](%d+)-terkait') or
                     mw.ustring.match(cpagename, '[:%s](%d+)-silinder') or
                     mw.ustring.match(cpagename, '[:%-VW](%d+)%s') -- misalnya "Mesin Straight-8"
    
    -- Pola untuk angka Romawi, misalnya "III"
    local roman    = mw.ustring.match(cpagename, '%s([IVXLCDM]+)%s')
    
    -- Menemukan variabel yang cocok
    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
            -- Mengembalikan berdasarkan kompleksitas/kemungkinan duplikasi
            if nordinal and season then -- misalnya "Pendiri abad ke-18 di Indonesia (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
        -- Mencoba ordinal dalam Bahasa Inggris ('zeroth' hingga 'ninety-ninth' saja)
        local eng2ord = require('Module:ConvertNumeric').indonesian_to_ordinal
        local split = mw.text.split(pagename, ' ')
        for i=1, #split do
            if eng2ord(split[i]) > -1 then
                return { ['vtype'] = 'wordinal', ['v'] = split[i] }
            end
        end
        
        -- Mencoba numerik dalam Bahasa Inggris ('one'/'single' hingga 'ninety-nine' saja)
        local eng2num = require('Module:ConvertNumeric').indonesian_to_numeral
        local split = mw.text.split(pagename, '[%s%-]') -- misalnya "Mesin-silinder sembilan"
        for i=1, #split do
            if eng2num(split[i]) > -1 then
                return { ['vtype'] = 'enumeric', ['v'] = split[i] }
            end
        end
    end
    
    -- Jika tidak ditemukan, kembalikan error
    errors = p.errorclass('Fungsi find_var tidak dapat menemukan teks variabel dalam kategori "'..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