Source code for pythainlp.util.date

# SPDX-FileCopyrightText: 2016-2026 PyThaiNLP Project
# SPDX-FileType: SOURCE
# SPDX-License-Identifier: Apache-2.0
"""
Thai date and time conversion.

Note: It does not take into account the change of New Year's Day in
Thailand.
"""

from __future__ import annotations

from typing import Optional, Union

__all__: list[str] = [
    "convert_years",
    "thai_abbr_months",
    "thai_abbr_weekdays",
    "thai_full_months",
    "thai_full_weekdays",
    "thai_strptime",
    "thaiword_to_date",
]

import re
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo

from pythainlp.util._calendar import _ERA_OFFSETS_FROM_BE

thai_abbr_weekdays: list[str] = ["จ", "อ", "พ", "พฤ", "ศ", "ส", "อา"]
thai_full_weekdays: list[str] = [
    "วันจันทร์",
    "วันอังคาร",
    "วันพุธ",
    "วันพฤหัสบดี",
    "วันศุกร์",
    "วันเสาร์",
    "วันอาทิตย์",
]

thai_abbr_months: list[str] = [
    "ม.ค.",
    "ก.พ.",
    "มี.ค.",
    "เม.ย.",
    "พ.ค.",
    "มิ.ย.",
    "ก.ค.",
    "ส.ค.",
    "ก.ย.",
    "ต.ค.",
    "พ.ย.",
    "ธ.ค.",
]
thai_full_months: list[str] = [
    "มกราคม",
    "กุมภาพันธ์",
    "มีนาคม",
    "เมษายน",
    "พฤษภาคม",
    "มิถุนายน",
    "กรกฎาคม",
    "สิงหาคม",
    "กันยายน",
    "ตุลาคม",
    "พฤศจิกายน",
    "ธันวาคม",
]
thai_full_month_lists: list[list[str]] = [
    ["มกราคม", "มกรา", "ม.ค.", "01", "1"],
    ["กุมภาพันธ์", "กุมภา", "ก.พ.", "02", "2"],
    ["มีนาคม", "มีนา", "มี.ค.", "03", "3"],
    ["เมษายน", "เมษา", "เม.ย.", "04", "4"],
    ["พฤษภาคม", "พฤษภา", "พ.ค.", "05", "5"],
    ["มิถุนายน", "มิถุนา", "มิ.ย.", "06", "6"],
    ["กรกฎาคม", "ก.ค.", "07", "7"],
    ["สิงหาคม", "สิงหา", "ส.ค.", "08", "8"],
    ["กันยายน", "กันยา", "ก.ย.", "09", "9"],
    ["ตุลาคม", "ตุลา", "ต.ค.", "10"],
    ["พฤศจิกายน", "พฤศจิกา", "พ.ย.", "11"],
    ["ธันวาคม", "ธันวา", "ธ.ค.", "12"],
]
thai_full_month_lists_regex: str = (
    "(" + "|".join(["|".join(i) for i in thai_full_month_lists]) + ")"
)
year_all_regex: str = r"(\d\d\d\d|\d\d)"
dates_list: str = (
    "("
    + "|".join(
        list(map(str, range(32, 0, -1))) + ["0" + str(i) for i in range(1, 10)]
    )
    + ")"
)

_DAY: dict[str, int] = {
    "วันนี้": 0,
    "คืนนี้": 0,
    "พรุ่งนี้": 1,
    "วันพรุ่งนี้": 1,
    "คืนถัดจากนี้": 1,
    "คืนหน้า": 1,
    "มะรืน": 2,
    "มะรืนนี้": 2,
    "วันมะรืนนี้": 2,
    "ถัดจากพรุ่งนี้": 2,
    "ถัดจากวันพรุ่งนี้": 2,
    "เมื่อวาน": -1,
    "เมื่อวานนี้": -1,
    "วานนี้": -1,
    "เมื่อคืน": -1,
    "เมื่อคืนนี้": -1,
    "วานซืน": -2,
    "เมื่อวานซืน": -2,
    "เมื่อวานของเมื่อวาน": -2,
}


