Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed22105ec8 | ||
|
|
7c5af588c7 | ||
|
|
2690a285d9 | ||
|
|
bb60a570d0 | ||
|
|
ca0ab2d7d1 | ||
|
|
38bae7fb46 | ||
|
|
14466c3ff8 | ||
|
|
fe824f9fb4 | ||
|
|
ef5981b473 | ||
|
|
7d1ee70cf6 | ||
|
|
7c72d99619 | ||
|
|
b32887a6d8 | ||
|
|
37a197e7f1 | ||
|
|
74cb3d2c54 | ||
|
|
d19abcabc7 |
@@ -1,7 +1,7 @@
|
||||
# MARK: Project info
|
||||
[project]
|
||||
name = "corelibs"
|
||||
version = "0.22.5"
|
||||
version = "0.24.4"
|
||||
description = "Collection of utils for Python scripts"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.13"
|
||||
|
||||
@@ -11,6 +11,7 @@ from types import TracebackType
|
||||
# _typeshed.OptExcInfo
|
||||
OptExcInfo = Tuple[None, None, None] | Tuple[Type[BaseException], BaseException, TracebackType]
|
||||
|
||||
|
||||
def call_stack(
|
||||
start: int = 0,
|
||||
skip_last: int = -1,
|
||||
|
||||
@@ -6,7 +6,7 @@ import json
|
||||
from typing import Any
|
||||
|
||||
|
||||
def dump_data(data: Any) -> str:
|
||||
def dump_data(data: Any, use_indent: bool = True) -> str:
|
||||
"""
|
||||
dump formated output from dict/list
|
||||
|
||||
@@ -16,6 +16,7 @@ def dump_data(data: Any) -> str:
|
||||
Returns:
|
||||
str: _description_
|
||||
"""
|
||||
return json.dumps(data, indent=4, ensure_ascii=False, default=str)
|
||||
indent = 4 if use_indent else None
|
||||
return json.dumps(data, indent=indent, ensure_ascii=False, default=str)
|
||||
|
||||
# __END__
|
||||
|
||||
@@ -22,6 +22,7 @@ def array_search(
|
||||
"""depreacted, old call order"""
|
||||
return find_in_array_from_list(data, search_params, return_index)
|
||||
|
||||
|
||||
def find_in_array_from_list(
|
||||
data: list[dict[str, Any]],
|
||||
search_params: list[ArraySearchList],
|
||||
|
||||
@@ -3,7 +3,7 @@ json encoder for datetime
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
from json import JSONEncoder
|
||||
from json import JSONEncoder, dumps
|
||||
from datetime import datetime, date
|
||||
|
||||
|
||||
@@ -28,4 +28,17 @@ def default(obj: Any) -> str | None:
|
||||
return obj.isoformat()
|
||||
return None
|
||||
|
||||
|
||||
def json_dumps(data: Any):
|
||||
"""
|
||||
wrapper for json.dumps with sure dump without throwing Exceptions
|
||||
|
||||
Arguments:
|
||||
data {Any} -- _description_
|
||||
|
||||
Returns:
|
||||
_type_ -- _description_
|
||||
"""
|
||||
return dumps(data, ensure_ascii=False, default=str)
|
||||
|
||||
# __END__
|
||||
|
||||
@@ -23,13 +23,17 @@ class TimestampStrings:
|
||||
|
||||
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.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:
|
||||
self.timestamp_now_tz = datetime.now(ZoneInfo(self.time_zone))
|
||||
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")
|
||||
@@ -60,6 +64,11 @@ def convert_to_seconds(time_string: str | int | float) -> int:
|
||||
return int(round(float(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
|
||||
unit_factors: dict[str, int] = {
|
||||
'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)
|
||||
|
||||
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_b=123.5
|
||||
element_c=True
|
||||
elemend_d=AB:CD;EF
|
||||
email=foo@bar.com,other+bar-fee@domain-com.cp,
|
||||
email_not_mandatory=
|
||||
email_bad=gii@bar.com
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
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:
|
||||
@@ -21,10 +23,19 @@ def main() -> None:
|
||||
"1d 12h", # 1 day, 12 hours
|
||||
"3M 2d 4h", # 3 months, 2 days, 4 hours
|
||||
"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
|
||||
# ]
|
||||
# invalid_test_cases = [
|
||||
# invalid tests
|
||||
"5M 6d 2M", # months appears twice
|
||||
"2h 30m 45s 1h", # hours appears twice
|
||||
"1d 2 days", # days appears twice (short and long form)
|
||||
@@ -43,10 +54,33 @@ def main() -> None:
|
||||
for time_string in test_cases:
|
||||
try:
|
||||
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:
|
||||
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__":
|
||||
main()
|
||||
|
||||
@@ -4,10 +4,12 @@
|
||||
Test for double byte format
|
||||
"""
|
||||
|
||||
from zoneinfo import ZoneInfo
|
||||
from corelibs.string_handling.timestamp_strings import TimestampStrings
|
||||
|
||||
|
||||
def main():
|
||||
"""test"""
|
||||
ts = TimestampStrings()
|
||||
print(f"TS: {ts.timestamp_now}")
|
||||
|
||||
@@ -16,6 +18,14 @@ def main():
|
||||
except ValueError as 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__":
|
||||
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 unittest.mock import patch, MagicMock
|
||||
from unittest.mock import patch
|
||||
from zoneinfo import ZoneInfo
|
||||
import pytest
|
||||
|
||||
@@ -119,18 +119,55 @@ class TestTimestampStrings:
|
||||
assert ts2.time_zone == 'Europe/London'
|
||||
assert ts1.time_zone != ts2.time_zone
|
||||
|
||||
@patch('corelibs.string_handling.timestamp_strings.ZoneInfo')
|
||||
def test_zoneinfo_called_correctly(self, mock_zoneinfo: MagicMock):
|
||||
"""Test that ZoneInfo is called with correct timezone"""
|
||||
def test_zoneinfo_called_correctly_with_string(self):
|
||||
"""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.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:
|
||||
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'
|
||||
ts = TimestampStrings(time_zone=custom_tz)
|
||||
assert ts.time_zone == custom_tz
|
||||
# Create a ZoneInfo object
|
||||
custom_tz_obj = ZoneInfo('Europe/Paris')
|
||||
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):
|
||||
"""Test timestamp formatting at midnight"""
|
||||
|
||||
Reference in New Issue
Block a user