Source code for pythainlp.soundex.lk82

# SPDX-FileCopyrightText: 2016-2026 PyThaiNLP Project
# SPDX-FileType: SOURCE
# SPDX-License-Identifier: Apache-2.0
"""
Thai soundex, LK82 system.

Original paper:
Vichit Lorchirachoonkul. 1982. A Thai soundex
system. Information Processing & Management,
18(5):243–255.
https://doi.org/10.1016/0306-4573(82)90003-6

Python implementation:
by Korakot Chaovavanich
https://gist.github.com/korakot/0b772e09340cac2f493868da035597e8
"""

from __future__ import annotations

import re
from re import Pattern
from typing import Optional

from pythainlp.util import remove_tonemark

_TRANS1: dict[int, int] = str.maketrans(
    "กขฃคฅฆงจฉชฌซศษสญยฎดฏตณนฐฑฒถทธบปผพภฝฟมรลฬฤฦวหฮอ",
    "กกกกกกงจชชชซซซซยยดดตตนนททททททบปพพพฟฟมรรรรรวหหอ",
)
_TRANS2: dict[int, int] = str.maketrans(
    "กขฃคฅฆงจฉชซฌฎฏฐฑฒดตถทธศษสญณนรลฬฤฦบปพฟภผฝมำยวไใหฮาๅึืเแโุูอ",
    "1111112333333333333333333444444445555555667777889AAABCDEEF",
)
_CODE2: dict[str, str] = {chr(k): chr(v) for k, v in _TRANS2.items()}

# silenced
_RE_KARANT: Pattern[str] = re.compile(r"จน์|มณ์|ณฑ์|ทร์|ตร์|[ก-ฮ]์|[ก-ฮ][ะ-ู]์")

# signs, symbols, vowel that has no explicit sounds
# Paiyannoi, Phinthu, Maiyamok, Maitaikhu, Nikhahit
_RE_SIGN: Pattern[str] = re.compile(r"[\u0e2f\u0e3a\u0e46\u0e47\u0e4d]")


# Sara Ue, Sara Uee, Sara U, Sara Uu
_LONG_U: str = "\u0e36\u0e37\u0e38\u0e39"
# 7. separators without a code: Sara A, Mai Han-Akat, Sara I, Sara II
_SEPARATORS: str = "\u0e30\u0e31\u0e34\u0e35"
# 8. separators with a code: Sara Aa, Sara Ue, Sara Uee, Sara Uu, Lakkhangyao
_CODED_SEPARATORS: str = "\u0e32\u0e36\u0e37\u0e39\u0e45"
_SARA_U: str = "\u0e38"  # 9.
_ALL_SEPARATORS: str = _SEPARATORS + _CODED_SEPARATORS + _SARA_U
# separators 7. and 8. mapped to their codes
_SEPARATOR_CODE: dict[str, str] = {
    **dict.fromkeys(_SEPARATORS, ""),
    **{c: _CODE2[c] for c in _CODED_SEPARATORS},
}
_HO_O: str = "\u0e2b\u0e2d"  # Ho Hip, O Ang
_SEMIVOWELS: str = "\u0e22\u0e23\u0e24\u0e26\u0e27"
_SPECIAL: frozenset[str] = frozenset(_ALL_SEPARATORS + _HO_O + _SEMIVOWELS)


def _encode_first(text: str) -> tuple[list[str], str]:
    """6. Encode the first character; return the codes and the rest."""
    if "ก" <= text[0] <= "ฮ":
        return [text[0].translate(_TRANS1)], text[1:]
    codes = []
    if len(text) > 1:
        codes.append(text[1].translate(_TRANS1))
    codes.append(text[0].translate(_TRANS2))
    return codes, text[2:]


def _before_long_u(text: str, i: int) -> bool:
    """Check if text[i] is followed by a long U sara."""
    return i + 1 < len(text) and text[i + 1] in _LONG_U


def _encode_char(text: str, i: int, i_v: Optional[int]) -> Optional[str]:
    """
    Encode Sara U, Ho Hip, O Ang, or a semivowel at text[i].

    Return None when the character has no code, or "" for Sara U after
    ต/ธ (the empty string breaks repeat removal in step 13).
    The caller sets ``i_v`` for Sara U.

    :param str text: text after the first character
    :param int i: position of the character
    :param i_v: position of the latest separator (vowel)
    :type i_v: Optional[int]
    """
    c = text[i]
    if c == _SARA_U:
        if i == 0 or text[i - 1] not in "ตธ":
            return _CODE2.get(c, c)
        return ""
    if c in _HO_O:
        voiced = _before_long_u(text, i)
    else:
        voiced = i_v == i - 1 or _before_long_u(text, i)
    return _CODE2.get(c, c) if voiced else None


def _finish(res: list[str]) -> str:
    """13. Remove repetitions and 14. fill with zeros."""
    res2 = [res[0]]
    for code in res:
        if code != res2[-1]:
            res2.append(code)
    return ("".join(res2) + "0000")[:5]


[docs] def lk82(text: str) -> str: """ Convert text into a LK82 phonetic code. LK82 [#lk82]_ is a Thai soundex algorithm. :param str text: Thai word to be encoded :return: LK82 soundex code :rtype: str :Example: >>> from pythainlp.soundex import lk82 >>> lk82("ลัก") 'ร1000' >>> lk82("รัก") 'ร1000' >>> lk82("รักษ์") 'ร1000' >>> lk82("บูรณการ") 'บE419' >>> lk82("ปัจจุบัน") 'ป3E54' """ if not text or not isinstance(text, str): return "" text = remove_tonemark(text) # 4. remove tone marks text = _RE_KARANT.sub("", text) # 4. remove "karat" characters text = _RE_SIGN.sub("", text) # 5. remove signs and symbols if not text: return "" res, text = _encode_first(text) i_v: Optional[int] = None # position of the latest separator (vowel) for i, c in enumerate(text): if c not in _SPECIAL: # 12. fast path for other characters res.append(_CODE2.get(c, c)) continue code = _SEPARATOR_CODE.get(c) if code is None: if c == _SARA_U: # 9. i_v = i code = _encode_char(text, i, i_v) else: i_v = i if code is not None: res.append(code) return _finish(res)