Module:gender and number: Difference between revisions

From Linguifex
Jump to navigation Jump to search
Dillon (talk | contribs)
No edit summary
No edit summary
 
(7 intermediate revisions by 3 users not shown)
Line 1: Line 1:
--[=[
local export = {}
This module creates standardised displays for gender and number.
It converts a gender specification into Wiki/HTML format.
A gender/number specification consists of one or more gender/number elements, separated by hyphens.
Examples are: "n" (neuter gender), "f-p" (feminine plural), "m-an-p" (masculine animate plural),
"pf" (perfective aspect). Each gender/number element has the following properties:
1. A code, as used in the spec, e.g. "f" for feminine, "p" for plural".
2. A type, e.g. "gender", "number" or "animacy". Each element in a given spec must be of a different type.
3. A display form, which in turn consists of a display code and a tooltip gloss. The display code
  may not be the same as the spec code, e.g. the spec code "an" has display code "anim" and tooltip
  gloss "animate".
    4. A category into which lemmas of the right part of speech are placed if they have a gender/number
  spec containing the given element. For example, a noun with gender/number spec "m-an-p" is placed
  into the categories "LANG masculine nouns", "LANG animate nouns" and "LANG pluralia tantum".
]=]--


local export = {}
local debug_track_module = "Module:debug/track"
local load_module = "Module:load"
local pron_qualifier_module = "Module:pron qualifier"
local parameters_module = "Module:parameters"
local string_utilities_module = "Module:string utilities"
local table_module = "Module:table"
local utilities_module = "Module:utilities"


local codes = {}
local concat = table.concat
local insert = table.insert


-- A list of all possible "parts" that a specification can be made out of. For each part, we list
local function debug_track(...)
-- the class it's in (gender, animacy, etc.), the associated category (if any) and the display form.
debug_track = require(debug_track_module)
-- In a given gender/number spec, only one part of each class is allowed.
return debug_track(...)
end
 
local function deep_copy(...)
deep_copy = require(table_module).deepCopy
return deep_copy(...)
end


codes["?"] = {type = "other", display = '<abbr title="gender incomplete">?</abbr>'}
local function format_categories(...)
format_categories = require(utilities_module).format_categories
return format_categories(...)
end


-- Genders
local function format_pron_qualifiers(...)
codes["m"] = {type = "gender", cat = "masculine POS", display = '<abbr title="masculine gender">m</abbr>'}
format_pron_qualifiers = require(pron_qualifier_module).format_qualifiers
codes["f"] = {type = "gender", cat = "feminine POS", display = '<abbr title="feminine gender">f</abbr>'}
return format_pron_qualifiers(...)
codes["n"] = {type = "gender", cat = "neuter POS", display = '<abbr title="neuter gender">n</abbr>'}
end
codes["c"] = {type = "gender", cat = "common-gender POS", display = '<abbr title="common gender">c</abbr>'}
-- Noun classes for High Valyrian, feel free to add more custom classes for your language
codes["sol"] = {type = "gender", cat = "solar POS", display = '<abbr title="solar class">sol</abbr>'}
codes["lun"] = {type = "gender", cat = "lunar POS", display = '<abbr title="lunar class">lun</abbr>'}
codes["aq"] = {type = "gender", cat = "aquatic POS", display = '<abbr title="aquatic class">aq</abbr>'}
codes["ter"] = {type = "gender", cat = "terrestrial POS", display = '<abbr title="terrestrial class">ter</abbr>'}
-- Noun classes for Gwaxol
codes["hum"] = {type = "gender", cat = "human POS", display = '<abbr title="human class">hum</abbr>'}
codes["anim1"] = {type = "gender", cat = "animate 1 POS", display = '<abbr title="animate class 1">anim1</abbr>'}
codes["anim2"] = {type = "gender", cat = "animate 2 POS", display = '<abbr title="animate class 2">anim2</abbr>'}
codes["inan1"] = {type = "gender", cat = "inanimate 1 POS", display = '<abbr title="inanimate class 1">inan1</abbr>'}
codes["inan2"] = {type = "gender", cat = "inanimate 2 POS", display = '<abbr title="inanimate class 2">inan2</abbr>'}
-- Noun classes for Wascotl
codes["d"] = {type = "gender", cat = "divine POS", display = '<abbr title="divine class">d</abbr>'}
codes["h"] = {type = "gender", cat = "human POS", display = '<abbr title="human class">h</abbr>'}
codes["o"] = {type = "gender", cat = "organic POS", display = '<abbr title="organic class">o</abbr>'}
codes["a"] = {type = "gender", cat = "animate POS", display = '<abbr title="animate class">a</abbr>'}
codes["i"] = {type = "gender", cat = "inanimate POS", display = '<abbr title="inanimate class">i</abbr>'}


