Source code for pythainlp.util.thai

# SPDX-FileCopyrightText: 2016-2026 PyThaiNLP Project
# SPDX-FileType: SOURCE
# SPDX-License-Identifier: Apache-2.0
"""Check whether text is Thai."""

from __future__ import annotations

import string
from collections import Counter, defaultdict
from types import MappingProxyType
from typing import TYPE_CHECKING, Optional

from pythainlp import (
    thai_above_vowels,
    thai_below_vowels,
    thai_consonants,
    thai_digits,
    thai_follow_vowels,
    thai_lead_vowels,
    thai_punctuations,
    thai_signs,
    thai_tonemarks,
    thai_vowels,
)
from pythainlp.tools import warn_deprecation

if TYPE_CHECKING:
    from collections.abc import Iterable

_DEFAULT_IGNORE_CHARS: str = (
    string.whitespace + string.digits + string.punctuation
)
_TH_FIRST_CHAR_ASCII: int = 3584
_TH_LAST_CHAR_ASCII: int = 3711

# A comprehensive map of Thai characters to their descriptive names.
# MappingProxyType makes this constant read-only at runtime.
_THAI_CHAR_NAMES: MappingProxyType[str, str] = MappingProxyType(
    {
        # Consonants
        **{char: char for char in thai_consonants},
        # Vowels and Signs
        "\u0e24": "ฤ",
        "\u0e26": "ฦ",
        "\u0e30": "สระ อะ",
        "\u0e31": "ไม้หันอากาศ",
        "\u0e32": "สระ อา",
        "\u0e33": "สระ อำ",
        "\u0e34": "สระ อิ",
        "\u0e35": "สระ อี",
        "\u0e36": "สระ อึ",
        "\u0e37": "สระ อือ",
        "\u0e38": "สระ อุ",
        "\u0e39": "สระ อู",
        "\u0e40": "สระ เอ",
        "\u0e41": "สระ แอ",
        "\u0e42": "สระ โอ",
        "\u0e43": "สระ ใอ",
        "\u0e44": "สระ ไอ",
        "\u0e45": "ไม้ม้วน",
        "\u0e4d": "นฤคหิต",
        "\u0e47": "ไม้ไต่คู้",
        # Tone Marks
        "\u0e48": "ไม้เอก",
        "\u0e49": "ไม้โท",
        "\u0e4a": "ไม้ตรี",
        "\u0e4b": "ไม้จัตวา",
        # Other Signs
        "\u0e2f": "ไปยาลน้อย",
        "\u0e3a": "พินทุ",
        "\u0e46": "ไม้ยมก",
        "\u0e4c": "การันต์",
        "\u0e4e": "ยามักการ",
        # Punctuation
        "\u0e4f": "ฟองมัน",
        "\u0e5a": "อังคั่นคู่",
        "\u0e5b": "โคมุต",
        # Digits
        **{char: char for char in thai_digits},
        # Symbol
        "\u0e3f": "฿",
    }
)


def is_thai_char(ch: str) -> bool:
    """
    Check whether a character is a Thai character.

    :param str ch: character to check
    :return: True if the character is a Thai character, otherwise False
    :rtype: bool

    :Example:

        >>> from pythainlp.util import is_thai_char
        >>> is_thai_char("ก")  # THAI CHARACTER KO KAI
        True
        >>> is_thai_char("๕")  # THAI DIGIT FIVE
        True
    """
    ch_val = ord(ch)
    return _TH_FIRST_CHAR_ASCII <= ch_val <= _TH_LAST_CHAR_ASCII


