Skip to content
Merged
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
7 changes: 6 additions & 1 deletion src/moneyx/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -444,7 +444,12 @@ def from_json(cls: Type[T], json_str: str) -> T:

return from_json(cls, json_str)

def _validate_precision(self, amount: Decimal, currency: Currency, rounding_mode: RoundingModeStr) -> None:
def _validate_precision(
self,
amount: Decimal,
currency: Currency,
rounding_mode: RoundingModeStr,
) -> None:
"""
Validate that the amount has the correct precision for the currency.

Expand Down
65 changes: 50 additions & 15 deletions tests/test_edge_cases.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,34 +83,69 @@ def test_half_odd_rounding(self):
def test_special_decimal_cases(self):
"""Test special cases for decimal rounding."""
# Test already correctly rounded values
assert apply_rounding(Decimal("2.00"), RoundingMode.HALF_UP, 2) == Decimal("2.00")
assert apply_rounding(Decimal("2.00"), RoundingMode.HALF_UP, 2) == Decimal(
"2.00",
)
assert apply_rounding(Decimal("2"), RoundingMode.HALF_UP, 0) == Decimal("2")

# Test special cases with non-int exponents (NaN, Infinity) - we just want to ensure no errors
# and proper handling - these should fall back to 0 decimal places as a safe default
try:
special_decimal = Decimal("NaN")
result = apply_rounding(special_decimal, RoundingMode.HALF_UP, 2)
# Just call the function without assigning to a variable we don't use
apply_rounding(special_decimal, RoundingMode.HALF_UP, 2)
# We don't need to assert the exact result, just that it handled the case
except Exception:
pytest.fail("apply_rounding should handle special Decimal values")

def test_half_towards_zero_rounding(self):
"""Test the HALF_TOWARDS_ZERO rounding mode."""
# Test with exactly 0.5 - should round towards zero
assert apply_rounding(Decimal("2.5"), RoundingMode.HALF_TOWARDS_ZERO, 0) == Decimal("2")
assert apply_rounding(Decimal("-2.5"), RoundingMode.HALF_TOWARDS_ZERO, 0) == Decimal("-2")

assert apply_rounding(
Decimal("2.5"),
RoundingMode.HALF_TOWARDS_ZERO,
0,
) == Decimal("2")
assert apply_rounding(
Decimal("-2.5"),
RoundingMode.HALF_TOWARDS_ZERO,
0,
) == Decimal("-2")

# Test with non-half values - should use HALF_UP
assert apply_rounding(Decimal("2.4"), RoundingMode.HALF_TOWARDS_ZERO, 0) == Decimal("2")
assert apply_rounding(Decimal("2.6"), RoundingMode.HALF_TOWARDS_ZERO, 0) == Decimal("3")

assert apply_rounding(
Decimal("2.4"),
RoundingMode.HALF_TOWARDS_ZERO,
0,
) == Decimal("2")
assert apply_rounding(
Decimal("2.6"),
RoundingMode.HALF_TOWARDS_ZERO,
0,
) == Decimal("3")

def test_half_away_from_zero_rounding(self):
"""Test the HALF_AWAY_FROM_ZERO rounding mode."""
# Test with exactly 0.5 - should round away from zero
assert apply_rounding(Decimal("2.5"), RoundingMode.HALF_AWAY_FROM_ZERO, 0) == Decimal("3")
assert apply_rounding(Decimal("-2.5"), RoundingMode.HALF_AWAY_FROM_ZERO, 0) == Decimal("-3")

assert apply_rounding(
Decimal("2.5"),
RoundingMode.HALF_AWAY_FROM_ZERO,
0,
) == Decimal("3")
assert apply_rounding(
Decimal("-2.5"),
RoundingMode.HALF_AWAY_FROM_ZERO,
0,
) == Decimal("-3")

# Test with non-half values - should use HALF_UP
assert apply_rounding(Decimal("2.4"), RoundingMode.HALF_AWAY_FROM_ZERO, 0) == Decimal("2")
assert apply_rounding(Decimal("2.6"), RoundingMode.HALF_AWAY_FROM_ZERO, 0) == Decimal("3")
assert apply_rounding(
Decimal("2.4"),
RoundingMode.HALF_AWAY_FROM_ZERO,
0,
) == Decimal("2")
assert apply_rounding(
Decimal("2.6"),
RoundingMode.HALF_AWAY_FROM_ZERO,
0,
) == Decimal("3")
24 changes: 13 additions & 11 deletions tests/test_missing_coverage.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
from moneyx import Money
from moneyx.bulk import bulk_add, bulk_allocate, bulk_multiply, bulk_with_tax
from moneyx.exceptions import SerializationError
from moneyx.serialization import from_dict, from_json, to_json
from moneyx.rounding import RoundingMode
from moneyx.serialization import from_dict, from_json, to_json


class TestComparisons:
Expand Down Expand Up @@ -357,24 +357,24 @@ def test_distribute_remainder_negative(self):
m = Money("10.00", "USD")
amounts = [Decimal("3.33"), Decimal("3.33"), Decimal("3.34")]
unit = Decimal("0.01")

# Call the internal method directly with is_positive=False
m._distribute_remainder(amounts, 0, False, unit)

# Check that the amount was decreased by the unit
assert amounts[0] == Decimal("3.32")
assert amounts[1] == Decimal("3.33")
assert amounts[2] == Decimal("3.34")

def test_distribute_remainder_positive(self):
"""Test distributing positive remainders to amounts."""
m = Money("10.00", "USD")
amounts = [Decimal("3.33"), Decimal("3.33"), Decimal("3.33")]
unit = Decimal("0.01")

# Call the internal method directly with is_positive=True
m._distribute_remainder(amounts, 0, True, unit)

# Check that the amount was increased by the unit
assert amounts[0] == Decimal("3.34")
assert amounts[1] == Decimal("3.33")
Expand All @@ -383,28 +383,30 @@ def test_distribute_remainder_positive(self):

class TestValidatePrecision:
"""Test the _validate_precision method directly to increase coverage."""

def test_non_int_exponent_handling(self):
"""Test _validate_precision with non-integer exponents."""
from decimal import Decimal

from moneyx.core import Money
from moneyx.rounding import RoundingMode


m = Money("10.00", "USD")

# Create a Decimal with a non-int exponent
# We'll mock a non-int exponent by adding a patch
class MockDecimal(Decimal):
def as_tuple(self):
result = super().as_tuple()

# Return an object with a non-int exponent attribute
class Tuple:
def __init__(self, sign, digits, exponent):
self.sign = sign
self.digits = digits
self.exponent = "n" # Non-int exponent

return Tuple(result.sign, result.digits, "n")

# Test the method directly
amount = MockDecimal("123.456")
m._validate_precision(amount, m.currency, RoundingMode.HALF_UP)
Expand Down
17 changes: 12 additions & 5 deletions tests/test_serialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,20 @@ def test_from_dict_invalid_types(self):

# Test with invalid currency type
with pytest.raises(SerializationError) as exc_info:
data = {"amount": "10.00", "currency": 123} # Integer is not a valid currency
data = {
"amount": "10.00",
"currency": 123,
} # Integer is not a valid currency
from_dict(Money, data)
assert "Currency code must be string" in str(exc_info.value)

# Test with invalid rounding type
with pytest.raises(SerializationError) as exc_info:
data = {"amount": "10.00", "currency": "USD", "rounding": 123} # Integer is not valid rounding
data = {
"amount": "10.00",
"currency": "USD",
"rounding": 123,
} # Integer is not valid rounding
from_dict(Money, data)
assert "Rounding mode must be string" in str(exc_info.value)

Expand Down Expand Up @@ -189,17 +196,17 @@ def test_money_from_json_method(self):
def test_from_dict_money_creation_error(self):
"""Test from_dict error handling for Money creation failure."""
from moneyx.exceptions import SerializationError

# This will fail when creating the Money object because "XYZ" is not a valid currency
with pytest.raises(SerializationError) as exc_info:
data = {"amount": "10.00", "currency": "XYZ", "rounding": "HALF_UP"}
from_dict(Money, data)
assert "Failed to create Money object" in str(exc_info.value)

def test_invalid_rounding_mode(self):
"""Test from_dict with invalid rounding mode."""
from moneyx.exceptions import SerializationError

with pytest.raises(SerializationError) as exc_info:
data = {"amount": "10.00", "currency": "USD", "rounding": "INVALID_MODE"}
from_dict(Money, data)
Expand Down