-- Animacy
local function load_data(...)
codes["an"] = {type = "animacy", cat = "animate POS", display = '<abbr title="animate">anim</abbr>'}
load_data = require(load_module).load_data
codes["in"] = {type = "animacy", cat = "inanimate POS", display = '<abbr title="inanimate">inan</abbr>'}
return load_data(...)
-- Animal (for Ukrainian, Belarusian, Polish)
end
codes["anml"] = {type = "animacy", cat = "animal POS", display = '<abbr title="animal">animal</abbr>'}
-- Personal (for Ukrainian, Belarusian, Polish)
codes["pr"] = {type = "animacy", cat = "personal POS", display = '<abbr title="personal">pers</abbr>'}
-- Nonpersonal not currently used
codes["np"] = {type = "animacy", cat = "nonpersonal POS", display = '<abbr title="nonpersonal">npers</abbr>'}


-- Virility (for Polish)
local function process_params(...)
codes["vr"] = {type = "virility", cat = "virile POS", display = '<abbr title="virile">vir</abbr>'}
process_params = require(parameters_module).process
codes["nv"] = {type = "virility", cat = "nonvirile POS", display = '<abbr title="nonvirile">nvir</abbr>'}
return process_params(...)
end


-- Numbers
local function split(...)
codes["s"] = {type = "number", display = '<abbr title="singular number">sg</abbr>'}
split = require(string_utilities_module).split
codes["d"] = {type = "number", display = '<abbr title="dual number">du</abbr>'}
return split(...)
codes["p"] = {type = "number", cat = "pluralia tantum", display = '<abbr title="plural number">pl</abbr>'}
end
codes["pa"] = {type = "number", display = '<abbr title="paucal number">pa</abbr>'}
codes["ol"] = {type = "number", display = '<abbr title="collective number">col</abbr>'}


-- Verb qualifiers
local gender_and_number_data
codes["impf"] = {type = "aspect", cat = "imperfective POS", display = '<abbr title="imperfective aspect">impf</abbr>'}
local function get_gender_and_number_data()
codes["pf"] = {type = "aspect", cat = "perfective POS", display = '<abbr title="perfective aspect">pf</abbr>'}
gender_and_number_data, get_gender_and_number_data = load_data("Module:gender and number/data"), nil
-- Soc'ul'
return gender_and_number_data
codes["acc"] = {type = "aspect", cat = "accusative POS", display = '<abbr title="accusative aligned">ᴀᴄᴄ</abbr>'}
end
codes["erg"] = {type = "aspect", cat = "ergative POS", display = '<abbr title="ergative aligned">ᴇʀɢ</abbr>'}


-- Combined codes that are equivalent to giving multiple specs. `mf` is the same as specifying two separate specs,
-- one with `m` in it and the other with `f`. `mfbysense` is similar but is used for nouns that can be either masculine
-- or feminine according as to whether they refer to masculine or feminine beings.
local combined_codes = {}
combined_codes["mf"] = {codes = {"m", "f"}}
combined_codes["mfbysense"] = {codes = {"m", "f"}, cat = "masculine and feminine POS by sense"}


-- Categories when multiple gender/number specs of a given type occur in different
--[==[ intro:
local codetype_cats = {}
This module creates standardised displays for gender and number. It converts a gender specification into Wiki/HTML format.
codetype_cats["gender"] = "POS with multiple genders"
codetype_cats["animacy"] = "ambiguous POS"
codetype_cats["aspect"] = "biaspectual POS"


A gender/number specification consists of one or more gender/number elements, separated by hyphens. Examples are:
{"n"} (neuter gender), {"f-p"} (feminine plural), {"m-an-p"} (masculine animate plural),
{"pf"} (perfective aspect). Each gender/number element has the following properties:
# A code, as used in the spec, e.g. {"f"} for feminine, {"p"} for plural.
# A type, e.g. `gender`, `number` or `animacy`. Each element in a given spec must be of a different type.
# A display form, which in turn consists of a display code and a tooltip gloss. The display code
  may not be the same as the spec code, e.g. the spec code {"an"} has display code {"anim"} and tooltip
  gloss ''animate''.
# A category into which lemmas of the right part of speech are placed if they have a gender/number
  spec containing the given element. For example, a noun with gender/number spec {"m-an-p"} is placed
  into the categories `<var>lang</var> masculine nouns`, `<var>lang</var> animate nouns` and `<var>lang</var> pluralia tantum`.
]==]


