• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

PyThaiNLP / pythainlp / 11640665679

02 Nov 2024 06:41AM UTC coverage: 33.191%. First build
11640665679

Pull #962

github

web-flow
Merge fbaecaa2a into 1c9a2432a
Pull Request #962: Fix expand maiyamok

23 of 24 new or added lines in 1 file covered. (95.83%)

2480 of 7472 relevant lines covered (33.19%)

4.88 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.47
/pythainlp/util/normalize.py
1
# -*- coding: utf-8 -*-
2
# SPDX-FileCopyrightText: 2016-2024 PyThaiNLP Project
3
# SPDX-License-Identifier: Apache-2.0
4
"""
3✔
5
Text normalization
6
"""
7

8
import re
15✔
9
from typing import List, Union
15✔
10

11
from pythainlp import thai_above_vowels as above_v
15✔
12
from pythainlp import thai_below_vowels as below_v
15✔
13
from pythainlp import thai_follow_vowels as follow_v
15✔
14
from pythainlp import thai_lead_vowels as lead_v
15✔
15
from pythainlp import thai_tonemarks as tonemarks
15✔
16
from pythainlp.tokenize import word_tokenize
15✔
17
from pythainlp.tools import warn_deprecation
15✔
18

19
_DANGLING_CHARS = f"{above_v}{below_v}{tonemarks}\u0e3a\u0e4c\u0e4d\u0e4e"
15✔
20
_RE_REMOVE_DANGLINGS = re.compile(f"^[{_DANGLING_CHARS}]+")
15✔
21

22
_ZERO_WIDTH_CHARS = "\u200b\u200c"  # ZWSP, ZWNJ
15✔
23

24
_REORDER_PAIRS = [
15✔
25
    ("\u0e40\u0e40", "\u0e41"),  # Sara E + Sara E -> Sara Ae
26
    (
27
        f"([{tonemarks}\u0e4c]+)([{above_v}{below_v}]+)",
28
        "\\2\\1",
29
    ),  # TONE/Thanthakhat + ABV/BLW VOWEL -> ABV/BLW VOWEL + TONE/Thanthakhat
30
    (
31
        f"\u0e4d([{tonemarks}]*)\u0e32",
32
        "\\1\u0e33",
33
    ),  # Nikhahit + TONEMARK + Sara Aa -> TONEMARK + Sara Am
34
    (
35
        f"([{follow_v}]+)([{tonemarks}]+)",
36
        "\\2\\1",
37
    ),  # FOLLOW VOWEL + TONEMARK+ -> TONEMARK + FOLLOW VOWEL
38
    ("([^\u0e24\u0e26])\u0e45", "\\1\u0e32"),  # Lakkhangyao -> Sara Aa
39
]
40

41
# VOWELS + Phinthu, Thanthakhat, Nikhahit, Yamakkan
42
_NOREPEAT_CHARS = (
15✔
43
    f"{follow_v}{lead_v}{above_v}{below_v}\u0e3a\u0e4c\u0e4d\u0e4e"
44
)
45
_NOREPEAT_PAIRS = list(
15✔
46
    zip([f"({ch}[ ]*)+{ch}" for ch in _NOREPEAT_CHARS], _NOREPEAT_CHARS)
47
)
48

49
_RE_TONEMARKS = re.compile(f"[{tonemarks}]+")
15✔
50

51
_RE_REMOVE_NEWLINES = re.compile("[ \n]*\n[ \n]*")
15✔
52

53

54
def _last_char(matchobj):  # to be used with _RE_NOREPEAT_TONEMARKS
15✔
55
    return matchobj.group(0)[-1]
15✔
56

57

58
def remove_dangling(text: str) -> str:
15✔
59
    """
60
    Remove Thai non-base characters at the beginning of text.
61

62
    This is a common "typo", especially for input field in a form,
63
    as these non-base characters can be visually hidden from user
64
    who may accidentally typed them in.
65

66
    A character to be removed should be both:
67

68
        * tone mark, above vowel, below vowel, or non-base sign AND
69
        * located at the beginning of the text
70

71
    :param str text: input text
72
    :return: text without dangling Thai characters at the beginning
73
    :rtype: str
74

75
    :Example:
76
    ::
77

78
        from pythainlp.util import remove_dangling
79

80
        remove_dangling("๊ก")
81
        # output: 'ก'
82
    """
