Compare commits

...

3 Commits

Author SHA1 Message Date
Clemens Schwaighofer
7d1ee70cf6 v0.24.0: Add timestamp seconds to human readable 2025-09-19 10:25:44 +09:00
Clemens Schwaighofer
7c72d99619 add pytests for seconds to human readable convert 2025-09-19 10:17:36 +09:00
Clemens Schwaighofer
b32887a6d8 Add time in seconds convert to human readable format 2025-09-19 09:57:51 +09:00
5 changed files with 271 additions and 7 deletions

View File

@@ -1,7 +1,7 @@
# MARK: Project info
[project]
name = "corelibs"
version = "0.23.0"
version = "0.24.0"
description = "Collection of utils for Python scripts"
readme = "README.md"
requires-python = ">=3.13"

View File

@@ -110,3 +110,57 @@ def convert_to_seconds(time_string: str | int | float) -> int:
seen_units.append(unit)
return 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__

View File

@@ -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,9 @@ 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
"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 +44,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()

View 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__

2
uv.lock generated
View File

@@ -53,7 +53,7 @@ wheels = [
[[package]]
name = "corelibs"
version = "0.22.6"
version = "0.23.0"
source = { editable = "." }
dependencies = [
{ name = "jmespath" },