-- Version of format_list that can be invoked from a template.
--[==[
Version of format_list that can be invoked from a template.
]==]
function export.show_list(frame)
function export.show_list(frame)
local args = frame.args
local params = {
local lang = args["lang"]; if lang == "" then lang = nil end
[1] = {list = true},
local list = {}
["lang"] = {type = "language"},
local i = 1
}
local iargs = process_params(frame.args, params)
while args[i] and args[i] ~= "" do
return export.format_list(iargs[1], iargs.lang)
table.insert(list, args[i])
i = i + 1
end
return export.format_list(list, lang)
end
end




-- Older entry point; equivalent to format_genders() except that it formats the
--[==[
-- categories and returns them appended to the formatted gender text rather than
Older entry point; equivalent to format_genders() except that it formats the
-- returning the formatted text and categories separately.
categories and returns them appended to the formatted gender text rather than
returning the formatted text and categories separately.
]==]
function export.format_list(specs, lang, pos_for_cat, sort_key)
function export.format_list(specs, lang, pos_for_cat, sort_key)
debug_track("gender and number/old-format-list")
local text, cats = export.format_genders(specs, lang, pos_for_cat)
local text, cats = export.format_genders(specs, lang, pos_for_cat)
if #cats == 0 then
if not cats then
return text
return text
end
end
return text .. require("Module:utilities").format_categories(cats, lang, sort_key)
return text .. format_categories(cats, lang, sort_key)
end
 
 
local function autoadd_abbr(display)
if not display then
error("Internal error: '.display' for gender/number code is missing")
end
if display:find("<abbr", nil, true) then
return display
end
return ('%s'):format(display, display)
end
end


 
-- Format one or more gender/number specifications. Each spec is either a string, e.g. "f-p", or
-- Add qualifiers, labels and references to a formatted gender/number spec. `spec` is the object describing the
-- a table of the form {spec = "SPEC", qualifiers = {"QUALIFIER", "QUALIFIER", ...}} where `.spec`
-- gender/number spec, which should optionally contain:
-- is a gender/number spec such as "f-p" and `.qualifiers` is a list of qualifiers to display before
-- * left qualifiers in `q` or (for compatibility) `qualifiers`, an array of strings;
-- the formatted gender/number spec. `.spec` must be present but `.qualifiers` may be omitted.
-- * right qualifiers in `qq`, an array of strings;
-- The function returns two values:
-- * left labels in `l`, an array of strings;
-- (a) the formatted text;
-- * right labels in `ll`, an array of strings;
-- (b) a list of the categories to add.
-- * references in `refs`, an array either of strings (formatted reference text) or objects containing fields `text`
-- If `lang` and `pos_for_cat` are given, gender categories such as "German masculine nouns" or
--  (formatted reference text) and optionally `name` and/or `group`;
-- "Russian imperfective verbs" are added to the categories. Otherwise, if only `lang` is given,
-- `formatted` is the formatted version of the term itself, and `lang` is the optional language object passed into
-- the only category that may be returned is "Requests for gender in LANG entries". If both are
-- format_genders().
-- omitted, the returned list is empty.
local function add_qualifiers_and_refs(formatted, spec, lang)
local function field_non_empty(field)
local list = spec[field]
if not list then
return nil
end
if type(list) ~= "table" then
error(("Internal error: Wrong type for `spec.%s`=%s, should be \"table\""):format(
field, mw.dumpObject(list)))
end
return list[1]
end
 
if field_non_empty("q") or field_non_empty("qq") or field_non_empty("l") or field_non_empty("ll") or
field_non_empty("qualifiers") or field_non_empty("refs") then
formatted = format_pron_qualifiers{
lang = lang,
text = formatted,
q = spec.q,
qq = spec.qq,
qualifiers = spec.qualifiers,
l = spec.l,
ll = spec.ll,
refs = spec.refs,
}
end
 
