-
Notifications
You must be signed in to change notification settings - Fork 609
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(datatypes): decimal normalization failed for integers
- Loading branch information
Showing
8 changed files
with
166 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
from __future__ import annotations | ||
|
||
from decimal import Context, Decimal, InvalidOperation | ||
|
||
|
||
def normalize_decimal(value, precision: int | None = None, scale: int | None = None): | ||
context = Context(prec=38 if precision is None else precision) | ||
|
||
try: | ||
if isinstance(value, float): | ||
out = Decimal(str(value)) | ||
else: | ||
out = Decimal(value) | ||
except InvalidOperation: | ||
raise TypeError(f"Unable to construct decimal from {value!r}") | ||
|
||
out = out.normalize(context=context) | ||
components = out.as_tuple() | ||
n_digits = len(components.digits) | ||
exponent = components.exponent | ||
|
||
if precision is not None and precision < n_digits: | ||
raise TypeError( | ||
f"Decimal value {value} has too many digits for precision: {precision}" | ||
) | ||
|
||
if scale is not None: | ||
if exponent < -scale: | ||
raise TypeError( | ||
f"Normalizing {value} with scale {exponent} to scale -{scale} " | ||
"would loose precision" | ||
) | ||
|
||
other = Decimal(10) ** -scale | ||
try: | ||
out = out.quantize(other, context=context) | ||
except InvalidOperation: | ||
raise TypeError( | ||
f"Unable to normalize {value!r} as decimal with precision {precision} " | ||
f"and scale {scale}" | ||
) | ||
|
||
return out |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
from __future__ import annotations | ||
|
||
from decimal import Context, localcontext | ||
from decimal import Decimal as D | ||
|
||
import pytest | ||
|
||
from ibis.common.numeric import normalize_decimal | ||
|
||
|
||
@pytest.mark.parametrize( | ||
("value", "precision", "scale", "expected"), | ||
[ | ||
(1, None, None, D("1")), | ||
(1.0, None, None, D("1.0")), | ||
(1.0, 2, None, D("1.0")), | ||
(1.0, 2, 1, D("1.0")), | ||
(1.0, 3, 2, D("1.0")), | ||
(1.0, 3, 1, D("1.0")), | ||
(1.0, 3, 0, D("1")), | ||
(1.0, 2, 0, D("1")), | ||
(1.0, 1, 0, D("1")), | ||
(3.14, 3, 2, D("3.14")), | ||
(3.14, 10, 2, D("3.14")), | ||
(3.14, 10, 3, D("3.14")), | ||
(3.14, 10, 4, D("3.14")), | ||
(1234.567, 10, 4, D("1234.567")), | ||
(1234.567, 10, 3, D("1234.567")), | ||
], | ||
) | ||
def test_normalize_decimal(value, precision, scale, expected): | ||
assert normalize_decimal(value, precision, scale) == expected | ||
|
||
|
||
@pytest.mark.parametrize( | ||
("value", "precision", "scale"), | ||
[ | ||
(1.0, 2, 2), | ||
(1.0, 1, 1), | ||
(D("1.1234"), 5, 3), | ||
(D("1.1234"), 4, 2), | ||
(D("23145"), 4, 2), | ||
(1234.567, 10, 2), | ||
(1234.567, 10, 1), | ||
(3.14, 10, 0), | ||
(3.14, 3, 0), | ||
(3.14, 3, 1), | ||
(3.14, 10, 1), | ||
], | ||
) | ||
def test_normalize_failing(value, precision, scale): | ||
with pytest.raises(TypeError): | ||
normalize_decimal(value, precision, scale) | ||
|
||
|
||
def test_normalize_decimal_dont_truncate_precision(): | ||
# test that the decimal context is ignored, 38 is the default precision | ||
for prec in [10, 30, 38]: | ||
with localcontext(Context(prec=prec)): | ||
v = "1.123456789" | ||
assert str(normalize_decimal(v + "0000")) == "1.123456789" | ||
|
||
v = v + "1" * 28 | ||
assert len(v) == 39 | ||
assert str(normalize_decimal(v)) == v | ||
|
||
# if no precision is specified, we use precision 38 for dec.normalize() | ||
v = v + "1" | ||
assert len(v) == 40 | ||
assert str(normalize_decimal(v)) == v[:-1] | ||
|
||
# pass the precision explicitly | ||
assert str(normalize_decimal(v, precision=39)) == v | ||
|
||
v = v + "1" * 11 | ||
assert len(v) == 51 | ||
assert str(normalize_decimal(v, precision=50)) == v | ||
assert str(normalize_decimal(v, precision=45)) == v[:-5] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters