local export = {}
--[=[
Authorship: Ben Wing <benwing2>
]=]
--[=[
TERMINOLOGY:
-- "slot" = A particular combination of case/number.
Example slot names for nouns are "gen_s" (genitive singular) and
"abl_2s_inform_mpos" (ablative 2nd-singular informal multiple-possession).
Each slot is filled with zero or more forms.
-- "form" = The declined Kyrgyz form representing the value of a given slot.
-- "lemma" = The dictionary form of a given Kyrgyz term. Generally the nominative
masculine singular, but may occasionally be another form if the nominative
masculine singular is missing.
]=]
local lang = require("Module:languages").getByCode("ky")
local m_table = require("Module:table")
local m_string_utilities = require("Module:string utilities")
local m_script_utilities = require("Module:script utilities")
local iut = require("Module:inflection utilities")
local m_para = require("Module:parameters")
local current_title = mw.title.getCurrentTitle()
local NAMESPACE = current_title.nsText
local PAGENAME = current_title.text
local u = require("Module:string/char")
local rsplit = m_string_utilities.split
local rfind = m_string_utilities.find
local rmatch = m_string_utilities.match
local rgmatch = m_string_utilities.gmatch
local rsubn = m_string_utilities.gsub
local ulen = m_string_utilities.len
local usub = m_string_utilities.sub
local uupper = m_string_utilities.upper
local ulower = m_string_utilities.lower
local insert = table.insert
local dump = mw.dumpObject
-- version of rsubn() that discards all but the first return value
local function rsub(term, foo, bar)
local retval = rsubn(term, foo, bar)
return retval
end
-- version of rsubn() that returns a 2nd argument boolean indicating whether
-- a substitution was made.
local function rsubb(term, foo, bar)
local retval, nsubs = rsubn(term, foo, bar)
return retval, nsubs > 0
end
local lower_vowel = "аоөүуыэяёюие"
local upper_vowel = uupper(lower_vowel)
local vowel = lower_vowel .. upper_vowel
local V = ""
local C = ""
local lower_jr = "йр"
local upper_jr = uupper(lower_jr)
local jr = lower_jr .. upper_jr
local lower_voiced_cons_not_jr = "дмлнңбвгжз"
local upper_voiced_cons_not_jr = uupper(lower_voiced_cons_not_jr)
local voiced_cons_not_jr = lower_voiced_cons_not_jr .. upper_voiced_cons_not_jr
local lower_unvoiced_cons = "кпстфхчцшщ"
local upper_unvoiced_cons = uupper(lower_unvoiced_cons)
local unvoiced_cons = lower_unvoiced_cons .. upper_unvoiced_cons
local letter_classes = {
-- Subclasses of vowels. These are named after the predominant vowel of the associated suffix.
a_type_vowel = "аыяюу", -- low or high back vowels
e_type_vowel = "эие", -- front unrounded vowels
o_type_vowel = "оё", -- mid back vowels
oe_type_vowel = "өү", -- front rounded vowels
y_type_vowel = "аыя", -- back unrounded vowels
u_type_vowel = "оёюу", -- back rounded vowels
}
local function construct_vowel_harmony_table(spec)
local tab = {}
for _, triggers_result in ipairs(spec) do
local triggers, result = unpack(triggers_result)
for trigger in rgmatch(triggers, ".") do
tab = result
end
end
return tab
end
local high_harmony_table = construct_vowel_harmony_table {
{ "эие", "и" }, -- front unrounded
{ "аыя", "ы" }, -- back unrounded
{ "өү", "ү" }, -- front rounded
{ "оёюу", "у" }, -- back rounded
}
local low_harmony_table = construct_vowel_harmony_table {
{ "эие", "е" }, -- front unrounded
{ "аыяюу", "а" }, -- back unrounded
{ "өү", "ө" }, -- front rounded
{ "оё", "о" }, -- back rounded
}
local cases = { "nom", "gen", "dat", "acc", "loc", "abl" }
local non_possessed_numbers = { "s", "p" }
local possessed_numbers = { "spos", "mpos" }
local person_number_possession = { "1s", "2s_inform", "2s_formal", "3", "1p", "2p_inform", "2p_formal" }
local person_number_possession_suffixes = {
= "им",
= "иң",
= "иңиз",
= "Си",
= "ибиз",
= "иңар",
= "иңиздар",
}
local case_suffixes = {
= "",
= "Нин",
= "га", -- -а after 1s poss and 2s_inform poss, -на after 3 poss
= "Ни",
= "да", -- -нда after 3 poss
= "дан", -- нан after 3 poss
}
local noun_slots = {}
local function make_noun_slots()
for _, case in ipairs(cases) do
for _, possessed in ipairs { false, true} do
if possessed then
for _, person_number in ipairs(person_number_possession) do
for _, number in ipairs(possessed_numbers) do
local slot = ("%s_%s_%s"):format(case, person_number, number)
accel = slot:gsub("_", "|")
noun_slots = accel
end
end
else
for _, number in ipairs(non_possessed_numbers) do
local slot = ("%s_%s"):format(case, number)
accel = slot:gsub("_", "|")
noun_slots = accel
end
end
end
end
end
make_noun_slots()
local function skip_slot(decl_spec, slot)
return decl_spec.number == "sg" and (slot:find("_p$") or slot:find("_mpos$")) or
decl_spec.number == "pl" and (slot:find("_s$") or slot:find("_spos$"))
end
local function combine_stem_ending(stem, ending)
if ending == "" then
return stem
end
local split_suffix = rsplit(ending, "(" .. V .. ")")
local suffix_syls = {}
for i, sufpart in ipairs(split_suffix) do
if i == #split_suffix and suffix_syls or i % 2 == 0 then
suffix_syls = suffix_syls .. sufpart
else
insert(suffix_syls, sufpart)
end
end
for _, endsyl in ipairs(suffix_syls) do
local stem_init, stem_last_vowel, stem_final_cons_cluster = rmatch(stem, "^(.*)(" .. V .. ")(.-)$")
if not stem_last_vowel then
error(("Lemma or stem '%s' has no vowel, not sure how to implement vowel harmony"):format(stem))
end
local first_endsyl_letter = usub(endsyl, 1)
local endsyl_init, endsyl_vowel, endsyl_final = rmatch(endsyl, "^(.-)(" .. V .. ")(.*)$")
if not endsyl_init then
error(("Internal error: Ending '%s' has no vowel"):format(endsyl))
end
if endsyl_vowel ~= "и" and endsyl_vowel ~= "а" then
error(("Internal error: All endsyl vowels should be high и or low а, but saw %s"):format(endsyl))
end
local harmony_table
if endsyl_vowel == "и" then
harmony_table = high_harmony_table
else
harmony_table = low_harmony_table
end
if endsyl_init == "" and stem_final_cons_cluster == "" then
endsyl_vowel = ""
else
endsyl_vowel = harmony_table
end
if endsyl_init == "Л" then
if stem_final_cons_cluster == "" or rfind(stem_final_cons_cluster, "$") then
endsyl_init = "л"
else
endsyl_init = "д"
end
elseif endsyl_init == "Н" then
if stem_final_cons_cluster == "" then
endsyl_init = "н"
else
endsyl_init = "д"
end
elseif endsyl_init == "С" then
if stem_final_cons_cluster == "" then
endsyl_init = "с"
else
endsyl_init = ""
end
end
if rfind(stem_final_cons_cluster, "$") then
if endsyl_init == "д" then
endsyl_init = "т"
elseif endsyl_init == "г" then
endsyl_init = "к"
end
end
if endsyl_init == "" then
stem_final_cons_cluster = rsub(stem_final_cons_cluster, "$", {
= "б",
= "Б",
= "г",
= "Г",
})
end
stem = stem_init .. stem_last_vowel .. stem_final_cons_cluster .. endsyl_init .. endsyl_vowel .. endsyl_final
end
return stem
end
local function decline_noun(decl_spec, lemma)
local function make_form(stem, number, possession, case)
local number_ending, possession_ending, case_ending
if number == "s" then
number_ending = ""
else
number_ending = "Лар"
end
local number_stem = number == "p" and decl_spec.pl or combine_stem_ending(stem, number_ending)
possession_ending = possession and person_number_possession_suffixes or ""
case_ending = case_suffixes
if case == "dat" then
if possession == "1s" or possession == "2s_inform" then
case_ending = "а"
elseif possession == "3" then
case_ending = "на"
end
elseif case == "loc" and possession == "3" then
case_ending = "нда"
elseif case == "abl" and possession == "3" then
case_ending = "нан"
end
return combine_stem_ending(combine_stem_ending(number_stem, possession_ending), case_ending)
end
local function insert_form(slot, form)
if skip_slot(decl_spec, slot) then
return
end
iut.insert_form(decl_spec.forms, slot, {form = form})
end
for _, case in ipairs(cases) do
for _, possessed in ipairs { false, true} do
if possessed then
for _, person_number in ipairs(person_number_possession) do
for _, number in ipairs(possessed_numbers) do
local slot = ("%s_%s_%s"):format(case, person_number, number)
local form = make_form(lemma, number == "mpos" and "p" or "s", person_number, case)
insert_form(slot, form)
end
end
else
for _, number in ipairs(non_possessed_numbers) do
local slot = ("%s_%s"):format(case, number)
local form = make_form(lemma, number, "", case)
insert_form(slot, form)
end
end
end
end
-- 1. There are two types of stem vowel harmony, based on the last stem vowel: a-e-o-oe (which always occurs when
-- the first suffix vowel is low а/е/о/ө) vs. y-e-u-oe (which always occurs when the first suffix vowel is high
-- ы/и/у/ү).
-- 2. Endings can vary depending on the last sound of the stem in three possible ways:
-- (a) vowel_or_jr vs. voiced_cons_not_jr vs. unvoiced_cons;
-- (b) vowel vs. voiced_cons vs. unvoiced_cons;
-- (c) consonant vs. vowel.
-- 3. The following consonant suffix variations are found:
-- (a) Ending variation 2(a) is only associated with the plural suffix -лар, where л after vowel_or_jr becomes д
-- after voiced_cons_not_jr and т after unvoiced_cons.
-- (b) Ending variation 2(b) is associated with endings in н- after a vowel, changing to д after a voiced_cons
-- and т after an unvoiced_cons; or д or г after either vowel or voiced_cons, changing to the corresponding
-- unvoiced consonant т or к after an unvoiced_cons.
-- (c) Ending variation 2(c) is only associated with endings beginning with a high-harmonizing suffix vowel
-- ы/и/у/ү, which disappears after a vowel-final stem.
-- 4. The following types of vowel suffix variations are found:
-- (a) low-harmonizing а/е/о/ө;
-- (b) high-harmonizing ы/и/у/ү;
-- (c) partial low-harmonizing а/е/а/ө (after у/ю).
-- Based on this, we use capital letters in suffixes to indicate varying consonant sounds. We use а for the
-- low-harmonizing vowel and и for the high-harmonizing vowel. Specifically:
-- * Л: л/д variation.
-- * Н: н/д variation.
-- * С: с/- variation.
-- We automatically handle devoicing of consonants after unvoiced consonants.
--
-- We can analyze the suffixes further:
-- 1. nom_s has no ending.
-- 2. gen_s uses -нин after a vowel, -дин after a consonant with voicing assimilation.
-- 3. dat_s uses -га with voicing assimilation. This drops to -а after 1s possessive -им, 2s informal possessive
-- -иң) and changes to -на after 3 possessive -(с)и.
-- 4. acc_s uses -ни after a vowel, -ди after a consonant with voicing assimilation.
-- 5. loc_s uses -да with voicing assimilation. This changes to -нда after 3 possessive -(с)и.
-- 6. abl_s uses -дан with voicing assimilation. This changes to -нан after 3 possessive -(с)и.
-- 7. plural uses -лар after a vowel or й/р, -дар after a consonant with voicing assimilation.
-- 8. 1s possessive uses -им after a consonant, dropping to -м after a vowel.
-- 9. 2s informal possessive uses -иң after a consonant, dropping to -ң after a vowel.
-- 10. 2s formal possessive uses -иңиз after a consonant, dropping to -ңиз after a vowel.
-- 11. 3s/3p possessive uses -и after a consonant, -си after a vowel.
-- 12. 1p uses -ибиз after a consonant, dropping to -биз after a vowel.
-- 13. 2p informal possessive uses -иңар after a consonant, dropping to -ңар after a vowel.
-- 14. 2p formal possessive uses -иңиздар after a consonant, dropping to -ңиздар after a vowel.
end
-- Compute the categories to add the noun to, as well as the annotation to display in the
-- declension title bar. We combine the code to do these functions as both categories and
-- title bar contain similar information.
local function compute_categories_and_annotation(decl_spec)
local cats = {}
local function insert(cattype)
m_table.insertIfNot(cats, "Kyrgyz " .. cattype)
end
if decl_spec.number == "sg" then
insert("uncountable nouns")
elseif decl_spec.number == "pl" then
insert("pluralia tantum")
end
decl_spec.annotation =
decl_spec.number == "sg" and "sg-only" or
decl_spec.number == "pl" and "pl-only" or
""
decl_spec.categories = cats
end
local function show_forms(decl_spec)
local lemmas = {}
if decl_spec.forms.nom_s then
for _, nom_s in ipairs(decl_spec.forms.nom_s) do
table.insert(lemmas, nom_s.form)
end
elseif decl_spec.forms.nom_p then
for _, nom_p in ipairs(decl_spec.forms.nom_p) do
table.insert(lemmas, nom_p.form)
end
end
local props = {
lemmas = lemmas,
slot_table = noun_slots,
lang = lang,
include_translit = true,
}
iut.show_forms(decl_spec.forms, props)
end
local function make_table(decl_spec)
local forms = decl_spec.forms
local header = mw.getCurrentFrame():expandTemplate{
title = 'inflection-table-top',
args = {
title = '{title}{annotation}',
palette = 'blue',
tall = 'yes',
class = 'tr-alongside', -- hack to suppress excess space below each term
}
}
local table_spec_sg = [=[
! class="outer" colspan="9"| singular<br />{jekelik}
|-
!
! —
! first-person<br />singular<br />{menin}
! second-person<br />singular informal<br />{senin}
! second-person<br />singular formal<br />{sizdin}
! third-person<br />singular/plural<br />{anyn_alardyn}
! first-person<br />plural<br />{bizdin}
! second-person<br />plural informal<br />{silerdin}
! second-person<br />plural formal<br />{sizderdin}
|-
! nominative {atooch}
| {nom_s}
| {nom_1s_spos}
| {nom_2s_inform_spos}
| {nom_2s_formal_spos}
| {nom_3_spos}
| {nom_1p_spos}
| {nom_2p_inform_spos}
| {nom_2p_formal_spos}
|-
! genitive {ilik}
| {gen_s}
| {gen_1s_spos}
| {gen_2s_inform_spos}
| {gen_2s_formal_spos}
| {gen_3_spos}
| {gen_1p_spos}
| {gen_2p_inform_spos}
| {gen_2p_formal_spos}
|-
! dative {barysh}
| {dat_s}
| {dat_1s_spos}
| {dat_2s_inform_spos}
| {dat_2s_formal_spos}
| {dat_3_spos}
| {dat_1p_spos}
| {dat_2p_inform_spos}
| {dat_2p_formal_spos}
|-
! accusative {tabysh}
| {acc_s}
| {acc_1s_spos}
| {acc_2s_inform_spos}
| {acc_2s_formal_spos}
| {acc_3_spos}
| {acc_1p_spos}
| {acc_2p_inform_spos}
| {acc_2p_formal_spos}
|-
! locative {jatysh}
| {loc_s}
| {loc_1s_spos}
| {loc_2s_inform_spos}
| {loc_2s_formal_spos}
| {loc_3_spos}
| {loc_1p_spos}
| {loc_2p_inform_spos}
| {loc_2p_formal_spos}
|-
! ablative {chygysh}
| {abl_s}
| {abl_1s_spos}
| {abl_2s_inform_spos}
| {abl_2s_formal_spos}
| {abl_3_spos}
| {abl_1p_spos}
| {abl_2p_inform_spos}
| {abl_2p_formal_spos}
]=]
local table_spec_pl = [=[
! class="outer" colspan="9" | plural<br />{koeptoegoen}
|-
!
! —
! first-person<br />singular<br />{menin}
! second-person<br />singular informal<br />{senin}
! second-person<br />singular formal<br />{sizdin}
! third-person<br />singular/plural<br />{anyn_alardyn}
! first-person<br />plural<br />{bizdin}
! second-person<br />plural informal<br />{silerdin}
! second-person<br />plural formal<br />{sizderdin}
|-
! nominative {atooch}
| {nom_p}
| {nom_1s_mpos}
| {nom_2s_inform_mpos}
| {nom_2s_formal_mpos}
| {nom_3_mpos}
| {nom_1p_mpos}
| {nom_2p_inform_mpos}
| {nom_2p_formal_mpos}
|-
! genitive {ilik}
| {gen_p}
| {gen_1s_mpos}
| {gen_2s_inform_mpos}
| {gen_2s_formal_mpos}
| {gen_3_mpos}
| {gen_1p_mpos}
| {gen_2p_inform_mpos}
| {gen_2p_formal_mpos}
|-
! dative {barysh}
| {dat_p}
| {dat_1s_mpos}
| {dat_2s_inform_mpos}
| {dat_2s_formal_mpos}
| {dat_3_mpos}
| {dat_1p_mpos}
| {dat_2p_inform_mpos}
| {dat_2p_formal_mpos}
|-
! accusative {tabysh}
| {acc_p}
| {acc_1s_mpos}
| {acc_2s_inform_mpos}
| {acc_2s_formal_mpos}
| {acc_3_mpos}
| {acc_1p_mpos}
| {acc_2p_inform_mpos}
| {acc_2p_formal_mpos}
|-
! locative {jatysh}
| {loc_p}
| {loc_1s_mpos}
| {loc_2s_inform_mpos}
| {loc_2s_formal_mpos}
| {loc_3_mpos}
| {loc_1p_mpos}
| {loc_2p_inform_mpos}
| {loc_2p_formal_mpos}
|-
! ablative {chygysh}
| {abl_p}
| {abl_1s_mpos}
| {abl_2s_inform_mpos}
| {abl_2s_formal_mpos}
| {abl_3_mpos}
| {abl_1p_mpos}
| {abl_2p_inform_mpos}
| {abl_2p_formal_mpos}
]=]
local footer = mw.getCurrentFrame():expandTemplate{ title = 'inflection-table-bottom' }
if decl_spec.title then
forms.title = decl_spec.title
else
forms.title = 'Declension of <i lang="ky" class="Cyrl">' .. forms.lemma .. '</i>'
end
local function make_text_smaller(text)
return "(<span style=\"font-size: smaller;\">" .. text .. "</span>)"
end
local annotation = decl_spec.annotation
if annotation == "" then
forms.annotation = ""
else
forms.annotation = " " .. make_text_smaller(annotation)
end
local function tag_text(text)
return make_text_smaller(m_script_utilities.tag_text(text, lang))
end
-- grammatical terms used in the table
forms.jekelik = tag_text("жекелик")
forms.koeptoegoen = tag_text("көптөгөн")
forms.atooch = tag_text("атооч")
forms.ilik = tag_text("илик")
forms.barysh = tag_text("барыш")
forms.tabysh = tag_text("табыш")
forms.jatysh = tag_text("жатыш")
forms.chygysh = tag_text("чыгыш")
forms.menin = tag_text("менин")
forms.senin = tag_text("сенин")
forms.sizdin = tag_text("сиздин")
forms.anyn_alardyn = tag_text("анын/алардын")
forms.bizdin = tag_text("биздин")
forms.silerdin = tag_text("силердин")
forms.sizderdin = tag_text("сиздердин")
local table_spec =
decl_spec.number == "sg" and table_spec_sg or
decl_spec.number == "pl" and table_spec_pl or
table_spec_sg .. "|-\n" .. table_spec_pl
return m_string_utilities.format(header .. table_spec .. footer, forms)
end
-- Externally callable function to parse and decline a noun where all forms
-- are given manually. Return value is WORD_SPEC, an object where the declined
-- forms are in `WORD_SPEC.forms` for each slot. If there are no values for a
-- slot, the slot key will be missing. The value for a given slot is a list of
-- objects {form=FORM, footnotes=FOOTNOTES}.
function export.do_generate_forms(parent_args, number)
if number ~= "sg" and number ~= "pl" and number ~= "both" then
error("Internal error: number (arg 1) must be 'sg', 'pl' or 'both': '" .. number .. "'")
end
local params = {
= {},
pl = {}, -- override the plural, for бала pl. балдар instead of expected балалар
title = {},
}
local args = m_para.process(parent_args, params)
local decl_spec = {
pl = args.pl,
title = args.title,
forms = {},
number = number,
}
local lemma = args or PAGENAME
if number == "pl" then
local sg_lemma = rmatch(lemma, "(.*)р$")
if not sg_lemma then
error("Plural lemma doesn't end with nominative plural ending (-лар, -дер, -тор, etc.): " .. lemma)
end
lemma = sg_lemma
end
decline_noun(decl_spec, lemma)
compute_categories_and_annotation(decl_spec)
return decl_spec
end
-- Entry point for {{ky-decl-noun}}, {{ky-decl-noun-sg}} and {{ky-decl-noun-pl}}.
function export.show(frame)
local iparams = {
= {required = true},
}
local iargs = m_para.process(frame.args, iparams)
local parent_args = frame:getParent().args
local decl_spec = export.do_generate_forms(parent_args, iargs)
show_forms(decl_spec)
return make_table(decl_spec) .. require("Module:utilities").format_categories(decl_spec.categories, lang)
end
return export