83
    return _RE_REMOVE_DANGLINGS.sub("", text)
15✔
84

85

86
def remove_dup_spaces(text: str) -> str:
15✔
87
    """
88
    Remove duplicate spaces. Replace multiple spaces with one space.
89

90
    Multiple newline characters and empty lines will be replaced
91
    with one newline character.
92

93
    :param str text: input text
94
    :return: text without duplicated spaces and newlines
95
    :rtype: str
96

97
    :Example:
98
    ::
99

100
        from pythainlp.util import remove_dup_spaces
101

102
        remove_dup_spaces("ก    ข    ค")
103
        # output: 'ก ข ค'
104
    """
105
    while "  " in text:
15✔
106
        text = text.replace("  ", " ")
15✔
107
    text = _RE_REMOVE_NEWLINES.sub("\n", text)
15✔
108
    text = text.strip()
15✔
109
    return text
15✔
110

111

112
def remove_tonemark(text: str) -> str:
15✔
113
    """
114
    Remove all Thai tone marks from the text.
115

116
    Thai script has four tone marks indicating four tones as follows:
117

118
        * Down tone (Thai: ไม้เอก  _่ )
119
        * Falling tone  (Thai: ไม้โท  _้ )
120
        * High tone (Thai: ไม้ตรี  _๊ )
121
        * Rising tone (Thai: ไม้จัตวา _๋ )
122

123
    Putting wrong tone mark is a common mistake in Thai writing.
124
    By removing tone marks from the string, it could be used to
125
    for a approximate string matching.
126

127
    :param str text: input text
128
    :return: text without Thai tone marks
129
    :rtype: str
130

131
    :Example:
132
    ::
133

134
        from pythainlp.util import remove_tonemark
135

136
        remove_tonemark("สองพันหนึ่งร้อยสี่สิบเจ็ดล้านสี่แสนแปดหมื่นสามพันหกร้อยสี่สิบเจ็ด")
137
        # output: สองพันหนึงรอยสีสิบเจ็ดลานสีแสนแปดหมืนสามพันหกรอยสีสิบเจ็ด
138
    """
139
    for ch in tonemarks:
15✔
140
        while ch in text:
15✔
141
            text = text.replace(ch, "")
15✔
142
    return text
15✔
143

144

145
def remove_zw(text: str) -> str:
15✔
146
    """
147
    Remove zero-width characters.
148

149
    These non-visible characters may cause unexpected result from the
150
    user's point of view. Removing them can make string matching more robust.
151

152
    Characters to be removed:
153

154
        * Zero-width space (ZWSP)
155
        * Zero-width non-joiner (ZWJP)
156

157
    :param str text: input text
158
    :return: text without zero-width characters
159
    :rtype: str
160
    """
161
    for ch in _ZERO_WIDTH_CHARS:
15✔
162
        while ch in text:
15✔
163
            text = text.replace(ch, "")
15✔
164

165
    return text
15✔
166

167

168
def reorder_vowels(text: str) -> str:
15✔
169
    """
170
    Reorder vowels and tone marks to the standard logical order/spelling.
171

172
    Characters in input text will be reordered/transformed,
173
    according to these rules:
174

175
        * Sara E + Sara E -> Sara Ae
176
        * Nikhahit + Sara Aa -> Sara Am
177
        * tone mark + non-base vowel -> non-base vowel + tone mark
178
        * follow vowel + tone mark -> tone mark + follow vowel
179

180
    :param str text: input text
181
    :return: text with vowels and tone marks in the standard logical order
182
    :rtype: str
183
    """
184
    for pair in _REORDER_PAIRS:
15✔
185
        text = re.sub(pair[0], pair[1], text)
15✔
186

187
    return text
15✔
188

189

190
def remove_repeat_vowels(text: str) -> str:
15✔
191
    """
192
    Remove repeating vowels, tone marks, and signs.
193

194
    This function will call reorder_vowels() first, to make sure that
195
    double Sara E will be converted to Sara Ae and not be removed.
196

197
    :param str text: input text
198
    :return: text without repeating Thai vowels, tone marks, and signs
199
    :rtype: str
200
    """
201
    text = reorder_vowels(text)
15✔
202
    for pair in _NOREPEAT_PAIRS:
15✔
203
        text = re.sub(pair[0], pair[1], text)
