# SPDX-FileCopyrightText: 2016-2026 PyThaiNLP Project
# SPDX-FileType: SOURCE
# SPDX-License-Identifier: Apache-2.0
"""
Spell out time as Thai words.
Convert a time string or a time object to Thai words.
"""
from __future__ import annotations
from contextlib import suppress
from datetime import datetime, time
from functools import lru_cache
from typing import Callable, Optional, Union
from pythainlp.tokenize import Tokenizer
from pythainlp.util.numtoword import num_to_thaiword
from pythainlp.util.wordtonum import thaiword_to_num
_TIME_FORMAT_WITH_SEC: str = "%H:%M:%S"
_TIME_FORMAT_WITHOUT_SEC: str = "%H:%M"
_DICT_THAI_TIME: dict[str, int] = {
"ศูนย์": 0,
"หนึ่ง": 1,
"สอง": 2,
"ยี่": 2,
"สาม": 3,
"สี่": 4,
"ห้า": 5,
"หก": 6,
"เจ็ด": 7,
"แปด": 8,
"เก้า": 9,
"สิบ": 10,
"เอ็ด": 1,
# set the value of the time unit
"โมงเช้า": 6, # start counting at 7:00 a.m.
"โมงเย็น": 13,
"บ่าย": 13,
"บ่ายโมง": 13,
"ตี": 0,
"เที่ยงวัน": 12,
"เที่ยงคืน": 0,
"เที่ยง": 12,
"ทุ่ม": 18,
"นาฬิกา": 0,
"ครึ่ง": 30,
}
@lru_cache
def _thai_time_cut() -> Tokenizer:
"""Load the Thai time tokenizer lazily, with cache."""
return Tokenizer(custom_dict=list(_DICT_THAI_TIME.keys()), engine="newmm")
_THAI_TIME_AFFIX: list[str] = [
"โมงเช้า",
"บ่ายโมง",
"โมงเย็น",
"โมง",
"นาฬิกา",
"ทุ่ม",
"ตี",
"เที่ยงคืน",
"เที่ยงวัน",
"เที่ยง",
]
def _format_6h(h: int) -> str:
"""Spell out an hour on the Thai 6-hour clock."""
text = ""
if h == 0:
text += "เที่ยงคืน"
elif h < 7:
text += "ตี" + num_to_thaiword(h)
elif h < 12:
text += num_to_thaiword(h - 6) + "โมงเช้า"
elif h == 12:
text += "เที่ยง"
elif h < 18:
if h == 13:
text += "บ่ายโมง"
else:
text += "บ่าย" + num_to_thaiword(h - 12) + "โมง"
elif h == 18:
text += "หกโมงเย็น"
else:
text += num_to_thaiword(h - 18) + "ทุ่ม"
return text
def _format_m6h(h: int) -> str:
"""Spell out an hour on the Thai modified 6-hour clock."""
text = ""
if h == 0:
text += "เที่ยงคืน"
elif h < 6:
text += "ตี" + num_to_thaiword(h)
elif h < 12:
text += num_to_thaiword(h) + "โมง"
elif h == 12:
text += "เที่ยง"
elif h < 19:
text += num_to_thaiword(h - 12) + "โมง"
else:
text += num_to_thaiword(h - 18) + "ทุ่ม"
return text
def _format_24h(h: int) -> str:
"""Spell out an hour on the 24-hour clock."""
text = num_to_thaiword(h) + "นาฬิกา"
return text
# Hour formatters by output format name; looked up with ``==``
_HOUR_FORMATTERS: tuple[tuple[str, Callable[[int], str]], ...] = (
("6h", _format_6h),
("m6h", _format_m6h),
("24h", _format_24h),
)
_HALF_HOUR_FORMATS: tuple[str, str] = ("6h", "m6h")
def _format_with_precision(
m: int, s: int, fmt: str, precision: Optional[str]
) -> str:
"""Spell out minutes and seconds when precision is "m" or "s"."""
if m == 30 and (s == 0 or precision == "m") and fmt in _HALF_HOUR_FORMATS:
return "ครึ่ง"
text = num_to_thaiword(m) + "นาที"
if precision == "s":
text += num_to_thaiword(s) + "วินาที"
return text
def _format_non_zero(m: int, s: int, fmt: str) -> str:
"""Spell out only the non-zero minutes and seconds."""
text = ""
if m:
if m == 30 and s == 0 and fmt in _HALF_HOUR_FORMATS:
text += "ครึ่ง"
else:
text += num_to_thaiword(m) + "นาที"
if s:
text += num_to_thaiword(s) + "วินาที"
return text
def _format(
h: int,
m: int,
s: int,
fmt: str = "24h",
precision: Optional[str] = None,
) -> str:
for name, formatter in _HOUR_FORMATTERS:
if fmt == name:
text = formatter(h)
break
else:
raise NotImplementedError(f"Time format not supported: {fmt}")
if precision in ("m", "s"):
return text + _format_with_precision(m, s, fmt, precision)
return text + _format_non_zero(m, s, fmt)
[docs]
def time_to_thaiword(
time_data: Union[time, datetime, str],
fmt: str = "24h",
precision: Optional[str] = None,
) -> str:
"""
Spell out time as Thai words.
:param Union[datetime.time, datetime.datetime, str] time_data:
a :class:`datetime.time` object, a :class:`datetime.datetime`
object, or a string in ``H:M`` or ``H:M:S`` format
(24-hour clock)
:param str fmt: output format
* *24h* - 24-hour clock (default)
* *6h* - 6-hour clock
* *m6h* - modified 6-hour clock
:param Optional[str] precision: precision of the spelled-out time
* *m* - always spell out to the minute
* *s* - always spell out to the second
* None - spell out only non-zero parts (default)
:return: time spelled out as Thai words
:rtype: str
:raises TypeError: if time_data is not a datetime.time,
datetime.datetime, or str
:raises ValueError: if time_data is an empty string or does not
match the H:M or H:M:S format
:Example:
>>> from datetime import time
>>> from pythainlp.util import time_to_thaiword
>>> time_to_thaiword("8:17")
'แปดนาฬิกาสิบเจ็ดนาที'
>>> time_to_thaiword("8:17", "6h")
'สองโมงเช้าสิบเจ็ดนาที'
>>> time_to_thaiword("8:17", "m6h")
'แปดโมงสิบเจ็ดนาที'
>>> time_to_thaiword("18:30", fmt="m6h")
'หกโมงครึ่ง'
>>> time_to_thaiword(time(12, 3, 0))
'สิบสองนาฬิกาสามนาที'
>>> time_to_thaiword(time(12, 3, 0), precision="s")
'สิบสองนาฬิกาสามนาทีศูนย์วินาที'
"""
_time = None
if isinstance(time_data, (time, datetime)):
_time = time_data
else:
if not isinstance(time_data, str):
raise TypeError(
"Time input must be a datetime.time object, "
"a datetime.datetime object, or a string."
)
if not time_data:
raise ValueError("Time string cannot be empty.")
try:
_time = datetime.strptime(time_data, _TIME_FORMAT_WITH_SEC)
except ValueError:
with suppress(ValueError):
_time = datetime.strptime(time_data, _TIME_FORMAT_WITHOUT_SEC)
if not _time:
raise ValueError(
f"Time string '{time_data}' does not match H:M or H:M:S format."
)
text = _format(_time.hour, _time.minute, _time.second, fmt, precision)
return text
_TI_HOURS: tuple[str, ...] = (
"ตีหนึ่ง",
"ตีสอง",
"ตีสาม",
"ตีสี่",
"ตีห้า",
"ตีหก",
)
def _mark_affix(text: str) -> str:
"""
Insert "|" after the hour affix; return "" if none is found.
Affixes are tried in order. A non-"ตี" affix ends the search;
a "ตี" match does not, so a later affix can override it.
"""
marked = ""
for affix in _THAI_TIME_AFFIX:
if affix not in text:
continue
if affix != "ตี":
return text.replace(affix, affix + "|")
for ti_hour in _TI_HOURS:
if ti_hour in text:
marked = text.replace(ti_hour, ti_hour + "|")
break
return marked
def _is_evening(last: str) -> bool:
return last == "โมงเย็น" or last == "โมง"
def _hour_from_morning_six(hour: list[str]) -> str:
value = _DICT_THAI_TIME[hour[0]]
return str(value + 6 if value < 6 else value)
def _hour_from_thum(hour: list[str]) -> Optional[str]:
"""Convert hours ending in ทุ่ม; None if the first token is unknown."""
if len(hour) == 1:
return "19"
if hour[0] not in _DICT_THAI_TIME:
return None
return str(_DICT_THAI_TIME[hour[0]] + 18)
def _hour_from_unit(hour: list[str]) -> Optional[str]:
"""Convert hours ending in นาฬิกา, ตี, โมง; None if no rule matches."""
if hour[-1] == "นาฬิกา" and hour[0] in _DICT_THAI_TIME and hour[:-1]:
return str(thaiword_to_num("".join(hour[:-1])))
if hour[0] == "ตี" and hour[1] in _DICT_THAI_TIME:
return str(_DICT_THAI_TIME[hour[1]])
if hour[-1] == "โมงเช้า" and hour[0] in _DICT_THAI_TIME:
return _hour_from_morning_six(hour)
if _is_evening(hour[-1]) and hour[0] == "บ่าย":
if hour[1] not in _DICT_THAI_TIME:
return None
return str(_DICT_THAI_TIME[hour[1]] + 12)
if _is_evening(hour[-1]) and hour[0] in _DICT_THAI_TIME:
return str(_DICT_THAI_TIME[hour[0]] + 12)
return None
def _hour_from_name(hour: list[str]) -> Optional[str]:
"""Convert named hours (เที่ยง, บ่ายโมง, ทุ่ม); None if no rule matches."""
if hour[-1] == "เที่ยงคืน":
return "0"
if hour[-1] == "เที่ยงวัน" or hour[-1] == "เที่ยง":
return "12"
if hour[0] == "บ่ายโมง":
return "13"
if hour[-1] == "ทุ่ม":
return _hour_from_thum(hour)
return None
def _hour_text(hour: list[str]) -> str:
"""
Convert hour tokens to the hour number; "" if no rule matches.
The first matching rule wins; an earlier rule shadows a later one.
"""
text = _hour_from_unit(hour)
if text is None:
text = _hour_from_name(hour)
return text if text is not None else ""
def _minute_text(minute: Union[list[str], int]) -> str:
"""Convert minute tokens to a two-digit (or longer) minute string."""
if not (minute and isinstance(minute, list)):
return "00"
n = 0
for affix in minute:
if affix not in _DICT_THAI_TIME:
continue
if affix != "สิบ":
n += _DICT_THAI_TIME[affix]
elif n != 0:
n *= 10
else:
n += 10
if n > 9:
return str(n)
return "0" + str(n)
[docs]
def thaiword_to_time(text: str, padding: bool = True) -> str:
"""
Convert Thai time in words to a time string (H:M).
:param str text: Thai time in words
:param bool padding: zero-pad the hour if True
:return: time string
:rtype: str
:raises ValueError: if the text is not a valid Thai time
:Example:
>>> from pythainlp.util import thaiword_to_time
>>> thaiword_to_time("บ่ายโมงครึ่ง")
'13:30'
"""
text = text.replace("กว่า", "").replace("ๆ", "").replace(" ", "")
marked = _mark_affix(text)
if "|" not in marked:
raise ValueError("Cannot find any Thai word for time affix.")
hour_raw, minute_raw = marked.split("|")[:2]
hour = _thai_time_cut().word_tokenize(hour_raw)
minute: Union[list[str], int]
if len(minute_raw) > 1:
minute = _thai_time_cut().word_tokenize(minute_raw)
else:
minute = 0
hour_text = _hour_text(hour)
if not hour_text:
raise ValueError("Cannot find any Thai word for hour.")
if padding and len(hour_text) == 1:
hour_text = "0" + hour_text
return hour_text + ":" + _minute_text(minute)