Skip to content

Commit d16f479

Browse files
committed
Merge branch 'MR/newapi' of https://github.com/cubicibo/timecode into MR/newapi
2 parents 78ec905 + 08d0500 commit d16f479

3 files changed

Lines changed: 215 additions & 68 deletions

File tree

src/timecode/helpers.py

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
"""Helper class for Timecode handling and byproducts."""
2+
3+
from __future__ import annotations
4+
5+
import sys
6+
from fractions import Fraction
7+
from typing import NewType
8+
9+
if sys.version_info >= (3, 11):
10+
_frate_type = Fraction | str | float | tuple[int, int]
11+
else:
12+
from typing import Union
13+
_frate_type = Union[Fraction, str, float, tuple[int, int]]
14+
15+
_Framerate = NewType("_Framerate", _frate_type)
16+
17+
class _Timestamp:
18+
def __init__(self, ts: Fraction, usec_precision: bool = False) -> None:
19+
if ts < 0:
20+
raise ValueError(f"Timestamp cannot be negative, got {ts}.")
21+
self.usec_precision = usec_precision
22+
self._exact_ts = ts
23+
24+
def __float__(self) -> float:
25+
"""Convert this _Timestamp instance to a float (timestamp in seconds).
26+
27+
Returns:
28+
float: timestamp value in seconds of this instance
29+
"""
30+
return float(self._exact_ts)
31+
32+
def total_seconds(self) -> float:
33+
"""Return the time in seconds of this Timestamp instance as a float.
34+
35+
Returns:
36+
float: timestamp value in seconds of this instance
37+
38+
Truncation is possible, it's not an exact representation.
39+
"""
40+
return float(self)
41+
42+
def exact(self) -> Fraction:
43+
"""Return the time in seconds of this Timestamp instance as a fraction.
44+
45+
Returns:
46+
Fraction: exact timestamp value in seconds of this instance.
47+
"""
48+
return self._exact_ts
49+
50+
def __eq__(self, other: _Timestamp | float | Fraction) -> bool:
51+
"""Equal implementation for _Timestamp instance.
52+
53+
Args:
54+
other (_Timestamp | int | float | Fraction): object to compare
55+
instance to.
56+
57+
Returns:
58+
Result of the operation.
59+
"""
60+
if isinstance(other, __class__):
61+
return self._exact_ts == other._exact_ts
62+
if isinstance(other, Fraction):
63+
return self._exact_ts == other
64+
if isinstance(other, (float, int)):
65+
return float(self) == other
66+
return NotImplemented
67+
68+
def __ne__(self, other: _Timestamp | float | Fraction) -> bool:
69+
"""Not equal implementation for _Timestamp instance.
70+
71+
Args:
72+
other (_Timestamp | int | float | Fraction): object to compare
73+
instance to.
74+
75+
Returns:
76+
Result of the operation.
77+
"""
78+
if isinstance(other, __class__):
79+
return self._exact_ts != other._exact_ts
80+
if isinstance(other, Fraction):
81+
return self._exact_ts != other
82+
if isinstance(other, (float, int)):
83+
return float(self) != other
84+
return NotImplemented
85+
86+
def __lt__(self, other: _Timestamp | float | Fraction) -> bool:
87+
"""Less than implementation for _Timestamp instance.
88+
89+
Args:
90+
other (_Timestamp | int | float | Fraction): object to compare
91+
instance to.
92+
93+
Returns:
94+
Result of the operation.
95+
"""
96+
if isinstance(other, __class__):
97+
return self._exact_ts < other._exact_ts
98+
if isinstance(other, Fraction):
99+
return self._exact_ts < other
100+
if isinstance(other, (float, int)):
101+
return float(self) < other
102+
return NotImplemented
103+
104+
def __gt__(self, other: _Timestamp | float | Fraction) -> bool:
105+
"""Greater than implementation for _Timestamp instance.
106+
107+
Args:
108+
other (_Timestamp | int | float | Fraction): object to compare
109+
instance to.
110+
111+
Returns:
112+
Result of the operation.
113+
"""
114+
if isinstance(other, __class__):
115+
return self._exact_ts > other._exact_ts
116+
if isinstance(other, Fraction):
117+
return self._exact_ts > other
118+
if isinstance(other, (float, int)):
119+
return float(self) > other
120+
return NotImplemented
121+
122+
def __le__(self, other: _Timestamp | float | Fraction) -> bool:
123+
"""Less or equal implementation for _Timestamp instance.
124+
125+
Args:
126+
other (_Timestamp | int | float | Fraction): object to compare
127+
instance to.
128+
129+
Returns:
130+
Result of the operation.
131+
"""
132+
if isinstance(other, __class__):
133+
return self._exact_ts <= other._exact_ts
134+
if isinstance(other, Fraction):
135+
return self._exact_ts <= other
136+
if isinstance(other, (float, int)):
137+
return float(self) <= other
138+
return NotImplemented
139+
140+
def __ge__(self, other: _Timestamp | float | Fraction) -> bool:
141+
"""Greater or equal implementation for _Timestamp instance.
142+
143+
Args:
144+
other (_Timestamp | int | float | Fraction): object to compare
145+
instance to.
146+
147+
Returns:
148+
Result of the operation.
149+
"""
150+
if isinstance(other, __class__):
151+
return self._exact_ts >= other._exact_ts
152+
if isinstance(other, Fraction):
153+
return self._exact_ts >= other
154+
if isinstance(other, (float, int)):
155+
return float(self) >= other
156+
return NotImplemented
157+
158+
def __str__(self) -> str:
159+
"""Convert the _Timestamp instance to a timestamp string.
160+
161+
Returns:
162+
string: Timestamp string of this _Timestamp instance.
163+
"""
164+
hh = int(self._exact_ts // 3600)
165+
mm = int(self._exact_ts // 60) % 60
166+
ss = int(self._exact_ts % 60)
167+
decimal_part = (self._exact_ts - int(self._exact_ts))*1000
168+
169+
if self.usec_precision:
170+
s_decimal_part = f"{round(decimal_part*1000):>06}"
171+
else:
172+
s_decimal_part = f"{round(decimal_part):03}"
173+
return f"{hh:02d}:{mm:02d}:{ss:02d}.{s_decimal_part}"
174+
175+
def __repr__(self) -> str:
176+
"""Represent a _Timestamp instance.
177+
178+
Returns:
179+
string: representation of this _Timestamp instance.
180+
"""
181+
usec_part = f", usec_precision={self.usec_precision}" * self.usec_precision
182+
return f"{__class__.__name__}({self._exact_ts}{usec_part})"
183+
####

src/timecode/timecode.py

Lines changed: 3 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -6,82 +6,18 @@
66
import math
77
import sys
88
from fractions import Fraction
9-
from typing import TYPE_CHECKING, NewType
9+
from typing import TYPE_CHECKING
10+
11+
from .helpers import _Framerate, _Timestamp
1012

1113
if TYPE_CHECKING:
1214
from collections.abc import Iterator
1315

1416
if sys.version_info >= (3, 11):
1517
from typing import Self
16-
_frate_type = Fraction | str | float | tuple[int, int]
1718
else:
1819
from typing_extensions import Self
19-
from typing import Union
20-
_frate_type = Union[Fraction, str, float, tuple[int, int]]
21-
22-
_Framerate = NewType("_Framerate", _frate_type)
23-
24-
#%%
25-
class _Timestamp:
26-
def __init__(self, ts: Fraction, usec_precision: bool = False) -> None:
27-
if ts < 0:
28-
raise ValueError(f"Timestamp cannot be negative, got {ts}.")
29-
self.usec_precision = usec_precision
30-
self._exact_ts = ts
31-
32-
def __float__(self) -> float:
33-
"""Convert this _Timestamp instance to a float (timestamp in seconds).
34-
35-
Returns:
36-
float: timestamp value in seconds of this instance
37-
"""
38-
return float(self._exact_ts)
39-
40-
def total_seconds(self) -> float:
41-
"""Return the time in seconds of this Timestamp instance as a float.
42-
43-
Returns:
44-
float: timestamp value in seconds of this instance
45-
46-
Truncation is possible, it's not an exact representation.
47-
"""
48-
return float(self)
49-
50-
def exact(self) -> Fraction:
51-
"""Return the time in seconds of this Timestamp instance as a fraction.
52-
53-
Returns:
54-
Fraction: exact timestamp value in seconds of this instance.
55-
"""
56-
return self._exact_ts
5720

58-
def __str__(self) -> str:
59-
"""Convert the _Timestamp instance to a timestamp string.
60-
61-
Returns:
62-
string: Timestamp string of this _Timestamp instance.
63-
"""
64-
hh = int(self._exact_ts // 3600)
65-
mm = int(self._exact_ts // 60) % 60
66-
ss = int(self._exact_ts % 60)
67-
decimal_part = (self._exact_ts - int(self._exact_ts))*1000
68-
69-
if self.usec_precision:
70-
s_decimal_part = f"{round(decimal_part*1000):>06}"
71-
else:
72-
s_decimal_part = f"{round(decimal_part):03}"
73-
return f"{hh:02d}:{mm:02d}:{ss:02d}.{s_decimal_part}"
74-
75-
def __repr__(self) -> str:
76-
"""Represent a _Timestamp instance.
77-
78-
Returns:
79-
string: representation of this _Timestamp instance.
80-
"""
81-
usec_part = f", usec_precision={self.usec_precision}" * self.usec_precision
82-
return f"{__class__.__name__}({self._exact_ts}{usec_part})"
83-
####
84-
8521
#%%
8622
class Timecode:
8723
"""The main timecode class.

tests/test_timecode.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1756,7 +1756,35 @@ def test_repr_parse():
17561756
#force NDF to verify there is no mangling even with a DF framerate
17571757
tc = Timecode('30000/1001', '12:34:56:21', force_non_drop_frame=True)
17581758
tc_eval = eval(repr(tc))
1759-
1759+
17601760
assert tc.frames == tc_eval.frames
17611761
assert tc.framerate == tc_eval.framerate
1762+
1763+
def test_reference_time():
1764+
framerate = Fraction(120000, 1001)
1765+
#start_seconds is in the system timebase, so use int_fps
1766+
tc1 = Timecode(framerate, start_seconds=1/120)
1767+
tc2 = Timecode(framerate, start_seconds=0, reference_time_on_display=True)
1768+
assert tc1 == tc2
1769+
1770+
def test_reference_time_frame_offset():
1771+
framerate = Fraction(120000, 1001)
1772+
#start_seconds is in the system timebase, so use int_fps
1773+
tc1 = Timecode(framerate, start_seconds=600)
1774+
tc2 = Timecode(framerate, start_seconds=600, reference_time_on_display=True)
1775+
assert (tc1 + 1) == tc2
1776+
1777+
def test_reference_real_system_time_value():
1778+
framerate = Fraction(48000, 1001)
1779+
tc = Timecode(framerate, frames=1, reference_time_on_display=True)
1780+
assert tc.frames == 1
1781+
assert tc._is_ntsc_rate is True # needed for this test
1782+
1783+
# Time reference on system starting to draw the first frame.
1784+
# only time where both are equal.
1785+
assert float(tc.to_realtime()) == 0.0
1786+
assert float(tc.to_systemtime()) == 0.0
17621787

1788+
tc.reference_time_on_display = False
1789+
# No longer true, NTSC is slower than wall-clock
1790+
assert 0 < tc.to_systemtime() < tc.to_realtime()

0 commit comments

Comments
 (0)