15✔
204

205
    # remove repeating tone marks, use last tone mark
206
    text = _RE_TONEMARKS.sub(_last_char, text)
15✔
207

208
    return text
15✔
209

210

211
def normalize(text: str) -> str:
15✔
212
    """
213
    Normalize and clean Thai text with normalizing rules as follows:
214

215
        * Remove zero-width spaces
216
        * Remove duplicate spaces
217
        * Reorder tone marks and vowels to standard order/spelling
218
        * Remove duplicate vowels and signs
219
        * Remove duplicate tone marks
220
        * Remove dangling non-base characters at the beginning of text
221

222
    normalize() simply call remove_zw(), remove_dup_spaces(),
223
    remove_repeat_vowels(), and remove_dangling(), in that order.
224

225
    If a user wants to customize the selection or the order of rules
226
    to be applied, they can choose to call those functions by themselves.
227

228
    Note: for Unicode normalization, see unicodedata.normalize().
229

230
    :param str text: input text
231
    :return: normalized text according to the rules
232
    :rtype: str
233

234
    :Example:
235
    ::
236

237
        from pythainlp.util import normalize
238

239
        normalize("เเปลก")  # starts with two Sara E
240
        # output: แปลก
241

242
        normalize("นานาาา")
243
        # output: นานา
244
    """
245
    text = remove_zw(text)
15✔
246
    text = remove_dup_spaces(text)
15✔
247
    text = remove_repeat_vowels(text)
15✔
248
    text = remove_dangling(text)
15✔
249

250
    return text
15✔
251

252

253
def expand_maiyamok(sent: Union[str, List[str]]) -> List[str]:
15✔
254
    if isinstance(sent, str):
15✔
255
        sent = word_tokenize(sent)
15✔
256

257
    # Breaks Maiyamok that attached to others, e.g. "นกๆๆ", "นกๆ ๆ", "นกๆคน"
258
    temp_toks: list[str] = []
15✔
259
    for _, token in enumerate(sent):
15✔
260
        toks = re.split(r"(ๆ)", token)
15✔
261
        toks = [tok for tok in toks if tok]  # remove empty string ("")
15✔
262
        temp_toks.extend(toks)
15✔
263
    sent = temp_toks
15✔
264

265
    output_toks: list[str] = []
15✔
266

267
    yamok = "ๆ"
15✔
268
    yamok_count = 0
15✔
269
    len_sent = len(sent)
15✔
270
    for i in range(len_sent - 1, -1, -1):  # do it backward
15✔
271
        if yamok_count == 0 or (i + 1 >= len_sent):
15✔
272
            if sent[i] == yamok:
15✔
273
                yamok_count = yamok_count + 1
15✔
274
            else:
275
                output_toks.append(sent[i])
15✔
276
            continue
15✔
277

278
        if sent[i] == yamok:
15✔
279
            yamok_count = yamok_count + 1
15✔
280
        else:
281
            if sent[i].isspace():
15✔
282
                if yamok_count > 0:  # remove space before yamok
15✔
283
                    continue
15✔
284
                else:  # with preprocessing above, this should not happen
NEW
285
                    output_toks.append(sent[i])
×
286
            else:
287
                output_toks.extend([sent[i]] * (yamok_count + 1))
15✔
288
                yamok_count = 0
15✔
289

290
    return output_toks[::-1]
15✔
291

292

293
def maiyamok(sent: Union[str, List[str]]) -> List[str]:
15✔
294
    """
295
    Expand Maiyamok.
296

297
    Deprecated. Use expand_maiyamok() instead.
298

299
    Maiyamok (ๆ) (Unicode U+0E46) is a Thai character indicating word
300
    repetition. This function preprocesses Thai text by replacing
301
    Maiyamok with a word being repeated.
302

303
    :param Union[str, List[str]] sent: input sentence (list or str)
304
    :return: list of words
305
    :rtype: List[str]
306

307
    :Example:
308
    ::
309

310
        from pythainlp.util import expand_maiyamok
311

312
        expand_maiyamok("เด็กๆกิน")
313
        # output: ['เด็ก', 'เด็ก', 'กิน']
314
    """
315
    warn_deprecation(
×
316
        "pythainlp.util.maiyamok", "pythainlp.util.expand_maiyamok"
317
    )
318
    return expand_maiyamok(sent)
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc