Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 12 additions & 7 deletions pyiceberg/expressions/literals.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
import struct
from abc import ABC, abstractmethod
from datetime import date, datetime, time
from decimal import ROUND_HALF_UP, Decimal
from decimal import ROUND_HALF_UP, Decimal, getcontext, localcontext
from functools import singledispatchmethod
from math import isfinite, isnan
from typing import Any, Generic
Expand Down Expand Up @@ -371,6 +371,15 @@ def _(self, type_var: DecimalType) -> Literal[Decimal]:
return DecimalLiteral(Decimal((sign, digits + zeros, -type_var.scale)))


def _float_to_decimal(value: float, type_var: DecimalType) -> Decimal:
if not isfinite(value):
raise ValueError(f"Could not convert {value} into a {type_var}, value is not finite")
exact = Decimal(value)
# Quantize needs a context precision that covers every digit of the result, which can exceed the default 28 digits
with localcontext(prec=max(getcontext().prec, exact.adjusted() + type_var.scale + 2)):
return exact.quantize(Decimal((0, (1,), -type_var.scale)), rounding=ROUND_HALF_UP)


class FloatLiteral(Literal[float]):
def __init__(self, value: float) -> None:
super().__init__(value, float)
Expand Down Expand Up @@ -414,9 +423,7 @@ def _(self, _: DoubleType) -> Literal[float]:

@to.register(DecimalType)
def _(self, type_var: DecimalType) -> Literal[Decimal]:
if not isfinite(self.value):
raise ValueError(f"Could not convert {self.value} into a {type_var}, value is not finite")
return DecimalLiteral(Decimal(self.value).quantize(Decimal((0, (1,), -type_var.scale)), rounding=ROUND_HALF_UP))
return DecimalLiteral(_float_to_decimal(self.value, type_var))


class DoubleLiteral(Literal[float]):
Expand All @@ -441,9 +448,7 @@ def _(self, _: FloatType) -> Literal[float]:

@to.register(DecimalType)
def _(self, type_var: DecimalType) -> Literal[Decimal]:
if not isfinite(self.value):
raise ValueError(f"Could not convert {self.value} into a {type_var}, value is not finite")
return DecimalLiteral(Decimal(self.value).quantize(Decimal((0, (1,), -type_var.scale)), rounding=ROUND_HALF_UP))
return DecimalLiteral(_float_to_decimal(self.value, type_var))


class DateLiteral(Literal[int]):
Expand Down
10 changes: 10 additions & 0 deletions tests/expressions/test_literals.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,16 @@ def test_double_to_decimal_conversion_with_infinity() -> None:
assert "Could not convert inf into a decimal(9, 2), value is not finite" in str(e.value)


def test_float_to_decimal_conversion_beyond_default_context_precision() -> None:
# The default decimal context keeps 28 digits, fewer than a decimal(38, s) value can have
for value, decimal_type, expected in [
(2.0**87, DecimalType(38, 2), "154742504910672534362390528.00"),
(0.5, DecimalType(38, 30), "0.500000000000000000000000000000"),
]:
for lit in (FloatLiteral(value), DoubleLiteral(value)):
assert lit.to(decimal_type).value.as_tuple() == Decimal(expected).as_tuple(), (lit, decimal_type)


def test_decimal_to_decimal_conversion() -> None:
lit = literal(Decimal("34.11").quantize(Decimal(".01")))

Expand Down
Loading