import datetime
import time
import sys
import math
try:
from typing import Literal, Optional, Union
except ImportError:
from typing_extensions import Literal, Optional, Union
from pawnlib.typing.converter import append_zero
from pawnlib.typing.constants import const
from functools import lru_cache
if sys.version_info >= (3, 9):
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
USE_ZONEINFO = True
else:
USE_ZONEINFO = False
try:
import pytz
except ImportError:
pytz = None
[docs]class TimeCalculator:
"""
A class to convert seconds into various human-readable string formats.
:param seconds: The time in seconds to be converted (integer or float).
Example:
.. code-block:: python
tc = TimeCalculator(1224411.5)
print(tc.to_strings()) # "14 days, 04:06:51"
print(tc.to_strings(include_ms=True)) # "14 days, 04:06:51.500"
print(tc.to_minutes()) # 20406
"""
def __init__(self, seconds: Union[int, float] = 0):
self._seconds = 0.0
self._days = 0
self._hours = 0
self._minutes = 0
self._remaining_seconds = 0.0
self.hhmmss = ""
self.set_seconds(seconds)
[docs] def set_seconds(self, seconds: Union[int, float]) -> None:
"""
Set the seconds value and recalculate the time components.
:param seconds: The time in seconds (integer or float).
:raises TypeError: If seconds is not an int or float.
"""
if not isinstance(seconds, (int, float)):
raise TypeError(f"seconds must be an int or float, got {type(seconds).__name__}")
self._seconds = float(seconds) # Convert to float to handle decimals
self.calculate()
[docs] def calculate(self) -> None:
"""
Calculate the days, hours, minutes, and remaining seconds from the given seconds.
"""
seconds = self._seconds
self._days = math.floor(seconds / const.DAY_IN_SECONDS)
seconds -= self._days * const.DAY_IN_SECONDS
self._hours = math.floor(seconds / const.HOUR_IN_SECONDS)
seconds -= self._hours * const.HOUR_IN_SECONDS
self._minutes = math.floor(seconds / const.MINUTE_IN_SECONDS)
self._remaining_seconds = seconds - self._minutes * const.MINUTE_IN_SECONDS
self.hhmmss = self._format_hhmmss()
def _format_hhmmss(self, include_ms: bool = False) -> str:
"""
Format the time components into a string.
:param include_ms: Whether to include milliseconds in the output.
:return: The formatted time string.
"""
if include_ms:
ms = int((self._remaining_seconds % 1) * 1000)
sec = int(self._remaining_seconds)
hhmmss = f"{self._hours:02d}:{self._minutes:02d}:{sec:02d}.{ms:03d}"
else:
hhmmss = f"{self._hours:02d}:{self._minutes:02d}:{int(self._remaining_seconds):02d}"
if self._days:
day_unit = "day" if self._days == 1 else "days"
return f"{self._days} {day_unit}, {hhmmss}"
return hhmmss
[docs] def to_strings(self, format_type: Literal["default", "detailed"] = "default", include_ms: bool = False) -> str:
"""
Return the calculated time as a string.
:param format_type: "default" for "days HH:MM:SS", "detailed" for "X days Y hours Z minutes W seconds".
:param include_ms: Whether to include milliseconds.
:return: The formatted time string.
"""
if format_type == "detailed":
ms_part = f".{int((self._remaining_seconds % 1) * 1000):03d}" if include_ms else ""
return (f"{self._days} days {self._hours} hours {self._minutes} minutes "
f"{int(self._remaining_seconds)}{ms_part} seconds")
return self._format_hhmmss(include_ms)
[docs] def to_seconds(self) -> int:
"""Convert seconds to total seconds."""
return int(self._seconds)
[docs] def to_minutes(self) -> int:
"""Convert seconds to total minutes."""
return int(self._seconds // const.MINUTE_IN_SECONDS)
[docs] def to_hours(self) -> int:
"""Convert seconds to total hours."""
return int(self._seconds // const.HOUR_IN_SECONDS)
[docs] def to_days(self) -> int:
"""Convert seconds to total days."""
return int(self._seconds // const.DAY_IN_SECONDS)
[docs] def to_weeks(self) -> int:
"""Convert seconds to total weeks."""
return int(self._seconds // (const.DAY_IN_SECONDS * 7))
[docs] @classmethod
def from_hhmmss(cls, hhmmss: str) -> 'TimeCalculator':
"""
Create a TimeCalculator object from an "HH:MM:SS" string.
:param hhmmss: Time string in "HH:MM:SS" format.
:return: A TimeCalculator object.
"""
h, m, s = map(int, hhmmss.split(':'))
seconds = h * const.HOUR_IN_SECONDS + m * const.MINUTE_IN_SECONDS + s
return cls(seconds)
def __str__(self) -> str:
return self.to_strings()
def __repr__(self) -> str:
return f"TimeCalculator(seconds={self._seconds}, hhmmss='{self.hhmmss}')"
[docs]def convert_unix_timestamp(date_param):
"""
Convert a date parameter to a Unix timestamp.
:param date_param: A date parameter to be converted to a Unix timestamp.
:type date_param: datetime.datetime or int
:return: A Unix timestamp.
:rtype: int
Example:
.. code-block:: python
import datetime
# Convert a datetime object to a Unix timestamp.
date = datetime.datetime(2022, 1, 1, 0, 0, 0)
convert_unix_timestamp(date)
# >> 1640995200
# Convert an integer to a Unix timestamp.
date = 1640995200
convert_unix_timestamp(date)
# >> 1640995200
"""
if isinstance(date_param, datetime.datetime):
return int(date_param.timestamp())
if int(date_param) > 1:
return int(date_param)
return 0
[docs]def get_range_day_of_month(year: int, month: int, return_unix: bool = True):
"""
This functions will be returned first_day and last_day in parameter.
:param year:
:param month:
:param return_unix:
:return: (1646060400, 1648738799)
Example:
.. code-block:: python
from pawnlib.typing import date_utils
date_utils.get_range_day_of_month(year=2022, month=3, return_unix=False)
# >> ('2022-3-01 00:00:00', '2022-03-31 23:59:59')
"""
dst_date = datetime.date(year, month, 1)
next_month = dst_date.replace(day=28) + datetime.timedelta(days=4)
first_day = f"{year}-{month}-01 00:00:00"
last_day = f"{next_month - datetime.timedelta(days=next_month.day)} 23:59:59"
if return_unix:
first_day = int(time.mktime(datetime.datetime.strptime(first_day, "%Y-%m-%d %H:%M:%S").timetuple()))
last_day = int(time.mktime(datetime.datetime.strptime(str(last_day), "%Y-%m-%d %H:%M:%S").timetuple()))
return first_day, last_day
[docs]@lru_cache(maxsize=128)
def get_timezone(timezone: str):
"""Cache timezone objects for performance."""
if USE_ZONEINFO:
return ZoneInfo(timezone)
elif pytz:
try:
return pytz.timezone(timezone)
except pytz.exceptions.UnknownTimeZoneError:
raise ValueError(f"Unknown timezone: {timezone}")
return None
[docs]def todaydate(
date_type: Optional[Literal[
"file", "md", "time", "time_sec", "hour", "ms", "log",
"log_ms", "ms_text", "unix", "ms_unix"
]] = None,
target_datetime: datetime.datetime = None,
timezone: Optional[str] = None
)-> str:
"""
Returns today's date as a formatted string based on the specified type.
Args:
date_type: Optional format type. Options are:
- None: "YYYYMMDD" (default)
- "file": "YYYYMMDD_HHMM"
- "md": "MMDD"
- "time": "HH:MM:SS.sss"
- "time_sec": "HH:MM:SS"
- "hour": "HHMM"
- "ms" or "log" or "log_ms": "YYYY-MM-DD HH:MM:SS.sss"
- "ms_text": "YYYYMMDD-HHMMSSsss"
- "unix": Hexadecimal Unix timestamp (seconds)
- "ms_unix": Hexadecimal Unix timestamp (microseconds)
target_datetime: Optional datetime object. Defaults to current time if None.
timezone: Optional timezone name (e.g., "Asia/Seoul"). Uses system timezone if None or unsupported.
Returns:
str: Formatted date string.
Examples:
>>> todaydate("ms_unix")
'0x5e66be4a93496'
>>> todaydate("log")
'2025-03-25 12:34:56.789'
"""
tz = get_timezone(timezone) if timezone else None
now = target_datetime if target_datetime is not None else datetime.datetime.now(tz)
formats = {
None: "%Y%m%d",
"md": "%m%d",
"file": "%Y%m%d_%H%M",
"time": "%H:%M:%S.%f",
"time_sec": "%H:%M:%S",
"hour": "%H%M",
"ms": "%Y-%m-%d %H:%M:%S.%f",
"log": "%Y-%m-%d %H:%M:%S.%f",
"log_ms": "%Y-%m-%d %H:%M:%S.%f",
"ms_text": "%Y%m%d-%H%M%S%f",
}
if not isinstance(now, datetime.datetime):
raise TypeError(f"target_datetime must be a datetime object, got {type(now).__name__}")
if date_type in formats:
result = now.strftime(formats[date_type])
return result[:-3] if date_type in {"time", "ms", "log", "log_ms", "ms_text"} else result
if date_type == "unix":
return hex(int(now.timestamp()))
if date_type == "ms_unix":
return hex(int(now.timestamp() * 1_000_000))
raise ValueError(f"Unsupported date_type: {date_type}")
[docs]def timestamp_to_string(unix_timestamp: int, str_format: str = '%Y-%m-%d %H:%M:%S', tz: Optional[Union[str, datetime.tzinfo]] = None) -> str:
"""
Converts a Unix timestamp to a formatted string based on local timezone or specified timezone.
:param unix_timestamp: Unix timestamp (seconds, milliseconds, or microseconds)
:param str_format: Output string format (default: '%Y-%m-%d %H:%M:%S')
:param tz: Timezone as string (e.g., 'UTC', 'Asia/Seoul') or tzinfo object (default: None, uses local timezone)
:return: Formatted datetime string
Example:
.. code-block:: python
from pawnlib.typing import date_utils
# Local timezone (default)
date_utils.timestamp_to_string(12323232323)
# >> '2360-07-05 09:05:23' (local timezone, e.g., KST)
# UTC timezone
date_utils.timestamp_to_string(12323232323, tz='UTC')
# >> '2360-07-05 00:05:23' (UTC)
# Asia/Seoul timezone
date_utils.timestamp_to_string(12323232323, tz='Asia/Seoul')
# >> '2360-07-05 09:05:23' (Asia/Seoul)
"""
if isinstance(unix_timestamp, str):
unix_timestamp = unix_timestamp.strip()
if not unix_timestamp.isdigit():
raise ValueError(f"Invalid timestamp format: {unix_timestamp} ({type(unix_timestamp)})")
ts_length = len(str(unix_timestamp))
if ts_length == const.SECONDS_DIGITS:
timestamp_in_seconds = int(unix_timestamp)
elif ts_length == const.MILLI_SECONDS_DIGITS:
timestamp_in_seconds = int(unix_timestamp) / 1_000
elif ts_length == const.MICRO_SECONDS_DIGITS:
timestamp_in_seconds = int(unix_timestamp) / 1_000_000
else:
raise ValueError(f'Invalid timestamp length - length={ts_length}, timestamp={unix_timestamp}')
if isinstance(tz, str):
try:
timezone = get_timezone(tz)
except ZoneInfoNotFoundError as e:
raise ValueError(f"Unsupported timezone: {tz} ({e})")
else:
timezone = tz
dt = datetime.datetime.fromtimestamp(timestamp_in_seconds, tz=timezone)
return dt.strftime(str_format)
[docs]def second_to_dayhhmm(seconds: int = 0):
"""
This functions will be returned unix timestamp to days hh:mm:ss format
:param seconds:
:return:
Example:
.. code-block:: python
from pawnlib.typing import date_utils
date_utils.second_to_dayhhmm(2132323)
# >> '24 days 16:18:43'
"""
day = int(seconds // const.DAY_IN_SECONDS)
input_time = int(seconds % const.DAY_IN_SECONDS)
hour = input_time // const.HOUR_IN_SECONDS
input_time %= const.HOUR_IN_SECONDS
minutes = input_time // const.MINUTE_IN_SECONDS
input_time %= const.MINUTE_IN_SECONDS
sec = input_time
return f"{day} days {append_zero(hour)}:{append_zero(minutes)}:{append_zero(sec)}"