Source code for pythainlp.util.thai_lunar_date

# -*- coding: utf-8 -*-
# SPDX-FileCopyrightText: 2016-2025 PyThaiNLP Project
# SPDX-FileType: SOURCE
# SPDX-License-Identifier: Apache-2.0
"""
This file is a port from
> https://gist.github.com/touchiep/99f4f5bb349d6b983ef78697630ab78e
"""

from datetime import date, timedelta
from typing import Dict, List, Tuple, Union

_YEAR_DEV: Dict[int, float] = {
    0: 0,
    1901: 0.122733000004352,
    1906: 1.91890000045229e-02,
    1911: -8.43549999953059e-02,
    1916: -0.187898999995135,
    1921: -0.291442999994964,
    1926: 7.44250000052413e-02,
    1931: -2.91189999945876e-02,
    1936: -0.132662999994416,
    1941: -0.236206999994245,
    1946: -0.339750999994074,
    1951: -0.443294999993903,
    1956: -7.74269999936981e-02,
    1961: -0.180970999993527,
    1966: -0.284514999993356,
    1971: -0.388058999993185,
    1976: -0.491602999993014,
    1981: -0.595146999992842,
    1986: -0.698690999992671,
    1991: -0.332822999992466,
    1996: -0.436366999992295,
    2001: -0.539910999992124,
    2006: -0.643454999991953,
    2011: 0.253001000008218,
    2016: 0.149457000008389,
    2021: -0.484674999991406,
    2026: -0.588218999991235,
    2031: 0.308237000008937,
    2036: 0.204693000009108,
    2041: 0.101149000009279,
    2046: -2.39499999055015e-03,
    2051: -0.105938999990379,
    2056: 0.259929000009826,
    2061: 0.156385000009997,
    2066: 5.28410000101682e-02,
    2071: -5.07029999896607e-02,
    2076: -0.15424699998949,
    2081: -0.257790999989318,
    2086: 0.108077000010887,
    2091: 4.53300001105772e-03,
    2096: -9.90109999887712e-02,
    2101: -0.2025549999886,
    2106: -0.306098999988429,
    2111: -0.409642999988258,
    2116: -4.37749999880528e-02,
    2121: -0.147318999987882,
    2126: -0.250862999987711,
    2131: -0.354406999987539,
    2136: -0.457950999987368,
    2141: -0.561494999987197,
    2146: -0.665038999987026,
    2151: -0.299170999986821,
    2156: -0.40271499998665,
    2161: -0.506258999986479,
    2166: -0.609802999986308,
    2171: -0.713346999986137,
    2176: 0.183109000014035,
    2181: -0.45102299998576,
    2186: -0.554566999985589,
    2191: 0.341889000014582,
    2196: 0.238345000014753,
    2201: 0.134801000014924,
    2206: 3.12570000150951e-02,
    2211: -7.22869999847338e-02,
    2216: 0.293581000015471,
    2221: 0.190037000015642,
    2226: 8.64930000158135e-02,
    2231: -1.70509999840154e-02,
    2236: -0.120594999983844,
    2241: -0.224138999983673,
    2246: 0.141729000016532,
    2251: 0.038185000016703,
    2256: -6.53589999831259e-02,
    2261: -0.168902999982955,
    2266: -0.272446999982784,
    2271: -0.375990999982613,
    2276: -1.01229999824075e-02,
    2281: -0.113666999982236,
    2286: -0.217210999982065,
    2291: -0.320754999981894,
    2296: -0.424298999981723,
    2301: -0.527842999981552,
    2306: -0.631386999981381,
    2311: -0.265518999981176,
    2316: -0.369062999981005,
    2321: -0.472606999980834,
    2326: -0.576150999980662,
    2331: -0.679694999980491,
    2336: 0.21676100001968,
    2341: -0.417370999980115,
    2346: -0.520914999979944,
    2351: -0.624458999979773,
    2356: 0.271997000020398,
    2361: 0.168453000020569,
    2366: 6.49090000207404e-02,
    2371: -3.86349999790885e-02,
    2376: 0.327233000021117,
    2381: 0.223689000021288,
    2386: 0.120145000021459,
    2391: 1.66010000216299e-02,
    2396: -0.086942999978199,
    2401: -0.190486999978028,
    2406: 0.175381000022177,
    2411: 7.18370000223483e-02,
    2416: -3.17069999774806e-02,
    2421: -0.135250999977309,
    2426: -0.238794999977138,
    2431: -0.342338999976967,
    2436: 2.35290000232378e-02,
    2441: -8.00149999765911e-02,
    2446: -0.18355899997642,
    2451: -0.287102999976249,
    2456: -0.390646999976078,
}