return formatted
end
 
 
--[==[
Format one or more gender/number specifications. Each spec is either a string, e.g. {"f-p"}, or a table of the form
{ {spec = "SPEC", qualifiers = {"QUALIFIER", "QUALIFIER", ...}}} where `.spec` is a gender/number spec such as {"f-p"}
and `.qualifiers` is a list of qualifiers to display before the formatted gender/number spec. `.spec` must be present
but `.qualifiers` may be omitted.
 
The function returns two values:
# the formatted text;
# a list of the categories to add.
 
If `lang` (which should be a language object) and `pos_for_cat` (which should be a plural part of speech) are given,
gender categories such as `German masculine nouns` or `Russian imperfective verbs` are added to the categories, and
request categories such as `Requests for gender in <var>lang</var> entries` or
`Requests for animacy in <var>lang</var> entries` may also be added. Otherwise, if only `lang` is given, only request
categories may be returned. If both are omitted, the returned list is empty.
]==]
function export.format_genders(specs, lang, pos_for_cat)
function export.format_genders(specs, lang, pos_for_cat)
local formatted_specs = {}
local formatted_specs, categories, seen_types = {}
local categories = {}
local seen_types = {}
local category_text = ""
local all_is_nounclass = nil
local all_is_nounclass = nil
local full_langname = lang and lang:getFullName() or nil


local function do_gender_spec(spec, parts)
local function do_gender_spec(spec, parts)
local types = {}
local types = {}
local codes = (gender_and_number_data or get_gender_and_number_data()).codes


for key, code in ipairs(parts) do
for key, code in ipairs(parts) do
-- Is this code valid?
-- Is this code valid?
if not codes[code] then
if not codes[code] then
error('The tag "' .. code .. '" in the gender specification "' .. spec.spec .. '" is not valid.')
error('The tag "' .. code .. '" in the gender specification "' .. spec.spec .. '" is not valid. See [[Module:gender and number]] for a list of valid tags.')
end
end
Line 153: Line 188:
types[typ] = true
types[typ] = true
if spec.qualifiers and #spec.qualifiers > 0 then
parts[key] = autoadd_abbr(codes[code].display)
parts[key] = require("Module:qualifier").format_qualifier(spec.qualifiers) .. " " .. codes[code].display
else
parts[key] = codes[code].display
end
-- Generate categories if called for.
-- Generate categories if called for.
Line 163: Line 194:
local cat = codes[code].cat
local cat = codes[code].cat
if cat then
if cat then
table.insert(categories, lang:getCanonicalName() .. " " .. cat)
if not categories then
categories = {}
end
insert(categories, full_langname .. " " .. cat)
end
end
if seen_types[typ] and seen_types[typ] ~= code then
if not seen_types then
cat = codetype_cats[typ]
seen_types = {}
elseif seen_types[typ] and seen_types[typ] ~= code then
cat = (gender_and_number_data or get_gender_and_number_data()).multicode_cats[typ]
if cat then
if cat then
table.insert(categories, lang:getCanonicalName() .. " " .. cat)
if not categories then
categories = {}
end
insert(categories, full_langname .. " " .. cat)
end
end
end
end
seen_types[typ] = code
seen_types[typ] = code
end
if lang and codes[code].req then
local type_for_req = typ
if code == "?" then
-- Keep in mind `pos_for_cat` may be nil here.
type_for_req = pos_for_cat == "verbs" and "aspect" or "gender"
end
if not categories then
categories = {}
end
insert(categories, "Requests for " .. type_for_req .. " in " .. full_langname .. " entries")
end
end
end
end
 
-- Add the processed codes together with non-breaking spaces
-- Add the processed codes together with non-breaking spaces
if #parts == 1 then
if not parts[2] and parts[1] then
return parts[1]
return parts[1]
else
return concat(parts, "&nbsp;")
end
end
return table.concat(parts, "&nbsp;")
end
end


Line 186: Line 237:
spec = {spec = spec}
spec = {spec = spec}
end
end
local is_nounclass
local spec_spec, is_nounclass = spec.spec
-- If the specification starts with cX, then it is a noun class specification.
-- If the specification starts with cX, then it is a noun class specification.
if spec.spec:find("^[1-9]") or spec.spec:find("^c[^-]") then
if spec_spec:match("^%d") or spec_spec:match("^c[^-]") then
is_nounclass = true
is_nounclass = true
code = spec.spec:gsub("^c", "")
local code = spec_spec:gsub("^c", "")
local text
local text
if code == "?" then
if code == "?" then
text = '<abbr class="noun-class" style="font-style: italic;" title="noun class missing">?</abbr>'
text = '<abbr class="noun-class" title="noun class missing">?</abbr>'
if lang then
if not categories then
categories = {}
end
insert(categories, "Requests for noun class in " .. full_langname .. " entries")
end
else
else
text = '<abbr class="noun-class" style="font-style: italic;" title="noun class ' .. code .. '">' .. code .. "</abbr>"
text = '<abbr class="noun-class" title="noun class ' .. code .. '">' .. code .. "</abbr>"
if lang and pos_for_cat then
if lang and pos_for_cat then
table.insert(categories, lang:getCanonicalName() .. " class-" .. code .. " POS")
if not categories then
categories = {}
end
insert(categories, full_langname .. " class " .. code .. " POS")
end
end
end
end
local text_with_qual
local text_with_qual = add_qualifiers_and_refs(text, spec, lang)
if spec.qualifiers and #spec.qualifiers > 0 then
insert(formatted_specs, text_with_qual)
text_with_qual = require("Module:qualifier").format_qualifier(spec.qualifiers) .. " " .. text
else
text_with_qual = text
end
table.insert(formatted_specs, text_with_qual)
else
else
-- Split the parts and iterate over each part, converting it into its display form
-- Split the parts and iterate over each part, converting it into its display form
local parts = mw.text.split(spec.spec, "%-")
local parts = split(spec.spec, "-", true, true)
local extra_cats = {}
local combined_codes = (gender_and_number_data or get_gender_and_number_data()).combinations
 
