This module contains functions that use the declension information in the submodules data and staticdata to determine the declension type, generate the inflected forms, and add the forms of the definite article.
local module_path = 'Module:grc-decl/decl'
local m_classes = mw.loadData(module_path .. '/classes')
local m_paradigms = mw.loadData(module_path .. '/staticdata/paradigms')
local m_dialect_groups = mw.loadData(module_path .. '/staticdata/dialects')
local m_decl_data = require(module_path .. '/data')
local m_accent = require('Module:grc-accent')
local usub = mw.ustring.sub
local decompose = mw.ustring.toNFD
local compose = mw.ustring.toNFC
-- Equivalent to mw.ustring.len.
local function ulen(str)
local _, length = string.gsub(str, '*', '')
return length
end
-- Basic string functions can be used when there are no character sets or
-- quantifiers.
local ufind = mw.ustring.find
local ugsub = mw.ustring.gsub
local export = {
inflections = m_decl_data.inflections,
adjinflections = m_decl_data.adjinflections,
adjinflections_con = m_decl_data.adjinflections_con,
}
local function quote(text)
return "“" .. text .. "”"
end
local function get_accent_info(form)
-- Get position of accent (nth vowel from beginning of word).
local accent = {}
local is_suffix = form:sub(1, 1) == '-'
accent.position, accent.type = m_accent.detect_accent(form)
-- Position: position of accent from beginning of word (number) or nil.
-- Accent: accent name (string) or nil.
-- Form must have an accent, unless it is a suffix.
if not is_suffix and not next(accent) then
error('No accent detected on ' .. quote(form) ..
'. Please add an accent by copying this template and placing ' ..
quote('/') .. ' for acute or ' .. quote('~') ..
' for circumflex after the vowel that should be accented: {{subst:chars|grc|' .. form .. '}}.')
end
-- Accent term as proxy for distinguishing between oxytone and perispomenon.
accent.term = m_accent.get_accent_term(form)
return accent, is_suffix
end
local form_redirects = {
AS = 'NS', VS = 'NS',
DD = 'GD', AD = 'ND', VD = 'ND',
AP = 'NP', VP = 'NP',
}
local form_metatable = {
__index = function (self, form_code)
if type(form_code) ~= 'string' then return nil end
if form_redirects then
return self]
elseif form_redirects then
return self]
-- If this is a neuter form but not in the nominative case,
-- use the corresponding masculine form.
elseif form_code:match('N.') then
return self
end
end,
}
local function add_redirects(form_table)
return setmetatable(form_table, form_metatable)
end
local function add_forms(args)
if not args.irregular then
--add stem to forms
local function add_stem(forms)
return forms:gsub('%$', args.stem)
end
-- args.suffix indicates that this is a paradigm for an unaccented suffix,
-- such as ].
if args.indeclinable then
for k, v in pairs(args.ctable) do
if k:find('') then -- only format case-number forms
args.ctable = args
end
end
elseif args.suffix and not next(args.accent) then
for k, v in pairs(args.ctable) do
if k:find('') then -- only format case-number forms
args.ctable = add_stem(v)
end
end
else
-- If the term is not a suffix and no accent was detected, then
-- get_accent_info above must throw an error,
-- or else there will be an uncaught error here.
local add_circumflex = args.accent.type == 'circumflex'
local recessive = -3
-- Force recessive accent in the Lesbian dialect.
local accent_position = args.dial == 'les' and recessive
or args.accent.position
-- Circumflex on monosyllabic DS and AS in consonant-stem third-
-- declension nouns: for example, Τρῷ and Τρῶ, DS and AS of Τρώς.
local DS_AS = args.accent_alternating == true
-- Added by "kles" function: for example, Περίκλεις.
local VS = args.recessive_VS and recessive
local synaeresis = args.synaeresis
local add_accent = m_accent.add_accent
local function add_accent_to_forms(forms, code)
return ugsub(forms,
'+',
function(form)
return add_accent(form,
code == 'VS' and VS or accent_position,
{
synaeresis = synaeresis,
circumflex = (code == 'DS' or code == 'AS') and DS_AS or add_circumflex,
short_diphthong = true,
force_antepenult = args.force_antepenult,
})
end)
end
for k, v in pairs(args.ctable) do
if k:find('') then -- only format case-number forms
args.ctable = add_accent_to_forms(add_stem(v), k)
end
end
end
end
end
local gender_codes = { 'M', 'F', 'N' }
local case_codes = { 'N', 'G', 'D', 'A', 'V' }
local number_codes = { 'S', 'D', 'P' }
local function handle_noun_overrides(form_table, override_table)
for _, case in ipairs(case_codes) do
for _, number in ipairs(number_codes) do
local key = case .. number
if override_table then
require('Module:debug').track('grc-decl/form-override')
end
form_table = override_table or form_table
end
end
end
local function handle_adjective_overrides(form_table, override_table)
for _, gender in ipairs(gender_codes) do
for _, case in ipairs(case_codes) do
for _, number in ipairs(number_codes) do
local key = gender .. case .. number
if override_table then
require('Module:debug').track('grc-adecl/form-override')
end
form_table = override_table or form_table
end
end
end
end
--[=[
Gets stem-ending combinations from ]
and ]. Called a single time to get forms
of a noun, and two or three times by make_decl_adj for each of the genders
of an adjective.
]=]
function export.make_decl(args, decl, root, is_adjective)
if not export.inflections then
error('Inflection type ' .. quote(decl) .. ' not found in ].")
end
if args.adjective and args.irregular then
error('Irregular adjectives are not handled by make_decl.')
end
if not root then
error('No root for ' .. args .. '.')
end
args.stem = root
export.inflections(args)
args.gender = args.gender or args.ctable
args.declheader = args.declheader or args.ctable
add_forms(args)
if not is_adjective then
handle_noun_overrides(args.ctable, args)
end
add_redirects(args.ctable)
end
-- String (comparative ending), function (input: root; output: comparative),
-- false (the declension has no comparative form), or nil (use the if-statements
-- to determine a comparative form).
local decl_to_comp = {
= 'ᾰ́ντερος',
= 'έντερος',
= 'έστερος', -- hopefully this is general?
= 'ῠ́τερος',
= function(root)
local last2 = usub(root, -2)
if last2 == 'ον' then
return root .. 'έστερος'
elseif last2 == 'εσ' then
return root:gsub('εσ$', 'έστερος')
else
return false
end
end,
= false,
= 'έστερος',
= 'έστερος',
= 'εώτερος', -- I assume—though I can't find examples
= 'εώτερος',
}
local function retrieve_comp(root, decl_type)
local data = decl_to_comp
if not data then
return data
elseif type(data) == 'string' then
return root .. data
elseif type(data) == 'function' then
return data(root)
else
error('Data for ' .. decl_type .. ' is invalid.')
end
end
-- Constructs an adverb, comparative, and superlative.
function export.make_acs(args)
-- input:
-- strings
local root, decl_type = args.root, args.decl_type
-- tables
local accent, atable = args.accent, args.atable
-- output:
local comp = retrieve_comp(root, decl_type)
local super, adv
if comp == nil then
local alpha_nonultima = decl_type == '1&2-alp' and
accent.term ~= 'oxytone' and
accent.term ~= 'perispomenon'
local last3 = usub(root, -3)
if alpha_nonultima and last3 == 'τερ' then
comp = '(' .. atable .. ')'
adv = atable
-- ?
-- comp = nil
elseif alpha_nonultima and last3 == 'τᾰτ' then
super = '(' .. atable .. ')'
adv = atable
-- ?
-- super = nil
elseif decl_type:find('ντ') then
comp = nil -- participles
super = nil
elseif (m_accent.get_weight(root, 1) == "light" or decl_type:find('att$')) then
comp = root .. 'ώτερος'
else
comp = root .. 'ότερος'
end
end
atable.adv = adv
-- Actually neuter accusative singular. This is correct for -τερος and
-- for μείζων; also for all comparatives in -ων?
or args.comparative and atable.NNS
-- actually neuter accusative plural
or args.superlative and atable.NNP
or atable.MGP and atable.MGP:gsub('ν$', 'ς'):gsub('ν<', 'ς<')
atable.comp = comp
atable.super = super or comp and comp:gsub('ερος$', 'ᾰτος')
-- Remove comparative and superlative if adjective is a comparative or superlative.
-- Parameters that trigger this condition are |deg=comp, |deg=super, and the
-- deprecated |form=comp.
if args.comparative or args.superlative then
atable.comp, atable.super = nil, nil
end
for _, form in ipairs { 'adv', 'comp', 'super' } do
if args == "-" then
atable = nil
elseif args then
atable = args
end
end
end
--[[
noun_table contains case-number forms.
adjective_table will contain gender-case-number forms.
override_table contains gender-case-number forms that will override the
forms in noun_table.
]]
local function transfer_forms_to_adjective_table(adjective_table, noun_table, gender_code)
for case_and_number_code, form in pairs(noun_table) do
adjective_table = form
end
end
--[=[
Interprets the table for the adjective's inflection type
in ].
]=]
function export.make_decl_adj(args, ct)
if args.irregular then
return export.inflections(args)
end
--[[
Two possibilities, with the indices of the table of endings
and the stem augmentation that they use:
- masculine–feminine (1, a1), neuter (2, a2)
- masculine (1, a1), feminine (2, a2), neuter (3, a1)
]]
-- Masculine or masculine and feminine forms.
export.make_decl(args, ct, args.root .. (ct.a1 or ''), true)
transfer_forms_to_adjective_table(args.atable, args.ctable, 'M')
-- Feminine or neuter forms.
if ct then
export.make_decl(args, ct, args.fstem or (args.root .. (ct.a2 or '')), true)
transfer_forms_to_adjective_table(args.atable, args.ctable, 'F')
end
export.make_decl(args, ct, args.root .. (ct.a1 or ''), true)
transfer_forms_to_adjective_table(args.atable, args.ctable, 'N')
add_redirects(args.atable)
args.ctable = nil
export.make_acs(args)
handle_adjective_overrides(args.atable, args)
args.adeclheader = ct.adeclheader or 'Declension'
end
-- This function requires NFC forms for ],
-- but NFD forms for ].
function export.get_decl(args)
if args.indeclinable then
if not args then error("Specify the indeclinable form in the 2nd parameter.") end
args.decl_type, args.root = 'indecl', ''
return
elseif args.irregular then
if args.gender == "N" then
args.decl_type, args.root = 'irregN', ''
else
args.decl_type, args.root = 'irreg', ''
end
return
elseif not (args and args) then
error("Use the 1st and 2nd parameters for the nominative and genitive singular.")
end
local infl_info = m_classes.infl_info.noun
args = compose(args)
args = compose(args)
local arg1, arg2 = args, args
local nom_without_accent = compose(m_accent.strip_tone(arg1))
local gen_without_accent = compose(m_accent.strip_tone(arg2))
local decl_types_by_genitive_ending, decl_type, root
local nominative_matches = {}
for i = -infl_info.longest_nominative_ending, -1 do
local nominative_ending = usub(nom_without_accent, i)
local decl_types_by_genitive_ending = infl_info
-- If decl_types_by_genitive_ending is a string, then it is the key of
-- another table in infl_info, a nominative ending with a macron or
-- breve (ι → ῐ).
if type(decl_types_by_genitive_ending) == "string" then
decl_types_by_genitive_ending = infl_info
end
if decl_types_by_genitive_ending then
table.insert(nominative_matches, decl_types_by_genitive_ending)
root = usub(nom_without_accent, 1, -1 - ulen(nominative_ending))
for i = -6, -1 do
local genitive_ending = usub(gen_without_accent, i)
local name = decl_types_by_genitive_ending
if name then
decl_type = name
break
end
end
if decl_type then
break
end
end
end
args.accent, args.suffix = get_accent_info(arg1)
if decl_type and root then
if args.contracted == 'false' and not decl_type:find('open') then
decl_type = decl_type .. '-open'
end
args.decl_type, args.root = decl_type, decompose(root)
return
elseif gen_without_accent:find('ος$') then
local root = decompose(usub(gen_without_accent, 1, -3))
if args.gender == "N" or ufind(root, 'ατ$') and not (args.gender == "M" and args.gender == "F") then
args.decl_type, args.root = '3rd-N-cons', root
else
args.decl_type, args.root = '3rd-cons', root
end
return
end
if nominative_matches then
local m_table = require 'Module:table'
local fun, grc =
require 'Module:fun', require 'Module:languages'.getByCode 'grc'
local make_sort_key = fun.memoize(
function (term)
return (grc:makeSortKey(term))
end)
if nominative_matches then
local new_nominative_matches = {}
for _, matches in ipairs(nominative_matches) do
for k, v in pairs(matches) do
new_nominative_matches = v
end
end
nominative_matches = new_nominative_matches
else
nominative_matches = nominative_matches
end
local gens = require 'Module:fun'.map(
function (gen)
return quote("-" .. gen)
end,
m_table.keysToList(
nominative_matches,
function (gen1, gen2)
local sort_key1, sort_key2 =
make_sort_key(gen1), make_sort_key(gen2)
if sort_key1 == sort_key2 then
return gen1 < gen2
else
return sort_key1 < sort_key2
end
end))
local agreement
if #gens > 1 then
agreement = { "Declensions were", "s ", " do" }
else
agreement = { "A declension was", " ", " does" }
end
gens = table.concat(gens, ", ")
error(agreement .. " found that matched the ending of the nominative form " .. quote(arg1) ..
", but the genitive ending" .. agreement .. gens ..
agreement .. " not match the genitive form " .. quote(arg2) .. ".")
else
for nom, gens in pairs(m_classes.ambig_forms) do
if arg1:find(nom .. "$") then
for gen, _ in pairs(gens) do
if arg2:find(gen .. "$") then
error("No declension found for nominative " .. quote(arg1) .. " and genitive " .. quote(arg2) ..
". There are two declensions with nominative " .. quote("-" .. nom) ..
" and genitive " .. quote("-" .. gen) ..
". To indicate which one you mean, mark the vowel length of the endings with a macron or breve.")
end
end
end
end
error("Can’t find a declension type for nominative " .. quote(arg1) .. " and genitive " .. quote(arg2) .. ".")
end
end
-- This function requires NFC forms for ],
-- but NFD forms for ].
function export.get_decl_adj(args)
if args.irregular then
args.decl_type, args.root = 'irreg', ''
return
elseif not args then
error('Use the 1st and 2nd parameters for the masculine and the ' ..
'feminine or neuter nominative singular, or the first parameter ' ..
' alone for the 3rd declension stem.')
end
args = compose(args)
if args then
args = compose(args)
end
local arg1, arg2 = args, args
local mstrip = compose(m_accent.strip_tone(arg1))
local fstrip
if arg2 then
fstrip = compose(m_accent.strip_tone(arg2))
else
args.accent, args.suffix = get_accent_info(arg1)
args.decl_type, args.root = '3rd-cons', decompose(mstrip)
return
end
local infl_info = m_classes.infl_info.adj
-- See if last three or two characters of masc have an entry.
local masc, decl
for i = -infl_info.longest_masculine_ending, -2 do
local ending = usub(mstrip, i)
local data = infl_info
if data then
masc = ending
decl = data
break
end
end
-- Allows redirecting, so that macrons or breves can be omitted for instance.
if type(decl) == "string" then
decl = infl_info
end
if decl then
-- Look for a feminine ending that matches the end of the feminine form.
local fem, name
for feminine, decl_name in pairs(decl) do
if fstrip:find(feminine .. '$') then
fem = feminine
name = decl_name:gsub("%d$", "")
break
end
end
if fem then
args.accent, args.suffix = get_accent_info(arg1)
-- The only indication that λέγων, λέγουσᾰ (stem λεγοντ-) and
-- ποιῶν, ποιοῦσᾰ (stem ποιουντ-) have different stems is the
-- accentuation of the masculine form.
if name == '1&3-οντ' and args.accent.term == 'perispomenon' then
name = '1&3-οντ-con'
end
if not export.adjinflections then
error('Inflection recognition failed. Function for generated inflection code ' ..
quote(name) .. ' not found in ].")
end
args.decl_type, args.root = name, decompose(mstrip:gsub(masc .. "$", ""))
return
else
-- No declension type found.
local fems = {}
local is_neuter = false
for fem in pairs(decl) do
if fem == "ον" then
is_neuter = true
end
table.insert(fems, quote("-" .. fem))
end
fems = table.concat(fems, ", ")
local agreement = { "A declension was", " ", " does" }
if #fems > 1 then
agreement = { "Declensions were", "s ", " do" }
end
error(agreement .. " found that matched the ending of the masculine " .. quote(arg1) ..
", but the corresponding feminine" .. (is_neuter and " and neuter" or "") .. " ending" .. agreement .. fems ..
agreement .. " not match the feminine " .. quote(arg2) .. ".")
end
end
error("Can’t find a declension type for masculine " .. quote(arg1) .. " and feminine or neuter " .. quote(arg2) .. ".")
end
--[[
Returns a table containing the inflected forms of the article,
to be placed before each inflected noun form.
]]
function export.infl_art(args)
if args.dial == 'epi' or args.adjective or args.no_article then
return {}
end
local art = {}
local arttable
if args.gender then
arttable = m_paradigms.art_att]
else
error('Gender not specified.')
end
for code, suffix in pairs(arttable) do
if (args.gender == "M" and args.gender == "F") and
m_paradigms.art_att.M ~= m_paradigms.art_att.F then
art = m_paradigms.art_att.M .. ', ' .. m_paradigms.art_att.F
else
art = suffix
end
end
if args.gender == 'F' then
if m_dialect_groups then
art = 'ᾱ̔' -- 104.1-4
art = 'τᾶς'
art = 'τᾷ'
art = 'τᾱ̀ν'
end
if args.dial == 'the' or args.dial == 'les' then
art = 'τᾶ' -- 39
elseif args.dial == 'boi' or args.dial == 'ara' or args.dial == 'ele' then
art = 'ται' -- 104.3
end
if m_dialect_groups then
art = 'τᾶν' -- 104.6
end
if args.dial == 'ato' then
art = 'τῆσῐ(ν)' -- 104.7
elseif args.dial == 'ion' then
art = 'τῇσῐ(ν)' -- 104.7
end
if m_dialect_groups then
art = 'τᾰ̀ς' -- 104.8
elseif args.dial == 'kre' or args.dial == 'arg' then
art = 'τὰνς'
elseif args.dial == 'les' then
art = 'ταῖς'
elseif args.dial == 'ele' then
art = 'ταὶρ'
end
if args.dial == 'kre' or args.dial == 'les' or args.dial == 'kyp' then
art = 'ᾱ̓' -- 57
art = 'αἰ'
elseif args.dial == 'ele' then
art = 'ᾱ̓'
art = 'ταὶ'
elseif args.dial == 'boi' then
art = 'τὴ' -- 104.5
elseif m_dialect_groups then --boeotian is covered above
art = 'ταὶ'
end
elseif args.gender == 'M' or args.gender == 'N' then
if args.dial == 'the' then
art = 'τοῖ' -- 106.1
art = 'τοῦ' -- 23
art = 'τοὺ'
art = 'τοῦν'
end
if args.dial == 'les' then
art = 'τῶ' -- 106.2
elseif args.dial == 'boi' or args.dial == 'ara' or args.dial == 'ele' or args.dial == 'eub' then
art = 'τοι' -- 106.2
end
if args.dial == 'ato' or args.dial == 'ion' then
art = 'τοῖσῐ(ν)' -- 106.4
end
if args.gender == 'M' then
if m_dialect_groups then
art = 'τὸς' -- 106.5
elseif args.dial == 'kre' or args.dial == 'arg' then
art = 'τὸνς'
elseif args.dial == 'les' then
art = 'τοῖς'
elseif args.dial == 'ele' then
art = 'τοὶρ'
elseif m_dialect_groups or args.dial == 'boi' then
art = 'τὼς'
end
if args.dial == 'kre' or args.dial == 'les' or args.dial == 'kyp' then
art = 'ὀ' -- 57
art = 'οἰ'
elseif args.dial == 'ele' then
art = 'ὀ'
art = 'τοὶ'
elseif m_dialect_groups or args.dial == 'boi' then
art = 'τοὶ'
end
end
if args.dial == 'ele' then
art = 'τοίοις'
-- elseif args.dial == 'ara' then
-- art = 'τοιυν'
end
end
return art
end
local lang = require("Module:languages").getByCode("grc")
local function tag(text)
return require("Module:script utilities").tag_text("-" .. text, lang)
end
local function print_detection_table(detection_table, labels, noun)
local out = require('Module:array')()
local function sort(item1, item2)
-- Put 'longest_nominative_ending' and 'longest_masculine_ending' first.
if item1:find '^longest' or item2:find '^longest' then
return item1:find '^longest' ~= nil
end
local sort1, sort2 = (lang:makeSortKey(item1)), (lang:makeSortKey(item2))
local decomp_length1, decomp_length2 = ulen(decompose(item1)), ulen(decompose(item2))
if sort1 == sort2 then
-- Sort ᾱ or ᾰ before α.
if decomp_length1 > decomp_length2 then
return true
else
return false
end
else
return sort1 < sort2
end
end
for key1, value1 in require("Module:table").sortedPairs(detection_table, sort) do
if key1:find '^longest' then
out:insert('* ' .. key1:gsub('_', ' ') .. ': ' .. value1 .. ' characters')
else
table.insert(out, "\n* " .. labels .. " " .. tag(key1))
if type(value1) == "string" then
out:insert(" → " .. tag(value1))
elseif type(value1) == "table" then
for key2, value2 in require("Module:table").sortedPairs(value1, sort) do
-- mw.log(len(key1), len(key2))
out:insert("\n** " ..
(noun and labels or key2 == "ον" and "neuter" or "feminine") ..
" " .. tag(key2) .. ": <code>" .. value2 .. "</code>")
if noun then
out:insert(" (<code>" .. (m_classes.conversion or "?") .. "</code>)")
end
end
end
end
end
return out:concat()
end
function export.show_noun_categories(frame)
return print_detection_table(m_classes.infl_info.noun, { "nominative", "genitive" }, true)
end
function export.show_adj_categories(frame)
return print_detection_table(m_classes.infl_info.adj, { "masculine", "feminine or neuter" })
end
return export