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
28 changes: 14 additions & 14 deletions docs/plans/api_roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -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). |
Expand Down Expand Up @@ -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 | **MEDIUM** — less common but expected |
| `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 |

---

Expand Down Expand Up @@ -294,10 +294,10 @@ 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
5. **`ROUND_HALF_DOWN`** rounding mode
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)
8. **`__float__()`** on BigInt — `float(n)` interop
Expand Down Expand Up @@ -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
```
1 change: 1 addition & 0 deletions src/decimo/__init__.mojo
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ from .rounding_mode import (
RoundingMode,
ROUND_DOWN,
ROUND_HALF_UP,
ROUND_HALF_DOWN,
ROUND_HALF_EVEN,
ROUND_UP,
ROUND_CEILING,
Expand Down
24 changes: 22 additions & 2 deletions src/decimo/bigdecimal/bigdecimal.mojo
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
44 changes: 15 additions & 29 deletions src/decimo/bigdecimal/rounding.mojo
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -54,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:
Expand All @@ -82,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,
Expand All @@ -99,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(
Expand All @@ -127,6 +122,7 @@ fn round_to_precision(
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 +∞.
RoundingMode.ROUND_FLOOR: Round toward -∞.
Expand All @@ -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

Expand All @@ -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
Expand Down
45 changes: 37 additions & 8 deletions src/decimo/biguint/biguint.mojo
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -1885,15 +1891,36 @@ 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_even():
elif effective_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 effective_mode == RoundingMode.half_even():
var cut_off_digit = self.ith_digit(ndigits - 1)
if cut_off_digit > 5:
round_up = True
Expand All @@ -1904,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(
Expand Down
Loading