Skip to content

Commit 84cbefb

Browse files
authored
Merge pull request MIT-LCP#384 from MIT-LCP/time-conversion
Convenience methods for time conversion
2 parents 59b09ba + f224b0f commit 84cbefb

File tree

4 files changed

+170
-4
lines changed

4 files changed

+170
-4
lines changed

docs/io.rst

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@ WFDB Records
1212
:members: rdrecord, rdheader, rdsamp, wrsamp
1313

1414
.. autoclass:: wfdb.io.Record
15-
:members: wrsamp, adc, dac
15+
:members: get_frame_number, get_elapsed_time, get_absolute_time,
16+
wrsamp, adc, dac
1617

1718
.. autoclass:: wfdb.io.MultiRecord
18-
:members: multi_to_single
19+
:members: get_frame_number, get_elapsed_time, get_absolute_time,
20+
multi_to_single
1921

2022

2123
WFDB Anotations

docs/wfdb.rst

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@ WFDB Records
1212
:members: rdrecord, rdheader, rdsamp, wrsamp
1313

1414
.. autoclass:: wfdb.Record
15-
:members: wrsamp, adc, dac
15+
:members: get_frame_number, get_elapsed_time, get_absolute_time,
16+
wrsamp, adc, dac
1617

1718
.. autoclass:: wfdb.MultiRecord
18-
:members: multi_to_single
19+
:members: get_frame_number, get_elapsed_time, get_absolute_time,
20+
multi_to_single
1921

2022

2123
WFDB Anotations

tests/test_record.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import datetime
12
import os
23
import shutil
34
import unittest
@@ -817,6 +818,68 @@ def test_multi_variable_d(self):
817818
assert record.__eq__(record_named)
818819

819820