if lang then
-- Check if the specification is valid
--elseif langinfo.genders then
-- local valid_genders = {}
-- for _, g in ipairs(langinfo.genders) do valid_genders[g] = true end
--
-- if not valid_genders[spec.spec] then
-- local valid_string = {}
-- for i, g in ipairs(langinfo.genders) do valid_string[i] = g end
-- error('The gender specification "' .. spec.spec .. '" is not valid for ' .. langinfo.names[1] .. ". Valid are: " .. concat(valid_string, ", "))
-- end
--end
end


local has_combined = false
local has_combined = false
Line 222: Line 291:


if not has_combined then
if not has_combined then
table.insert(formatted_specs, do_gender_spec(spec, parts))
if formatted_specs[1] then
insert(formatted_specs, "or")
end
insert(formatted_specs, add_qualifiers_and_refs(do_gender_spec(spec, parts), spec, lang))
else
else
-- This logic is to handle combined gender specs like 'mf' and 'mfbysense'.
local all_parts = {{}}
local all_parts = {{}}
local extra_displays
local this_formatted_specs = {}


for i, code in ipairs(parts) do
for _, code in ipairs(parts) do
if combined_codes[code] then
if combined_codes[code] then
local new_all_parts = {}
local new_all_parts = {}
for _, one_parts in ipairs(all_parts) do
for _, one_parts in ipairs(all_parts) do
for _, one_code in ipairs(combined_codes[code].codes) do
for _, one_code in ipairs(combined_codes[code].codes) do
local new_combined_parts = mw.clone(one_parts)
local new_combined_parts = deep_copy(one_parts)
table.insert(new_combined_parts, one_code)
insert(new_combined_parts, one_code)
table.insert(new_all_parts, new_combined_parts)
insert(new_all_parts, new_combined_parts)
end
end
end
end
Line 240: Line 315:
local extra_cat = combined_codes[code].cat
local extra_cat = combined_codes[code].cat
if extra_cat then
if extra_cat then
table.insert(extra_cats, lang:getCanonicalName() .. " " .. extra_cat)
if not categories then
categories = {}
end
insert(categories, full_langname .. " " .. extra_cat)
end
end
local extra_display = combined_codes[code].display
if extra_display then
if not extra_displays then
extra_displays = {}
end
end
insert(extra_displays, autoadd_abbr(extra_display))
end
end
else
else
for _, one_parts in ipairs(all_parts) do
for _, one_parts in ipairs(all_parts) do
table.insert(one_parts, code)
insert(one_parts, code)
end
end
end
end
Line 251: Line 336:


for _, parts in ipairs(all_parts) do
for _, parts in ipairs(all_parts) do
table.insert(formatted_specs, do_gender_spec(spec, parts))
if this_formatted_specs[1] then
insert(this_formatted_specs, "or")
end
insert(this_formatted_specs, do_gender_spec(spec, parts))
end
end
end


if #extra_cats > 0 then
if extra_displays then
for _, cat in ipairs(extra_cats) do
for _, display in ipairs(extra_displays) do
table.insert(categories, cat)
insert(this_formatted_specs, display)
end
end
end
end


if lang then
insert(formatted_specs, add_qualifiers_and_refs(
-- Do some additional gender checks if a language was given
concat(this_formatted_specs, " "), spec, lang))
-- Is this an incomplete gender?
if spec.spec:find("?") then
table.insert(categories, "Requests for gender in " .. lang:getCanonicalName() .. " entries")
end
-- Check if the specification is valid
--elseif langinfo.genders then
-- local valid_genders = {}
-- for _, g in ipairs(langinfo.genders) do valid_genders[g] = true end
--
-- if not valid_genders[spec.spec] then
-- local valid_string = {}
-- for i, g in ipairs(langinfo.genders) do valid_string[i] = g end
-- error('The gender specification "' .. spec.spec .. '" is not valid for ' .. langinfo.names[1] .. ". Valid are: " .. table.concat(valid_string, ", "))
-- end
--end
end
end