_BEGIN_DATES = [
    date(1902, 11, 30),
    date(1912, 12, 8),
    date(1922, 11, 19),
    date(1932, 11, 27),
    date(1942, 12, 7),
    date(1952, 11, 16),
    date(1962, 11, 26),
    date(1972, 12, 5),
    date(1982, 11, 15),
    date(1992, 11, 24),
    date(2002, 12, 4),
    date(2012, 11, 13),
    date(2022, 11, 23),
    date(2032, 12, 2),
    date(2042, 12, 12),
    date(2052, 11, 21),
    date(2062, 12, 1),
    date(2072, 12, 9),
    date(2082, 11, 20),
    date(2092, 11, 28),
    date(2102, 12, 9),
    date(2112, 11, 18),
    date(2122, 11, 28),
    date(2132, 12, 7),
    date(2142, 11, 17),
    date(2152, 11, 26),
    date(2162, 12, 6),
    date(2172, 11, 15),
    date(2182, 11, 25),
    date(2192, 12, 4),
    date(2202, 12, 15),
    date(2212, 11, 24),
    date(2222, 12, 4),
    date(2232, 12, 12),
    date(2242, 11, 23),
    date(2252, 12, 1),
    date(2262, 12, 11),
    date(2272, 11, 20),
    date(2282, 11, 30),
    date(2292, 12, 9),
    date(2302, 11, 20),
    date(2312, 11, 29),
    date(2322, 12, 9),
    date(2332, 11, 18),
    date(2342, 11, 28),
    date(2352, 12, 7),
    date(2362, 12, 17),
    date(2372, 11, 26),
    date(2382, 12, 6),
    date(2392, 12, 14),
    date(2402, 11, 25),
    date(2412, 12, 3),
    date(2422, 12, 13),
    date(2432, 11, 23),
    date(2442, 12, 2),
    date(2452, 12, 11),
]

_DAYS_354 = [29, 30, 29, 30, 29, 30, 29, 30, 29, 30, 29, 30, 29, 30]
_DAYS_355 = [29, 30, 29, 30, 29, 30, 30, 30, 29, 30, 29, 30, 29, 30]
_DAYS_384 = [29, 30, 29, 30, 29, 30, 29, 30, 30, 29, 30, 29, 30, 29, 30]

# Zodiac names in Thai, English, and Numeric representations
_ZODIAC: Dict[int, List[Union[str, int]]] = {
    1: [
        "ชวด",
        "ฉลู",
        "ขาล",
        "เถาะ",
        "มะโรง",
        "มะเส็ง",
        "มะเมีย",
        "มะแม",
        "วอก",
        "ระกา",
        "จอ",
        "กุน",
    ],
    2: [
        "RAT",
        "OX",
        "TIGER",
        "RABBIT",
        "DRAGON",
        "SNAKE",
        "HORSE",
        "GOAT",
        "MONKEY",
        "ROOSTER",
        "DOG",
        "PIG",
    ],
    3: list(range(1, 13)),
}


def _calculate_f_year_f_dev(year: int) -> Tuple[int, float]:
    if year in _YEAR_DEV:
        return year, _YEAR_DEV[year]

    nearest_lower_year = max(y for y in _YEAR_DEV if y < year)
    return nearest_lower_year, _YEAR_DEV[nearest_lower_year]


def athikamas(year: int) -> bool:
    athi = ((year - 78) - 0.45222) % 2.7118886
    return athi < 1