821+
class TestTimeConversion(unittest.TestCase):
822+
"""
823+
Test cases for time conversion
824+
"""
825+
826+
def test_single(self):
827+
"""
828+
Time conversion for a single-segment record
829+
830+
This checks the get_frame_number, get_elapsed_time, and
831+
get_absolute_time methods for a Record object. The example record
832+
has no base date defined, so attempting to convert to/from absolute
833+
time should raise an exception.
834+
835+
"""
836+
header = wfdb.rdheader("sample-data/test01_00s")
837+
838+
# these time values should be equivalent
839+
n = 123 * header.fs
840+
t = datetime.timedelta(seconds=123)
841+
self.assertEqual(header.get_frame_number(n), n)
842+
self.assertEqual(header.get_frame_number(t), n)
843+
self.assertEqual(header.get_elapsed_time(n), t)
844+
self.assertEqual(header.get_elapsed_time(t), t)
845+
846+
# record test01_00s has no base date, so absolute time conversions
847+
# should fail
848+
self.assertIsNone(header.base_date)
849+
d = datetime.datetime(2001, 1, 1, 12, 0, 0)
850+
self.assertRaises(ValueError, header.get_frame_number, d)
851+
self.assertRaises(ValueError, header.get_absolute_time, n)
852+
self.assertRaises(ValueError, header.get_absolute_time, t)
853+
854+
def test_multisegment_with_date(self):
855+
"""
856+
Time conversion for a multi-segment record with base date
857+
858+
This checks the get_frame_number, get_elapsed_time, and
859+
get_absolute_time methods for a MultiRecord object. The example
860+
record has a base date, so we can convert timestamps between all
861+
three of the supported representations.
862+
863+
"""
864+
header = wfdb.rdheader(
865+
"sample-data/multi-segment/p000878/p000878-2137-10-26-16-57"
866+
)
867+
868+
# these time values should be equivalent
869+
n = 123 * header.fs
870+
t = datetime.timedelta(seconds=123)
871+
d = t + header.base_datetime
872+
self.assertEqual(header.get_frame_number(n), n)
873+
self.assertEqual(header.get_frame_number(t), n)
874+
self.assertEqual(header.get_frame_number(d), n)
875+
self.assertEqual(header.get_elapsed_time(n), t)
876+
self.assertEqual(header.get_elapsed_time(t), t)
877+
self.assertEqual(header.get_elapsed_time(d), t)
878+
self.assertEqual(header.get_absolute_time(n), d)
879+
self.assertEqual(header.get_absolute_time(t), d)
880+
self.assertEqual(header.get_absolute_time(d), d)
881+
882+
820883
class TestSignal(unittest.TestCase):
821884
"""
822885
For lower level signal tests

wfdb/io/record.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,105 @@ def base_datetime(self, value):
249249
else:
250250
raise TypeError(f"invalid base_datetime value: {value!r}")
251251

252+
def get_frame_number(self, time_value):
253+
"""
254+
Convert a time value to a frame number.
255+
256+
A time value may be specified as:
257+
- An integer or floating-point number, representing the number of
258+
WFDB frames elapsed from the start of the record.
259+
- A `datetime.timedelta` object, representing elapsed time from the
260+
start of the record.
261+
- A `datetime.datetime` object, representing an absolute date and
262+
time (if the record starting time is known.)
263+
264+
Note that this function may return a value that is less than zero
265+
or greater than the actual length of the record.
266+
267+
Parameters
268+
----------
269+
time_value : number or timedelta or datetime
270+
A time value.
271+
272+
Returns
273+
-------
274+
frame_number : float
275+
Frame number (possibly a fractional frame number).
276+
277+
"""
278+
if hasattr(time_value, "__float__"):
279+
return float(time_value)
280+
281+
if isinstance(time_value, datetime.datetime):
282+
if not self.base_datetime:
283+
raise ValueError(
284+
"base_datetime is unknown; cannot convert absolute "
285+
"date/time to a frame number"
286+
)
287+
time_value -= self.base_datetime
288+
289+
if isinstance(time_value, datetime.timedelta):
290+
return time_value.total_seconds() * self.fs
291+
292+
raise TypeError(f"invalid time value: {time_value!r}")
293+
294+
def get_elapsed_time(self, time_value):
295+
"""
296+
Convert a time value to an elapsed time in seconds.
297+
298+
A time value may be specified as:
299+
- An integer or floating-point number, representing the number of
300+
WFDB frames elapsed from the start of the record.
301+
- A `datetime.timedelta` object, representing elapsed time from the
302+
start of the record.
303+
- A `datetime.datetime` object, representing an absolute date and
304+
time (if the record starting time is known.)
305+
306+
Parameters
307+
----------
308+
time_value : number or timedelta or datetime
309+
A time value.
310+
311+
Returns
312+
-------
313+
elapsed_time : timedelta
314+
Elapsed time from the start of the record.
315+
316+
"""
317+
time_value = self.get_frame_number(time_value)
318+
return datetime.timedelta(seconds=time_value / self.fs)
319+
320+
def get_absolute_time(self, time_value):
321+
"""
322+
Convert a time value to an absolute date and time.
323+
324+
A time value may be specified as:
325+
- An integer or floating-point number, representing the number of
326+
WFDB frames elapsed from the start of the record.
327+
- A `datetime.timedelta` object, representing elapsed time from the
328+
start of the record.
329+
- A `datetime.datetime` object, representing an absolute date and
330+
time (if the record starting time is known.)
331+
332+
Parameters
333+
----------
334+
time_value : number or timedelta or datetime
335+
A time value.
336+
337+
Returns
338+
-------
339+
absolute_time : datetime
340+
Absolute date and time.
341+
342+
"""
343+
time_value = self.get_elapsed_time(time_value)
344+
if not self.base_datetime:
345+
raise ValueError(
346+
"base_datetime is unknown; cannot convert frame number "
347+
"to an absolute date/time"
348+
)
349+
return time_value + self.base_datetime
350+
252351
def check_field(self, field, required_channels="all"):
253352
"""
254353
Check whether a single field is valid in its basic form. Does

0 commit comments

Comments
 (0)