Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed22105ec8 | ||
|
|
7c5af588c7 | ||
|
|
2690a285d9 | ||
|
|
bb60a570d0 | ||
|
|
ca0ab2d7d1 | ||
|
|
38bae7fb46 | ||
|
|
14466c3ff8 | ||
|
|
fe824f9fb4 | ||
|
|
ef5981b473 | ||
|
|
7d1ee70cf6 | ||
|
|
7c72d99619 | ||
|
|
b32887a6d8 |
@@ -1,7 +1,7 @@
|
|||||||
# MARK: Project info
|
# MARK: Project info
|
||||||
[project]
|
[project]
|
||||||
name = "corelibs"
|
name = "corelibs"
|
||||||
version = "0.23.0"
|
version = "0.24.4"
|
||||||
description = "Collection of utils for Python scripts"
|
description = "Collection of utils for Python scripts"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.13"
|
requires-python = ">=3.13"
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from types import TracebackType
|
|||||||
# _typeshed.OptExcInfo
|
# _typeshed.OptExcInfo
|
||||||
OptExcInfo = Tuple[None, None, None] | Tuple[Type[BaseException], BaseException, TracebackType]
|
OptExcInfo = Tuple[None, None, None] | Tuple[Type[BaseException], BaseException, TracebackType]
|
||||||
|
|
||||||
|
|
||||||
def call_stack(
|
def call_stack(
|
||||||
start: int = 0,
|
start: int = 0,
|
||||||
skip_last: int = -1,
|
skip_last: int = -1,
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ def array_search(
|
|||||||
"""depreacted, old call order"""
|
"""depreacted, old call order"""
|
||||||
return find_in_array_from_list(data, search_params, return_index)
|
return find_in_array_from_list(data, search_params, return_index)
|
||||||
|
|
||||||
|
|
||||||
def find_in_array_from_list(
|
def find_in_array_from_list(
|
||||||
data: list[dict[str, Any]],
|
data: list[dict[str, Any]],
|
||||||
search_params: list[ArraySearchList],
|
search_params: list[ArraySearchList],
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ def default(obj: Any) -> str | None:
|
|||||||
return obj.isoformat()
|
return obj.isoformat()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def json_dumps(data: Any):
|
def json_dumps(data: Any):
|
||||||
"""
|
"""
|
||||||
wrapper for json.dumps with sure dump without throwing Exceptions
|
wrapper for json.dumps with sure dump without throwing Exceptions
|
||||||
|
|||||||
@@ -23,13 +23,17 @@ class TimestampStrings:
|
|||||||
|
|
||||||
TIME_ZONE: str = 'Asia/Tokyo'
|
TIME_ZONE: str = 'Asia/Tokyo'
|
||||||
|
|
||||||
def __init__(self, time_zone: str | None = None):
|
def __init__(self, time_zone: str | ZoneInfo | None = None):
|
||||||
self.timestamp_now = datetime.now()
|
self.timestamp_now = datetime.now()
|
||||||
self.time_zone = time_zone if time_zone is not None else self.TIME_ZONE
|
# 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:
|
try:
|
||||||
self.timestamp_now_tz = datetime.now(ZoneInfo(self.time_zone))
|
self.time_zone_zi = ZoneInfo(self.time_zone)
|
||||||
except ZoneInfoNotFoundError as e:
|
except ZoneInfoNotFoundError as e:
|
||||||
raise ValueError(f'Zone could not be loaded [{self.time_zone}]: {e}') from 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.today = self.timestamp_now.strftime('%Y-%m-%d')
|
||||||
self.timestamp = self.timestamp_now.strftime("%Y-%m-%d %H:%M:%S")
|
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_tz = self.timestamp_now_tz.strftime("%Y-%m-%d %H:%M:%S %Z")
|
||||||
@@ -60,6 +64,11 @@ def convert_to_seconds(time_string: str | int | float) -> int:
|
|||||||
return int(round(float(time_string)))
|
return int(round(float(time_string)))
|
||||||
time_string = str(time_string)
|
time_string = str(time_string)
|
||||||
|
|
||||||
|
# Check if the time string is negative
|
||||||
|
negative = time_string.startswith('-')
|
||||||
|
if negative:
|
||||||
|
time_string = time_string[1:] # Remove the negative sign for processing
|
||||||
|
|
||||||
# Define time unit conversion factors
|
# Define time unit conversion factors
|
||||||
unit_factors: dict[str, int] = {
|
unit_factors: dict[str, int] = {
|
||||||
'Y': 31536000, # 365 days * 86400 seconds/day
|
'Y': 31536000, # 365 days * 86400 seconds/day
|
||||||
@@ -109,4 +118,58 @@ def convert_to_seconds(time_string: str | int | float) -> int:
|
|||||||
|
|
||||||
seen_units.append(unit)
|
seen_units.append(unit)
|
||||||
|
|
||||||
return total_seconds
|
return -total_seconds if negative else total_seconds
|
||||||
|
|
||||||
|
|
||||||
|
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")
|
||||||
|
Supports negative values with "-" prefix
|
||||||
|
|
||||||
|
Args:
|
||||||
|
seconds (float): Time in seconds (can be negative)
|
||||||
|
show_microseconds (bool): Whether to show microseconds precision
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Compact human readable time format
|
||||||
|
"""
|
||||||
|
# if not int or float, return as is
|
||||||
|
if not isinstance(seconds, (int, float)):
|
||||||
|
return seconds
|
||||||
|
# Handle negative values
|
||||||
|
negative = seconds < 0
|
||||||
|
seconds = abs(seconds)
|
||||||
|
|
||||||
|
whole_seconds = int(seconds)
|
||||||
|
fractional = seconds - whole_seconds
|
||||||
|
|
||||||
|
days = whole_seconds // 86400
|
||||||
|
hours = (whole_seconds % 86400) // 3600
|
||||||
|
minutes = (whole_seconds % 3600) // 60
|
||||||
|
secs = whole_seconds % 60
|
||||||
|
|
||||||
|
parts: list[str] = []
|
||||||
|
if days > 0:
|
||||||
|
parts.append(f"{days}d")
|
||||||
|
if hours > 0:
|
||||||
|
parts.append(f"{hours}h")
|
||||||
|
if minutes > 0:
|
||||||
|
parts.append(f"{minutes}m")
|
||||||
|
|
||||||
|
# Handle seconds with fractional part
|
||||||
|
if fractional > 0:
|
||||||
|
if show_microseconds:
|
||||||
|
total_seconds = secs + fractional
|
||||||
|
formatted = f"{total_seconds:.6f}".rstrip('0').rstrip('.')
|
||||||
|
parts.append(f"{formatted}s")
|
||||||
|
else:
|
||||||
|
total_seconds = secs + fractional
|
||||||
|
formatted = f"{total_seconds:.3f}".rstrip('0').rstrip('.')
|
||||||
|
parts.append(f"{formatted}s")
|
||||||
|
elif secs > 0 or not parts:
|
||||||
|
parts.append(f"{secs}s")
|
||||||
|
|
||||||
|
result = " ".join(parts)
|
||||||
|
return f"-{result}" if negative else result
|
||||||
|
|
||||||
|
# __END__
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ match_source_list=foo,bar
|
|||||||
element_a=Static energy
|
element_a=Static energy
|
||||||
element_b=123.5
|
element_b=123.5
|
||||||
element_c=True
|
element_c=True
|
||||||
|
elemend_d=AB:CD;EF
|
||||||
email=foo@bar.com,other+bar-fee@domain-com.cp,
|
email=foo@bar.com,other+bar-fee@domain-com.cp,
|
||||||
email_not_mandatory=
|
email_not_mandatory=
|
||||||
email_bad=gii@bar.com
|
email_bad=gii@bar.com
|
||||||
|
|||||||
@@ -4,7 +4,9 @@
|
|||||||
timestamp string checks
|
timestamp string checks
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from corelibs.string_handling.timestamp_strings import convert_to_seconds, TimeParseError, TimeUnitError
|
from corelibs.string_handling.timestamp_strings import (
|
||||||
|
seconds_to_string, convert_to_seconds, TimeParseError, TimeUnitError
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
@@ -21,10 +23,19 @@ def main() -> None:
|
|||||||
"1d 12h", # 1 day, 12 hours
|
"1d 12h", # 1 day, 12 hours
|
||||||
"3M 2d 4h", # 3 months, 2 days, 4 hours
|
"3M 2d 4h", # 3 months, 2 days, 4 hours
|
||||||
"45s", # 45 seconds
|
"45s", # 45 seconds
|
||||||
"1 year 2 months", # 1 year, 2 months
|
"-45s", # -45 seconds
|
||||||
|
"-1h", # -1 hour
|
||||||
|
"-30m", # -30 minutes
|
||||||
|
"-2h 30m 45s", # -2 hours, 30 minutes, 45 seconds
|
||||||
|
"-1d 12h", # -1 day, 12 hours
|
||||||
|
"-3M 2d 4h", # -3 months, 2 days, 4 hours
|
||||||
|
"-1Y 2M 3d", # -1 year, 2 months, 3 days
|
||||||
|
"-2 hours 15 minutes", # -2 hours, 15 minutes
|
||||||
|
"-1 year 2 months", # -1 year, 2 months
|
||||||
|
"-2Y 6M 15d 8h 30m 45s", # Complex negative example
|
||||||
|
"1 year 2 months", # 1 year, 2 months
|
||||||
"2Y 6M 15d 8h 30m 45s", # Complex example
|
"2Y 6M 15d 8h 30m 45s", # Complex example
|
||||||
# ]
|
# invalid tests
|
||||||
# invalid_test_cases = [
|
|
||||||
"5M 6d 2M", # months appears twice
|
"5M 6d 2M", # months appears twice
|
||||||
"2h 30m 45s 1h", # hours appears twice
|
"2h 30m 45s 1h", # hours appears twice
|
||||||
"1d 2 days", # days appears twice (short and long form)
|
"1d 2 days", # days appears twice (short and long form)
|
||||||
@@ -43,10 +54,33 @@ def main() -> None:
|
|||||||
for time_string in test_cases:
|
for time_string in test_cases:
|
||||||
try:
|
try:
|
||||||
result = convert_to_seconds(time_string)
|
result = convert_to_seconds(time_string)
|
||||||
print(f"{time_string} => {result}")
|
print(f"Human readable to seconds: {time_string} => {result}")
|
||||||
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}")
|
||||||
|
|
||||||
|
test_values = [
|
||||||
|
'as is string',
|
||||||
|
-172800.001234, # -2 days, -0.001234 seconds
|
||||||
|
-90061.789, # -1 day, -1 hour, -1 minute, -1.789 seconds
|
||||||
|
-3661.456, # -1 hour, -1 minute, -1.456 seconds
|
||||||
|
-65.123, # -1 minute, -5.123 seconds
|
||||||
|
-1.5, # -1.5 seconds
|
||||||
|
-0.001, # -1 millisecond
|
||||||
|
-0.000001, # -1 microsecond
|
||||||
|
0, # 0 seconds
|
||||||
|
0.000001, # 1 microsecond
|
||||||
|
0.001, # 1 millisecond
|
||||||
|
1.5, # 1.5 seconds
|
||||||
|
65.123, # 1 minute, 5.123 seconds
|
||||||
|
3661.456, # 1 hour, 1 minute, 1.456 seconds
|
||||||
|
90061.789, # 1 day, 1 hour, 1 minute, 1.789 seconds
|
||||||
|
172800.001234 # 2 days, 0.001234 seconds
|
||||||
|
]
|
||||||
|
|
||||||
|
for time_value in test_values:
|
||||||
|
result = seconds_to_string(time_value, show_microseconds=True)
|
||||||
|
print(f"Seconds to human readable: {time_value} => {result}")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -4,10 +4,12 @@
|
|||||||
Test for double byte format
|
Test for double byte format
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from zoneinfo import ZoneInfo
|
||||||
from corelibs.string_handling.timestamp_strings import TimestampStrings
|
from corelibs.string_handling.timestamp_strings import TimestampStrings
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
"""test"""
|
||||||
ts = TimestampStrings()
|
ts = TimestampStrings()
|
||||||
print(f"TS: {ts.timestamp_now}")
|
print(f"TS: {ts.timestamp_now}")
|
||||||
|
|
||||||
@@ -16,6 +18,14 @@ def main():
|
|||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
print(f"Value error: {e}")
|
print(f"Value error: {e}")
|
||||||
|
|
||||||
|
ts = TimestampStrings("Europe/Vienna")
|
||||||
|
print(f"TZ: {ts.time_zone} -> TS: {ts.timestamp_now_tz}")
|
||||||
|
ts = TimestampStrings(ZoneInfo("Europe/Vienna"))
|
||||||
|
print(f"TZ: {ts.time_zone} -> TS: {ts.timestamp_now_tz}")
|
||||||
|
custom_tz = 'Europe/Paris'
|
||||||
|
ts = TimestampStrings(time_zone=custom_tz)
|
||||||
|
print(f"TZ: {ts.time_zone} -> TS: {ts.timestamp_now_tz}")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
205
tests/unit/string_handling/test_convert_to_seconds.py
Normal file
205
tests/unit/string_handling/test_convert_to_seconds.py
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
"""
|
||||||
|
Unit tests for convert_to_seconds function from timestamp_strings module.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from corelibs.string_handling.timestamp_strings import convert_to_seconds, TimeParseError, TimeUnitError
|
||||||
|
|
||||||
|
|
||||||
|
class TestConvertToSeconds:
|
||||||
|
"""Test class for convert_to_seconds function."""
|
||||||
|
|
||||||
|
def test_numeric_input_int(self):
|
||||||
|
"""Test with integer input."""
|
||||||
|
assert convert_to_seconds(42) == 42
|
||||||
|
assert convert_to_seconds(0) == 0
|
||||||
|
assert convert_to_seconds(-5) == -5
|
||||||
|
|
||||||
|
def test_numeric_input_float(self):
|
||||||
|
"""Test with float input."""
|
||||||
|
assert convert_to_seconds(42.7) == 43 # rounds to 43
|
||||||
|
assert convert_to_seconds(42.3) == 42 # rounds to 42
|
||||||
|
assert convert_to_seconds(42.5) == 42 # rounds to 42 (banker's rounding)
|
||||||
|
assert convert_to_seconds(0.0) == 0
|
||||||
|
assert convert_to_seconds(-5.7) == -6
|
||||||
|
|
||||||
|
def test_numeric_string_input(self):
|
||||||
|
"""Test with numeric string input."""
|
||||||
|
assert convert_to_seconds("42") == 42
|
||||||
|
assert convert_to_seconds("42.7") == 43
|
||||||
|
assert convert_to_seconds("42.3") == 42
|
||||||
|
assert convert_to_seconds("0") == 0
|
||||||
|
assert convert_to_seconds("-5.7") == -6
|
||||||
|
|
||||||
|
def test_single_unit_seconds(self):
|
||||||
|
"""Test with seconds unit."""
|
||||||
|
assert convert_to_seconds("30s") == 30
|
||||||
|
assert convert_to_seconds("1s") == 1
|
||||||
|
assert convert_to_seconds("0s") == 0
|
||||||
|
|
||||||
|
def test_single_unit_minutes(self):
|
||||||
|
"""Test with minutes unit."""
|
||||||
|
assert convert_to_seconds("5m") == 300 # 5 * 60
|
||||||
|
assert convert_to_seconds("1m") == 60
|
||||||
|
assert convert_to_seconds("0m") == 0
|
||||||
|
|
||||||
|
def test_single_unit_hours(self):
|
||||||
|
"""Test with hours unit."""
|
||||||
|
assert convert_to_seconds("2h") == 7200 # 2 * 3600
|
||||||
|
assert convert_to_seconds("1h") == 3600
|
||||||
|
assert convert_to_seconds("0h") == 0
|
||||||
|
|
||||||
|
def test_single_unit_days(self):
|
||||||
|
"""Test with days unit."""
|
||||||
|
assert convert_to_seconds("1d") == 86400 # 1 * 86400
|
||||||
|
assert convert_to_seconds("2d") == 172800 # 2 * 86400
|
||||||
|
assert convert_to_seconds("0d") == 0
|
||||||
|
|
||||||
|
def test_single_unit_months(self):
|
||||||
|
"""Test with months unit (30 days * 12 = 1 year)."""
|
||||||
|
# Note: The code has M: 2592000 * 12 which is 1 year, not 1 month
|
||||||
|
# This seems like a bug in the original code, but testing what it actually does
|
||||||
|
assert convert_to_seconds("1M") == 31104000 # 2592000 * 12
|
||||||
|
assert convert_to_seconds("2M") == 62208000 # 2 * 2592000 * 12
|
||||||
|
|
||||||
|
def test_single_unit_years(self):
|
||||||
|
"""Test with years unit."""
|
||||||
|
assert convert_to_seconds("1Y") == 31536000 # 365 * 86400
|
||||||
|
assert convert_to_seconds("2Y") == 63072000 # 2 * 365 * 86400
|
||||||
|
|
||||||
|
def test_long_unit_names(self):
|
||||||
|
"""Test with long unit names."""
|
||||||
|
assert convert_to_seconds("1year") == 31536000
|
||||||
|
assert convert_to_seconds("2years") == 63072000
|
||||||
|
assert convert_to_seconds("1month") == 31104000
|
||||||
|
assert convert_to_seconds("2months") == 62208000
|
||||||
|
assert convert_to_seconds("1day") == 86400
|
||||||
|
assert convert_to_seconds("2days") == 172800
|
||||||
|
assert convert_to_seconds("1hour") == 3600
|
||||||
|
assert convert_to_seconds("2hours") == 7200
|
||||||
|
assert convert_to_seconds("1minute") == 60
|
||||||
|
assert convert_to_seconds("2minutes") == 120
|
||||||
|
assert convert_to_seconds("30min") == 1800
|
||||||
|
assert convert_to_seconds("1second") == 1
|
||||||
|
assert convert_to_seconds("2seconds") == 2
|
||||||
|
assert convert_to_seconds("30sec") == 30
|
||||||
|
|
||||||
|
def test_multiple_units(self):
|
||||||
|
"""Test with multiple units combined."""
|
||||||
|
assert convert_to_seconds("1h30m") == 5400 # 3600 + 1800
|
||||||
|
assert convert_to_seconds("1d2h") == 93600 # 86400 + 7200
|
||||||
|
assert convert_to_seconds("1h30m45s") == 5445 # 3600 + 1800 + 45
|
||||||
|
assert convert_to_seconds("2d3h4m5s") == 183845 # 172800 + 10800 + 240 + 5
|
||||||
|
|
||||||
|
def test_multiple_units_with_spaces(self):
|
||||||
|
"""Test with multiple units and spaces."""
|
||||||
|
assert convert_to_seconds("1h 30m") == 5400
|
||||||
|
assert convert_to_seconds("1d 2h") == 93600
|
||||||
|
assert convert_to_seconds("1h 30m 45s") == 5445
|
||||||
|
assert convert_to_seconds("2d 3h 4m 5s") == 183845
|
||||||
|
|
||||||
|
def test_mixed_unit_formats(self):
|
||||||
|
"""Test with mixed short and long unit names."""
|
||||||
|
assert convert_to_seconds("1hour 30min") == 5400
|
||||||
|
assert convert_to_seconds("1day 2hours") == 93600
|
||||||
|
assert convert_to_seconds("1h 30minutes 45sec") == 5445
|
||||||
|
|
||||||
|
def test_negative_values(self):
|
||||||
|
"""Test with negative time strings."""
|
||||||
|
assert convert_to_seconds("-30s") == -30
|
||||||
|
assert convert_to_seconds("-1h") == -3600
|
||||||
|
assert convert_to_seconds("-1h30m") == -5400
|
||||||
|
assert convert_to_seconds("-2d3h4m5s") == -183845
|
||||||
|
|
||||||
|
def test_case_insensitive_long_names(self):
|
||||||
|
"""Test that long unit names are case insensitive."""
|
||||||
|
assert convert_to_seconds("1Hour") == 3600
|
||||||
|
assert convert_to_seconds("1MINUTE") == 60
|
||||||
|
assert convert_to_seconds("1Day") == 86400
|
||||||
|
assert convert_to_seconds("2YEARS") == 63072000
|
||||||
|
|
||||||
|
def test_duplicate_units_error(self):
|
||||||
|
"""Test that duplicate units raise TimeParseError."""
|
||||||
|
with pytest.raises(TimeParseError, match="Unit 'h' appears more than once"):
|
||||||
|
convert_to_seconds("1h2h")
|
||||||
|
|
||||||
|
with pytest.raises(TimeParseError, match="Unit 's' appears more than once"):
|
||||||
|
convert_to_seconds("30s45s")
|
||||||
|
|
||||||
|
with pytest.raises(TimeParseError, match="Unit 'm' appears more than once"):
|
||||||
|
convert_to_seconds("1m30m")
|
||||||
|
|
||||||
|
def test_invalid_units_error(self):
|
||||||
|
"""Test that invalid units raise TimeUnitError."""
|
||||||
|
with pytest.raises(TimeUnitError, match="Unit 'x' is not a valid unit name"):
|
||||||
|
convert_to_seconds("30x")
|
||||||
|
|
||||||
|
with pytest.raises(TimeUnitError, match="Unit 'invalid' is not a valid unit name"):
|
||||||
|
convert_to_seconds("1invalid")
|
||||||
|
|
||||||
|
with pytest.raises(TimeUnitError, match="Unit 'z' is not a valid unit name"):
|
||||||
|
convert_to_seconds("1h30z")
|
||||||
|
|
||||||
|
def test_empty_string(self):
|
||||||
|
"""Test with empty string."""
|
||||||
|
assert convert_to_seconds("") == 0
|
||||||
|
|
||||||
|
def test_no_matches(self):
|
||||||
|
"""Test with string that has no time units."""
|
||||||
|
assert convert_to_seconds("hello") == 0
|
||||||
|
assert convert_to_seconds("no time here") == 0
|
||||||
|
|
||||||
|
def test_zero_values(self):
|
||||||
|
"""Test with zero values for different units."""
|
||||||
|
assert convert_to_seconds("0s") == 0
|
||||||
|
assert convert_to_seconds("0m") == 0
|
||||||
|
assert convert_to_seconds("0h") == 0
|
||||||
|
assert convert_to_seconds("0d") == 0
|
||||||
|
assert convert_to_seconds("0h0m0s") == 0
|
||||||
|
|
||||||
|
def test_large_values(self):
|
||||||
|
"""Test with large time values."""
|
||||||
|
assert convert_to_seconds("999d") == 86313600 # 999 * 86400
|
||||||
|
assert convert_to_seconds("100Y") == 3153600000 # 100 * 31536000
|
||||||
|
|
||||||
|
def test_order_independence(self):
|
||||||
|
"""Test that order of units doesn't matter."""
|
||||||
|
assert convert_to_seconds("30m1h") == 5400 # same as 1h30m
|
||||||
|
assert convert_to_seconds("45s30m1h") == 5445 # same as 1h30m45s
|
||||||
|
assert convert_to_seconds("5s4m3h2d") == 183845 # same as 2d3h4m5s
|
||||||
|
|
||||||
|
def test_whitespace_handling(self):
|
||||||
|
"""Test various whitespace scenarios."""
|
||||||
|
assert convert_to_seconds("1 h") == 3600
|
||||||
|
assert convert_to_seconds("1h 30m") == 5400
|
||||||
|
assert convert_to_seconds(" 1h30m ") == 5400
|
||||||
|
assert convert_to_seconds("1h\t30m") == 5400
|
||||||
|
|
||||||
|
def test_mixed_case_short_units(self):
|
||||||
|
"""Test that short units work with different cases."""
|
||||||
|
# Note: The regex only matches [a-zA-Z]+ so case matters for the lookup
|
||||||
|
with pytest.raises(TimeUnitError, match="Unit 'H' is not a valid unit name"):
|
||||||
|
convert_to_seconds("1H") # 'H' is not in unit_factors, raises error
|
||||||
|
assert convert_to_seconds("1h") == 3600 # lowercase works
|
||||||
|
|
||||||
|
def test_boundary_conditions(self):
|
||||||
|
"""Test boundary conditions and edge cases."""
|
||||||
|
# Test with leading zeros
|
||||||
|
assert convert_to_seconds("01h") == 3600
|
||||||
|
assert convert_to_seconds("001m") == 60
|
||||||
|
|
||||||
|
# Test very small values
|
||||||
|
assert convert_to_seconds("1s") == 1
|
||||||
|
|
||||||
|
def test_negative_with_multiple_units(self):
|
||||||
|
"""Test negative values with multiple units."""
|
||||||
|
assert convert_to_seconds("-1h30m45s") == -5445
|
||||||
|
assert convert_to_seconds("-2d3h") == -183600
|
||||||
|
|
||||||
|
def test_duplicate_with_long_names(self):
|
||||||
|
"""Test duplicate detection with long unit names."""
|
||||||
|
with pytest.raises(TimeParseError, match="Unit 'h' appears more than once"):
|
||||||
|
convert_to_seconds("1hour2h") # both resolve to 'h'
|
||||||
|
|
||||||
|
with pytest.raises(TimeParseError, match="Unit 's' appears more than once"):
|
||||||
|
convert_to_seconds("1second30sec") # both resolve to 's'
|
||||||
186
tests/unit/string_handling/test_seconds_to_string.py
Normal file
186
tests/unit/string_handling/test_seconds_to_string.py
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
"""
|
||||||
|
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__
|
||||||
@@ -3,7 +3,7 @@ PyTest: string_handling/timestamp_strings
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from unittest.mock import patch, MagicMock
|
from unittest.mock import patch
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@@ -119,18 +119,55 @@ class TestTimestampStrings:
|
|||||||
assert ts2.time_zone == 'Europe/London'
|
assert ts2.time_zone == 'Europe/London'
|
||||||
assert ts1.time_zone != ts2.time_zone
|
assert ts1.time_zone != ts2.time_zone
|
||||||
|
|
||||||
@patch('corelibs.string_handling.timestamp_strings.ZoneInfo')
|
def test_zoneinfo_called_correctly_with_string(self):
|
||||||
def test_zoneinfo_called_correctly(self, mock_zoneinfo: MagicMock):
|
"""Test that ZoneInfo is called with correct timezone when passing string"""
|
||||||
"""Test that ZoneInfo is called with correct timezone"""
|
with patch('corelibs.string_handling.timestamp_strings.ZoneInfo') as mock_zoneinfo:
|
||||||
|
with patch('corelibs.string_handling.timestamp_strings.datetime') as mock_datetime:
|
||||||
|
mock_now = datetime(2023, 12, 25, 15, 30, 45)
|
||||||
|
mock_datetime.now.return_value = mock_now
|
||||||
|
|
||||||
|
custom_tz = 'Europe/Paris'
|
||||||
|
ts = TimestampStrings(time_zone=custom_tz)
|
||||||
|
assert ts.time_zone == custom_tz
|
||||||
|
|
||||||
|
mock_zoneinfo.assert_called_with(custom_tz)
|
||||||
|
|
||||||
|
def test_zoneinfo_object_parameter(self):
|
||||||
|
"""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.string_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_now_tz = datetime(2023, 12, 25, 15, 30, 45, tzinfo=ZoneInfo('Europe/Paris'))
|
||||||
|
mock_datetime.now.side_effect = [mock_now, mock_now_tz]
|
||||||
|
|
||||||
custom_tz = 'Europe/Paris'
|
# Create a ZoneInfo object
|
||||||
ts = TimestampStrings(time_zone=custom_tz)
|
custom_tz_obj = ZoneInfo('Europe/Paris')
|
||||||
assert ts.time_zone == custom_tz
|
ts = TimestampStrings(time_zone=custom_tz_obj)
|
||||||
|
|
||||||
mock_zoneinfo.assert_called_with(custom_tz)
|
# The time_zone should be the ZoneInfo object itself
|
||||||
|
assert ts.time_zone_zi is custom_tz_obj
|
||||||
|
assert isinstance(ts.time_zone_zi, ZoneInfo)
|
||||||
|
|
||||||
|
def test_zoneinfo_object_vs_string_equivalence(self):
|
||||||
|
"""Test that ZoneInfo object and string produce equivalent results"""
|
||||||
|
with patch('corelibs.string_handling.timestamp_strings.datetime') as mock_datetime:
|
||||||
|
mock_now = datetime(2023, 12, 25, 15, 30, 45)
|
||||||
|
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]
|
||||||
|
|
||||||
|
# Test with string
|
||||||
|
ts_string = TimestampStrings(time_zone='Europe/Paris')
|
||||||
|
|
||||||
|
# Test with ZoneInfo object
|
||||||
|
ts_zoneinfo = TimestampStrings(time_zone=ZoneInfo('Europe/Paris'))
|
||||||
|
|
||||||
|
# Both should produce the same timestamp formats (though time_zone attributes will differ)
|
||||||
|
assert ts_string.today == ts_zoneinfo.today
|
||||||
|
assert ts_string.timestamp == ts_zoneinfo.timestamp
|
||||||
|
assert ts_string.timestamp_file == ts_zoneinfo.timestamp_file
|
||||||
|
|
||||||
|
# The time_zone attributes will be different types but represent the same timezone
|
||||||
|
assert str(ts_string.time_zone) == 'Europe/Paris'
|
||||||
|
assert isinstance(ts_zoneinfo.time_zone_zi, ZoneInfo)
|
||||||
|
|
||||||
def test_edge_case_midnight(self):
|
def test_edge_case_midnight(self):
|
||||||
"""Test timestamp formatting at midnight"""
|
"""Test timestamp formatting at midnight"""
|
||||||
|
|||||||
Reference in New Issue
Block a user