935 lines
38 KiB
EmacsLisp
935 lines
38 KiB
EmacsLisp
// morphology-sw.el - Swahili morphology for the NLG engine.
|
|
//
|
|
// Implements Swahili noun class detection, subject/object agreement prefixes,
|
|
// tense markers, verb conjugation, negation, noun pluralization, and adjective
|
|
// agreement.
|
|
//
|
|
// Swahili is an agglutinative SVO language that uses a noun class system
|
|
// (15+ classes) instead of grammatical gender. Noun classes determine the
|
|
// agreement prefixes on verbs, adjectives, and other nominals.
|
|
//
|
|
// Key facts:
|
|
// - No case endings — word order and prepositions handle relations
|
|
// - Verb morphology: SUBJ-TENSE-OBJ-STEM(-final vowel)
|
|
// - Latin script; tonal but tone unmarked in standard orthography
|
|
//
|
|
// Conventions used throughout:
|
|
// class: "1"|"2"|"3"|"4"|"5"|"6"|"7"|"8"|"9"|"10"|"11"|"15"
|
|
// person: "1sg"|"2sg"|"3sg"|"1pl"|"2pl"|"3pl" | "1"|"2"|"3" + number "sg"/"pl"
|
|
// tense: "present"|"progressive"|"past"|"future"|"perfect"
|
|
// number: "sg" | "pl"
|
|
//
|
|
// Depends on: morphology.el (str_eq, str_len, str_slice, str_ends_with,
|
|
// str_drop_last)
|
|
|
|
// ── String helpers ────────────────────────────────────────────────────────────
|
|
|
|
fn sw_str_ends(s: String, suf: String) -> Bool {
|
|
return str_ends_with(s, suf)
|
|
}
|
|
|
|
fn sw_str_drop_last(s: String, n: Int) -> String {
|
|
let len: Int = str_len(s)
|
|
if n >= len {
|
|
return ""
|
|
}
|
|
return str_slice(s, 0, len - n)
|
|
}
|
|
|
|
fn sw_str_first_char(s: String) -> String {
|
|
let n: Int = str_len(s)
|
|
if n == 0 {
|
|
return ""
|
|
}
|
|
return str_slice(s, 0, 1)
|
|
}
|
|
|
|
fn sw_str_first2(s: String) -> String {
|
|
let n: Int = str_len(s)
|
|
if n < 2 {
|
|
return s
|
|
}
|
|
return str_slice(s, 0, 2)
|
|
}
|
|
|
|
fn sw_str_first3(s: String) -> String {
|
|
let n: Int = str_len(s)
|
|
if n < 3 {
|
|
return s
|
|
}
|
|
return str_slice(s, 0, 3)
|
|
}
|
|
|
|
fn sw_str_last_char(s: String) -> String {
|
|
let n: Int = str_len(s)
|
|
if n == 0 {
|
|
return ""
|
|
}
|
|
return str_slice(s, n - 1, n)
|
|
}
|
|
|
|
// ── Noun class detection ──────────────────────────────────────────────────────
|
|
//
|
|
// Returns the singular noun class number as a string.
|
|
// The noun class is inferred from the noun's prefix according to the standard
|
|
// Bantu noun class system used in Swahili.
|
|
//
|
|
// Class pairs (singular/plural):
|
|
// Class 1/2 (M-WA): m-/mw- → wa- people, animates
|
|
// Class 3/4 (M-MI): m-/mw- → mi- trees, plants, objects
|
|
// Class 5/6 (JI-MA): ji-/j- → ma- fruits, augmentatives
|
|
// Class 7/8 (KI-VI): ki-/ch- → vi-/vy- things, languages
|
|
// Class 9/10 (N): n-/m-/∅ → same animals, loanwords
|
|
// Class 11 (U): u-/w- → (10) abstract nouns
|
|
// Class 15 (KU): ku- → — infinitives as nouns
|
|
//
|
|
// Disambiguation between class 1 and class 3 (both m-/mw- prefix):
|
|
// Class 1 nouns refer to people/animates; class 3 to inanimate objects/plants.
|
|
// A lexical lookup for common class 1 nouns handles the ambiguous cases.
|
|
//
|
|
// Note: Class 9/10 nouns often have no visible prefix (zero-prefix or nasal
|
|
// prefix that assimilates to the following consonant).
|
|
|
|
fn sw_is_class1_noun(noun: String) -> Bool {
|
|
// Known class 1 (M-WA: human/animate) nouns
|
|
if str_eq(noun, "mtu") { return true } // person
|
|
if str_eq(noun, "mwanafunzi") { return true } // student
|
|
if str_eq(noun, "mwalimu") { return true } // teacher
|
|
if str_eq(noun, "mke") { return true } // wife/woman
|
|
if str_eq(noun, "mume") { return true } // husband/man
|
|
if str_eq(noun, "mtoto") { return true } // child
|
|
if str_eq(noun, "mgeni") { return true } // guest/stranger
|
|
if str_eq(noun, "mwana") { return true } // child/son
|
|
if str_eq(noun, "mkubwa") { return true } // elder/adult
|
|
if str_eq(noun, "mdogo") { return true } // young one
|
|
if str_eq(noun, "mgonjwa") { return true } // sick person/patient
|
|
if str_eq(noun, "mfanyakazi") { return true } // worker
|
|
if str_eq(noun, "mkulima") { return true } // farmer
|
|
if str_eq(noun, "mwimbaji") { return true } // singer
|
|
if str_eq(noun, "msomaji") { return true } // reader
|
|
if str_eq(noun, "mwandishi") { return true } // writer
|
|
if str_eq(noun, "mpiganaji") { return true } // fighter/warrior
|
|
if str_eq(noun, "msaidizi") { return true } // helper
|
|
if str_eq(noun, "mpishi") { return true } // cook
|
|
if str_eq(noun, "mwanasheria") { return true } // lawyer
|
|
if str_eq(noun, "daktari") { return true } // doctor (cl.9, but animate)
|
|
if str_eq(noun, "rafiki") { return true } // friend (cl.9)
|
|
if str_eq(noun, "ndugu") { return true } // sibling/relative (cl.9)
|
|
return false
|
|
}
|
|
|
|
fn sw_noun_class(noun: String) -> String {
|
|
// Class 15: ku- prefix — infinitives used as nouns
|
|
if sw_str_ends(noun, "ku") {
|
|
if str_eq(sw_str_first2(noun), "ku") { return "15" }
|
|
}
|
|
if sw_str_first2(noun) == "ku" { return "15" }
|
|
|
|
// Explicit class 15 check
|
|
let p2: String = sw_str_first2(noun)
|
|
if str_eq(p2, "ku") { return "15" }
|
|
|
|
// Class 7/8 (KI-VI): ki- or ch-
|
|
let p3: String = sw_str_first3(noun)
|
|
if str_eq(p3, "ki-") { return "7" }
|
|
if str_eq(p2, "ki") { return "7" }
|
|
if str_eq(p2, "ch") { return "7" } // e.g. chakula (food), choo (toilet)
|
|
|
|
// Class 11 (U): u- or w- prefix — abstract nouns
|
|
// Must check before class 1/3 m- prefix checks
|
|
let p1: String = sw_str_first_char(noun)
|
|
if str_eq(p1, "u") { return "11" }
|
|
if str_eq(p1, "w") { return "11" } // e.g. wimbo (song) class 11
|
|
|
|
// Class 5/6 (JI-MA): ji- prefix or zero-prefix fruits/augmentatives
|
|
if str_eq(p2, "ji") { return "5" }
|
|
// Common class 5 nouns with zero prefix (j- before vowel)
|
|
if str_eq(noun, "jicho") { return "5" } // eye
|
|
if str_eq(noun, "jino") { return "5" } // tooth
|
|
if str_eq(noun, "bega") { return "5" } // shoulder
|
|
if str_eq(noun, "tunda") { return "5" } // fruit
|
|
if str_eq(noun, "embe") { return "5" } // mango
|
|
if str_eq(noun, "gari") { return "5" } // car/vehicle
|
|
if str_eq(noun, "bei") { return "5" } // price
|
|
if str_eq(noun, "sauti") { return "5" } // voice/sound
|
|
if str_eq(noun, "thamani") { return "5" } // value
|
|
|
|
// Class 1/3 (M- prefix): disambiguate by lexical lookup
|
|
if str_eq(p1, "m") {
|
|
if sw_is_class1_noun(noun) { return "1" }
|
|
return "3" // class 3: trees, plants, inanimate m- nouns
|
|
}
|
|
if str_eq(p2, "mw") {
|
|
if sw_is_class1_noun(noun) { return "1" }
|
|
return "3"
|
|
}
|
|
|
|
// Class 9/10 (N): nasal prefix or zero prefix — catch-all for most
|
|
// loanwords and animal names
|
|
// Many class 9 nouns start with: n, ny, ng, mb, nd, nj, nz, or bare vowel
|
|
if str_eq(p2, "ny") { return "9" }
|
|
if str_eq(p2, "ng") { return "9" }
|
|
if str_eq(p2, "mb") { return "9" }
|
|
if str_eq(p2, "nd") { return "9" }
|
|
if str_eq(p2, "nj") { return "9" }
|
|
if str_eq(p2, "nz") { return "9" }
|
|
if str_eq(p1, "n") { return "9" }
|
|
|
|
// Common lexical class 9 nouns (animals, loanwords, etc.)
|
|
if str_eq(noun, "paka") { return "9" } // cat
|
|
if str_eq(noun, "mbwa") { return "9" } // dog
|
|
if str_eq(noun, "simba") { return "9" } // lion
|
|
if str_eq(noun, "tembo") { return "9" } // elephant
|
|
if str_eq(noun, "nyoka") { return "9" } // snake
|
|
if str_eq(noun, "samaki") { return "9" } // fish
|
|
if str_eq(noun, "rafiki") { return "9" } // friend
|
|
if str_eq(noun, "daktari") { return "9" } // doctor
|
|
if str_eq(noun, "serikali") { return "9" } // government
|
|
if str_eq(noun, "hospitali") { return "9" } // hospital
|
|
if str_eq(noun, "shule") { return "9" } // school
|
|
if str_eq(noun, "kanisa") { return "9" } // church
|
|
if str_eq(noun, "ofisi") { return "9" } // office
|
|
if str_eq(noun, "picha") { return "9" } // picture/photo
|
|
if str_eq(noun, "sehemu") { return "9" } // part/section
|
|
if str_eq(noun, "habari") { return "9" } // news
|
|
if str_eq(noun, "nchi") { return "9" } // country/land
|
|
if str_eq(noun, "bahari") { return "9" } // sea/ocean
|
|
if str_eq(noun, "dunia") { return "9" } // world
|
|
if str_eq(noun, "ardhi") { return "9" } // ground/earth
|
|
|
|
// Default to class 9 for unknown nouns (most common catch-all)
|
|
return "9"
|
|
}
|
|
|
|
// ── Subject agreement prefixes ────────────────────────────────────────────────
|
|
//
|
|
// Returns the subject prefix for verb conjugation.
|
|
//
|
|
// For personal pronouns: person "1","2","3" + number "sg","pl"
|
|
// For noun class agreement: person "3" + the noun's class
|
|
//
|
|
// Personal:
|
|
// 1sg (mimi) → ni-
|
|
// 2sg (wewe) → u-
|
|
// 3sg class 1 → a-
|
|
// 1pl (sisi) → tu-
|
|
// 2pl (nyinyi)→ m-
|
|
// 3pl class 2 → wa-
|
|
//
|
|
// Noun class 3sg:
|
|
// cl.1 → a- cl.2 → wa-
|
|
// cl.3 → u- cl.4 → i-
|
|
// cl.5 → li- cl.6 → ya-
|
|
// cl.7 → ki- cl.8 → vi-
|
|
// cl.9 → i- cl.10 → zi-
|
|
// cl.11 → u- cl.15 → ku-
|
|
|
|
fn sw_subj_prefix(person: String, number: String, noun_class: String) -> String {
|
|
// First and second person always use personal prefixes regardless of noun class
|
|
if str_eq(person, "1") {
|
|
if str_eq(number, "sg") { return "ni" }
|
|
return "tu"
|
|
}
|
|
if str_eq(person, "2") {
|
|
if str_eq(number, "sg") { return "u" }
|
|
return "m"
|
|
}
|
|
|
|
// Third person: agree with noun class
|
|
// Plural classes
|
|
if str_eq(number, "pl") {
|
|
if str_eq(noun_class, "1") { return "wa" } // class 2 (wa-)
|
|
if str_eq(noun_class, "2") { return "wa" }
|
|
if str_eq(noun_class, "3") { return "i" } // class 4 (i-)
|
|
if str_eq(noun_class, "4") { return "i" }
|
|
if str_eq(noun_class, "5") { return "ya" } // class 6 (ya-)
|
|
if str_eq(noun_class, "6") { return "ya" }
|
|
if str_eq(noun_class, "7") { return "vi" } // class 8 (vi-)
|
|
if str_eq(noun_class, "8") { return "vi" }
|
|
if str_eq(noun_class, "9") { return "zi" } // class 10 (zi-)
|
|
if str_eq(noun_class, "10") { return "zi" }
|
|
if str_eq(noun_class, "11") { return "zi" } // class 11 pl uses cl.10
|
|
return "zi"
|
|
}
|
|
|
|
// Singular classes
|
|
if str_eq(noun_class, "1") { return "a" }
|
|
if str_eq(noun_class, "3") { return "u" }
|
|
if str_eq(noun_class, "4") { return "i" }
|
|
if str_eq(noun_class, "5") { return "li" }
|
|
if str_eq(noun_class, "6") { return "ya" }
|
|
if str_eq(noun_class, "7") { return "ki" }
|
|
if str_eq(noun_class, "8") { return "vi" }
|
|
if str_eq(noun_class, "9") { return "i" }
|
|
if str_eq(noun_class, "10") { return "zi" }
|
|
if str_eq(noun_class, "11") { return "u" }
|
|
if str_eq(noun_class, "15") { return "ku" }
|
|
// Default: third person animate sg
|
|
return "a"
|
|
}
|
|
|
|
// ── Object agreement prefixes ─────────────────────────────────────────────────
|
|
//
|
|
// Object infixes are inserted between tense marker and verb stem.
|
|
// They mirror the subject prefixes with minor differences.
|
|
//
|
|
// 1sg → ni- 2sg → ku- 3sg cl.1 → m-/mw-
|
|
// 1pl → tu- 2pl → wa- 3pl cl.2 → wa-
|
|
// cl.3 sg → u- cl.4 pl → i-
|
|
// cl.5 sg → li- cl.6 pl → ya-
|
|
// cl.7 sg → ki- cl.8 pl → vi-
|
|
// cl.9 sg → i- cl.10 pl → zi-
|
|
// cl.11 → u- cl.15 → ku-
|
|
|
|
fn sw_obj_prefix(person: String, number: String, noun_class: String) -> String {
|
|
if str_eq(person, "1") {
|
|
if str_eq(number, "sg") { return "ni" }
|
|
return "tu"
|
|
}
|
|
if str_eq(person, "2") {
|
|
if str_eq(number, "sg") { return "ku" }
|
|
return "wa"
|
|
}
|
|
// Third person object
|
|
if str_eq(number, "pl") {
|
|
if str_eq(noun_class, "1") { return "wa" }
|
|
if str_eq(noun_class, "2") { return "wa" }
|
|
if str_eq(noun_class, "3") { return "i" }
|
|
if str_eq(noun_class, "4") { return "i" }
|
|
if str_eq(noun_class, "5") { return "ya" }
|
|
if str_eq(noun_class, "6") { return "ya" }
|
|
if str_eq(noun_class, "7") { return "vi" }
|
|
if str_eq(noun_class, "8") { return "vi" }
|
|
if str_eq(noun_class, "9") { return "zi" }
|
|
if str_eq(noun_class, "10") { return "zi" }
|
|
return "wa"
|
|
}
|
|
// Singular
|
|
if str_eq(noun_class, "1") { return "m" }
|
|
if str_eq(noun_class, "3") { return "u" }
|
|
if str_eq(noun_class, "5") { return "li" }
|
|
if str_eq(noun_class, "7") { return "ki" }
|
|
if str_eq(noun_class, "9") { return "i" }
|
|
if str_eq(noun_class, "11") { return "u" }
|
|
if str_eq(noun_class, "15") { return "ku" }
|
|
return "m"
|
|
}
|
|
|
|
// ── Tense markers ─────────────────────────────────────────────────────────────
|
|
//
|
|
// Tense markers are inserted between the subject prefix and the verb stem.
|
|
//
|
|
// "present" → -a- (habitual/simple present; final vowel -a)
|
|
// "progressive" → -na- (present progressive)
|
|
// "past" → -li- (simple past)
|
|
// "future" → -ta- (future)
|
|
// "perfect" → -me- (present perfect)
|
|
// "remote_past" → -li- (same marker; distinction via context or -ku-)
|
|
// "subjunctive" → (no tense marker; final vowel changes to -e)
|
|
|
|
fn sw_tense_marker(tense: String) -> String {
|
|
if str_eq(tense, "present") { return "a" }
|
|
if str_eq(tense, "progressive") { return "na" }
|
|
if str_eq(tense, "past") { return "li" }
|
|
if str_eq(tense, "future") { return "ta" }
|
|
if str_eq(tense, "perfect") { return "me" }
|
|
if str_eq(tense, "subjunctive") { return "" }
|
|
if str_eq(tense, "remote_past") { return "li" }
|
|
// Default to progressive marker
|
|
return "na"
|
|
}
|
|
|
|
// ── Final vowel (verb ending) ─────────────────────────────────────────────────
|
|
//
|
|
// The final vowel of the verb stem changes based on tense and negation.
|
|
//
|
|
// Positive forms:
|
|
// present habitual / progressive / past / future / perfect: -a
|
|
// subjunctive: -e
|
|
//
|
|
// Negative forms:
|
|
// present: -i (the negative present uses -i instead of -a)
|
|
// past: -a (negative past: ha- prefix + -ku- tense + stem + -a)
|
|
// others: -e (negative subjunctive)
|
|
//
|
|
// Note: For most tenses the final vowel is simply -a. Only the negative present
|
|
// and subjunctive change it.
|
|
|
|
fn sw_verb_final(tense: String, negative: Bool) -> String {
|
|
if negative {
|
|
if str_eq(tense, "present") { return "i" }
|
|
if str_eq(tense, "progressive") { return "i" }
|
|
if str_eq(tense, "subjunctive") { return "e" }
|
|
return "a"
|
|
}
|
|
if str_eq(tense, "subjunctive") { return "e" }
|
|
return "a"
|
|
}
|
|
|
|
// ── Negative subject prefix ───────────────────────────────────────────────────
|
|
//
|
|
// Negation in Swahili is expressed by replacing the subject prefix with a
|
|
// negative counterpart (ha- for most; si- for 1sg).
|
|
//
|
|
// Positive → Negative subject prefixes:
|
|
// ni- → si- (1sg)
|
|
// u- → hu- (2sg)
|
|
// a- → ha- (3sg cl.1)
|
|
// tu- → hatu- (1pl)
|
|
// m- → ham- (2pl)
|
|
// wa- → hawa- (3pl cl.2)
|
|
// other class 3sg prefixes: ha + prefix (e.g. ha+u = hau, ha+li = hali)
|
|
|
|
fn sw_neg_subj_prefix(person: String, number: String, noun_class: String) -> String {
|
|
if str_eq(person, "1") {
|
|
if str_eq(number, "sg") { return "si" }
|
|
return "hatu"
|
|
}
|
|
if str_eq(person, "2") {
|
|
if str_eq(number, "sg") { return "hu" }
|
|
return "ham"
|
|
}
|
|
// Third person negative = ha + positive subject prefix
|
|
let pos: String = sw_subj_prefix(person, number, noun_class)
|
|
return "ha" + pos
|
|
}
|
|
|
|
// ── Stem extraction ───────────────────────────────────────────────────────────
|
|
//
|
|
// Swahili infinitives begin with ku-. The verb stem is the infinitive minus ku-.
|
|
// For verbs where the stem itself starts with a vowel, the ku- is retained in
|
|
// some contexts (kuomba, kusoma etc.) but stripped for conjugation.
|
|
//
|
|
// Special cases:
|
|
// kula (to eat) → stem: l- (kula → la in conjugation: anakula)
|
|
// kuwa (to be) → stem: wa (but equatorial forms are suppletive: ni/u/ni)
|
|
// kwenda (to go) → stem: end (kw + end → enda)
|
|
// kuja (to come) → stem: ja
|
|
|
|
fn sw_verb_stem(infinitive: String) -> String {
|
|
if str_eq(infinitive, "kula") { return "l" } // eat — irregular: anakula
|
|
if str_eq(infinitive, "kuwa") { return "wa" } // be
|
|
if str_eq(infinitive, "kwenda") { return "enda" }
|
|
if str_eq(infinitive, "kuja") { return "ja" }
|
|
if str_eq(infinitive, "kusoma") { return "soma" }
|
|
if str_eq(infinitive, "kusema") { return "sema" }
|
|
if str_eq(infinitive, "kuona") { return "ona" }
|
|
if str_eq(infinitive, "kufanya") { return "fanya" }
|
|
if str_eq(infinitive, "kutaka") { return "taka" }
|
|
if str_eq(infinitive, "kujua") { return "jua" }
|
|
if str_eq(infinitive, "kupata") { return "pata" }
|
|
if str_eq(infinitive, "kuambia") { return "ambia" }
|
|
if str_eq(infinitive, "kuleta") { return "leta" }
|
|
if str_eq(infinitive, "kuweka") { return "weka" }
|
|
if str_eq(infinitive, "kuingia") { return "ingia" }
|
|
if str_eq(infinitive, "kutoka") { return "toka" }
|
|
if str_eq(infinitive, "kupiga") { return "piga" }
|
|
if str_eq(infinitive, "kuimba") { return "imba" }
|
|
if str_eq(infinitive, "kucheza") { return "cheza" }
|
|
if str_eq(infinitive, "kulala") { return "lala" }
|
|
if str_eq(infinitive, "kuandika") { return "andika" }
|
|
if str_eq(infinitive, "kununua") { return "nunua" }
|
|
if str_eq(infinitive, "kuuza") { return "uza" }
|
|
if str_eq(infinitive, "kupenda") { return "penda" }
|
|
if str_eq(infinitive, "kuchukua") { return "chukua" }
|
|
if str_eq(infinitive, "kulipa") { return "lipa" }
|
|
if str_eq(infinitive, "kusikia") { return "sikia" }
|
|
if str_eq(infinitive, "kuamka") { return "amka" }
|
|
if str_eq(infinitive, "kukaa") { return "kaa" }
|
|
if str_eq(infinitive, "kurudi") { return "rudi" }
|
|
if str_eq(infinitive, "kushinda") { return "shinda" }
|
|
if str_eq(infinitive, "kusaidia") { return "saidia" }
|
|
if str_eq(infinitive, "kuzungumza") { return "zungumza" }
|
|
if str_eq(infinitive, "kupumzika") { return "pumzika" }
|
|
if str_eq(infinitive, "kufika") { return "fika" }
|
|
if str_eq(infinitive, "kuomba") { return "omba" }
|
|
if str_eq(infinitive, "kushukuru") { return "shukuru" }
|
|
// Generic: strip leading ku (2 chars) or kw (for kwenda etc.)
|
|
if sw_str_first2(infinitive) == "ku" {
|
|
return str_slice(infinitive, 2, str_len(infinitive))
|
|
}
|
|
if sw_str_first2(infinitive) == "kw" {
|
|
return str_slice(infinitive, 2, str_len(infinitive))
|
|
}
|
|
return infinitive
|
|
}
|
|
|
|
// ── Positive verb conjugation ─────────────────────────────────────────────────
|
|
//
|
|
// Structure: SUBJ_PREFIX + TENSE_MARKER + STEM + FINAL_VOWEL
|
|
//
|
|
// Examples:
|
|
// anasoma = a (3sg cl.1) + na (prog) + soma (read) + [stem ends -a]
|
|
// nilikula = ni (1sg) + li (past) + ku (special) + la (kula stem)
|
|
// tutasema = tu (1pl) + ta (future) + sema (say) + [stem ends -a]
|
|
// amesoma = a (3sg) + me (perfect) + soma + [final -a]
|
|
//
|
|
// Special case: kula — the stem is bare "l", and the full conjugation inserts
|
|
// ku before the stem in most tenses: a-na-ku-la, a-li-ku-la, a-ta-ku-la
|
|
// but a-me-l-a is not standard — standard is amekula.
|
|
|
|
fn sw_conjugate(verb_stem: String, person: String, number: String, noun_class: String, tense: String) -> String {
|
|
let subj: String = sw_subj_prefix(person, number, noun_class)
|
|
let tm: String = sw_tense_marker(tense)
|
|
let fv: String = sw_verb_final(tense, false)
|
|
|
|
// kula (eat) — stem is "l" but conjugates as "kula" after prefix+tense
|
|
if str_eq(verb_stem, "l") {
|
|
if str_eq(tm, "") {
|
|
return subj + "kula"
|
|
}
|
|
return subj + tm + "kula"
|
|
}
|
|
|
|
// kuwa (be) — present equational forms are suppletive
|
|
if str_eq(verb_stem, "wa") {
|
|
if str_eq(tense, "present") {
|
|
// Equational: ni/u/ni for 1sg/2sg/3sg; tu/m/ni for pl
|
|
if str_eq(person, "1") {
|
|
if str_eq(number, "sg") { return "ni" }
|
|
return "tu ni"
|
|
}
|
|
if str_eq(person, "2") {
|
|
if str_eq(number, "sg") { return "u" }
|
|
return "m ni"
|
|
}
|
|
// 3sg/3pl: use class-based copula
|
|
if str_eq(number, "sg") {
|
|
// Locative/existential forms: niko, uko, yuko etc. (class-based)
|
|
if str_eq(noun_class, "1") { return "yuko" }
|
|
if str_eq(noun_class, "3") { return "upo" }
|
|
if str_eq(noun_class, "5") { return "lipo" }
|
|
if str_eq(noun_class, "7") { return "kipo" }
|
|
if str_eq(noun_class, "9") { return "ipo" }
|
|
if str_eq(noun_class, "11") { return "upo" }
|
|
return "yuko"
|
|
}
|
|
// plural
|
|
if str_eq(noun_class, "1") { return "wako" }
|
|
if str_eq(noun_class, "3") { return "ipo" }
|
|
if str_eq(noun_class, "5") { return "yapo" }
|
|
if str_eq(noun_class, "7") { return "vipo" }
|
|
if str_eq(noun_class, "9") { return "zipo" }
|
|
return "wako"
|
|
}
|
|
// Progressive kuwa: niko/uko/yuko (existence/location)
|
|
if str_eq(tense, "progressive") {
|
|
if str_eq(person, "1") {
|
|
if str_eq(number, "sg") { return "niko" }
|
|
return "tuko"
|
|
}
|
|
if str_eq(person, "2") {
|
|
if str_eq(number, "sg") { return "uko" }
|
|
return "mko"
|
|
}
|
|
if str_eq(number, "sg") {
|
|
if str_eq(noun_class, "1") { return "yuko" }
|
|
return subj + "ko"
|
|
}
|
|
if str_eq(noun_class, "1") { return "wako" }
|
|
return subj + "ko"
|
|
}
|
|
// Past/future/perfect kuwa: regular
|
|
}
|
|
|
|
// Regular conjugation
|
|
let stem_final: String = sw_str_last_char(verb_stem)
|
|
|
|
// If stem already ends in the correct final vowel, don't double it
|
|
if str_eq(fv, "a") {
|
|
if str_eq(stem_final, "a") {
|
|
// Stem ends in -a: result is subj+tm+stem (final -a already present)
|
|
if str_eq(tm, "") {
|
|
return subj + verb_stem
|
|
}
|
|
return subj + tm + verb_stem
|
|
}
|
|
}
|
|
|
|
if str_eq(tm, "") {
|
|
return subj + verb_stem + fv
|
|
}
|
|
return subj + tm + verb_stem + fv
|
|
}
|
|
|
|
// ── Negative verb conjugation ─────────────────────────────────────────────────
|
|
//
|
|
// Negative structure: NEG_SUBJ_PREFIX + (tense) + STEM + FINAL_VOWEL_I
|
|
//
|
|
// Negative present:
|
|
// ha- prefix replaces subject prefix; final vowel -i
|
|
// e.g. hasomi (he/she doesn't read), sisomi (I don't read)
|
|
//
|
|
// Negative past:
|
|
// ha- prefix + -ku- tense infix + stem + -a
|
|
// e.g. hakusoma (he/she didn't read), sikusoma (I didn't read)
|
|
//
|
|
// Negative future:
|
|
// ha- prefix + -ta- + stem + -i (or sometimes -a; -i is more standard)
|
|
// e.g. hatasoma (he/she won't read)
|
|
//
|
|
// Negative perfect:
|
|
// ha- prefix + -ja- (not yet) + stem + -a
|
|
// e.g. hajaenda (he/she hasn't gone yet)
|
|
|
|
fn sw_negative(verb_stem: String, person: String, number: String, noun_class: String, tense: String) -> String {
|
|
let neg_subj: String = sw_neg_subj_prefix(person, number, noun_class)
|
|
|
|
// kula special gram_case
|
|
if str_eq(verb_stem, "l") {
|
|
if str_eq(tense, "past") { return neg_subj + "kukula" }
|
|
if str_eq(tense, "perfect") { return neg_subj + "jakula" }
|
|
return neg_subj + "kuli"
|
|
}
|
|
|
|
if str_eq(tense, "present") {
|
|
let fv: String = sw_verb_final("present", true) // -i
|
|
let stem_no_a: String = verb_stem
|
|
// If stem ends in -a, drop it before adding -i (some grammars)
|
|
// Standard: ha + stem-minus-final-a + i
|
|
let slen: Int = str_len(verb_stem)
|
|
if slen > 0 {
|
|
let last: String = sw_str_last_char(verb_stem)
|
|
if str_eq(last, "a") {
|
|
return neg_subj + sw_str_drop_last(verb_stem, 1) + fv
|
|
}
|
|
}
|
|
return neg_subj + verb_stem + fv
|
|
}
|
|
|
|
if str_eq(tense, "past") {
|
|
// Negative past: ha + ku + stem + a
|
|
return neg_subj + "ku" + verb_stem + "a"
|
|
}
|
|
|
|
if str_eq(tense, "future") {
|
|
// Negative future: ha + ta + stem + i (standard)
|
|
let fv: String = sw_verb_final("present", true) // -i
|
|
return neg_subj + "ta" + verb_stem + fv
|
|
}
|
|
|
|
if str_eq(tense, "perfect") {
|
|
// Negative perfect "not yet": ha + ja + stem + a
|
|
return neg_subj + "ja" + verb_stem + "a"
|
|
}
|
|
|
|
if str_eq(tense, "progressive") {
|
|
// Negative progressive: same as negative present
|
|
let fv: String = sw_verb_final("present", true)
|
|
let slen: Int = str_len(verb_stem)
|
|
if slen > 0 {
|
|
let last: String = sw_str_last_char(verb_stem)
|
|
if str_eq(last, "a") {
|
|
return neg_subj + sw_str_drop_last(verb_stem, 1) + fv
|
|
}
|
|
}
|
|
return neg_subj + verb_stem + fv
|
|
}
|
|
|
|
// Default negative: ha + stem + i
|
|
return neg_subj + verb_stem + "i"
|
|
}
|
|
|
|
// ── Noun plural formation ─────────────────────────────────────────────────────
|
|
//
|
|
// Swahili plurals are formed by changing the noun class prefix.
|
|
//
|
|
// Class 1 (m-/mw-) → Class 2 (wa-): mtu → watu, mwalimu → walimu
|
|
// Class 3 (m-/mw-) → Class 4 (mi-): mti → miti, mwamba → miamba
|
|
// Class 5 (ji-/∅) → Class 6 (ma-): jicho → macho, tunda → matunda
|
|
// Class 7 (ki-/ch-)→ Class 8 (vi-/vy-): kitu → vitu, chakula → vyakula
|
|
// Class 9 (n-/∅) → Class 10 (n-/∅): same prefix — no change in prefix;
|
|
// some class 9 nouns add ma- (ma- class) in plural
|
|
// Class 11 (u-) → Class 10 (n-/∅): ubao → mbao
|
|
//
|
|
// Known lexical exceptions are handled first.
|
|
|
|
fn sw_noun_plural(noun: String) -> String {
|
|
// ── Lexical irregulars ──────────────────────────────────────────────────
|
|
if str_eq(noun, "mtu") { return "watu" }
|
|
if str_eq(noun, "mtoto") { return "watoto" }
|
|
if str_eq(noun, "mke") { return "wake" }
|
|
if str_eq(noun, "mume") { return "waume" }
|
|
if str_eq(noun, "mwana") { return "wana" }
|
|
if str_eq(noun, "mwalimu") { return "walimu" }
|
|
if str_eq(noun, "mgeni") { return "wageni" }
|
|
if str_eq(noun, "mwanafunzi") { return "wanafunzi" }
|
|
if str_eq(noun, "mfanyakazi") { return "wafanyakazi" }
|
|
if str_eq(noun, "mkulima") { return "wakulima" }
|
|
if str_eq(noun, "mgonjwa") { return "wagonjwa" }
|
|
if str_eq(noun, "jicho") { return "macho" }
|
|
if str_eq(noun, "jino") { return "meno" }
|
|
if str_eq(noun, "bega") { return "mabega" }
|
|
if str_eq(noun, "tunda") { return "matunda" }
|
|
if str_eq(noun, "gari") { return "magari" }
|
|
if str_eq(noun, "embe") { return "maembe" }
|
|
if str_eq(noun, "wimbo") { return "nyimbo" } // class 11 → 10
|
|
if str_eq(noun, "ubao") { return "mbao" }
|
|
if str_eq(noun, "ugonjwa") { return "magonjwa" }
|
|
if str_eq(noun, "uso") { return "nyuso" }
|
|
if str_eq(noun, "ukuta") { return "kuta" }
|
|
if str_eq(noun, "ulimi") { return "ndimi" }
|
|
if str_eq(noun, "upande") { return "pande" }
|
|
if str_eq(noun, "uwezo") { return "nguvu" } // approximate
|
|
// Class 9/10 nouns: same form sg/pl (zero-change class)
|
|
if str_eq(noun, "paka") { return "paka" }
|
|
if str_eq(noun, "samaki") { return "samaki" }
|
|
if str_eq(noun, "rafiki") { return "rafiki" }
|
|
if str_eq(noun, "daktari") { return "madaktari" } // some use ma- pl
|
|
if str_eq(noun, "habari") { return "habari" }
|
|
if str_eq(noun, "nchi") { return "nchi" }
|
|
if str_eq(noun, "bahari") { return "bahari" }
|
|
if str_eq(noun, "shule") { return "shule" }
|
|
if str_eq(noun, "hospitali") { return "hospitali" }
|
|
if str_eq(noun, "ofisi") { return "ofisi" }
|
|
if str_eq(noun, "serikali") { return "serikali" }
|
|
|
|
// ── Productive rules ────────────────────────────────────────────────────
|
|
|
|
// Class 1 (m- people): m- → wa-
|
|
if sw_is_class1_noun(noun) {
|
|
if str_eq(sw_str_first2(noun), "mw") {
|
|
return "wa" + str_slice(noun, 2, str_len(noun))
|
|
}
|
|
if str_eq(sw_str_first_char(noun), "m") {
|
|
return "wa" + str_slice(noun, 1, str_len(noun))
|
|
}
|
|
}
|
|
|
|
// Class 7 (ki-/ch-) → Class 8 (vi-/vy-)
|
|
let p2: String = sw_str_first2(noun)
|
|
if str_eq(p2, "ki") {
|
|
return "vi" + str_slice(noun, 2, str_len(noun))
|
|
}
|
|
if str_eq(p2, "ch") {
|
|
return "vy" + str_slice(noun, 2, str_len(noun))
|
|
}
|
|
|
|
// Class 5 (ji-) → Class 6 (ma-)
|
|
if str_eq(p2, "ji") {
|
|
return "ma" + str_slice(noun, 2, str_len(noun))
|
|
}
|
|
|
|
// Class 11 (u-/w-) → Class 10 (n-/∅): drop u, may add n
|
|
let p1: String = sw_str_first_char(noun)
|
|
if str_eq(p1, "u") {
|
|
// Most u- class nouns: drop u → bare stem (class 10 zero prefix)
|
|
return str_slice(noun, 1, str_len(noun))
|
|
}
|
|
|
|
// Class 3 (m-/mw- inanimate) → Class 4 (mi-)
|
|
if str_eq(p1, "m") {
|
|
if str_eq(p2, "mw") {
|
|
return "mi" + str_slice(noun, 2, str_len(noun))
|
|
}
|
|
return "mi" + str_slice(noun, 1, str_len(noun))
|
|
}
|
|
|
|
// Class 9/10: no prefix change (same form)
|
|
return noun
|
|
}
|
|
|
|
// ── Adjective agreement ───────────────────────────────────────────────────────
|
|
//
|
|
// Adjectives in Swahili take a class agreement prefix that matches the noun
|
|
// they modify. The prefix is placed before the adjective stem.
|
|
//
|
|
// Agreement prefixes (singular):
|
|
// cl.1 → m-/mw- cl.2 → wa-
|
|
// cl.3 → m-/mw- cl.4 → mi-
|
|
// cl.5 → (j-)l- cl.6 → ma-
|
|
// cl.7 → ki-/ch- cl.8 → vi-/vy-
|
|
// cl.9 → (n-) cl.10 → (n-)
|
|
// cl.11 → mw- cl.15 → ku-
|
|
//
|
|
// The adjective prefix for class 1/3 singular before a vowel-initial stem
|
|
// uses mw- instead of m-.
|
|
//
|
|
// number: "sg" | "pl"
|
|
// noun_class: singular class of the noun (the function applies the correct
|
|
// plural class internally when number == "pl")
|
|
|
|
fn sw_adj_prefix(noun_class: String, number: String) -> String {
|
|
// Plural classes
|
|
if str_eq(number, "pl") {
|
|
if str_eq(noun_class, "1") { return "wa" } // cl.2
|
|
if str_eq(noun_class, "2") { return "wa" }
|
|
if str_eq(noun_class, "3") { return "mi" } // cl.4
|
|
if str_eq(noun_class, "4") { return "mi" }
|
|
if str_eq(noun_class, "5") { return "ma" } // cl.6
|
|
if str_eq(noun_class, "6") { return "ma" }
|
|
if str_eq(noun_class, "7") { return "vi" } // cl.8
|
|
if str_eq(noun_class, "8") { return "vi" }
|
|
if str_eq(noun_class, "9") { return "n" } // cl.10 (nasal)
|
|
if str_eq(noun_class, "10") { return "n" }
|
|
if str_eq(noun_class, "11") { return "n" } // cl.10 in plural
|
|
return "wa"
|
|
}
|
|
|
|
// Singular classes
|
|
if str_eq(noun_class, "1") { return "m" }
|
|
if str_eq(noun_class, "3") { return "m" }
|
|
if str_eq(noun_class, "4") { return "mi" }
|
|
if str_eq(noun_class, "5") { return "j" } // l- before consonant, j- before vowel
|
|
if str_eq(noun_class, "6") { return "ma" }
|
|
if str_eq(noun_class, "7") { return "ki" }
|
|
if str_eq(noun_class, "8") { return "vi" }
|
|
if str_eq(noun_class, "9") { return "n" }
|
|
if str_eq(noun_class, "10") { return "n" }
|
|
if str_eq(noun_class, "11") { return "mw" }
|
|
if str_eq(noun_class, "15") { return "ku" }
|
|
return ""
|
|
}
|
|
|
|
// sw_agree_adj: apply the agreement prefix to an adjective stem.
|
|
//
|
|
// adj_stem: the adjective without any class prefix (e.g. "zuri", "kubwa", "dogo")
|
|
// noun_class: class of the noun being modified (singular class number as string)
|
|
// number: "sg" | "pl"
|
|
//
|
|
// Returns the fully prefixed adjective form.
|
|
//
|
|
// Note: many common adjectives in Swahili are Arabic/Bantu borrowings and do
|
|
// not take class prefixes (e.g. "nzuri" — beautiful, "kubwa" — big, "dogo" — small).
|
|
// Prefix agreement applies primarily to native Bantu adjective stems and
|
|
// demonstratives. This function applies the prefix mechanically; for adjectives
|
|
// that are invariant loanwords the caller should use the bare form.
|
|
|
|
fn sw_agree_adj(adj_stem: String, noun_class: String, number: String) -> String {
|
|
// Invariant adjectives (no class prefix required in standard usage)
|
|
if str_eq(adj_stem, "nzuri") { return "nzuri" } // good/beautiful
|
|
if str_eq(adj_stem, "baya") { return "baya" } // bad
|
|
if str_eq(adj_stem, "safi") { return "safi" } // clean
|
|
if str_eq(adj_stem, "chafu") { return "chafu" } // dirty
|
|
if str_eq(adj_stem, "ghali") { return "ghali" } // expensive
|
|
if str_eq(adj_stem, "rahisi") { return "rahisi" } // cheap/easy
|
|
if str_eq(adj_stem, "mzuri") { return sw_adj_prefix(noun_class, number) + "zuri" }
|
|
|
|
// Apply class prefix to Bantu adjective stems
|
|
let prefix: String = sw_adj_prefix(noun_class, number)
|
|
if str_eq(prefix, "") {
|
|
return adj_stem
|
|
}
|
|
|
|
// cl.1/3 prefix m- before consonant, mw- before vowel
|
|
if str_eq(prefix, "m") {
|
|
let first: String = sw_str_first_char(adj_stem)
|
|
if str_eq(first, "a") { return "mw" + adj_stem }
|
|
if str_eq(first, "e") { return "mw" + adj_stem }
|
|
if str_eq(first, "i") { return "mw" + adj_stem }
|
|
if str_eq(first, "o") { return "mw" + adj_stem }
|
|
if str_eq(first, "u") { return "mw" + adj_stem }
|
|
return "m" + adj_stem
|
|
}
|
|
|
|
// cl.5 prefix j- before vowel, l- before consonant
|
|
if str_eq(prefix, "j") {
|
|
let first: String = sw_str_first_char(adj_stem)
|
|
if str_eq(first, "a") { return "j" + adj_stem }
|
|
if str_eq(first, "e") { return "j" + adj_stem }
|
|
if str_eq(first, "i") { return "j" + adj_stem }
|
|
if str_eq(first, "o") { return "j" + adj_stem }
|
|
if str_eq(first, "u") { return "j" + adj_stem }
|
|
return "l" + adj_stem
|
|
}
|
|
|
|
return prefix + adj_stem
|
|
}
|
|
|
|
// ── Demonstrative agreement ───────────────────────────────────────────────────
|
|
//
|
|
// Swahili demonstratives agree with the noun class.
|
|
// "this" (proximal): huyu (cl.1/2), huu (cl.3/11), hili (cl.5), hiki (cl.7),
|
|
// hii (cl.9), huu (cl.11)
|
|
// "that" (distal): yule (cl.1/2), ule (cl.3/11), lile (cl.5), kile (cl.7),
|
|
// ile (cl.9)
|
|
//
|
|
// proximity: "near" | "far"
|
|
|
|
fn sw_demonstrative(noun_class: String, number: String, proximity: String) -> String {
|
|
if str_eq(proximity, "near") {
|
|
if str_eq(number, "pl") {
|
|
if str_eq(noun_class, "1") { return "hawa" }
|
|
if str_eq(noun_class, "3") { return "hii" }
|
|
if str_eq(noun_class, "5") { return "haya" }
|
|
if str_eq(noun_class, "7") { return "hivi" }
|
|
if str_eq(noun_class, "9") { return "hizi" }
|
|
return "hawa"
|
|
}
|
|
if str_eq(noun_class, "1") { return "huyu" }
|
|
if str_eq(noun_class, "3") { return "huu" }
|
|
if str_eq(noun_class, "5") { return "hili" }
|
|
if str_eq(noun_class, "7") { return "hiki" }
|
|
if str_eq(noun_class, "9") { return "hii" }
|
|
if str_eq(noun_class, "11") { return "huu" }
|
|
if str_eq(noun_class, "15") { return "huku" }
|
|
return "hii"
|
|
}
|
|
// distal
|
|
if str_eq(number, "pl") {
|
|
if str_eq(noun_class, "1") { return "wale" }
|
|
if str_eq(noun_class, "3") { return "ile" }
|
|
if str_eq(noun_class, "5") { return "yale" }
|
|
if str_eq(noun_class, "7") { return "vile" }
|
|
if str_eq(noun_class, "9") { return "zile" }
|
|
return "wale"
|
|
}
|
|
if str_eq(noun_class, "1") { return "yule" }
|
|
if str_eq(noun_class, "3") { return "ule" }
|
|
if str_eq(noun_class, "5") { return "lile" }
|
|
if str_eq(noun_class, "7") { return "kile" }
|
|
if str_eq(noun_class, "9") { return "ile" }
|
|
if str_eq(noun_class, "11") { return "ule" }
|
|
if str_eq(noun_class, "15") { return "kule" }
|
|
return "ile"
|
|
}
|
|
|
|
// ── Swahili copula (kuwa — to be) ─────────────────────────────────────────────
|
|
//
|
|
// The copula in Swahili is complex:
|
|
//
|
|
// Equational ("X is Y", noun predicate):
|
|
// ni (is/am/are) — invariant for present equational
|
|
// si — negative present equational
|
|
//
|
|
// Locative/existential ("X is [somewhere]", with -ko/-po/-mo):
|
|
// niko/uko/yuko/tuko/mko/wako — with -ko (definite location)
|
|
// nipo/upo/yupo/tupo/mpo/wapo — with -po (near location)
|
|
// nimo/umo/yumo/tumo/mmo/wamo — with -mo (inside)
|
|
//
|
|
// sw_copula_present: returns the present copula form.
|
|
// use_case: "equative" | "locative"
|
|
|
|
fn sw_copula_present(person: String, number: String, use_case: String) -> String {
|
|
if str_eq(use_case, "equative") {
|
|
if str_eq(person, "1") { return "ni" }
|
|
if str_eq(person, "2") { return "ni" }
|
|
return "ni"
|
|
}
|
|
// locative -ko (most common)
|
|
if str_eq(person, "1") {
|
|
if str_eq(number, "sg") { return "niko" }
|
|
return "tuko"
|
|
}
|
|
if str_eq(person, "2") {
|
|
if str_eq(number, "sg") { return "uko" }
|
|
return "mko"
|
|
}
|
|
if str_eq(number, "sg") { return "yuko" }
|
|
return "wako"
|
|
}
|
|
|
|
// sw_copula_neg_present: negative present copula.
|
|
// equative: si (invariant for all persons in negative equational)
|
|
fn sw_copula_neg_present(person: String, number: String) -> String {
|
|
if str_eq(person, "1") {
|
|
if str_eq(number, "sg") { return "si" }
|
|
return "si"
|
|
}
|
|
if str_eq(person, "2") {
|
|
if str_eq(number, "sg") { return "si" }
|
|
return "si"
|
|
}
|
|
return "si"
|
|
}
|