Skip to content

Commit 572c455

Browse files
author
Philip de Nier
committed
Support negative timestamp in to unix and iso8691 conversions
Removed the to_utc method which was deprecated in favour of to_unix. CHanged to_unix to return the sign as well. sem-ver: api-break
1 parent 5afe484 commit 572c455

3 files changed

Lines changed: 118 additions & 46 deletions

File tree

mediatimestamp/__main__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
print("ips-tai-nsec {}".format(ts.to_tai_sec_nsec()))
2727
print("ips-tai-frac {}".format(ts.to_tai_sec_frac()))
2828
print("utc {}".format(ts.to_iso8601_utc()))
29-
print("utc-secs {}".format(ts.to_utc()[0]))
29+
print("utc-secs {}".format(ts.to_unix()[0]))
3030
print("smpte time label {}".format(ts.to_smpte_timelabel(50, 1)))
3131
sys.exit(0)
3232

mediatimestamp/immutable/timestamp.py

Lines changed: 68 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,12 @@ def __mediatimerange__(self) -> "TimeRange":
148148
@classmethod
149149
def get_time(cls) -> "Timestamp":
150150
unix_time = time.time()
151-
return cls.from_unix(int(unix_time), int(unix_time*cls.MAX_NANOSEC) - int(unix_time)*cls.MAX_NANOSEC)
151+
abs_unix_time = abs(unix_time)
152+
unix_sec = int(abs_unix_time)
153+
unix_ns = int(abs_unix_time*cls.MAX_NANOSEC) - int(abs_unix_time)*cls.MAX_NANOSEC
154+
unix_sign = 1 if unix_time >= 0 else -1
155+
156+
return cls.from_unix(unix_sec, unix_ns, unix_sign=unix_sign)
152157