[docs] def isthaichar(ch: str) -> bool: """ Check whether a character is a Thai character. .. deprecated:: 5.3.2 Use :func:`is_thai_char` instead. :param str ch: character to check :return: True if the character is a Thai character, otherwise False :rtype: bool """ warn_deprecation( "pythainlp.util.isthaichar", "pythainlp.util.is_thai_char", "5.3.2", "6.0", ) return is_thai_char(ch)
def is_thai(text: str, ignore_chars: str = ".") -> bool: """ Check whether every character in text is a Thai character. :param str text: text to check :param str ignore_chars: characters to be ignored (default is ".") :return: True if every character in the text is Thai, otherwise False :rtype: bool :Example: >>> from pythainlp.util import is_thai >>> is_thai("กาลเวลา") True >>> is_thai("กาลเวลา.") True >>> is_thai("กาล-เวลา") False >>> is_thai("กาล-เวลา +66", ignore_chars="01234567890+-., ") True """ if not ignore_chars: ignore_chars = "" for ch in text: if ch not in ignore_chars and not is_thai_char(ch): return False return True
[docs] def isthai(text: str, ignore_chars: str = ".") -> bool: """ Check whether every character in text is a Thai character. .. deprecated:: 5.3.2 Use :func:`is_thai` instead. :param str text: text to check :param str ignore_chars: characters to be ignored (default is ".") :return: True if every character in the text is Thai, otherwise False :rtype: bool """ warn_deprecation( "pythainlp.util.isthai", "pythainlp.util.is_thai", "5.3.2", "6.0", ) return is_thai(text, ignore_chars)
def count_thai(text: str, ignore_chars: str = _DEFAULT_IGNORE_CHARS) -> float: """ Calculate the proportion of Thai characters in text. :param str text: text to check :param str ignore_chars: characters to be ignored (default is whitespace, digits, and punctuation marks) :return: proportion of Thai characters in the text (percentage) :rtype: float :Example: >>> from pythainlp.util import count_thai >>> count_thai("ไทยเอ็นแอลพี 3.0") 100.0 >>> count_thai("PyThaiNLP 3.0") 0.0 >>> count_thai("ใช้งาน PyThaiNLP 3.0") 40.0 >>> count_thai("ใช้งาน PyThaiNLP 3.0", ignore_chars="") 30.0 """ if not text or not isinstance(text, str): return 0.0 if not ignore_chars: ignore_chars = "" num_thai = 0 num_ignore = 0 for ch in text: if ch in ignore_chars: num_ignore += 1 elif is_thai_char(ch): num_thai += 1 num_count = len(text) - num_ignore if num_count == 0: return 0.0 return (num_thai / num_count) * 100
[docs] def countthai(text: str, ignore_chars: str = _DEFAULT_IGNORE_CHARS) -> float: """ Calculate the proportion of Thai characters in text. .. deprecated:: 5.3.2 Use :func:`count_thai` instead. :param str text: text to check :param str ignore_chars: characters to be ignored (default is whitespace, digits, and punctuation marks) :return: proportion of Thai characters in the text (percentage) :rtype: float """ warn_deprecation( "pythainlp.util.countthai", "pythainlp.util.count_thai", "5.3.2", "6.0", ) return count_thai(text, ignore_chars)
[docs] def display_thai_char(ch: str) -> str: """ Prefix an underscore (_) to a high-position vowel or a tone mark. The underscore eases readability. :param str ch: character to be displayed :return: "_" + ch for a high-position vowel or a tone mark, otherwise the character itself :rtype: str :Example: >>> from pythainlp.util import display_thai_char >>> display_thai_char("้") '_้' """ if ( ch in thai_above_vowels or ch in thai_tonemarks or ch in "\u0e33\u0e4c\u0e4d\u0e4e" ): # last condition is Sra Aum, Thanthakhat, Nikhahit, Yamakkan return "_" + ch return ch
[docs] def thai_word_tone_detector(word: Optional[str]) -> list[tuple[str, str]]: """ Detect the tone of each syllable in a Thai word. This function converts the word to pronunciation with :func:`pythainlp.transliterate.pronunciate`. :param Optional[str] word: Thai word, or None :return: list of (syllable, tone) tuples, one for each syllable. Tone values are ``l`` (low), ``m`` (mid), ``h`` (high), ``r`` (rising), ``f`` (falling), or an empty string if it cannot be detected. Return ``[]`` if the word is None or empty. :rtype: list[tuple[str, str]] :Example: >>> from pythainlp.util import thai_word_tone_detector >>> print(thai_word_tone_detector("คนดี")) [('คน', 'm'), ('ดี', 'm')] >>> print(thai_word_tone_detector("มือถือ")) [('มือ', 'm'), ('ถือ', 'r')] >>> print(thai_word_tone_detector(None)) [] """ if not word: return [] from ..transliterate import pronunciate from ..util.syllable import tone_detector _pronunciate = pronunciate(word).split("-") return [(i, tone_detector(i.replace("หฺ", "ห"))) for i in _pronunciate]
# Character types for count_thai_chars(), checked in this order. _CHAR_TYPES: tuple[tuple[str, str], ...] = ( ("lead_vowels", thai_lead_vowels), ("follow_vowels", thai_follow_vowels), ("above_vowels", thai_above_vowels), ("below_vowels", thai_below_vowels), ("consonants", thai_consonants), ("tonemarks", thai_tonemarks), ("signs", thai_signs), ("thai_digits", thai_digits), ("punctuations", thai_punctuations), ) _COUNT_KEYS: tuple[str, ...] = ( "vowels", *(name for name, _ in _CHAR_TYPES), "non_thai", ) def _build_char_type_table() -> dict[str, str]: """Map each character to its type; the first matching type wins.""" table: dict[str, str] = {} for name, chars in _CHAR_TYPES: for char in chars: table.setdefault(char, name) return table _CHAR_TYPE_TABLE: dict[str, str] = _build_char_type_table() _VOWEL_SET: frozenset[str] = frozenset(thai_vowels) # Below this length, a direct loop is faster than counting with Counter. _COUNTER_MIN_LEN: int = 64 def _char_type(char: str) -> str: """Return the name of the first character type containing ``char``.""" for name, chars in _CHAR_TYPES: if char in chars: return name return "non_thai" def _count_iterable( text: Iterable[str], counts: dict[str, int] ) -> dict[str, int]: """Count the items of a non-string iterable, as ``str`` containment.""" for c in text: if c in thai_vowels: counts["vowels"] += 1 counts[_char_type(c)] += 1 return counts
[docs] def count_thai_chars(text: str) -> dict[str, int]: """ Count Thai characters by type. The types are consonants, vowels, lead_vowels, follow_vowels, above_vowels, below_vowels, tonemarks, signs, thai_digits, punctuations, and non_thai. :param str text: text to be counted :return: dictionary of character counts by type :rtype: dict[str, int] :Example: >>> from pythainlp.util import count_thai_chars >>> count_thai_chars("ทดสอบภาษาไทย") # doctest: +NORMALIZE_WHITESPACE {'vowels': 3, 'lead_vowels': 1, 'follow_vowels': 2, 'above_vowels': 0, 'below_vowels': 0, 'consonants': 9, 'tonemarks': 0, 'signs': 0, 'thai_digits': 0, 'punctuations': 0, 'non_thai': 0} """ counts = dict.fromkeys(_COUNT_KEYS, 0) if not isinstance(text, str): # Tolerate non-str iterables. return _count_iterable(text, counts) # type: ignore[unreachable] if len(text) < _COUNTER_MIN_LEN: type_of = _CHAR_TYPE_TABLE.get for char in text: counts[type_of(char, "non_thai")] += 1 if char in _VOWEL_SET: counts["vowels"] += 1 return counts for char, num in Counter(text).items(): if char in _VOWEL_SET: counts["vowels"] += num counts[_CHAR_TYPE_TABLE.get(char, "non_thai")] += num return counts
[docs] def analyze_thai_text(text: str) -> dict[str, int]: """ Count characters in Thai text by descriptive name. Process the text character by character and map each Thai character to its descriptive name, or to itself (for consonants and digits). :param str text: Thai text to be analyzed :return: dictionary mapping character names to their counts :rtype: dict[str, int] :Example: >>> from pythainlp.util import analyze_thai_text >>> analyze_thai_text("คนดี") {'ค': 1, 'น': 1, 'ด': 1, 'สระ อี': 1} >>> analyze_thai_text("เล่น") {'สระ เอ': 1, 'ล': 1, 'ไม้เอก': 1, 'น': 1} """ results: dict[str, int] = defaultdict(int) for char in text: # Unknown characters are counted as themselves. results[_THAI_CHAR_NAMES.get(char, char)] += 1 return dict(results)