Skip to content

Commit 8544bb4

Browse files
fix(decimal): use minimal byte length for negative powers of two (#3746)
* fix(decimal): use minimal byte length for negative powers of two * test(decimal): add positive Decimal case to bytes_required * test(decimal): cover negative boundary bucketing --------- Co-authored-by: Kevin Liu <kevin.jq.liu@gmail.com>
1 parent e43be1e commit 8544bb4

3 files changed

Lines changed: 29 additions & 5 deletions

File tree

pyiceberg/utils/decimal.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,16 @@ def bytes_required(value: int | Decimal) -> int:
5858
int: the minimum number of bytes needed to serialize the value.
5959
"""
6060
if isinstance(value, int):
61-
return (value.bit_length() + 8) // 8
61+
unscaled = value
6262
elif isinstance(value, Decimal):
63-
return (decimal_to_unscaled(value).bit_length() + 8) // 8
64-
65-
raise ValueError(f"Unsupported value: {value}")
63+
unscaled = decimal_to_unscaled(value)
64+
else:
65+
raise ValueError(f"Unsupported value: {value}")
66+
67+
# bit_length() ignores the sign, so -128 appears to need 9 signed bits instead of 8.
68+
# Adding 1 before counting avoids the extra byte at negative boundaries.
69+
num_bits = unscaled.bit_length() if unscaled >= 0 else (unscaled + 1).bit_length()
70+
return (num_bits + 8) // 8
6671

6772

6873
def decimal_to_bytes(value: Decimal, byte_length: int | None = None) -> bytes:

tests/test_transforms.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ def test_bucket_hash_values(test_input: Any, test_type: PrimitiveType, expected:
187187
(BucketTransform(100).transform(TimeType()), 81068000000, 59),
188188
(BucketTransform(100).transform(TimestampType()), 1510871468000000, 7),
189189
(BucketTransform(100).transform(DecimalType(9, 2)), Decimal("14.20"), 59),
190+
(BucketTransform(16).transform(DecimalType(5, 2)), Decimal("-1.28"), 13),
190191
(BucketTransform(100).transform(StringType()), "iceberg", 89),
191192
(
192193
BucketTransform(100).transform(UUIDType()),

tests/utils/test_decimal.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919
import pytest
2020

21-
from pyiceberg.utils.decimal import decimal_required_bytes, decimal_to_bytes
21+
from pyiceberg.utils.decimal import bytes_required, decimal_required_bytes, decimal_to_bytes
2222

2323

2424
def test_decimal_required_bytes() -> None:
@@ -42,8 +42,26 @@ def test_decimal_required_bytes() -> None:
4242
assert "(0, 40]" in str(exc_info.value)
4343

4444

45+
def test_bytes_required() -> None:
46+
assert bytes_required(0) == 1
47+
assert bytes_required(127) == 1
48+
assert bytes_required(128) == 2
49+
# Check negative signed-byte boundaries and their neighbors.
50+
assert bytes_required(-127) == 1
51+
assert bytes_required(-128) == 1
52+
assert bytes_required(-129) == 2
53+
assert bytes_required(-32768) == 2
54+
assert bytes_required(-8388608) == 3
55+
assert bytes_required(Decimal("1.27")) == 1
56+
assert bytes_required(Decimal("-1.28")) == 1
57+
assert bytes_required(Decimal("-327.68")) == 2
58+
59+
4560
def test_decimal_to_bytes() -> None:
4661
# Check the boundary between 2 and 3 bytes.
4762
# 2 bytes has a minimum of -32,768 and a maximum value of 32,767 (inclusive).
4863
assert decimal_to_bytes(Decimal("32767.")) == b"\x7f\xff"
4964
assert decimal_to_bytes(Decimal("32768.")) == b"\x00\x80\x00"
65+
# Unscaled values -128 and -32768 require no sign padding.
66+
assert decimal_to_bytes(Decimal("-1.28")) == b"\x80"
67+
assert decimal_to_bytes(Decimal("-327.68")) == b"\x80\x00"

0 commit comments

Comments
 (0)