diff --git a/lib/timer.py b/lib/timer.py index 1f220052e..62f4080f1 100644 --- a/lib/timer.py +++ b/lib/timer.py @@ -1,55 +1,56 @@ """A timer for use in lichess-bot.""" -import time -import datetime + +from datetime import datetime, timedelta +from time import perf_counter from typing import Optional -def msec(time_in_msec: float) -> datetime.timedelta: +def msec(time_in_msec: float) -> timedelta: """Create a timedelta duration in milliseconds.""" - return datetime.timedelta(milliseconds=time_in_msec) + return timedelta(milliseconds=time_in_msec) -def to_msec(duration: datetime.timedelta) -> float: +def to_msec(duration: timedelta) -> float: """Return a bare number representing the length of the duration in milliseconds.""" return duration / msec(1) -def msec_str(duration: datetime.timedelta) -> str: +def msec_str(duration: timedelta) -> str: """Return a string with the duration value in whole number milliseconds.""" return str(round(to_msec(duration))) -def seconds(time_in_sec: float) -> datetime.timedelta: +def seconds(time_in_sec: float) -> timedelta: """Create a timedelta duration in seconds.""" - return datetime.timedelta(seconds=time_in_sec) + return timedelta(seconds=time_in_sec) -def to_seconds(duration: datetime.timedelta) -> float: +def to_seconds(duration: timedelta) -> float: """Return a bare number representing the length of the duration in seconds.""" return duration.total_seconds() -def sec_str(duration: datetime.timedelta) -> str: +def sec_str(duration: timedelta) -> str: """Return a string with the duration value in whole number seconds.""" return str(round(to_seconds(duration))) -def minutes(time_in_minutes: float) -> datetime.timedelta: +def minutes(time_in_minutes: float) -> timedelta: """Create a timedelta duration in minutes.""" - return datetime.timedelta(minutes=time_in_minutes) + return timedelta(minutes=time_in_minutes) -def hours(time_in_hours: float) -> datetime.timedelta: +def hours(time_in_hours: float) -> timedelta: """Create a timedelta duration in hours.""" - return datetime.timedelta(hours=time_in_hours) + return timedelta(hours=time_in_hours) -def days(time_in_days: float) -> datetime.timedelta: +def days(time_in_days: float) -> timedelta: """Create a timedelta duration in days.""" - return datetime.timedelta(days=time_in_days) + return timedelta(days=time_in_days) -def years(time_in_years: float) -> datetime.timedelta: +def years(time_in_years: float) -> timedelta: """Create a timedelta duration in median years--i.e., 365 days.""" return days(365) * time_in_years @@ -68,8 +69,10 @@ class Timer: the timer was created or since it was last reset. """ - def __init__(self, duration: datetime.timedelta = seconds(0), - backdated_timestamp: Optional[datetime.datetime] = None) -> None: + __slots__ = ["duration", "starting_time"] + + def __init__(self, duration: timedelta = seconds(0), + backdated_timestamp: Optional[datetime] = None) -> None: """ Start the timer. @@ -77,10 +80,10 @@ def __init__(self, duration: datetime.timedelta = seconds(0), :param backdated_timestamp: When the timer should have started. Used to keep the timers between sessions. """ self.duration = duration - self.reset() - if backdated_timestamp is not None: - time_already_used = datetime.datetime.now() - backdated_timestamp - self.starting_time -= to_seconds(time_already_used) + self.starting_time = perf_counter() + + if backdated_timestamp: + self.starting_time -= to_seconds(datetime.now() - backdated_timestamp) def is_expired(self) -> bool: """Check if a timer is expired.""" @@ -88,16 +91,16 @@ def is_expired(self) -> bool: def reset(self) -> None: """Reset the timer.""" - self.starting_time = time.perf_counter() + self.starting_time = perf_counter() - def time_since_reset(self) -> datetime.timedelta: + def time_since_reset(self) -> timedelta: """How much time has passed.""" - return seconds(time.perf_counter() - self.starting_time) + return seconds(perf_counter() - self.starting_time) - def time_until_expiration(self) -> datetime.timedelta: + def time_until_expiration(self) -> timedelta: """How much time is left until it expires.""" return max(seconds(0), self.duration - self.time_since_reset()) def starting_timestamp(self, timestamp_format: str) -> str: """When the timer started.""" - return (datetime.datetime.now() - self.time_since_reset()).strftime(timestamp_format) + return (datetime.now() - self.time_since_reset()).strftime(timestamp_format) diff --git a/test_bot/test_timer.py b/test_bot/test_timer.py new file mode 100644 index 000000000..576180ebf --- /dev/null +++ b/test_bot/test_timer.py @@ -0,0 +1,95 @@ +"""Test functions dedicated to time measurement and conversion.""" + +from datetime import datetime, timedelta + +from lib import timer + + +def test_time_conversion() -> None: + """Test conversion of time units.""" + assert timer.msec(1000) == timedelta(milliseconds=1000) + assert timer.to_msec(timedelta(milliseconds=1000)) == 1000 + + assert timer.msec_str(timedelta(milliseconds=1000)) == "1000" + + assert timer.seconds(1) == timedelta(seconds=1) + assert timer.to_seconds(timedelta(seconds=1)) == 1 + + assert timer.sec_str(timedelta(seconds=1)) == "1" + + assert timer.minutes(1) == timedelta(minutes=1) + assert timer.hours(1) == timedelta(hours=1) + assert timer.days(1) == timedelta(days=1) + assert timer.years(1) == timedelta(days=365) + + assert timer.to_msec(timer.seconds(1)) == 1000 + assert timer.to_seconds(timer.minutes(1)) == 60 + assert timer.to_seconds(timer.hours(1)) == 60*60 + assert timer.to_seconds(timer.days(1)) == 24*60*60 + assert timer.to_seconds(timer.years(1)) == 365*24*60*60 + + +def test_init() -> None: + """Test Timer class init.""" + t = timer.Timer() + assert t.duration == timedelta(0) + assert t.starting_time is not None + + duration = timedelta(seconds=10) + t = timer.Timer(duration) + assert t.duration == duration + assert t.starting_time is not None + + backdated_timestamp = datetime.now() - timedelta(seconds=10) + t = timer.Timer(backdated_timestamp=backdated_timestamp) + assert t.starting_time is not None + assert t.time_since_reset() >= timedelta(seconds=10) + +def test_is_expired() -> None: + """Test timer expiration.""" + t = timer.Timer(timedelta(seconds=10)) + assert not t.is_expired() + + t = timer.Timer(timedelta(seconds=0)) + assert t.is_expired() + + t = timer.Timer(timedelta(seconds=10)) + t.reset() + t.starting_time -= 10 + assert t.is_expired() + +def test_reset() -> None: + """Test timer reset.""" + t = timer.Timer(timedelta(seconds=10)) + t.reset() + assert t.starting_time is not None + assert timer.sec_str(t.time_since_reset()) == timer.sec_str(timedelta(0)) + +def test_time() -> None: + """Test time measurement, expiration, and time until expiration.""" + t = timer.Timer(timedelta(seconds=10)) + t.starting_time -= 5 + assert timer.sec_str(t.time_since_reset()) == timer.sec_str(timedelta(seconds=5)) + + t = timer.Timer(timedelta(seconds=10)) + t.starting_time -= 5 + assert timer.sec_str(t.time_until_expiration()) == timer.sec_str(timedelta(seconds=5)) + + t = timer.Timer(timedelta(seconds=10)) + t.starting_time -= 15 # Simulate time passing + assert t.time_until_expiration() == timedelta(0) + + t = timer.Timer(timedelta(seconds=10)) + t.starting_time -= 15 + assert t.time_until_expiration() == timedelta(0) + + t = timer.Timer(timedelta(seconds=10)) + t.starting_time -= 5 + assert timer.sec_str(t.time_until_expiration()) == timer.sec_str(timedelta(seconds=5)) + +def test_starting_timestamp() -> None: + """Test timestamp conversion and integration.""" + t = timer.Timer(timedelta(seconds=10)) + timestamp_format = "%Y-%m-%d %H:%M:%S" + expected_timestamp = (datetime.now() - t.time_since_reset()).strftime(timestamp_format) + assert t.starting_timestamp(timestamp_format) == expected_timestamp