Line 292: Line 363:
end
end


if lang and pos_for_cat then
if categories and lang and pos_for_cat then
for i, cat in ipairs(categories) do
for i, cat in ipairs(categories) do
categories[i] = cat:gsub("POS", pos_for_cat)
categories[i] = cat:gsub("POS", pos_for_cat)
Line 298: Line 369:
end
end


if is_nounclass then
if all_is_nounclass then
-- Add the processed codes together with slashes
-- Add the processed codes together with slashes
return '<span class="gender" style="font-style: italic;">class ' .. table.concat(formatted_specs, "/") .. "</span>", categories
return '<span class="gender">class ' .. concat(formatted_specs, "/") .. "</span>", categories
else
else
-- Add the processed codes together with " or "
-- Add the processed codes together with spaces
return '<span class="gender" style="font-style: italic;">' .. table.concat(formatted_specs, " or ") .. "</span>", categories
return '<span class="gender">' .. concat(formatted_specs, " ") .. "</span>", categories
end
end
end
end


return export
return export

Latest revision as of 11:43, 21 April 2026



local export = {}

local debug_track_module = "Module:debug/track"
local load_module = "Module:load"
local pron_qualifier_module = "Module:pron qualifier"
local parameters_module = "Module:parameters"
local string_utilities_module = "Module:string utilities"
local table_module = "Module:table"
local utilities_module = "Module:utilities"

local concat = table.concat
local insert = table.insert

local function debug_track(...)
	debug_track = require(debug_track_module)
	return debug_track(...)
end

local function deep_copy(...)
	deep_copy = require(table_module).deepCopy
	return deep_copy(...)
end

local function format_categories(...)
	format_categories = require(utilities_module).format_categories
	return format_categories(...)
end

local function format_pron_qualifiers(...)
	format_pron_qualifiers = require(pron_qualifier_module).format_qualifiers
	return format_pron_qualifiers(...)
end

local function load_data(...)
	load_data = require(load_module).load_data
	return load_data(...)
end

local function process_params(...)
	process_params = require(parameters_module).process
	return process_params(...)
end

local function split(...)
	split = require(string_utilities_module).split
	return split(...)
end

local gender_and_number_data
local function get_gender_and_number_data()
	gender_and_number_data, get_gender_and_number_data = load_data("Module:gender and number/data"), nil
	return gender_and_number_data
end


--[==[ intro:
This module creates standardised displays for gender and number. It converts a gender specification into Wiki/HTML format.

A gender/number specification consists of one or more gender/number elements, separated by hyphens. Examples are:
{"n"} (neuter gender), {"f-p"} (feminine plural), {"m-an-p"} (masculine animate plural),
{"pf"} (perfective aspect). Each gender/number element has the following properties:
# A code, as used in the spec, e.g. {"f"} for feminine, {"p"} for plural.
# A type, e.g. `gender`, `number` or `animacy`. Each element in a given spec must be of a different type.
# A display form, which in turn consists of a display code and a tooltip gloss. The display code
  may not be the same as the spec code, e.g. the spec code {"an"} has display code {"anim"} and tooltip
  gloss ''animate''.
# A category into which lemmas of the right part of speech are placed if they have a gender/number
  spec containing the given element. For example, a noun with gender/number spec {"m-an-p"} is placed
  into the categories `<var>lang</var> masculine nouns`, `<var>lang</var> animate nouns` and `<var>lang</var> pluralia tantum`.
]==]

--[==[
Version of format_list that can be invoked from a template.
]==]
function export.show_list(frame)
	local params = {
		[1] = {list = true},
		["lang"] = {type = "language"},
	}
	local iargs = process_params(frame.args, params)
	return export.format_list(iargs[1], iargs.lang)
end


--[==[
Older entry point; equivalent to format_genders() except that it formats the
categories and returns them appended to the formatted gender text rather than
returning the formatted text and categories separately.
]==]
function export.format_list(specs, lang, pos_for_cat, sort_key)
	debug_track("gender and number/old-format-list")
	local text, cats = export.format_genders(specs, lang, pos_for_cat)
	if not cats then
		return text
	end
	return text .. format_categories(cats, lang, sort_key)
end


local function autoadd_abbr(display)
	if not display then
		error("Internal error: '.display' for gender/number code is missing")
	end
	if display:find("<abbr", nil, true) then
		return display
	end
	return ('%s'):format(display, display)
