"""Tests for decimal utility functions.""" from decimal import Decimal import pytest from abacus_common_logic.utils.decimals import safe_round_up, to_cent class TestSafeRoundUp: """Tests for safe_round_up function.""" def test_defaults(self): """Test that default exp rounds to integers.""" assert safe_round_up(5) == Decimal('5') assert safe_round_up(5.4) == Decimal('6') assert safe_round_up(5.9) == Decimal('6') assert safe_round_up(0) == Decimal('0') assert safe_round_up(-5.4) == Decimal('-6') def test_with_int(self): """Test rounding up integers with int exp.""" assert safe_round_up(5, 0) == Decimal('5') assert safe_round_up(0, 0) == Decimal('0') assert safe_round_up(-5, 0) == Decimal('-5') def test_with_decimal_exp(self): """Test rounding up with Decimal exp.""" assert safe_round_up(1.231, Decimal('0.01')) == Decimal('1.24') assert safe_round_up(1.234, Decimal('0.01')) == Decimal('1.24') assert safe_round_up(1.235, Decimal('0.01')) == Decimal('1.24') assert safe_round_up(1.236, Decimal('0.01')) == Decimal('1.24') assert safe_round_up(1.239, Decimal('0.01')) == Decimal('1.24') def test_with_int_exp(self): """Test rounding up with int exp (decimal places).""" # 0 decimal places (integers) assert safe_round_up(4.1, 0) == Decimal('5') assert safe_round_up(4.4, 0) == Decimal('5') assert safe_round_up(4.5, 0) == Decimal('5') assert safe_round_up(4.6, 0) == Decimal('5') assert safe_round_up(4.9, 0) == Decimal('5') # 2 decimal places (cents) assert safe_round_up(1.231, 2) == Decimal('1.24') assert safe_round_up(1.234, 2) == Decimal('1.24') assert safe_round_up(1.235, 2) == Decimal('1.24') assert safe_round_up(1.236, 2) == Decimal('1.24') assert safe_round_up(1.239, 2) == Decimal('1.24') # 3 decimal places (thousandths) assert safe_round_up(1.2341, 3) == Decimal('1.235') assert safe_round_up(1.2344, 3) == Decimal('1.235') assert safe_round_up(1.2345, 3) == Decimal('1.235') assert safe_round_up(1.2346, 3) == Decimal('1.235') assert safe_round_up(1.2349, 3) == Decimal('1.235') def test_floating_point_drift(self): """Test that floating-point drift near exact values.""" barely_five = 4.000000001000000 assert safe_round_up(barely_five) == Decimal('5') assert safe_round_up(barely_five, 0) == Decimal('5') almost_five = 4.999999999999999 assert safe_round_up(almost_five) == Decimal('5') assert safe_round_up(almost_five, 0) == Decimal('5') def test_negative_numbers(self): """Test rounding up negative numbers.""" # For negative numbers, "rounding up" means toward zero # With default exp (rounds to integer) assert safe_round_up(-4.5) == Decimal('-5') assert safe_round_up(-4.9) == Decimal('-5') assert safe_round_up(-1.234) == Decimal('-2') # With explicit exp assert safe_round_up(-1.234, 2) == Decimal('-1.24') assert safe_round_up(-1.236, 2) == Decimal('-1.24') assert safe_round_up(-4.5, 0) == Decimal('-5') assert safe_round_up(-4.9, 0) == Decimal('-5') def test_zero_and_near_zero_values(self): """Test zero and near-zero values.""" # With default exp (rounds to integer) assert safe_round_up(0) == Decimal('0') assert safe_round_up(0.001) == Decimal('1') assert safe_round_up(0.0001) == Decimal('1') # With explicit exp for decimal places assert safe_round_up(0, 2) == Decimal('0.00') assert safe_round_up(0.001, 2) == Decimal('0.01') assert safe_round_up(0.0001, 2) == Decimal('0.01') assert safe_round_up(-0.001, 2) == Decimal('-0.01') def test_accepts_different_input_types(self): """Test that function accepts Decimal, float, int, and str.""" # Decimal input (using default exp) assert safe_round_up(Decimal('5.234')) == Decimal('6') assert safe_round_up(Decimal('1.234'), 2) == Decimal('1.24') # Float input (using default exp) assert safe_round_up(5.234) == Decimal('6') assert safe_round_up(1.234, 2) == Decimal('1.24') # Int input (using default exp) assert safe_round_up(5) == Decimal('5') assert safe_round_up(5, 2) == Decimal('5.00') # String input (using default exp) assert safe_round_up('5.234') == Decimal('6') assert safe_round_up('1.234', 2) == Decimal('1.24') def test_custom_tolerance(self): """Test using custom tolerance value.""" value = Decimal('1.00001') assert safe_round_up(value, 2, tolerance=Decimal('1e-2')) == Decimal('1') assert safe_round_up(value, 2, tolerance=Decimal('1e-3')) == Decimal('1') assert safe_round_up(value, 2, tolerance=Decimal('1e-4')) == Decimal('1') assert safe_round_up(value, 2, tolerance=Decimal('1e-5')) == Decimal('1.01') def test_invalid_exp_type(self): """Test that invalid exp type raises TypeError.""" with pytest.raises(TypeError, match='exp must be int or Decimal'): safe_round_up(1.234, 'invalid') # type: ignore[arg-type] with pytest.raises(TypeError, match='exp must be int or Decimal'): safe_round_up(1.234, 1.5) # type: ignore[arg-type] with pytest.raises(TypeError, match='exp must be int or Decimal'): safe_round_up(1.234, [2]) # type: ignore[arg-type] def test_precision_with_repeating_decimals(self): """Test precision with values that have repeating decimals.""" # 1/3 = 0.333... one_third = Decimal('1') / Decimal('3') assert safe_round_up(one_third, 2) == Decimal('0.34') # 2/3 = 0.666... two_thirds = Decimal('2') / Decimal('3') assert safe_round_up(two_thirds, 2) == Decimal('0.67') class TestToCent: """Tests for to_cent function.""" def test_zero(self): """Test zero value.""" assert to_cent(0) == Decimal('0.00') assert to_cent(0.0) == Decimal('0.00') def test_with_integers(self): """Test rounding up to cents.""" assert to_cent(1) == Decimal('1') assert to_cent(10) == Decimal('10') assert to_cent(1e3) == Decimal('1000') assert to_cent(1e6) == Decimal('1000000') assert to_cent(1e9) == Decimal('1000000000') def test_with_integers_negative(self): """Test rounding up to cents.""" assert to_cent(-1) == Decimal('-1') assert to_cent(-10) == Decimal('-10') assert to_cent(-1e3) == Decimal('-1000') assert to_cent(-1e6) == Decimal('-1000000') assert to_cent(-1e9) == Decimal('-1000000000') def test_with_tenths(self): """Test rounding up to cents.""" for digit in range(0, 10): assert to_cent(f'1000.{digit}') == Decimal(f'1000.{digit}') def test_with_tenths_negative(self): """Test rounding up to cents.""" for digit in range(0, 10): assert to_cent(f'-1000.{digit}') == Decimal(f'-1000.{digit}') def test_with_hundredths(self): """Test rounding up to cents.""" assert to_cent(0.001) == Decimal('0.01') assert to_cent(0.999) == Decimal('1.00') assert to_cent(1.231) == Decimal('1.24') assert to_cent(1.234) == Decimal('1.24') assert to_cent(1.451) == Decimal('1.46') assert to_cent(1.456) == Decimal('1.46') assert to_cent(1.671) == Decimal('1.68') assert to_cent(1.679) == Decimal('1.68') for digit in range(0, 10): assert to_cent(f'1000.0{digit}') == Decimal(f'1000.0{digit}') def test_with_hundredths_negative(self): """Test rounding up to cents.""" assert to_cent(-0.001) == Decimal('-0.01') assert to_cent(-0.999) == Decimal('-1.00') assert to_cent(-1.231) == Decimal('-1.24') assert to_cent(-1.234) == Decimal('-1.24') assert to_cent(-1.451) == Decimal('-1.46') assert to_cent(-1.456) == Decimal('-1.46') assert to_cent(-1.671) == Decimal('-1.68') assert to_cent(-1.679) == Decimal('-1.68') for digit in range(0, 10): assert to_cent(f'-1000.0{digit}') == Decimal(f'-1000.0{digit}') def test_with_thousandths(self): """Test rounding up to cents.""" assert to_cent('1000.000') == Decimal('1000.00') for digit in range(1, 10): assert to_cent(f'1000.00{digit}') == Decimal('1000.01') def test_with_thousandths_negative(self): """Test rounding up to cents.""" assert to_cent('-1000.000') == Decimal('-1000.00') for digit in range(1, 10): assert to_cent(f'-1000.00{digit}') == Decimal('-1000.01') def test_with_billionths(self): """Test rounding up to cents.""" assert to_cent('1000.000000000') == Decimal('1000') for digit in range(1, 10): assert to_cent(f'1000.00000000{digit}') == Decimal('1000.01') def test_with_billionths_negative(self): """Test rounding up to cents.""" assert to_cent('-1000.000000000') == Decimal('-1000') for digit in range(1, 10): assert to_cent(f'-1000.00000000{digit}') == Decimal('-1000.01') def test_handles_floating_point_drift(self): """Test floating-point drift handling for cent values.""" assert to_cent('1000.0000000000') == Decimal('1000') assert to_cent('1000.0000000001') == Decimal('1000') assert to_cent('1000.0000000004') == Decimal('1000') assert to_cent('1000.0000000005') == Decimal('1000') assert to_cent('1000.0000000006') == Decimal('1000') assert to_cent('1000.0000000009') == Decimal('1000') assert to_cent('1000.000000000999999') == Decimal('1000') assert to_cent('0.9999999999') == Decimal('1.00') assert to_cent('1.0000000001') == Decimal('1.00') assert to_cent('1.000000000999999') == Decimal('1.00') assert to_cent('1.0000000010') == Decimal('1.01') assert to_cent('1.2300000010') == Decimal('1.24') assert to_cent('1.239999999999999') == Decimal('1.24') assert to_cent('1.2400000001') == Decimal('1.24') assert to_cent('1.240000001') == Decimal('1.25') def test_handles_floating_point_drift_negative(self): """Test floating-point drift handling for cent values.""" assert to_cent('-1000.0000000000') == Decimal('-1000') assert to_cent('-1000.0000000001') == Decimal('-1000') assert to_cent('-1000.0000000004') == Decimal('-1000') assert to_cent('-1000.0000000005') == Decimal('-1000') assert to_cent('-1000.0000000006') == Decimal('-1000') assert to_cent('-1000.0000000009') == Decimal('-1000') assert to_cent('-1000.000000000999999') == Decimal('-1000') assert to_cent('-0.9999999999') == Decimal('-1.00') assert to_cent('-1.0000000001') == Decimal('-1.00') assert to_cent('-1.000000000999999') == Decimal('-1.00') assert to_cent('-1.0000000010') == Decimal('-1.01') assert to_cent('-1.2300000010') == Decimal('-1.24') assert to_cent('-1.239999999999999') == Decimal('-1.24') assert to_cent('-1.2400000001') == Decimal('-1.24') assert to_cent('-1.240000001') == Decimal('-1.25') def test_accepts_different_input_types_for_cents(self): """Test different input types for to_cent.""" assert to_cent(Decimal('1.234')) == Decimal('1.24') assert to_cent(1.234) == Decimal('1.24') assert to_cent(-5) == Decimal('-5.00') assert to_cent('-1.234') == Decimal('-1.24') def test_large_currency_amounts(self): """Test large monetary amounts.""" assert to_cent(1234567.891) == Decimal('1234567.90') assert to_cent(9999999.999) == Decimal('10000000.00')