def athikavar(year: int) -> bool:
    if athikamas(year):
        return False

    if athikamas(year + 1):
        cutoff = 1.69501433191599e-02
    else:
        cutoff = -1.42223099315486e-02
    return deviation(year) > cutoff


def deviation(year: int) -> float:
    curr_dev = 0.0
    last_dev = 0.0
    f_year, f_dev = _calculate_f_year_f_dev(year)
    if year == f_year:
        curr_dev = f_dev
    else:
        f_year = f_year + 1
        for i in range(f_year, year + 1):
            if i == f_year:
                last_dev = f_dev
            else:
                last_dev = curr_dev
            if athikamas(i - 1):
                curr_dev = -0.102356
            elif athikavar(i - 1):
                curr_dev = -0.632944
            else:
                curr_dev = 0.367056
            curr_dev = last_dev + curr_dev

    return curr_dev


def last_day_in_year(year: int) -> int:
    if athikamas(year):
        return 384
    elif athikavar(year):
        return 355

    return 354


def athikasurathin(year: int) -> bool:
    """
    Check if a year is a leap year in the Thai lunar calendar
    """
    # Check divisibility by 400 (divisible by 400 is always a leap year)
    if year % 400 == 0:
        return True

    # Check divisibility by 100 (divisible by 100 but not 400 is not a leap
    # year)
    elif year % 100 == 0:
        return False

    # Check divisibility by 4 (divisible by 4 but not by 100 is a leap year)
    elif year % 4 == 0:
        return True

    # All other cases are not leap years
    return False


def number_day_in_year(year: int) -> int:
    if athikasurathin(year):
        return 366

    return 365


[docs] def th_zodiac(year: int, output_type: int = 1) -> Union[str, int]: """ Thai Zodiac Year Name Converts a Gregorian year to its corresponding Zodiac name. :param int year: The Gregorian year. AD (Anno Domini) :param int output_type: Output type (1 = Thai, 2 = English, 3 = Number). :return: The Zodiac name or number corresponding to the input year. :rtype: Union[str, int] """ # Calculate zodiac index result = year % 12 if result - 3 < 1: result = result - 3 + 12 else: result = result - 3 # Return the zodiac based on the output type return _ZODIAC[output_type][result - 1]
[docs] def to_lunar_date(input_date: date) -> str: """ Convert the solar date to Thai Lunar Date :param date input_date: date of the day. :return: Thai text lunar date :rtype: str """ # Check if date is within supported range if input_date.year < 1903 or input_date.year > 2460: raise NotImplementedError("Unsupported date") # Unsupported date # Choose the nearest begin date c_year = input_date.year - 1 begin_date = _BEGIN_DATES[0] for _date in reversed(_BEGIN_DATES): if c_year > _date.year: begin_date = _date break current_date = begin_date for year in range(begin_date.year + 1, input_date.year): day_in_year = last_day_in_year(year) current_date += timedelta(days=day_in_year) r_day_prev = (date(current_date.year, 12, 31) - current_date).days day_of_year = (input_date - date(input_date.year, 1, 1)).days day_from_one = r_day_prev + day_of_year + 1 last_day = last_day_in_year(input_date.year) if last_day == 354: days_in_month = _DAYS_354 elif last_day == 355: days_in_month = _DAYS_355 elif last_day == 384: days_in_month = _DAYS_384 days_of_year = day_from_one for j, days in enumerate(days_in_month, start=1): th_m = j if 0 < days_of_year <= days: break else: days_of_year -= days if last_day <= 355: # 354 or 355 if th_m > 12: th_m = th_m - 12 elif last_day == 384: if th_m > 13: th_m = th_m - 13 if th_m >= 9 and th_m <= 13: th_m = th_m - 1 if days_of_year > 15: th_s = "แรม" days_of_year = days_of_year - 15 else: th_s = "ขึ้น" thai_lunar_date = f"{th_s} {days_of_year} ค่ำ เดือน {th_m}" return thai_lunar_date