Skip to content

Commit 60270a9

Browse files
committed
Add tests for seconds-to-text and SRT format conversion; fix bugs found by the tests.
1 parent 16ed21d commit 60270a9

File tree

3 files changed

+95
-29
lines changed

3 files changed

+95
-29
lines changed

src/pytranscript.py

Lines changed: 28 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@
2323
TranscriptFormat = Literal["csv", "json", "srt", "txt", "vtt"]
2424
TRANSCRIPT_FORMATS: tuple[TranscriptFormat] = typing.get_args(TranscriptFormat)
2525

26+
SECONDS_IN_MINUTE = 60
27+
SECONDS_IN_HOUR = SECONDS_IN_MINUTE * 60
28+
SECONDS_IN_DAY = SECONDS_IN_HOUR * 24
29+
2630

2731
class LineError(NamedTuple):
2832
time: float
@@ -34,37 +38,33 @@ def seconds_to_time(seconds: float) -> str:
3438
"""Convert seconds to time.
3539
3640
Args:
37-
seconds (float): the number of seconds
41+
seconds (float): the number of seconds. It must be less than 2**32.
3842
3943
Returns:
4044
str: the time in the format :
4145
-"mm:ss.ms" if less than 1 hour
4246
-"hh:mm:ss" if less than 1 day
4347
-"dd hh:mm:ss" if at least 1 day
48+
49+
Raises:
50+
ValueError: if seconds > 2**32
4451
"""
52+
if seconds > 2**32:
53+
# conversion from float to int here will lose precision
54+
msg = "seconds >= 2**32 are not supported because of loss of precision."
55+
raise ValueError(msg)
56+
4557
seconds = float(seconds)
46-
47-
if seconds < 3600: # Less than 1 hour
48-
minutes, seconds = divmod(seconds, 60)
49-
minutes = int(minutes)
50-
if seconds == int(seconds): # No decimal part in seconds
51-
return f"{minutes:02d}:{int(seconds):02d}"
52-
# Handle fractional seconds
53-
int_part, dec_part = str(seconds).split(".")
54-
dec_part = dec_part[:2] # Keep only two decimal places
55-
int_part = int(int_part)
56-
return f"{minutes:02d}:{int(int_part):02d}.{dec_part}"
57-
58-
elif seconds < 86400: # Less than 1 day
59-
hours, remaining_seconds = divmod(seconds, 3600)
60-
minutes, seconds = divmod(remaining_seconds, 60)
61-
return f"{int(hours):02d}:{int(minutes):02d}:{int(seconds):02d}"
62-
63-
else: # 1 day or more
64-
days, remaining_seconds = divmod(seconds, 86400)
65-
hours, remaining_seconds = divmod(remaining_seconds, 3600)
66-
minutes, seconds = divmod(remaining_seconds, 60)
67-
return f"{int(days)}d {int(hours):02d}:{int(minutes):02d}:{int(seconds):02d}"
58+
days, seconds = divmod(seconds, SECONDS_IN_DAY)
59+
hours, seconds = divmod(seconds, SECONDS_IN_HOUR)
60+
minutes, seconds = divmod(seconds, SECONDS_IN_MINUTE)
61+
62+
if days > 0:
63+
return f"{int(days)}d {int(hours):02d}:{int(minutes):02d}:{round(seconds):02d}"
64+
if hours > 0:
65+
return f"{int(hours):02d}:{int(minutes):02d}:{round(seconds):02d}"
66+
return f"{int(minutes):02d}:{seconds:05.2f}"
67+
6868

