Module:Television episode short description

--- @module local television = {}

-- Unique suffix list. local uniqueSuffix = { [1] = "st", [2] = "nd", [3] = "rd", }

-- Common suffix. local commonSuffix = "th"

-- Test validation. local test = false

local descriptions = { no_series = { type = 1, text = "Television episode", category = "", },	only_series_name = { type = 2, text = "Episode of %s", category = "", },	season_and_series_name = { type = 3, text = "Episode of the %s %s of %s", category = "", },	single_episode = { type = 4, text = "%s episode of the %s %s of %s", category = "", },	multi_episodes = { type = 5, text = "%s episodes of the %s %s of %s", category = "", },	limited_series = { type = 6, text = { single_episode = "%s episode of %s", multi_episodes = "%s episodes of %s", },		category = "", -- None },	special_episode = { type = 7, text = "%s episode of %s", category = "", -- None }, }

-- Tracking category list. local trackingCategories = { disambiguated = "" }

--- Returns a tracking category from a list by its name and adds a sort key. --- @param typeName string The name of the category type. --- @param useTrackingList boolean Whether to return a category from the trackingCategories list. --- @param sortKey string The key by which to sort the page in the category. local function getTrackingCategoryFromList(typeName, useTrackingList, sortKey) local category if useTrackingList then category = trackingCategories[typeName] else category = descriptions[typeName].category end return string.format(category, sortKey) end

--- Returns true if the article name is disambiguated. --- --- This is usually in the format of "Episode name ()" or "Episode name ( episode)". --- @param articleTitle string The name of the page. --- @param tvSeriesName string The TV series name. local function isDisambiguated(articleTitle, tvSeriesName) local disambiguation = string.match(tostring(articleTitle), "%s%((.-)%)")

if not (disambiguation and tvSeriesName) then return false end

-- Search for the TV series name in the article name disambiguation. if (string.find(disambiguation, tvSeriesName)) then return true end

return false end

--- Returns the sort key for the current page. local function getSortKey local sortTitleModule = require("Module:Sort title") return sortTitleModule._getSortKey end

--- Returns a tracking category depending on the type of short description created. --- @param tvSeriesName string The TV series name. --- @param descriptionName string local function getTrackingCategory(tvSeriesName, descriptionName) local articleTitle = mw.title.getCurrentTitle local namespace = articleTitle.nsText

-- Check if the invoking page is from the allowed namespace. if (not (namespace == "" or namespace == "Draft" or test)) then return "" end

local sortKey = getSortKey if (isDisambiguated(articleTitle, tvSeriesName) == true) then local category1 = getTrackingCategoryFromList(descriptionName, false, sortKey) local category2 = getTrackingCategoryFromList("disambiguated", true, sortKey) return category1 .. category2 end

return getTrackingCategoryFromList(descriptionName, false, sortKey) end

--- Returns a short description in the style of: "Television episode" and a maintenance category: --- "Category:Television episode articles with short description with no series name". local function getShortDescriptionNoSeries local shortDescription = descriptions.no_series.text local category = getTrackingCategory(nil, "no_series") return shortDescription, category end

--- Returns a short description in the style of: "Episode of Lost" and a maintenance category: --- "Category:Television episode articles with short description with no season number". --- @param tvSeriesName string The TV series name. local function getShortDescriptionOnlySeriesName(tvSeriesName) local text = descriptions.only_series_name.text local shortDescription = string.format(text, tvSeriesName) local category = getTrackingCategory(tvSeriesName, "only_series_name") return shortDescription, category end

--- Returns a short description in the style of: "Episode of the first season of Lost" and a maintenance category: --- "Category:Television episode articles with short description with no episode number". --- @param tvSeriesName string The TV series name. --- @param seasonOrdinalNumber string The season's ordinal number. --- @param seasonTextStyle string The text to use for seasons - either "season" or "series". local function getShortDescriptionSeasonAndSeriesName(tvSeriesName, seasonOrdinalNumber, seasonTextStyle) local text = descriptions.season_and_series_name.text local shortDescription = string.format(text, seasonOrdinalNumber, seasonTextStyle, tvSeriesName) local category = getTrackingCategory(tvSeriesName, "season_and_series_name") return shortDescription, category end

--- Returns a short description for a limited series in the style of: "1st episode of WandaVision" and a tracking category --- based on the categoryKey value. --- @param tvSeriesName string The TV series name. --- @param episodeOrdinalNumber string The episode's ordinal number. --- @param descriptionName string A key from the descriptions table. local function getShortDescriptionLimitedSeries(tvSeriesName, episodeOrdinalNumber, descriptionName) local text = descriptions.limited_series.text[descriptionName] local shortDescription = string.format(text, episodeOrdinalNumber, tvSeriesName) local category = getTrackingCategory(tvSeriesName, descriptionName) return shortDescription, category end