def _is_known_era(era: object) -> bool:
    return isinstance(era, str) and era in _ERA_OFFSETS_FROM_BE


[docs] def convert_years(year: str, src: str = "be", target: str = "ad") -> str: """ Convert a year from one era to another. :param str year: year, as an integer string :param str src: source era :param str target: target era :return: converted year :rtype: str :raises NotImplementedError: if ``src`` or ``target`` is not a supported era :raises ValueError: if ``year`` is not an integer string Options for ``src`` and ``target``: * *be* - Buddhist Era * *ad* - Anno Domini * *re* - Rattanakosin Era * *ah* - Anno Hegirae **Warning**: This function works properly only after 1941, because Thailand changed its calendar in 1941. Historians need to take the correct calendar into account. :Example: >>> from pythainlp.util import convert_years >>> # Convert Buddhist Era (BE) to Anno Domini (AD) >>> convert_years("2566", src="be", target="ad") '2023' >>> # Convert AD to BE >>> convert_years("2023", src="ad", target="be") '2566' >>> # Convert BE to Rattanakosin Era (RE) >>> convert_years("2566", src="be", target="re") '242' >>> # The same era returns the year as a normalized integer string >>> convert_years("2566", src="be", target="be") '2566' """ if not _is_known_era(src) or not _is_known_era(target): raise NotImplementedError( f"This function doesn't support {src} to {target}" ) be_year = int(year) - _ERA_OFFSETS_FROM_BE[src] return str(be_year + _ERA_OFFSETS_FROM_BE[target])
def _find_month(text: str) -> int: for i, m in enumerate(thai_full_month_lists): for j in m: if j in text: return i + 1 return 0 # Not found in list # Directives of thai_strptime() and their regular expressions. # The order matters: each replacement is applied in turn. _STRPTIME_REGEX: tuple[tuple[str, str], ...] = ( ("%d", dates_list), ("%B", thai_full_month_lists_regex), ("%Y", year_all_regex), ("%H", r"(\d\d|\d)"), ("%M", r"(\d\d|\d)"), ("%S", r"(\d\d|\d)"), ("%f", r"(\d+)"), ) _TIME_KEYS: tuple[str, ...] = ("H", "M", "S", "f") def _fmt_to_regex(fmt: str) -> str: for directive, regex in _STRPTIME_REGEX: fmt = fmt.replace(directive, regex) return fmt def _fmt_keys(fmt: str) -> list[str]: """Names of the directives in ``fmt``, with the separators removed.""" return [ i.strip().strip("-").strip(":").strip(".") for i in fmt.split("%") if i != "" ] def _full_year(y: str, year: str, add_year: Optional[int]) -> str: """ Normalize a year text, and convert a BE year to an AD year. Add a year below 100 to ``add_year``, or to 2500 (BE) or 2000 (AD) if ``add_year`` is None. """ if int(y) < 100 and year in ("be", "ad"): if add_year is None: add_year = 2500 if year == "be" else 2000 y = str(int(add_year) + int(y)) if year == "be": y = convert_years(y, src="be", target="ad") return y
[docs] def thai_strptime( text: str, fmt: str, year: str = "be", add_year: Optional[int] = None, tzinfo: Optional[ZoneInfo] = ZoneInfo("Asia/Bangkok"), # noqa: B008 ) -> datetime: """ Parse Thai date and time text into a :class:`datetime.datetime`. :param str text: text containing date and time :param str fmt: string containing date and time directives :param str year: era of the year in the text (*ad* for Anno Domini, *be* for Buddhist Era) :param Optional[int] add_year: year to add to a two-digit year (default is None) :param Optional[zoneinfo.ZoneInfo] tzinfo: time zone (default is Asia/Bangkok) :return: parsed date and time :rtype: datetime.datetime Supported directives in ``fmt``: * *%d* - Day (1 - 31) * *%B* - Thai month (03, 3, มี.ค., or มีนาคม) * *%Y* - Year (66, 2566, or 2023) * *%H* - Hour (0 - 23) * *%M* - Minute (0 - 59) * *%S* - Second (0 - 59) * *%f* - Microsecond :Example: >>> from pythainlp.util import thai_strptime >>> thai_strptime("15 ก.ค. 2565 09:00:01", "%d %B %Y %H:%M:%S") datetime.datetime(2022, 7, 15, 9, 0, 1, tzinfo=zoneinfo.ZoneInfo(key='Asia/Bangkok')) """ fmt = fmt.replace("%-m", "%m") fmt = fmt.replace("%-d", "%d") fmt = fmt.replace("%b", "%B") fmt = fmt.replace("%-y", "%y") keys = _fmt_keys(fmt) y_matches = re.findall(_fmt_to_regex(fmt), text) data = {i: "".join(list(j)) for i, j in zip(keys, y_matches[0])} d = data["d"] m: int = _find_month(data["B"]) y = data["Y"] time_parts: list[Union[int, str]] = [ data[name] if name in keys else 0 for name in _TIME_KEYS ] hour, minute, second, f = time_parts y = _full_year(y, year, add_year) return datetime( year=int(y), month=m, day=int(d), hour=int(hour), minute=int(minute), second=int(second), microsecond=int(f), tzinfo=tzinfo, )
[docs] def now_reign_year() -> int: """ Return the current reign year of the 10th King of Chakri dynasty. :return: reign year of the 10th King of Chakri dynasty :rtype: int :Example: >>> from pythainlp.util import now_reign_year # doctest: +SKIP >>> text = "เป็นปีที่ {reign_year} ในรัชกาลปัจจุบัน"\\ # doctest: +SKIP ... .format(reign_year=now_reign_year()) >>> print(text) # doctest: +SKIP เป็นปีที่ 11 ในรัชกาลปัจจุบัน """ now_ = datetime.now() return now_.year - 2015 # hard coded
[docs] def reign_year_to_ad(reign_year: int, reign: int) -> int: """ Convert a reign year to an AD year. Return the AD year for a reign year of the 7th to 10th King of Chakri dynasty, Thailand. For instance, the AD year of the 4th reign year of the 10th King is 2019. :param int reign_year: reign year of the King :param int reign: reign of the King (7, 8, 9, or 10) :return: AD year of the given reign and reign year :rtype: int :Example: >>> from pythainlp.util import reign_year_to_ad >>> print( ... "The 4th reign year of the King Rama X is in", ... reign_year_to_ad(4, 10), ... ) The 4th reign year of the King Rama X is in 2019 >>> print( ... "The 1st reign year of the King Rama IX is in", ... reign_year_to_ad(1, 9), ... ) The 1st reign year of the King Rama IX is in 1946 """ ad = 0 if int(reign) == 10: ad = int(reign_year) + 2015 elif int(reign) == 9: ad = int(reign_year) + 1945 elif int(reign) == 8: ad = int(reign_year) + 1928 elif int(reign) == 7: ad = int(reign_year) + 1924 return ad
[docs] def thaiword_to_date( text: str, date: Optional[datetime] = None ) -> Optional[datetime]: """ Convert Thai relative date to :class:`datetime.datetime`. :param str text: Thai text containing a relative date :param datetime.datetime date: reference date (default is datetime.datetime.now()) :return: date and time if it can be calculated, otherwise None :rtype: Optional[datetime.datetime] :Example: >>> from datetime import datetime >>> from pythainlp.util import thaiword_to_date >>> thaiword_to_date("พรุ่งนี้", datetime(2024, 1, 31)) datetime.datetime(2024, 2, 1, 0, 0) >>> thaiword_to_date("เมื่อวาน", datetime(2024, 1, 31)) datetime.datetime(2024, 1, 30, 0, 0) >>> print(thaiword_to_date("ไม่มีคำนี้")) None """ if text not in _DAY: return None day_num = _DAY.get(text, 0) if not date: date = datetime.now() return date + timedelta(days=day_num)