From 2c5043cb658ba8567ae43393e03ceefd4c6160d54be2eda306170317dec82aa5 Mon Sep 17 00:00:00 2001 From: Sergey Matveev Date: Wed, 20 Nov 2024 15:39:34 +0300 Subject: [PATCH] Revised and corrected work with TAI64 --- pyac/pyac.py | 118 +++++++++++++++++++++++++++--------------- pyac/test_leapsecs.py | 45 ++++++++++++++++ 2 files changed, 120 insertions(+), 43 deletions(-) create mode 100644 pyac/test_leapsecs.py diff --git a/pyac/pyac.py b/pyac/pyac.py index 4941451..a3a2db2 100755 --- a/pyac/pyac.py +++ b/pyac/pyac.py @@ -89,36 +89,38 @@ def LenFirstSort(v): TAI64Base = 0x4000000000000000 Leapsecs1972 = 10 Leapsecs = tuple( - int(datetime(y, m, 1, 0, 0, 0, 0, tzinfo=timezone.utc).timestamp()) - for y, m in ( - (2017, 1), - (2015, 7), - (2012, 7), - (2009, 1), - (2006, 1), - (1999, 1), - (1997, 7), - (1996, 1), - (1994, 7), - (1993, 7), - (1992, 7), - (1991, 1), - (1990, 1), - (1988, 1), - (1985, 7), - (1983, 7), - (1982, 7), - (1981, 7), - (1980, 1), - (1979, 1), - (1978, 1), - (1977, 1), - (1976, 1), - (1975, 1), - (1974, 1), - (1973, 1), - (1972, 7), + Leapsecs1972 + i + int( + datetime(y, m, 1, 0, 0, 0, 0, tzinfo=timezone.utc).timestamp() ) + for i, (y, m) in enumerate(( + (1972, 7), + (1973, 1), + (1974, 1), + (1975, 1), + (1976, 1), + (1977, 1), + (1978, 1), + (1979, 1), + (1980, 1), + (1981, 7), + (1982, 7), + (1983, 7), + (1985, 7), + (1988, 1), + (1990, 1), + (1991, 1), + (1992, 7), + (1993, 7), + (1994, 7), + (1996, 1), + (1997, 7), + (1999, 1), + (2006, 1), + (2009, 1), + (2012, 7), + (2015, 7), + (2017, 1), + )) ) @@ -162,12 +164,7 @@ def dumps(v): raise NotImplementedError("no FLOAT* support") if isinstance(v, datetime): secs = int(v.replace(tzinfo=timezone.utc).timestamp()) - diff = Leapsecs1972 - for n, leapsec in enumerate(Leapsecs): - if secs > leapsec: - diff += len(Leapsecs) - n - break - secs += TAI64Base + diff + secs = utc2tai(secs) if v.microsecond == 0: return _byte(TagTAI64) + secs.to_bytes(8, "big") return ( @@ -233,7 +230,46 @@ _floats = {TagFloat16: 2, TagFloat32: 4, TagFloat64: 8, TagFloat128: 16, TagFloa _tais = {TagTAI64: 8, TagTAI64N: 12, TagTAI64NA: 16} -def loads(v, sets=False): +def tai2utc(secs, leapsecUTCAllow=False): + """Subtract leapseconds from TAI + + :returns: UTC seconds, or None of TAI equals to leap second + """ + secs -= TAI64Base + diff = 0 + for leapsec in Leapsecs: + if secs < leapsec: + break + diff += 1 + if secs == leapsec: + if leapsecUTCAllow: + break + return None + return secs - (diff + Leapsecs1972) + + +def utc2tai(secs): + """Add leapseconds to UTC + """ + for diff, leapsec in enumerate(Leapsecs): + if secs < leapsec: + break + secs += Leapsecs1972 + diff + if secs in Leapsecs: + secs += 1 + return secs + TAI64Base + + +def loads(v, sets=False, leapsecUTCAllow=False): + """Decode YAC-encoded data. + + :param bool sets: transform maps with NIL-only values to set()s + :param bool leapsecUTCAllow: allow TAI64 values equal to leap seconds, + to be decoded as datetime UTC value. Raw() + value will be returned instead + :returns: decoded data and the undecoded tail + :rtype: (any, bytes) + """ if len(v) == 0: raise NotEnoughData(1) if v[0] == TagEOC: @@ -260,13 +296,6 @@ def loads(v, sets=False): secs = int.from_bytes(v[1:1+8], "big") if secs >= (1 << 63): raise DecodeError("reserved TAI64 value is in use") - secs -= TAI64Base - diff = 0 - for n, leapsec in enumerate(Leapsecs): - if secs > (leapsec + len(Leapsecs) - n): - diff = 10 + len(Leapsecs) - n - break - secs -= diff nsecs = 0 if l > 8: nsecs = int.from_bytes(v[1+8:1+8+4], "big") @@ -277,6 +306,9 @@ def loads(v, sets=False): asecs = int.from_bytes(v[1+8+4:1+8+4+4], "big") if asecs > 999999999: raise DecodeError("too many attoseconds") + secs = tai2utc(secs, leapsecUTCAllow) + if secs is None: + return Raw(v[0], v[1:1+l]), v[1+l:] if (abs(secs) > (1 << 60)) or (asecs > 0) or ((nsecs % 1000) > 0): # Python can represent neither big values, nor nanoseconds return Raw(v[0], v[1:1+l]), v[1+l:] diff --git a/pyac/test_leapsecs.py b/pyac/test_leapsecs.py new file mode 100644 index 0000000..eff9435 --- /dev/null +++ b/pyac/test_leapsecs.py @@ -0,0 +1,45 @@ +from unittest import TestCase + +from pyac import Leapsecs +from pyac import TAI64Base + + +class TestLeapsecs(TestCase): + """Check that our pyac.Leapsecs is equally calculated to DJB's + ``curl http://cr.yp.to/libtai/leapsecs.dat | xxd -c 8 -p`` + """ + + def runTest(self): + for our, their in zip(Leapsecs, ( + "4000000004b2580a", + "4000000005a4ec0b", + "4000000007861f8c", + "400000000967530d", + "400000000b48868e", + "400000000d2b0b8f", + "400000000f0c3f10", + "4000000010ed7291", + "4000000012cea612", + "40000000159fca93", + "400000001780fe14", + "4000000019623195", + "400000001d25ea16", + "4000000021dae517", + "40000000259e9d98", + "40000000277fd119", + "400000002a50f59a", + "400000002c32291b", + "400000002e135c9c", + "4000000030e7241d", + "4000000033b8489e", + "40000000368c101f", + "4000000043b71ba0", + "40000000495c07a1", + "400000004fef9322", + "4000000055932da3", + "40000000586846a4", + )): + self.assertEqual( + our, + int.from_bytes(bytes.fromhex(their), "big") - TAI64Base, + ) -- 2.50.0