--- Returns a short description in the style of: "5th episode of the fourth season of Lost" and a tracking category: --- "Category:Television episode articles with short description for single episodes". --- @param tvSeriesName string The TV series name. --- @param seasonOrdinalNumber string The season's ordinal number. --- @param seasonTextStyle string The text to use for seasons - either "season" or "series". --- @param episodeOrdinalNumber string The episode's ordinal number. --- @param limitedSeries boolean Whether the episode belongs to a limited series. local function getShortDescriptionSingleEpisode(tvSeriesName, seasonOrdinalNumber, seasonTextStyle, episodeOrdinalNumber, limitedSeries) if (limitedSeries) then return getShortDescriptionLimitedSeries(tvSeriesName, episodeOrdinalNumber,"single_episode") end

local text = descriptions.single_episode.text local shortDescription = string.format(text, episodeOrdinalNumber, seasonOrdinalNumber, seasonTextStyle, tvSeriesName) local category = getTrackingCategory(tvSeriesName, "single_episode") return shortDescription, category end

--- Returns a short description for a multi-part episode in the style of: --- "23rd and 24th episodes of the third season of Lost" and a tracking category: --- "Category:Television episode articles with short description for multi-part episodes". --- @param tvSeriesName string The TV series name. --- @param seasonOrdinalNumber string The season's ordinal number. --- @param seasonTextStyle string The text to use for seasons - either "season" or "series". --- @param episodeOrdinalNumbers table A list of episode ordinal numbers. --- @param limitedSeries boolean Whether the episode belongs to a limited series. local function getShortDescriptionMultiEpisode(tvSeriesName, seasonOrdinalNumber, seasonTextStyle, episodeOrdinalNumbers, limitedSeries) local episodeText = mw.text.listToText(episodeOrdinalNumbers)

if (limitedSeries) then return getShortDescriptionLimitedSeries(tvSeriesName, episodeText, "multi_episodes") end

local text = descriptions.multi_episodes.text local shortDescription = string.format(text, episodeText, seasonOrdinalNumber, seasonTextStyle, tvSeriesName) local category = getTrackingCategory(tvSeriesName, "multi_episodes") return shortDescription, category end

--- Returns a short description for a special episode in the style of: --- "Special episode of Lost" or " episode of Lost" and a tracking category: --- "Category:Television episode articles with short description for single episodes". --- @param tvSeriesName string The TV series name. --- @param special string The type of special episode. A "yes" value defaults to "Special". local function getShortDescriptionSpecialEpisode(tvSeriesName, special) if (special == "yes" or special == "y") then special = "Special" end local text = descriptions.special_episode.text local shortDescription = string.format(text, special, tvSeriesName) local category = getTrackingCategory(tvSeriesName, "single_episode") return shortDescription, category end

--- Returns a short description based on the description type passed. --- @param descriptionType number A description type number. --- @param tvSeriesName string The TV series name. --- @param seasonOrdinalNumber string The season's ordinal number. --- @param seasonTextStyle string The text to use for seasons - either "season" or "series". --- @param episodeOrdinalNumbers table A list of episode ordinal numbers. --- @param specialEpisode string The type of special episode. --- @param limitedSeries boolean Whether the episode belongs to a limited series. local function getShortDescriptionByType(		descriptionType, tvSeriesName, seasonOrdinalNumber, seasonTextStyle, episodeOrdinalNumbers, specialEpisode, limitedSeries) if descriptionType == descriptions.no_series.type then return getShortDescriptionNoSeries elseif descriptionType == descriptions.only_series_name.type then return getShortDescriptionOnlySeriesName(tvSeriesName) elseif descriptionType == descriptions.season_and_series_name.type then return getShortDescriptionSeasonAndSeriesName(tvSeriesName, seasonOrdinalNumber, seasonTextStyle) elseif descriptionType == descriptions.single_episode.type then return getShortDescriptionSingleEpisode(               tvSeriesName, seasonOrdinalNumber, seasonTextStyle, episodeOrdinalNumbers[1], limitedSeries) elseif descriptionType == descriptions.multi_episodes.type then return getShortDescriptionMultiEpisode(				tvSeriesName, seasonOrdinalNumber, seasonTextStyle, episodeOrdinalNumbers, limitedSeries) elseif descriptionType == descriptions.special_episode.type then return getShortDescriptionSpecialEpisode(tvSeriesName, specialEpisode) else return "" end end

--- Returns the type of the description to use. --- @param tvSeriesName string The TV series name. --- @param seasonOrdinalNumber string The season's ordinal number. --- @param episodeOrdinalNumbers table A list of episode ordinal numbers. --- @param specialEpisode string The type of special episode. --- @param limitedSeries boolean Whether the episode belongs to a limited series. local function getDescriptionType(tvSeriesName, seasonOrdinalNumber, episodeOrdinalNumbers, specialEpisode, limitedSeries) if (not tvSeriesName) then return descriptions.no_series.type end

