diff --git a/pythainlp/khavee/core.py b/pythainlp/khavee/core.py index 71a0dde2d..69b8a344d 100644 --- a/pythainlp/khavee/core.py +++ b/pythainlp/khavee/core.py @@ -12,11 +12,109 @@ class KhaveeVerifier: + """ + Verifier for Thai poetry (ฉันทลักษณ์) principles. + + Provides methods to analyze Thai words and validate poetic structures + according to traditional Thai prosody rules. This class checks vowel + sounds (สระ), spelling sections (มาตราตัวสะกด), rhymes (สัมผัส), + syllable weight (ครุ/ลหุ), and full Thai klon 4/8 poem structure (กลอน). + + This class is designed to be deterministic according to the Royal Society of + Thailand's orthographic standards. The only exception is the use of "ssg" for + syllable segmentation in the :meth:`check_klon` method. + + Key capabilities: + - :meth:`check_sara` -> Identify the phonetic vowel sound of a Thai word, + handling complex vowels (สระประสม), transformed vowels (สระเปลี่ยนรูป), + and reduced vowels (สระลดรูป). + - :meth:`check_marttra` -> Determine the orthographic spelling section + (relative to the final consonant) per Royal Society standards. + - :meth:`is_sumpus` -> Evaluate whether two words rhyme by comparing + both vowel sound and spelling section, with phonetic normalisation + for สระเกิน (e.g., อำ, ไอ). + - :meth:`check_karu_lahu` -> Classify a syllable as heavy (ครุ) or + light (ลหุ) for meter analysis. + - :meth:`check_klon` -> Validate an entire poem against traditional + กลอนสี่ (4-syllable) or กลอนแปด (8-syllable) rhyme rules. + - :meth:`check_aek_too` -> Identify tonal marks (เอก/โท) on Thai words. + - :meth:`handle_karun_sound_silence` -> Strip characters silenced by + the การันย์ marker (e.g., \"โอห์ม\" → \"โอ\"). + - :meth:`_has_true_final_yl` and :meth:`_is_true_final` -> Determine + whether a word-ending \"ย\" or \"ล\" is a genuine final consonant + rather than part of an initial cluster or vowel digraph. + + :Example: + Basic usage:: + + >>> from pythainlp.khavee import KhaveeVerifier + >>> kv = KhaveeVerifier() + >>> kv.check_sara("เริง") + 'เออ' + >>> kv.is_sumpus("สรร", "อัน") + True + >>> kv.check_klon( + ... 'ฉันชื่อหมูกรอบ ฉันชอบกินไก่ แล้ววิ่งตามไป ไล่หมาน้ำทอง', + ... k_type=4 + ... ) + 'The poem is correct according to the principle.' + + :Note: + The method :meth:`check_klon` requires the external ``ssg`` library. + """ + # explicitly include ฤ and ฦ as they act as initial consonants but aren't in thai_consonants + VALID_CONSONANTS = frozenset(thai_consonants + "ฤฦ") + + # Pali/Sanskrit loanwords where the final -ิ or -ุ is orthographically + # present but phonetically silent. Stripping it is harmless in check_sara + # (the leading vowel is detected first) and necessary in check_marttra + # (to expose the true final consonant for spelling-section classification). + _MASKING_TERMINAL_VOWELS: tuple[str, ...] = ( + "เกียรติ", "ชาติ", "ญาติ", "มัติ", "วัติ", "บัติ", "ญัติ", + "ยัติ", "ภูมิ", "พฤติ", "พรรดิ", "วรรดิ", "พยาธิ", "โพธิ", + "เกตุ", "เมรุ", "เหตุ", "ธาตุ", "วุฒิ", "สมมุติ", "วิมุติ", + ) + + # Syllables that are always light (ลหุ) regardless of orthography. + _LAHU_SYLLABLE_OVERRIDES: frozenset[str] = frozenset({ + "บ", "บ่", "ณ", "ธ", "ก็", "ฤ", "ฦ", + }) + + # Pre-computed frozensets for high-frequency set operations + _SINGLE_CHAR_WORDS: frozenset[str] = frozenset({"บ", "ณ", "ธ", "พณ", "ฤ", "ฦ"}) + + # Initial cluster sets for _is_true_final + _LAM_CLUSTERS: frozenset[str] = frozenset({ + "กล", "ขล", "คล", "ปล", "ผล", "พล", "หล", "ถล", "ฉล", "สล", "ศล", "ตล" + }) + _RUA_CLUSTERS: frozenset[str] = frozenset({ + "กร", "ขร", "คร", "ตร", "ปร", "พร", "ฟร", "บร", "ศร", "สร", "หร" + }) + _WA_CLUSTERS: frozenset[str] = frozenset({ + "กว", "ขว", "คว", "สว", "หว", "ทว", "ชว", "ศว", "ถว" + }) + _WA_WHITELIST: frozenset[str] = frozenset({"เขว", "เหว่", "แคว", "แหว", "โคว", "โหว", "โหว่"}) + + # Spelling sections (มาตราตัวสะกด) + _OPEN_SYLLABLE_VOWELS: frozenset[str] = frozenset({"า", "ๅ", "ะ", "ิ", "ี", "ึ", "ุ", "ู", "อ"}) + _KOK_CHARS: frozenset[str] = frozenset({"ก", "ข", "ค", "ฆ"}) + _KOD_CHARS: frozenset[str] = frozenset({ + "จ", "ช", "ซ", "ฎ", "ฏ", "ฐ", "ฑ", "ฒ", "ด", "ต", "ถ", "ท", "ธ", "ศ", "ษ", "ส" + }) + _KON_CHARS: frozenset[str] = frozenset({"ญ", "ณ", "น", "ร", "ล", "ฬ"}) + _KOB_CHARS: frozenset[str] = frozenset({"บ", "ป", "พ", "ฟ", "ภ"}) + + # Karu / Lahu prosody + _LONG_VOWELS: frozenset[str] = frozenset({ + "อา", "อี", "อือ", "อู", "เอ", "แอ", "เออ", "โอ", "ออ", "เอีย", "เอือ", "อัว" + }) + _SPECIAL_VOWELS: frozenset[str] = frozenset({"อำ", "ไอ", "เอา"}) + _EXPLICIT_SARA_WORDS: frozenset[str] = frozenset({"เออะ", "เออ", "เอ", "เอะ", "เอา", "เอาะ"}) + def __init__(self) -> None: - """ - KhaveeVerifier: Thai Poetry verifier - """ + """Initialize the KhaveeVerifier class.""" + # For backward compatibility, this method is kept as a private method. def _has_true_final_yl(self, word: str) -> bool: """ Check if ย or ล is a true final consonant @@ -26,40 +124,126 @@ def _has_true_final_yl(self, word: str) -> bool: :return: True if ย or ล is a true final consonant :rtype: bool """ + return self._is_true_final(word) + + def _is_true_final(self, word: str) -> bool: + """ + Check if the last character is a true final consonant + (not part of a vowel sound or an initial cluster). + + :param str word: Thai word + :return: True if the ending character acts as a final consonant + :rtype: bool + """ + # Handle การันย์ + word = self.handle_karun_sound_silence(word) + # Store original word to distinguish tone-dependent structures + original_word = word + # Strip tone marks first so words like 'ใกล้' properly evaluate as ending in 'ล' + word = remove_tonemark(word) + if len(word) < 2: return False - # Count consonants in the word - consonant_count = sum(1 for c in word if c in thai_consonants) - # If there are 2+ consonants and word ends with ย or ล, it's a true final - return consonant_count >= 2 and word[-1] in ["ย", "ล"] + + last_char = word[-1] + + consonants = [c for c in word if c in self.VALID_CONSONANTS] + if len(consonants) < 2: + return False + + # คำควบกล้ำ / อักษรนำ (initial clusters) + cluster = consonants[0] + consonants[1] + + # ไ/ใ never take a final consonant ย here is silent (ไทย, ไชย) + # Check for ย inside เ-ีย (เสีย, เมีย) (part of the vowel) + if last_char == "ย": + if ("ไ" in word or "ใ" in word) or ("เ" in word and "ี" in word): + return False + + # --------------------------------------------------------------------- + # Guard Clauses: If it's not ending in ล, ร, ว, or doesn't have exactly 2 + # consonants, or lacks pre-posed vowels, it bypasses the cluster checks. + # --------------------------------------------------------------------- + if last_char not in "ลรว" or len(consonants) != 2: + return True + + # Check for ล, ร, ว in initial clusters (คำควบกล้ำ / อักษรนำ) + # with pre-posed vowels เ-, แ-, โ-, ไ-, ใ- + # (เปล, เถล, แผล, โหล, ไกล, ใกล้, โปร, แตร, ไกว, เขว) + if not ("เ" in word or "แ" in word or "โ" in word or "ไ" in word or "ใ" in word): + return True + + # Check for ล + if last_char == "ล" and cluster in self._LAM_CLUSTERS: + # Exception 'เพล' - แม่กน (monk food ฉันเพล) Returns True, otherwise False + return word == "เพล" + + # Check for ร + if last_char == "ร" and cluster in self._RUA_CLUSTERS: + return False + + # Check for ว (ควบแท้ and อักษรนำ) + if last_char == "ว": + # With ไ/ใ, 'ว' is ALWAYS a cluster (ไกว, ไขว้) + if ("ไ" in word or "ใ" in word) and (cluster in self._WA_CLUSTERS): + return False + + # With เ/แ/โ, 'ว' is mostly is a true final (เลว, เหว, แก้ว, แห้ว). + # Whitelist อักษรนำ/คำควบกล้ำ as exceptions + elif ("เ" in word or "แ" in word or "โ" in word): + # USE ORIGINAL_WORD to safely catch open syllables แม่ ก กา + # เดินเขว, ว้าเหว่, แม่น้ำแคว, ตวาดแหว, โควตา, ช่องโหว่ + if original_word in self._WA_WHITELIST: + return False + + # If it passed all the filters above, it is a true final (จัย, สมัย, ชล, ผล, เหนื่อย) + return True def check_sara(self, word: str) -> str: """ - Check the vowels in the Thai word. + Check the phonetic vowel sound (สระ) of a Thai word. + + Extracts the core vowel representation used for rhyme matching, + handling complex vowel combinations, + transformed vowels (สระเปลี่ยนรูป), and reductions (สระลดรูป). :param str word: Thai word - :return: vowel name of the word + :return: The name of the vowel sound of the word (e.g., 'เออ', 'อะ', 'เอาะ') :rtype: str :Example: >>> from pythainlp.khavee import KhaveeVerifier # doctest: +SKIP - >>> kv = KhaveeVerifier() # doctest: +SKIP - >>> print(kv.check_sara("เริง")) # doctest: +SKIP 'เออ' """ sara = [] - countoa = 0 + countoa = 0 # Count occurrences of 'อ' + + # Store original word to safely evaluate exceptions (like ฤทธิ์) after Karun stripping + original_word = word + + # In case of การันย์ (word stripped of Karun characters) + word = self.handle_karun_sound_silence(word) + # Remove tonemarks for checking endings safely + word_req = remove_tonemark(word) - # In case of การันย์ - if "์" in word[-1]: - word = word[:-2] + # Empty string / weird input should return empty string (Catches "", "อ์", "้", etc.) + if not word_req: + return "" + + # Intercept Pali/Sanskrit words with silent terminal vowels (สระที่ไม่ออกเสียงท้ายคำ) + # Removing the final character -ิ or -ุ + if word_req.endswith(self._MASKING_TERMINAL_VOWELS): + word = word[:-1] + word_req = word_req[:-1] # In case of สระเดี่ยว for i in word: - if i in ("ะ", "ั"): + if i == "ั" and word_req.endswith("ว"): + sara.append("อัว") + elif i in ("ะ", "ั"): sara.append("อะ") elif i == "ิ": sara.append("อิ") @@ -86,31 +270,31 @@ def check_sara(self, word: str) -> str: elif i == "อ": countoa += 1 sara.append("ออ") - elif i == "ั" and "ว" in word: - sara.append("อัว") elif i in ("ไ", "ใ"): sara.append("ไอ") elif i == "็": - sara.append("ออ") - elif "รร" in word: - if self.check_marttra(word) == "กม": - sara.append("อำ") - else: - sara.append("อะ") + sara.append("็") + + # In case of รร + if "รร" in word: + if self.check_marttra(word) == "กม": + sara.append("อำ") + else: + sara.append("อะ") - # In case of ออ - if countoa == 1 and "อ" in word[-1] and "เ" not in word: + # Clean up 'ออ' if 'อ' is acting purely as an initial consonant (อต, อด, อบ, อวบ) + if "ออ" in sara and len(sara) == 1 and word.startswith("อ") and countoa == 1: sara.remove("ออ") - # In case of เอ เอ - countA = 0 - for i in sara: - if i == "เอ": - countA = countA + 1 - if countA > 1: - sara.remove("เอ") - sara.remove("เอ") - sara.append("แ") + # In case of ออ (Clean redundant ออ from compound vowels like คือ, มือ) + if countoa == 1 and "อ" == word[-1] and "เ" not in word and "ออ" in sara and len(sara) > 1: + sara.remove("ออ") + + # In case of เอ เอ (merging two 'เอ' into 'แอ') + if sara.count("เอ") >= 2: + sara.remove("เอ") + sara.remove("เอ") + sara.append("แอ") # In case of สระประสม if "เอ" in sara and "อะ" in sara: @@ -122,6 +306,20 @@ def check_sara(self, word: str) -> str: sara.remove("อะ") sara.append("แอะ") + # In case of สระประสม Transformed vowels ไม้ไต่คู้ (-็) + if "็" in sara: + sara.remove("็") + if "เอ" in sara: + sara.remove("เอ") + sara.append("เอะ") # เจ็ด, เป็น, เด็ก + elif "แอ" in sara: + sara.remove("แอ") + sara.append("แอะ") # แข็ง, แท็กซี่, แย็บ + else: + if "ออ" in sara: + sara.remove("ออ") + sara.append("เอาะ") # ก็, ล็อก, ผล็อย + if "เอะ" in sara and "ออ" in sara: sara.remove("เอะ") sara.remove("ออ") @@ -129,23 +327,19 @@ def check_sara(self, word: str) -> str: elif "เอ" in sara and "อิ" in sara: sara.remove("เอ") sara.remove("อิ") - sara.append("เออ") - elif "เอ" in sara and "ออ" in sara and "อ" in word[-1]: + sara.append("เออ") # เกิด, เมิน + elif "อ" == word[-1] and "เอ" in sara and "ออ" in sara: sara.remove("เอ") sara.remove("ออ") - sara.append("เออ") + sara.append("เออ") # เหม่อ elif "โอ" in sara and "อะ" in sara: sara.remove("โอ") sara.remove("อะ") - sara.append("โอะ") + sara.append("โอะ") # โต๊ะ elif "เอ" in sara and "อี" in sara: sara.remove("เอ") sara.remove("อี") - sara.append("เอีย") - elif "เอ" in sara and "อือ" in sara: - sara.remove("เอ") - sara.remove("อือ") - sara.append("อัว") + sara.append("เอีย") # เรียน elif "เอ" in sara and "อา" in sara: sara.remove("เอ") sara.remove("อา") @@ -157,225 +351,301 @@ def check_sara(self, word: str) -> str: if "อือ" in sara and "เออ" in sara: sara.remove("เออ") sara.remove("อือ") - sara.append("เอือ") + sara.append("เอือ") # มะเขือ, เสือ, เงือก elif "ออ" in sara and len(sara) > 1: sara.remove("ออ") elif "ว" in word and len(sara) == 0: - sara.append("อัว") + if word_req in {"บวร", "วร"}: + sara.append("ออ") + else: + sara.append("อัว") # ควร, บวก, สวม if "ั" in word and self.check_marttra(word) == "กา": - sara = [] - sara.append("ไอ") + if "อัว" not in sara: + sara = ["ไอ"] # In case of อ - if word == "เออะ": - sara = [] - sara.append("เออะ") - elif word == "เออ": - sara = [] - sara.append("เออ") - elif word == "เอ": - sara = [] - sara.append("เอ") - elif word == "เอะ": - sara = [] - sara.append("เอะ") - elif word == "เอา": - sara = [] - sara.append("เอา") - elif word == "เอาะ": - sara = [] - sara.append("เอาะ") + if word in self._EXPLICIT_SARA_WORDS: + sara = [word] - if "ฤา" in word or "ฦา" in word: - sara = [] - sara.append("อือ") - elif "ฤ" in word or "ฦ" in word: + # In case of เ-ือ + if "เ" in word and "ื" in word and "อ" in word: + sara = ["เอือ"] + + # In case of เ-ย (ลดรูป เ-อ) เลย, เคย, เอย + if "เอ" in sara and word_req.endswith("ย") and self._is_true_final(original_word): + # Ensure no competing vowels exist ('เตียง' uses เอีย, not เออ) + other_vowels = [v for v in sara if v not in {"เอ", "ออ"}] + if not other_vowels: + sara = ["เออ"] + + # In case of ฤ ฦ + if any(ex in original_word for ex in ("ฤา", "ฤๅ", "ฦา", "ฦๅ")): + sara = ["อือ"] + elif "ฤ" in original_word or "ฦ" in original_word: sara = [] - sara.append("อึ") - - # In case of กน - if not sara and len(word) == 2: - if word[-1] != "ร": - sara.append("โอะ") + # for 'เออ' (ฤกษ์ - เริก) the only 'เออ' sound exception of ฤ + if word == "ฤก" or original_word.startswith("ฤกษ"): + sara.append("เออ") + # for 'อิ' (กฤษณ์, กฤษณะ, ตฤณ, ตฤตีย, ทฤษฎี, ประกฤติ, วิกฤต, ฤทธิ์, อังกฤษ) + # Use original_word here to ensure stripped Karun characters (like ธิ์) are evaluated + elif any(ex in original_word for ex in + ("กฤช", "กฤต", "กฤษ", "ตฤต", "ตฤณ", "ทฤษ", "ปฤษ", "ศฤง", "สฤต", "ฤทธ")): + sara.append("อิ") + # Default 'อึ' (รึ) (ฤดู, ฤทัย, พฤษภาคมม) else: - sara.append("ออ") - elif not sara and len(word) == 3: - sara.append("ออ") + sara.append("อึ") - # In case of บ่ - if word == "บ่": - sara = [] - sara.append("ออ") + # In case of สระลดรูป (ออ, โอะ) + if not sara and len(word) >= 2: + if word[-1] == "ร": + # Words ending with ร without vowels usually take the 'ออ' sound (พร, นคร) + sara.append("ออ") + else: + # Other consonants without vowels usually take the hidden 'โอะ' sound (นม, กรด) + sara.append("โอะ") + # In case of นิกหิต (-ํ) + า (miss-typed of สระอำ) or standalone นิกหิต (-ํ) 'อัง' if "ํ" in word: - sara = [] - sara.append("อำ") + if "ํา" in word: + sara = ["อำ"] # The strict decomposed 'อำ' typo + else: + # Standalone sounds like 'อัง' (อะ + ง) from pali/sanskrit + sara = ["อะ"] - if "เ" in word and "ื" in word and "อ" in word: - sara = [] - sara.append("เอือ") + # In case of บ่ / บ + if word_req == "บ": + sara = ["ออ"] + + # In case of isolated symbols as words (ลดรูป อะ) + if word_req in {"ณ", "ธ", "อ", "พณ"}: + sara = ["อะ"] if not sara: - return "Can't find Sara in this word" + # Fallback: If no explicit vowel is found and it doesn't fit closed-syllable + # reductions, assume the implied short 'a' (เสียงอะกึ่งมาตรา). + return "อะ" return sara[0] def check_marttra(self, word: str) -> str: """ - Check the Thai spelling section of the Thai word. + Check the spelling section (มาตราตัวสะกด) of a Thai word. + + Note: This function strictly adheres to orthographic spelling (รูป) based on + the Royal Society of Thailand (ราชบัณฑิตยสภา) standards, rather than phonetics (เสียง). + Therefore, words ending in สระเกิน (อำ, ไอ, ใอ, เอา) as well as ฤ, ฤๅ, ฦ, ฦๅ + are correctly classified grammatically as แม่ ก กา ("กา"). Phonetic rhyming + for these vowels is handled dynamically in the `is_sumpus` function. :param str word: Thai word - :return: name of the spelling section of the word + :return: name of the spelling section of the word (e.g., กา, กก, กด, กน, กบ, กม, เกย, เกอว) :rtype: str :Example: >>> from pythainlp.khavee import KhaveeVerifier # doctest: +SKIP - >>> kv = KhaveeVerifier() # doctest: +SKIP - >>> print(kv.check_marttra("สาว")) # doctest: +SKIP 'เกอว' + >>> print(kv.check_marttra("ทำ")) # doctest: +SKIP + 'กา' """ + # Resolve Karun first to prevents "ศาสตร์" or "ศุกร์" + # from complicating the cluster logic. + word = self.handle_karun_sound_silence(word) + + # is_true_final process requires the original word + # to check for exceptions in อักษรนำ/คำควบกล้ำ + original_word = word + word = remove_tonemark(word) + + # Empty string should be returned as empty string (Catches "", "อ์", "้", etc.) + if not word: + return "" + + # Intercept Pali/Sanskrit words with silent terminal vowels (สระที่ไม่ออกเสียงท้ายคำ) + # Removing the final character -ิ or -ุ + if word.endswith(self._MASKING_TERMINAL_VOWELS): + word = word[:-1] + # Handle consonant clusters ending with ร - # ตร, ทร → remove ร (treat as final ต/ท sound) - # กร, ขร, คร, ฆร in compound words → remove ร (treat as final ก/ข/ค sound) - # But single syllable words like "กร" should keep ร if len(word) >= 3 and word[-1] == "ร": - if word[-2] in ["ต", "ท"]: - word = word[:-1] - elif word[-2] in ["ก", "ข", "ค", "ฆ"]: + prev_char = word[-2] + + # Safe to always strip + # (e.g., บุตร, เนตร, มิตร, เกษตร, บัตร, เพชร, กอปร (อ่านว่า กอบ)) + if prev_char in {"ต", "ช", "ป"}: word = word[:-1] - word = self.handle_karun_sound_silence(word) - word = remove_tonemark(word) + # Ambiguous (e.g., มังกร vs จักร, สุนทร vs สมุทร) + # Only strip 'ร' if the cluster is preceded by a specific short vowel. + elif prev_char in {"ก", "ข", "ค", "ฆ", "ท"}: + char_before_prev = word[-3] - # Check for ำ at the end (represents "am" sound, ends with m) - if word[-1] == "ำ": - return "กม" + # If preceded by -ั, -ิ, -ี, -ุ, -ู (e.g., จั-ก-ร, สมุ-ท-ร) + if char_before_prev in {"ั", "ิ", "ี", "ุ", "ู"}: + word = word[:-1] + # (Words like นคร, มังกร, สุนทร will correctly BYPASS this and remain แม่กน) + + # Check for อักษรตัวเดียวแทนคำ Standalone words + if word in self._SINGLE_CHAR_WORDS: + return "กา" + + # ------------------------------------------------------------------------- + # สระเกิน (อำ, ไอ, ใอ, เอา) Orthographic Handlers + # According to the Royal Society, these are classified structurally as แม่ ก กา. + # Phonetic rhyming (e.g. กรรม rhyming with จำ) is normalized in `is_sumpus`. + # ------------------------------------------------------------------------- - # Check for vowels and special patterns that indicate open syllables (กา) - # For words with ไ/ใ, check if ย/ล is a true final or just part of vowel + # Check for ำ or นิคหิต (-ํ) + า + if word[-1] == "ำ" or word.endswith("ํา"): + return "กา" + + # Check for standalone นิคหิต (-ํ) 'อัง' + if word.endswith("ํ"): + return "กง" + + # Any word with exactly 1 consonant (and not ending in ำ/ํ) + # cannot have a final consonant and therefore must be "กา" + + consonants = sum(1 for c in word if c in self.VALID_CONSONANTS) + if consonants == 1: + return "กา" + + # Check for ไ/ใ if "ไ" in word or "ใ" in word: - if word[-1] not in ["ย", "ล"]: + if word[-1] not in "ยลรว": return "กา" - elif not self._has_true_final_yl(word): - # ย/ล is part of the vowel sound, not a true final + elif not self._is_true_final(original_word): return "กา" - # else: ย/ล is a true final, continue to consonant classification below - if "ํ" in word and "า" in word: - return "กา" - elif ( - word[-1] in ["า", "ะ", "ิ", "ี", "ุ", "ู", "อ"] - or ("ี" in word and "ย" in word[-1]) - or ("ื" in word and "อ" in word[-1]) + # Check for เ, แ, โ + ย, ร, ล, ว (คำควบกล้ำ / อักษรนำ) + if word[-1] in "ยลรว" and ("เ" in word or "แ" in word or "โ" in word): + # ไม่ใช่ตัวสะกดแท้ -> แม่ก กา + if not self._is_true_final(original_word): + return "กา" + + # Check for ตัวสะกด final consonants + # Add รากยาว "ๅ" (not สระอา) for word like ฤๅ(ษี) + last_char = word[-1] + if ( + last_char in self._OPEN_SYLLABLE_VOWELS + or ("ี" in word and last_char == "ย") # Catch สระเอีย (เสีย, เมีย) + or ("ื" in word and last_char == "อ") # Catch สระอือ (เรือ, เสือ) + or ("ั" in word and last_char == "ว") # Catch สระอัว (ตัว, ชั่ว, กลัว, อัว) ): return "กา" - elif word[-1] in ["ง"]: + elif last_char == "ง": return "กง" - elif word[-1] in ["ม"]: + elif last_char == "ม": return "กม" - elif word[-1] in ["ย"]: - return "เกย" - elif word[-1] in ["ล"]: + elif last_char == "ย": return "เกย" - elif word[-1] in ["ว"]: + elif last_char == "ว": return "เกอว" - elif word[-1] in ["ก", "ข", "ค", "ฆ"]: + elif last_char in self._KOK_CHARS: return "กก" - elif word[-1] in [ - "จ", - "ช", - "ซ", - "ฎ", - "ฏ", - "ฐ", - "ฑ", - "ฒ", - "ด", - "ต", - "ถ", - "ท", - "ธ", - "ศ", - "ษ", - "ส", - ]: + elif last_char in self._KOD_CHARS: return "กด" - elif word[-1] in ["ญ", "ณ", "น", "ร", "ฬ"]: + elif last_char in self._KON_CHARS: return "กน" - elif word[-1] in ["บ", "ป", "พ", "ฟ", "ภ"]: + elif last_char in self._KOB_CHARS: return "กบ" else: - if "็" in word: - return "กา" - else: - return "Can't find Marttra in this word" + return "กา" def is_sumpus(self, word1: str, word2: str) -> bool: """ - Check the rhyme between two words. + Check the rhyme (สัมผัส) between two Thai words. - :param str word1: Thai word - :param str word2: Thai word - :return: boolean + This function evaluates both the vowel sound (สระ) and the spelling section (มาตราตัวสะกด). + It incorporates phonetic normalization for สระเกิน (อำ, ไอ, ใอ) to ensure that + words with matching sounds but differing orthographies (e.g., "จำ" and "กรรม") + are correctly evaluated as rhymes. + + :param str word1: First Thai word + :param str word2: Second Thai word + :return: True if the words rhyme, False otherwise. :rtype: bool :Example: >>> from pythainlp.khavee import KhaveeVerifier # doctest: +SKIP - >>> kv = KhaveeVerifier() # doctest: +SKIP - >>> print(kv.is_sumpus("สรร", "อัน")) # doctest: +SKIP True - - >>> print(kv.is_sumpus("สรร", "แมว")) # doctest: +SKIP - False + >>> print(kv.is_sumpus("จำ", "กรรม")) # doctest: +SKIP + True """ + # Empty string should be returned as False + if not word1 or not word2: + return False + marttra1 = self.check_marttra(word1) marttra2 = self.check_marttra(word2) sara1 = self.check_sara(word1) sara2 = self.check_sara(word2) + + # ------------------------------------------------------------------------- + # Phonetic Normalization for สระเกิน (อำ, ไอ, ใอ) + # + # While check_marttra classifies words like "วัย" as อะ+เกย and "ใจ" as ไอ+กา, + # poetry cares about the sound (เสียง). + # We normalize the phonetic CVC structures into their สระเกิน counterparts. + # ('เอา' requires no normalizer as native Thai spelling forces the /aw/ sound + # to use 'เ-า', bypassing the need for an อะ+เกอว collision). + # ------------------------------------------------------------------------- + + # อัย -> ไอ (Normalize 'อะ' + 'เกย' to 'ไอ' + 'กา') if sara1 == "อะ" and marttra1 == "เกย": sara1 = "ไอ" marttra1 = "กา" - elif sara2 == "อะ" and marttra2 == "เกย": + if sara2 == "อะ" and marttra2 == "เกย": sara2 = "ไอ" marttra2 = "กา" - if sara1 == "อำ" and marttra1 == "กม": + # อัม -> อำ (Normalize both 'อำ' and 'อะ' + 'กม' to 'อำ' + 'กา') + if (sara1 == "อะ" or sara1 == "อำ") and marttra1 == "กม": sara1 = "อำ" marttra1 = "กา" - elif sara2 == "อำ" and marttra2 == "กม": + if (sara2 == "อะ" or sara2 == "อำ") and marttra2 == "กม": sara2 = "อำ" marttra2 = "กา" return bool(marttra1 == marttra2 and sara1 == sara2) - def check_karu_lahu(self, text: str) -> str: + def check_karu_lahu(self, text: str) -> Union[str, bool]: + """Classify a Thai syllable as heavy (ครุ karu) or light (ลหุ lahu). + + Syllable weight is determined by Thai prosody rules for classical poetry: + + - A syllable is heavy (ครุ) if it contains a long vowel, ends with any + final consonant (including sonorant finals / นมยวง), or contains one + of the special inherently bound vowels (อำ, ไอ, ใอ, เอา). + - A syllable is light (ลหุ) if it is an open syllable (แม่ ก กา) + containing a short vowel with no final consonant. + + Args: + text (str): A single Thai syllable or word to classify. + + Returns: + Union[str, bool]: "karu" for heavy syllables or "lahu" for light syllables. + or False if the input is an empty string. + """ + # Empty string should be returned as False + if not text: + return False + + if text in self._LAHU_SYLLABLE_OVERRIDES: + return "lahu" + + marttra = self.check_marttra(text) + sara = self.check_sara(text) + if ( - self.check_marttra(text) != "กา" - or ( - self.check_marttra(text) == "กา" - and self.check_sara(text) - in [ - "อา", - "อี", - "อือ", - "อู", - "เอ", - "แอ", - "โอ", - "ออ", - "เออ", - "เอีย", - "เอือ", - "อัว", - ] - ) - or self.check_sara(text) in ["อำ", "ไอ", "เอา"] - ) and text not in ["บ่", "ณ", "ธ", "ก็"]: + marttra != "กา" + or sara in self._LONG_VOWELS + or sara in self._SPECIAL_VOWELS + ): return "karu" else: return "lahu" @@ -385,242 +655,157 @@ def check_klon(self, text: str, k_type: int = 8) -> Union[list[str], str]: Check the suitability of the poem according to Thai principles. :param str text: Thai poem - :param int k_type: type of Thai poem - :return: the check results of the suitability of the - poem according to Thai principles. + :param int k_type: type of Thai poem (4 or 8) + :return: the check results of the suitability of the poem according to Thai principles. :rtype: Union[list[str], str] + ════════════════════════════════════════════════════════════════════════ + + กลอนสี่ (Klon 4) Diagram: + วรรคที่ ๑ (สดับ) วรรคที่ ๒ (รับ) + วรรคที่ ๓ (รอง) วรรคที่ ๔ (ส่ง) + + ┏━━━━━━━━┯━┓ [สัมผัสคำที่ 1 หรือ 2] + O O O X X X O O + ┏━━━━━━━━┯━┳━━━┛ + O O O X X X O X ━┓ + ┏━━━━━━━━┯━┓ ┃ สัมผัสระหว่างบท (Inter-stanza rhyme) + O O O X X X O O ━┛ + ┏━━━━━━━━┯━┳━━━┛ + O O O X X X O X + + ════════════════════════════════════════════════════════════════════════ + + กลอนแปด (Klon 8) Diagram: + วรรคที่ ๑ (สดับ) วรรคที่ ๒ (รับ) + วรรคที่ ๓ (รอง) วรรคที่ ๔ (ส่ง) + + ┏━━━━━━━━┯━┯━┳━┯━┑ [สัมผัสคำที่ 3 หรือ 5 / อนุโลม 1,2,4] + O O O O O O O X O O X O O O O X + ┏━━━━━━━━┯━┯━┳━┯━━━━━━━┛ + O O O O O O O X O O X O O O O X ━┓ + ┏━━━━━━━━┯━┯━┳━┯━┑ ┃ สัมผัสระหว่างบท (Inter-stanza rhyme) + O O O O O O O X O O X O O O O X ━┛ + ┏━━━━━━━━┯━┯━┳━┯━━━━━━━┛ + O O O O O O O X O O X O O O O X + + ════════════════════════════════════════════════════════════════════════ + :Example: >>> from pythainlp.khavee import KhaveeVerifier # doctest: +SKIP - >>> kv = KhaveeVerifier() # doctest: +SKIP - >>> print(kv.check_klon( # doctest: +SKIP - ... 'ฉันชื่อหมูกรอบ ฉันชอบกินไก่ แล้วก็วิ่งไล่ หมาชื่อนํ้าทอง ลคคนเก่ง เอ๋งเอ๋งคะนอง \ - ... มีคนจับจอง เขาชื่อน้องเธียร', + ... 'ฉันชื่อหมูกรอบ ฉันชอบกินไก่ แล้ววิ่งตามไป ไล่หมาน้ำทอง \ + ... ฉันมันคนเก่ง เอ๋งเอ๋งคะนอง มีคนจับจอง เป็นของน้องเธียร', \ ... k_type=4 ... )) The poem is correct according to the principle. - - >>> print(kv.check_klon( # doctest: +SKIP - ... 'ฉันชื่อหมูกรอบ ฉันชอบกินไก่ แล้วก็วิ่งไล่ หมาชื่อนํ้าทอง ลคคนเก่ง \ - ... เอ๋งเอ๋งเสียงหมา มีคนจับจอง เขาชื่อน้องเธียร', - ... k_type=4 - ... )) - [ - "Can't find rhyme between paragraphs ('หมา', 'จอง') in paragraph 2", - "Can't find rhyme between paragraphs ('หมา', 'ทอง') in paragraph 2" - ] """ - if k_type == 8: - try: - error = [] - list_sumpus_sent1 = [] - list_sumpus_sent2h = [] - list_sumpus_sent2l = [] - list_sumpus_sent3 = [] - list_sumpus_sent4 = [] - for i, sent in enumerate(text.split()): - sub_sent = subword_tokenize(sent, engine="dict") - if len(sub_sent) > 10: - error.append( - "In sentence " - + str(i + 2) - + ", there are more than 10 words. " - + str(sub_sent) - ) - if (i + 1) % 4 == 1: - list_sumpus_sent1.append(sub_sent[-1]) - elif (i + 1) % 4 == 2: - list_sumpus_sent2h.append( - [ - sub_sent[1], - sub_sent[2], - sub_sent[3], - sub_sent[4], - ] - ) - list_sumpus_sent2l.append(sub_sent[-1]) - elif (i + 1) % 4 == 3: - list_sumpus_sent3.append(sub_sent[-1]) - elif (i + 1) % 4 == 0: - list_sumpus_sent4.append(sub_sent[-1]) - if ( - len(list_sumpus_sent1) != len(list_sumpus_sent2h) - or len(list_sumpus_sent2h) != len(list_sumpus_sent2l) - or len(list_sumpus_sent2l) != len(list_sumpus_sent3) - or len(list_sumpus_sent3) != len(list_sumpus_sent4) - or len(list_sumpus_sent4) != len(list_sumpus_sent1) - ): - return "The poem does not have 4 complete sentences." - else: - for i in range(len(list_sumpus_sent1)): - countwrong = 0 - for j in list_sumpus_sent2h[i]: - if ( - self.is_sumpus(list_sumpus_sent1[i], j) - is False - ): - countwrong += 1 - if countwrong > 3: - error.append( - "Can't find rhyme between paragraphs " - + str( - ( - list_sumpus_sent1[i], - list_sumpus_sent2h[i], - ) - ) - + " in paragraph " - + str(i + 1) - ) - if ( - self.is_sumpus( - list_sumpus_sent2l[i], list_sumpus_sent3[i] - ) - is False - ): - error.append( - "Can't find rhyme between paragraphs " - + str( - ( - list_sumpus_sent2l[i], - list_sumpus_sent3[i], - ) - ) - + " in paragraph " - + str(i + 1) - ) - if i > 0: - if ( - self.is_sumpus( - list_sumpus_sent2l[i], - list_sumpus_sent4[i - 1], - ) - is False - ): - error.append( - "Can't find rhyme between paragraphs " - + str( - ( - list_sumpus_sent2l[i], - list_sumpus_sent4[i - 1], - ) - ) - + " in paragraph " - + str(i + 1) - ) - if not error: - return ( - "The poem is correct according to the principle." - ) - else: - return error - except Exception: - return "Something went wrong. Make sure you enter it in the correct form of klon 8." - elif k_type == 4: - try: - error = [] - list_sumpus_sent1 = [] - list_sumpus_sent2h = [] - list_sumpus_sent2l = [] - list_sumpus_sent3 = [] - list_sumpus_sent4 = [] - for i, sent in enumerate(text.split()): - sub_sent = subword_tokenize(sent, engine="dict") - if len(sub_sent) > 5: - error.append( - "In sentence " - + str(i + 2) - + ", there are more than 4 words. " - + str(sub_sent) + + try: + __import__("ssg") + except ImportError as exc: + raise ImportError( + "The 'ssg' library is required for comprehensive poem analysis (check_klon). " + "Please install it using: pip install ssg" + ) from exc + + if k_type not in {4, 8}: + return "Something went wrong. Make sure you enter it in the correct form (k_type 4 or 8)." + + try: + # Normalize spacing and split phrases/sentences across arbitrary whitespace + waks = text.split() + # Ensure the poem has complete stanzas (4 waks per stanza) + if len(waks) % 4 != 0 or len(waks) == 0: + return "The poem does not have complete stanzas (บท). A stanza must contain exactly 4 sentences (วรรค)." + + errors = [] + stanzas = [] + # วรรคสดับ (Wak 1), วรรครับ (Wak 2), วรรครอง (Wak 3), วรรคส่ง (Wak 4) + wak_names = ["Wak 1", "Wak 2", "Wak 3", "Wak 4"] + + # 1. Tokenize and group sentences into stanzas (4 Waks per stanza) + for i in range(0, len(waks), 4): + stanza = [subword_tokenize(waks[i + j], engine="ssg") for j in range(4)] + stanzas.append(stanza) + + # 2. Evaluate rules for each stanza + for stanza_index, stanza in enumerate(stanzas): + wak1, wak2, wak3, wak4 = stanza + + # Safety check against empty sentences + if not all((wak1, wak2, wak3, wak4)): + errors.append(f"Stanza (บทที่) {stanza_index + 1} contains empty sentences.") + continue + + # Check word counts + max_words = 10 if k_type == 8 else 5 + for wak_index, wak in enumerate(stanza): + if len(wak) > max_words: + errors.append( + f"Stanza (บทที่) {stanza_index + 1} {wak_names[wak_index]}: " + f"Word count exceeds {max_words}: {wak}" ) - if (i + 1) % 4 == 1: - list_sumpus_sent1.append(sub_sent[-1]) - elif (i + 1) % 4 == 2: - list_sumpus_sent2h.append([sub_sent[1], sub_sent[2]]) - list_sumpus_sent2l.append(sub_sent[-1]) - elif (i + 1) % 4 == 3: - list_sumpus_sent3.append(sub_sent[-1]) - elif (i + 1) % 4 == 0: - list_sumpus_sent4.append(sub_sent[-1]) - if ( - len(list_sumpus_sent1) != len(list_sumpus_sent2h) - or len(list_sumpus_sent2h) != len(list_sumpus_sent2l) - or len(list_sumpus_sent2l) != len(list_sumpus_sent3) - or len(list_sumpus_sent3) != len(list_sumpus_sent4) - or len(list_sumpus_sent4) != len(list_sumpus_sent1) - ): - return "The poem does not have 4 complete sentences." + + # Define rhyme target lengths based on Klon type + # Klon 8: Targets first 5 words (อนุโลม 1, 2, 4 บังคับ 3, 5) + # Klon 4: Targets first 2 words + if k_type == 8: + wak2_targets = wak2[:5] + wak4_targets = wak4[:5] else: - for i in range(len(list_sumpus_sent1)): - countwrong = 0 - for j in list_sumpus_sent2h[i]: - if ( - self.is_sumpus(list_sumpus_sent1[i], j) - is False - ): - countwrong += 1 - if countwrong > 1: - error.append( - "Can't find rhyme between paragraphs " - + str( - ( - list_sumpus_sent1[i], - list_sumpus_sent2h[i], - ) - ) - + " in paragraph " - + str(i + 1) - ) - if ( - self.is_sumpus( - list_sumpus_sent2l[i], list_sumpus_sent3[i] - ) - is False - ): - error.append( - "Can't find rhyme between paragraphs " - + str( - ( - list_sumpus_sent2l[i], - list_sumpus_sent3[i], - ) - ) - + " in paragraph " - + str(i + 1) - ) - if i > 0: - if ( - self.is_sumpus( - list_sumpus_sent2l[i], - list_sumpus_sent4[i - 1], - ) - is False - ): - error.append( - "Can't find rhyme between paragraphs " - + str( - ( - list_sumpus_sent2l[i], - list_sumpus_sent4[i - 1], - ) - ) - + " in paragraph " - + str(i + 1) - ) - if not error: - return ( - "The poem is correct according to the principle." + # Klon 4: If the target sentence has 5 words, check the first 3 words. + # Otherwise, check the standard first 2 words. + limit_wak2 = 3 if len(wak2) == 5 else 2 + limit_wak4 = 3 if len(wak4) == 5 else 2 + + wak2_targets = wak2[:limit_wak2] + wak4_targets = wak4[:limit_wak4] + + # Extract the last word of each Wak + wak1_last = wak1[-1] + wak2_last = wak2[-1] + wak3_last = wak3[-1] + + # Rule 1: วรรคสดับ -> วรรครับ + if not any(self.is_sumpus(wak1_last, target) for target in wak2_targets): + errors.append( + f"Rhyme error in Stanza (บทที่) {stanza_index + 1}: " + f"'{wak1_last}' ({wak_names[0]}) does not rhyme with {wak2_targets} ({wak_names[1]})" + ) + + # Rule 2: วรรครับ -> วรรครอง + if not self.is_sumpus(wak2_last, wak3_last): + errors.append( + f"Rhyme error in Stanza (บทที่) {stanza_index + 1}: " + f"'{wak2_last}' ({wak_names[1]}) does not rhyme with '{wak3_last}' ({wak_names[2]})" + ) + + # Rule 3: วรรครอง -> วรรคส่ง + if not any(self.is_sumpus(wak3_last, target) for target in wak4_targets): + errors.append( + f"Rhyme error in Stanza (บทที่) {stanza_index + 1}: " + f"'{wak3_last}' ({wak_names[2]}) does not rhyme with {wak4_targets} ({wak_names[3]})" + ) + + # Rule 4: สัมผัสระหว่างบท (Inter-stanza) + if stanza_index > 0: + # Target Wak 4 of the previous stanza + prev_wak4_last = stanzas[stanza_index - 1][3][-1] + if not self.is_sumpus(prev_wak4_last, wak2_last): + errors.append( + f"Inter-stanza rhyme error (ผิดสัมผัสระหว่างบท) between Stanza {stanza_index} and {stanza_index + 1}: " + f"'{prev_wak4_last}' ({wak_names[3]}) does not rhyme with '{wak2_last}' ({wak_names[1]})" ) - else: - return error - except Exception: - return "Something went wrong. Make sure you enter it in the correct form." - else: - return "Something went wrong. Make sure you enter it in the correct form." + if not errors: + return "The poem is correct according to the principle." + return errors + + except Exception as e: + return f"Something went wrong during evaluation: {e}" def check_aek_too( self, text: Union[list[str], str], dead_syllable_as_aek: bool = False @@ -648,8 +833,8 @@ def check_aek_too( ... kv.check_aek_too("เอ้ง"), ... ) >>> # -> False, aek, too - >>> print(kv.check_aek_too(["เอง", "เอ่ง", "เอ้ง"])) # ใช้ List ได้เหมือนกัน # doctest: +SKIP - >>> # -> [False, 'aek', 'too'] + >>> print(kv.check_aek_too(["เอง", "เอ่ง", "เอ้ง"])) # doctest: +SKIP + >>> # -> [False, 'aek', 'too'] ^^^^^^^^^^ # ใช้ List ได้เหมือนกัน """ if isinstance(text, list): return [self.check_aek_too(t, dead_syllable_as_aek) for t in text] # type: ignore[misc] @@ -657,10 +842,9 @@ def check_aek_too( if not isinstance(text, str): raise TypeError("text must be str or iterable list[str]") - word_characters = [*text] - if "่" in word_characters and "้" not in word_characters: + if "่" in text and "้" not in text: return "aek" - elif "้" in word_characters and "่" not in word_characters: + elif "้" in text and "่" not in text: return "too" if dead_syllable_as_aek and sound_syllable(text) == "dead": return "aek" @@ -669,7 +853,7 @@ def check_aek_too( def handle_karun_sound_silence(self, word: str) -> str: """ - Handle silent sounds in Thai words using '์' character (Karun) + Handle silent sounds in Thai words using '-์' character (Karun) by stripping all characters before the 'Karun' character that should be silenced @@ -677,10 +861,37 @@ def handle_karun_sound_silence(self, word: str) -> str: :return: Thai word with silent consonant stripped :rtype: str """ - sound_silenced = word.endswith("์") - if not sound_silenced: + # Only process if the word ends with Karun (-์) + # [word like โอห์ม which has Karun in the middle will not be processed] + if not word.endswith("์"): return word - # Remove ์ and the silent consonant before it - # การันต์ (์) marks the consonant immediately before it as silent - word = word[:-2] - return word + + # For specific multi-letter Karun silent suffixes + # 'พระลักษมณ์' -> strip 'ษมณ์' (leaving 'ก' for แม่กก) + if word.endswith("กษมณ์"): + return word[:-4] + # 'ลักษณ์', 'ทรลักษณ์' -> strip 'ษณ์' avoid breaking 'สัมภาษณ์' + if word.endswith("กษณ์"): + return word[:-3] + # 'กษัตริย์' -> strip 'ริย์' + if word.endswith("ตริย์"): + return word[:-4] + # 'กาญจน์' -> strip 'จน์' avoid breaking 'โรจน์' + if word.endswith("ญจน์"): + return word[:-3] + + # For 2-Consonant Karun silent suffixes + # ตร์: ศาสตร์, ภาพยนตร์, กาสาวพัสตร์, เวทมนตร์ + # ทร์: จันทร์, บดินทร์, ภูมินทร์, นราธิเบนทร์ + # ดร์: นิรันดร์ + # ฎร์: ราษฎร์, สุราษฎร์ + if word.endswith(("ตร์", "ทร์", "ดร์", "ฎร์")): + return word[:-3] + + # For Standard Karun silent suffixes (1 Consonant + Optional Vowel + Karun) + # สัตว์ (ว์), แพทย์ (ย์), พันธุ์ (พันธุ์), สิทธิ์ (ธิ์) + # Check if there is an upper/lower vowel right before the Karun (พันธุ์, ธิ์) + if len(word) >= 3 and word[-2] in {"ิ", "ี", "ึ", "ื", "ุ", "ู", "ั"}: + return word[:-3] + else: + return word[:-2] diff --git a/tests/core/test_khavee.py b/tests/core/test_khavee.py index 0a89559e6..11d0f546a 100644 --- a/tests/core/test_khavee.py +++ b/tests/core/test_khavee.py @@ -6,20 +6,226 @@ from pythainlp.khavee import KhaveeVerifier +# pylint: disable=protected-access kv = KhaveeVerifier() class KhaveeTestCase(unittest.TestCase): + + """ + Tests for KhaveeVerifier.check_sara, check_marttra, is_sumpus, and check_aek_too methods. + check_klon method is tested in KhaveeCheckKlonExtendedTestCase class in test_khavee_extended.py. + """ + def test_check_sara(self): + """ + Test check_sara with basic, reduced, complex, embedded, + and standalone character vowels. + """ + # Basic Vowels + self.assertEqual(kv.check_sara("ฉะ"), "อะ") + self.assertEqual(kv.check_sara("ค่ะ"), "อะ") + self.assertEqual(kv.check_sara("กระ"), "อะ") + self.assertEqual(kv.check_sara("อรรถ"), "อะ") + self.assertEqual(kv.check_sara("สัตว์"), "อะ") + self.assertEqual(kv.check_sara("พันธุ์"), "อะ") + self.assertEqual(kv.check_sara("พาล"), "อา") + self.assertEqual(kv.check_sara("พลา"), "อา") + self.assertEqual(kv.check_sara("ฆาต"), "อา") + self.assertEqual(kv.check_sara("ซ่า"), "อา") + self.assertEqual(kv.check_sara("นิ"), "อิ") + self.assertEqual(kv.check_sara("มิต"), "อิ") + self.assertEqual(kv.check_sara("บิน"), "อิ") + self.assertEqual(kv.check_sara("ยิ้ม"), "อิ") + self.assertEqual(kv.check_sara("พิมพ์"), "อิ") + self.assertEqual(kv.check_sara("หยิบ"), "อิ") + self.assertEqual(kv.check_sara("ตรี"), "อี") + self.assertEqual(kv.check_sara("ปี"), "อี") + self.assertEqual(kv.check_sara("ปี่"), "อี") + self.assertEqual(kv.check_sara("ฎี"), "อี") # ทฤษฎี + self.assertEqual(kv.check_sara("ตรี"), "อี") + self.assertEqual(kv.check_sara("พลี"), "อี") + self.assertEqual(kv.check_sara("นีย์"), "อี") + self.assertEqual(kv.check_sara("ปรีดิ์"), "อี") + self.assertEqual(kv.check_sara("ตรึก"), "อึ") + self.assertEqual(kv.check_sara("ผึ้ง"), "อึ") + self.assertEqual(kv.check_sara("อึ"), "อึ") + self.assertEqual(kv.check_sara("ซึ้ง"), "อึ") + self.assertEqual(kv.check_sara("ขึ้น"), "อึ") + self.assertEqual(kv.check_sara("หนึ่ง"), "อึ") + self.assertEqual(kv.check_sara("อึ่ง"), "อึ") + self.assertEqual(kv.check_sara("อือ"), "อือ") + self.assertEqual(kv.check_sara("มือ"), "อือ") + self.assertEqual(kv.check_sara("ซื้อ"), "อือ") + self.assertEqual(kv.check_sara("ปรือ"), "อือ") + self.assertEqual(kv.check_sara("ธุ"), "อุ") + self.assertEqual(kv.check_sara("ญุ"), "อุ") + self.assertEqual(kv.check_sara("อุ๊ป"), "อุ") + self.assertEqual(kv.check_sara("สุทธิ์"), "อุ") + self.assertEqual(kv.check_sara("รุฬห์"), "อุ") + self.assertEqual(kv.check_sara("ถู"), "อู") + self.assertEqual(kv.check_sara("หรู"), "อู") + self.assertEqual(kv.check_sara("ธูป"), "อู") + self.assertEqual(kv.check_sara("กู้ด"), "อู") + self.assertEqual(kv.check_sara("กูฏ"), "อู") + self.assertEqual(kv.check_sara("บูรณ์"), "อู") + self.assertEqual(kv.check_sara("กูณฑ์"), "อู") + self.assertEqual(kv.check_sara("สูรย์"), "อู") + self.assertEqual(kv.check_sara("เซะ"), "เอะ") + self.assertEqual(kv.check_sara("เอ"), "เอ") + self.assertEqual(kv.check_sara("เพล"), "เอ") + self.assertEqual(kv.check_sara("เพช"), "เอ") + self.assertEqual(kv.check_sara("เขษม"), "เอ") + self.assertEqual(kv.check_sara("แอะ"), "แอะ") + self.assertEqual(kv.check_sara("และ"), "แอะ") + self.assertEqual(kv.check_sara("แประ"), "แอะ") + self.assertEqual(kv.check_sara("แอ๊ะ"), "แอะ") + self.assertEqual(kv.check_sara("แปร"), "แอ") + self.assertEqual(kv.check_sara("แอร์"), "แอ") + self.assertEqual(kv.check_sara("เรียน"), "เอีย") + self.assertEqual(kv.check_sara("เกียร์"), "เอีย") + self.assertEqual(kv.check_sara("เกียว"), "เอีย") + self.assertEqual(kv.check_sara("เงือก"), "เอือ") + self.assertEqual(kv.check_sara("เอือ"), "เอือ") + self.assertEqual(kv.check_sara("เสือ"), "เอือ") + self.assertEqual(kv.check_sara("เขือ"), "เอือ") + self.assertEqual(kv.check_sara("กลัว"), "อัว") + self.assertEqual(kv.check_sara("โอห์ม"), "โอ") + self.assertEqual(kv.check_sara("โต"), "โอ") + self.assertEqual(kv.check_sara("โดรน"), "โอ") + self.assertEqual(kv.check_sara("ไป"), "ไอ") + self.assertEqual(kv.check_sara("ไตร"), "ไอ") + self.assertEqual(kv.check_sara("ไดร์ฟ"), "ไอ") + self.assertEqual(kv.check_sara("ใบ"), "ไอ") + self.assertEqual(kv.check_sara("ใกล้"), "ไอ") + self.assertEqual(kv.check_sara("ใหญ่"), "ไอ") + self.assertEqual(kv.check_sara("เต้า"), "เอา") + self.assertEqual(kv.check_sara("เป๋า"), "เอา") + self.assertEqual(kv.check_sara("เชาว์"), "เอา") + + # Reduced and Transformed Vowels (สระลดรูป/เปลี่ยนรูป) + self.assertEqual(kv.check_sara("อัน"), "อะ") + self.assertEqual(kv.check_sara("กัน"), "อะ") + self.assertEqual(kv.check_sara("สัญ"), "อะ") + self.assertEqual(kv.check_sara("พวก"), "อัว") + self.assertEqual(kv.check_sara("จวก"), "อัว") + self.assertEqual(kv.check_sara("พรวด"), "อัว") + self.assertEqual(kv.check_sara("คน"), "โอะ") + self.assertEqual(kv.check_sara("คล"), "โอะ") + self.assertEqual(kv.check_sara("โอ๊ะ"), "โอะ") + self.assertEqual(kv.check_sara("พร"), "ออ") + self.assertEqual(kv.check_sara("วร"), "ออ") + self.assertEqual(kv.check_sara("บวร"), "ออ") + self.assertEqual(kv.check_sara("เป็น"), "เอะ") + self.assertEqual(kv.check_sara("เจ็ด"), "เอะ") + self.assertEqual(kv.check_sara("เผด็จ"), "เอะ") + self.assertEqual(kv.check_sara("แข็ง"), "แอะ") + self.assertEqual(kv.check_sara("แจ็ค"), "แอะ") + self.assertEqual(kv.check_sara("แกร็น"), "แอะ") + self.assertEqual(kv.check_sara("เผลอ"), "เออ") + self.assertEqual(kv.check_sara("เลย"), "เออ") self.assertEqual(kv.check_sara("เริง"), "เออ") + self.assertEqual(kv.check_sara("เดิน"), "เออ") + self.assertEqual(kv.check_sara("เกิด"), "เออ") + self.assertEqual(kv.check_sara("ล็อก"), "เอาะ") + self.assertEqual(kv.check_sara("อ็อก"), "เอาะ") + self.assertEqual(kv.check_sara("ก็"), "เอาะ") + + # Complex compound and hidden vowels + self.assertEqual(kv.check_sara("ภูมิ"), "อู") # ภูมิใจ (ไม่ใช่ ภู-มิ) + self.assertEqual(kv.check_sara("เกียรติ"), "เอีย") + self.assertEqual(kv.check_sara("เกตุ"), "เอ") + self.assertEqual(kv.check_sara("เมรุ"), "เอ") + self.assertEqual(kv.check_sara("เหตุ"), "เอ") + self.assertEqual(kv.check_sara("ชาติ"), "อา") + self.assertEqual(kv.check_sara("ญาติ"), "อา") + self.assertEqual(kv.check_sara("ธาตุ"), "อา") + self.assertEqual(kv.check_sara("พยาธิ"), "อา") + self.assertEqual(kv.check_sara("วัติ"), "อะ") # ประวัติ + self.assertEqual(kv.check_sara("พรรดิ"), "อะ") # จักรพรรดิ + self.assertEqual(kv.check_sara("วรรดิ"), "อะ") # จักรวรรดิ + self.assertEqual(kv.check_sara("สมมุติ"), "อุ") + + self.assertEqual(kv.check_sara("กอปร"), "ออ") # กอปร อ่านว่า กอบ + self.assertEqual(kv.check_sara("บวร"), "ออ") + self.assertEqual(kv.check_sara("อมร"), "ออ") + self.assertEqual(kv.check_sara("นคร"), "ออ") + self.assertEqual(kv.check_sara("กร"), "ออ") + self.assertEqual(kv.check_sara("จร"), "ออ") + self.assertEqual(kv.check_sara("พร"), "ออ") + self.assertEqual(kv.check_sara("ออ"), "ออ") + self.assertEqual(kv.check_sara("ขอ"), "ออ") + self.assertEqual(kv.check_sara("งอ"), "ออ") + self.assertEqual(kv.check_sara("กรม"), "โอะ") + self.assertEqual(kv.check_sara("อต"), "โอะ") + self.assertEqual(kv.check_sara("อล"), "โอะ") + self.assertEqual(kv.check_sara("ยศ"), "โอะ") + self.assertEqual(kv.check_sara("โต๊ะ"), "โอะ") + self.assertEqual(kv.check_sara("เร็จ"), "เอะ") + self.assertEqual(kv.check_sara("แข็ง"), "แอะ") + self.assertEqual(kv.check_sara("เตลิด"), "เออ") + self.assertEqual(kv.check_sara("เหม่อ"), "เออ") + self.assertEqual(kv.check_sara("เนย"), "เออ") + self.assertEqual(kv.check_sara("เขนย"), "เออ") + self.assertEqual(kv.check_sara("เพนียด"), "เอีย") + self.assertEqual(kv.check_sara("เกลี้ยง"), "เอีย") + self.assertEqual(kv.check_sara("อวก"), "อัว") + self.assertEqual(kv.check_sara("ควร"), "อัว") + self.assertEqual(kv.check_sara("เกลือ"), "เอือ") + self.assertEqual(kv.check_sara("เรื่อง"), "เอือ") + self.assertEqual(kv.check_sara("ธรรม"), "อำ") + self.assertEqual(kv.check_sara("จำ"), "อำ") + self.assertEqual(kv.check_sara("ผล็อย"), "เอาะ") + + # Vowels embedded with Karun (testing correct truncation before check) + self.assertEqual(kv.check_sara("จันทร์"), "อะ") + self.assertEqual(kv.check_sara("กษัตริย์"), "อะ") + self.assertEqual(kv.check_sara("ลักษมณ์"), "อะ") + self.assertEqual(kv.check_sara("ศาสตร์"), "อา") + self.assertEqual(kv.check_sara("สินธุ์"), "อิ") + self.assertEqual(kv.check_sara("ฟิล์ม"), "อิ") + self.assertEqual(kv.check_sara("ทรีย์"), "อี") + self.assertEqual(kv.check_sara("กอล์ฟ"), "ออ") + self.assertEqual(kv.check_sara("เฮิรตซ์"), "เออ") + + # Standalone Character Vowels + self.assertEqual(kv.check_sara("อ"), "อะ") + self.assertEqual(kv.check_sara("ณ"), "อะ") + self.assertEqual(kv.check_sara("ธ"), "อะ") + self.assertEqual(kv.check_sara("พณ"), "อะ") + self.assertEqual(kv.check_sara("บ"), "ออ") + self.assertEqual(kv.check_sara("บ่"), "ออ") + + # ฤ / ฦ Phonemic Rules + self.assertEqual(kv.check_sara("ฤทธิ์"), "อิ") + self.assertEqual(kv.check_sara("กฤษ"), "อิ") + self.assertEqual(kv.check_sara("กฤษณ์"), "อิ") + self.assertEqual(kv.check_sara("ทฤษ"), "อิ") # ทฤษฎี + self.assertEqual(kv.check_sara("ฤกษ์"), "เออ") + self.assertEqual(kv.check_sara("พฤษ"), "อึ") + self.assertEqual(kv.check_sara("พฤติ"), "อึ") + self.assertEqual(kv.check_sara("ฤดู"), "อึ") + self.assertEqual(kv.check_sara("ฤา"), "อือ") + self.assertEqual(kv.check_sara("ฤๅ"), "อือ") + self.assertEqual(kv.check_sara("ฦา"), "อือ") + self.assertEqual(kv.check_sara("ฦๅ"), "อือ") def test_check_marttra(self): + """Test check_marttra for various final consonant patterns.""" self.assertEqual(kv.check_marttra("ปลิง"), "กง") self.assertEqual(kv.check_marttra("ยูง"), "กง") self.assertEqual(kv.check_marttra("กล่อง"), "กง") self.assertEqual(kv.check_marttra("สอง"), "กง") self.assertEqual(kv.check_marttra("เอ็ง"), "กง") self.assertEqual(kv.check_marttra("งง"), "กง") + self.assertEqual(kv.check_marttra("ผึ้ง"), "กง") + self.assertEqual(kv.check_marttra("ซึ้ง"), "กง") + self.assertEqual(kv.check_marttra("หนึ่ง"), "กง") + self.assertEqual(kv.check_marttra("อึ่ง"), "กง") + self.assertEqual(kv.check_marttra("แข็ง"), "กง") + self.assertEqual(kv.check_marttra("เริง"), "กง") + self.assertEqual(kv.check_marttra("เกลี้ยง"), "กง") + self.assertEqual(kv.check_marttra("เรื่อง"), "กง") self.assertEqual(kv.check_marttra("ลม"), "กม") self.assertEqual(kv.check_marttra("เฉลิม"), "กม") @@ -27,8 +233,14 @@ def test_check_marttra(self): self.assertEqual(kv.check_marttra("จาม"), "กม") self.assertEqual(kv.check_marttra("ยิ้ม"), "กม") self.assertEqual(kv.check_marttra("เกม"), "กม") - self.assertEqual(kv.check_marttra("ขำ"), "กม") self.assertEqual(kv.check_marttra("รมย์"), "กม") + self.assertEqual(kv.check_marttra("พิมพ์"), "กม") + self.assertEqual(kv.check_marttra("เขษม"), "กม") + self.assertEqual(kv.check_marttra("ภูมิ"), "กม") + self.assertEqual(kv.check_marttra("กรม"), "กม") + self.assertEqual(kv.check_marttra("ธรรม"), "กม") + self.assertEqual(kv.check_marttra("ฟิล์ม"), "กม") + self.assertEqual(kv.check_marttra("โอห์ม"), "กม") self.assertEqual(kv.check_marttra("สวย"), "เกย") self.assertEqual(kv.check_marttra("โปรย"), "เกย") @@ -36,30 +248,95 @@ def test_check_marttra(self): self.assertEqual(kv.check_marttra("คอย"), "เกย") self.assertEqual(kv.check_marttra("ง่าย"), "เกย") self.assertEqual(kv.check_marttra("ทัย"), "เกย") - self.assertEqual(kv.check_marttra("ไทย"), "เกย") - self.assertEqual(kv.check_marttra("ไกล"), "เกย") - self.assertEqual(kv.check_marttra("ใกล้"), "เกย") + self.assertEqual(kv.check_marttra("เลื่อย"), "เกย") + self.assertEqual(kv.check_marttra("เปื่อย"), "เกย") + self.assertEqual(kv.check_marttra("เฉื่อย"), "เกย") + self.assertEqual(kv.check_marttra("เหนื่อย"), "เกย") + self.assertEqual(kv.check_marttra("เลย"), "เกย") + self.assertEqual(kv.check_marttra("เขนย"), "เกย") + self.assertEqual(kv.check_marttra("ผล็อย"), "เกย") self.assertEqual(kv.check_marttra("สาว"), "เกอว") self.assertEqual(kv.check_marttra("นิ้ว"), "เกอว") self.assertEqual(kv.check_marttra("แมว"), "เกอว") self.assertEqual(kv.check_marttra("ดาว"), "เกอว") self.assertEqual(kv.check_marttra("แก้ว"), "เกอว") + self.assertEqual(kv.check_marttra("เกียว"), "เกอว") + self.assertEqual(kv.check_marttra("เหว"), "เกอว") self.assertEqual(kv.check_marttra("บก"), "กก") self.assertEqual(kv.check_marttra("โรค"), "กก") self.assertEqual(kv.check_marttra("ลาก"), "กก") self.assertEqual(kv.check_marttra("นัข"), "กก") self.assertEqual(kv.check_marttra("จักร"), "กก") + self.assertEqual(kv.check_marttra("สมัคร"), "กก") + self.assertEqual(kv.check_marttra("ตรึก"), "กก") + self.assertEqual(kv.check_marttra("เงือก"), "กก") + self.assertEqual(kv.check_marttra("พวก"), "กก") + self.assertEqual(kv.check_marttra("จวก"), "กก") + self.assertEqual(kv.check_marttra("แจ็ค"), "กก") + self.assertEqual(kv.check_marttra("ล็อก"), "กก") + self.assertEqual(kv.check_marttra("อ็อก"), "กก") + self.assertEqual(kv.check_marttra("อวก"), "กก") + self.assertEqual(kv.check_marttra("ฤกษ์"), "กก") + self.assertEqual(kv.check_marttra("ศุกร์"), "กก") + self.assertEqual(kv.check_marttra("ลักษมณ์"), "กก") self.assertEqual(kv.check_marttra("จด"), "กด") self.assertEqual(kv.check_marttra("ตรวจ"), "กด") self.assertEqual(kv.check_marttra("เสริฐ"), "กด") + self.assertEqual(kv.check_marttra("สมุทร"), "กด") + self.assertEqual(kv.check_marttra("ภัทร"), "กด") self.assertEqual(kv.check_marttra("บุตร"), "กด") self.assertEqual(kv.check_marttra("ตรุษ"), "กด") self.assertEqual(kv.check_marttra("มืด"), "กด") + self.assertEqual(kv.check_marttra("ชาติ"), "กด") + self.assertEqual(kv.check_marttra("เกียรติ"), "กด") + self.assertEqual(kv.check_marttra("วรรดิ"), "กด") + self.assertEqual(kv.check_marttra("สมมุติ"), "กด") + self.assertEqual(kv.check_marttra("อรรถ"), "กด") + self.assertEqual(kv.check_marttra("ฆาต"), "กด") + self.assertEqual(kv.check_marttra("มิต"), "กด") + self.assertEqual(kv.check_marttra("สุทธิ์"), "กด") + self.assertEqual(kv.check_marttra("กู้ด"), "กด") + self.assertEqual(kv.check_marttra("กูฏ"), "กด") + self.assertEqual(kv.check_marttra("เพช"), "กด") + self.assertEqual(kv.check_marttra("เจ็ด"), "กด") + self.assertEqual(kv.check_marttra("เผด็จ"), "กด") + self.assertEqual(kv.check_marttra("เกิด"), "กด") + self.assertEqual(kv.check_marttra("เกตุ"), "กด") + self.assertEqual(kv.check_marttra("เหตุ"), "กด") + self.assertEqual(kv.check_marttra("ญาติ"), "กด") + self.assertEqual(kv.check_marttra("ธาตุ"), "กด") + self.assertEqual(kv.check_marttra("พยาธิ"), "กด") + self.assertEqual(kv.check_marttra("วัติ"), "กด") + self.assertEqual(kv.check_marttra("พรรดิ"), "กด") + self.assertEqual(kv.check_marttra("อต"), "กด") + self.assertEqual(kv.check_marttra("ยศ"), "กด") + self.assertEqual(kv.check_marttra("เร็จ"), "กด") + self.assertEqual(kv.check_marttra("เตลิด"), "กด") + self.assertEqual(kv.check_marttra("เพนียด"), "กด") + self.assertEqual(kv.check_marttra("สัตว์"), "กด") + self.assertEqual(kv.check_marttra("กษัตริย์"), "กด") + self.assertEqual(kv.check_marttra("ศาสตร์"), "กด") + self.assertEqual(kv.check_marttra("เฮิรตซ์"), "กด") self.assertEqual(kv.check_marttra("โยชน์"), "กด") - + self.assertEqual(kv.check_marttra("กฤษณ์"), "กด") + self.assertEqual(kv.check_marttra("ฤทธิ์"), "กด") + self.assertEqual(kv.check_marttra("กฤษ"), "กด") + self.assertEqual(kv.check_marttra("ทฤษ"), "กด") + + self.assertEqual(kv.check_marttra("เพล"), "กน") + self.assertEqual(kv.check_marttra("สุนทร"), "กน") + self.assertEqual(kv.check_marttra("มังกร"), "กน") + self.assertEqual(kv.check_marttra("สาคร"), "กน") + self.assertEqual(kv.check_marttra("พล"), "กน") + self.assertEqual(kv.check_marttra("กร"), "กน") + self.assertEqual(kv.check_marttra("จร"), "กน") + self.assertEqual(kv.check_marttra("พร"), "กน") + self.assertEqual(kv.check_marttra("บวร"), "กน") + self.assertEqual(kv.check_marttra("อมร"), "กน") + self.assertEqual(kv.check_marttra("นคร"), "กน") self.assertEqual(kv.check_marttra("มึน"), "กน") self.assertEqual(kv.check_marttra("ร้าน"), "กน") self.assertEqual(kv.check_marttra("ขนุน"), "กน") @@ -67,10 +344,34 @@ def test_check_marttra(self): self.assertEqual(kv.check_marttra("ทมิฬ"), "กน") self.assertEqual(kv.check_marttra("ซีน"), "กน") self.assertEqual(kv.check_marttra("บรร"), "กน") - self.assertEqual(kv.check_marttra("กร"), "กน") self.assertEqual(kv.check_marttra("เณร"), "กน") self.assertEqual(kv.check_marttra("ยนต์"), "กน") self.assertEqual(kv.check_marttra("กรรณ"), "กน") + self.assertEqual(kv.check_marttra("พาล"), "กน") + self.assertEqual(kv.check_marttra("พาน"), "กน") + self.assertEqual(kv.check_marttra("บิน"), "กน") + self.assertEqual(kv.check_marttra("ขึ้น"), "กน") + self.assertEqual(kv.check_marttra("รุฬห์"), "กน") + self.assertEqual(kv.check_marttra("บูรณ์"), "กน") + self.assertEqual(kv.check_marttra("กูณฑ์"), "กน") + self.assertEqual(kv.check_marttra("สูรย์"), "กน") + self.assertEqual(kv.check_marttra("เรียน"), "กน") + self.assertEqual(kv.check_marttra("อัน"), "กน") + self.assertEqual(kv.check_marttra("กัน"), "กน") + self.assertEqual(kv.check_marttra("สัญ"), "กน") + self.assertEqual(kv.check_marttra("คล"), "กน") + self.assertEqual(kv.check_marttra("พร"), "กน") + self.assertEqual(kv.check_marttra("วร"), "กน") + self.assertEqual(kv.check_marttra("บวร"), "กน") + self.assertEqual(kv.check_marttra("เป็น"), "กน") + self.assertEqual(kv.check_marttra("แกร็น"), "กน") + self.assertEqual(kv.check_marttra("เดิน"), "กน") + self.assertEqual(kv.check_marttra("เมรุ"), "กน") + self.assertEqual(kv.check_marttra("อล"), "กน") + self.assertEqual(kv.check_marttra("ควร"), "กน") + self.assertEqual(kv.check_marttra("จันทร์"), "กน") + self.assertEqual(kv.check_marttra("พันธุ์"), "กน") + self.assertEqual(kv.check_marttra("สินธุ์"), "กน") self.assertEqual(kv.check_marttra("ชอบ"), "กบ") self.assertEqual(kv.check_marttra("ภาพ"), "กบ") @@ -78,39 +379,193 @@ def test_check_marttra(self): self.assertEqual(kv.check_marttra("รูป"), "กบ") self.assertEqual(kv.check_marttra("เวฟ"), "กบ") self.assertEqual(kv.check_marttra("โลพ"), "กบ") - + self.assertEqual(kv.check_marttra("หยิบ"), "กบ") + self.assertEqual(kv.check_marttra("อุ๊ป"), "กบ") + self.assertEqual(kv.check_marttra("ธูป"), "กบ") + self.assertEqual(kv.check_marttra("กอล์ฟ"), "กบ") + self.assertEqual(kv.check_marttra("กอปร"), "กบ") # กอปร อ่านว่า กอบ + + self.assertEqual(kv.check_marttra("อะ"), "กา") + self.assertEqual(kv.check_marttra("อา"), "กา") + self.assertEqual(kv.check_marttra("อิ"), "กา") + self.assertEqual(kv.check_marttra("อี"), "กา") + self.assertEqual(kv.check_marttra("อึ"), "กา") + self.assertEqual(kv.check_marttra("อือ"), "กา") + self.assertEqual(kv.check_marttra("อุ"), "กา") + self.assertEqual(kv.check_marttra("อู"), "กา") + self.assertEqual(kv.check_marttra("เอะ"), "กา") + self.assertEqual(kv.check_marttra("เอ"), "กา") + self.assertEqual(kv.check_marttra("แอะ"), "กา") + self.assertEqual(kv.check_marttra("แอ"), "กา") + self.assertEqual(kv.check_marttra("โอะ"), "กา") + self.assertEqual(kv.check_marttra("โอ"), "กา") + self.assertEqual(kv.check_marttra("เอาะ"), "กา") + self.assertEqual(kv.check_marttra("ออ"), "กา") + self.assertEqual(kv.check_marttra("เอาะ"), "กา") + self.assertEqual(kv.check_marttra("เออ"), "กา") + self.assertEqual(kv.check_marttra("เอียะ"), "กา") + self.assertEqual(kv.check_marttra("เอีย"), "กา") + self.assertEqual(kv.check_marttra("เอือะ"), "กา") + self.assertEqual(kv.check_marttra("เอือ"), "กา") + self.assertEqual(kv.check_marttra("อัวะ"), "กา") + self.assertEqual(kv.check_marttra("อัว"), "กา") + self.assertEqual(kv.check_marttra("อำ"), "กา") + self.assertEqual(kv.check_marttra("ไอ"), "กา") + self.assertEqual(kv.check_marttra("ใอ"), "กา") + self.assertEqual(kv.check_marttra("เอา"), "กา") + self.assertEqual(kv.check_marttra("จะ"), "กา") + self.assertEqual(kv.check_marttra("ผี"), "กา") self.assertEqual(kv.check_marttra("ปลา"), "กา") self.assertEqual(kv.check_marttra("งู"), "กา") self.assertEqual(kv.check_marttra("หมู"), "กา") self.assertEqual(kv.check_marttra("มือ"), "กา") self.assertEqual(kv.check_marttra("ล้อ"), "กา") + self.assertEqual(kv.check_marttra("เมา"), "กา") + self.assertEqual(kv.check_marttra("เหล้า"), "กา") + self.assertEqual(kv.check_marttra("ฉะ"), "กา") + self.assertEqual(kv.check_marttra("ค่ะ"), "กา") + self.assertEqual(kv.check_marttra("กระ"), "กา") + self.assertEqual(kv.check_marttra("พลา"), "กา") + self.assertEqual(kv.check_marttra("ซ่า"), "กา") + self.assertEqual(kv.check_marttra("นิ"), "กา") + self.assertEqual(kv.check_marttra("ตริ"), "กา") + self.assertEqual(kv.check_marttra("ปี"), "กา") + self.assertEqual(kv.check_marttra("ปี่"), "กา") + self.assertEqual(kv.check_marttra("ฎี"), "กา") + self.assertEqual(kv.check_marttra("ตรี"), "กา") + self.assertEqual(kv.check_marttra("พลี"), "กา") + self.assertEqual(kv.check_marttra("แคว"), "กา") # แม่น้ำแคว (ควบกล้ำ คว) + self.assertEqual(kv.check_marttra("ซื้อ"), "กา") + self.assertEqual(kv.check_marttra("ปรือ"), "กา") + self.assertEqual(kv.check_marttra("ธุ"), "กา") + self.assertEqual(kv.check_marttra("ญุ"), "กา") + self.assertEqual(kv.check_marttra("ถู"), "กา") + self.assertEqual(kv.check_marttra("หรู"), "กา") + self.assertEqual(kv.check_marttra("เซะ"), "กา") + self.assertEqual(kv.check_marttra("เฉ"), "กา") + self.assertEqual(kv.check_marttra("และ"), "กา") + self.assertEqual(kv.check_marttra("แประ"), "กา") + self.assertEqual(kv.check_marttra("แอ๊ะ"), "กา") + self.assertEqual(kv.check_marttra("แอร์"), "กา") + self.assertEqual(kv.check_marttra("เกียร์"), "กา") + self.assertEqual(kv.check_marttra("เอือ"), "กา") + self.assertEqual(kv.check_marttra("เสือ"), "กา") + self.assertEqual(kv.check_marttra("เขือ"), "กา") + self.assertEqual(kv.check_marttra("กลัว"), "กา") + self.assertEqual(kv.check_marttra("ก็"), "กา") + self.assertEqual(kv.check_marttra("ขอ"), "กา") + self.assertEqual(kv.check_marttra("งอ"), "กา") + self.assertEqual(kv.check_marttra("ขำ"), "กา") + self.assertEqual(kv.check_marttra("จำ"), "กา") + self.assertEqual(kv.check_marttra("โต๊ะ"), "กา") + self.assertEqual(kv.check_marttra("เผลอ"), "กา") + self.assertEqual(kv.check_marttra("เหม่อ"), "กา") + self.assertEqual(kv.check_marttra("เกลือ"), "กา") + self.assertEqual(kv.check_marttra("ไป"), "กา") + self.assertEqual(kv.check_marttra("น้ำ"), "กา") + self.assertEqual(kv.check_marttra("เรือ"), "กา") + self.assertEqual(kv.check_marttra("ตัว"), "กา") + self.assertEqual(kv.check_marttra("ครัว"), "กา") + self.assertEqual(kv.check_marttra("ทรีย์"), "กา") + self.assertEqual(kv.check_marttra("ปรีดิ์"), "กา") + self.assertEqual(kv.check_marttra("นีย์"), "กา") + self.assertEqual(kv.check_marttra("อัชฌ"), "กา") + self.assertEqual(kv.check_marttra("มัจฉ"), "กา") + + # Fake Finals (คำควบกล้า, คำที่มีพยัญชนะ/สระไม่ออกเสียง) mapping to open syllables + self.assertEqual(kv.check_marttra("ไทย"), "กา") + self.assertEqual(kv.check_marttra("ไกล"), "กา") + self.assertEqual(kv.check_marttra("ใกล้"), "กา") + self.assertEqual(kv.check_marttra("เสีย"), "กา") + self.assertEqual(kv.check_marttra("เปล"), "กา") + self.assertEqual(kv.check_marttra("ไกว"), "กา") + self.assertEqual(kv.check_marttra("โปร"), "กา") + self.assertEqual(kv.check_marttra("โปล"), "กา") + self.assertEqual(kv.check_marttra("แปร"), "กา") + self.assertEqual(kv.check_marttra("ไฟล์"), "กา") + + # Standalone Characters mapping to open syllables + self.assertEqual(kv.check_marttra("ธ"), "กา") + self.assertEqual(kv.check_marttra("ณ"), "กา") + self.assertEqual(kv.check_marttra("พณ"), "กา") + self.assertEqual(kv.check_marttra("บ"), "กา") + self.assertEqual(kv.check_marttra("บ่"), "กา") + self.assertEqual(kv.check_marttra("อ"), "กา") + + # ฤ / ฦ + self.assertEqual(kv.check_marttra("ฤ"), "กา") + self.assertEqual(kv.check_marttra("ฦ"), "กา") + self.assertEqual(kv.check_marttra("ฤา"), "กา") + self.assertEqual(kv.check_marttra("ฤๅ"), "กา") + self.assertEqual(kv.check_marttra("ฦา"), "กา") + self.assertEqual(kv.check_marttra("ฦๅ"), "กา") def test_is_sumpus(self): - self.assertTrue(kv.is_sumpus("สรร", "อัน")) + """Test is_sumpus for checking structural equivalence of Thai words.""" self.assertFalse(kv.is_sumpus("สรร", "แมว")) - - def test_check_klon(self): - self.assertEqual( - kv.check_klon( - "ฉันชื่อหมูกรอบ ฉันชอบกินไก่ แล้วก็วิ่งไล่ หมาชื่อนํ้าทอง \ - ลคคนเก่ง เอ๋งเอ๋งคะนอง มีคนจับจอง เขาชื่อน้องเธียร", - k_type=4, - ), - "The poem is correct according to the principle.", - ) - self.assertEqual( - kv.check_klon( - "ฉันชื่อหมูกรอบ ฉันชอบกินไก่ แล้วก็วิ่งไล่ หมาชื่อนํ้าทอง \ - ลคคนเก่ง เอ๋งเอ๋งเสียงหมา มีคนจับจอง เขาชื่อน้องเธียร", - k_type=4, - ), - [ - "Can't find rhyme between paragraphs ('หมา', 'จอง') in paragraph 2", - "Can't find rhyme between paragraphs ('หมา', 'ทอง') in paragraph 2", - ], - ) + self.assertFalse(kv.is_sumpus("กลัว", "ไกล")) + self.assertFalse(kv.is_sumpus("ตัว", "ตะ")) + self.assertFalse(kv.is_sumpus("ตัว", "ไต")) + self.assertFalse(kv.is_sumpus("สาว", "อา")) + self.assertFalse(kv.is_sumpus("เอว", "อา")) + self.assertFalse(kv.is_sumpus("อัว", "อา")) + self.assertFalse(kv.is_sumpus("บวก", "อัว")) + self.assertFalse(kv.is_sumpus("สวม", "อัว")) + self.assertFalse(kv.is_sumpus("ชัวร์", "ชัน")) + self.assertFalse(kv.is_sumpus("เลว", "เร็ว")) + self.assertFalse(kv.is_sumpus("ฤทธิ์", "ฤกษ์")) # ฤทธิ์ = ริด, ฤกษ์ = เริก + self.assertFalse(kv.is_sumpus("ฤทธิ์", "ลึด")) # ฤทธิ์ = ริด != ลึด + self.assertFalse(kv.is_sumpus("ฤกษ์", "ลึก")) # ฤกษ์ = เริก != ลึก + self.assertFalse(kv.is_sumpus("โหว่", "โถ่ว")) # แม่ก กา vs แม่เกอว + + self.assertTrue(kv.is_sumpus("สมุทร", "สมุด")) + self.assertTrue(kv.is_sumpus("ภัทร", "พัด")) + self.assertTrue(kv.is_sumpus("สมัคร", "นัก")) + self.assertTrue(kv.is_sumpus("จักร", "พรรค")) + self.assertTrue(kv.is_sumpus("เพชร", "เพด")) + self.assertTrue(kv.is_sumpus("เขว", "เอ")) + self.assertTrue(kv.is_sumpus("เขว", "เหว่")) + self.assertTrue(kv.is_sumpus("เหว", "เอว")) + self.assertTrue(kv.is_sumpus("โหว่", "โถ")) + self.assertTrue(kv.is_sumpus("ครัว", "ตัว")) + self.assertTrue(kv.is_sumpus("สรร", "อัน")) + self.assertTrue(kv.is_sumpus("ธ", "ณ")) + self.assertTrue(kv.is_sumpus("ธ", "ทะ")) + self.assertTrue(kv.is_sumpus("ศาสตร์", "มารถ")) + self.assertTrue(kv.is_sumpus("แตร", "แปร")) # แม่ก กา + self.assertTrue(kv.is_sumpus("แหล่", "แต่")) # เหลือแหล่ + self.assertTrue(kv.is_sumpus("แหน", "แกน")) # แม่ กน + + # Structural equivalence logic & Normalization + self.assertTrue(kv.is_sumpus("มอญ", "อมร")) + self.assertTrue(kv.is_sumpus("นอน", "บวร")) + self.assertTrue(kv.is_sumpus("นอน", "กร")) + self.assertTrue(kv.is_sumpus("บ้าน", "พาล")) + self.assertTrue(kv.is_sumpus("ทำ", "จำ")) + self.assertTrue(kv.is_sumpus("ทำ", "กัม")) + self.assertTrue(kv.is_sumpus("กรรม", "ธรรม")) + self.assertTrue(kv.is_sumpus("ธรรม", "สัม")) + self.assertTrue(kv.is_sumpus("ธรรม", "จำ")) + self.assertTrue(kv.is_sumpus("กัย", "ไก")) + self.assertTrue(kv.is_sumpus("กัย", "ไกล")) + self.assertTrue(kv.is_sumpus("ใจ", "ไทย")) + self.assertTrue(kv.is_sumpus("ใจ", "จัย")) + self.assertTrue(kv.is_sumpus("ไกว", "ใด")) + self.assertTrue(kv.is_sumpus("ไกว", "ใคร")) + self.assertTrue(kv.is_sumpus("เลย", "เกย")) + self.assertTrue(kv.is_sumpus("พวก", "จวก")) + self.assertTrue(kv.is_sumpus("พันธุ์", "จันทร์")) + self.assertTrue(kv.is_sumpus("ฤทธิ์", "กิด")) + self.assertTrue(kv.is_sumpus("ฤกษ์", "เริก")) + self.assertTrue(kv.is_sumpus("พฤษ", "พรึด")) + self.assertTrue(kv.is_sumpus("พฤก", "พรึก")) + self.assertTrue(kv.is_sumpus("ฤ", "รึ")) + + # Verify strict phonemic constraints are maintained + self.assertFalse(kv.is_sumpus("ก็", "ก้อ")) # เอาะ vs ออ def test_check_aek_too(self): + """Test check_aek_too for Thai tone mark detection.""" self.assertFalse(kv.check_aek_too("ไกด์")) self.assertEqual(kv.check_aek_too("ไก่"), "aek") self.assertEqual(kv.check_aek_too("ไก้"), "too") @@ -120,141 +575,211 @@ def test_check_aek_too(self): class KhaveeCheckKaruLahuTestCase(unittest.TestCase): - """Tests for KhaveeVerifier.check_karu_lahu""" def setUp(self): + """Set up test fixtures.""" self.kv = KhaveeVerifier() - def test_dead_syllable_is_karu(self): - self.assertEqual(self.kv.check_karu_lahu("กด"), "karu") - - def test_long_live_syllable_is_karu(self): - self.assertEqual(self.kv.check_karu_lahu("กา"), "karu") - - def test_live_syllable_with_final_consonant_is_karu(self): - self.assertEqual(self.kv.check_karu_lahu("กาน"), "karu") - - def test_bo_mai_ek_is_lahu(self): - self.assertEqual(self.kv.check_karu_lahu("บ่"), "lahu") - - def test_no_short_word_is_lahu(self): - self.assertEqual(self.kv.check_karu_lahu("ณ"), "lahu") - - def test_tho_short_word_is_lahu(self): - self.assertEqual(self.kv.check_karu_lahu("ธ"), "lahu") - - def test_ko_mai_is_lahu(self): - self.assertEqual(self.kv.check_karu_lahu("ก็"), "lahu") + def test_karu_words(self): + """Test that all specified heavy syllables (Karu) are correctly identified.""" + karu_words = [ + "กด", "กา", "กาน", + "ใน", "นา", "มี", "ปู", "ตา", "ดำ", "วัว", "ลาก", "ไถ", "พุทธ", + "สันดาน", "มูล", "หมองมัว", "ยั่ว", "เอว", "เจ็บ", "โสภา", "ศาลา", + "วัด", "แม่", "ข้าวสาร", "ดวงใจ", "ไฉไล", "เขลา", "เนื้อ", "เต้น", + "ทั่ว", "ร่าง", "สั่น", "ไหว", "ช่อฟ้า", "หัว", "อีกา", "สาม", + "ฤาษี", "คาวี", "วับวาบ", "ญาณ", "เรา", "ครอง", "แผ่นดิน", "โดย", + "ธรรม", "พรรณ", "เย้ยหยัน", "ดุก", "โดด", "โลด", "หยอย", "น้ำ", "พร่ำ" + ] + + for word in karu_words: + with self.subTest(word=word): + self.assertEqual(self.kv.check_karu_lahu(word), "karu") + + def test_lahu_words(self): + """Test that all specified light syllables (Lahu) are correctly identified.""" + lahu_words = [ + "บ", "บ่", "ณ", "ธ", "ก็", "ฤ", "ฦ", + "ชะ", "กระ", "ยะ", "พะ", "ระ", "ละ", "ประ", "ฉะ", + "มติ", "กะปิ", "กะทิ", "กะทะ", "ฐิติ", "อุระ", "อมตะ", + "มิ", "จะ", "เกะกะ", "ทะลุ", "รวิ", "วจนะ", "ศศิ", + "และ", "สุ", "จิ", "ปุ", "ลิ", "สติ", "พระ", + "ระยะ", "เยาะ", "ธุระ" + ] + + for word in lahu_words: + with self.subTest(word=word): + self.assertEqual(self.kv.check_karu_lahu(word), "lahu") + + def test_invalid_karu_lahu_words(self): + """ + Test that invalid words are not identified as karu or lahu and should return False. + This includes empty strings. + """ + invalid_karu_lahu_words = [ + "" + ] + for word in invalid_karu_lahu_words: + with self.subTest(word=word): + self.assertEqual(self.kv.check_karu_lahu(word), False) class KhaveeHandleKarunTestCase(unittest.TestCase): - """Tests for KhaveeVerifier.handle_karun_sound_silence""" + + """Tests for KhaveeVerifier.handle_karun_sound_silence.""" def setUp(self): + """Set up test fixtures.""" self.kv = KhaveeVerifier() def test_word_without_karun_unchanged(self): + """Test that words without karun are unchanged.""" self.assertEqual(self.kv.handle_karun_sound_silence("คน"), "คน") self.assertEqual(self.kv.handle_karun_sound_silence("กา"), "กา") + self.assertEqual(self.kv.handle_karun_sound_silence(""), "") # empty string unchanged + # internal karun unchanged + self.assertEqual(self.kv.handle_karun_sound_silence("การ์ตูน"), "การ์ตูน") + self.assertEqual(self.kv.handle_karun_sound_silence("โอร์ม"), "โอร์ม") + self.assertEqual(self.kv.handle_karun_sound_silence("กอล์ฟ"), "กอล์ฟ") + self.assertEqual(self.kv.handle_karun_sound_silence("ฟิล์ม"), "ฟิล์ม") + self.assertEqual(self.kv.handle_karun_sound_silence("สตาร์ตอัป"), "สตาร์ตอัป") def test_word_ending_with_karun_stripped(self): - # เกมส์ → drop ์ and the consonant before it (ส) → เกม + """Test that karun and preceding consonant are stripped from end of word.""" + # เกมส์ -> drop ์ and the consonant before it (ส) -> เกม self.assertEqual(self.kv.handle_karun_sound_silence("เกมส์"), "เกม") - - def test_word_ending_with_karun_stripped_2(self): - # รักษ์ → drop ์ + ษ → รัก + # รักษ์ -> drop ์ + ษ -> รัก self.assertEqual(self.kv.handle_karun_sound_silence("รักษ์"), "รัก") + self.assertEqual(self.kv.handle_karun_sound_silence("สัตว์"), "สัต") + + def test_complex_karun_stripped(self): + """ + Test complex karun stripping with single, multi-consonant, + and vowel-embedded patterns. + """ + # Explicit evaluation of single, multi-consonant, and vowel-embedded Karun rules + self.assertEqual(self.kv.handle_karun_sound_silence("จันทร์"), "จัน") + self.assertEqual(self.kv.handle_karun_sound_silence("สิทธิ์"), "สิท") + self.assertEqual(self.kv.handle_karun_sound_silence("กษัตริย์"), "กษัต") + self.assertEqual(self.kv.handle_karun_sound_silence("พระลักษมณ์"), "พระลัก") + self.assertEqual(self.kv.handle_karun_sound_silence("อินทรีย์"), "อินทรี") + self.assertEqual(self.kv.handle_karun_sound_silence("ภาพยนตร์"), "ภาพยน") + self.assertEqual(self.kv.handle_karun_sound_silence("กาสาวพัสตร์"), "กาสาวพัส") + self.assertEqual(self.kv.handle_karun_sound_silence("ไปรษณีย์"), "ไปรษณี") + self.assertEqual(self.kv.handle_karun_sound_silence("สัปดาห์"), "สัปดา") + self.assertEqual(self.kv.handle_karun_sound_silence("เฮิรตซ์"), "เฮิรต") + self.assertEqual(self.kv.handle_karun_sound_silence("วิศวกรรมศาสตร์"), "วิศวกรรมศาส") + self.assertEqual(self.kv.handle_karun_sound_silence("กบินทร์"), "กบิน") + self.assertEqual(self.kv.handle_karun_sound_silence("นราธิเบนทร์"), "นราธิเบน") + self.assertEqual(self.kv.handle_karun_sound_silence("พรหมจรรย์"), "พรหมจรร") + self.assertEqual(self.kv.handle_karun_sound_silence("กรณีย์"), "กรณี") + self.assertEqual(self.kv.handle_karun_sound_silence("รังสิมันตุ์"), "รังสิมัน") + self.assertEqual(self.kv.handle_karun_sound_silence("รามเกียรติ์"), "รามเกียร") + self.assertEqual(self.kv.handle_karun_sound_silence("ทรลักษณ์"), "ทรลัก") + self.assertEqual(self.kv.handle_karun_sound_silence("ธำมรงค์"), "ธำมรง") + self.assertEqual(self.kv.handle_karun_sound_silence("ศัพท์"), "ศัพ") + self.assertEqual(self.kv.handle_karun_sound_silence("ฉันท์"), "ฉัน") + self.assertEqual(self.kv.handle_karun_sound_silence("เจ้าเล่ห์"), "เจ้าเล่") + self.assertEqual(self.kv.handle_karun_sound_silence("สงเคราะห์"), "สงเคราะ") + self.assertEqual(self.kv.handle_karun_sound_silence("ราชทัณฑ์"), "ราชทัณ") + self.assertEqual(self.kv.handle_karun_sound_silence("สวาสดิ์"), "สวาส") + self.assertEqual(self.kv.handle_karun_sound_silence("สุปรีดิ์"), "สุปรี") def test_returns_string(self): + """Test that handle_karun_sound_silence returns a string.""" self.assertIsInstance(self.kv.handle_karun_sound_silence("สวัสดี"), str) +class KhaveeIsTrueFinalTestCase(unittest.TestCase): + + """Tests for internal method KhaveeVerifier._is_true_final.""" + + def setUp(self): + """Set up test fixtures.""" + self.kv = KhaveeVerifier() + + def test_true_finals(self): + """Test identification of true final consonant patterns.""" + self.assertTrue(self.kv._is_true_final("จัย")) + self.assertTrue(self.kv._is_true_final("สมัย")) + self.assertTrue(self.kv._is_true_final("เลื่อย")) + self.assertTrue(self.kv._is_true_final("เปื่อย")) + self.assertTrue(self.kv._is_true_final("เฉื่อย")) + self.assertTrue(self.kv._is_true_final("เหนื่อย")) + + def test_fake_finals(self): + """Test identification of fake final consonant patterns.""" + self.assertFalse(self.kv._is_true_final("ไทย")) + self.assertFalse(self.kv._is_true_final("ใคร")) + self.assertFalse(self.kv._is_true_final("ไกล")) + self.assertFalse(self.kv._is_true_final("ใกล้")) + self.assertFalse(self.kv._is_true_final("เสีย")) + self.assertFalse(self.kv._is_true_final("ไกว")) + self.assertFalse(self.kv._is_true_final("โปร")) + self.assertFalse(self.kv._is_true_final("แปร")) + self.assertFalse(self.kv._is_true_final("เปล")) + self.assertFalse(self.kv._is_true_final("ไฟล์")) + + class KhaveeCheckAekTooEdgeCasesTestCase(unittest.TestCase): - """Edge-case tests for KhaveeVerifier.check_aek_too""" + + """Edge-case tests for KhaveeVerifier.check_aek_too.""" def setUp(self): + """Set up test fixtures.""" self.kv = KhaveeVerifier() def test_non_string_raises_type_error(self): + """Test that non-string input raises TypeError.""" with self.assertRaises(TypeError): self.kv.check_aek_too(123) # type: ignore[arg-type] def test_dead_syllable_as_aek_flag(self): + """Test dead_syllable_as_aek flag behavior.""" self.assertEqual(self.kv.check_aek_too("บท", dead_syllable_as_aek=True), "aek") def test_dead_syllable_without_flag_returns_false(self): + """Test that dead syllables return False when flag is not set.""" self.assertFalse(self.kv.check_aek_too("บท", dead_syllable_as_aek=False)) def test_list_with_non_string_element_raises(self): + """Test that list with non-string element raises TypeError.""" with self.assertRaises(TypeError): self.kv.check_aek_too(["ไก่", 42]) # type: ignore[list-item] def test_both_tone_marks_returns_false(self): + """Test that word with both tone marks returns False.""" # word with both ่ and ้ should return False self.assertFalse(self.kv.check_aek_too("ก่้")) - -class KhaveeCheckKlonExtendedTestCase(unittest.TestCase): - """Tests for check_klon k_type=8 and invalid k_type""" - - def setUp(self): - self.kv = KhaveeVerifier() - - def test_invalid_k_type_returns_error_string(self): - result = self.kv.check_klon("บทกวีทดสอบ", k_type=99) - self.assertIsInstance(result, str) - self.assertIn("Something went wrong", result) - - def test_incomplete_klon4_poem(self): - result = self.kv.check_klon("ฉันชื่อหมูกรอบ", k_type=4) - self.assertIsInstance(result, str) - self.assertIn("does not have 4 complete sentences", result) - - def test_incomplete_klon8_poem(self): - result = self.kv.check_klon("ฉันชื่อหมูกรอบ", k_type=8) - self.assertIsInstance(result, str) - - def test_check_klon8_correct_poem(self): - poem = ( - "ฉันชื่อหมูกรอบ ฉันชอบกินไก่ แล้วก็วิ่งไล่ หมาชื่อนํ้าทอง " - "ลคคนเก่ง เอ๋งเอ๋งคะนอง มีคนจับจอง เขาชื่อน้องเธียร" - ) - self.assertIsNotNone(self.kv.check_klon(poem, k_type=8)) +# Test KhaveeCheckKlonExtendedTestCase is moved to tests/extra/test_khavee_extra.py +# because it use extra dependency "ssg" that is not part of the core test class KhaveeCheckSaraEdgeCasesTestCase(unittest.TestCase): - """Edge-case tests for KhaveeVerifier.check_sara""" + + """Edge-case tests for KhaveeVerifier.check_sara.""" def setUp(self): + """Set up test fixtures.""" self.kv = KhaveeVerifier() def test_bo_mai_ek_returns_oo(self): + """Test that bo mai ek returns ออ vowel.""" self.assertEqual(self.kv.check_sara("บ่"), "ออ") - def test_special_word_เออ(self): + def test_special_word(self): + """Test special vowel combinations.""" self.assertEqual(self.kv.check_sara("เออ"), "เออ") - - def test_special_word_เอ(self): - self.assertEqual(self.kv.check_sara("เอ"), "เอ") - - def test_special_word_เอะ(self): self.assertEqual(self.kv.check_sara("เอะ"), "เอะ") - - def test_special_word_เอา(self): self.assertEqual(self.kv.check_sara("เอา"), "เอา") - - def test_special_word_เอาะ(self): self.assertEqual(self.kv.check_sara("เอาะ"), "เอาะ") - - def test_ru_sara(self): self.assertEqual(self.kv.check_sara("ฤ"), "อึ") + self.assertEqual(self.kv.check_sara("เรือ"), "เอือ") + self.assertIsInstance(self.kv.check_sara("เริง"), str) def test_ruea_sara(self): - # ฤา (ฤ + sara aa U+0E32) → อือ; note: ฤๅ uses lakkhangyao, not sara aa + """Test ฤา and ฤๅ (ru with aa vowel) characters.""" + # ฤา (ฤ + sara า U+0E32) -> อือ; note: ฤๅ uses lakkhangyao, not sara aa self.assertEqual(self.kv.check_sara("ฤา"), "อือ") - - def test_เอือ_sara(self): - self.assertEqual(self.kv.check_sara("เรือ"), "เอือ") - - def test_returns_string(self): - self.assertIsInstance(self.kv.check_sara("เริง"), str) + self.assertEqual(self.kv.check_sara("ฤๅ"), "อือ") diff --git a/tests/extra/__init__.py b/tests/extra/__init__.py index b72414ca3..770fc793d 100644 --- a/tests/extra/__init__.py +++ b/tests/extra/__init__.py @@ -18,6 +18,7 @@ "tests.extra.testx_tokenize", "tests.extra.testx_translate_helpers", "tests.extra.testx_word_vector", + "tests.extra.test_khavee_extended.py" ] diff --git a/tests/extra/test_khavee_extended.py b/tests/extra/test_khavee_extended.py new file mode 100644 index 000000000..f6f76d81d --- /dev/null +++ b/tests/extra/test_khavee_extended.py @@ -0,0 +1,228 @@ +# SPDX-FileCopyrightText: 2016-2026 PyThaiNLP Project +# SPDX-FileType: SOURCE +# SPDX-License-Identifier: Apache-2.0 + +import unittest + +from pythainlp.khavee import KhaveeVerifier + +kv = KhaveeVerifier() + + +class KhaveeCheckKlonExtendedTestCase(unittest.TestCase): + + """Tests for check_klon k_type=8 and invalid k_type.""" + + def setUp(self): + """Set up test fixtures.""" + self.kv = KhaveeVerifier() + + def test_invalid_k_type_returns_error_string(self): + """Test that invalid k_type returns error string.""" + poem = "บทกวีทดสอบ" + result = self.kv.check_klon(poem, k_type=99) + self.assertIsInstance(result, str) + self.assertIn( + result, + "Something went wrong. Make sure you enter it in the correct form " + "(k_type 4 or 8)." + ) + + def test_incomplete_klon4_poem(self): + """Test that incomplete klon4 poem is detected.""" + result = self.kv.check_klon("ฉันชื่อหมูกรอบ", k_type=4) + self.assertIsInstance(result, str) + self.assertIn( + result, + "The poem does not have complete stanzas (บท). " + "A stanza must contain exactly 4 sentences (วรรค)." + ) + + def test_incomplete_klon8_poem(self): + """Test that incomplete klon8 poem is detected.""" + poem = "ฉันชื่อหมูกรอบ" + result = self.kv.check_klon(poem, k_type=8) + self.assertIsInstance(result, str) + self.assertIn( + result, + "The poem does not have complete stanzas (บท). " + "A stanza must contain exactly 4 sentences (วรรค)." + ) + + def test_check_klon4_incorrect_poem(self): + """Test that invalid klon4 poem is detected.""" + poem = ( + "มวลไม้ดอกสวด ระรวยกลิ่นหอม หมู่ผึ้งดมดอม พรั่งพร้อมนานา " + "ผีเสื้อปีกบาง บินกางปีกมา ช่างงามจับตา เริงร่าสุขใจ" + ) + result = self.kv.check_klon(poem, k_type=4) + self.assertIsInstance(result, list) + self.assertEqual( + result, [ + "Rhyme error in Stanza (บทที่) 1: 'สวด' (Wak 1) " + "does not rhyme with ['ระ', 'รวย'] (Wak 2)" + ]) + + def test_check_klon4_incorrect_poem_2(self): + """Test that invalid klon4 poem with wrong inter-stanza rhyme is detected.""" + poem = ( + "มวลไม้ดอกสวด ระรวยกลิ่นหอม หมู่ผึ้งดมดอม พรั่งพร้อมนานะ " + "ผีเสื้อปีกบาง บินกางปีกมา ช่างงามจับตา เริงร่าสุขใจ" + ) + result = self.kv.check_klon(poem, k_type=4) + self.assertIsInstance(result, list) + self.assertEqual( + result, + [ + "Rhyme error in Stanza (บทที่) 1: " + "'สวด' (Wak 1) does not rhyme with ['ระ', 'รวย'] (Wak 2)", + "Inter-stanza rhyme error (ผิดสัมผัสระหว่างบท) between Stanza 1 and 2: " + "'นะ' (Wak 4) does not rhyme with 'มา' (Wak 2)" + ] + ) + + def test_check_klon4_correct_poem(self): + """Test that valid klon4 poem is recognized.""" + poem = ( + "มวลไม้ดอกสวย ระรวยกลิ่นหอม หมู่ผึ้งดมดอม พรั่งพร้อมนานา " + "ผีเสื้อปีกบาง บินกางปีกมา ช่างงามจับตา เริงร่าสุขใจ" + ) + self.assertIsNotNone(self.kv.check_klon(poem, k_type=4)) + result = self.kv.check_klon(poem, k_type=4) + self.assertEqual(result, "The poem is correct according to the principle.") + + def test_check_klon8_correct_poem(self): + """Test that valid klon8 poem is recognized.""" + poem = ( + "มวลไม้ดอกสวยระรวยกลิ่นหอม หมู่ผึ้งดมดอมพรั่งพร้อมนานา " + "ผีเสื้อปีกบางบินกางปีกมา ช่างงามจับตาเริงร่าสุขใจ" + ) + self.assertIsNotNone(self.kv.check_klon(poem, k_type=8)) + result = self.kv.check_klon(poem, k_type=8) + self.assertEqual(result, "The poem is correct according to the principle.") + + def test_check_klon8_correct_poem_2(self): + """Test that another valid klon8 poem is recognized.""" + poem = ( + "แม่รักลูกลูกก็รู้อยู่ว่ารัก คนอื่นสักหมื่นแสนไม่แม้นเหมือน " + "จะกินนอนวอนว่าเมตตาเตือน จะจากเรือนร้างแม่ไปแต่ตัว " + "แม่วันทองของลูกจงกลับบ้าน เขาจะพาลว้าวุ่นแม่ทูนหัว " + "จะก้มหน้าลาไปมิได้กลัว แม่อย่ามัวหมองนักจงหักใจ" + ) + self.assertEqual( + self.kv.check_klon(poem, k_type=8), + "The poem is correct according to the principle." + ) + + def test_check_klon8_correct_poem_3(self): + """Test that third valid klon8 poem is recognized.""" + poem = ( + "นางกอดจูบลูบหลังแล้วสั่งสอน อำนวยพรพลายน้อยละห้อยไห้ " + "พ่อไปดีศรีสวัสดิ์กำจัดภัย จนเติบใหญ่ยิ่งยวดได้บวชเรียน " + "ลูกผู้ชายลายมือนั้นคือยศ เเจ้าจงอตส่าห์ทำสม่ำเสมียน " + "แล้วพาลูกออกมาข้างท่าเกวียน จะจากเจียนใจขาดอนาถใจ" + ) + self.assertEqual( + self.kv.check_klon(poem, k_type=8), + "The poem is correct according to the principle." + ) + + def test_check_klon8_invalid_poem(self): + """Test that invalid klon8 poem with too many words. + (แม่รักลูกลูกก็รู้อยู่ว่ารักมากมาก)""" + poem = ( + "แม่รักลูกลูกก็รู้อยู่ว่ารักมากมาก คนอื่นสักหมื่นแสนไม่แม้นเหมือน " + "จะกินนอนวอนว่าเมตตาเตือน จะจากเรือนร้างแม่ไปแต่ตัว " + "แม่วันทองของลูกจงกลับบ้าน เขาจะพาลว้าวุ่นแม่ทูนหัว " + "จะก้มหน้าลาไปมิได้กลัว แม่อย่ามัวหมองนักจงหักใจ" + ) + result = self.kv.check_klon(poem, k_type=8) + self.assertIsInstance(result, list) + self.assertEqual( + result, + [ + "Stanza (บทที่) 1 Wak 1: Word count exceeds 10: " + "['แม่', 'รัก', 'ลูก', 'ลูก', 'ก็', 'รู้', 'อยู่', 'ว่า', 'รัก', 'มาก', 'มาก']", + "Rhyme error in Stanza (บทที่) 1: 'มาก' (Wak 1) does not rhyme with " + "['คน', 'อื่น', 'สัก', 'หมื่น', 'แสน'] (Wak 2)" + ] + ) + + def test_check_klon8_invalid_poem_2(self): + """Test that invalid klon8 poem with incorrect rhyme is detected.""" + poem = ( + "แม่รักลูกลูกก็รู้อยู่ว่ารักมาก คนอื่นสักหมื่นแสนไม่แม้นเหมือน " + "จะกินนอนวอนว่าเมตตาเตือน จะจากเรือร้างแม่ไปแต่ตัว " + "แม่วันทองของลูกจงกลับบ้าน เขาจะพาลว้าวุ่นแม่ทูนหัว " + "จะก้มหน้าลาไปมิได้กลัว แม่อย่ามัวหมองนักจงหักใจ" + ) + result = self.kv.check_klon(poem, k_type=8) + self.assertIsInstance(result, list) + self.assertEqual( + result, [ + "Rhyme error in Stanza (บทที่) 1: 'มาก' (Wak 1) " + "does not rhyme with ['คน', 'อื่น', 'สัก', 'หมื่น', 'แสน'] (Wak 2)", + "Rhyme error in Stanza (บทที่) 1: 'เตือน' (Wak 3) does not rhyme with " + "['จะ', 'จาก', 'เรือ', 'ร้าง', 'แม่'] (Wak 4)"] + ) + + def test_check_klon8_invalid_poem_3(self): + """Test that invalid klon8 poem with wrong final word is detected.""" + poem = ( + "แม่รักลูกลูกก็รู้อยู่ว่ารัก คนอื่นสักหมื่นแสนไม่แม้นเหมือน " + "จะกินนอนวอนว่าเมตตาเตือด จะจากเรือนร้างแม่ไปแต่ตัว " + "แม่วันทองของลูกจงกลับบ้าน เขาจะพาลว้าวุ่นแม่ทูนหัว " + "จะก้มหน้าลาไปมิได้กลัว แม่อย่ามัวหมองนักจงหักใจ" + ) + result = self.kv.check_klon(poem, k_type=8) + self.assertIsInstance(result, list) + self.assertEqual( + result, [ + "Rhyme error in Stanza (บทที่) 1: 'เหมือน' (Wak 2) " + "does not rhyme with 'เตือด' (Wak 3)", + "Rhyme error in Stanza (บทที่) 1: 'เตือด' (Wak 3) " + "does not rhyme with ['จะ', 'จาก', 'เรือน', 'ร้าง', 'แม่'] (Wak 4)"] + ) + + def test_check_klon8_invalid_poem_4(self): + """Test that invalid klon8 poem with wrong inter-stanza rhyme is detected.""" + poem = ( + "แม่รักลูกลูกก็รู้อยู่ว่ารัก คนอื่นสักหมื่นแสนไม่แม้นเหมือน " + "จะกินนอนวอนว่าเมตตาเตือน จะจากเรือนร้างแม่ไปแต่ตัง " + "แม่วันทองของลูกจงกลับบ้าน เขาจะพาลว้าวุ่นแม่ทูนหัว " + "จะก้มหน้าลาไปมิได้กลัว แม่อย่ามัวหมองนักจงหักใจ" + ) + result = self.kv.check_klon(poem, k_type=8) + self.assertIsInstance(result, list) + self.assertEqual( + result, + [ + "Inter-stanza rhyme error (ผิดสัมผัสระหว่างบท) " + "between Stanza 1 and 2: 'ตัง' (Wak 4) " + "does not rhyme with 'หัว' (Wak 2)" + ] + ) + + def test_check_klon(self): + """Test check_klon for Thai poem verification (k_type=4).""" + poem = ( + "ฉันชื่อหมูกรอบ ฉันชอบกินไก่ แล้ววิ่งตามไป ไล่หมาน้ำทอง " + "ฉันมันคนเก่ง เอ๋งเอ๋งคะนอง มีคนจับจอง เป็นของน้องเธียร" + ) + result = self.kv.check_klon(poem, k_type=4) + self.assertIsInstance(result, str) + self.assertEqual(result, "The poem is correct according to the principle.") + + poem_invalid = ( + "ฉันชื่อหมูกรอบ ฉันชอบกินไก่ แล้ววิ่งตามไล่ น้องหมาน้ำทอง " + "ฉันมันคนโหด เอ๋งเอ๋งคะนอง มีคนจับจอง เป็นของน้องเธียร" + ) + result_invalid = self.kv.check_klon(poem_invalid, k_type=4) + self.assertIsInstance(result_invalid, list) + self.assertEqual( + result_invalid, [ + "Rhyme error in Stanza (บทที่) 1: 'ไล่' (Wak 3) " + "does not rhyme with ['น้อง', 'หมา'] (Wak 4)", + "Rhyme error in Stanza (บทที่) 2: 'โหด' (Wak 1) " + "does not rhyme with ['เอ๋ง', 'เอ๋ง'] (Wak 2)"] + )