From 3885b309fd6b305334b99a823663f257c37151eb Mon Sep 17 00:00:00 2001 From: ZHU Yuhao Date: Sat, 28 Feb 2026 17:37:31 +0100 Subject: [PATCH 1/5] same_quantize --- docs/plans/api_roadmap.md | 18 +++++----- src/decimo/bigdecimal/bigdecimal.mojo | 24 ++++++++++++-- tests/bigdecimal/test_bigdecimal_methods.mojo | 33 +++++++++++++++++++ 3 files changed, 64 insertions(+), 11 deletions(-) diff --git a/docs/plans/api_roadmap.md b/docs/plans/api_roadmap.md index ac4b97ce..415abea1 100644 --- a/docs/plans/api_roadmap.md +++ b/docs/plans/api_roadmap.md @@ -87,11 +87,11 @@ These are the gaps vs Python's `decimal.Decimal`, prioritized by user impact. | Method | What It Does | Notes | | ----------------------------------- | -------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | | `as_tuple()` | Returns `(sign, digits, exponent)` | ✓ **DONE** — returns `(sign: Bool, digits: List[UInt8], exponent: Int)` matching Python's `DecimalTuple` | -| `adjusted()` | Returns adjusted exponent (= exponent + len(digits) - 1) | ✅ **DONE** — renamed from former `exponent()` method. | -| `copy_abs()` | Returns `abs(self)` | ✅ **DONE** — alias for `__abs__()`. | -| `copy_negate()` | Returns `-self` | ✅ **DONE** — alias for `__neg__()`. | -| `copy_sign(other)` | Returns self with the sign of other | ✅ **DONE**. | -| `same_quantum(other)` | True if both have same exponent/scale | Useful for financial code. | +| `adjusted()` | Returns adjusted exponent (= exponent + len(digits) - 1) | ✓ **DONE** — renamed from former `exponent()` method. | +| `copy_abs()` | Returns `abs(self)` | ✓ **DONE** — alias for `__abs__()`. | +| `copy_negate()` | Returns `-self` | ✓ **DONE** — alias for `__neg__()`. | +| `copy_sign(other)` | Returns self with the sign of other | ✓ **DONE**. | +| `same_quantum(other)` | True if both have same exponent/scale | ✓ **DONE**. | | `normalize()` | Already exists | ✓ | | `to_eng_string()` | Engineering notation (exponent multiple of 3) | ✓ **DONE** — alias for `to_string(engineering=True)` | | `to_integral_value(rounding)` | Round to integer, keep as Decimal | `round(ndigits=0)` is close but not identical (doesn't strip trailing zeros). | @@ -294,9 +294,9 @@ BigInt has `to_string_with_separators()`. This should be extended to BigDecimal. ### Tier 2: Important (Remaining) 1. ✓ **`as_tuple()`** on BigDecimal — returns `(sign: Bool, digits: List[UInt8], exponent: Int)` matching Python's `DecimalTuple` -2. ✅ **`copy_abs()` / `copy_negate()` / `copy_sign(other)`** on BigDecimal -3. ✅ **`adjusted()`** on BigDecimal — renamed from `exponent()` to match Python's `Decimal.adjusted()` -4. **`same_quantum(other)`** on BigDecimal +2. ✓ **`copy_abs()` / `copy_negate()` / `copy_sign(other)`** on BigDecimal +3. ✓ **`adjusted()`** on BigDecimal — renamed from `exponent()` to match Python's `Decimal.adjusted()` +4. ✓ **`same_quantum(other)`** on BigDecimal 5. **`ROUND_HALF_DOWN`** rounding mode 6. **`scaleb(n)`** on BigDecimal — multiply by 10^n efficiently 7. **`bit_count()`** on BigInt — popcount (number of 1-bits in abs value) @@ -354,6 +354,6 @@ For tracking against the above: ✗ max_mag ✓ min ✗ min_mag ✗ next_minus ✗ next_plus ✗ next_toward ✓ normalize ✗ number_class ✓ quantize ✗ radix ✗ remainder_near ✗ rotate -✗ same_quantum ✗ scaleb ✗ shift ✓ sqrt +✓ same_quantum ✗ scaleb ✗ shift ✓ sqrt ✓ to_eng_string ✗ to_integral_exact ✗ to_integral_value ``` diff --git a/src/decimo/bigdecimal/bigdecimal.mojo b/src/decimo/bigdecimal/bigdecimal.mojo index b766b9d3..dc55dab4 100644 --- a/src/decimo/bigdecimal/bigdecimal.mojo +++ b/src/decimo/bigdecimal/bigdecimal.mojo @@ -1355,8 +1355,7 @@ struct BigDecimal( # ===------------------------------------------------------------------=== # fn adjusted(self) -> Int: - """Returns the adjusted exponent, matching Python's - `decimal.Decimal.adjusted()`. + """Returns the adjusted exponent. This is the exponent of the number when written with a single leading digit in scientific notation. Equivalently, @@ -1460,6 +1459,27 @@ struct BigDecimal( sign=other.sign, ) + @always_inline + fn same_quantum(self, other: Self) -> Bool: + """Returns True if both operands have the same scale (exponent). + + Matches Python's `decimal.Decimal.same_quantum(other)`. Two numbers + are in the same quantum when they have the same scale, meaning they + are expressed with the same number of decimal places. + + Args: + other: The BigDecimal to compare quantum with. + + Examples: + + ``` + BigDecimal("1.23").same_quantum(BigDecimal("4.56")) # True (both scale=2) + BigDecimal("1.2").same_quantum(BigDecimal("4.56")) # False (scale 1 vs 2) + BigDecimal("100").same_quantum(BigDecimal("1")) # True (both scale=0) + ``` + """ + return self.scale == other.scale + fn extend_precision(self, precision_diff: Int) -> Self: """Returns a number with additional decimal places (trailing zeros). This multiplies the coefficient by 10^precision_diff and increases diff --git a/tests/bigdecimal/test_bigdecimal_methods.mojo b/tests/bigdecimal/test_bigdecimal_methods.mojo index f3cc2540..a2b0e98e 100644 --- a/tests/bigdecimal/test_bigdecimal_methods.mojo +++ b/tests/bigdecimal/test_bigdecimal_methods.mojo @@ -7,6 +7,7 @@ Tests for BigDecimal utility methods added in v0.8.x: - as_tuple() - copy_abs() / copy_negate() / copy_sign() - adjusted() + - same_quantum() """ import testing @@ -434,5 +435,37 @@ fn test_adjusted_scientific() raises: testing.assert_equal(BigDecimal("1.23E+10").adjusted(), 10) +# ===----------------------------------------------------------------------=== # +# same_quantum() +# ===----------------------------------------------------------------------=== # + + +fn test_same_quantum_same_scale() raises: + """Same scale returns True.""" + testing.assert_true(BigDecimal("1.23").same_quantum(BigDecimal("4.56"))) + testing.assert_true(BigDecimal("100").same_quantum(BigDecimal("1"))) + testing.assert_true(BigDecimal("0").same_quantum(BigDecimal("5"))) + + +fn test_same_quantum_different_scale() raises: + """Different scale returns False.""" + testing.assert_false(BigDecimal("1.2").same_quantum(BigDecimal("4.56"))) + testing.assert_false(BigDecimal("1").same_quantum(BigDecimal("1.0"))) + testing.assert_false(BigDecimal("0").same_quantum(BigDecimal("0.00"))) + + +fn test_same_quantum_negative() raises: + """Sign does not affect quantum comparison.""" + testing.assert_true(BigDecimal("1.23").same_quantum(BigDecimal("-4.56"))) + testing.assert_true(BigDecimal("-1.23").same_quantum(BigDecimal("4.56"))) + + +fn test_same_quantum_zero_variants() raises: + """Zeros with different scales have different quanta.""" + testing.assert_true(BigDecimal("0").same_quantum(BigDecimal("0"))) + testing.assert_true(BigDecimal("0.00").same_quantum(BigDecimal("0.00"))) + testing.assert_false(BigDecimal("0").same_quantum(BigDecimal("0.0"))) + + fn main() raises: testing.TestSuite.discover_tests[__functions_in_module()]().run() From fa2bbeb84af6171b6c41156a42955fc31249b185 Mon Sep 17 00:00:00 2001 From: ZHU Yuhao Date: Sat, 28 Feb 2026 17:49:37 +0100 Subject: [PATCH 2/5] Add round_half_down rounding mode --- docs/plans/api_roadmap.md | 4 +- src/decimo/__init__.mojo | 1 + src/decimo/bigdecimal/rounding.mojo | 4 +- src/decimo/biguint/biguint.mojo | 8 ++ src/decimo/rounding_mode.mojo | 11 +++ .../bigdecimal/test_bigdecimal_rounding.mojo | 31 ++++++ .../test_data/bigdecimal_rounding.toml | 97 +++++++++++++++++++ 7 files changed, 152 insertions(+), 4 deletions(-) diff --git a/docs/plans/api_roadmap.md b/docs/plans/api_roadmap.md index 415abea1..e7979fc4 100644 --- a/docs/plans/api_roadmap.md +++ b/docs/plans/api_roadmap.md @@ -165,7 +165,7 @@ These are the gaps vs Python's `decimal.Decimal`, prioritized by user impact. | Mode | Python Name | Description | Priority | | ----------------- | ----------------- | ---------------------- | ------------------------------------- | -| `ROUND_HALF_DOWN` | `ROUND_HALF_DOWN` | Round ties toward zero | **MEDIUM** — less common but expected | +| `ROUND_HALF_DOWN` | `ROUND_HALF_DOWN` | Round ties toward zero | ✓ Implemented | | `ROUND_05UP` | `ROUND_05UP` | Special IEEE rounding | **LOW** — rarely used | --- @@ -297,7 +297,7 @@ BigInt has `to_string_with_separators()`. This should be extended to BigDecimal. 2. ✓ **`copy_abs()` / `copy_negate()` / `copy_sign(other)`** on BigDecimal 3. ✓ **`adjusted()`** on BigDecimal — renamed from `exponent()` to match Python's `Decimal.adjusted()` 4. ✓ **`same_quantum(other)`** on BigDecimal -5. **`ROUND_HALF_DOWN`** rounding mode +5. ✓ **`ROUND_HALF_DOWN`** rounding mode 6. **`scaleb(n)`** on BigDecimal — multiply by 10^n efficiently 7. **`bit_count()`** on BigInt — popcount (number of 1-bits in abs value) 8. **`__float__()`** on BigInt — `float(n)` interop diff --git a/src/decimo/__init__.mojo b/src/decimo/__init__.mojo index bea2cfca..c906185a 100644 --- a/src/decimo/__init__.mojo +++ b/src/decimo/__init__.mojo @@ -33,6 +33,7 @@ from .rounding_mode import ( RoundingMode, ROUND_DOWN, ROUND_HALF_UP, + ROUND_HALF_DOWN, ROUND_HALF_EVEN, ROUND_UP, ROUND_CEILING, diff --git a/src/decimo/bigdecimal/rounding.mojo b/src/decimo/bigdecimal/rounding.mojo index e50bafbc..6c50a0ab 100644 --- a/src/decimo/bigdecimal/rounding.mojo +++ b/src/decimo/bigdecimal/rounding.mojo @@ -40,6 +40,7 @@ fn round( RoundingMode.ROUND_DOWN: Round toward zero. RoundingMode.ROUND_UP: Round away from zero. RoundingMode.ROUND_HALF_UP: Round half away from zero. + RoundingMode.ROUND_HALF_DOWN: Round half toward zero. RoundingMode.ROUND_HALF_EVEN: Round half to even (banker's). RoundingMode.ROUND_CEILING: Round toward positive infinity. RoundingMode.ROUND_FLOOR: Round toward negative infinity. @@ -126,8 +127,7 @@ fn round_to_precision( rounding_mode: Rounding mode to use. RoundingMode.ROUND_DOWN: Round toward zero. RoundingMode.ROUND_UP: Round away from zero. - RoundingMode.ROUND_HALF_UP: Round half away from zero. - RoundingMode.ROUND_HALF_EVEN: Round half to even (banker's). + RoundingMode.ROUND_HALF_UP: Round half away from zero. RoundingMode.ROUND_HALF_DOWN: Round half toward zero. RoundingMode.ROUND_HALF_EVEN: Round half to even (banker's). RoundingMode.ROUND_CEILING: Round toward +∞. RoundingMode.ROUND_FLOOR: Round toward -∞. remove_extra_digit_due_to_rounding: If True, remove a trailing digit if diff --git a/src/decimo/biguint/biguint.mojo b/src/decimo/biguint/biguint.mojo index ff3c717d..72fef094 100644 --- a/src/decimo/biguint/biguint.mojo +++ b/src/decimo/biguint/biguint.mojo @@ -1893,6 +1893,14 @@ struct BigUInt( elif rounding_mode == RoundingMode.half_up(): if self.ith_digit(ndigits - 1) >= 5: round_up = True + elif rounding_mode == RoundingMode.half_down(): + var cut_off_digit = self.ith_digit(ndigits - 1) + if cut_off_digit > 5: + round_up = True + elif cut_off_digit == 5: + # Round up only if there are non-zero digits beyond the 5 + if self.number_of_trailing_zeros() < ndigits - 1: + round_up = True elif rounding_mode == RoundingMode.half_even(): var cut_off_digit = self.ith_digit(ndigits - 1) if cut_off_digit > 5: diff --git a/src/decimo/rounding_mode.mojo b/src/decimo/rounding_mode.mojo index 24a9a884..36250038 100644 --- a/src/decimo/rounding_mode.mojo +++ b/src/decimo/rounding_mode.mojo @@ -21,6 +21,8 @@ comptime ROUND_DOWN = RoundingMode.ROUND_DOWN """Rounding mode: Truncate (toward zero).""" comptime ROUND_HALF_UP = RoundingMode.ROUND_HALF_UP """Rounding mode: Round away from zero if >= 0.5.""" +comptime ROUND_HALF_DOWN = RoundingMode.ROUND_HALF_DOWN +"""Rounding mode: Round toward zero if <= 0.5.""" comptime ROUND_HALF_EVEN = RoundingMode.ROUND_HALF_EVEN """Rounding mode: Round to nearest even digit if equidistant (banker's rounding).""" comptime ROUND_UP = RoundingMode.ROUND_UP @@ -38,6 +40,7 @@ struct RoundingMode(Copyable, ImplicitlyCopyable, Movable, Stringable): Available modes: - DOWN: Truncate (toward zero) - HALF_UP: Round away from zero if >= 0.5 + - HALF_DOWN: Round toward zero if <= 0.5 - HALF_EVEN: Round to nearest even digit if equidistant (banker's rounding) - UP: Round away from zero - CEILING: Round toward positive infinity @@ -52,6 +55,7 @@ struct RoundingMode(Copyable, ImplicitlyCopyable, Movable, Stringable): # alias comptime ROUND_DOWN = Self.down() comptime ROUND_HALF_UP = Self.half_up() + comptime ROUND_HALF_DOWN = Self.half_down() comptime ROUND_HALF_EVEN = Self.half_even() comptime ROUND_UP = Self.up() comptime ROUND_CEILING = Self.ceiling() @@ -72,6 +76,11 @@ struct RoundingMode(Copyable, ImplicitlyCopyable, Movable, Stringable): """Round away from zero if >= 0.5.""" return Self(1) + @staticmethod + fn half_down() -> Self: + """Round toward zero if <= 0.5.""" + return Self(6) + @staticmethod fn half_even() -> Self: """Round to nearest even digit if equidistant (banker's rounding).""" @@ -112,6 +121,8 @@ struct RoundingMode(Copyable, ImplicitlyCopyable, Movable, Stringable): return "ROUND_UP" elif self == Self.ceiling(): return "ROUND_CEILING" + elif self == Self.half_down(): + return "ROUND_HALF_DOWN" elif self == Self.floor(): return "ROUND_FLOOR" else: diff --git a/tests/bigdecimal/test_bigdecimal_rounding.mojo b/tests/bigdecimal/test_bigdecimal_rounding.mojo index 03d4fa16..375fc5f7 100644 --- a/tests/bigdecimal/test_bigdecimal_rounding.mojo +++ b/tests/bigdecimal/test_bigdecimal_rounding.mojo @@ -141,6 +141,37 @@ fn test_bigdecimal_rounding() raises: "ROUND_HALF_EVEN: Mojo and Python results differ. See above.", ) + # ------------------------------------------------------- + # Testing BigDecimal ROUND_HALF_DOWN mode + # ------------------------------------------------------- + + pydecimal.getcontext().rounding = pydecimal.ROUND_HALF_DOWN + test_cases = load_test_cases(toml, "round_half_down_tests") + count_wrong = 0 + for test_case in test_cases: + var precision = Int(test_case.b) + var result = BDec(test_case.a).round( + precision, RoundingMode.half_down() + ) + var mojo_str = String(result) + var template = pydecimal.Decimal("1E" + String(-precision)) + var py_str = String(pydecimal.Decimal(test_case.a).quantize(template)) + if mojo_str != py_str: + print( + test_case.description, + "\n Mojo: ", + mojo_str, + "\n Python: ", + py_str, + "\n", + ) + count_wrong += 1 + testing.assert_equal( + count_wrong, + 0, + "ROUND_HALF_DOWN: Mojo and Python results differ. See above.", + ) + # ------------------------------------------------------- # Testing BigDecimal rounding with extreme values (HALF_EVEN) # ------------------------------------------------------- diff --git a/tests/bigdecimal/test_data/bigdecimal_rounding.toml b/tests/bigdecimal/test_data/bigdecimal_rounding.toml index 6d3686e0..64ace838 100644 --- a/tests/bigdecimal/test_data/bigdecimal_rounding.toml +++ b/tests/bigdecimal/test_data/bigdecimal_rounding.toml @@ -218,6 +218,103 @@ b = "0" description = "Round half even exactly -0.5 to integer (0 is even)" expected = "-0" +# === ROUND HALF DOWN TESTS === +[[round_half_down_tests]] +a = "12.345" +b = "0" +description = "Round half down to integer" +expected = "12" + +[[round_half_down_tests]] +a = "12.345" +b = "1" +description = "Round half down to 1 decimal place" +expected = "12.3" + +[[round_half_down_tests]] +a = "12.345" +b = "2" +description = "Round half down to 2 decimal places" +expected = "12.34" + +[[round_half_down_tests]] +a = "12.5" +b = "0" +description = "Round half down exactly 0.5 to integer (tie goes toward zero)" +expected = "12" + +[[round_half_down_tests]] +a = "13.5" +b = "0" +description = "Round half down exactly 13.5 to integer (tie goes toward zero)" +expected = "13" + +[[round_half_down_tests]] +a = "12.501" +b = "0" +description = "Round half down 12.501 to integer (above tie, rounds up)" +expected = "13" + +[[round_half_down_tests]] +a = "-12.5" +b = "0" +description = "Round half down negative tie (toward zero)" +expected = "-12" + +[[round_half_down_tests]] +a = "-12.501" +b = "0" +description = "Round half down negative above tie (away from zero)" +expected = "-13" + +[[round_half_down_tests]] +a = "12.345" +b = "3" +description = "Round half down at exactly 0.005 tie (toward zero)" +expected = "12.344" + +[[round_half_down_tests]] +a = "12.3451" +b = "3" +description = "Round half down above 0.005 tie (rounds up)" +expected = "12.345" + +[[round_half_down_tests]] +a = "-12.345" +b = "2" +description = "Round half down negative -12.345 to 2 decimal places" +expected = "-12.34" + +[[round_half_down_tests]] +a = "0.5" +b = "0" +description = "Round half down exactly 0.5 to integer" +expected = "0" + +[[round_half_down_tests]] +a = "-0.5" +b = "0" +description = "Round half down exactly -0.5 to integer" +expected = "-0" + +[[round_half_down_tests]] +a = "1.050" +b = "1" +description = "Round half down exactly at tie 1.050" +expected = "1.0" + +[[round_half_down_tests]] +a = "1.15" +b = "1" +description = "Round half down exactly at tie 1.15" +expected = "1.1" + +[[round_half_down_tests]] +a = "1.16" +b = "1" +description = "Round half down above tie 1.16" +expected = "1.2" + # === EXTREME VALUES TESTS === [[extreme_value_tests]] a = "0.{0,25}1" From 15caab8d33f4aec88cffd6e341cb6aeb00670fb6 Mon Sep 17 00:00:00 2001 From: ZHU Yuhao Date: Sat, 28 Feb 2026 19:47:30 +0100 Subject: [PATCH 3/5] =?UTF-8?q?=E6=9B=B4=E6=96=B0rounding=5Fmode=20?= =?UTF-8?q?=E5=92=8C=E8=8B=A5=E5=B9=B2util=E5=87=BD=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/decimo/bigdecimal/rounding.mojo | 46 ++++++++++------------------- src/decimo/biguint/biguint.mojo | 37 +++++++++++++++++------ src/decimo/decimal128/rounding.mojo | 19 +++++++++++- src/decimo/decimal128/utility.mojo | 40 ++++++++++++++++++++----- 4 files changed, 95 insertions(+), 47 deletions(-) diff --git a/src/decimo/bigdecimal/rounding.mojo b/src/decimo/bigdecimal/rounding.mojo index 6c50a0ab..b45a4fde 100644 --- a/src/decimo/bigdecimal/rounding.mojo +++ b/src/decimo/bigdecimal/rounding.mojo @@ -55,18 +55,6 @@ fn round( round(123.456, -3) -> 0E+3 round(678.890, -3) -> 1E+3 """ - # Translate CEILING/FLOOR to UP/DOWN based on the number's sign. - # CEILING (toward +inf): positive -> UP, negative -> DOWN - # FLOOR (toward -inf): positive -> DOWN, negative -> UP - var effective_mode = rounding_mode - if rounding_mode == RoundingMode.ceiling(): - effective_mode = ( - RoundingMode.up() if not number.sign else RoundingMode.down() - ) - elif rounding_mode == RoundingMode.floor(): - effective_mode = ( - RoundingMode.down() if not number.sign else RoundingMode.up() - ) var ndigits_to_remove = number.scale - ndigits if ndigits_to_remove == 0: @@ -83,10 +71,15 @@ fn round( # ROUND_HALF_EVEN, it depends on whether the removed value is # >= 0.5 at the target scale (it can't be when all digits are # below the target precision), so it's 0. - if ( - effective_mode == RoundingMode.up() - and number.coefficient != BigUInt.zero() - ): + # + # For CEILING: round up if positive and non-zero. + # For FLOOR: round up if negative and non-zero. + var rounds_away = ( + rounding_mode == RoundingMode.up() + or (rounding_mode == RoundingMode.ceiling() and not number.sign) + or (rounding_mode == RoundingMode.floor() and number.sign) + ) + if rounds_away and number.coefficient != BigUInt.zero(): return BigDecimal( coefficient=BigUInt.one(), scale=ndigits, @@ -100,8 +93,9 @@ fn round( var coefficient = ( number.coefficient.remove_trailing_digits_with_rounding( ndigits=ndigits_to_remove, - rounding_mode=effective_mode, + rounding_mode=rounding_mode, remove_extra_digit_due_to_rounding=False, + sign=number.sign, ) ) return BigDecimal( @@ -127,7 +121,9 @@ fn round_to_precision( rounding_mode: Rounding mode to use. RoundingMode.ROUND_DOWN: Round toward zero. RoundingMode.ROUND_UP: Round away from zero. - RoundingMode.ROUND_HALF_UP: Round half away from zero. RoundingMode.ROUND_HALF_DOWN: Round half toward zero. RoundingMode.ROUND_HALF_EVEN: Round half to even (banker's). + RoundingMode.ROUND_HALF_UP: Round half away from zero. + RoundingMode.ROUND_HALF_DOWN: Round half toward zero. + RoundingMode.ROUND_HALF_EVEN: Round half to even (banker's). RoundingMode.ROUND_CEILING: Round toward +∞. RoundingMode.ROUND_FLOOR: Round toward -∞. remove_extra_digit_due_to_rounding: If True, remove a trailing digit if @@ -135,17 +131,6 @@ fn round_to_precision( fill_zeros_to_precision: If True, fill trailing zeros to the precision. """ - # Translate CEILING/FLOOR to UP/DOWN based on the number's sign. - var effective_mode = rounding_mode - if rounding_mode == RoundingMode.ceiling(): - effective_mode = ( - RoundingMode.up() if not number.sign else RoundingMode.down() - ) - elif rounding_mode == RoundingMode.floor(): - effective_mode = ( - RoundingMode.down() if not number.sign else RoundingMode.up() - ) - var ndigits_coefficient = number.coefficient.number_of_digits() var ndigits_to_remove = ndigits_coefficient - precision @@ -162,8 +147,9 @@ fn round_to_precision( number.coefficient = ( number.coefficient.remove_trailing_digits_with_rounding( ndigits=ndigits_to_remove, - rounding_mode=effective_mode, + rounding_mode=rounding_mode, remove_extra_digit_due_to_rounding=False, + sign=number.sign, ) ) number.scale -= ndigits_to_remove diff --git a/src/decimo/biguint/biguint.mojo b/src/decimo/biguint/biguint.mojo index 72fef094..c5b0968d 100644 --- a/src/decimo/biguint/biguint.mojo +++ b/src/decimo/biguint/biguint.mojo @@ -1824,18 +1824,24 @@ struct BigUInt( ndigits: Int, rounding_mode: RoundingMode, remove_extra_digit_due_to_rounding: Bool, + sign: Bool = False, ) raises -> Self: """Removes trailing digits from the BigUInt with rounding. Args: ndigits: The number of digits to remove. rounding_mode: The rounding mode to use. - RoundingMode.ROUND_DOWN: Round down. - RoundingMode.ROUND_UP: Round up. - RoundingMode.ROUND_HALF_UP: Round half up. - RoundingMode.ROUND_HALF_EVEN: Round half even. + RoundingMode.ROUND_DOWN: Round toward zero. + RoundingMode.ROUND_UP: Round away from zero. + RoundingMode.ROUND_HALF_UP: Round half away from zero. + RoundingMode.ROUND_HALF_DOWN: Round half toward zero. + RoundingMode.ROUND_HALF_EVEN: Round half to even (banker's). + RoundingMode.ROUND_CEILING: Round toward +inf. + RoundingMode.ROUND_FLOOR: Round toward -inf. remove_extra_digit_due_to_rounding: If True, remove an trailing digit if the rounding mode result in an extra digit. + sign: The sign of the original number (True = negative). + Only needed for CEILING/FLOOR modes. Returns: The BigUInt with the trailing digits removed. @@ -1885,15 +1891,28 @@ struct BigUInt( ) var round_up: Bool = False - if rounding_mode == RoundingMode.down(): + # Translate CEILING/FLOOR to UP/DOWN based on sign. + # CEILING (toward +inf): positive -> UP, negative -> DOWN + # FLOOR (toward -inf): positive -> DOWN, negative -> UP + var effective_mode = rounding_mode + if rounding_mode == RoundingMode.ceiling(): + effective_mode = ( + RoundingMode.up() if not sign else RoundingMode.down() + ) + elif rounding_mode == RoundingMode.floor(): + effective_mode = ( + RoundingMode.down() if not sign else RoundingMode.up() + ) + + if effective_mode == RoundingMode.down(): pass - elif rounding_mode == RoundingMode.up(): + elif effective_mode == RoundingMode.up(): if self.number_of_trailing_zeros() < ndigits: round_up = True - elif rounding_mode == RoundingMode.half_up(): + elif effective_mode == RoundingMode.half_up(): if self.ith_digit(ndigits - 1) >= 5: round_up = True - elif rounding_mode == RoundingMode.half_down(): + elif effective_mode == RoundingMode.half_down(): var cut_off_digit = self.ith_digit(ndigits - 1) if cut_off_digit > 5: round_up = True @@ -1901,7 +1920,7 @@ struct BigUInt( # Round up only if there are non-zero digits beyond the 5 if self.number_of_trailing_zeros() < ndigits - 1: round_up = True - elif rounding_mode == RoundingMode.half_even(): + elif effective_mode == RoundingMode.half_even(): var cut_off_digit = self.ith_digit(ndigits - 1) if cut_off_digit > 5: round_up = True diff --git a/src/decimo/decimal128/rounding.mojo b/src/decimo/decimal128/rounding.mojo index c199a58a..a60e2064 100644 --- a/src/decimo/decimal128/rounding.mojo +++ b/src/decimo/decimal128/rounding.mojo @@ -53,6 +53,13 @@ fn round( ndigits: Number of decimal places to round to. Defaults to 0. rounding_mode: Rounding mode to use. + RoundingMode.ROUND_DOWN: Round toward zero. + RoundingMode.ROUND_UP: Round away from zero. + RoundingMode.ROUND_HALF_UP: Round half away from zero. + RoundingMode.ROUND_HALF_DOWN: Round half toward zero. + RoundingMode.ROUND_HALF_EVEN: Round half to even (banker's). + RoundingMode.ROUND_CEILING: Round toward positive infinity. + RoundingMode.ROUND_FLOOR: Round toward negative infinity. Defaults to ROUND_HALF_EVEN (banker's rounding). Returns: @@ -140,7 +147,10 @@ fn round( # Keep the first `ndigits_to_keep` digits with specified rounding mode var res_coef = decimo.decimal128.utility.round_to_keep_first_n_digits( - x_coef, ndigits=ndigits_to_keep, rounding_mode=rounding_mode + x_coef, + ndigits=ndigits_to_keep, + rounding_mode=rounding_mode, + sign=number.is_negative(), ) if ndigits >= 0: @@ -174,6 +184,13 @@ fn quantize( value: The Decimal128 value to quantize. exp: A Decimal128 whose scale (exponent) will be used for the result. rounding_mode: The rounding mode to use. + RoundingMode.ROUND_DOWN: Round toward zero. + RoundingMode.ROUND_UP: Round away from zero. + RoundingMode.ROUND_HALF_UP: Round half away from zero. + RoundingMode.ROUND_HALF_DOWN: Round half toward zero. + RoundingMode.ROUND_HALF_EVEN: Round half to even (banker's). + RoundingMode.ROUND_CEILING: Round toward positive infinity. + RoundingMode.ROUND_FLOOR: Round toward negative infinity. Defaults to ROUND_HALF_EVEN (banker's rounding). Returns: diff --git a/src/decimo/decimal128/utility.mojo b/src/decimo/decimal128/utility.mojo index aa49207a..4abb9827 100644 --- a/src/decimo/decimal128/utility.mojo +++ b/src/decimo/decimal128/utility.mojo @@ -226,6 +226,7 @@ fn round_to_keep_first_n_digits[ value: Scalar[dtype], ndigits: Int, rounding_mode: RoundingMode = RoundingMode.ROUND_HALF_EVEN, + sign: Bool = False, ) -> Scalar[dtype]: """ Rounds and keeps the first n digits of a integral value. @@ -333,23 +334,42 @@ fn round_to_keep_first_n_digits[ var truncated_value = value // divisor var remainder = value % divisor + # Translate CEILING/FLOOR to UP/DOWN based on sign. + # CEILING (toward +inf): positive -> UP, negative -> DOWN + # FLOOR (toward -inf): positive -> DOWN, negative -> UP + var effective_mode = rounding_mode + if rounding_mode == RoundingMode.ceiling(): + effective_mode = ( + RoundingMode.up() if not sign else RoundingMode.down() + ) + elif rounding_mode == RoundingMode.floor(): + effective_mode = ( + RoundingMode.down() if not sign else RoundingMode.up() + ) + # If RoundingMode is down(), just truncate the value - if rounding_mode == RoundingMode.down(): + if effective_mode == RoundingMode.down(): pass # If RoundingMode is up(), round up the value if remainder is greater than 0 - elif rounding_mode == RoundingMode.up(): + elif effective_mode == RoundingMode.up(): if remainder > 0: truncated_value += 1 - # If RoundingMode is half_up(), round up the value if remainder is greater than 5 - elif rounding_mode == RoundingMode.half_up(): + # If RoundingMode is half_up(), round up the value if remainder is >= 0.5 + elif effective_mode == RoundingMode.half_up(): var cutoff_value = 5 * power_of_10[dtype](ndigits_to_remove - 1) if remainder >= cutoff_value: truncated_value += 1 + # If RoundingMode is half_down(), round up only if remainder is > 0.5 + elif effective_mode == RoundingMode.half_down(): + var cutoff_value = 5 * power_of_10[dtype](ndigits_to_remove - 1) + if remainder > cutoff_value: + truncated_value += 1 + # If RoundingMode is ROUND_HALF_EVEN, round to nearest even digit if equidistant - else: + elif effective_mode == RoundingMode.half_even(): var cutoff_value: ValueType = 5 * power_of_10[dtype]( ndigits_to_remove - 1 ) @@ -359,8 +379,14 @@ fn round_to_keep_first_n_digits[ # If truncated_value is even, do not round up # If truncated_value is odd, round up truncated_value += truncated_value % 2 - else: - pass + + # If we reach here, it's an unknown rounding mode. + else: + debug_assert( + False, + "Unknown rounding mode in round_to_keep_first_n_digits: " + + String(rounding_mode), + ) return truncated_value From dfaee06444d781f3850f9385daa349954aa391cf Mon Sep 17 00:00:00 2001 From: ZHU Yuhao Date: Sat, 28 Feb 2026 20:19:23 +0100 Subject: [PATCH 4/5] Update --- docs/plans/api_roadmap.md | 8 ++--- src/decimo/biguint/biguint.mojo | 2 ++ src/decimo/decimal128/arithmetics.mojo | 42 +++++++++++++++----------- src/decimo/decimal128/decimal128.mojo | 6 ++-- src/decimo/decimal128/utility.mojo | 7 +++-- 5 files changed, 38 insertions(+), 27 deletions(-) diff --git a/docs/plans/api_roadmap.md b/docs/plans/api_roadmap.md index e7979fc4..05fce233 100644 --- a/docs/plans/api_roadmap.md +++ b/docs/plans/api_roadmap.md @@ -163,10 +163,10 @@ These are the gaps vs Python's `decimal.Decimal`, prioritized by user impact. ### Missing: 2 modes -| Mode | Python Name | Description | Priority | -| ----------------- | ----------------- | ---------------------- | ------------------------------------- | -| `ROUND_HALF_DOWN` | `ROUND_HALF_DOWN` | Round ties toward zero | ✓ Implemented | -| `ROUND_05UP` | `ROUND_05UP` | Special IEEE rounding | **LOW** — rarely used | +| Mode | Python Name | Description | Priority | +| ----------------- | ----------------- | ---------------------- | --------------------- | +| `ROUND_HALF_DOWN` | `ROUND_HALF_DOWN` | Round ties toward zero | ✓ Implemented | +| `ROUND_05UP` | `ROUND_05UP` | Special IEEE rounding | **LOW** — rarely used | --- diff --git a/src/decimo/biguint/biguint.mojo b/src/decimo/biguint/biguint.mojo index c5b0968d..9bbe09a0 100644 --- a/src/decimo/biguint/biguint.mojo +++ b/src/decimo/biguint/biguint.mojo @@ -1931,6 +1931,8 @@ struct BigUInt( round_up = True else: round_up = self.ith_digit(ndigits) % 2 == 1 + # TODO: Remove this fallback once Mojo has proper enum support, + # which will make exhaustive matching a compile-time guarantee. else: raise Error( ValueError( diff --git a/src/decimo/decimal128/arithmetics.mojo b/src/decimo/decimal128/arithmetics.mojo index 937b3923..13489686 100644 --- a/src/decimo/decimal128/arithmetics.mojo +++ b/src/decimo/decimal128/arithmetics.mojo @@ -225,13 +225,13 @@ fn add(x1: Decimal128, x2: Decimal128) raises -> Decimal128: var truncated_summation = ( decimo.decimal128.utility.round_to_keep_first_n_digits( - summation, Decimal128.MAX_NUM_DIGITS + summation, False, Decimal128.MAX_NUM_DIGITS ) ) if truncated_summation > Decimal128.MAX_AS_UINT128: truncated_summation = ( decimo.decimal128.utility.round_to_keep_first_n_digits( - summation, Decimal128.MAX_NUM_DIGITS - 1 + summation, False, Decimal128.MAX_NUM_DIGITS - 1 ) ) final_scale -= 1 @@ -306,13 +306,13 @@ fn add(x1: Decimal128, x2: Decimal128) raises -> Decimal128: truncated_summation = ( decimo.decimal128.utility.round_to_keep_first_n_digits( - summation, Decimal128.MAX_NUM_DIGITS + summation, False, Decimal128.MAX_NUM_DIGITS ) ) if truncated_summation > Decimal128.MAX_AS_UINT256: truncated_summation = ( decimo.decimal128.utility.round_to_keep_first_n_digits( - summation, Decimal128.MAX_NUM_DIGITS - 1 + summation, False, Decimal128.MAX_NUM_DIGITS - 1 ) ) final_scale -= 1 @@ -468,7 +468,7 @@ fn multiply(x1: Decimal128, x2: Decimal128) raises -> Decimal128: ) var truncated_prod = ( decimo.decimal128.utility.round_to_keep_first_n_digits( - prod, num_digits_to_keep + prod, False, num_digits_to_keep ) ) var final_scale = min(Decimal128.MAX_SCALE, combined_scale) @@ -496,7 +496,7 @@ fn multiply(x1: Decimal128, x2: Decimal128) raises -> Decimal128: ) var truncated_prod = ( decimo.decimal128.utility.round_to_keep_first_n_digits( - prod, num_digits_to_keep + prod, False, num_digits_to_keep ) ) var final_scale = min(Decimal128.MAX_SCALE, combined_scale) @@ -603,7 +603,7 @@ fn multiply(x1: Decimal128, x2: Decimal128) raises -> Decimal128: combined_scale - Decimal128.MAX_SCALE ) prod = decimo.decimal128.utility.round_to_keep_first_n_digits( - prod, num_digits_to_keep + prod, False, num_digits_to_keep ) var final_scale = min(Decimal128.MAX_SCALE, combined_scale) @@ -612,7 +612,9 @@ fn multiply(x1: Decimal128, x2: Decimal128) raises -> Decimal128: prod ) prod = decimo.decimal128.utility.round_to_keep_first_n_digits( - prod, ndigits_prod - (final_scale - Decimal128.MAX_SCALE) + prod, + False, + ndigits_prod - (final_scale - Decimal128.MAX_SCALE), ) final_scale = Decimal128.MAX_SCALE @@ -630,7 +632,7 @@ fn multiply(x1: Decimal128, x2: Decimal128) raises -> Decimal128: # Truncated first 29 digits var truncated_prod_at_max_length = ( decimo.decimal128.utility.round_to_keep_first_n_digits( - prod, Decimal128.MAX_NUM_DIGITS + prod, False, Decimal128.MAX_NUM_DIGITS ) ) @@ -658,7 +660,7 @@ fn multiply(x1: Decimal128, x2: Decimal128) raises -> Decimal128: if truncated_prod_at_max_length > Decimal128.MAX_AS_UINT128: num_digits_of_decimal_part -= 1 prod = decimo.decimal128.utility.round_to_keep_first_n_digits( - prod, Decimal128.MAX_NUM_DIGITS - 1 + prod, False, Decimal128.MAX_NUM_DIGITS - 1 ) else: prod = truncated_prod_at_max_length @@ -669,7 +671,7 @@ fn multiply(x1: Decimal128, x2: Decimal128) raises -> Decimal128: if final_scale > Decimal128.MAX_SCALE: var ndigits_prod = decimo.decimal128.utility.number_of_digits(prod) prod = decimo.decimal128.utility.round_to_keep_first_n_digits( - prod, ndigits_prod - (final_scale - Decimal128.MAX_SCALE) + prod, False, ndigits_prod - (final_scale - Decimal128.MAX_SCALE) ) final_scale = Decimal128.MAX_SCALE @@ -687,7 +689,7 @@ fn multiply(x1: Decimal128, x2: Decimal128) raises -> Decimal128: # Truncated first 29 digits var truncated_prod_at_max_length = ( decimo.decimal128.utility.round_to_keep_first_n_digits( - prod, Decimal128.MAX_NUM_DIGITS + prod, False, Decimal128.MAX_NUM_DIGITS ) ) @@ -717,7 +719,7 @@ fn multiply(x1: Decimal128, x2: Decimal128) raises -> Decimal128: if truncated_prod_at_max_length > Decimal128.MAX_AS_UINT256: num_digits_of_decimal_part -= 1 prod = decimo.decimal128.utility.round_to_keep_first_n_digits( - prod, Decimal128.MAX_NUM_DIGITS - 1 + prod, False, Decimal128.MAX_NUM_DIGITS - 1 ) else: prod = truncated_prod_at_max_length @@ -728,7 +730,7 @@ fn multiply(x1: Decimal128, x2: Decimal128) raises -> Decimal128: if final_scale > Decimal128.MAX_SCALE: var ndigits_prod = decimo.decimal128.utility.number_of_digits(prod) prod = decimo.decimal128.utility.round_to_keep_first_n_digits( - prod, ndigits_prod - (final_scale - Decimal128.MAX_SCALE) + prod, False, ndigits_prod - (final_scale - Decimal128.MAX_SCALE) ) final_scale = Decimal128.MAX_SCALE @@ -1022,6 +1024,7 @@ fn divide(x1: Decimal128, x2: Decimal128) raises -> Decimal128: if scale_of_quot > Decimal128.MAX_SCALE: quot = decimo.decimal128.utility.round_to_keep_first_n_digits( quot, + False, ndigits_quot - (scale_of_quot - Decimal128.MAX_SCALE), ) scale_of_quot = Decimal128.MAX_SCALE @@ -1032,7 +1035,7 @@ fn divide(x1: Decimal128, x2: Decimal128) raises -> Decimal128: else: var truncated_quot = ( decimo.decimal128.utility.round_to_keep_first_n_digits( - quot, Decimal128.MAX_NUM_DIGITS + quot, False, Decimal128.MAX_NUM_DIGITS ) ) var scale_of_truncated_quot = ( @@ -1042,7 +1045,7 @@ fn divide(x1: Decimal128, x2: Decimal128) raises -> Decimal128: if truncated_quot > Decimal128.MAX_AS_UINT128: truncated_quot = ( decimo.decimal128.utility.round_to_keep_first_n_digits( - quot, Decimal128.MAX_NUM_DIGITS - 1 + quot, False, Decimal128.MAX_NUM_DIGITS - 1 ) ) scale_of_truncated_quot -= 1 @@ -1054,6 +1057,7 @@ fn divide(x1: Decimal128, x2: Decimal128) raises -> Decimal128: truncated_quot = ( decimo.decimal128.utility.round_to_keep_first_n_digits( truncated_quot, + False, num_digits_truncated_quot - (scale_of_truncated_quot - Decimal128.MAX_SCALE), ) @@ -1125,6 +1129,7 @@ fn divide(x1: Decimal128, x2: Decimal128) raises -> Decimal128: quot256 = ( decimo.decimal128.utility.round_to_keep_first_n_digits( quot256, + False, ndigits_quot - (scale_of_quot - Decimal128.MAX_SCALE), ) ) @@ -1140,7 +1145,7 @@ fn divide(x1: Decimal128, x2: Decimal128) raises -> Decimal128: else: var truncated_quot = ( decimo.decimal128.utility.round_to_keep_first_n_digits( - quot256, Decimal128.MAX_NUM_DIGITS + quot256, False, Decimal128.MAX_NUM_DIGITS ) ) @@ -1158,7 +1163,7 @@ fn divide(x1: Decimal128, x2: Decimal128) raises -> Decimal128: if truncated_quot > Decimal128.MAX_AS_UINT256: truncated_quot = ( decimo.decimal128.utility.round_to_keep_first_n_digits( - quot256, Decimal128.MAX_NUM_DIGITS - 1 + quot256, False, Decimal128.MAX_NUM_DIGITS - 1 ) ) scale_of_truncated_quot -= 1 @@ -1170,6 +1175,7 @@ fn divide(x1: Decimal128, x2: Decimal128) raises -> Decimal128: truncated_quot = ( decimo.decimal128.utility.round_to_keep_first_n_digits( truncated_quot, + False, num_digits_truncated_quot - (scale_of_truncated_quot - Decimal128.MAX_SCALE), ) diff --git a/src/decimo/decimal128/decimal128.mojo b/src/decimo/decimal128/decimal128.mojo index 855d3f07..b0fb28cb 100644 --- a/src/decimo/decimal128/decimal128.mojo +++ b/src/decimo/decimal128/decimal128.mojo @@ -716,6 +716,7 @@ struct Decimal128( if scale > Decimal128.MAX_SCALE: coef = decimo.decimal128.utility.round_to_keep_first_n_digits( coef, + False, Int(num_mantissa_digits) - Int(scale - Decimal128.MAX_SCALE), ) @@ -729,7 +730,7 @@ struct Decimal128( var truncated_coef = ( decimo.decimal128.utility.round_to_keep_first_n_digits( - coef, Decimal128.MAX_NUM_DIGITS + coef, False, Decimal128.MAX_NUM_DIGITS ) ) var scale_of_truncated_coef = ( @@ -739,7 +740,7 @@ struct Decimal128( if truncated_coef > Decimal128.MAX_AS_UINT128: truncated_coef = ( decimo.decimal128.utility.round_to_keep_first_n_digits( - coef, Decimal128.MAX_NUM_DIGITS - 1 + coef, False, Decimal128.MAX_NUM_DIGITS - 1 ) ) scale_of_truncated_coef -= 1 @@ -751,6 +752,7 @@ struct Decimal128( truncated_coef = ( decimo.decimal128.utility.round_to_keep_first_n_digits( truncated_coef, + False, num_digits_truncated_coef - Int(scale_of_truncated_coef - Decimal128.MAX_SCALE), ) diff --git a/src/decimo/decimal128/utility.mojo b/src/decimo/decimal128/utility.mojo index 4abb9827..5dea562c 100644 --- a/src/decimo/decimal128/utility.mojo +++ b/src/decimo/decimal128/utility.mojo @@ -219,14 +219,13 @@ fn sqrt(x: UInt128) -> UInt128: # TODO: Evaluate whether this can replace truncate_to_max in some cases. -# TODO: Add rounding modes to this function. fn round_to_keep_first_n_digits[ dtype: DType, // ]( value: Scalar[dtype], + sign: Bool, ndigits: Int, rounding_mode: RoundingMode = RoundingMode.ROUND_HALF_EVEN, - sign: Bool = False, ) -> Scalar[dtype]: """ Rounds and keeps the first n digits of a integral value. @@ -239,6 +238,7 @@ fn round_to_keep_first_n_digits[ Args: value: The integral value to truncate. + sign: The sign of the original number. ndigits: The number of significant digits to evaluate. rounding_mode: The rounding mode to use. @@ -380,7 +380,8 @@ fn round_to_keep_first_n_digits[ # If truncated_value is odd, round up truncated_value += truncated_value % 2 - # If we reach here, it's an unknown rounding mode. + # TODO: Remove this fallback once Mojo has proper enum support, + # which will make exhaustive matching a compile-time guarantee. else: debug_assert( False, From c6ffc7e59026d5924ec666807f950a565dac1684 Mon Sep 17 00:00:00 2001 From: ZHU Yuhao Date: Sat, 28 Feb 2026 20:35:54 +0100 Subject: [PATCH 5/5] Fix --- tests/decimal128/test_decimal128_utility.mojo | 33 ++++++++++++------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/tests/decimal128/test_decimal128_utility.mojo b/tests/decimal128/test_decimal128_utility.mojo index 1103089e..87894c3c 100644 --- a/tests/decimal128/test_decimal128_utility.mojo +++ b/tests/decimal128/test_decimal128_utility.mojo @@ -134,40 +134,51 @@ fn test_truncate_banker_rounding() raises: fn test_round_to_keep_first_n_digits() raises: """Tests for round_to_keep_first_n_digits.""" # Keep 0 digits (round to nearest power of 10) - assert_equal(round_to_keep_first_n_digits(UInt128(997), 0), UInt128(1)) + assert_equal( + round_to_keep_first_n_digits(UInt128(997), False, 0), UInt128(1) + ) # Truncate one digit assert_equal( - round_to_keep_first_n_digits(UInt128(234567), 5), UInt128(23457) + round_to_keep_first_n_digits(UInt128(234567), False, 5), UInt128(23457) ) # Fewer digits than n → unchanged assert_equal( - round_to_keep_first_n_digits(UInt128(234567), 29), UInt128(234567) + round_to_keep_first_n_digits(UInt128(234567), False, 29), + UInt128(234567), ) # Banker's rounding: trailing 5 with even preceding digit - assert_equal(round_to_keep_first_n_digits(UInt128(12345), 4), UInt128(1234)) + assert_equal( + round_to_keep_first_n_digits(UInt128(12345), False, 4), UInt128(1234) + ) # Banker's rounding: trailing 5 with odd preceding digit → round up - assert_equal(round_to_keep_first_n_digits(UInt128(23455), 4), UInt128(2346)) + assert_equal( + round_to_keep_first_n_digits(UInt128(23455), False, 4), UInt128(2346) + ) # Round down (< 5) - assert_equal(round_to_keep_first_n_digits(UInt128(12342), 4), UInt128(1234)) + assert_equal( + round_to_keep_first_n_digits(UInt128(12342), False, 4), UInt128(1234) + ) # Round up (> 5) - assert_equal(round_to_keep_first_n_digits(UInt128(12347), 4), UInt128(1235)) + assert_equal( + round_to_keep_first_n_digits(UInt128(12347), False, 4), UInt128(1235) + ) # Zero input - assert_equal(round_to_keep_first_n_digits(UInt128(0), 5), UInt128(0)) + assert_equal(round_to_keep_first_n_digits(UInt128(0), False, 5), UInt128(0)) # Single digit - assert_equal(round_to_keep_first_n_digits(UInt128(7), 1), UInt128(7)) - assert_equal(round_to_keep_first_n_digits(UInt128(7), 0), UInt128(1)) + assert_equal(round_to_keep_first_n_digits(UInt128(7), False, 1), UInt128(7)) + assert_equal(round_to_keep_first_n_digits(UInt128(7), False, 0), UInt128(1)) # Large UInt256 assert_equal( - round_to_keep_first_n_digits(UInt256(9876543210987654321), 18), + round_to_keep_first_n_digits(UInt256(9876543210987654321), False, 18), UInt256(987654321098765432), )