153158
@classmethod
154159
@deprecated(version="4.0.0",
@@ -238,10 +243,18 @@ def from_float(cls, toff_float: float) -> "Timestamp":
238243
def from_datetime(cls, dt: datetime) -> "Timestamp":
239244
minTs = datetime.fromtimestamp(0, tz.gettz('UTC'))
240245
utcdt = dt.astimezone(tz.gettz('UTC'))
241-
seconds = int((utcdt - minTs).total_seconds())
246+
seconds = abs(int((utcdt - minTs).total_seconds()))
242247
nanoseconds = utcdt.microsecond * 1000
248+
if utcdt < minTs:
249+
sign = -1
250+
if nanoseconds > 0:
251+
# The microseconds was for a positive date-time. In a negative
252+
# unix time it needs to be flipped.
253+
nanoseconds = cls.MAX_NANOSEC - nanoseconds
254+
else:
255+
sign = 1
243256

244-
return cls.from_unix(seconds, nanoseconds, False)
257+
return cls.from_unix(unix_sec=seconds, unix_ns=nanoseconds, unix_sign=sign, is_leap=False)
245258

246259
@classmethod
247260
def from_iso8601_utc(cls, iso8601utc: str) -> "Timestamp":
@@ -250,7 +263,18 @@ def from_iso8601_utc(cls, iso8601utc: str) -> "Timestamp":
250263
year, month, day, hour, minute, second, ns = _parse_iso8601(iso8601utc[:-1])
251264
gmtuple = (year, month, day, hour, minute, second - (second == 60))
252265
secs_since_epoch = calendar.timegm(gmtuple)
253-
return cls.from_unix(secs_since_epoch, ns, (second == 60))
266+
if secs_since_epoch < 0:
267+
sign = -1
268+
secs_since_epoch = abs(secs_since_epoch)
269+
if ns > 0:
270+
# The ns parsed from the timestamp was for a positive ISO 8601 date-time. In a negative
271+
# unix time it needs to be flipped.
272+
ns = cls.MAX_NANOSEC - ns
273+
secs_since_epoch -= 1
274+
else:
275+
sign = 1
276+
277+
return cls.from_unix(unix_sec=secs_since_epoch, unix_ns=ns, unix_sign=sign, is_leap=(second == 60))
254278

255279
@classmethod
256280
def from_smpte_timelabel(cls, timelabel: str) -> "Timestamp":
@@ -319,19 +343,16 @@ def from_count(cls, count: int, rate_num: RationalTypes, rate_den: RationalTypes
319343
return cls(ns=ns, sign=sign)
320344

321345
@classmethod
322-
def from_unix(cls, unix_sec: int, unix_ns: int, is_leap: bool = False) -> "Timestamp":
346+
def from_unix(cls, unix_sec: int, unix_ns: int, unix_sign: int = 1, is_leap: bool = False) -> "Timestamp":
323347
leap_sec = 0
324-
for tbl_sec, tbl_tai_sec_minus_1 in UTC_LEAP:
325-
if unix_sec >= tbl_sec:
326-
leap_sec = (tbl_tai_sec_minus_1 + 1) - tbl_sec
327-
break
328-
return cls(sec=unix_sec+leap_sec+is_leap, ns=unix_ns)
329-
330-
@classmethod
331-
def from_utc(cls, utc_sec: int, utc_ns: int, is_leap: bool = False) -> "Timestamp":
332-
""" Wrapper of from_unix for back-compatibility.
333-
"""
334-
return cls.from_unix(utc_sec, utc_ns, is_leap)
348+
if unix_sign >= 0:
349+
for tbl_sec, tbl_tai_sec_minus_1 in UTC_LEAP:
350+
if unix_sec + is_leap >= tbl_sec:
351+
leap_sec = (tbl_tai_sec_minus_1 + 1) - tbl_sec
352+
break
353+
else:
354+
is_leap = False
355+
return cls(sec=unix_sec+leap_sec, ns=unix_ns, sign=unix_sign)
335356

336357
def is_null(self) -> bool:
337358
return self._value == 0
@@ -411,55 +432,61 @@ def to_float(self) -> float:
411432
return self._value / Timestamp.MAX_NANOSEC
412433

413434
def to_datetime(self) -> datetime:
414-
sec, nsec, leap = self.to_unix()
435+
sec, nsec, sign, leap = self.to_unix()
415436
microsecond = int(round(nsec/1000))
416437
if microsecond > 999999:
417438
sec += 1
418439
microsecond = 0
419-
dt = datetime.fromtimestamp(sec, tz.gettz('UTC'))
440+
if sign < 0 and microsecond > 0:
441+
# The microseconds is for a negative unix time. In a positive date-time
442+
# it needs to be flipped.
443+
microsecond = 1000000 - microsecond
444+
sec += 1
445+
dt = datetime.fromtimestamp(sign * sec, tz.gettz('UTC'))
420446
dt = dt.replace(microsecond=microsecond)
421447

422448
return dt
423449

424-
def to_unix(self) -> Tuple[int, int, bool]:
450+
def to_unix(self) -> Tuple[int, int, int, bool]:
425451
""" Convert to unix seconds.
426452
Returns a tuple of (seconds, nanoseconds, is_leap), where `is_leap` is
427453
`True` when the input time corresponds exactly to a UTC leap second.
428454
Note that this deliberately returns a tuple, to try and avoid confusion.
429455
"""
430-
leap_sec = 0
431-
is_leap = False
432-
for unix_sec, tai_sec_minus_1 in UTC_LEAP:
433-
if self.sec >= tai_sec_minus_1:
434-
leap_sec = (tai_sec_minus_1 + 1) - unix_sec
435-
is_leap = self.sec == tai_sec_minus_1
436-
break
437-
438-
return (self.sec - leap_sec, self.ns, is_leap)
456+
if self._value < 0:
457+
return (self.sec, self.ns, self.sign, False)
458+
else:
459+
leap_sec = 0
460+
is_leap = False
461+
for unix_sec, tai_sec_minus_1 in UTC_LEAP:
462+
if self.sec >= tai_sec_minus_1:
463+
leap_sec = (tai_sec_minus_1 + 1) - unix_sec
464+
is_leap = self.sec == tai_sec_minus_1
465+
break
439466

440-
def to_utc(self) -> Tuple[int, int, bool]:
441-
""" Wrapper of to_unix for back-compatibility.
442-
"""
443-
return self.to_unix()
467+
return (self.sec - leap_sec, self.ns, self.sign, is_leap)
444468

445469
def to_unix_float(self) -> float:
446470
""" Convert to unix seconds since the epoch as a floating point number
447471
"""
448-
if self._value < 0:
449-
return self.to_float()
450-
else:
451-
(sec, ns, _) = self.to_unix()
452-
return sec + ns / Timestamp.MAX_NANOSEC
472+
(sec, ns, sign, _) = self.to_unix()
473+
return sign * (sec + ns / Timestamp.MAX_NANOSEC)
453474

454475
def to_iso8601_utc(self) -> str:
455476
""" Get printed representation in ISO8601 format (UTC)
456477
YYYY-MM-DDThh:mm:ss.s
457478
where `s` is fractional seconds at nanosecond precision (always 9-chars wide)
458479
"""
459-
unix_s, unix_ns, is_leap = self.to_unix()
460-
utc_bd = time.gmtime(unix_s)
461-
frac_sec = self._get_fractional_seconds(fixed_size=True)
480+
unix_s, unix_ns, unix_sign, is_leap = self.to_unix()
481+
if unix_sign < 0 and unix_ns > 0:
482+
# The nanoseconds is for a negative unix time. In a positive ISO 8601 date-time
483+
# it needs to be flipped.
484+
unix_ns = Timestamp.MAX_NANOSEC - unix_ns
485+
unix_s += 1
486+
utc_bd = time.gmtime(unix_sign * unix_s)
487+
frac_sec = Timestamp(ns=unix_ns)._get_fractional_seconds(fixed_size=True)
462488
leap_sec = int(is_leap)
489+
463490
return '%04d-%02d-%02dT%02d:%02d:%02d.%sZ' % (utc_bd.tm_year,
464491
utc_bd.tm_mon,
465492
utc_bd.tm_mday,
@@ -481,7 +508,7 @@ def to_smpte_timelabel(self,
481508
count_on_or_after_second = Timestamp(tai_seconds, 0).to_count(rate, rounding=self.ROUND_UP)
482509
count_within_second = count - count_on_or_after_second
483510

484-
unix_sec, unix_ns, is_leap = normalised_ts.to_unix()
511+
unix_sec, unix_ns, unix_sign, is_leap = normalised_ts.to_unix()
485512
leap_sec = int(is_leap)
486513

487514
if utc_offset is None:

tests/test_timestamp.py

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -746,6 +746,12 @@ def test_convert_iso_utc(self):
746746
"""This tests that conversion to and from ISO date format UTC time works as expected."""
747747

748748
tests = [
749+
(Timestamp(62135596800, 0, -1), "0001-01-01T00:00:00.000000000Z"),
750+
(Timestamp(1, 0, -1), "1969-12-31T23:59:59.000000000Z"),
751+
(Timestamp(0, 999999999, -1), "1969-12-31T23:59:59.000000001Z"),
752+
(Timestamp(0, 1, -1), "1969-12-31T23:59:59.999999999Z"),
753+
(Timestamp(0, 0, 1), "1970-01-01T00:00:00.000000000Z"),
754+
749755
(Timestamp(1424177663, 102003), "2015-02-17T12:53:48.000102003Z"),
750756

751757
# the leap second is 23:59:60
@@ -767,7 +773,8 @@ def test_convert_iso_utc(self):
767773
(Timestamp(1341100835, 100000000), "2012-07-01T00:00:00.100000000Z"),
768774
(Timestamp(1341100835, 999999999), "2012-07-01T00:00:00.999999999Z"),
769775

770-
(Timestamp(283996818, 0), "1979-01-01T00:00:00.000000000Z") # 1979
776+
(Timestamp(283996818, 0), "1979-01-01T00:00:00.000000000Z"), # 1979
777+
(Timestamp(253402300836, 999999999), "9999-12-31T23:59:59.999999999Z")
771778
]
772779

773780
for t in tests:
@@ -843,9 +850,16 @@ def test_from_datetime(self):
843850
"""Conversion from python's datetime object."""
844851

845852
tests = [
853+
(datetime(1, 1, 1, 0, 0, 0, 0, tz.gettz('UTC')), Timestamp(62135596800, 0, -1)),
854+
(datetime(1969, 12, 31, 23, 59, 59, 0, tz.gettz('UTC')), Timestamp(1, 0, -1)),
855+
(datetime(1969, 12, 31, 23, 59, 59, 1, tz.gettz('UTC')), Timestamp(0, 999999000, -1)),
856+
(datetime(1969, 12, 31, 23, 59, 59, 999999, tz.gettz('UTC')), Timestamp(0, 1000, -1)),
846857
(datetime(1970, 1, 1, 0, 0, 0, 0, tz.gettz('UTC')), Timestamp(0, 0)),
847858
(datetime(1983, 3, 29, 15, 45, 0, 0, tz.gettz('UTC')), Timestamp(417800721, 0)),
848859
(datetime(2017, 12, 5, 16, 33, 12, 196, tz.gettz('UTC')), Timestamp(1512491629, 196000)),
860+
(datetime(2514, 1, 1, 0, 0, 0, 0, tz.gettz('UTC')), Timestamp(17166988837, 0, 1)),
861+
# Stopping around here because high datetime values have a floating point error.
862+
# See https://stackoverflow.com/a/75582241.
849863
]
850864

851865
for t in tests:
@@ -855,10 +869,18 @@ def test_to_datetime(self):
855869
"""Conversion to python's datetime object."""
856870

857871
tests = [
872+
(datetime(1, 1, 1, 0, 0, 0, 0, tz.gettz('UTC')), Timestamp(62135596800, 0, -1)),
873+
(datetime(1969, 12, 31, 23, 59, 59, 0, tz.gettz('UTC')), Timestamp(1, 0, -1)),
874+
(datetime(1969, 12, 31, 23, 59, 59, 1, tz.gettz('UTC')), Timestamp(0, 999999000, -1)),
875+
(datetime(1969, 12, 31, 23, 59, 59, 999999, tz.gettz('UTC')), Timestamp(0, 1000, -1)),
858876
(datetime(1970, 1, 1, 0, 0, 0, 0, tz.gettz('UTC')), Timestamp(0, 0)),
877+
(datetime(1970, 1, 1, 0, 0, 0, 1, tz.gettz('UTC')), Timestamp(0, 1000)),
859878
(datetime(1983, 3, 29, 15, 45, 0, 0, tz.gettz('UTC')), Timestamp(417800721, 0)),
860879
(datetime(2017, 12, 5, 16, 33, 12, 196, tz.gettz('UTC')), Timestamp(1512491629, 196000)),
861880
(datetime(2017, 12, 5, 16, 33, 13, 0, tz.gettz('UTC')), Timestamp(1512491629, 999999999)),
881+
(datetime(2514, 1, 1, 0, 0, 0, 0, tz.gettz('UTC')), Timestamp(17166988837, 0, 1)),
882+
# Stopping around here because high datetime values have a floating point error.
883+
# See https://stackoverflow.com/a/75582241
862884
]
863885

864886
for t in tests:
@@ -896,11 +918,34 @@ def test_get_leap_seconds(self):
896918
for t in tests:
897919
self.assertEqual(t[0].get_leap_seconds(), t[1])
898920

921+
def test_from_unix(self):
922+
tests = [
923+
((Timestamp.MAX_SECONDS - 1, Timestamp.MAX_NANOSEC - 1, -1, False), # 0 leap seconds
924+
Timestamp(Timestamp.MAX_SECONDS - 1, Timestamp.MAX_NANOSEC - 1, -1)), # 0 leap seconds
925+
((1000, 0, -1, False), Timestamp(1000, 0, -1)), # 0 leap seconds
926+
((63071999, 999999999, 1, False), Timestamp(63071999, 999999999)), # 0 leap seconds
927+
((63071999, 0, 1, True), Timestamp(63072009, 0)), # 10 leap seconds at leap
928+
((63072000, 0, 1, False), Timestamp(63072010, 0)), # 10 leap seconds
929+
((63072008, 999999999, 1, False), Timestamp(63072018, 999999999)), # 10 leap seconds
930+
((1512491592, 0, 1, False), Timestamp(1512491629, 0)), # 37 leap seconds
931+
((Timestamp.MAX_SECONDS - 1 - 37, Timestamp.MAX_NANOSEC - 1, 1, False),
932+
Timestamp(Timestamp.MAX_SECONDS - 1, Timestamp.MAX_NANOSEC - 1)), # 37 leap seconds
933+
]
934+
935+
for t in tests:
936+
self.assertEqual(Timestamp.from_unix(*t[0]), t[1])
937+
899938
def test_to_unix(self):
900939
tests = [
901-
(Timestamp(63072008, 999999999), (63072008, 999999999, False)), # 0 leap seconds
902-
(Timestamp(63072009, 0), (63071999, 0, True)), # 10 leap seconds at leap
903-
(Timestamp(1512491629, 0), (1512491592, 0, False)), # 37 leap seconds
940+
(Timestamp(Timestamp.MAX_SECONDS - 1, Timestamp.MAX_NANOSEC - 1, -1), # 0 leap seconds
941+
(Timestamp.MAX_SECONDS - 1, Timestamp.MAX_NANOSEC - 1, -1, False)), # 0 leap seconds
942+
(Timestamp(1000, 0, -1), (1000, 0, -1, False)), # 0 leap seconds
943+
(Timestamp(63072008, 999999999), (63072008, 999999999, 1, False)), # 0 leap seconds
944+
(Timestamp(63072009, 0), (63071999, 0, 1, True)), # 10 leap seconds at leap
945+
(Timestamp(63072010, 0), (63072000, 0, 1, False)), # 10 leap seconds
946+
(Timestamp(1512491629, 0), (1512491592, 0, 1, False)), # 37 leap seconds
947+
(Timestamp(Timestamp.MAX_SECONDS - 1, Timestamp.MAX_NANOSEC - 1),
948+
(Timestamp.MAX_SECONDS - 1 - 37, Timestamp.MAX_NANOSEC - 1, 1, False)), # 37 leap seconds
904949
]
905950

906951
for t in tests:

0 commit comments

Comments
 (0)