Add tlv.FloatMember

Adds a FloatMember that encodes LE floats/doubles as specified in A.11.5
This commit is contained in:
Chris Wesseling 2024-07-22 22:50:24 +02:00
parent 6d7ee584e6
commit 4bd35adc5a
2 changed files with 57 additions and 6 deletions

View file

@ -407,14 +407,46 @@ class IntMember(NumberMember[int, _OPT, _NULLABLE]):
nullable: _NULLABLE = False,
**kwargs,
):
"""
:param octets: Number of octests to use for encoding.
1, 2, 4, 8 are 8, 16, 32, and 64 bits respectively
:param optional: Indicates whether the value MAY be omitted from the encoding.
Can be used for deprecation.
:param nullable: Indicates whether a TLV Null MAY be encoded in place of a value.
"""
# TODO 7.18.1 mentions other bit lengths (that are not a power of 2) than the TLV Appendix
uformat = INT_SIZE[int(math.log2(octets))]
# little-endian
# < = little-endian
self.format = f"<{uformat.lower() if signed else uformat}"
super().__init__(
tag, _format=self.format, optional=optional, nullable=nullable, **kwargs
)
class FloatMember(NumberMember[float, _OPT, _NULLABLE]):
def __init__(
self,
tag,
*,
octets: Literal[4, 8] = 4,
optional: _OPT = False,
nullable: _NULLABLE = False,
**kwargs,
):
"""
:param octets: Number of octests to use for encoding.
4, 8 are single and double precision floats respectively.
:param optional: Indicates whether the value MAY be omitted from the encoding.
Can be used for deprecation.
:param nullable: Indicates whether a TLV Null MAY be encoded in place of a value.
"""
# < = little-endian
self.format = f"<{'f' if octets == 4 else 'd'}"
super().__init__(
tag, _format=self.format, optional=optional, nullable=nullable, **kwargs
)
class BoolMember(Member[bool, _OPT, _NULLABLE]):
max_value_length = 0

View file

@ -338,11 +338,11 @@ class TestNull:
# Double precision floating point negative infinity 0b 00 00 00 00 00 00 f0 ff
# (-∞)
class FloatSingle(tlv.TLVStructure):
f = tlv.NumberMember(None, "f")
f = tlv.FloatMember(None)
class FloatDouble(tlv.TLVStructure):
f = tlv.NumberMember(None, "d")
f = tlv.FloatMember(None, octets=8)
class TestFloatSingle:
@ -398,12 +398,12 @@ class TestFloatSingle:
assert s.encode().tobytes() == b"\x0a\x00\x00\x80\xff"
@given(v=...)
def test_roundtrip(self, v: float):
s = FloatSingle()
def test_roundtrip_double(self, v: float):
s = FloatDouble()
s.f = v
buffer = s.encode().tobytes()
s2 = FloatSingle(buffer)
s2 = FloatDouble(buffer)
assert (
(math.isnan(s.f) and math.isnan(s2.f))
@ -412,6 +412,25 @@ class TestFloatSingle:
or math.isclose(s2.f, s.f, rel_tol=1e-7, abs_tol=1e-9)
)
@given(
v=st.floats(
# encoding to LE float32 raises OverflowError outside these ranges
# TODO: should we raise ValueError with a bounds check or encode -inf/inf?
min_value=(2**-126),
max_value=(2 - 2**-23) * 2**127,
),
)
def test_roundtrip_single(self, v: float):
s = FloatSingle()
s.f = v
buffer = s.encode().tobytes()
s2 = FloatSingle(buffer)
assert (math.isnan(s.f) and math.isnan(s2.f)) or math.isclose(
s2.f, s.f, rel_tol=1e-7, abs_tol=1e-9
)
class TestFloatDouble:
def test_precision_float_0_0_decode(self):