Compare commits

...

5 Commits

Author SHA1 Message Date
Clemens Schwaighofer
ca0ab2d7d1 v0.24.2: TimestampString allows ZoneInfo object as zone name 2025-09-25 15:16:19 +09:00
Clemens Schwaighofer
38bae7fb46 TimestampStrings allows ZoneInfo object as time_zone parameter
So we can use pre-parsed data

Some tests for parsing settings, timestamp output
2025-09-25 15:14:40 +09:00
Clemens Schwaighofer
14466c3ff8 v0.24.1: allow negative timestamp convert to seconds, add pytests for this function 2025-09-24 15:27:15 +09:00
Clemens Schwaighofer
fe824f9fb4 Merge branch 'development' 2025-09-24 15:26:22 +09:00
Clemens Schwaighofer
ef5981b473 convert_to_seconds allow negative time strings and add pytests 2025-09-24 15:25:53 +09:00
10 changed files with 238 additions and 5 deletions

View File

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

View File

@@ -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,

View File

@@ -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],

View File

@@ -28,6 +28,7 @@ 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

View File

@@ -23,11 +23,13 @@ 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
try:
self.timestamp_now_tz = datetime.now(ZoneInfo(self.time_zone))
self.timestamp_now_tz = datetime.now(
ZoneInfo(self.time_zone) if not isinstance(self.time_zone, ZoneInfo) else self.time_zone
)
except ZoneInfoNotFoundError as e:
raise ValueError(f'Zone could not be loaded [{self.time_zone}]: {e}') from e
self.today = self.timestamp_now.strftime('%Y-%m-%d')
@@ -60,6 +62,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,7 +116,7 @@ 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:

View File

@@ -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

View File

@@ -23,6 +23,16 @@ def main() -> None:
"1d 12h", # 1 day, 12 hours
"3M 2d 4h", # 3 months, 2 days, 4 hours
"45s", # 45 seconds
"-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 tests

View File

@@ -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,11 @@ 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}")
if __name__ == "__main__":
main()

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

2
uv.lock generated
View File

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