if (specialEpisode) then return descriptions.special_episode.type end

if (not seasonOrdinalNumber and not limitedSeries) then return descriptions.only_series_name.type end

if (#episodeOrdinalNumbers < 1) then return descriptions.season_and_series_name.type end

if (#episodeOrdinalNumbers == 1) then return descriptions.single_episode.type end

if (#episodeOrdinalNumbers > 1) then return descriptions.multi_episodes.type end end

--- Returns true if the TV series is a limited series. --- @param limitedSeries string Any value will be considered as true. local function isLimitedSeries(limitedSeries) if (limitedSeries) then return true end return false end

--- Returns the ordinal indicator for an integer between 0 and 100. --- --- Numbers "1", "2" and "3" have unique suffixes. --- Numbers between 4 and 20 have the same common suffix - "th". --- Numbers ending with 0 have the same common suffix - "th". --- @param number number A number value. local function getOrdinalIndicatorLessThan100(number) local suffix while (not suffix) do -- Check if the number equals 0; This should never be a valid entry. Assign suffix as an empty string. if (number == 0) then suffix = "" -- Check if the number is less than 4; Numbers "1", "2" and "3" have unique suffixes. elseif (number < 4) then suffix = uniqueSuffix[number] -- Check if the number is more than 4 AND less than 20; These numbers all have the same common suffix. elseif (number < 20) then suffix = commonSuffix -- Check if the remainder after division of the number by 10 equals 0. elseif (number % 10 == 0) then suffix = commonSuffix else -- Numbers that are above 20 and which their remainder doesn't equal 0 (such as 45). -- Remainder after division of the number by 10; So if the current number is 45, the new number is 5. number = number % 10 end end return suffix end

--- Returns the ordinal indicator for an integer between 0 and 1000. --- @param number number A number value. local function getOrdinalIndicatorLessThan1000(number) if (number < 100) then return getOrdinalIndicatorLessThan100(number) elseif (number % 100 == 0) then return commonSuffix else -- Numbers that are above 100 and which their remainder doesn't equal 0 (such as 345). -- Pass the remainder after division of the number by 100 (So for 345, it would pass 45) as the parameter. return getOrdinalIndicatorLessThan100(number % 100) end end

--- Returns a table of episode numbers. --- --- Episode values may be of multipart episodes, in such situations, an episode may be seperated by one of the following: --- ",", "/", "&", "-", "–", "and". --- Decimal values and episode overall values sometimes erroneously used are removed. --- @param number string A number value in string format. local function cleanEpisodeNumber(number) if (not number) then return {} end

number = string.gsub(number, "%(.*%)", " ") number = string.gsub(number, "%.%d+", " ")

local numbers = {} for digits in string.gmatch(number, "%d+") do		table.insert(numbers, tonumber(digits)) end return numbers end

--- Returns a table of episode ordinal numbers. --- --- In most situations there will be only one episode, but this can support more. --- @param episodeNumber number The episode's number. local function getEpisodeOrdinalNumbers(episodeNumber) local episodeNumbers = cleanEpisodeNumber(episodeNumber)

if (#episodeNumbers < 1) then return episodeNumbers end

local episodeOrdinals = {} for _, cleanedEpisodeNumber in pairs(episodeNumbers) do		local ordinalIndicator = getOrdinalIndicatorLessThan1000(cleanedEpisodeNumber) table.insert(episodeOrdinals, cleanedEpisodeNumber .. ordinalIndicator) end

return episodeOrdinals end

--- Returns true if the season number value is a number. --- @param seasonNumber string The season number value in string format. local function validateSeasonNumber(seasonNumber) if (tonumber(seasonNumber)) then return true else return false end end

--- Returns the season's ordinal number, or nil if no season number was set. --- @param seasonNumber string The season number. local function getSeasonOrdinalNumber(seasonNumber) if (seasonNumber) then local convertOrdinal = require("Module:Ordinal") return convertOrdinal._ordinal(seasonNumber) end return nil end

--- Returns a season number after removing from it unwanted characters. --- --- This is done to make sure that no malformed season values have been entered. --- The function will remove all text which is not part of the first number in the string. --- --- The function converts entries such as: --- "1.2" -> "1" --- "12.2" -> "12" --- @param seasonNumber string The season number value in string format. local function cleanSeasonNumber(seasonNumber) if (seasonNumber) then return string.match(seasonNumber, "%d+") end return nil end

--- Returns the season number after or cleaning it from unwanted values and validating value is a number. --- Also returns the text style to use - either "season" or "series". --- If no value was entered or if value was not a number, return nil. --- @param seasonNumber string The season number. --- @param seasonNumberUK string The season number, if UK style was used. local function getSeasonNumberAndTextStyle(seasonNumber, seasonNumberUK) for _, v in ipairs({{seasonNumber, "season"}, {seasonNumberUK, "series"}}) do       local cleanedSeasonNumber = cleanSeasonNumber(v[1]) if (validateSeasonNumber(cleanedSeasonNumber)) then return cleanedSeasonNumber, v[2] end end return nil end

--- Returns the TV series title without disambiguation, or nil if no TV series name was set. --- @param tvSeriesName string The TV series name. --- @param notDab string If set, the parenthesis in the title is not disambiguation. local function getTVSeriesName(tvSeriesName, notDab) if (tvSeriesName) then if (not notDab) then return string.gsub(tvSeriesName, "%s+%b$", "", 1, false) end return tvSeriesName end return nil end

--- Returns the initial values after removing unwanted characters. --- @param args table The values that should be processed. local function cleanValues(args) for _, v in ipairs({"episode_num", "season_num", "season_num_uk", "series_name"}) do		if (args[v]) then args[v] = args[v]:gsub("\127[^\127]*UNIQ%-%-(%a+)%-%x+%-QINU[^\127]*\127", "")	-- Remove all strip-markers. args[v] = args[v]:gsub("", " ")					-- Replace (and variants) with space character. args[v] = args[v]:gsub("%b<>[^<]+%b<>", "")					-- Remove html markup. args[v] = args[v]:gsub("%b<>", "")							-- Remove self-closed html tags. args[v] = args[v]:gsub("%[%([^%+)%]%]", "%1")		-- Remove wiki-link retain label. args[v] = args[v]:gsub("%[%[([^%]]+)%]%]", "%1")			-- Remove wiki-link retain article. args[v] = args[v]:gsub("%[%S+ +([^%]]-)%]", "%1")			-- Remove URLs retain label. args[v] = args[v]:gsub("%^%-%]", "")					-- Remove all remaining URLs.

if (args[v] == "") then										-- Check if the value is an empty string. args[v] = nil											-- The value is an empty string; Set it to nil. end end end return args end

--- Public function - main process. --- @param frame table The frame invoking the module. --- @param args table The key-value parameters passed to the module. function television._getShortDescription(frame, args) args = cleanValues(args) local tvSeriesName = getTVSeriesName(args.series_name, args.not_dab) local seasonNumber, seasonTextStyle = getSeasonNumberAndTextStyle(args.season_num, args.season_num_uk) local seasonOrdinalNumber = getSeasonOrdinalNumber(seasonNumber) local episodeOrdinalNumbers = getEpisodeOrdinalNumbers(args.episode_num) local limitedSeries = isLimitedSeries(args.limited) local descriptionType = getDescriptionType(			tvSeriesName,			seasonOrdinalNumber,			episodeOrdinalNumbers,			args.special,			limitedSeries	)

local shortDescription, trackingCat = getShortDescriptionByType(			descriptionType,			tvSeriesName,			seasonOrdinalNumber,           seasonTextStyle,			episodeOrdinalNumbers,			args.special,			limitedSeries	)

-- Check if the invoking page is from /testcases or /doc pages. if (args.test) then return shortDescription, trackingCat elseif (args.doc) then return shortDescription else local tableData = {shortDescription, "noreplace"} return frame:expandTemplate({title = "short description", args = tableData}) .. trackingCat end end

--- Public function which is used to create a television episode's short description --- from the data available in [Template:Infobox television episode]. --- A suitable description will be generated depending on the values of the various parameters. --- See documentation for examples. --- --- Parameters: --- |episode_num=		— optional; The episode's number. --- |season_num=		— optional; The season's number. --- |season_num_uk=		— optional; The season's number if using the British "series" term. --- |series_name=		— optional; The TV series name. --- |not_dab=			— optional; Set if the TV series name has parentheses as part of its name. --- |special=			— optional; Setting to "yes" will set the description as a "special episode". ---							Any other value will replace the word "special" with the one entered. ---							For example "special=recap" will create "recap episode". ---	|limited=			— optional; Set if the series is a single season series, such as miniseries or limited series ---							and does not need a season number as part of the description. --- @param frame table The frame invoking the module. function television.getShortDescription(frame) local getArgs = require("Module:Arguments").getArgs local args = getArgs(frame) return television._getShortDescription(frame, args) end

--- Public function which is used for testing output only. --- @param frame table The frame invoking the module. function television.test(frame) local getArgs = require("Module:Arguments").getArgs local args = getArgs(frame)

test = args.test local shortDescription, categories = television._getShortDescription(frame, args)

if (test == "cat") then return categories else return shortDescription end end

return television