end


-- Add qualifiers, labels and references to a formatted gender/number spec. `spec` is the object describing the
-- gender/number spec, which should optionally contain:
-- * left qualifiers in `q` or (for compatibility) `qualifiers`, an array of strings;
-- * right qualifiers in `qq`, an array of strings;
-- * left labels in `l`, an array of strings;
-- * right labels in `ll`, an array of strings;
-- * references in `refs`, an array either of strings (formatted reference text) or objects containing fields `text`
--   (formatted reference text) and optionally `name` and/or `group`;
-- `formatted` is the formatted version of the term itself, and `lang` is the optional language object passed into
-- format_genders().
local function add_qualifiers_and_refs(formatted, spec, lang)
	local function field_non_empty(field)
		local list = spec[field]
		if not list then
			return nil
		end
		if type(list) ~= "table" then
			error(("Internal error: Wrong type for `spec.%s`=%s, should be \"table\""):format(
				field, mw.dumpObject(list)))
		end
		return list[1]
	end

	if field_non_empty("q") or field_non_empty("qq") or field_non_empty("l") or field_non_empty("ll") or
		field_non_empty("qualifiers") or field_non_empty("refs") then
		formatted = format_pron_qualifiers{
			lang = lang,
			text = formatted,
			q = spec.q,
			qq = spec.qq,
			qualifiers = spec.qualifiers,
			l = spec.l,
			ll = spec.ll,
			refs = spec.refs,
		}
	end

	return formatted
end


