پرش به محتوا

پودمان:Navseasoncats

از ویکی‌پدیا، دانشنامهٔ آزاد
توضیحات پودمان[ایجاد] [پاکسازی]
local p = {}
local num_con = require('Module:Numeral converter').convert

--[[==========================================================================]]
--[[                                Globals                                   ]]
--[[==========================================================================]]

local errors = ''
local nexistingcats = 0
local currtitle = mw.title.getCurrentTitle()
local testcasecolon = ''
local testcases = (currtitle.subpageText == 'testcases')
if    testcases then testcasecolon = ':' end
local navborder = true
local followRs = true
local listall = false
local listofalllinks = {}
local skipgaps = false
local tlistall = {}
local tlistallbwd = {}
local tlistallfwd = {}
local misctrackingcats = {
	'', -- [1] placeholder for [[Category:Navseasoncats using cat parameter]]'
	'', -- [2] placeholder for [[Category:Navseasoncats using testcase parameter]]'
	'', -- [3] placeholder for [[Category:Navseasoncats range not using en dash]]
	'', -- [4] placeholder for [[Category:Navseasoncats range abbreviated]]
	'', -- [5] placeholder for [[Category:Navseasoncats range redirected (base change)]]
	'', -- [6] placeholder for [[Category:Navseasoncats range redirected (MOS)]]
	'', -- [7] placeholder for [[Category:Navseasoncats isolated]]
	'', -- [8] placeholder for [[Category:Navseasoncats default season gap size]]
	'', -- [9] placeholder for [[Category:Navseasoncats decade redirected]]
	'', --[10] placeholder for [[Category:Navseasoncats year redirected]]
	'', --[11] placeholder for [[Category:Navseasoncats roman numeral redirected]]
	'', --[12] placeholder for [[Category:Navseasoncats nordinal redirected]]
	'', --[13] placeholder for [[Category:Navseasoncats wordinal redirected]]
	'', --[14] placeholder for [[Category:Navseasoncats TV season redirected]]
	'', --[15] placeholder for [[Category:Navseasoncats using skip-gaps parameter]]
}
local ttrackingcats = { --when reindexing, Ctrl+H 'trackcat(13,' & 'ttrackingcats[16]'
	'', -- [1] placeholder for [[Category:Navseasoncats using cat parameter]]
	'', -- [2] placeholder for [[Category:Navseasoncats using testcase parameter]]
	'', -- [3] placeholder for [[Category:Navseasoncats using unknown parameter]]
	'', -- [4] placeholder for [[Category:Navseasoncats range not using en dash]]
	'', -- [5] placeholder for [[Category:Navseasoncats range abbreviated (MOS)]]
	'', -- [6] placeholder for [[Category:Navseasoncats range redirected (base change)]]
	'', -- [7] placeholder for [[Category:Navseasoncats range redirected (var change)]] --new
	'', -- [8] placeholder for [[Category:Navseasoncats range redirected (end)]]
	'', -- [9] placeholder for [[Category:Navseasoncats range redirected (MOS)]]
	'', --[10] placeholder for [[Category:Navseasoncats range redirected (other)]]
	'', --[11] placeholder for [[Category:Navseasoncats range gaps]]
	'', --[12] placeholder for [[Category:Navseasoncats range irregular]]
	'', --[13] placeholder for [[Category:Navseasoncats range irregular, 0-length]]
	'', --[14] placeholder for [[Category:Navseasoncats range ends (present)]]
	'', --[15] placeholder for [[Category:Navseasoncats range ends (blank, MOS)]]
	'', --[16] placeholder for [[Category:Navseasoncats isolated]]
	'', --[17] placeholder for [[Category:Navseasoncats default season gap size]]
	'', --[18] placeholder for [[Category:Navseasoncats decade redirected]]
	'', --[19] placeholder for [[Category:Navseasoncats year redirected (base change)]]
	'', --[20] placeholder for [[Category:Navseasoncats year redirected (var change)]]
	'', --[21] placeholder for [[Category:Navseasoncats year redirected (other)]]
	'', --[22] placeholder for [[Category:Navseasoncats roman numeral redirected]]
	'', --[23] placeholder for [[Category:Navseasoncats nordinal redirected]]
	'', --[24] placeholder for [[Category:Navseasoncats wordinal redirected]]
	'', --[25] placeholder for [[Category:Navseasoncats TV season redirected]]
	'', --[26] placeholder for [[Category:Navseasoncats using skip-gaps parameter]]
	'', --[27] placeholder for [[Category:Navseasoncats year and range]]
	'', --[28] placeholder for [[Category:Navseasoncats year and decade]]
	'', --[29] placeholder for [[Category:Navseasoncats decade and century]]
	'', --[30] placeholder for [[Category:Navseasoncats in mainspace]]
	'', --[31] placeholder for [[Category:Navseasoncats redirection error]]
}
local avoidself =  (currtitle.text ~= 'Navseasoncats' and          --avoid self
					currtitle.text ~= 'Navseasoncats/توضیحات' and      --avoid self
					currtitle.text ~= 'Navseasoncats/تمرین' and  --avoid self
					(currtitle.nsText ~= 'الگو' 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, 'رده' ).exists
end

--Error message handling
--Also used by {{Navseasoncats with centuries below decade}}.
function p.errorclass( msg )
	return mw.text.tag( 'span', {class='error mw-ext-cite-error'}, '<b>خطا!</b> '..string.gsub(msg, '&#', '&amp;#') )
end

--Failure handling
--Also used by {{Navseasoncats with centuries below decade}}.
function p.failedcat( errors, sortkey )
	if avoidself then
		return (errors or '')..'&#42;&#42;&#42;Navseasoncats ناتوان در ایجاد جعبه ناوبری***'..
			   '[['..testcasecolon..'رده:Navseasoncats failed to generate navbox|'..(sortkey or 'O')..']]'
	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..'Category:'..cat..']]'
		else
			ttrackingcats[key] = ''
		end
	end
	return
end

--Check for nav_*() navigational isolation (not necessarily an error).
--Used by all nav_*().
function isolatedcat()
	if nexistingcats == 0 and avoidself then
		misctrackingcats[7] = '[['..testcasecolon..'رده:Navseasoncats isolated]]'
	end
end

--
--Now unused, in favor of catlinkfollowr().
--
--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.
function catlink( catname, displaytext )
	catname = num_con("fa", mw.text.trim(catname or ''))
	displaytext = num_con("fa", mw.text.trim(displaytext or ''))
	local grey = '#888'
	local disp = catname
	if displaytext ~= '' then --use 'displaytext' parameter if present
		disp = mw.ustring.gsub(displaytext, '%s+%(.+$', ''); --strip any trailing disambiguator
	end
	
	local exists = mw.title.new( catname, 'رده' ).exists
	if exists then
		nexistingcats = nexistingcats + 1
		return '[[:رده:'..catname..'|'..disp..']]'
	else
		return '<span style="color:'..grey..'">'..disp..'</span>'
	end
end

--Similar to catlink() but follows {{Category redirect}}s.
--Returns { <#R target navelement>, <basetext of #R target> } if {{Category redirect}} followed;
--returns { <original navelement>, nil } otherwise.
--Used by all nav_*().
function catlinkfollowr( catname, displaytext , displayend )
	catname = num_con("fa", mw.text.trim(catname or ''))
	displaytext = num_con("fa", mw.text.trim(displaytext or ''))
	local grey = '#305'
	local disp = catname
	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(catname)
	if exists then
		nexistingcats = nexistingcats + 1
		if followRs then
			local R = num_con("fa", rtarget(catname))
			if R == catname then --no #R
				link = '[[:رده:'..catname..'|'..disp..']]'
				nilorR = nil
			else --#R followed
				link = '[[:رده:'..R..'|'..disp..']]'
				nilorR = R
			end
		else
			link = '[[:رده:'..catname..'|'..disp..']]'
			nilorR = nil
		end
	else
		link = '<span style="color:'..grey..'">'..disp..'</span>'
		nilorR = nil
		--return { '[[:رده:'..catname..'|'..disp..']]', nil } --for debugging
	end
	
	if listall then
		if nilorR then --#R followed
			table.insert( listofalllinks, '[[:رده:'..catname..']] → '..'[[:رده:'..nilorR..']] ('..link..')' )
		else --no #R
			table.insert( listofalllinks, '[[:رده:'..catname..']] ('..link..')' )
		end
	end
	
	return {
		['cat'] = cat,
		['catexists'] = exists,
		['rtarget'] = nilorR,
		['navelement'] = link,
		['displaytext'] = disp,
	}
end

--Returns the target of {{Category redirect}}, if it exists, else returns the original cat.
--Used by catlinkfollowr(), and so indirectly by all nav_*().
function rtarget( cat )
	local catcontent = mw.title.new( cat or '', 'رده' ):getContent()
	if string.match( catcontent or '', '*رده }}' ) then
		local regex = {
			--the following 11 pages (7 condensed) redirect to [[Template:Category redirect]] (as of 6/2019):
			{ '1', '{{ *[Cc]ategory *[Rr]edirect' }, --most likely match 1st
			{ '2', '{{ *رده *بهتر' },         --444+240 transclusions
			{ '3', '{{ *تغییر *مسیر *رده' },            --8+3
			{ '4', '{{ *تغییرمسیر *رده' },        --6
			{ '5', '{{ *تغییرمسیررده' },              --6
			{ '6', '{{ *[Cc]atr' },                  --4
			{ '7', '{{ *[Cc]at *move' },             --0
		}
		for k, v in pairs (regex) do
			local rtarget = mw.ustring.match( catcontent, v[2]..'%s*|%s*([^|}]+)' )
			if rtarget then
				rtarget = mw.ustring.gsub(rtarget, '^1%s*=%s*', '')
				rtarget = string.gsub(rtarget, '^رده:', '')
				return rtarget
			end
		end
	end
	return cat
end

--Returns a numbered list of all {{Category redirect}}s followed by catlinkfollowr() -> rtarget().
--Used by all nav_*().
function listalllinks()
	local nl = '\n# '
	local out = ''
	if currtitle.nsText == 'رده' then
		errors = p.errorclass('پارامتر <b><code>|list-followed-redirects=yes</code></b> '..
							'نباید در فضای نام رده ذخیره شود، تنها باید پیش‌نمایانده شود.')
		out = p.failedcat(errors, 'Z')
	end
	if listofalllinks[1] then
		return out..nl..table.concat(listofalllinks, nl)
	else
		return out..nl..'پیوندی یافت نشد؟!'
	end
end

--Returns the difference b/w 2 ints separated by endash|hyphen, nil if error.
--Used by nav_hyphen() only.
local function find_duration( cat )
	local from, to = mw.ustring.match(cat, '(%d+)[–-](%d+)')
	if from and to then
		if to == '00' then return nil end --doesn't follow MOS:DATERANGE
		if (#from == 4) and (#to == 2) then             --1900-01
			to = string.match(from, '(%d%d)%d%d')..to   --1900-1901
		elseif (#from == 2) and (#to == 4) then         --  01-1902
			from = string.match(to, '(%d%d)%d%d')..from --1901-1902
		end
		return (tonumber(to) - tonumber(from))
	end
	return 0
end

--Returns the ending of a terminal cat, and sets the appropriate tracking cat, else nil.
--Used by nav_hyphen() only.
local function find_terminaltxt( cat )
	local terminaltxt = nil
	if mw.ustring.match(cat, '%d+[–-]present$') then
		terminaltxt = 'present'
		trackcat(14, 'Navseasoncats range ends (present)')
	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 error.
--Used by nav_decade() only.
function sterilizedec( decade )
	if decade == nil or decade == '' then
		return nil
	end
	
	local dec = mw.ustring.match(decade, '^[-%+]?(%d?%d?%d?۰)$') or 
				mw.ustring.match(decade, '^[-%+]?(%d?%d?%d?۰)%D')
	if dec then
		return dec
	else
		--fix 2-4 digit decade
		local decade_fixed234 = mw.ustring.match(decade, '^[-%+]?(%d%d?%d?)%d$') or
								mw.ustring.match(decade, '^[-%+]?(%d%d?%d?)%d%D')
		if decade_fixed234 then
			return decade_fixed234..'۰'
		end
		
		--fix 1-digit decade
		local decade_fixed1   = mw.ustring.match(decade, '^[-%+]?(%d)$') or
								mw.ustring.match(decade, '^[-%+]?(%d)%D')
		if decade_fixed1 then
			return '۰'
		end
		
		--unfixable
		errors = 'sterilizedec() error'
		return nil
	end
end

--Check for nav_hyphen default gap size + isolatedcat() (not necessarily an error).
--Used by nav_hyphen() only.
function defaultgapcat( bool )
	if bool and nexistingcats == 0 and avoidself then
		--using "nexistingcats > 0" isn't as useful, since the default gap size obviously worked
		misctrackingcats[8] = '[['..testcasecolon..'رده:Navseasoncats default season gap size]]'
	end
end

--12 -> 12th, etc.
--Used by nav_nordinal(), nav_wordinal(), and {{Navseasoncats with centuries below decade}}.
function p.addord( i )
	if tonumber(i) then
		local s = tostring(i)
		
		local tens = mw.ustring.match(s, '۱%d$')
		if    tens then return s..'' end
		
		local  ones = mw.ustring.match(s, '%d$')
		if     ones == '۱' then return s..''
		elseif ones == '۲' then return s..''
		elseif ones == '۳' then return s..'' end
		
		return s..''
	end
	return i
end



--[[==========================={{  nav_decade  }}=============================]]

function nav_decade( firstpart, decade, lastpart)
	--Expects a PAGENAME of the form "Some sequential 2000 example cat", where 
	--	firstpart = Some sequential
	--	decade    = 2000
	--	lastpart  = example cat
	--	mindecade = 1800 ('min' decade parameter; optional)
	--	maxdecade = 2020 ('max' decade parameter; optional; defaults to next decade)
	
	--sterilize dec
	-- local dec = sterilizedec(decade)
	if errors ~= '' then
		errors = p.errorclass('عملگر nav_decade «'..(decade or '')..'» را به‌عنوان پارامتر دومش دریافت کرده، '..
							'اما انتظار دارد که ورودی یک سال چهار رقمی باشد که با «۰» پایان می‌یابد".')
		return p.failedcat(errors, 'D')
	end
	
	local ndec = tonumber(num_con("en", decade))
	local ncen = ndec
	
	--begin navdecade
	local bnb = '' --border/no border
	if navborder == false then --for embedding in ...
		bnb = ' border-style: none; background-color: transparent;'
	end
	
	local navd = '{| class="toccolours hlist" style="text-align: center; margin: auto;'..bnb..'"\n'..'|\n'
	
	local i = -50
	while i <= 50 do
		local d = ndec + i
	
			--determine target cat
			local disp = num_con("fa", d)
			local catlink = catlinkfollowr(firstpart..' '..num_con("fa", d)..' '..lastpart, disp)
			if catlink.rtarget and avoidself then --a {{Category redirect}} was followed
				misctrackingcats[9] = '[['..testcasecolon..'رده:Navseasoncats decade redirected]]'
			end
		
			--populate left/right navd
			local shown = '*'..catlink.navelement..'\n'
			local hidden = '*'..disp..'\n'
			local exists = mw.title.new(firstpart..' '..num_con("fa", d)..' '..lastpart, 'رده').exists
		
			if exists then
				navd = navd..shown
			else
				navd = navd..hidden
			end
		
			if listall and hidden then
				listofalllinks[#listofalllinks] = listofalllinks[#listofalllinks]..' ('..hidden..')'
			end
	
		i = i + 10
	end
	
	isolatedcat()
	if listall then
		return listalllinks()
	else
		return navd..'|}'
	end
end

--[[==========================={{  nav_century  }}=============================]]

function nav_century( firstpart, century, lastpart)
	
	local ncen = tonumber(num_con("en", century))
	
	--begin navcentury
	local bnb = '' --border/no border
	if navborder == false then --for embedding in ...
		bnb = ' border-style: none; background-color: transparent;'
	end
	
	local navc = '{| class="toccolours hlist" style="text-align: center; margin: auto;'..bnb..'"\n'..'|\n'

	local i = -5
	while i <= 5 do
		local d = ncen + i

		--determine target cat
		local disp = num_con("fa", d)
		local catlink = catlinkfollowr(firstpart..' '..num_con("fa", d)..' '..lastpart, disp)
		if catlink.rtarget and avoidself then --a {{Category redirect}} was followed
			misctrackingcats[9] = '[['..testcasecolon..'رده:Navseasoncats decade redirected]]'
		end
	
		--populate left/right navd
		local shown = '*'..catlink.navelement..'\n'
		local hidden = '*'..disp..'\n'
		local exists = mw.title.new(firstpart..' '..num_con("fa", d)..' '..lastpart, 'رده').exists
	
		if exists then
			navc = navc..shown
		else
			navc = navc..hidden
		end
	
		if listall and hidden then
			listofalllinks[#listofalllinks] = listofalllinks[#listofalllinks]..' ('..hidden..')'
		end

		i = i + 1
	end
	
	isolatedcat()
	if listall then
		return listalllinks()
	else
		return navc..'|}'
	end
end

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

function nav_year(firstpart, year, lastpart)
	--Expects a PAGENAME of the form "Some sequential 1760 example cat", where 
	--	firstpart   = Some sequential
	--	year        = 1760
	--	lastpart    = example cat
	--	minimumyear = 1758 ('min' year parameter; optional)
	--	maximumyear = 1800 ('max' year parameter; optional)
	
	if year == nil then
		errors = p.errorclass('Function nav_year can\'t recognize the year sent to its 2nd parameter.')
		return p.failedcat(errors, 'Y')
	end
	
	local nyear = tonumber(num_con("en", year))
	
	--begin navyear
	local bnb = '' --border/no border
	if navborder == false then --for embedding in ...
		bnb = ' border-style: none; background-color: transparent;'
	end
	
	local navy = '{| class="toccolours hlist" style="text-align: center; margin: auto;'..bnb..'"\n'..'|\n'
	
	local i = -5
	while i <= 5 do
		local d = nyear + i
	
		--determine target cat
		local disp = num_con("fa", d)
		local catlink = catlinkfollowr(firstpart..' '..num_con("fa", d)..' '..lastpart, disp)
		if catlink.rtarget and avoidself then --a {{Category redirect}} was followed
			misctrackingcats[9] = '[['..testcasecolon..'رده:Navseasoncats decade redirected]]'
		end
	
		--populate left/right navd
		local shown = '*'..catlink.navelement..'\n'
		local hidden = '*'..disp..'\n'
		local exists = mw.title.new(firstpart..' '..num_con("fa", d)..' '..lastpart, 'رده').exists
	
		if exists then
			navy = navy..shown
		else
			navy = navy..hidden
		end
	
		if listall and hidden then
			listofalllinks[#listofalllinks] = listofalllinks[#listofalllinks]..' ('..hidden..')'
		end
	
		i = i + 1
	end

	isolatedcat()
	if listall then
		return listalllinks()
	else
		return navy..'|}'
	end
end


--[[==========================================================================]]
--[[                  Formerly separated templates/modules                    ]]
--[[==========================================================================]]


--[[==========================={{  nav_hyphen  }}=============================]]

function nav_hyphen(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)
	
	start = num_con("en", start)
	finish = num_con("en", finish)
	
	--sterilize start
	if mw.ustring.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 --"Members of the Scottish Parliament 2021–present"
	   (finish == 0)	 --"Members of the Scottish Parliament 2021–"
	then
		regularparent = false
		if maxseas == nil or maxseas == '' then
			maxseas = start --hide subsequent ranges
		end
		if finish == -1 then trackcat(13, 'Navseasoncats range ends (present)')
		else				 trackcat(14, 'Navseasoncats range ends (blank, MOS)') end
	elseif (start == finish) and
		   (ttrackingcats[15] ~= '') --nav_year found isolated; check for surrounding hyphenated terms (e.g. UK MPs 1974)
	then
		trackcat(15, '') --reset for another check later
		trackcat(12, '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 mw.ustring.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 mw.ustring.match(firstpart, '%($') then lspace = '' end --DNE for "Madrid city councillors (2007–2011)"-type cats
	if mw.ustring.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="..start..hyph.."finish="..finish..'".')
			return p.failedcat(errors, 'K')
		end
		t = t + 1
	end
	
	--apply MOS:DATERANGE to parent
	local lenstart = mw.ustring.len(start)
	local lenfinish = mw.ustring.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..mw.ustring.match(nstart-hgap, '%d?%d$')    ..tspace..lastpart
		local nextseason2 = firstpart..lspace..(nstart+t+hgap)..hyph..mw.ustring.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 mw.title.new(num_con("fa", prevseason2), 'رده').exists or --use 'or', in case we're at the edge of the cat structure,
			   mw.title.new(num_con("fa", nextseason2), 'رده').exists or --or we hit a "–00"/"–2000" situation on one side
			   mw.title.new(num_con("fa", prevseason4), 'رده').exists or
			   mw.title.new(num_con("fa", nextseason4), 'رده').exists
			then
				hgap_success = true
				break
			end
		elseif t > 1 then --test full range first, then abbreviated range, to be frugal with expensive functions
			if mw.title.new(num_con("fa", prevseason4), 'رده').exists or --use 'or', in case we're at the edge of the cat structure,
			   mw.title.new(num_con("fa", nextseason4), 'رده').exists or --or we hit a "–00"/"–2000" situation on one side
			   mw.title.new(num_con("fa", prevseason2), 'رده').exists or 
			   mw.title.new(num_con("fa", nextseason2), 'رده').exists
			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 --pseudo nav position & index of tirregs[] for j > 0
		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( full ).rtarget ~= nil then --#R followed
									table.insert( tlistallbwd, spangreen..j..', '..g..', '..k..span..'] '..full..spanred..'#R ignored'..span..')' )
									full, to = '', '' --don't use/follow 0-length cat #Rs from nav_hyphen(); otherwise gets messy
								end
							end
						end
						if (k >= 1) or		  --the normal case; only continue k = 0 if 0-length found
						   (to == '0-length') --ghetto "continue" (thx Lua) to avoid expensive searches for "UK MPs 1974-1974", etc.
						then
							table.insert( tlistallbwd, spangreen..j..', '..g..', '..k..span..'] '..full )
							if (k == 1) and
							   (g == 0 or g == 1) and
							   (mw.title.new( full, 'رده' ).exists == false)
							then --allow bare-bones MOS:DATERANGE alternation, in case we're on a 0|1-gap, 1-year term series
								local to2 = mw.ustring.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 mw.title.new( num_con("fa", full), 'رده' ).exists then
								if to == '0-length' then
									trackcat(12, '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( full ).rtarget ~= nil then --#R followed
									table.insert( tlistallfwd, spangreen..j..', '..g..', '..k..span..'] '..full..spanred..'#R ignored'..span..')' )
									full, to4 = '', '' --don't use/follow 0-length cat #Rs from nav_hyphen(); otherwise gets messy
								end
							end
						end
						if (k >= -1) or		   --only continue k = -2 if 0-length found
						   (to4 == '0-length') --ghetto "continue" (thx Lua) to avoid expensive searches for "UK MPs 1974-1974", etc.
						then
							table.insert( tlistallfwd, spangreen..j..', '..g..', '..k..span..'] '..full )
							if (k == 1) and
							   (g == 0 or g == 1) and
							   (mw.title.new( full, 'Category' ).exists == false)
							then --allow bare-bones MOS:DATERANGE alternation, in case we're on a 0|1-gap, 1-year term series
								to2 = mw.ustring.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 mw.title.new( full, 'Category' ).exists then
								if to4 == '0-length' then
									if rtarget(frame, full) == full then --only use 0-length cats that don't #R
										trackcat(12, '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 = mw.ustring.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 = mw.ustring.match(to, '%d?%d$')
		local tofinal = (to2 or '')    --assume t=1 and abbreviated 'to' (the most common case)
		if t > 1 or                    --per MOS:DATERANGE (e.g. 1999-2004)
		  (from2 - (to2 or from2)) > 0 --century transition exception (e.g. 1999–2000)
		then
			tofinal = (to or '')       --default to the MOS-correct format, in case no fallbacks found
		end
		if to == '0-length' then
			tofinal = to
		end
		
		--check existance of 4-digit, MOS-correct range, with abbreviation fallback
		if tofinal ~= '0-length' then
			if t > 1 and mw.ustring.len(from) == 4 then --e.g. 1999-2004
				--determine which link exists (full or abbr)
				local full = firstpart..lspace..from..hyph..tofinal..tspace..lastpart
				full = num_con("fa", full)
				if not mw.title.new( full, 'رده' ).exists then
					local abbr = firstpart..lspace..from..hyph..to2..tspace..lastpart
					if mw.title.new( abbr, 'رده' ).exists 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 mw.title.new( abbr, 'رده' ).exists and tofinal ~= to then
					local full = firstpart..lspace..from..hyph..to..tspace..lastpart
					full = num_con("fa", full)
					if mw.title.new( full, 'رده' ).exists 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(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 = mw.ustring.gsub(catlink.rtarget, base_regex, '')
				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(7, 'Navseasoncats range redirected (end)')
				else
					local all4s_regex = '%d%d%d%d[–-]%d%d%d%d'
					local all4s = (mw.ustring.match(orig, all4s_regex) and
								   mw.ustring.match(catlink.rtarget, all4s_regex))
					if all4s then
						trackcat(9, 'Navseasoncats range redirected (other)')
					else
						trackcat(8, 'Navseasoncats range redirected (MOS)')
					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 = 'present'
			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>'..num_con("fa", 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(10, 'Navseasoncats range gaps') end
		if itlens > 1 and ttrackingcats[12] == '' then --avoid duplication in "Navseasoncats range irregular, 0-length"
			trackcat(11, 'Navseasoncats range irregular')
		end
	end
	isolatedcat()
	defaultgapcat(not hgap_success)
	if listall then
		return listalllinks()
	else
		return navh..'|}'
	end
end


--[[==========================={{  find_var  }}===============================]]
function p.find_var( pn )
	--Extracts the variable text (e.g. 2015–16, 3rd, 2000s, III, etc.) from a string
	local pagename = currtitle.text
	if pn and pn ~= '' then
		pagename = pn
	end
	
	local cpagename = 'رده:'..pagename--limited-Lua-regex workaround
					
	local e_season = mw.ustring.match(cpagename, '%s(%d+[–-])$') --irreg; ending unknown, e.g. "Members of the Scottish Parliament 2021–"
	local season   = mw.ustring.match(cpagename, '[:%s%(](%d+[–-]%d+)[%)%s]') or --split in 2 b/c you can't frontier '$'/eos?
					 mw.ustring.match(cpagename, '[:%s](%d+[–-]%d+)$')
	local decade   = mw.ustring.match(cpagename, '[:%s]دهه%s(%d+)')
	local century  = mw.ustring.match(cpagename, '[:%s]سده%s(%d+)')
	local year     = mw.ustring.match(cpagename, '[:%s](%d%d%d%d)%s') or --prioritize 4-digit years first
					 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
					 mw.ustring.match(cpagename, '[:%s].-(%d+).-$')

	local found    = e_season or season or decade or century or year
	
	if found then
		if e_season then return { 'ending',   e_season } end
		if season   then return { 'season',   season   } end
		if decade then return { 'decade',   decade } end
		if century then return { 'century',  century } end
		if year then return { 'year', year } end
	end
	
	errors = p.errorclass('Function find_var can\'t find the variable x text in the category: "'..cpagename..'"')
	return { 'error', p.failedcat(errors, 'V') }
end

--[[==========================================================================]]
--[[                                  Main                                    ]]
--[[==========================================================================]]

function p.navseasoncats( frame )
	local args = frame:getParent().args
	local dby  = args['decade-below-year']    --used by {{Navseasoncats with decades below year}}
	local cbd  = args['century-below-decade'] --used by {{Navseasoncats with centuries below decade}}
	local cat  = args['cat']                  --'testcase' alias for mainspace
	local list = args['list-all-links']       --utility to output all links & followed #Rs instead of a navbar
	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']
	
	if dby then
		navborder = false
		dby = string.gsub(dby, '&#32;', ' ') --unicodify forced whitespace
	end
	if cbd then
		navborder = false
		cbd = string.gsub(cbd, '&#32;', ' ') --unicodify forced whitespace
	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
		if avoidself then
			misctrackingcats[15] = '[['..testcasecolon..'رده:Navseasoncats using skip-gaps parameter]]'
		end
	end
	
	if currtitle.nsText == 'Category' then
		if cat      then misctrackingcats[1] = '[['..testcasecolon..'رده:Navseasoncats using cat parameter]]' end
		if testcase then misctrackingcats[2] = '[['..testcasecolon..'رده:Navseasoncats using testcase parameter]]' end
	end
	
	local pagename = testcase or cat or dby or cbd or currtitle.text
	
	local findvar = p.find_var(pagename)
	if findvar[1] == 'error' then return findvar[2]..table.concat(misctrackingcats) end --basic format error checking in find_var()

	local findvar_escaped = mw.ustring.gsub( findvar[2], '-', '%-')
	local firstpart, lastpart = mw.ustring.match(pagename, '^(.-)'..findvar_escaped..'(.*)$')
	firstpart = mw.text.trim(firstpart or '')
	lastpart  = mw.text.trim(lastpart or '')
	
	local start = mw.ustring.match(findvar[2], '^%d+')
	
	--determine the appropriate nav function
	if findvar[1] == 'season' then       --e.g. "1–4", "1999–2000", "2001–02", "2001–2002", "2005–2010", etc.
		finish = start --در فارسی برعکس
		local hyphen, start = mw.ustring.match(findvar[2], '%d([–-])(%d+)') --ascii 150 & 45 (ndash & keyboard hyphen); mw req'd
		return nav_hyphen( start, hyphen, finish, firstpart, lastpart, minimum, maximum, testcasegap )..table.concat(misctrackingcats)
		
	elseif findvar[1] == 'ending' then   --e.g. "2021–" (irregular; ending unknown)
		finish = start --در فارسی برعکس
		local hyphen, start = mw.ustring.match(findvar[2], '%d([–-])$'), 0 --ascii 150 & 45 (ndash & keyboard hyphen); mw req'd
		return nav_hyphen( start, hyphen, finish, firstpart, lastpart, minimum, maximum, testcasegap )..table.concat(misctrackingcats)
		
	elseif findvar[1] == 'decade' then   --مثلاً دهه 1870 میلادی
		return nav_decade( firstpart, start, lastpart)..table.concat(misctrackingcats)
		
	elseif findvar[1] == 'century' then  --مثلاً سده 18 میلادی
		return nav_century( firstpart, start, lastpart)..table.concat(misctrackingcats)
		
	elseif findvar[1] == 'year' then --مثلاً سال 2005 میلادی
		return nav_year( firstpart, start, lastpart)..table.concat(misctrackingcats)
		
	else                                 --بدشکل
		errors = p.errorclass('Failed to determine the appropriate nav function from malformed season "'..findvar[2]..'". ')
		return p.failedcat(errors, 'N')..table.concat(misctrackingcats)
	end
end

return p