Add datetime helpers and move all time/date time datetime_handling folder
previous string_handling located datetime and timestamp files have been moved to the datetime handling folder Update readme file with more information about currently covered areas
This commit is contained in:
18
README.md
18
README.md
@@ -1,27 +1,37 @@
|
|||||||
# CoreLibs for Python
|
# CoreLibs for Python
|
||||||
|
|
||||||
This is a pip package that can be installed into any project and covers the following pars
|
> [!warning]
|
||||||
|
> This is pre-production, location of methods and names of paths can change
|
||||||
|
|
||||||
|
This is a pip package that can be installed into any project and covers the following parts
|
||||||
|
|
||||||
- logging update with exception logs
|
- logging update with exception logs
|
||||||
- requests wrapper for easier auth pass on access
|
- requests wrapper for easier auth pass on access
|
||||||
- dict fingerprinting
|
- dict fingerprinting
|
||||||
- jmespath search
|
- jmespath search
|
||||||
- dump outputs for data
|
- json helpers for conten replace and output
|
||||||
|
- dump outputs for data for debugging
|
||||||
- progress printing
|
- progress printing
|
||||||
- string formatting, time creation, byte formatting
|
- string formatting, time creation, byte formatting
|
||||||
|
- Enum base class
|
||||||
|
- SQLite simple IO class
|
||||||
|
- Symmetric encryption
|
||||||
|
|
||||||
## Current list
|
## Current list
|
||||||
|
|
||||||
- config_handling: simple INI config file data loader with check/convert/etc
|
- config_handling: simple INI config file data loader with check/convert/etc
|
||||||
- csv_handling: csv dict writer helper
|
- csv_handling: csv dict writer helper
|
||||||
- debug_handling: various debug helpers like data dumper, timer, utilization, etc
|
- debug_handling: various debug helpers like data dumper, timer, utilization, etc
|
||||||
|
- db_handling: SQLite interface class
|
||||||
|
- encyption_handling: symmetric encryption
|
||||||
- file_handling: crc handling for file content and file names, progress bar
|
- file_handling: crc handling for file content and file names, progress bar
|
||||||
- json_handling: jmespath support and json date support
|
- json_handling: jmespath support and json date support, replace content in dict with json paths
|
||||||
- iterator_handling: list and dictionary handling support (search, fingerprinting, etc)
|
- iterator_handling: list and dictionary handling support (search, fingerprinting, etc)
|
||||||
- logging_handling: extend log and also error message handling
|
- logging_handling: extend log and also error message handling
|
||||||
- requests_handling: requests wrapper for better calls with auth headers
|
- requests_handling: requests wrapper for better calls with auth headers
|
||||||
- script_handling: pid lock file handling, abort timer
|
- script_handling: pid lock file handling, abort timer
|
||||||
- string_handling: byte format, datetime format, hashing, string formats for numbrers, double byte string format, etc
|
- string_handling: byte format, datetime format, datetime compare, hashing, string formats for numbers, double byte string format, etc
|
||||||
|
- var_handling: var type checkers, enum base class
|
||||||
|
|
||||||
## UV setup
|
## UV setup
|
||||||
|
|
||||||
|
|||||||
0
src/corelibs/datetime_handling/__init__.py
Normal file
0
src/corelibs/datetime_handling/__init__.py
Normal file
435
src/corelibs/datetime_handling/datetime_helpers.py
Normal file
435
src/corelibs/datetime_handling/datetime_helpers.py
Normal file
@@ -0,0 +1,435 @@
|
|||||||
|
"""
|
||||||
|
Various string based date/time helpers
|
||||||
|
"""
|
||||||
|
|
||||||
|
import time as time_t
|
||||||
|
from datetime import datetime, time
|
||||||
|
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
|
DAYS_OF_WEEK_LONG_TO_SHORT: dict[str, str] = {
|
||||||
|
'Monday': 'Mon',
|
||||||
|
'Tuesday': 'Tue',
|
||||||
|
'Wednesay': 'Wed',
|
||||||
|
'Thursday': 'Thu',
|
||||||
|
'Friday': 'Fri',
|
||||||
|
'Saturday': 'Sat',
|
||||||
|
'Sunday': 'Sun',
|
||||||
|
}
|
||||||
|
DAYS_OF_WEEK_ISO: dict[int, str] = {
|
||||||
|
1: 'Mon', 2: 'Tue', 3: 'Wed', 4: 'Thu', 5: 'Fri', 6: 'Sat', 7: 'Sun'
|
||||||
|
}
|
||||||
|
DAYS_OF_WEEK_ISO_REVERSED: dict[str, int] = {value: key for key, value in DAYS_OF_WEEK_ISO.items()}
|
||||||
|
|
||||||
|
|
||||||
|
def create_time(timestamp: float, timestamp_format: str = "%Y-%m-%d %H:%M:%S") -> str:
|
||||||
|
"""
|
||||||
|
just takes a timestamp and prints out humand readable format
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
timestamp {float} -- _description_
|
||||||
|
|
||||||
|
Keyword Arguments:
|
||||||
|
timestamp_format {_type_} -- _description_ (default: {"%Y-%m-%d %H:%M:%S"})
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str -- _description_
|
||||||
|
"""
|
||||||
|
return time_t.strftime(timestamp_format, time_t.localtime(timestamp))
|
||||||
|
|
||||||
|
|
||||||
|
def get_system_timezone():
|
||||||
|
"""Get system timezone using datetime's automatic detection"""
|
||||||
|
# Get current time with system timezone
|
||||||
|
local_time = datetime.now().astimezone()
|
||||||
|
|
||||||
|
# Extract timezone info
|
||||||
|
system_tz = local_time.tzinfo
|
||||||
|
timezone_name = str(system_tz)
|
||||||
|
|
||||||
|
return system_tz, timezone_name
|
||||||
|
|
||||||
|
|
||||||
|
def parse_timezone_data(timezone_tz: str = '') -> ZoneInfo:
|
||||||
|
"""
|
||||||
|
parses a string to get the ZoneInfo
|
||||||
|
If not set or not valid gets local time,
|
||||||
|
if that is not possible get UTC
|
||||||
|
|
||||||
|
Keyword Arguments:
|
||||||
|
timezone_tz {str} -- _description_ (default: {''})
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ZoneInfo -- _description_
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return ZoneInfo(timezone_tz)
|
||||||
|
except (ZoneInfoNotFoundError, ValueError, TypeError):
|
||||||
|
# use default
|
||||||
|
time_tz, time_tz_str = get_system_timezone()
|
||||||
|
if time_tz is None:
|
||||||
|
return ZoneInfo('UTC')
|
||||||
|
# TODO build proper TZ lookup
|
||||||
|
tz_mapping = {
|
||||||
|
'JST': 'Asia/Tokyo',
|
||||||
|
'KST': 'Asia/Seoul',
|
||||||
|
'IST': 'Asia/Kolkata',
|
||||||
|
'CST': 'Asia/Shanghai', # Default to China for CST
|
||||||
|
'AEST': 'Australia/Sydney',
|
||||||
|
'AWST': 'Australia/Perth',
|
||||||
|
'EST': 'America/New_York',
|
||||||
|
'EDT': 'America/New_York',
|
||||||
|
'CDT': 'America/Chicago',
|
||||||
|
'MST': 'America/Denver',
|
||||||
|
'MDT': 'America/Denver',
|
||||||
|
'PST': 'America/Los_Angeles',
|
||||||
|
'PDT': 'America/Los_Angeles',
|
||||||
|
'GMT': 'UTC',
|
||||||
|
'UTC': 'UTC',
|
||||||
|
'CET': 'Europe/Berlin',
|
||||||
|
'CEST': 'Europe/Berlin',
|
||||||
|
'BST': 'Europe/London',
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
return ZoneInfo(tz_mapping[time_tz_str])
|
||||||
|
except (ZoneInfoNotFoundError, IndexError) as e:
|
||||||
|
raise ValueError(f"No mapping for {time_tz_str}: {e}") from e
|
||||||
|
|
||||||
|
|
||||||
|
def get_datetime_iso8601(timezone_tz: str | ZoneInfo = '', sep: str = 'T', timespec: str = 'microseconds') -> str:
|
||||||
|
"""
|
||||||
|
set a datetime in the iso8601 format with microseconds
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str -- _description_
|
||||||
|
"""
|
||||||
|
# parse if this is a string
|
||||||
|
if isinstance(timezone_tz, str):
|
||||||
|
timezone_tz = parse_timezone_data(timezone_tz)
|
||||||
|
return datetime.now(timezone_tz).isoformat(sep=sep, timespec=timespec)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_date(date: str, not_before: datetime | None = None, not_after: datetime | None = None) -> bool:
|
||||||
|
"""
|
||||||
|
check if Y-m-d or Y/m/d are parsable and valid
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
date {str} -- _description_
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool -- _description_
|
||||||
|
"""
|
||||||
|
formats = ['%Y-%m-%d', '%Y/%m/%d']
|
||||||
|
for __format in formats:
|
||||||
|
try:
|
||||||
|
__date = datetime.strptime(date, __format).date()
|
||||||
|
if not_before is not None and __date < not_before.date():
|
||||||
|
return False
|
||||||
|
if not_after is not None and __date > not_after.date():
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def parse_flexible_date(
|
||||||
|
date_str: str,
|
||||||
|
timezone_tz: str | ZoneInfo | None = None,
|
||||||
|
shift_time_zone: bool = True
|
||||||
|
) -> datetime | None:
|
||||||
|
"""
|
||||||
|
Parse date string in multiple formats
|
||||||
|
will add time zone info if not None
|
||||||
|
on default it will change the TZ and time to the new time zone
|
||||||
|
if no TZ info is set in date_str, then localtime is assumed
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
date_str {str} -- _description_
|
||||||
|
|
||||||
|
Keyword Arguments:
|
||||||
|
timezone_tz {str | ZoneInfo | None} -- _description_ (default: {None})
|
||||||
|
shift_time_zone {bool} -- _description_ (default: {True})
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
datetime | None -- _description_
|
||||||
|
"""
|
||||||
|
|
||||||
|
date_str = date_str.strip()
|
||||||
|
|
||||||
|
# Try different parsing methods
|
||||||
|
parsers: list[Callable[[str], datetime]] = [
|
||||||
|
# ISO 8601 format
|
||||||
|
lambda x: datetime.fromisoformat(x), # pylint: disable=W0108
|
||||||
|
# Simple date format
|
||||||
|
lambda x: datetime.strptime(x, "%Y-%m-%d"),
|
||||||
|
# Alternative ISO formats (fallback)
|
||||||
|
lambda x: datetime.strptime(x, "%Y-%m-%dT%H:%M:%S"),
|
||||||
|
lambda x: datetime.strptime(x, "%Y-%m-%dT%H:%M:%S.%f"),
|
||||||
|
]
|
||||||
|
|
||||||
|
if timezone_tz is not None:
|
||||||
|
if isinstance(timezone_tz, str):
|
||||||
|
timezone_tz = parse_timezone_data(timezone_tz)
|
||||||
|
|
||||||
|
date_new = None
|
||||||
|
for parser in parsers:
|
||||||
|
try:
|
||||||
|
date_new = parser(date_str)
|
||||||
|
break
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if date_new is not None:
|
||||||
|
if timezone_tz is not None:
|
||||||
|
# shift time zone (default), this will change the date
|
||||||
|
# if the date has no +HH:MM it will take the local time zone as base
|
||||||
|
if shift_time_zone:
|
||||||
|
return date_new.astimezone(timezone_tz)
|
||||||
|
# just add the time zone
|
||||||
|
return date_new.replace(tzinfo=timezone_tz)
|
||||||
|
return date_new
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def compare_dates(date1_str: str, date2_str: str) -> None | bool:
|
||||||
|
"""
|
||||||
|
compare two dates, if the first one is newer than the second one return True
|
||||||
|
If the dates are equal then false will be returned
|
||||||
|
on error return None
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
date1_str {str} -- _description_
|
||||||
|
date2_str {str} -- _description_
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None | bool -- _description_
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Parse both dates
|
||||||
|
date1 = parse_flexible_date(date1_str)
|
||||||
|
date2 = parse_flexible_date(date2_str)
|
||||||
|
|
||||||
|
# Check if parsing was successful
|
||||||
|
if date1 is None or date2 is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Compare dates
|
||||||
|
return date1.date() > date2.date()
|
||||||
|
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def find_newest_datetime_in_list(date_list: list[str]) -> None | str:
|
||||||
|
"""
|
||||||
|
Find the newest date from a list of ISO 8601 formatted date strings.
|
||||||
|
Handles potential parsing errors gracefully.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
date_list (list): List of date strings in format '2025-08-06T16:17:39.747+09:00'
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: The date string with the newest/latest date, or None if list is empty or all dates are invalid
|
||||||
|
"""
|
||||||
|
if not date_list:
|
||||||
|
return None
|
||||||
|
|
||||||
|
valid_dates: list[tuple[str, datetime]] = []
|
||||||
|
|
||||||
|
for date_str in date_list:
|
||||||
|
try:
|
||||||
|
# Parse the date string and store both original string and parsed datetime
|
||||||
|
parsed_date = parse_flexible_date(date_str)
|
||||||
|
if parsed_date is None:
|
||||||
|
continue
|
||||||
|
valid_dates.append((date_str, parsed_date))
|
||||||
|
except ValueError:
|
||||||
|
# Skip invalid date strings
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not valid_dates:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Find the date string with the maximum datetime value
|
||||||
|
newest_date_str: str = max(valid_dates, key=lambda x: x[1])[0]
|
||||||
|
|
||||||
|
return newest_date_str
|
||||||
|
|
||||||
|
|
||||||
|
def parse_day_of_week_range(dow_days: str) -> list[tuple[int, str]]:
|
||||||
|
"""
|
||||||
|
Parse a day of week list/range string and return a list of tuples with day index and name.
|
||||||
|
Allowed are short (eg Mon) or long names (eg Monday).
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
dow_days {str} -- A comma-separated list of days or ranges (e.g., "Mon,Wed-Fri")
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If the input format is invalid or if duplicate days are found.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[tuple[int, str]] -- A list of tuples containing the day index and name.
|
||||||
|
"""
|
||||||
|
# we have Sun twice because it can be 0 or 7
|
||||||
|
# Mon is 1 and Sun is 7, which is ISO standard
|
||||||
|
dow_day = dow_days.split(",")
|
||||||
|
dow_day = [day.strip() for day in dow_day if day.strip()]
|
||||||
|
__out_dow_days: list[tuple[int, str]] = []
|
||||||
|
for __dow_day in dow_day:
|
||||||
|
# if we have a "-" in there fill
|
||||||
|
if "-" in __dow_day:
|
||||||
|
__dow_range = __dow_day.split("-")
|
||||||
|
__dow_range = [day.strip().capitalize() for day in __dow_range if day.strip()]
|
||||||
|
try:
|
||||||
|
start_day = DAYS_OF_WEEK_ISO_REVERSED[__dow_range[0]]
|
||||||
|
end_day = DAYS_OF_WEEK_ISO_REVERSED[__dow_range[1]]
|
||||||
|
except KeyError:
|
||||||
|
# try long time
|
||||||
|
try:
|
||||||
|
start_day = DAYS_OF_WEEK_ISO_REVERSED[DAYS_OF_WEEK_LONG_TO_SHORT[__dow_range[0]]]
|
||||||
|
end_day = DAYS_OF_WEEK_ISO_REVERSED[DAYS_OF_WEEK_LONG_TO_SHORT[__dow_range[1]]]
|
||||||
|
except KeyError as e:
|
||||||
|
raise ValueError(f"Invalid day of week entry found: {__dow_day}: {e}") from e
|
||||||
|
# Check if this spans across the weekend (e.g., Fri-Mon)
|
||||||
|
if start_day > end_day:
|
||||||
|
# Handle weekend-spanning range: start_day to 7, then 1 to end_day
|
||||||
|
__out_dow_days.extend(
|
||||||
|
[
|
||||||
|
(i, DAYS_OF_WEEK_ISO[i])
|
||||||
|
for i in range(start_day, 8) # start_day to Sunday (7)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
__out_dow_days.extend(
|
||||||
|
[
|
||||||
|
(i, DAYS_OF_WEEK_ISO[i])
|
||||||
|
for i in range(1, end_day + 1) # Monday (1) to end_day
|
||||||
|
]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Normal range: start_day to end_day
|
||||||
|
__out_dow_days.extend(
|
||||||
|
[
|
||||||
|
(i, DAYS_OF_WEEK_ISO[i])
|
||||||
|
for i in range(start_day, end_day + 1)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
__out_dow_days.append((DAYS_OF_WEEK_ISO_REVERSED[__dow_day], __dow_day))
|
||||||
|
except KeyError as e:
|
||||||
|
raise ValueError(f"Invalid day of week entry found: {__dow_day}: {e}") from e
|
||||||
|
# if there are duplicates, alert
|
||||||
|
if len(__out_dow_days) != len(set(__out_dow_days)):
|
||||||
|
raise ValueError(f"Duplicate day of week entries found: {__out_dow_days}")
|
||||||
|
|
||||||
|
return __out_dow_days
|
||||||
|
|
||||||
|
|
||||||
|
def parse_time_range(time_str: str, time_format: str = "%H:%M") -> tuple[time, time]:
|
||||||
|
"""
|
||||||
|
Parse a time range string in the format "HH:MM-HH:MM" and return a tuple of two time objects.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
time_str {str} -- The time range string to parse.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: Invalid time block set
|
||||||
|
ValueError: Invalid time format
|
||||||
|
ValueError: Start time must be before end time
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple[time, time] -- start time, end time: leading zeros formattd
|
||||||
|
"""
|
||||||
|
__time_str = time_str.strip()
|
||||||
|
# split by "-"
|
||||||
|
__time_split = __time_str.split("-")
|
||||||
|
if len(__time_split) != 2:
|
||||||
|
raise ValueError(f"Invalid time block: {__time_str}")
|
||||||
|
try:
|
||||||
|
__time_start = datetime.strptime(__time_split[0], time_format).time()
|
||||||
|
__time_end = datetime.strptime(__time_split[1], time_format).time()
|
||||||
|
except ValueError as e:
|
||||||
|
raise ValueError(f"Invalid time block format [{__time_str}]: {e}") from e
|
||||||
|
if __time_start >= __time_end:
|
||||||
|
raise ValueError(f"Invalid time block set, start time after end time or equal: {__time_str}")
|
||||||
|
|
||||||
|
return __time_start, __time_end
|
||||||
|
|
||||||
|
|
||||||
|
def times_overlap_or_connect(time1: tuple[time, time], time2: tuple[time, time], allow_touching: bool = False) -> bool:
|
||||||
|
"""
|
||||||
|
Check if two time ranges overlap or connect
|
||||||
|
|
||||||
|
Args:
|
||||||
|
time1 (tuple): (start_time, end_time) for first range
|
||||||
|
time2 (tuple): (start_time, end_time) for second range
|
||||||
|
allow_touching (bool): If True, touching ranges (e.g., 8:00-10:00 and 10:00-12:00) are allowed
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if ranges overlap or connect (based on allow_touching)
|
||||||
|
"""
|
||||||
|
start1, end1 = time1
|
||||||
|
start2, end2 = time2
|
||||||
|
|
||||||
|
if allow_touching:
|
||||||
|
# Only check for actual overlap (touching is OK)
|
||||||
|
return start1 < end2 and start2 < end1
|
||||||
|
# Check for overlap OR touching
|
||||||
|
return start1 <= end2 and start2 <= end1
|
||||||
|
|
||||||
|
|
||||||
|
def is_time_in_range(current_time: str, start_time: str, end_time: str) -> bool:
|
||||||
|
"""
|
||||||
|
Check if current_time is within start_time and end_time (inclusive)
|
||||||
|
Time format: "HH:MM" (24-hour format)
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
current_time {str} -- _description_
|
||||||
|
start_time {str} -- _description_
|
||||||
|
end_time {str} -- _description_
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool -- _description_
|
||||||
|
"""
|
||||||
|
# Convert string times to time objects
|
||||||
|
current = datetime.strptime(current_time, "%H:%M:%S").time()
|
||||||
|
start = datetime.strptime(start_time, "%H:%M:%S").time()
|
||||||
|
end = datetime.strptime(end_time, "%H:%M:%S").time()
|
||||||
|
|
||||||
|
# Handle case where range crosses midnight (e.g., 22:00 to 06:00)
|
||||||
|
if start <= end:
|
||||||
|
# Normal case: start time is before end time
|
||||||
|
return start <= current <= end
|
||||||
|
# Crosses midnight: e.g., 22:00 to 06:00
|
||||||
|
return current >= start or current <= end
|
||||||
|
|
||||||
|
|
||||||
|
def reorder_weekdays_from_today(base_day: str) -> dict[int, str]:
|
||||||
|
"""
|
||||||
|
Reorder the days of the week starting from the specified base_day.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
base_day {str} -- The day to start the week from (e.g., "Mon").
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict[int, str] -- A dictionary mapping day numbers to day names.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
today_num = DAYS_OF_WEEK_ISO_REVERSED[base_day]
|
||||||
|
except KeyError:
|
||||||
|
try:
|
||||||
|
today_num = DAYS_OF_WEEK_ISO_REVERSED[DAYS_OF_WEEK_LONG_TO_SHORT[base_day]]
|
||||||
|
except KeyError as e:
|
||||||
|
raise ValueError(f"Invalid day name provided: {base_day}: {e}") from e
|
||||||
|
# Convert to list of tuples
|
||||||
|
items = list(DAYS_OF_WEEK_ISO.items())
|
||||||
|
# Reorder: from today onwards + from beginning to yesterday
|
||||||
|
reordered_items = items[today_num - 1:] + items[:today_num - 1]
|
||||||
|
|
||||||
|
# Convert back to dictionary
|
||||||
|
return dict(reordered_items)
|
||||||
|
|
||||||
|
# __END__
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
"""
|
"""
|
||||||
Current timestamp strings and time zones
|
Convert timestamp strings with time units into seconds and vice versa.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from math import floor
|
||||||
import re
|
import re
|
||||||
from datetime import datetime
|
|
||||||
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
|
||||||
from corelibs.var_handling.var_helpers import is_float
|
from corelibs.var_handling.var_helpers import is_float
|
||||||
|
|
||||||
|
|
||||||
@@ -16,30 +15,6 @@ class TimeUnitError(Exception):
|
|||||||
"""Custom exception for time parsing errors."""
|
"""Custom exception for time parsing errors."""
|
||||||
|
|
||||||
|
|
||||||
class TimestampStrings:
|
|
||||||
"""
|
|
||||||
set default time stamps
|
|
||||||
"""
|
|
||||||
|
|
||||||
TIME_ZONE: str = 'Asia/Tokyo'
|
|
||||||
|
|
||||||
def __init__(self, time_zone: str | ZoneInfo | None = None):
|
|
||||||
self.timestamp_now = datetime.now()
|
|
||||||
# set time zone as string
|
|
||||||
time_zone = time_zone if time_zone is not None else self.TIME_ZONE
|
|
||||||
self.time_zone = str(time_zone) if not isinstance(time_zone, str) else time_zone
|
|
||||||
# set ZoneInfo type
|
|
||||||
try:
|
|
||||||
self.time_zone_zi = ZoneInfo(self.time_zone)
|
|
||||||
except ZoneInfoNotFoundError as e:
|
|
||||||
raise ValueError(f'Zone could not be loaded [{self.time_zone}]: {e}') from e
|
|
||||||
self.timestamp_now_tz = datetime.now(self.time_zone_zi)
|
|
||||||
self.today = self.timestamp_now.strftime('%Y-%m-%d')
|
|
||||||
self.timestamp = self.timestamp_now.strftime("%Y-%m-%d %H:%M:%S")
|
|
||||||
self.timestamp_tz = self.timestamp_now_tz.strftime("%Y-%m-%d %H:%M:%S %Z")
|
|
||||||
self.timestamp_file = self.timestamp_now.strftime("%Y-%m-%d_%H%M%S")
|
|
||||||
|
|
||||||
|
|
||||||
def convert_to_seconds(time_string: str | int | float) -> int:
|
def convert_to_seconds(time_string: str | int | float) -> int:
|
||||||
"""
|
"""
|
||||||
Conver a string with time units into a seconds string
|
Conver a string with time units into a seconds string
|
||||||
@@ -124,7 +99,10 @@ def convert_to_seconds(time_string: str | int | float) -> int:
|
|||||||
def seconds_to_string(seconds: str | int | float, show_microseconds: bool = False) -> str:
|
def seconds_to_string(seconds: str | int | float, show_microseconds: bool = False) -> str:
|
||||||
"""
|
"""
|
||||||
Convert seconds to compact human readable format (e.g., "1d 2h 3m 4.567s")
|
Convert seconds to compact human readable format (e.g., "1d 2h 3m 4.567s")
|
||||||
|
Zero values are omitted.
|
||||||
|
milliseconds if requested are added as fractional part of seconds.
|
||||||
Supports negative values with "-" prefix
|
Supports negative values with "-" prefix
|
||||||
|
if not int or float, will return as is
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
seconds (float): Time in seconds (can be negative)
|
seconds (float): Time in seconds (can be negative)
|
||||||
@@ -172,4 +150,51 @@ def seconds_to_string(seconds: str | int | float, show_microseconds: bool = Fals
|
|||||||
result = " ".join(parts)
|
result = " ".join(parts)
|
||||||
return f"-{result}" if negative else result
|
return f"-{result}" if negative else result
|
||||||
|
|
||||||
|
|
||||||
|
def convert_timestamp(timestamp: float | int | str, show_microseconds: bool = True) -> str:
|
||||||
|
"""
|
||||||
|
format timestamp into human readable format. This function will add 0 values between set values
|
||||||
|
for example if we have 1d 1s it would output 1d 0h 0m 1s
|
||||||
|
Milliseconds will be shown if set, and added with ms at the end
|
||||||
|
Negative values will be prefixed with "-"
|
||||||
|
if not int or float, will return as is
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
timestamp {float} -- _description_
|
||||||
|
|
||||||
|
Keyword Arguments:
|
||||||
|
show_micro {bool} -- _description_ (default: {True})
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str -- _description_
|
||||||
|
"""
|
||||||
|
if not isinstance(timestamp, (int, float)):
|
||||||
|
return timestamp
|
||||||
|
# cut of the ms, but first round them up to four
|
||||||
|
__timestamp_ms_split = str(round(timestamp, 4)).split(".")
|
||||||
|
timestamp = int(__timestamp_ms_split[0])
|
||||||
|
negative = timestamp < 0
|
||||||
|
timestamp = abs(timestamp)
|
||||||
|
try:
|
||||||
|
ms = int(__timestamp_ms_split[1])
|
||||||
|
except IndexError:
|
||||||
|
ms = 0
|
||||||
|
timegroups = (86400, 3600, 60, 1)
|
||||||
|
output: list[int] = []
|
||||||
|
for i in timegroups:
|
||||||
|
output.append(int(floor(timestamp / i)))
|
||||||
|
timestamp = timestamp % i
|
||||||
|
# output has days|hours|min|sec ms
|
||||||
|
time_string = ""
|
||||||
|
if output[0]:
|
||||||
|
time_string = f"{output[0]}d "
|
||||||
|
if output[0] or output[1]:
|
||||||
|
time_string += f"{output[1]}h "
|
||||||
|
if output[0] or output[1] or output[2]:
|
||||||
|
time_string += f"{output[2]}m "
|
||||||
|
time_string += f"{output[3]}s"
|
||||||
|
if show_microseconds:
|
||||||
|
time_string += f" {ms}ms" if ms else " 0ms"
|
||||||
|
return f"-{time_string}" if negative else time_string
|
||||||
|
|
||||||
# __END__
|
# __END__
|
||||||
32
src/corelibs/datetime_handling/timestamp_strings.py
Normal file
32
src/corelibs/datetime_handling/timestamp_strings.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
"""
|
||||||
|
Current timestamp strings and time zones
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
||||||
|
|
||||||
|
|
||||||
|
class TimestampStrings:
|
||||||
|
"""
|
||||||
|
set default time stamps
|
||||||
|
"""
|
||||||
|
|
||||||
|
TIME_ZONE: str = 'Asia/Tokyo'
|
||||||
|
|
||||||
|
def __init__(self, time_zone: str | ZoneInfo | None = None):
|
||||||
|
self.timestamp_now = datetime.now()
|
||||||
|
# set time zone as string
|
||||||
|
time_zone = time_zone if time_zone is not None else self.TIME_ZONE
|
||||||
|
self.time_zone = str(time_zone) if not isinstance(time_zone, str) else time_zone
|
||||||
|
# set ZoneInfo type
|
||||||
|
try:
|
||||||
|
self.time_zone_zi = ZoneInfo(self.time_zone)
|
||||||
|
except ZoneInfoNotFoundError as e:
|
||||||
|
raise ValueError(f'Zone could not be loaded [{self.time_zone}]: {e}') from e
|
||||||
|
self.timestamp_now_tz = datetime.now(self.time_zone_zi)
|
||||||
|
self.today = self.timestamp_now.strftime('%Y-%m-%d')
|
||||||
|
self.timestamp = self.timestamp_now.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
self.timestamp_tz = self.timestamp_now_tz.strftime("%Y-%m-%d %H:%M:%S %Z")
|
||||||
|
self.timestamp_file = self.timestamp_now.strftime("%Y-%m-%d_%H%M%S")
|
||||||
|
|
||||||
|
# __END__
|
||||||
@@ -32,7 +32,7 @@ show_position(file pos optional)
|
|||||||
import time
|
import time
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
from math import floor
|
from math import floor
|
||||||
from corelibs.string_handling.datetime_helpers import convert_timestamp
|
from corelibs.datetime_handling.datetime_helpers import convert_timestamp
|
||||||
from corelibs.string_handling.byte_helpers import format_bytes
|
from corelibs.string_handling.byte_helpers import format_bytes
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,136 +0,0 @@
|
|||||||
"""
|
|
||||||
Various string based date/time helpers
|
|
||||||
"""
|
|
||||||
|
|
||||||
from math import floor
|
|
||||||
import time as time_t
|
|
||||||
from datetime import datetime
|
|
||||||
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
|
||||||
|
|
||||||
|
|
||||||
def convert_timestamp(timestamp: float | int, show_micro: bool = True) -> str:
|
|
||||||
"""
|
|
||||||
format timestamp into human readable format
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
timestamp {float} -- _description_
|
|
||||||
|
|
||||||
Keyword Arguments:
|
|
||||||
show_micro {bool} -- _description_ (default: {True})
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str -- _description_
|
|
||||||
"""
|
|
||||||
# cut of the ms, but first round them up to four
|
|
||||||
__timestamp_ms_split = str(round(timestamp, 4)).split(".")
|
|
||||||
timestamp = int(__timestamp_ms_split[0])
|
|
||||||
try:
|
|
||||||
ms = int(__timestamp_ms_split[1])
|
|
||||||
except IndexError:
|
|
||||||
ms = 0
|
|
||||||
timegroups = (86400, 3600, 60, 1)
|
|
||||||
output: list[int] = []
|
|
||||||
for i in timegroups:
|
|
||||||
output.append(int(floor(timestamp / i)))
|
|
||||||
timestamp = timestamp % i
|
|
||||||
# output has days|hours|min|sec ms
|
|
||||||
time_string = ""
|
|
||||||
if output[0]:
|
|
||||||
time_string = f"{output[0]}d"
|
|
||||||
if output[0] or output[1]:
|
|
||||||
time_string += f"{output[1]}h "
|
|
||||||
if output[0] or output[1] or output[2]:
|
|
||||||
time_string += f"{output[2]}m "
|
|
||||||
time_string += f"{output[3]}s"
|
|
||||||
if show_micro:
|
|
||||||
time_string += f" {ms}ms" if ms else " 0ms"
|
|
||||||
return time_string
|
|
||||||
|
|
||||||
|
|
||||||
def create_time(timestamp: float, timestamp_format: str = "%Y-%m-%d %H:%M:%S") -> str:
|
|
||||||
"""
|
|
||||||
just takes a timestamp and prints out humand readable format
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
timestamp {float} -- _description_
|
|
||||||
|
|
||||||
Keyword Arguments:
|
|
||||||
timestamp_format {_type_} -- _description_ (default: {"%Y-%m-%d %H:%M:%S"})
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str -- _description_
|
|
||||||
"""
|
|
||||||
return time_t.strftime(timestamp_format, time_t.localtime(timestamp))
|
|
||||||
|
|
||||||
|
|
||||||
def get_system_timezone():
|
|
||||||
"""Get system timezone using datetime's automatic detection"""
|
|
||||||
# Get current time with system timezone
|
|
||||||
local_time = datetime.now().astimezone()
|
|
||||||
|
|
||||||
# Extract timezone info
|
|
||||||
system_tz = local_time.tzinfo
|
|
||||||
timezone_name = str(system_tz)
|
|
||||||
|
|
||||||
return system_tz, timezone_name
|
|
||||||
|
|
||||||
|
|
||||||
def parse_timezone_data(timezone_tz: str = '') -> ZoneInfo:
|
|
||||||
"""
|
|
||||||
parses a string to get the ZoneInfo
|
|
||||||
If not set or not valid gets local time,
|
|
||||||
if that is not possible get UTC
|
|
||||||
|
|
||||||
Keyword Arguments:
|
|
||||||
timezone_tz {str} -- _description_ (default: {''})
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
ZoneInfo -- _description_
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
return ZoneInfo(timezone_tz)
|
|
||||||
except (ZoneInfoNotFoundError, ValueError, TypeError):
|
|
||||||
# use default
|
|
||||||
time_tz, time_tz_str = get_system_timezone()
|
|
||||||
if time_tz is None:
|
|
||||||
return ZoneInfo('UTC')
|
|
||||||
# TODO build proper TZ lookup
|
|
||||||
tz_mapping = {
|
|
||||||
'JST': 'Asia/Tokyo',
|
|
||||||
'KST': 'Asia/Seoul',
|
|
||||||
'IST': 'Asia/Kolkata',
|
|
||||||
'CST': 'Asia/Shanghai', # Default to China for CST
|
|
||||||
'AEST': 'Australia/Sydney',
|
|
||||||
'AWST': 'Australia/Perth',
|
|
||||||
'EST': 'America/New_York',
|
|
||||||
'EDT': 'America/New_York',
|
|
||||||
'CDT': 'America/Chicago',
|
|
||||||
'MST': 'America/Denver',
|
|
||||||
'MDT': 'America/Denver',
|
|
||||||
'PST': 'America/Los_Angeles',
|
|
||||||
'PDT': 'America/Los_Angeles',
|
|
||||||
'GMT': 'UTC',
|
|
||||||
'UTC': 'UTC',
|
|
||||||
'CET': 'Europe/Berlin',
|
|
||||||
'CEST': 'Europe/Berlin',
|
|
||||||
'BST': 'Europe/London',
|
|
||||||
}
|
|
||||||
try:
|
|
||||||
return ZoneInfo(tz_mapping[time_tz_str])
|
|
||||||
except (ZoneInfoNotFoundError, IndexError) as e:
|
|
||||||
raise ValueError(f"No mapping for {time_tz_str}: {e}") from e
|
|
||||||
|
|
||||||
|
|
||||||
def get_datetime_iso8601(timezone_tz: str | ZoneInfo = '', sep: str = 'T', timespec: str = 'microseconds') -> str:
|
|
||||||
"""
|
|
||||||
set a datetime in the iso8601 format with microseconds
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str -- _description_
|
|
||||||
"""
|
|
||||||
# parse if this is a string
|
|
||||||
if isinstance(timezone_tz, str):
|
|
||||||
timezone_tz = parse_timezone_data(timezone_tz)
|
|
||||||
return datetime.now(timezone_tz).isoformat(sep=sep, timespec=timespec)
|
|
||||||
|
|
||||||
# __END__
|
|
||||||
236
test-run/datetime_handling/datetime_helpers.py
Normal file
236
test-run/datetime_handling/datetime_helpers.py
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
"""
|
||||||
|
date string helper test
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from corelibs.datetime_handling.datetime_helpers import (
|
||||||
|
get_datetime_iso8601, get_system_timezone, parse_timezone_data, validate_date,
|
||||||
|
parse_flexible_date, compare_dates, find_newest_datetime_in_list,
|
||||||
|
parse_day_of_week_range, parse_time_range, times_overlap_or_connect, is_time_in_range,
|
||||||
|
reorder_weekdays_from_today
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def __get_datetime_iso8601():
|
||||||
|
"""
|
||||||
|
Comment
|
||||||
|
"""
|
||||||
|
for tz in [
|
||||||
|
'', 'Asia/Tokyo', 'UTC', 'Europe/Vienna',
|
||||||
|
'America/New_York', 'Australia/Sydney',
|
||||||
|
'invalid'
|
||||||
|
]:
|
||||||
|
print(f"{tz} -> {get_datetime_iso8601(tz)}")
|
||||||
|
|
||||||
|
|
||||||
|
def __parse_timezone_data():
|
||||||
|
for tz in [
|
||||||
|
'JST', 'KST', 'UTC', 'CET', 'CEST',
|
||||||
|
]:
|
||||||
|
print(f"{tz} -> {parse_timezone_data(tz)}")
|
||||||
|
|
||||||
|
|
||||||
|
def __validate_date():
|
||||||
|
"""
|
||||||
|
Comment
|
||||||
|
"""
|
||||||
|
|
||||||
|
test_dates = [
|
||||||
|
"2024-01-01",
|
||||||
|
"2024-02-29", # Leap year
|
||||||
|
"2023-02-29", # Invalid date
|
||||||
|
"2024-13-01", # Invalid month
|
||||||
|
"2024-00-10", # Invalid month
|
||||||
|
"2024-04-31", # Invalid day
|
||||||
|
"invalid-date"
|
||||||
|
]
|
||||||
|
|
||||||
|
for date_str in test_dates:
|
||||||
|
is_valid = validate_date(date_str)
|
||||||
|
print(f"Date '{date_str}' is valid: {is_valid}")
|
||||||
|
|
||||||
|
# also test not before and not after
|
||||||
|
not_before_dates = [
|
||||||
|
"2023-12-31",
|
||||||
|
"2024-01-01",
|
||||||
|
"2024-02-29",
|
||||||
|
]
|
||||||
|
not_after_dates = [
|
||||||
|
"2024-12-31",
|
||||||
|
"2024-11-30",
|
||||||
|
"2025-01-01",
|
||||||
|
]
|
||||||
|
|
||||||
|
for date_str in not_before_dates:
|
||||||
|
datetime.strptime(date_str, "%Y-%m-%d") # Ensure valid date format
|
||||||
|
is_valid = validate_date(date_str, not_before=datetime.strptime("2024-01-01", "%Y-%m-%d"))
|
||||||
|
print(f"Date '{date_str}' is valid (not before 2024-01-01): {is_valid}")
|
||||||
|
|
||||||
|
for date_str in not_after_dates:
|
||||||
|
is_valid = validate_date(date_str, not_after=datetime.strptime("2024-12-31", "%Y-%m-%d"))
|
||||||
|
print(f"Date '{date_str}' is valid (not after 2024-12-31): {is_valid}")
|
||||||
|
|
||||||
|
for date_str in test_dates:
|
||||||
|
is_valid = validate_date(
|
||||||
|
date_str,
|
||||||
|
not_before=datetime.strptime("2024-01-01", "%Y-%m-%d"),
|
||||||
|
not_after=datetime.strptime("2024-12-31", "%Y-%m-%d")
|
||||||
|
)
|
||||||
|
print(f"Date '{date_str}' is valid (2024 only): {is_valid}")
|
||||||
|
|
||||||
|
|
||||||
|
def __parse_flexible_date():
|
||||||
|
for date_str in [
|
||||||
|
"2024-01-01",
|
||||||
|
"01/02/2024",
|
||||||
|
"February 29, 2024",
|
||||||
|
"Invalid date",
|
||||||
|
"2025-01-01 12:18:10",
|
||||||
|
"2025-01-01 12:18:10.566",
|
||||||
|
"2025-01-01T12:18:10.566",
|
||||||
|
"2025-01-01T12:18:10.566+02:00",
|
||||||
|
]:
|
||||||
|
print(f"{date_str} -> {parse_flexible_date(date_str)}")
|
||||||
|
|
||||||
|
|
||||||
|
def __compare_dates():
|
||||||
|
|
||||||
|
for date1, date2 in [
|
||||||
|
("2024-01-01 12:00:00", "2024-01-01 15:30:00"),
|
||||||
|
("2024-01-02", "2024-01-01"),
|
||||||
|
("2024-01-01T10:00:00+02:00", "2024-01-01T08:00:00Z"),
|
||||||
|
("invalid-date", "2024-01-01"),
|
||||||
|
("2024-01-01", "invalid-date"),
|
||||||
|
("invalid-date", "also-invalid"),
|
||||||
|
]:
|
||||||
|
result = compare_dates(date1, date2)
|
||||||
|
print(f"Comparing '{date1}' and '{date2}': {result}")
|
||||||
|
|
||||||
|
|
||||||
|
def __find_newest_datetime_in_list():
|
||||||
|
date_list = [
|
||||||
|
"2024-01-01 12:00:00",
|
||||||
|
"2024-01-02 09:30:00",
|
||||||
|
"2023-12-31 23:59:59",
|
||||||
|
"2024-01-02 15:45:00",
|
||||||
|
"2024-01-02T15:45:00.001",
|
||||||
|
"invalid-date",
|
||||||
|
]
|
||||||
|
newest_date = find_newest_datetime_in_list(date_list)
|
||||||
|
print(f"Newest date in list: {newest_date}")
|
||||||
|
|
||||||
|
|
||||||
|
def __parse_day_of_week_range():
|
||||||
|
ranges = [
|
||||||
|
"Mon-Fri",
|
||||||
|
"Saturday-Sunday",
|
||||||
|
"Wed-Mon",
|
||||||
|
"Fri-Fri",
|
||||||
|
"mon-tue",
|
||||||
|
"Invalid-Range"
|
||||||
|
]
|
||||||
|
for range_str in ranges:
|
||||||
|
try:
|
||||||
|
days = parse_day_of_week_range(range_str)
|
||||||
|
print(f"Day range '{range_str}' -> {days}")
|
||||||
|
except ValueError as e:
|
||||||
|
print(f"[!] Error parsing day range '{range_str}': {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def __parse_time_range():
|
||||||
|
ranges = [
|
||||||
|
"08:00-17:00",
|
||||||
|
"22:00-06:00",
|
||||||
|
"12:30-12:30",
|
||||||
|
"invalid-range"
|
||||||
|
]
|
||||||
|
for range_str in ranges:
|
||||||
|
try:
|
||||||
|
start_time, end_time = parse_time_range(range_str)
|
||||||
|
print(f"Time range '{range_str}' -> Start: {start_time}, End: {end_time}")
|
||||||
|
except ValueError as e:
|
||||||
|
print(f"[!] Error parsing time range '{range_str}': {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def __times_overlap_or_connect():
|
||||||
|
time_format = "%H:%M"
|
||||||
|
time_ranges = [
|
||||||
|
(("08:00", "12:00"), ("11:00", "15:00")), # Overlap
|
||||||
|
(("22:00", "02:00"), ("01:00", "05:00")), # Overlap across midnight
|
||||||
|
(("10:00", "12:00"), ("12:00", "14:00")), # Connect
|
||||||
|
(("09:00", "11:00"), ("12:00", "14:00")), # No overlap
|
||||||
|
]
|
||||||
|
for (start1, end1), (start2, end2) in time_ranges:
|
||||||
|
start1 = datetime.strptime(start1, time_format).time()
|
||||||
|
end1 = datetime.strptime(end1, time_format).time()
|
||||||
|
start2 = datetime.strptime(start2, time_format).time()
|
||||||
|
end2 = datetime.strptime(end2, time_format).time()
|
||||||
|
overlap = times_overlap_or_connect((start1, end1), (start2, end2))
|
||||||
|
overlap_connect = times_overlap_or_connect((start1, end1), (start2, end2), True)
|
||||||
|
print(f"Time ranges {start1}-{end1} and {start2}-{end2} overlap/connect: {overlap}/{overlap_connect}")
|
||||||
|
|
||||||
|
|
||||||
|
def __is_time_in_range():
|
||||||
|
time_format = "%H:%M:%S"
|
||||||
|
test_cases = [
|
||||||
|
("10:00:00", "09:00:00", "11:00:00"),
|
||||||
|
("23:30:00", "22:00:00", "01:00:00"), # Across midnight
|
||||||
|
("05:00:00", "06:00:00", "10:00:00"), # Not in range
|
||||||
|
("12:00:00", "12:00:00", "12:00:00"), # Exact match
|
||||||
|
]
|
||||||
|
for (check_time, start_time, end_time) in test_cases:
|
||||||
|
start_time = datetime.strptime(start_time, time_format).time()
|
||||||
|
end_time = datetime.strptime(end_time, time_format).time()
|
||||||
|
in_range = is_time_in_range(
|
||||||
|
f"{check_time}", start_time.strftime("%H:%M:%S"), end_time.strftime("%H:%M:%S")
|
||||||
|
)
|
||||||
|
print(f"Time {check_time} in range {start_time}-{end_time}: {in_range}")
|
||||||
|
|
||||||
|
|
||||||
|
def __reorder_weekdays_from_today():
|
||||||
|
for base_day in [
|
||||||
|
"Tue", "Wed", "Sunday", "Fri", "InvalidDay"
|
||||||
|
]:
|
||||||
|
try:
|
||||||
|
reordered_days = reorder_weekdays_from_today(base_day)
|
||||||
|
print(f"Reordered weekdays from {base_day}: {reordered_days}")
|
||||||
|
except ValueError as e:
|
||||||
|
print(f"[!] Error reordering weekdays from '{base_day}': {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""
|
||||||
|
Comment
|
||||||
|
"""
|
||||||
|
print("\nDatetime ISO 8601 tests:\n")
|
||||||
|
__get_datetime_iso8601()
|
||||||
|
print("\nSystem time test:")
|
||||||
|
print(f"System time: {get_system_timezone()}")
|
||||||
|
print("\nParse timezone data tests:\n")
|
||||||
|
__parse_timezone_data()
|
||||||
|
print("\nValidate date tests:\n")
|
||||||
|
__validate_date()
|
||||||
|
print("\nParse flexible date tests:\n")
|
||||||
|
__parse_flexible_date()
|
||||||
|
print("\nCompare dates tests:\n")
|
||||||
|
__compare_dates()
|
||||||
|
print("\nFind newest datetime in list tests:\n")
|
||||||
|
__find_newest_datetime_in_list()
|
||||||
|
print("\nParse day of week range tests:\n")
|
||||||
|
__parse_day_of_week_range()
|
||||||
|
print("\nParse time range tests:\n")
|
||||||
|
__parse_time_range()
|
||||||
|
print("\nTimes overlap or connect tests:\n")
|
||||||
|
__times_overlap_or_connect()
|
||||||
|
print("\nIs time in range tests:\n")
|
||||||
|
__is_time_in_range()
|
||||||
|
print("\nReorder weekdays from today tests:\n")
|
||||||
|
__reorder_weekdays_from_today()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
||||||
|
# __END__
|
||||||
@@ -4,8 +4,8 @@
|
|||||||
timestamp string checks
|
timestamp string checks
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from corelibs.string_handling.timestamp_strings import (
|
from corelibs.datetime_handling.timestamp_convert import (
|
||||||
seconds_to_string, convert_to_seconds, TimeParseError, TimeUnitError
|
convert_timestamp, seconds_to_string, convert_to_seconds, TimeParseError, TimeUnitError
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -13,6 +13,7 @@ def main() -> None:
|
|||||||
"""
|
"""
|
||||||
Comment
|
Comment
|
||||||
"""
|
"""
|
||||||
|
print("\n--- Testing convert_to_seconds ---\n")
|
||||||
test_cases = [
|
test_cases = [
|
||||||
"5M 6d", # 5 months, 6 days
|
"5M 6d", # 5 months, 6 days
|
||||||
"2h 30m 45s", # 2 hours, 30 minutes, 45 seconds
|
"2h 30m 45s", # 2 hours, 30 minutes, 45 seconds
|
||||||
@@ -58,6 +59,8 @@ def main() -> None:
|
|||||||
except (TimeParseError, TimeUnitError) as e:
|
except (TimeParseError, TimeUnitError) as e:
|
||||||
print(f"Error encountered for {time_string}: {type(e).__name__}: {e}")
|
print(f"Error encountered for {time_string}: {type(e).__name__}: {e}")
|
||||||
|
|
||||||
|
print("\n--- Testing seconds_to_string and convert_timestamp ---\n")
|
||||||
|
|
||||||
test_values = [
|
test_values = [
|
||||||
'as is string',
|
'as is string',
|
||||||
-172800.001234, # -2 days, -0.001234 seconds
|
-172800.001234, # -2 days, -0.001234 seconds
|
||||||
@@ -79,7 +82,8 @@ def main() -> None:
|
|||||||
|
|
||||||
for time_value in test_values:
|
for time_value in test_values:
|
||||||
result = seconds_to_string(time_value, show_microseconds=True)
|
result = seconds_to_string(time_value, show_microseconds=True)
|
||||||
print(f"Seconds to human readable: {time_value} => {result}")
|
result_alt = convert_timestamp(time_value, show_microseconds=True)
|
||||||
|
print(f"Seconds to human readable: {time_value} => {result} / {result_alt}")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
@@ -10,7 +10,8 @@ import sys
|
|||||||
import io
|
import io
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from corelibs.file_handling.progress import Progress
|
from corelibs.file_handling.progress import Progress
|
||||||
from corelibs.string_handling.datetime_helpers import convert_timestamp, create_time
|
from corelibs.datetime_handling.datetime_helpers import create_time
|
||||||
|
from corelibs.datetime_handling.timestamp_convert import convert_timestamp
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
"""
|
|
||||||
date string helper test
|
|
||||||
"""
|
|
||||||
|
|
||||||
from corelibs.string_handling.datetime_helpers import get_datetime_iso8601
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
"""
|
|
||||||
Comment
|
|
||||||
"""
|
|
||||||
print(get_datetime_iso8601())
|
|
||||||
print(get_datetime_iso8601('Asia/Tokyo'))
|
|
||||||
print(get_datetime_iso8601('UTC'))
|
|
||||||
print(get_datetime_iso8601('Europe/Vienna'))
|
|
||||||
print(get_datetime_iso8601('America/New_York'))
|
|
||||||
print(get_datetime_iso8601('Australia/Sydney'))
|
|
||||||
print(get_datetime_iso8601('invalid'))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
|
||||||
# __END__
|
|
||||||
@@ -5,7 +5,7 @@ Test for double byte format
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
from corelibs.string_handling.timestamp_strings import TimestampStrings
|
from corelibs.datetime_handling.timestamp_strings import TimestampStrings
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|||||||
3
tests/unit/datetime_handling/__init__.py
Normal file
3
tests/unit/datetime_handling/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
Unit tests for encryption_handling module
|
||||||
|
"""
|
||||||
@@ -3,7 +3,7 @@ Unit tests for convert_to_seconds function from timestamp_strings module.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from corelibs.string_handling.timestamp_strings import convert_to_seconds, TimeParseError, TimeUnitError
|
from corelibs.datetime_handling.timestamp_convert import convert_to_seconds, TimeParseError, TimeUnitError
|
||||||
|
|
||||||
|
|
||||||
class TestConvertToSeconds:
|
class TestConvertToSeconds:
|
||||||
690
tests/unit/datetime_handling/test_datetime_helpers.py
Normal file
690
tests/unit/datetime_handling/test_datetime_helpers.py
Normal file
@@ -0,0 +1,690 @@
|
|||||||
|
"""
|
||||||
|
PyTest: datetime_handling/datetime_helpers
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import datetime, time
|
||||||
|
from zoneinfo import ZoneInfo
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from corelibs.datetime_handling.datetime_helpers import (
|
||||||
|
create_time,
|
||||||
|
get_system_timezone,
|
||||||
|
parse_timezone_data,
|
||||||
|
get_datetime_iso8601,
|
||||||
|
validate_date,
|
||||||
|
parse_flexible_date,
|
||||||
|
compare_dates,
|
||||||
|
find_newest_datetime_in_list,
|
||||||
|
parse_day_of_week_range,
|
||||||
|
parse_time_range,
|
||||||
|
times_overlap_or_connect,
|
||||||
|
is_time_in_range,
|
||||||
|
reorder_weekdays_from_today,
|
||||||
|
DAYS_OF_WEEK_LONG_TO_SHORT,
|
||||||
|
DAYS_OF_WEEK_ISO,
|
||||||
|
DAYS_OF_WEEK_ISO_REVERSED,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestConstants:
|
||||||
|
"""Test suite for module constants"""
|
||||||
|
|
||||||
|
def test_days_of_week_long_to_short(self):
|
||||||
|
"""Test DAYS_OF_WEEK_LONG_TO_SHORT dictionary"""
|
||||||
|
assert DAYS_OF_WEEK_LONG_TO_SHORT['Monday'] == 'Mon'
|
||||||
|
assert DAYS_OF_WEEK_LONG_TO_SHORT['Tuesday'] == 'Tue'
|
||||||
|
assert DAYS_OF_WEEK_LONG_TO_SHORT['Friday'] == 'Fri'
|
||||||
|
assert DAYS_OF_WEEK_LONG_TO_SHORT['Sunday'] == 'Sun'
|
||||||
|
assert len(DAYS_OF_WEEK_LONG_TO_SHORT) == 7
|
||||||
|
|
||||||
|
def test_days_of_week_iso(self):
|
||||||
|
"""Test DAYS_OF_WEEK_ISO dictionary"""
|
||||||
|
assert DAYS_OF_WEEK_ISO[1] == 'Mon'
|
||||||
|
assert DAYS_OF_WEEK_ISO[5] == 'Fri'
|
||||||
|
assert DAYS_OF_WEEK_ISO[7] == 'Sun'
|
||||||
|
assert len(DAYS_OF_WEEK_ISO) == 7
|
||||||
|
|
||||||
|
def test_days_of_week_iso_reversed(self):
|
||||||
|
"""Test DAYS_OF_WEEK_ISO_REVERSED dictionary"""
|
||||||
|
assert DAYS_OF_WEEK_ISO_REVERSED['Mon'] == 1
|
||||||
|
assert DAYS_OF_WEEK_ISO_REVERSED['Fri'] == 5
|
||||||
|
assert DAYS_OF_WEEK_ISO_REVERSED['Sun'] == 7
|
||||||
|
assert len(DAYS_OF_WEEK_ISO_REVERSED) == 7
|
||||||
|
|
||||||
|
|
||||||
|
class TestCreateTime:
|
||||||
|
"""Test suite for create_time function"""
|
||||||
|
|
||||||
|
def test_create_time_default_format(self):
|
||||||
|
"""Test create_time with default format"""
|
||||||
|
timestamp = 1609459200.0 # 2021-01-01 00:00:00 UTC
|
||||||
|
result = create_time(timestamp)
|
||||||
|
# Result depends on system timezone, so just check format
|
||||||
|
assert len(result) == 19
|
||||||
|
assert '-' in result
|
||||||
|
assert ':' in result
|
||||||
|
|
||||||
|
def test_create_time_custom_format(self):
|
||||||
|
"""Test create_time with custom format"""
|
||||||
|
timestamp = 1609459200.0
|
||||||
|
result = create_time(timestamp, "%Y/%m/%d")
|
||||||
|
# Check basic format structure
|
||||||
|
assert '/' in result
|
||||||
|
assert len(result) == 10
|
||||||
|
|
||||||
|
def test_create_time_with_microseconds(self):
|
||||||
|
"""Test create_time with microseconds in format"""
|
||||||
|
timestamp = 1609459200.123456
|
||||||
|
result = create_time(timestamp, "%Y-%m-%d %H:%M:%S")
|
||||||
|
assert len(result) == 19
|
||||||
|
|
||||||
|
|
||||||
|
class TestGetSystemTimezone:
|
||||||
|
"""Test suite for get_system_timezone function"""
|
||||||
|
|
||||||
|
def test_get_system_timezone_returns_tuple(self):
|
||||||
|
"""Test that get_system_timezone returns a tuple"""
|
||||||
|
result = get_system_timezone()
|
||||||
|
assert isinstance(result, tuple)
|
||||||
|
assert len(result) == 2
|
||||||
|
|
||||||
|
def test_get_system_timezone_returns_valid_data(self):
|
||||||
|
"""Test that get_system_timezone returns valid timezone info"""
|
||||||
|
system_tz, timezone_name = get_system_timezone()
|
||||||
|
assert system_tz is not None
|
||||||
|
assert isinstance(timezone_name, str)
|
||||||
|
assert len(timezone_name) > 0
|
||||||
|
|
||||||
|
|
||||||
|
class TestParseTimezoneData:
|
||||||
|
"""Test suite for parse_timezone_data function"""
|
||||||
|
|
||||||
|
def test_parse_timezone_data_valid_timezone(self):
|
||||||
|
"""Test parse_timezone_data with valid timezone string"""
|
||||||
|
result = parse_timezone_data('Asia/Tokyo')
|
||||||
|
assert isinstance(result, ZoneInfo)
|
||||||
|
assert str(result) == 'Asia/Tokyo'
|
||||||
|
|
||||||
|
def test_parse_timezone_data_utc(self):
|
||||||
|
"""Test parse_timezone_data with UTC"""
|
||||||
|
result = parse_timezone_data('UTC')
|
||||||
|
assert isinstance(result, ZoneInfo)
|
||||||
|
assert str(result) == 'UTC'
|
||||||
|
|
||||||
|
def test_parse_timezone_data_empty_string(self):
|
||||||
|
"""Test parse_timezone_data with empty string falls back to system timezone"""
|
||||||
|
result = parse_timezone_data('')
|
||||||
|
assert isinstance(result, ZoneInfo)
|
||||||
|
|
||||||
|
def test_parse_timezone_data_invalid_timezone(self):
|
||||||
|
"""Test parse_timezone_data with invalid timezone falls back to system timezone"""
|
||||||
|
# Invalid timezones fall back to system timezone or UTC
|
||||||
|
result = parse_timezone_data('Invalid/Timezone')
|
||||||
|
assert isinstance(result, ZoneInfo)
|
||||||
|
# Should be either system timezone or UTC
|
||||||
|
|
||||||
|
def test_parse_timezone_data_none(self):
|
||||||
|
"""Test parse_timezone_data with None falls back to system timezone"""
|
||||||
|
result = parse_timezone_data()
|
||||||
|
assert isinstance(result, ZoneInfo)
|
||||||
|
|
||||||
|
def test_parse_timezone_data_various_timezones(self):
|
||||||
|
"""Test parse_timezone_data with various timezone strings"""
|
||||||
|
timezones = ['America/New_York', 'Europe/London', 'Asia/Seoul']
|
||||||
|
for tz in timezones:
|
||||||
|
result = parse_timezone_data(tz)
|
||||||
|
assert isinstance(result, ZoneInfo)
|
||||||
|
assert str(result) == tz
|
||||||
|
|
||||||
|
|
||||||
|
class TestGetDatetimeIso8601:
|
||||||
|
"""Test suite for get_datetime_iso8601 function"""
|
||||||
|
|
||||||
|
def test_get_datetime_iso8601_default_params(self):
|
||||||
|
"""Test get_datetime_iso8601 with default parameters"""
|
||||||
|
result = get_datetime_iso8601()
|
||||||
|
# Should be in ISO 8601 format with T separator and microseconds
|
||||||
|
assert 'T' in result
|
||||||
|
assert '.' in result # microseconds
|
||||||
|
# Check basic ISO 8601 format
|
||||||
|
datetime.fromisoformat(result) # Should not raise
|
||||||
|
|
||||||
|
def test_get_datetime_iso8601_custom_timezone_string(self):
|
||||||
|
"""Test get_datetime_iso8601 with custom timezone string"""
|
||||||
|
result = get_datetime_iso8601('UTC')
|
||||||
|
assert '+00:00' in result or 'Z' in result or result.endswith('+00:00')
|
||||||
|
|
||||||
|
def test_get_datetime_iso8601_custom_timezone_zoneinfo(self):
|
||||||
|
"""Test get_datetime_iso8601 with ZoneInfo object"""
|
||||||
|
tz = ZoneInfo('Asia/Tokyo')
|
||||||
|
result = get_datetime_iso8601(tz)
|
||||||
|
assert 'T' in result
|
||||||
|
datetime.fromisoformat(result) # Should not raise
|
||||||
|
|
||||||
|
def test_get_datetime_iso8601_custom_separator(self):
|
||||||
|
"""Test get_datetime_iso8601 with custom separator"""
|
||||||
|
result = get_datetime_iso8601(sep=' ')
|
||||||
|
assert ' ' in result
|
||||||
|
assert 'T' not in result
|
||||||
|
|
||||||
|
def test_get_datetime_iso8601_different_timespec(self):
|
||||||
|
"""Test get_datetime_iso8601 with different timespec values"""
|
||||||
|
result_seconds = get_datetime_iso8601(timespec='seconds')
|
||||||
|
assert '.' not in result_seconds # No microseconds
|
||||||
|
|
||||||
|
result_milliseconds = get_datetime_iso8601(timespec='milliseconds')
|
||||||
|
# Should have milliseconds (3 digits after decimal)
|
||||||
|
assert '.' in result_milliseconds
|
||||||
|
|
||||||
|
|
||||||
|
class TestValidateDate:
|
||||||
|
"""Test suite for validate_date function"""
|
||||||
|
|
||||||
|
def test_validate_date_valid_hyphen_format(self):
|
||||||
|
"""Test validate_date with valid Y-m-d format"""
|
||||||
|
assert validate_date('2023-12-25') is True
|
||||||
|
assert validate_date('2024-01-01') is True
|
||||||
|
|
||||||
|
def test_validate_date_valid_slash_format(self):
|
||||||
|
"""Test validate_date with valid Y/m/d format"""
|
||||||
|
assert validate_date('2023/12/25') is True
|
||||||
|
assert validate_date('2024/01/01') is True
|
||||||
|
|
||||||
|
def test_validate_date_invalid_format(self):
|
||||||
|
"""Test validate_date with invalid format"""
|
||||||
|
assert validate_date('25-12-2023') is False
|
||||||
|
assert validate_date('2023.12.25') is False
|
||||||
|
assert validate_date('invalid') is False
|
||||||
|
|
||||||
|
def test_validate_date_invalid_date(self):
|
||||||
|
"""Test validate_date with invalid date values"""
|
||||||
|
assert validate_date('2023-13-01') is False # Invalid month
|
||||||
|
assert validate_date('2023-02-30') is False # Invalid day
|
||||||
|
|
||||||
|
def test_validate_date_with_not_before(self):
|
||||||
|
"""Test validate_date with not_before constraint"""
|
||||||
|
not_before = datetime(2023, 12, 1)
|
||||||
|
assert validate_date('2023-12-25', not_before=not_before) is True
|
||||||
|
assert validate_date('2023-11-25', not_before=not_before) is False
|
||||||
|
|
||||||
|
def test_validate_date_with_not_after(self):
|
||||||
|
"""Test validate_date with not_after constraint"""
|
||||||
|
not_after = datetime(2023, 12, 31)
|
||||||
|
assert validate_date('2023-12-25', not_after=not_after) is True
|
||||||
|
assert validate_date('2024-01-01', not_after=not_after) is False
|
||||||
|
|
||||||
|
def test_validate_date_with_both_constraints(self):
|
||||||
|
"""Test validate_date with both not_before and not_after constraints"""
|
||||||
|
not_before = datetime(2023, 12, 1)
|
||||||
|
not_after = datetime(2023, 12, 31)
|
||||||
|
assert validate_date('2023-12-15', not_before=not_before, not_after=not_after) is True
|
||||||
|
assert validate_date('2023-11-30', not_before=not_before, not_after=not_after) is False
|
||||||
|
assert validate_date('2024-01-01', not_before=not_before, not_after=not_after) is False
|
||||||
|
|
||||||
|
|
||||||
|
class TestParseFlexibleDate:
|
||||||
|
"""Test suite for parse_flexible_date function"""
|
||||||
|
|
||||||
|
def test_parse_flexible_date_iso8601_full(self):
|
||||||
|
"""Test parse_flexible_date with full ISO 8601 format"""
|
||||||
|
result = parse_flexible_date('2023-12-25T15:30:45')
|
||||||
|
assert isinstance(result, datetime)
|
||||||
|
assert result.year == 2023
|
||||||
|
assert result.month == 12
|
||||||
|
assert result.day == 25
|
||||||
|
assert result.hour == 15
|
||||||
|
assert result.minute == 30
|
||||||
|
assert result.second == 45
|
||||||
|
|
||||||
|
def test_parse_flexible_date_iso8601_with_microseconds(self):
|
||||||
|
"""Test parse_flexible_date with microseconds"""
|
||||||
|
result = parse_flexible_date('2023-12-25T15:30:45.123456')
|
||||||
|
assert isinstance(result, datetime)
|
||||||
|
assert result.microsecond == 123456
|
||||||
|
|
||||||
|
def test_parse_flexible_date_simple_date(self):
|
||||||
|
"""Test parse_flexible_date with simple date format"""
|
||||||
|
result = parse_flexible_date('2023-12-25')
|
||||||
|
assert isinstance(result, datetime)
|
||||||
|
assert result.year == 2023
|
||||||
|
assert result.month == 12
|
||||||
|
assert result.day == 25
|
||||||
|
|
||||||
|
def test_parse_flexible_date_with_timezone_string(self):
|
||||||
|
"""Test parse_flexible_date with timezone string"""
|
||||||
|
result = parse_flexible_date('2023-12-25T15:30:45', timezone_tz='Asia/Tokyo')
|
||||||
|
assert isinstance(result, datetime)
|
||||||
|
assert result.tzinfo is not None
|
||||||
|
|
||||||
|
def test_parse_flexible_date_with_timezone_zoneinfo(self):
|
||||||
|
"""Test parse_flexible_date with ZoneInfo object"""
|
||||||
|
tz = ZoneInfo('UTC')
|
||||||
|
result = parse_flexible_date('2023-12-25T15:30:45', timezone_tz=tz)
|
||||||
|
assert isinstance(result, datetime)
|
||||||
|
assert result.tzinfo is not None
|
||||||
|
|
||||||
|
def test_parse_flexible_date_with_timezone_no_shift(self):
|
||||||
|
"""Test parse_flexible_date with timezone but no shift"""
|
||||||
|
result = parse_flexible_date('2023-12-25T15:30:45', timezone_tz='UTC', shift_time_zone=False)
|
||||||
|
assert isinstance(result, datetime)
|
||||||
|
assert result.hour == 15 # Should not shift
|
||||||
|
|
||||||
|
def test_parse_flexible_date_with_timezone_shift(self):
|
||||||
|
"""Test parse_flexible_date with timezone shift"""
|
||||||
|
result = parse_flexible_date('2023-12-25T15:30:45+00:00', timezone_tz='Asia/Tokyo', shift_time_zone=True)
|
||||||
|
assert isinstance(result, datetime)
|
||||||
|
assert result.tzinfo is not None
|
||||||
|
|
||||||
|
def test_parse_flexible_date_invalid_format(self):
|
||||||
|
"""Test parse_flexible_date with invalid format returns None"""
|
||||||
|
result = parse_flexible_date('invalid-date')
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
def test_parse_flexible_date_whitespace(self):
|
||||||
|
"""Test parse_flexible_date with whitespace"""
|
||||||
|
result = parse_flexible_date(' 2023-12-25 ')
|
||||||
|
assert isinstance(result, datetime)
|
||||||
|
assert result.year == 2023
|
||||||
|
|
||||||
|
|
||||||
|
class TestCompareDates:
|
||||||
|
"""Test suite for compare_dates function"""
|
||||||
|
|
||||||
|
def test_compare_dates_first_newer(self):
|
||||||
|
"""Test compare_dates when first date is newer"""
|
||||||
|
result = compare_dates('2024-01-02', '2024-01-01')
|
||||||
|
assert result is True
|
||||||
|
|
||||||
|
def test_compare_dates_first_older(self):
|
||||||
|
"""Test compare_dates when first date is older"""
|
||||||
|
result = compare_dates('2024-01-01', '2024-01-02')
|
||||||
|
assert result is False
|
||||||
|
|
||||||
|
def test_compare_dates_equal(self):
|
||||||
|
"""Test compare_dates when dates are equal"""
|
||||||
|
result = compare_dates('2024-01-01', '2024-01-01')
|
||||||
|
assert result is False
|
||||||
|
|
||||||
|
def test_compare_dates_with_time(self):
|
||||||
|
"""Test compare_dates with time components (should only compare dates)"""
|
||||||
|
result = compare_dates('2024-01-02T10:00:00', '2024-01-01T23:59:59')
|
||||||
|
assert result is True
|
||||||
|
|
||||||
|
def test_compare_dates_invalid_first_date(self):
|
||||||
|
"""Test compare_dates with invalid first date"""
|
||||||
|
result = compare_dates('invalid', '2024-01-01')
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
def test_compare_dates_invalid_second_date(self):
|
||||||
|
"""Test compare_dates with invalid second date"""
|
||||||
|
result = compare_dates('2024-01-01', 'invalid')
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
def test_compare_dates_both_invalid(self):
|
||||||
|
"""Test compare_dates with both dates invalid"""
|
||||||
|
result = compare_dates('invalid1', 'invalid2')
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
|
||||||
|
class TestFindNewestDatetimeInList:
|
||||||
|
"""Test suite for find_newest_datetime_in_list function"""
|
||||||
|
|
||||||
|
def test_find_newest_datetime_in_list_basic(self):
|
||||||
|
"""Test find_newest_datetime_in_list with basic list"""
|
||||||
|
dates = [
|
||||||
|
'2023-12-25T10:00:00',
|
||||||
|
'2024-01-01T12:00:00',
|
||||||
|
'2023-11-15T08:00:00'
|
||||||
|
]
|
||||||
|
result = find_newest_datetime_in_list(dates)
|
||||||
|
assert result == '2024-01-01T12:00:00'
|
||||||
|
|
||||||
|
def test_find_newest_datetime_in_list_with_timezone(self):
|
||||||
|
"""Test find_newest_datetime_in_list with timezone-aware dates"""
|
||||||
|
dates = [
|
||||||
|
'2025-08-06T16:17:39.747+09:00',
|
||||||
|
'2025-08-05T16:17:39.747+09:00',
|
||||||
|
'2025-08-07T16:17:39.747+09:00'
|
||||||
|
]
|
||||||
|
result = find_newest_datetime_in_list(dates)
|
||||||
|
assert result == '2025-08-07T16:17:39.747+09:00'
|
||||||
|
|
||||||
|
def test_find_newest_datetime_in_list_empty_list(self):
|
||||||
|
"""Test find_newest_datetime_in_list with empty list"""
|
||||||
|
result = find_newest_datetime_in_list([])
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
def test_find_newest_datetime_in_list_single_date(self):
|
||||||
|
"""Test find_newest_datetime_in_list with single date"""
|
||||||
|
dates = ['2024-01-01T12:00:00']
|
||||||
|
result = find_newest_datetime_in_list(dates)
|
||||||
|
assert result == '2024-01-01T12:00:00'
|
||||||
|
|
||||||
|
def test_find_newest_datetime_in_list_with_invalid_dates(self):
|
||||||
|
"""Test find_newest_datetime_in_list with some invalid dates"""
|
||||||
|
dates = [
|
||||||
|
'2023-12-25T10:00:00',
|
||||||
|
'invalid-date',
|
||||||
|
'2024-01-01T12:00:00'
|
||||||
|
]
|
||||||
|
result = find_newest_datetime_in_list(dates)
|
||||||
|
assert result == '2024-01-01T12:00:00'
|
||||||
|
|
||||||
|
def test_find_newest_datetime_in_list_all_invalid(self):
|
||||||
|
"""Test find_newest_datetime_in_list with all invalid dates"""
|
||||||
|
dates = ['invalid1', 'invalid2', 'invalid3']
|
||||||
|
result = find_newest_datetime_in_list(dates)
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
def test_find_newest_datetime_in_list_mixed_formats(self):
|
||||||
|
"""Test find_newest_datetime_in_list with mixed date formats"""
|
||||||
|
dates = [
|
||||||
|
'2023-12-25',
|
||||||
|
'2024-01-01T12:00:00',
|
||||||
|
'2023-11-15T08:00:00.123456'
|
||||||
|
]
|
||||||
|
result = find_newest_datetime_in_list(dates)
|
||||||
|
assert result == '2024-01-01T12:00:00'
|
||||||
|
|
||||||
|
|
||||||
|
class TestParseDayOfWeekRange:
|
||||||
|
"""Test suite for parse_day_of_week_range function"""
|
||||||
|
|
||||||
|
def test_parse_day_of_week_range_single_day(self):
|
||||||
|
"""Test parse_day_of_week_range with single day"""
|
||||||
|
result = parse_day_of_week_range('Mon')
|
||||||
|
assert result == [(1, 'Mon')]
|
||||||
|
|
||||||
|
def test_parse_day_of_week_range_multiple_days(self):
|
||||||
|
"""Test parse_day_of_week_range with multiple days"""
|
||||||
|
result = parse_day_of_week_range('Mon,Wed,Fri')
|
||||||
|
assert len(result) == 3
|
||||||
|
assert (1, 'Mon') in result
|
||||||
|
assert (3, 'Wed') in result
|
||||||
|
assert (5, 'Fri') in result
|
||||||
|
|
||||||
|
def test_parse_day_of_week_range_simple_range(self):
|
||||||
|
"""Test parse_day_of_week_range with simple range"""
|
||||||
|
result = parse_day_of_week_range('Mon-Fri')
|
||||||
|
assert len(result) == 5
|
||||||
|
assert result[0] == (1, 'Mon')
|
||||||
|
assert result[-1] == (5, 'Fri')
|
||||||
|
|
||||||
|
def test_parse_day_of_week_range_weekend_spanning(self):
|
||||||
|
"""Test parse_day_of_week_range with weekend-spanning range"""
|
||||||
|
result = parse_day_of_week_range('Fri-Mon')
|
||||||
|
assert len(result) == 4
|
||||||
|
assert (5, 'Fri') in result
|
||||||
|
assert (6, 'Sat') in result
|
||||||
|
assert (7, 'Sun') in result
|
||||||
|
assert (1, 'Mon') in result
|
||||||
|
|
||||||
|
def test_parse_day_of_week_range_long_names(self):
|
||||||
|
"""Test parse_day_of_week_range with long day names - only works in ranges"""
|
||||||
|
# Long names only work in ranges, not as standalone days
|
||||||
|
# This is a limitation of the current implementation
|
||||||
|
with pytest.raises(ValueError) as exc_info:
|
||||||
|
parse_day_of_week_range('Monday,Wednesday')
|
||||||
|
assert 'Invalid day of week entry found' in str(exc_info.value)
|
||||||
|
|
||||||
|
def test_parse_day_of_week_range_mixed_format(self):
|
||||||
|
"""Test parse_day_of_week_range with short names and ranges"""
|
||||||
|
result = parse_day_of_week_range('Mon,Wed-Fri')
|
||||||
|
assert len(result) == 4
|
||||||
|
assert (1, 'Mon') in result
|
||||||
|
assert (3, 'Wed') in result
|
||||||
|
assert (4, 'Thu') in result
|
||||||
|
assert (5, 'Fri') in result
|
||||||
|
|
||||||
|
def test_parse_day_of_week_range_invalid_day(self):
|
||||||
|
"""Test parse_day_of_week_range with invalid day"""
|
||||||
|
with pytest.raises(ValueError) as exc_info:
|
||||||
|
parse_day_of_week_range('InvalidDay')
|
||||||
|
assert 'Invalid day of week entry found' in str(exc_info.value)
|
||||||
|
|
||||||
|
def test_parse_day_of_week_range_duplicate_days(self):
|
||||||
|
"""Test parse_day_of_week_range with duplicate days"""
|
||||||
|
with pytest.raises(ValueError) as exc_info:
|
||||||
|
parse_day_of_week_range('Mon,Mon')
|
||||||
|
assert 'Duplicate day of week entries found' in str(exc_info.value)
|
||||||
|
|
||||||
|
def test_parse_day_of_week_range_whitespace_handling(self):
|
||||||
|
"""Test parse_day_of_week_range with extra whitespace"""
|
||||||
|
result = parse_day_of_week_range(' Mon , Wed , Fri ')
|
||||||
|
assert len(result) == 3
|
||||||
|
assert (1, 'Mon') in result
|
||||||
|
|
||||||
|
|
||||||
|
class TestParseTimeRange:
|
||||||
|
"""Test suite for parse_time_range function"""
|
||||||
|
|
||||||
|
def test_parse_time_range_valid(self):
|
||||||
|
"""Test parse_time_range with valid time range"""
|
||||||
|
start, end = parse_time_range('09:00-17:00')
|
||||||
|
assert start == time(9, 0)
|
||||||
|
assert end == time(17, 0)
|
||||||
|
|
||||||
|
def test_parse_time_range_different_times(self):
|
||||||
|
"""Test parse_time_range with different time values"""
|
||||||
|
start, end = parse_time_range('08:30-12:45')
|
||||||
|
assert start == time(8, 30)
|
||||||
|
assert end == time(12, 45)
|
||||||
|
|
||||||
|
def test_parse_time_range_invalid_block(self):
|
||||||
|
"""Test parse_time_range with invalid block format"""
|
||||||
|
with pytest.raises(ValueError) as exc_info:
|
||||||
|
parse_time_range('09:00')
|
||||||
|
assert 'Invalid time block' in str(exc_info.value)
|
||||||
|
|
||||||
|
def test_parse_time_range_invalid_format(self):
|
||||||
|
"""Test parse_time_range with invalid time format"""
|
||||||
|
with pytest.raises(ValueError) as exc_info:
|
||||||
|
parse_time_range('25:00-26:00')
|
||||||
|
assert 'Invalid time block format' in str(exc_info.value)
|
||||||
|
|
||||||
|
def test_parse_time_range_start_after_end(self):
|
||||||
|
"""Test parse_time_range with start time after end time"""
|
||||||
|
with pytest.raises(ValueError) as exc_info:
|
||||||
|
parse_time_range('17:00-09:00')
|
||||||
|
assert 'start time after end time' in str(exc_info.value)
|
||||||
|
|
||||||
|
def test_parse_time_range_equal_times(self):
|
||||||
|
"""Test parse_time_range with equal start and end times"""
|
||||||
|
with pytest.raises(ValueError) as exc_info:
|
||||||
|
parse_time_range('09:00-09:00')
|
||||||
|
assert 'start time after end time or equal' in str(exc_info.value)
|
||||||
|
|
||||||
|
def test_parse_time_range_custom_format(self):
|
||||||
|
"""Test parse_time_range with custom time format"""
|
||||||
|
start, end = parse_time_range('09:00:00-17:00:00', time_format='%H:%M:%S')
|
||||||
|
assert start == time(9, 0, 0)
|
||||||
|
assert end == time(17, 0, 0)
|
||||||
|
|
||||||
|
def test_parse_time_range_whitespace(self):
|
||||||
|
"""Test parse_time_range with whitespace"""
|
||||||
|
start, end = parse_time_range(' 09:00-17:00 ')
|
||||||
|
assert start == time(9, 0)
|
||||||
|
assert end == time(17, 0)
|
||||||
|
|
||||||
|
|
||||||
|
class TestTimesOverlapOrConnect:
|
||||||
|
"""Test suite for times_overlap_or_connect function"""
|
||||||
|
|
||||||
|
def test_times_overlap_or_connect_clear_overlap(self):
|
||||||
|
"""Test times_overlap_or_connect with clear overlap"""
|
||||||
|
time1 = (time(9, 0), time(12, 0))
|
||||||
|
time2 = (time(10, 0), time(14, 0))
|
||||||
|
assert times_overlap_or_connect(time1, time2) is True
|
||||||
|
|
||||||
|
def test_times_overlap_or_connect_no_overlap(self):
|
||||||
|
"""Test times_overlap_or_connect with no overlap"""
|
||||||
|
time1 = (time(9, 0), time(12, 0))
|
||||||
|
time2 = (time(13, 0), time(17, 0))
|
||||||
|
assert times_overlap_or_connect(time1, time2) is False
|
||||||
|
|
||||||
|
def test_times_overlap_or_connect_touching_not_allowed(self):
|
||||||
|
"""Test times_overlap_or_connect with touching ranges (not allowed)"""
|
||||||
|
time1 = (time(8, 0), time(10, 0))
|
||||||
|
time2 = (time(10, 0), time(12, 0))
|
||||||
|
assert times_overlap_or_connect(time1, time2, allow_touching=False) is True
|
||||||
|
|
||||||
|
def test_times_overlap_or_connect_touching_allowed(self):
|
||||||
|
"""Test times_overlap_or_connect with touching ranges (allowed)"""
|
||||||
|
time1 = (time(8, 0), time(10, 0))
|
||||||
|
time2 = (time(10, 0), time(12, 0))
|
||||||
|
assert times_overlap_or_connect(time1, time2, allow_touching=True) is False
|
||||||
|
|
||||||
|
def test_times_overlap_or_connect_one_contains_other(self):
|
||||||
|
"""Test times_overlap_or_connect when one range contains the other"""
|
||||||
|
time1 = (time(9, 0), time(17, 0))
|
||||||
|
time2 = (time(10, 0), time(12, 0))
|
||||||
|
assert times_overlap_or_connect(time1, time2) is True
|
||||||
|
|
||||||
|
def test_times_overlap_or_connect_same_start(self):
|
||||||
|
"""Test times_overlap_or_connect with same start time"""
|
||||||
|
time1 = (time(9, 0), time(12, 0))
|
||||||
|
time2 = (time(9, 0), time(14, 0))
|
||||||
|
assert times_overlap_or_connect(time1, time2) is True
|
||||||
|
|
||||||
|
def test_times_overlap_or_connect_same_end(self):
|
||||||
|
"""Test times_overlap_or_connect with same end time"""
|
||||||
|
time1 = (time(9, 0), time(12, 0))
|
||||||
|
time2 = (time(10, 0), time(12, 0))
|
||||||
|
assert times_overlap_or_connect(time1, time2) is True
|
||||||
|
|
||||||
|
|
||||||
|
class TestIsTimeInRange:
|
||||||
|
"""Test suite for is_time_in_range function"""
|
||||||
|
|
||||||
|
def test_is_time_in_range_within_range(self):
|
||||||
|
"""Test is_time_in_range with time within range"""
|
||||||
|
assert is_time_in_range('10:00:00', '09:00:00', '17:00:00') is True
|
||||||
|
|
||||||
|
def test_is_time_in_range_at_start(self):
|
||||||
|
"""Test is_time_in_range with time at start of range"""
|
||||||
|
assert is_time_in_range('09:00:00', '09:00:00', '17:00:00') is True
|
||||||
|
|
||||||
|
def test_is_time_in_range_at_end(self):
|
||||||
|
"""Test is_time_in_range with time at end of range"""
|
||||||
|
assert is_time_in_range('17:00:00', '09:00:00', '17:00:00') is True
|
||||||
|
|
||||||
|
def test_is_time_in_range_before_range(self):
|
||||||
|
"""Test is_time_in_range with time before range"""
|
||||||
|
assert is_time_in_range('08:00:00', '09:00:00', '17:00:00') is False
|
||||||
|
|
||||||
|
def test_is_time_in_range_after_range(self):
|
||||||
|
"""Test is_time_in_range with time after range"""
|
||||||
|
assert is_time_in_range('18:00:00', '09:00:00', '17:00:00') is False
|
||||||
|
|
||||||
|
def test_is_time_in_range_crosses_midnight(self):
|
||||||
|
"""Test is_time_in_range with range crossing midnight"""
|
||||||
|
# Range from 22:00 to 06:00
|
||||||
|
assert is_time_in_range('23:00:00', '22:00:00', '06:00:00') is True
|
||||||
|
assert is_time_in_range('03:00:00', '22:00:00', '06:00:00') is True
|
||||||
|
assert is_time_in_range('12:00:00', '22:00:00', '06:00:00') is False
|
||||||
|
|
||||||
|
def test_is_time_in_range_midnight_boundary(self):
|
||||||
|
"""Test is_time_in_range at midnight"""
|
||||||
|
assert is_time_in_range('00:00:00', '22:00:00', '06:00:00') is True
|
||||||
|
|
||||||
|
|
||||||
|
class TestReorderWeekdaysFromToday:
|
||||||
|
"""Test suite for reorder_weekdays_from_today function"""
|
||||||
|
|
||||||
|
def test_reorder_weekdays_from_monday(self):
|
||||||
|
"""Test reorder_weekdays_from_today starting from Monday"""
|
||||||
|
result = reorder_weekdays_from_today('Mon')
|
||||||
|
values = list(result.values())
|
||||||
|
assert values[0] == 'Mon'
|
||||||
|
assert values[-1] == 'Sun'
|
||||||
|
assert len(result) == 7
|
||||||
|
|
||||||
|
def test_reorder_weekdays_from_wednesday(self):
|
||||||
|
"""Test reorder_weekdays_from_today starting from Wednesday"""
|
||||||
|
result = reorder_weekdays_from_today('Wed')
|
||||||
|
values = list(result.values())
|
||||||
|
assert values[0] == 'Wed'
|
||||||
|
assert values[1] == 'Thu'
|
||||||
|
assert values[-1] == 'Tue'
|
||||||
|
|
||||||
|
def test_reorder_weekdays_from_sunday(self):
|
||||||
|
"""Test reorder_weekdays_from_today starting from Sunday"""
|
||||||
|
result = reorder_weekdays_from_today('Sun')
|
||||||
|
values = list(result.values())
|
||||||
|
assert values[0] == 'Sun'
|
||||||
|
assert values[-1] == 'Sat'
|
||||||
|
|
||||||
|
def test_reorder_weekdays_from_long_name(self):
|
||||||
|
"""Test reorder_weekdays_from_today with long day name"""
|
||||||
|
result = reorder_weekdays_from_today('Friday')
|
||||||
|
values = list(result.values())
|
||||||
|
assert values[0] == 'Fri'
|
||||||
|
assert values[-1] == 'Thu'
|
||||||
|
|
||||||
|
def test_reorder_weekdays_invalid_day(self):
|
||||||
|
"""Test reorder_weekdays_from_today with invalid day name"""
|
||||||
|
with pytest.raises(ValueError) as exc_info:
|
||||||
|
reorder_weekdays_from_today('InvalidDay')
|
||||||
|
assert 'Invalid day name provided' in str(exc_info.value)
|
||||||
|
|
||||||
|
def test_reorder_weekdays_preserves_all_days(self):
|
||||||
|
"""Test that reorder_weekdays_from_today preserves all 7 days"""
|
||||||
|
for day in ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']:
|
||||||
|
result = reorder_weekdays_from_today(day)
|
||||||
|
assert len(result) == 7
|
||||||
|
assert set(result.values()) == {'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'}
|
||||||
|
|
||||||
|
|
||||||
|
class TestEdgeCases:
|
||||||
|
"""Test suite for edge cases and integration scenarios"""
|
||||||
|
|
||||||
|
def test_parse_flexible_date_with_various_iso_formats(self):
|
||||||
|
"""Test parse_flexible_date handles various ISO format variations"""
|
||||||
|
formats = [
|
||||||
|
'2023-12-25',
|
||||||
|
'2023-12-25T15:30:45',
|
||||||
|
'2023-12-25T15:30:45.123456',
|
||||||
|
]
|
||||||
|
for date_str in formats:
|
||||||
|
result = parse_flexible_date(date_str)
|
||||||
|
assert result is not None
|
||||||
|
assert isinstance(result, datetime)
|
||||||
|
|
||||||
|
def test_timezone_consistency_across_functions(self):
|
||||||
|
"""Test timezone handling consistency across functions"""
|
||||||
|
tz_str = 'Asia/Tokyo'
|
||||||
|
tz_obj = parse_timezone_data(tz_str)
|
||||||
|
|
||||||
|
# Both should work with get_datetime_iso8601
|
||||||
|
result1 = get_datetime_iso8601(tz_str)
|
||||||
|
result2 = get_datetime_iso8601(tz_obj)
|
||||||
|
|
||||||
|
assert result1 is not None
|
||||||
|
assert result2 is not None
|
||||||
|
|
||||||
|
def test_date_validation_and_parsing_consistency(self):
|
||||||
|
"""Test that validate_date and parse_flexible_date agree"""
|
||||||
|
valid_dates = ['2023-12-25', '2024/01/01']
|
||||||
|
for date_str in valid_dates:
|
||||||
|
# normalize format for parse_flexible_date
|
||||||
|
normalized = date_str.replace('/', '-')
|
||||||
|
assert validate_date(date_str) is True
|
||||||
|
assert parse_flexible_date(normalized) is not None
|
||||||
|
|
||||||
|
def test_day_of_week_range_complex_scenario(self):
|
||||||
|
"""Test parse_day_of_week_range with complex mixed input"""
|
||||||
|
result = parse_day_of_week_range('Mon,Wed-Fri,Sun')
|
||||||
|
assert len(result) == 5
|
||||||
|
assert (1, 'Mon') in result
|
||||||
|
assert (3, 'Wed') in result
|
||||||
|
assert (4, 'Thu') in result
|
||||||
|
assert (5, 'Fri') in result
|
||||||
|
assert (7, 'Sun') in result
|
||||||
|
|
||||||
|
def test_time_range_boundary_conditions(self):
|
||||||
|
"""Test parse_time_range with boundary times"""
|
||||||
|
start, end = parse_time_range('00:00-23:59')
|
||||||
|
assert start == time(0, 0)
|
||||||
|
assert end == time(23, 59)
|
||||||
|
|
||||||
|
# __END__
|
||||||
462
tests/unit/datetime_handling/test_seconds_to_string.py
Normal file
462
tests/unit/datetime_handling/test_seconds_to_string.py
Normal file
@@ -0,0 +1,462 @@
|
|||||||
|
"""
|
||||||
|
PyTest: datetime_handling/timestamp_convert - seconds_to_string and convert_timestamp functions
|
||||||
|
"""
|
||||||
|
|
||||||
|
from corelibs.datetime_handling.timestamp_convert import seconds_to_string, convert_timestamp
|
||||||
|
|
||||||
|
|
||||||
|
class TestSecondsToString:
|
||||||
|
"""Test suite for seconds_to_string function"""
|
||||||
|
|
||||||
|
def test_basic_integer_seconds(self):
|
||||||
|
"""Test conversion of basic integer seconds"""
|
||||||
|
assert seconds_to_string(0) == "0s"
|
||||||
|
assert seconds_to_string(1) == "1s"
|
||||||
|
assert seconds_to_string(30) == "30s"
|
||||||
|
assert seconds_to_string(59) == "59s"
|
||||||
|
|
||||||
|
def test_minutes_conversion(self):
|
||||||
|
"""Test conversion involving minutes"""
|
||||||
|
assert seconds_to_string(60) == "1m"
|
||||||
|
assert seconds_to_string(90) == "1m 30s"
|
||||||
|
assert seconds_to_string(120) == "2m"
|
||||||
|
assert seconds_to_string(3599) == "59m 59s"
|
||||||
|
|
||||||
|
def test_hours_conversion(self):
|
||||||
|
"""Test conversion involving hours"""
|
||||||
|
assert seconds_to_string(3600) == "1h"
|
||||||
|
assert seconds_to_string(3660) == "1h 1m"
|
||||||
|
assert seconds_to_string(3661) == "1h 1m 1s"
|
||||||
|
assert seconds_to_string(7200) == "2h"
|
||||||
|
assert seconds_to_string(7260) == "2h 1m"
|
||||||
|
|
||||||
|
def test_days_conversion(self):
|
||||||
|
"""Test conversion involving days"""
|
||||||
|
assert seconds_to_string(86400) == "1d"
|
||||||
|
assert seconds_to_string(86401) == "1d 1s"
|
||||||
|
assert seconds_to_string(90000) == "1d 1h"
|
||||||
|
assert seconds_to_string(90061) == "1d 1h 1m 1s"
|
||||||
|
assert seconds_to_string(172800) == "2d"
|
||||||
|
|
||||||
|
def test_complex_combinations(self):
|
||||||
|
"""Test complex time combinations"""
|
||||||
|
# 1 day, 2 hours, 3 minutes, 4 seconds
|
||||||
|
total = 86400 + 7200 + 180 + 4
|
||||||
|
assert seconds_to_string(total) == "1d 2h 3m 4s"
|
||||||
|
|
||||||
|
# 5 days, 23 hours, 59 minutes, 59 seconds
|
||||||
|
total = 5 * 86400 + 23 * 3600 + 59 * 60 + 59
|
||||||
|
assert seconds_to_string(total) == "5d 23h 59m 59s"
|
||||||
|
|
||||||
|
def test_fractional_seconds_default_precision(self):
|
||||||
|
"""Test fractional seconds with default precision (3 decimal places)"""
|
||||||
|
assert seconds_to_string(0.1) == "0.1s"
|
||||||
|
assert seconds_to_string(0.123) == "0.123s"
|
||||||
|
assert seconds_to_string(0.1234) == "0.123s"
|
||||||
|
assert seconds_to_string(1.5) == "1.5s"
|
||||||
|
assert seconds_to_string(1.567) == "1.567s"
|
||||||
|
assert seconds_to_string(1.5678) == "1.568s"
|
||||||
|
|
||||||
|
def test_fractional_seconds_microsecond_precision(self):
|
||||||
|
"""Test fractional seconds with microsecond precision"""
|
||||||
|
assert seconds_to_string(0.1, show_microseconds=True) == "0.1s"
|
||||||
|
assert seconds_to_string(0.123456, show_microseconds=True) == "0.123456s"
|
||||||
|
assert seconds_to_string(0.1234567, show_microseconds=True) == "0.123457s"
|
||||||
|
assert seconds_to_string(1.5, show_microseconds=True) == "1.5s"
|
||||||
|
assert seconds_to_string(1.567890, show_microseconds=True) == "1.56789s"
|
||||||
|
|
||||||
|
def test_fractional_seconds_with_larger_units(self):
|
||||||
|
"""Test fractional seconds combined with larger time units"""
|
||||||
|
# 1 minute and 30.5 seconds
|
||||||
|
assert seconds_to_string(90.5) == "1m 30.5s"
|
||||||
|
assert seconds_to_string(90.5, show_microseconds=True) == "1m 30.5s"
|
||||||
|
|
||||||
|
# 1 hour, 1 minute, and 1.123 seconds
|
||||||
|
total = 3600 + 60 + 1.123
|
||||||
|
assert seconds_to_string(total) == "1h 1m 1.123s"
|
||||||
|
assert seconds_to_string(total, show_microseconds=True) == "1h 1m 1.123s"
|
||||||
|
|
||||||
|
def test_negative_values(self):
|
||||||
|
"""Test negative time values"""
|
||||||
|
assert seconds_to_string(-1) == "-1s"
|
||||||
|
assert seconds_to_string(-60) == "-1m"
|
||||||
|
assert seconds_to_string(-90) == "-1m 30s"
|
||||||
|
assert seconds_to_string(-3661) == "-1h 1m 1s"
|
||||||
|
assert seconds_to_string(-86401) == "-1d 1s"
|
||||||
|
assert seconds_to_string(-1.5) == "-1.5s"
|
||||||
|
assert seconds_to_string(-90.123) == "-1m 30.123s"
|
||||||
|
|
||||||
|
def test_zero_handling(self):
|
||||||
|
"""Test various zero values"""
|
||||||
|
assert seconds_to_string(0) == "0s"
|
||||||
|
assert seconds_to_string(0.0) == "0s"
|
||||||
|
assert seconds_to_string(-0) == "0s"
|
||||||
|
assert seconds_to_string(-0.0) == "0s"
|
||||||
|
|
||||||
|
def test_float_input_types(self):
|
||||||
|
"""Test various float input types"""
|
||||||
|
assert seconds_to_string(1.0) == "1s"
|
||||||
|
assert seconds_to_string(60.0) == "1m"
|
||||||
|
assert seconds_to_string(3600.0) == "1h"
|
||||||
|
assert seconds_to_string(86400.0) == "1d"
|
||||||
|
|
||||||
|
def test_large_values(self):
|
||||||
|
"""Test handling of large time values"""
|
||||||
|
# 365 days (1 year)
|
||||||
|
year_seconds = 365 * 86400
|
||||||
|
assert seconds_to_string(year_seconds) == "365d"
|
||||||
|
|
||||||
|
# 1000 days
|
||||||
|
assert seconds_to_string(1000 * 86400) == "1000d"
|
||||||
|
|
||||||
|
# Large number with all units
|
||||||
|
large_time = 999 * 86400 + 23 * 3600 + 59 * 60 + 59.999
|
||||||
|
result = seconds_to_string(large_time)
|
||||||
|
assert result.startswith("999d")
|
||||||
|
assert "23h" in result
|
||||||
|
assert "59m" in result
|
||||||
|
assert "59.999s" in result
|
||||||
|
|
||||||
|
def test_rounding_behavior(self):
|
||||||
|
"""Test rounding behavior for fractional seconds"""
|
||||||
|
# Default precision (3 decimal places) - values are truncated via rstrip
|
||||||
|
assert seconds_to_string(1.0004) == "1s" # Truncates trailing zeros after rstrip
|
||||||
|
assert seconds_to_string(1.0005) == "1s" # Truncates trailing zeros after rstrip
|
||||||
|
assert seconds_to_string(1.9999) == "2s" # Rounds up and strips .000
|
||||||
|
|
||||||
|
# Microsecond precision (6 decimal places)
|
||||||
|
assert seconds_to_string(1.0000004, show_microseconds=True) == "1s"
|
||||||
|
assert seconds_to_string(1.0000005, show_microseconds=True) == "1.000001s"
|
||||||
|
|
||||||
|
def test_trailing_zero_removal(self):
|
||||||
|
"""Test that trailing zeros are properly removed"""
|
||||||
|
assert seconds_to_string(1.100) == "1.1s"
|
||||||
|
assert seconds_to_string(1.120) == "1.12s"
|
||||||
|
assert seconds_to_string(1.123) == "1.123s"
|
||||||
|
assert seconds_to_string(1.100000, show_microseconds=True) == "1.1s"
|
||||||
|
assert seconds_to_string(1.123000, show_microseconds=True) == "1.123s"
|
||||||
|
|
||||||
|
def test_invalid_input_types(self):
|
||||||
|
"""Test handling of invalid input types"""
|
||||||
|
# String inputs should be returned as-is
|
||||||
|
assert seconds_to_string("invalid") == "invalid"
|
||||||
|
assert seconds_to_string("not a number") == "not a number"
|
||||||
|
assert seconds_to_string("") == ""
|
||||||
|
|
||||||
|
def test_edge_cases_boundary_values(self):
|
||||||
|
"""Test edge cases at unit boundaries"""
|
||||||
|
# Exactly 1 minute - 1 second
|
||||||
|
assert seconds_to_string(59) == "59s"
|
||||||
|
assert seconds_to_string(59.999) == "59.999s"
|
||||||
|
|
||||||
|
# Exactly 1 hour - 1 second
|
||||||
|
assert seconds_to_string(3599) == "59m 59s"
|
||||||
|
assert seconds_to_string(3599.999) == "59m 59.999s"
|
||||||
|
|
||||||
|
# Exactly 1 day - 1 second
|
||||||
|
assert seconds_to_string(86399) == "23h 59m 59s"
|
||||||
|
assert seconds_to_string(86399.999) == "23h 59m 59.999s"
|
||||||
|
|
||||||
|
def test_very_small_fractional_seconds(self):
|
||||||
|
"""Test very small fractional values"""
|
||||||
|
assert seconds_to_string(0.001) == "0.001s"
|
||||||
|
assert seconds_to_string(0.0001) == "0s" # Below default precision
|
||||||
|
assert seconds_to_string(0.000001, show_microseconds=True) == "0.000001s"
|
||||||
|
assert seconds_to_string(0.0000001, show_microseconds=True) == "0s" # Below microsecond precision
|
||||||
|
|
||||||
|
def test_precision_consistency(self):
|
||||||
|
"""Test that precision is consistent across different scenarios"""
|
||||||
|
# With other units present
|
||||||
|
assert seconds_to_string(61.123456) == "1m 1.123s"
|
||||||
|
assert seconds_to_string(61.123456, show_microseconds=True) == "1m 1.123456s"
|
||||||
|
|
||||||
|
# Large values with fractional seconds
|
||||||
|
large_val = 90061.123456 # 1d 1h 1m 1.123456s
|
||||||
|
assert seconds_to_string(large_val) == "1d 1h 1m 1.123s"
|
||||||
|
assert seconds_to_string(large_val, show_microseconds=True) == "1d 1h 1m 1.123456s"
|
||||||
|
|
||||||
|
def test_string_numeric_inputs(self):
|
||||||
|
"""Test string inputs that represent numbers"""
|
||||||
|
# String inputs should be returned as-is, even if they look like numbers
|
||||||
|
assert seconds_to_string("60") == "60"
|
||||||
|
assert seconds_to_string("1.5") == "1.5"
|
||||||
|
assert seconds_to_string("0") == "0"
|
||||||
|
assert seconds_to_string("-60") == "-60"
|
||||||
|
|
||||||
|
|
||||||
|
class TestConvertTimestamp:
|
||||||
|
"""Test suite for convert_timestamp function"""
|
||||||
|
|
||||||
|
def test_basic_integer_seconds(self):
|
||||||
|
"""Test conversion of basic integer seconds"""
|
||||||
|
assert convert_timestamp(0) == "0s 0ms"
|
||||||
|
assert convert_timestamp(1) == "1s 0ms"
|
||||||
|
assert convert_timestamp(30) == "30s 0ms"
|
||||||
|
assert convert_timestamp(59) == "59s 0ms"
|
||||||
|
|
||||||
|
def test_basic_without_microseconds(self):
|
||||||
|
"""Test conversion without showing microseconds"""
|
||||||
|
assert convert_timestamp(0, show_microseconds=False) == "0s"
|
||||||
|
assert convert_timestamp(1, show_microseconds=False) == "1s"
|
||||||
|
assert convert_timestamp(30, show_microseconds=False) == "30s"
|
||||||
|
assert convert_timestamp(59, show_microseconds=False) == "59s"
|
||||||
|
|
||||||
|
def test_minutes_conversion(self):
|
||||||
|
"""Test conversion involving minutes"""
|
||||||
|
assert convert_timestamp(60) == "1m 0s 0ms"
|
||||||
|
assert convert_timestamp(90) == "1m 30s 0ms"
|
||||||
|
assert convert_timestamp(120) == "2m 0s 0ms"
|
||||||
|
assert convert_timestamp(3599) == "59m 59s 0ms"
|
||||||
|
|
||||||
|
def test_minutes_conversion_without_microseconds(self):
|
||||||
|
"""Test conversion involving minutes without microseconds"""
|
||||||
|
assert convert_timestamp(60, show_microseconds=False) == "1m 0s"
|
||||||
|
assert convert_timestamp(90, show_microseconds=False) == "1m 30s"
|
||||||
|
assert convert_timestamp(120, show_microseconds=False) == "2m 0s"
|
||||||
|
|
||||||
|
def test_hours_conversion(self):
|
||||||
|
"""Test conversion involving hours"""
|
||||||
|
assert convert_timestamp(3600) == "1h 0m 0s 0ms"
|
||||||
|
assert convert_timestamp(3660) == "1h 1m 0s 0ms"
|
||||||
|
assert convert_timestamp(3661) == "1h 1m 1s 0ms"
|
||||||
|
assert convert_timestamp(7200) == "2h 0m 0s 0ms"
|
||||||
|
assert convert_timestamp(7260) == "2h 1m 0s 0ms"
|
||||||
|
|
||||||
|
def test_hours_conversion_without_microseconds(self):
|
||||||
|
"""Test conversion involving hours without microseconds"""
|
||||||
|
assert convert_timestamp(3600, show_microseconds=False) == "1h 0m 0s"
|
||||||
|
assert convert_timestamp(3660, show_microseconds=False) == "1h 1m 0s"
|
||||||
|
assert convert_timestamp(3661, show_microseconds=False) == "1h 1m 1s"
|
||||||
|
|
||||||
|
def test_days_conversion(self):
|
||||||
|
"""Test conversion involving days"""
|
||||||
|
assert convert_timestamp(86400) == "1d 0h 0m 0s 0ms"
|
||||||
|
assert convert_timestamp(86401) == "1d 0h 0m 1s 0ms"
|
||||||
|
assert convert_timestamp(90000) == "1d 1h 0m 0s 0ms"
|
||||||
|
assert convert_timestamp(90061) == "1d 1h 1m 1s 0ms"
|
||||||
|
assert convert_timestamp(172800) == "2d 0h 0m 0s 0ms"
|
||||||
|
|
||||||
|
def test_days_conversion_without_microseconds(self):
|
||||||
|
"""Test conversion involving days without microseconds"""
|
||||||
|
assert convert_timestamp(86400, show_microseconds=False) == "1d 0h 0m 0s"
|
||||||
|
assert convert_timestamp(86401, show_microseconds=False) == "1d 0h 0m 1s"
|
||||||
|
assert convert_timestamp(90000, show_microseconds=False) == "1d 1h 0m 0s"
|
||||||
|
|
||||||
|
def test_complex_combinations(self):
|
||||||
|
"""Test complex time combinations"""
|
||||||
|
# 1 day, 2 hours, 3 minutes, 4 seconds
|
||||||
|
total = 86400 + 7200 + 180 + 4
|
||||||
|
assert convert_timestamp(total) == "1d 2h 3m 4s 0ms"
|
||||||
|
|
||||||
|
# 5 days, 23 hours, 59 minutes, 59 seconds
|
||||||
|
total = 5 * 86400 + 23 * 3600 + 59 * 60 + 59
|
||||||
|
assert convert_timestamp(total) == "5d 23h 59m 59s 0ms"
|
||||||
|
|
||||||
|
def test_fractional_seconds_with_microseconds(self):
|
||||||
|
"""Test fractional seconds showing microseconds"""
|
||||||
|
# Note: ms value is the integer of the decimal part string after rounding to 4 places
|
||||||
|
assert convert_timestamp(0.1) == "0s 1ms" # 0.1 → "0.1" → ms=1
|
||||||
|
assert convert_timestamp(0.123) == "0s 123ms" # 0.123 → "0.123" → ms=123
|
||||||
|
assert convert_timestamp(0.1234) == "0s 1234ms" # 0.1234 → "0.1234" → ms=1234
|
||||||
|
assert convert_timestamp(1.5) == "1s 5ms" # 1.5 → "1.5" → ms=5
|
||||||
|
assert convert_timestamp(1.567) == "1s 567ms" # 1.567 → "1.567" → ms=567
|
||||||
|
assert convert_timestamp(1.5678) == "1s 5678ms" # 1.5678 rounds to 1.5678 → ms=5678
|
||||||
|
|
||||||
|
def test_fractional_seconds_rounding(self):
|
||||||
|
"""Test rounding of fractional seconds to 4 decimal places"""
|
||||||
|
# The function rounds to 4 decimal places before splitting
|
||||||
|
assert convert_timestamp(0.12345) == "0s 1235ms" # Rounds to 0.1235
|
||||||
|
assert convert_timestamp(0.123456) == "0s 1235ms" # Rounds to 0.1235
|
||||||
|
assert convert_timestamp(1.99999) == "2s 0ms" # Rounds to 2.0
|
||||||
|
|
||||||
|
def test_fractional_seconds_with_larger_units(self):
|
||||||
|
"""Test fractional seconds combined with larger time units"""
|
||||||
|
# 1 minute and 30.5 seconds
|
||||||
|
assert convert_timestamp(90.5) == "1m 30s 5ms"
|
||||||
|
|
||||||
|
# 1 hour, 1 minute, and 1.123 seconds
|
||||||
|
total = 3600 + 60 + 1.123
|
||||||
|
assert convert_timestamp(total) == "1h 1m 1s 123ms"
|
||||||
|
|
||||||
|
def test_negative_values(self):
|
||||||
|
"""Test negative time values"""
|
||||||
|
assert convert_timestamp(-1) == "-1s 0ms"
|
||||||
|
assert convert_timestamp(-60) == "-1m 0s 0ms"
|
||||||
|
assert convert_timestamp(-90) == "-1m 30s 0ms"
|
||||||
|
assert convert_timestamp(-3661) == "-1h 1m 1s 0ms"
|
||||||
|
assert convert_timestamp(-86401) == "-1d 0h 0m 1s 0ms"
|
||||||
|
assert convert_timestamp(-1.5) == "-1s 5ms"
|
||||||
|
assert convert_timestamp(-90.123) == "-1m 30s 123ms"
|
||||||
|
|
||||||
|
def test_negative_without_microseconds(self):
|
||||||
|
"""Test negative values without microseconds"""
|
||||||
|
assert convert_timestamp(-1, show_microseconds=False) == "-1s"
|
||||||
|
assert convert_timestamp(-60, show_microseconds=False) == "-1m 0s"
|
||||||
|
assert convert_timestamp(-90.123, show_microseconds=False) == "-1m 30s"
|
||||||
|
|
||||||
|
def test_zero_handling(self):
|
||||||
|
"""Test various zero values"""
|
||||||
|
assert convert_timestamp(0) == "0s 0ms"
|
||||||
|
assert convert_timestamp(0.0) == "0s 0ms"
|
||||||
|
assert convert_timestamp(-0) == "0s 0ms"
|
||||||
|
assert convert_timestamp(-0.0) == "0s 0ms"
|
||||||
|
|
||||||
|
def test_zero_filling_behavior(self):
|
||||||
|
"""Test that zeros are filled between set values"""
|
||||||
|
# If we have days and seconds, hours and minutes should be 0
|
||||||
|
assert convert_timestamp(86401) == "1d 0h 0m 1s 0ms"
|
||||||
|
|
||||||
|
# If we have hours and seconds, minutes should be 0
|
||||||
|
assert convert_timestamp(3601) == "1h 0m 1s 0ms"
|
||||||
|
|
||||||
|
# If we have days and hours, minutes and seconds should be 0
|
||||||
|
assert convert_timestamp(90000) == "1d 1h 0m 0s 0ms"
|
||||||
|
|
||||||
|
def test_milliseconds_display(self):
|
||||||
|
"""Test milliseconds are always shown when show_microseconds=True"""
|
||||||
|
# Even with no fractional part, 0ms should be shown
|
||||||
|
assert convert_timestamp(1) == "1s 0ms"
|
||||||
|
assert convert_timestamp(60) == "1m 0s 0ms"
|
||||||
|
assert convert_timestamp(3600) == "1h 0m 0s 0ms"
|
||||||
|
|
||||||
|
# With fractional part, ms should be shown
|
||||||
|
assert convert_timestamp(1.001) == "1s 1ms" # "1.001" → ms=1
|
||||||
|
assert convert_timestamp(1.0001) == "1s 1ms" # "1.0001" → ms=1
|
||||||
|
|
||||||
|
def test_float_input_types(self):
|
||||||
|
"""Test various float input types"""
|
||||||
|
assert convert_timestamp(1.0) == "1s 0ms"
|
||||||
|
assert convert_timestamp(60.0) == "1m 0s 0ms"
|
||||||
|
assert convert_timestamp(3600.0) == "1h 0m 0s 0ms"
|
||||||
|
assert convert_timestamp(86400.0) == "1d 0h 0m 0s 0ms"
|
||||||
|
|
||||||
|
def test_large_values(self):
|
||||||
|
"""Test handling of large time values"""
|
||||||
|
# 365 days (1 year)
|
||||||
|
year_seconds = 365 * 86400
|
||||||
|
assert convert_timestamp(year_seconds) == "365d 0h 0m 0s 0ms"
|
||||||
|
|
||||||
|
# 1000 days
|
||||||
|
assert convert_timestamp(1000 * 86400) == "1000d 0h 0m 0s 0ms"
|
||||||
|
|
||||||
|
# Large number with all units
|
||||||
|
large_time = 999 * 86400 + 23 * 3600 + 59 * 60 + 59.999
|
||||||
|
result = convert_timestamp(large_time)
|
||||||
|
assert result.startswith("999d")
|
||||||
|
assert "23h" in result
|
||||||
|
assert "59m" in result
|
||||||
|
assert "59s" in result
|
||||||
|
assert "999ms" in result # 59.999 rounds to 59.999, ms=999
|
||||||
|
|
||||||
|
def test_invalid_input_types(self):
|
||||||
|
"""Test handling of invalid input types"""
|
||||||
|
# String inputs should be returned as-is
|
||||||
|
assert convert_timestamp("invalid") == "invalid"
|
||||||
|
assert convert_timestamp("not a number") == "not a number"
|
||||||
|
assert convert_timestamp("") == ""
|
||||||
|
|
||||||
|
def test_string_numeric_inputs(self):
|
||||||
|
"""Test string inputs that represent numbers"""
|
||||||
|
# String inputs should be returned as-is, even if they look like numbers
|
||||||
|
assert convert_timestamp("60") == "60"
|
||||||
|
assert convert_timestamp("1.5") == "1.5"
|
||||||
|
assert convert_timestamp("0") == "0"
|
||||||
|
assert convert_timestamp("-60") == "-60"
|
||||||
|
|
||||||
|
def test_edge_cases_boundary_values(self):
|
||||||
|
"""Test edge cases at unit boundaries"""
|
||||||
|
# Exactly 1 minute - 1 second
|
||||||
|
assert convert_timestamp(59) == "59s 0ms"
|
||||||
|
assert convert_timestamp(59.999) == "59s 999ms"
|
||||||
|
|
||||||
|
# Exactly 1 hour - 1 second
|
||||||
|
assert convert_timestamp(3599) == "59m 59s 0ms"
|
||||||
|
assert convert_timestamp(3599.999) == "59m 59s 999ms"
|
||||||
|
|
||||||
|
# Exactly 1 day - 1 second
|
||||||
|
assert convert_timestamp(86399) == "23h 59m 59s 0ms"
|
||||||
|
assert convert_timestamp(86399.999) == "23h 59m 59s 999ms"
|
||||||
|
|
||||||
|
def test_very_small_fractional_seconds(self):
|
||||||
|
"""Test very small fractional values"""
|
||||||
|
assert convert_timestamp(0.001) == "0s 1ms" # 0.001 → "0.001" → ms=1
|
||||||
|
assert convert_timestamp(0.0001) == "0s 1ms" # 0.0001 → "0.0001" → ms=1
|
||||||
|
assert convert_timestamp(0.00005) == "0s 1ms" # 0.00005 rounds to 0.0001 → ms=1
|
||||||
|
assert convert_timestamp(0.00004) == "0s 0ms" # 0.00004 rounds to 0.0 → ms=0
|
||||||
|
|
||||||
|
def test_milliseconds_extraction(self):
|
||||||
|
"""Test that milliseconds are correctly extracted from fractional part"""
|
||||||
|
# The ms value is the integer of the decimal part string, not a conversion
|
||||||
|
# So 0.1 → "0.1" → ms=1, NOT 100ms as you might expect
|
||||||
|
assert convert_timestamp(0.1) == "0s 1ms"
|
||||||
|
# 0.01 seconds → "0.01" → ms=1 (int("01") = 1)
|
||||||
|
assert convert_timestamp(0.01) == "0s 1ms"
|
||||||
|
# 0.001 seconds → "0.001" → ms=1
|
||||||
|
assert convert_timestamp(0.001) == "0s 1ms"
|
||||||
|
# 0.0001 seconds → "0.0001" → ms=1
|
||||||
|
assert convert_timestamp(0.0001) == "0s 1ms"
|
||||||
|
# 0.00004 seconds rounds to "0.0" → ms=0
|
||||||
|
assert convert_timestamp(0.00004) == "0s 0ms"
|
||||||
|
|
||||||
|
def test_comparison_with_seconds_to_string(self):
|
||||||
|
"""Test differences between convert_timestamp and seconds_to_string"""
|
||||||
|
# convert_timestamp fills zeros and adds ms
|
||||||
|
# seconds_to_string omits zeros and no ms
|
||||||
|
assert convert_timestamp(86401) == "1d 0h 0m 1s 0ms"
|
||||||
|
assert seconds_to_string(86401) == "1d 1s"
|
||||||
|
|
||||||
|
assert convert_timestamp(3661) == "1h 1m 1s 0ms"
|
||||||
|
assert seconds_to_string(3661) == "1h 1m 1s"
|
||||||
|
|
||||||
|
# With microseconds disabled, still different due to zero-filling
|
||||||
|
assert convert_timestamp(86401, show_microseconds=False) == "1d 0h 0m 1s"
|
||||||
|
assert seconds_to_string(86401) == "1d 1s"
|
||||||
|
|
||||||
|
def test_precision_consistency(self):
|
||||||
|
"""Test that precision is consistent across different scenarios"""
|
||||||
|
# With other units present
|
||||||
|
assert convert_timestamp(61.123456) == "1m 1s 1235ms" # Rounds to 61.1235
|
||||||
|
|
||||||
|
# Large values with fractional seconds
|
||||||
|
large_val = 90061.123456 # 1d 1h 1m 1.123456s
|
||||||
|
assert convert_timestamp(large_val) == "1d 1h 1m 1s 1235ms" # Rounds to .1235
|
||||||
|
|
||||||
|
def test_microseconds_flag_consistency(self):
|
||||||
|
"""Test that show_microseconds flag works consistently"""
|
||||||
|
test_values = [0, 1, 60, 3600, 86400, 1.5, 90.123, -60]
|
||||||
|
|
||||||
|
for val in test_values:
|
||||||
|
with_ms = convert_timestamp(val, show_microseconds=True)
|
||||||
|
without_ms = convert_timestamp(val, show_microseconds=False)
|
||||||
|
|
||||||
|
# With microseconds should contain 'ms', without should not
|
||||||
|
assert "ms" in with_ms
|
||||||
|
assert "ms" not in without_ms
|
||||||
|
|
||||||
|
# Both should start with same sign if negative
|
||||||
|
if val < 0:
|
||||||
|
assert with_ms.startswith("-")
|
||||||
|
assert without_ms.startswith("-")
|
||||||
|
|
||||||
|
def test_format_consistency(self):
|
||||||
|
"""Test that output format is consistent"""
|
||||||
|
# All outputs should have consistent spacing and unit ordering
|
||||||
|
# Format should be: [d ]h m s[ ms]
|
||||||
|
result = convert_timestamp(93784.5678) # 1d 2h 3m 4.5678s
|
||||||
|
# 93784.5678 rounds to 93784.5678, splits to ["93784", "5678"]
|
||||||
|
assert result == "1d 2h 3m 4s 5678ms"
|
||||||
|
|
||||||
|
# Verify parts are in correct order
|
||||||
|
parts = result.split()
|
||||||
|
# Extract units properly: last 1-2 chars that are letters
|
||||||
|
units = []
|
||||||
|
for p in parts:
|
||||||
|
if p.endswith('ms'):
|
||||||
|
units.append('ms')
|
||||||
|
elif p[-1].isalpha():
|
||||||
|
units.append(p[-1])
|
||||||
|
# Should be in order: d, h, m, s, ms
|
||||||
|
expected_order = ['d', 'h', 'm', 's', 'ms']
|
||||||
|
assert units == expected_order
|
||||||
|
|
||||||
|
# __END__
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
PyTest: string_handling/timestamp_strings
|
PyTest: datetime_handling/timestamp_strings
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@@ -8,7 +8,7 @@ from zoneinfo import ZoneInfo
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
# Assuming the class is in a file called timestamp_strings.py
|
# Assuming the class is in a file called timestamp_strings.py
|
||||||
from corelibs.string_handling.timestamp_strings import TimestampStrings
|
from corelibs.datetime_handling.timestamp_strings import TimestampStrings
|
||||||
|
|
||||||
|
|
||||||
class TestTimestampStrings:
|
class TestTimestampStrings:
|
||||||
@@ -16,7 +16,7 @@ class TestTimestampStrings:
|
|||||||
|
|
||||||
def test_default_initialization(self):
|
def test_default_initialization(self):
|
||||||
"""Test initialization with default timezone"""
|
"""Test initialization with default timezone"""
|
||||||
with patch('corelibs.string_handling.timestamp_strings.datetime') as mock_datetime:
|
with patch('corelibs.datetime_handling.timestamp_strings.datetime') as mock_datetime:
|
||||||
mock_now = datetime(2023, 12, 25, 15, 30, 45)
|
mock_now = datetime(2023, 12, 25, 15, 30, 45)
|
||||||
mock_datetime.now.return_value = mock_now
|
mock_datetime.now.return_value = mock_now
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ class TestTimestampStrings:
|
|||||||
"""Test initialization with custom timezone"""
|
"""Test initialization with custom timezone"""
|
||||||
custom_tz = 'America/New_York'
|
custom_tz = 'America/New_York'
|
||||||
|
|
||||||
with patch('corelibs.string_handling.timestamp_strings.datetime') as mock_datetime:
|
with patch('corelibs.datetime_handling.timestamp_strings.datetime') as mock_datetime:
|
||||||
mock_now = datetime(2023, 12, 25, 15, 30, 45)
|
mock_now = datetime(2023, 12, 25, 15, 30, 45)
|
||||||
mock_datetime.now.return_value = mock_now
|
mock_datetime.now.return_value = mock_now
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ class TestTimestampStrings:
|
|||||||
|
|
||||||
def test_timestamp_formats(self):
|
def test_timestamp_formats(self):
|
||||||
"""Test various timestamp format outputs"""
|
"""Test various timestamp format outputs"""
|
||||||
with patch('corelibs.string_handling.timestamp_strings.datetime') as mock_datetime:
|
with patch('corelibs.datetime_handling.timestamp_strings.datetime') as mock_datetime:
|
||||||
# Mock both datetime.now() calls
|
# Mock both datetime.now() calls
|
||||||
mock_now = datetime(2023, 12, 25, 9, 5, 3)
|
mock_now = datetime(2023, 12, 25, 9, 5, 3)
|
||||||
mock_now_tz = datetime(2023, 12, 25, 23, 5, 3, tzinfo=ZoneInfo('Asia/Tokyo'))
|
mock_now_tz = datetime(2023, 12, 25, 23, 5, 3, tzinfo=ZoneInfo('Asia/Tokyo'))
|
||||||
@@ -68,7 +68,7 @@ class TestTimestampStrings:
|
|||||||
|
|
||||||
def test_different_timezones_produce_different_results(self):
|
def test_different_timezones_produce_different_results(self):
|
||||||
"""Test that different timezones produce different timestamp_tz values"""
|
"""Test that different timezones produce different timestamp_tz values"""
|
||||||
with patch('corelibs.string_handling.timestamp_strings.datetime') as mock_datetime:
|
with patch('corelibs.datetime_handling.timestamp_strings.datetime') as mock_datetime:
|
||||||
mock_now = datetime(2023, 12, 25, 12, 0, 0)
|
mock_now = datetime(2023, 12, 25, 12, 0, 0)
|
||||||
mock_datetime.now.return_value = mock_now
|
mock_datetime.now.return_value = mock_now
|
||||||
|
|
||||||
@@ -86,7 +86,7 @@ class TestTimestampStrings:
|
|||||||
|
|
||||||
def test_none_timezone_uses_default(self):
|
def test_none_timezone_uses_default(self):
|
||||||
"""Test that passing None for timezone uses class default"""
|
"""Test that passing None for timezone uses class default"""
|
||||||
with patch('corelibs.string_handling.timestamp_strings.datetime') as mock_datetime:
|
with patch('corelibs.datetime_handling.timestamp_strings.datetime') as mock_datetime:
|
||||||
mock_now = datetime(2023, 12, 25, 15, 30, 45)
|
mock_now = datetime(2023, 12, 25, 15, 30, 45)
|
||||||
mock_datetime.now.return_value = mock_now
|
mock_datetime.now.return_value = mock_now
|
||||||
|
|
||||||
@@ -96,7 +96,7 @@ class TestTimestampStrings:
|
|||||||
|
|
||||||
def test_timestamp_file_format_no_colons(self):
|
def test_timestamp_file_format_no_colons(self):
|
||||||
"""Test that timestamp_file format doesn't contain colons (safe for filenames)"""
|
"""Test that timestamp_file format doesn't contain colons (safe for filenames)"""
|
||||||
with patch('corelibs.string_handling.timestamp_strings.datetime') as mock_datetime:
|
with patch('corelibs.datetime_handling.timestamp_strings.datetime') as mock_datetime:
|
||||||
mock_now = datetime(2023, 12, 25, 15, 30, 45)
|
mock_now = datetime(2023, 12, 25, 15, 30, 45)
|
||||||
mock_datetime.now.return_value = mock_now
|
mock_datetime.now.return_value = mock_now
|
||||||
|
|
||||||
@@ -108,7 +108,7 @@ class TestTimestampStrings:
|
|||||||
|
|
||||||
def test_multiple_instances_independent(self):
|
def test_multiple_instances_independent(self):
|
||||||
"""Test that multiple instances don't interfere with each other"""
|
"""Test that multiple instances don't interfere with each other"""
|
||||||
with patch('corelibs.string_handling.timestamp_strings.datetime') as mock_datetime:
|
with patch('corelibs.datetime_handling.timestamp_strings.datetime') as mock_datetime:
|
||||||
mock_now = datetime(2023, 12, 25, 15, 30, 45)
|
mock_now = datetime(2023, 12, 25, 15, 30, 45)
|
||||||
mock_datetime.now.return_value = mock_now
|
mock_datetime.now.return_value = mock_now
|
||||||
|
|
||||||
@@ -121,8 +121,8 @@ class TestTimestampStrings:
|
|||||||
|
|
||||||
def test_zoneinfo_called_correctly_with_string(self):
|
def test_zoneinfo_called_correctly_with_string(self):
|
||||||
"""Test that ZoneInfo is called with correct timezone when passing string"""
|
"""Test that ZoneInfo is called with correct timezone when passing string"""
|
||||||
with patch('corelibs.string_handling.timestamp_strings.ZoneInfo') as mock_zoneinfo:
|
with patch('corelibs.datetime_handling.timestamp_strings.ZoneInfo') as mock_zoneinfo:
|
||||||
with patch('corelibs.string_handling.timestamp_strings.datetime') as mock_datetime:
|
with patch('corelibs.datetime_handling.timestamp_strings.datetime') as mock_datetime:
|
||||||
mock_now = datetime(2023, 12, 25, 15, 30, 45)
|
mock_now = datetime(2023, 12, 25, 15, 30, 45)
|
||||||
mock_datetime.now.return_value = mock_now
|
mock_datetime.now.return_value = mock_now
|
||||||
|
|
||||||
@@ -134,7 +134,7 @@ class TestTimestampStrings:
|
|||||||
|
|
||||||
def test_zoneinfo_object_parameter(self):
|
def test_zoneinfo_object_parameter(self):
|
||||||
"""Test that ZoneInfo objects can be passed directly as timezone parameter"""
|
"""Test that ZoneInfo objects can be passed directly as timezone parameter"""
|
||||||
with patch('corelibs.string_handling.timestamp_strings.datetime') as mock_datetime:
|
with patch('corelibs.datetime_handling.timestamp_strings.datetime') as mock_datetime:
|
||||||
mock_now = datetime(2023, 12, 25, 15, 30, 45)
|
mock_now = datetime(2023, 12, 25, 15, 30, 45)
|
||||||
mock_now_tz = datetime(2023, 12, 25, 15, 30, 45, tzinfo=ZoneInfo('Europe/Paris'))
|
mock_now_tz = datetime(2023, 12, 25, 15, 30, 45, tzinfo=ZoneInfo('Europe/Paris'))
|
||||||
mock_datetime.now.side_effect = [mock_now, mock_now_tz]
|
mock_datetime.now.side_effect = [mock_now, mock_now_tz]
|
||||||
@@ -149,7 +149,7 @@ class TestTimestampStrings:
|
|||||||
|
|
||||||
def test_zoneinfo_object_vs_string_equivalence(self):
|
def test_zoneinfo_object_vs_string_equivalence(self):
|
||||||
"""Test that ZoneInfo object and string produce equivalent results"""
|
"""Test that ZoneInfo object and string produce equivalent results"""
|
||||||
with patch('corelibs.string_handling.timestamp_strings.datetime') as mock_datetime:
|
with patch('corelibs.datetime_handling.timestamp_strings.datetime') as mock_datetime:
|
||||||
mock_now = datetime(2023, 12, 25, 15, 30, 45)
|
mock_now = datetime(2023, 12, 25, 15, 30, 45)
|
||||||
mock_now_tz = datetime(2023, 12, 25, 15, 30, 45, tzinfo=ZoneInfo('Europe/Paris'))
|
mock_now_tz = datetime(2023, 12, 25, 15, 30, 45, tzinfo=ZoneInfo('Europe/Paris'))
|
||||||
mock_datetime.now.side_effect = [mock_now, mock_now_tz, mock_now, mock_now_tz]
|
mock_datetime.now.side_effect = [mock_now, mock_now_tz, mock_now, mock_now_tz]
|
||||||
@@ -171,7 +171,7 @@ class TestTimestampStrings:
|
|||||||
|
|
||||||
def test_edge_case_midnight(self):
|
def test_edge_case_midnight(self):
|
||||||
"""Test timestamp formatting at midnight"""
|
"""Test timestamp formatting at midnight"""
|
||||||
with patch('corelibs.string_handling.timestamp_strings.datetime') as mock_datetime:
|
with patch('corelibs.datetime_handling.timestamp_strings.datetime') as mock_datetime:
|
||||||
mock_now = datetime(2023, 12, 25, 0, 0, 0)
|
mock_now = datetime(2023, 12, 25, 0, 0, 0)
|
||||||
mock_datetime.now.return_value = mock_now
|
mock_datetime.now.return_value = mock_now
|
||||||
|
|
||||||
@@ -182,7 +182,7 @@ class TestTimestampStrings:
|
|||||||
|
|
||||||
def test_edge_case_new_year(self):
|
def test_edge_case_new_year(self):
|
||||||
"""Test timestamp formatting at new year"""
|
"""Test timestamp formatting at new year"""
|
||||||
with patch('corelibs.string_handling.timestamp_strings.datetime') as mock_datetime:
|
with patch('corelibs.datetime_handling.timestamp_strings.datetime') as mock_datetime:
|
||||||
mock_now = datetime(2024, 1, 1, 0, 0, 0)
|
mock_now = datetime(2024, 1, 1, 0, 0, 0)
|
||||||
mock_datetime.now.return_value = mock_now
|
mock_datetime.now.return_value = mock_now
|
||||||
|
|
||||||
@@ -1,186 +0,0 @@
|
|||||||
"""
|
|
||||||
PyTest: string_handling/timestamp_strings - seconds_to_string function
|
|
||||||
"""
|
|
||||||
|
|
||||||
from corelibs.string_handling.timestamp_strings import seconds_to_string
|
|
||||||
|
|
||||||
|
|
||||||
class TestSecondsToString:
|
|
||||||
"""Test suite for seconds_to_string function"""
|
|
||||||
|
|
||||||
def test_basic_integer_seconds(self):
|
|
||||||
"""Test conversion of basic integer seconds"""
|
|
||||||
assert seconds_to_string(0) == "0s"
|
|
||||||
assert seconds_to_string(1) == "1s"
|
|
||||||
assert seconds_to_string(30) == "30s"
|
|
||||||
assert seconds_to_string(59) == "59s"
|
|
||||||
|
|
||||||
def test_minutes_conversion(self):
|
|
||||||
"""Test conversion involving minutes"""
|
|
||||||
assert seconds_to_string(60) == "1m"
|
|
||||||
assert seconds_to_string(90) == "1m 30s"
|
|
||||||
assert seconds_to_string(120) == "2m"
|
|
||||||
assert seconds_to_string(3599) == "59m 59s"
|
|
||||||
|
|
||||||
def test_hours_conversion(self):
|
|
||||||
"""Test conversion involving hours"""
|
|
||||||
assert seconds_to_string(3600) == "1h"
|
|
||||||
assert seconds_to_string(3660) == "1h 1m"
|
|
||||||
assert seconds_to_string(3661) == "1h 1m 1s"
|
|
||||||
assert seconds_to_string(7200) == "2h"
|
|
||||||
assert seconds_to_string(7260) == "2h 1m"
|
|
||||||
|
|
||||||
def test_days_conversion(self):
|
|
||||||
"""Test conversion involving days"""
|
|
||||||
assert seconds_to_string(86400) == "1d"
|
|
||||||
assert seconds_to_string(86401) == "1d 1s"
|
|
||||||
assert seconds_to_string(90000) == "1d 1h"
|
|
||||||
assert seconds_to_string(90061) == "1d 1h 1m 1s"
|
|
||||||
assert seconds_to_string(172800) == "2d"
|
|
||||||
|
|
||||||
def test_complex_combinations(self):
|
|
||||||
"""Test complex time combinations"""
|
|
||||||
# 1 day, 2 hours, 3 minutes, 4 seconds
|
|
||||||
total = 86400 + 7200 + 180 + 4
|
|
||||||
assert seconds_to_string(total) == "1d 2h 3m 4s"
|
|
||||||
|
|
||||||
# 5 days, 23 hours, 59 minutes, 59 seconds
|
|
||||||
total = 5 * 86400 + 23 * 3600 + 59 * 60 + 59
|
|
||||||
assert seconds_to_string(total) == "5d 23h 59m 59s"
|
|
||||||
|
|
||||||
def test_fractional_seconds_default_precision(self):
|
|
||||||
"""Test fractional seconds with default precision (3 decimal places)"""
|
|
||||||
assert seconds_to_string(0.1) == "0.1s"
|
|
||||||
assert seconds_to_string(0.123) == "0.123s"
|
|
||||||
assert seconds_to_string(0.1234) == "0.123s"
|
|
||||||
assert seconds_to_string(1.5) == "1.5s"
|
|
||||||
assert seconds_to_string(1.567) == "1.567s"
|
|
||||||
assert seconds_to_string(1.5678) == "1.568s"
|
|
||||||
|
|
||||||
def test_fractional_seconds_microsecond_precision(self):
|
|
||||||
"""Test fractional seconds with microsecond precision"""
|
|
||||||
assert seconds_to_string(0.1, show_microseconds=True) == "0.1s"
|
|
||||||
assert seconds_to_string(0.123456, show_microseconds=True) == "0.123456s"
|
|
||||||
assert seconds_to_string(0.1234567, show_microseconds=True) == "0.123457s"
|
|
||||||
assert seconds_to_string(1.5, show_microseconds=True) == "1.5s"
|
|
||||||
assert seconds_to_string(1.567890, show_microseconds=True) == "1.56789s"
|
|
||||||
|
|
||||||
def test_fractional_seconds_with_larger_units(self):
|
|
||||||
"""Test fractional seconds combined with larger time units"""
|
|
||||||
# 1 minute and 30.5 seconds
|
|
||||||
assert seconds_to_string(90.5) == "1m 30.5s"
|
|
||||||
assert seconds_to_string(90.5, show_microseconds=True) == "1m 30.5s"
|
|
||||||
|
|
||||||
# 1 hour, 1 minute, and 1.123 seconds
|
|
||||||
total = 3600 + 60 + 1.123
|
|
||||||
assert seconds_to_string(total) == "1h 1m 1.123s"
|
|
||||||
assert seconds_to_string(total, show_microseconds=True) == "1h 1m 1.123s"
|
|
||||||
|
|
||||||
def test_negative_values(self):
|
|
||||||
"""Test negative time values"""
|
|
||||||
assert seconds_to_string(-1) == "-1s"
|
|
||||||
assert seconds_to_string(-60) == "-1m"
|
|
||||||
assert seconds_to_string(-90) == "-1m 30s"
|
|
||||||
assert seconds_to_string(-3661) == "-1h 1m 1s"
|
|
||||||
assert seconds_to_string(-86401) == "-1d 1s"
|
|
||||||
assert seconds_to_string(-1.5) == "-1.5s"
|
|
||||||
assert seconds_to_string(-90.123) == "-1m 30.123s"
|
|
||||||
|
|
||||||
def test_zero_handling(self):
|
|
||||||
"""Test various zero values"""
|
|
||||||
assert seconds_to_string(0) == "0s"
|
|
||||||
assert seconds_to_string(0.0) == "0s"
|
|
||||||
assert seconds_to_string(-0) == "0s"
|
|
||||||
assert seconds_to_string(-0.0) == "0s"
|
|
||||||
|
|
||||||
def test_float_input_types(self):
|
|
||||||
"""Test various float input types"""
|
|
||||||
assert seconds_to_string(1.0) == "1s"
|
|
||||||
assert seconds_to_string(60.0) == "1m"
|
|
||||||
assert seconds_to_string(3600.0) == "1h"
|
|
||||||
assert seconds_to_string(86400.0) == "1d"
|
|
||||||
|
|
||||||
def test_large_values(self):
|
|
||||||
"""Test handling of large time values"""
|
|
||||||
# 365 days (1 year)
|
|
||||||
year_seconds = 365 * 86400
|
|
||||||
assert seconds_to_string(year_seconds) == "365d"
|
|
||||||
|
|
||||||
# 1000 days
|
|
||||||
assert seconds_to_string(1000 * 86400) == "1000d"
|
|
||||||
|
|
||||||
# Large number with all units
|
|
||||||
large_time = 999 * 86400 + 23 * 3600 + 59 * 60 + 59.999
|
|
||||||
result = seconds_to_string(large_time)
|
|
||||||
assert result.startswith("999d")
|
|
||||||
assert "23h" in result
|
|
||||||
assert "59m" in result
|
|
||||||
assert "59.999s" in result
|
|
||||||
|
|
||||||
def test_rounding_behavior(self):
|
|
||||||
"""Test rounding behavior for fractional seconds"""
|
|
||||||
# Default precision (3 decimal places) - values are truncated via rstrip
|
|
||||||
assert seconds_to_string(1.0004) == "1s" # Truncates trailing zeros after rstrip
|
|
||||||
assert seconds_to_string(1.0005) == "1s" # Truncates trailing zeros after rstrip
|
|
||||||
assert seconds_to_string(1.9999) == "2s" # Rounds up and strips .000
|
|
||||||
|
|
||||||
# Microsecond precision (6 decimal places)
|
|
||||||
assert seconds_to_string(1.0000004, show_microseconds=True) == "1s"
|
|
||||||
assert seconds_to_string(1.0000005, show_microseconds=True) == "1.000001s"
|
|
||||||
|
|
||||||
def test_trailing_zero_removal(self):
|
|
||||||
"""Test that trailing zeros are properly removed"""
|
|
||||||
assert seconds_to_string(1.100) == "1.1s"
|
|
||||||
assert seconds_to_string(1.120) == "1.12s"
|
|
||||||
assert seconds_to_string(1.123) == "1.123s"
|
|
||||||
assert seconds_to_string(1.100000, show_microseconds=True) == "1.1s"
|
|
||||||
assert seconds_to_string(1.123000, show_microseconds=True) == "1.123s"
|
|
||||||
|
|
||||||
def test_invalid_input_types(self):
|
|
||||||
"""Test handling of invalid input types"""
|
|
||||||
# String inputs should be returned as-is
|
|
||||||
assert seconds_to_string("invalid") == "invalid"
|
|
||||||
assert seconds_to_string("not a number") == "not a number"
|
|
||||||
assert seconds_to_string("") == ""
|
|
||||||
|
|
||||||
def test_edge_cases_boundary_values(self):
|
|
||||||
"""Test edge cases at unit boundaries"""
|
|
||||||
# Exactly 1 minute - 1 second
|
|
||||||
assert seconds_to_string(59) == "59s"
|
|
||||||
assert seconds_to_string(59.999) == "59.999s"
|
|
||||||
|
|
||||||
# Exactly 1 hour - 1 second
|
|
||||||
assert seconds_to_string(3599) == "59m 59s"
|
|
||||||
assert seconds_to_string(3599.999) == "59m 59.999s"
|
|
||||||
|
|
||||||
# Exactly 1 day - 1 second
|
|
||||||
assert seconds_to_string(86399) == "23h 59m 59s"
|
|
||||||
assert seconds_to_string(86399.999) == "23h 59m 59.999s"
|
|
||||||
|
|
||||||
def test_very_small_fractional_seconds(self):
|
|
||||||
"""Test very small fractional values"""
|
|
||||||
assert seconds_to_string(0.001) == "0.001s"
|
|
||||||
assert seconds_to_string(0.0001) == "0s" # Below default precision
|
|
||||||
assert seconds_to_string(0.000001, show_microseconds=True) == "0.000001s"
|
|
||||||
assert seconds_to_string(0.0000001, show_microseconds=True) == "0s" # Below microsecond precision
|
|
||||||
|
|
||||||
def test_precision_consistency(self):
|
|
||||||
"""Test that precision is consistent across different scenarios"""
|
|
||||||
# With other units present
|
|
||||||
assert seconds_to_string(61.123456) == "1m 1.123s"
|
|
||||||
assert seconds_to_string(61.123456, show_microseconds=True) == "1m 1.123456s"
|
|
||||||
|
|
||||||
# Large values with fractional seconds
|
|
||||||
large_val = 90061.123456 # 1d 1h 1m 1.123456s
|
|
||||||
assert seconds_to_string(large_val) == "1d 1h 1m 1.123s"
|
|
||||||
assert seconds_to_string(large_val, show_microseconds=True) == "1d 1h 1m 1.123456s"
|
|
||||||
|
|
||||||
def test_string_numeric_inputs(self):
|
|
||||||
"""Test string inputs that represent numbers"""
|
|
||||||
# String inputs should be returned as-is, even if they look like numbers
|
|
||||||
assert seconds_to_string("60") == "60"
|
|
||||||
assert seconds_to_string("1.5") == "1.5"
|
|
||||||
assert seconds_to_string("0") == "0"
|
|
||||||
assert seconds_to_string("-60") == "-60"
|
|
||||||
|
|
||||||
# __END__
|
|
||||||
Reference in New Issue
Block a user