--[==[
Format one or more gender/number specifications. Each spec is either a string, e.g. {"f-p"}, or a table of the form
{ {spec = "SPEC", qualifiers = {"QUALIFIER", "QUALIFIER", ...}}} where `.spec` is a gender/number spec such as {"f-p"}
and `.qualifiers` is a list of qualifiers to display before the formatted gender/number spec. `.spec` must be present
but `.qualifiers` may be omitted.

The function returns two values:
# the formatted text;
# a list of the categories to add.

If `lang` (which should be a language object) and `pos_for_cat` (which should be a plural part of speech) are given,
gender categories such as `German masculine nouns` or `Russian imperfective verbs` are added to the categories, and
request categories such as `Requests for gender in <var>lang</var> entries` or
`Requests for animacy in <var>lang</var> entries` may also be added. Otherwise, if only `lang` is given, only request
categories may be returned. If both are omitted, the returned list is empty.
]==]
function export.format_genders(specs, lang, pos_for_cat)
	local formatted_specs, categories, seen_types = {}
	local all_is_nounclass = nil
	local full_langname = lang and lang:getFullName() or nil

	local function do_gender_spec(spec, parts)
		local types = {}
		local codes = (gender_and_number_data or get_gender_and_number_data()).codes

		for key, code in ipairs(parts) do
			-- Is this code valid?
			if not codes[code] then
				error('The tag "' .. code .. '" in the gender specification "' .. spec.spec .. '" is not valid. See [[Module:gender and number]] for a list of valid tags.')
			end
			
			-- Check for multiple genders/numbers/animacies in a single spec.
			local typ = codes[code].type
			if typ ~= "other" and types[typ] then
				error('The gender specification "' .. spec.spec .. '" contains multiple tags of type "' .. typ .. '".')
			end
			types[typ] = true
				
			parts[key] = autoadd_abbr(codes[code].display)
		
			-- Generate categories if called for.
			if lang and pos_for_cat then
				local cat = codes[code].cat
				if cat then
					if not categories then
						categories = {}
					end
					insert(categories, full_langname .. " " .. cat)
				end
				if not seen_types then
					seen_types = {}
				elseif seen_types[typ] and seen_types[typ] ~= code then
					cat = (gender_and_number_data or get_gender_and_number_data()).multicode_cats[typ]
					if cat then
						if not categories then
							categories = {}
						end
						insert(categories, full_langname .. " " .. cat)
					end
				end
				seen_types[typ] = code
			end
			if lang and codes[code].req then
				local type_for_req = typ
				if code == "?" then
					-- Keep in mind `pos_for_cat` may be nil here.
					type_for_req = pos_for_cat == "verbs" and "aspect" or "gender"
				end
				if not categories then
					categories = {}
				end
				insert(categories, "Requests for " .. type_for_req .. " in " .. full_langname .. " entries")
			end
		end

		-- Add the processed codes together with non-breaking spaces
		if not parts[2] and parts[1] then
			return parts[1]
		else
			return concat(parts, "&nbsp;")
		end
	end

	for _, spec in ipairs(specs) do
		if type(spec) ~= "table" then
			spec = {spec = spec}
		end
		local spec_spec, is_nounclass = spec.spec
		-- If the specification starts with cX, then it is a noun class specification.
		if spec_spec:match("^%d") or spec_spec:match("^c[^-]") then
			is_nounclass = true
			local code = spec_spec:gsub("^c", "")
			
			local text
			if code == "?" then
				text = '<abbr class="noun-class" title="noun class missing">?</abbr>'
				if lang then
					if not categories then
						categories = {}
					end
					insert(categories, "Requests for noun class in " .. full_langname .. " entries")
				end
			else
				text = '<abbr class="noun-class" title="noun class ' .. code .. '">' .. code .. "</abbr>"
				if lang and pos_for_cat then
					if not categories then
						categories = {}
					end
					insert(categories, full_langname .. " class " .. code .. " POS")
				end
			end
			local text_with_qual = add_qualifiers_and_refs(text, spec, lang)
			insert(formatted_specs, text_with_qual)
		else
			-- Split the parts and iterate over each part, converting it into its display form
			local parts = split(spec.spec, "-", true, true)
			local combined_codes = (gender_and_number_data or get_gender_and_number_data()).combinations

			if lang then
				-- Check if the specification is valid
				--elseif langinfo.genders then
				--	local valid_genders = {}
				--	for _, g in ipairs(langinfo.genders) do valid_genders[g] = true end
				--	
				--	if not valid_genders[spec.spec] then
				--		local valid_string = {}
				--		for i, g in ipairs(langinfo.genders) do valid_string[i] = g end
				--		error('The gender specification "' .. spec.spec .. '" is not valid for ' .. langinfo.names[1] .. ". Valid are: " .. concat(valid_string, ", "))
				--	end
				--end
			end

			local has_combined = false
			for _, code in ipairs(parts) do
				if combined_codes[code] then
					has_combined = true
					break
				end
			end

			if not has_combined then
				if formatted_specs[1] then
					insert(formatted_specs, "or")
				end
				insert(formatted_specs, add_qualifiers_and_refs(do_gender_spec(spec, parts), spec, lang))
			else
				-- This logic is to handle combined gender specs like 'mf' and 'mfbysense'.
				local all_parts = {{}}
				local extra_displays
				local this_formatted_specs = {}

				for _, code in ipairs(parts) do
					if combined_codes[code] then
						local new_all_parts = {}
						for _, one_parts in ipairs(all_parts) do
							for _, one_code in ipairs(combined_codes[code].codes) do
								local new_combined_parts = deep_copy(one_parts)
								insert(new_combined_parts, one_code)
								insert(new_all_parts, new_combined_parts)
							end
						end
						all_parts = new_all_parts
						if lang and pos_for_cat then
							local extra_cat = combined_codes[code].cat
							if extra_cat then
								if not categories then
									categories = {}
								end
								insert(categories, full_langname .. " " .. extra_cat)
							end
						end
						local extra_display = combined_codes[code].display
						if extra_display then
							if not extra_displays then
								extra_displays = {}
							end
							insert(extra_displays, autoadd_abbr(extra_display))
						end
					else
						for _, one_parts in ipairs(all_parts) do
							insert(one_parts, code)
						end
					end
				end

				for _, parts in ipairs(all_parts) do
					if this_formatted_specs[1] then
						insert(this_formatted_specs, "or")
					end
					insert(this_formatted_specs, do_gender_spec(spec, parts))
				end

				if extra_displays then
					for _, display in ipairs(extra_displays) do
						insert(this_formatted_specs, display)
					end
				end

				insert(formatted_specs, add_qualifiers_and_refs(
					concat(this_formatted_specs, " "), spec, lang))
			end

			is_nounclass = false
		end

		-- Ensure that the specifications are either all noun classes, or none are.
		if all_is_nounclass == nil then
			all_is_nounclass = is_nounclass
		elseif all_is_nounclass ~= is_nounclass then
			error("Noun classes and genders cannot be mixed. Please use either one or the other.")
		end
	end

	if categories and lang and pos_for_cat then
		for i, cat in ipairs(categories) do
			categories[i] = cat:gsub("POS", pos_for_cat)
		end
	end

	if all_is_nounclass then
		-- Add the processed codes together with slashes
		return '<span class="gender">class ' .. concat(formatted_specs, "/") .. "</span>", categories
	else
		-- Add the processed codes together with spaces
		return '<span class="gender">' .. concat(formatted_specs, " ") .. "</span>", categories
	end
end

return export