From e6d9edddd0ac0ea83148ce42aeda8f2f56a16330 Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Sat, 14 Mar 2026 16:39:01 +0700 Subject: [PATCH 1/2] Add more tests for tone and vowel analysis --- tests/core/test_khavee.py | 141 ++++++++++++++++++++++++++++++++++++++ tests/core/test_tag.py | 76 ++++++++++++++++++++ tests/core/test_util.py | 113 ++++++++++++++++++++++++++++++ 3 files changed, 330 insertions(+) diff --git a/tests/core/test_khavee.py b/tests/core/test_khavee.py index 664c5e585..321f7ee41 100644 --- a/tests/core/test_khavee.py +++ b/tests/core/test_khavee.py @@ -117,3 +117,144 @@ def test_check_aek_too(self): self.assertTrue( kv.check_aek_too(["หนม", "หน่ม", "หน้ม"]), [False, "aek", "too"] ) + + +class KhaveeCheckKaruLahuTestCase(unittest.TestCase): + """Tests for KhaveeVerifier.check_karu_lahu""" + + def setUp(self): + 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") + + +class KhaveeHandleKarunTestCase(unittest.TestCase): + """Tests for KhaveeVerifier.handle_karun_sound_silence""" + + def setUp(self): + self.kv = KhaveeVerifier() + + def test_word_without_karun_unchanged(self): + 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 (ส) → เกม + self.assertEqual(self.kv.handle_karun_sound_silence("เกมส์"), "เกม") + + def test_word_ending_with_karun_stripped_2(self): + # รักษ์ → drop ์ + ษ → รัก + self.assertEqual(self.kv.handle_karun_sound_silence("รักษ์"), "รัก") + + def test_returns_string(self): + self.assertIsInstance(self.kv.handle_karun_sound_silence("สวัสดี"), str) + + +class KhaveeCheckAekTooEdgeCasesTestCase(unittest.TestCase): + """Edge-case tests for KhaveeVerifier.check_aek_too""" + + def setUp(self): + self.kv = KhaveeVerifier() + + def test_non_string_raises_type_error(self): + with self.assertRaises(TypeError): + self.kv.check_aek_too(123) + + def test_dead_syllable_as_aek_flag(self): + self.assertEqual(self.kv.check_aek_too("บท", dead_syllable_as_aek=True), "aek") + + def test_dead_syllable_without_flag_returns_false(self): + self.assertFalse(self.kv.check_aek_too("บท", dead_syllable_as_aek=False)) + + def test_list_with_non_string_element_raises(self): + with self.assertRaises(TypeError): + self.kv.check_aek_too(["ไก่", 42]) + + def test_both_tone_marks_returns_false(self): + # 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)) + + +class KhaveeCheckSaraEdgeCasesTestCase(unittest.TestCase): + """Edge-case tests for KhaveeVerifier.check_sara""" + + def setUp(self): + self.kv = KhaveeVerifier() + + def test_bo_mai_ek_returns_oo(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_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("ฤ"), "อึ") + + def test_ruea_sara(self): + # ฤา (ฤ + sara aa 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) diff --git a/tests/core/test_tag.py b/tests/core/test_tag.py index 95e25e03f..51ccba01f 100644 --- a/tests/core/test_tag.py +++ b/tests/core/test_tag.py @@ -228,6 +228,82 @@ def test_ner_locations(self): ) +class BlackboardPreProcessTestCase(unittest.TestCase): + """Tests for pythainlp.tag.blackboard.pre_process""" + + def setUp(self): + from pythainlp.tag.blackboard import pre_process + + self.pre_process = pre_process + + def test_space_is_escaped(self): + self.assertEqual(self.pre_process([" "]), ["_"]) + + def test_regular_words_unchanged(self): + self.assertEqual(self.pre_process(["ผม", "รัก", "คุณ"]), ["ผม", "รัก", "คุณ"]) + + def test_mixed_space_and_words(self): + self.assertEqual(self.pre_process(["ผม", " ", "คุณ"]), ["ผม", "_", "คุณ"]) + + def test_multiple_spaces(self): + self.assertEqual(self.pre_process([" ", " "]), ["_", "_"]) + + def test_empty_list(self): + self.assertEqual(self.pre_process([]), []) + + def test_non_space_special_chars_unchanged(self): + self.assertEqual(self.pre_process(["_"]), ["_"]) # already escaped form + + def test_single_thai_word(self): + self.assertEqual(self.pre_process(["สวัสดี"]), ["สวัสดี"]) + + +class BlackboardPostProcessTestCase(unittest.TestCase): + """Tests for pythainlp.tag.blackboard.post_process""" + + def setUp(self): + from pythainlp.tag.blackboard import TO_UD, post_process + + self.post_process = post_process + self.TO_UD = TO_UD + + def test_unescape_underscore_to_space(self): + result = self.post_process([("_", "NN"), ("คน", "NN")]) + self.assertEqual(result, [(" ", "NN"), ("คน", "NN")]) + + def test_regular_words_pass_through(self): + result = self.post_process([("ผม", "PPRS"), ("รัก", "VACT")]) + self.assertEqual(result, [("ผม", "PPRS"), ("รัก", "VACT")]) + + def test_unescape_to_space_with_ud(self): + result = self.post_process([("_", "NN")], to_ud=True) + self.assertEqual(result, [(" ", "NOUN")]) + + def test_regular_word_to_ud(self): + result = self.post_process([("คน", "NN")], to_ud=True) + self.assertEqual(result, [("คน", "NOUN")]) + + def test_all_to_ud_mappings(self): + for bb_tag, ud_tag in self.TO_UD.items(): + if bb_tag == "": + continue # skip empty-string sentinel + result = self.post_process([("word", bb_tag)], to_ud=True) + self.assertEqual( + result[0][1], + ud_tag, + f"TO_UD mapping failed for tag '{bb_tag}': " + f"expected '{ud_tag}', got '{result[0][1]}'", + ) + + def test_empty_list(self): + self.assertEqual(self.post_process([]), []) + self.assertEqual(self.post_process([], to_ud=True), []) + + def test_no_ud_default_preserves_tag(self): + result = self.post_process([("คน", "VV")]) + self.assertEqual(result[0][1], "VV") + + class TagNNERTestCase(unittest.TestCase): """Test pythainlp.tag.thai_nner""" diff --git a/tests/core/test_util.py b/tests/core/test_util.py index 3a02f49cf..4d1b9c4cb 100644 --- a/tests/core/test_util.py +++ b/tests/core/test_util.py @@ -17,6 +17,7 @@ analyze_thai_text, arabic_digit_to_thai_digit, bahttext, + check_khuap_klam, collate, convert_years, count_thai_chars, @@ -40,11 +41,16 @@ reign_year_to_ad, remove_dangling, remove_dup_spaces, + remove_repeat_vowels, + remove_spaces_before_marks, remove_tone_ipa, remove_tonemark, remove_trailing_repeat_consonants, remove_zw, + reorder_vowels, sound_syllable, + spell_syllable, + spell_word, spelling, syllable_length, syllable_open_close_detector, @@ -52,11 +58,13 @@ text_to_num, text_to_thai_digit, th_zodiac, + thai_consonant_to_spelling, thai_digit_to_arabic_digit, thai_keyboard_dist, thai_strftime, thai_strptime, thai_to_eng, + thai_word_tone_detector, thaiword_to_date, thaiword_to_num, thaiword_to_time, @@ -65,6 +73,7 @@ to_idna, to_lunar_date, tone_detector, + tone_to_spelling, words_to_num, ) from pythainlp.util.morse import morse_decode, morse_encode @@ -1048,3 +1057,107 @@ def test_analyze_thai_text(self): analyze_thai_text("เล่น"), {'สระ เอ': 1, 'ล': 1, 'ไม้เอก': 1, 'น': 1} ) + + # ### pythainlp.util.khuap_klam + + def test_check_khuap_klam(self): + # true consonant clusters (คำควบกล้ำแท้) + self.assertTrue(check_khuap_klam("กราบ")) + self.assertTrue(check_khuap_klam("ปลา")) + self.assertTrue(check_khuap_klam("เพราะ")) + # false consonant clusters (คำควบกล้ำไม่แท้) + self.assertFalse(check_khuap_klam("จริง")) + self.assertFalse(check_khuap_klam("ทราย")) + # not a cluster + self.assertIsNone(check_khuap_klam("แม่")) + self.assertIsNone(check_khuap_klam("ตา")) + # edge cases + self.assertIsNone(check_khuap_klam("")) + for word in ["กลม", "จริง", "ตา"]: + self.assertIn(check_khuap_klam(word), (True, False, None)) + + # ### pythainlp.util.pronounce + + def test_thai_consonant_to_spelling(self): + self.assertEqual(thai_consonant_to_spelling("ก"), "กอ") + self.assertEqual(thai_consonant_to_spelling("ข"), "ขอ") + self.assertEqual(thai_consonant_to_spelling("น"), "นอ") + # multi-char strings and non-consonants pass through unchanged + self.assertEqual(thai_consonant_to_spelling("กา"), "กา") + self.assertEqual(thai_consonant_to_spelling("า"), "า") + self.assertEqual(thai_consonant_to_spelling("A"), "A") + self.assertEqual(thai_consonant_to_spelling(""), "") + + def test_tone_to_spelling(self): + self.assertEqual(tone_to_spelling("่"), "ไม้เอก") + self.assertEqual(tone_to_spelling("้"), "ไม้โท") + self.assertEqual(tone_to_spelling("๊"), "ไม้ตรี") + self.assertEqual(tone_to_spelling("๋"), "ไม้จัตวา") + # non-tone-mark characters pass through unchanged + self.assertEqual(tone_to_spelling("ก"), "ก") + self.assertEqual(tone_to_spelling(""), "") + + # ### pythainlp.util.spell_words + + def test_spell_syllable(self): + result = spell_syllable("แมว") + self.assertIsInstance(result, list) + self.assertEqual(result[-1], "แมว") + result_kon = spell_syllable("คน") + self.assertGreater(len(result_kon), 0) + self.assertIn("คน", result_kon) + + def test_spell_word(self): + self.assertEqual(spell_word(None), []) + self.assertEqual(spell_word(""), []) + result = spell_word("คน") + self.assertIsInstance(result, list) + self.assertIn("คน", result) + # multi-syllable: last element is the full word + result_multi = spell_word("คนดี") + self.assertEqual(result_multi[-1], "คนดี") + + # ### pythainlp.util.thai – thai_word_tone_detector + + def test_thai_word_tone_detector(self): + self.assertEqual(thai_word_tone_detector(None), []) + self.assertEqual(thai_word_tone_detector(""), []) + result = thai_word_tone_detector("คนดี") + self.assertIsInstance(result, list) + valid_tones = {"l", "m", "h", "r", "f", ""} + for syllable, tone in result: + self.assertIsInstance(syllable, str) + self.assertIn(tone, valid_tones) + self.assertIsInstance(thai_word_tone_detector("มือถือ"), list) + + # ### pythainlp.util.normalize – remove_repeat_vowels, + # remove_spaces_before_marks, reorder_vowels + + def test_remove_repeat_vowels(self): + self.assertEqual(remove_repeat_vowels(""), "") + self.assertEqual(remove_repeat_vowels("สวัสดี"), "สวัสดี") + self.assertEqual(remove_repeat_vowels("นานาา"), "นานา") + self.assertEqual(remove_repeat_vowels("ดีีีี"), "ดี") + # double sara E is reordered to sara Ae before repeat-removal + self.assertEqual(remove_repeat_vowels("เเปลก"), "แปลก") + + def test_remove_spaces_before_marks(self): + self.assertEqual(remove_spaces_before_marks(""), "") + self.assertEqual(remove_spaces_before_marks("กิน"), "กิน") + self.assertEqual(remove_spaces_before_marks("ก ิ"), "กิ") + self.assertEqual(remove_spaces_before_marks("ก ุ"), "กุ") + self.assertEqual(remove_spaces_before_marks("ก ่า"), "ก่า") + # spaces between regular consonants are preserved + self.assertIn(" ", remove_spaces_before_marks("ก ข")) + + def test_reorder_vowels(self): + self.assertEqual(reorder_vowels(""), "") + self.assertEqual(reorder_vowels("สวัสดี"), "สวัสดี") + # two sara E → sara Ae + self.assertEqual(reorder_vowels("เเปลก"), "แปลก") + # nikhahit (ํ) + sara aa (า) → sara am (ำ) + self.assertEqual(reorder_vowels("\u0e01\u0e4d\u0e32"), "\u0e01\u0e33") + # tone mark reorder: both characters still present after reorder + result = reorder_vowels("\u0e01\u0e32\u0e48") + self.assertIn("\u0e48", result) + self.assertIn("\u0e32", result) From 22ead5a2c5b5a53668e88aa39b7736d430b865fa Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Sat, 14 Mar 2026 17:08:16 +0700 Subject: [PATCH 2/2] Move tone and khuap_klam test to compact suite --- tests/compact/testc_util.py | 17 ++++++++++++++ tests/core/test_util.py | 44 ------------------------------------- 2 files changed, 17 insertions(+), 44 deletions(-) diff --git a/tests/compact/testc_util.py b/tests/compact/testc_util.py index 31aacfa40..dcf6c1198 100644 --- a/tests/compact/testc_util.py +++ b/tests/compact/testc_util.py @@ -22,10 +22,17 @@ def test_spell_word(self): self.assertEqual( spell_word("คนดี"), ["คอ", "นอ", "คน", "ดอ", "อี", "ดี", "คนดี"] ) + result = spell_word("คน") + self.assertIsInstance(result, list) + self.assertIn("คน", result) + # Edge cases: None and empty string self.assertEqual(spell_word(None), []) self.assertEqual(spell_word(""), []) + # multi-syllable: last element is the full word + result_multi = spell_word("คนดี") + self.assertEqual(result_multi[-1], "คนดี") class UtilTestCaseC(unittest.TestCase): def test_rhyme(self): @@ -37,6 +44,14 @@ def test_thai_word_tone_detector(self): self.assertEqual( thai_word_tone_detector("ราคา"), [("รา", "m"), ("คา", "m")] ) + result = thai_word_tone_detector("คนดี") + self.assertIsInstance(result, list) + valid_tones = {"l", "m", "h", "r", "f", ""} + for syllable, tone in result: + self.assertIsInstance(syllable, str) + self.assertIn(tone, valid_tones) + self.assertIsInstance(thai_word_tone_detector("มือถือ"), list) + # Edge cases: None and empty string self.assertEqual(thai_word_tone_detector(None), []) self.assertEqual(thai_word_tone_detector(""), []) @@ -63,3 +78,5 @@ def test_check_khuap_klam(self): # Edge cases: empty string returns None self.assertIsNone(check_khuap_klam("")) + for word in ["กลม", "จริง", "ตา"]: + self.assertIn(check_khuap_klam(word), (True, False, None)) diff --git a/tests/core/test_util.py b/tests/core/test_util.py index 4d1b9c4cb..11d4ff8e9 100644 --- a/tests/core/test_util.py +++ b/tests/core/test_util.py @@ -17,7 +17,6 @@ analyze_thai_text, arabic_digit_to_thai_digit, bahttext, - check_khuap_klam, collate, convert_years, count_thai_chars, @@ -50,7 +49,6 @@ reorder_vowels, sound_syllable, spell_syllable, - spell_word, spelling, syllable_length, syllable_open_close_detector, @@ -64,7 +62,6 @@ thai_strftime, thai_strptime, thai_to_eng, - thai_word_tone_detector, thaiword_to_date, thaiword_to_num, thaiword_to_time, @@ -1058,24 +1055,6 @@ def test_analyze_thai_text(self): {'สระ เอ': 1, 'ล': 1, 'ไม้เอก': 1, 'น': 1} ) - # ### pythainlp.util.khuap_klam - - def test_check_khuap_klam(self): - # true consonant clusters (คำควบกล้ำแท้) - self.assertTrue(check_khuap_klam("กราบ")) - self.assertTrue(check_khuap_klam("ปลา")) - self.assertTrue(check_khuap_klam("เพราะ")) - # false consonant clusters (คำควบกล้ำไม่แท้) - self.assertFalse(check_khuap_klam("จริง")) - self.assertFalse(check_khuap_klam("ทราย")) - # not a cluster - self.assertIsNone(check_khuap_klam("แม่")) - self.assertIsNone(check_khuap_klam("ตา")) - # edge cases - self.assertIsNone(check_khuap_klam("")) - for word in ["กลม", "จริง", "ตา"]: - self.assertIn(check_khuap_klam(word), (True, False, None)) - # ### pythainlp.util.pronounce def test_thai_consonant_to_spelling(self): @@ -1107,29 +1086,6 @@ def test_spell_syllable(self): self.assertGreater(len(result_kon), 0) self.assertIn("คน", result_kon) - def test_spell_word(self): - self.assertEqual(spell_word(None), []) - self.assertEqual(spell_word(""), []) - result = spell_word("คน") - self.assertIsInstance(result, list) - self.assertIn("คน", result) - # multi-syllable: last element is the full word - result_multi = spell_word("คนดี") - self.assertEqual(result_multi[-1], "คนดี") - - # ### pythainlp.util.thai – thai_word_tone_detector - - def test_thai_word_tone_detector(self): - self.assertEqual(thai_word_tone_detector(None), []) - self.assertEqual(thai_word_tone_detector(""), []) - result = thai_word_tone_detector("คนดี") - self.assertIsInstance(result, list) - valid_tones = {"l", "m", "h", "r", "f", ""} - for syllable, tone in result: - self.assertIsInstance(syllable, str) - self.assertIn(tone, valid_tones) - self.assertIsInstance(thai_word_tone_detector("มือถือ"), list) - # ### pythainlp.util.normalize – remove_repeat_vowels, # remove_spaces_before_marks, reorder_vowels