6969
def seconds_to_srt_time(seconds: float) -> str:
7070
"""Convert seconds to SRT time format.
@@ -76,11 +76,10 @@ def seconds_to_srt_time(seconds: float) -> str:
7676
str: the time in the format "hh:mm:ss,ms"
7777
"""
7878
seconds = float(seconds)
79-
hours, remainder = divmod(seconds, 3600)
80-
minutes, seconds = divmod(remainder, 60)
81-
int_seconds, dec_seconds = str(seconds).split(".")
82-
dec_seconds = dec_seconds[:3]
83-
return f"{int(hours):02d}:{int(minutes):02d}:{int(int_seconds):02d},{dec_seconds}"
79+
hours, remainder = divmod(seconds, SECONDS_IN_HOUR)
80+
minutes, seconds = divmod(remainder, SECONDS_IN_MINUTE)
81+
seconds_str = f"{round(seconds, 3):06.3f}".replace(".", ",")
82+
return f"{int(hours):02d}:{int(minutes):02d}:{seconds_str}"
8483

8584

8685
@dataclass
@@ -467,7 +466,7 @@ def _init_format(self):
467466

468467
case (_, None):
469468
if self.output.is_dir():
470-
self.output = self.output / self.input.stem
469+
self.output /= self.input.stem
471470
self.format = "all"
472471
else:
473472
self.format = self.output.suffix[1:]

tests/__init__.py

Whitespace-only changes.

tests/time_test.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import re
2+
3+
import hypothesis.strategies as st
4+
import pytest
5+
from hypothesis import given
6+
7+
import pytranscript as pt
8+
9+
10+
def seconds_strat(start, end):
11+
return st.floats(start, end, allow_nan=False, allow_infinity=False)
12+
13+
14+
@given(seconds_strat(pt.SECONDS_IN_DAY, 2**32))
15+
def test_seconds_to_time_value_day(seconds):
16+
formated_time = pt.seconds_to_time(seconds)
17+
match = re.match(r"^(\d+)d (\d{2}):(\d{2}):(\d{2})$", formated_time)
18+
assert match is not None
19+
result_time = (
20+
int(match[1]) * pt.SECONDS_IN_DAY
21+
+ int(match[2]) * pt.SECONDS_IN_HOUR
22+
+ int(match[3]) * pt.SECONDS_IN_MINUTE
23+
+ int(match[4])
24+
)
25+
assert result_time == round(seconds)
26+
27+
28+
@given(seconds_strat(pt.SECONDS_IN_HOUR, pt.SECONDS_IN_DAY - 1))
29+
def test_seconds_to_time_value_hour(seconds):
30+
formated_time = pt.seconds_to_time(seconds)
31+
match = re.match(r"^(\d{2}):(\d{2}):(\d{2})$", formated_time)
32+
assert match is not None
33+
result_time = (
34+
int(match[1]) * pt.SECONDS_IN_HOUR
35+
+ int(match[2]) * pt.SECONDS_IN_MINUTE
36+
+ int(match[3])
37+
)
38+
assert result_time == round(seconds)
39+
40+
41+
@given(seconds_strat(0, pt.SECONDS_IN_HOUR - 1))
42+
def test_seconds_to_time_value_minute(seconds):
43+
formated_time = pt.seconds_to_time(seconds)
44+
match = re.match(r"^(\d{2}):(\d{2}\.\d{2})$", formated_time)
45+
assert match is not None
46+
result_time = int(match[1]) * pt.SECONDS_IN_MINUTE + float(match[2])
47+
# need approx because of the conversion str -> float
48+
assert pytest.approx(result_time) == round(seconds, 2)
49+
50+
51+
def test_seconds_to_time_exception():
52+
with pytest.raises(ValueError, match=r".*seconds >= 2\*\*32 are not supported.*"):
53+
pt.seconds_to_time(2**32 + 1)
54+
55+
56+
@given(seconds_strat(0, 2**32))
57+
def test_seconds_to_srt(seconds):
58+
formated_time = pt.seconds_to_srt_time(seconds)
59+
match = re.match(r"^(\d+):(\d{2}):(\d{2}),(\d{3})$", formated_time)
60+
assert match is not None
61+
result_time = (
62+
int(match[1]) * pt.SECONDS_IN_HOUR
63+
+ int(match[2]) * pt.SECONDS_IN_MINUTE
64+
+ int(match[3])
65+
+ int(match[4]) / 1000
66+
)
67+
assert result_time == round(seconds, 3)

0 commit comments

Comments
 (0)