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
18 changes: 9 additions & 9 deletions docs/plans/api_roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,10 @@ 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) | Useful for formatting and comparison. |
| `copy_abs()` | Returns `abs(self)` | Alias for `__abs__()`. Trivial to add. |
| `copy_negate()` | Returns `-self` | Alias for `__neg__()`. Trivial to add. |
| `copy_sign(other)` | Returns self with the sign of other | One-liner. |
| `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. |
| `normalize()` | Already exists | ✓ |
| `to_eng_string()` | Engineering notation (exponent multiple of 3) | ✓ **DONE** — alias for `to_string(engineering=True)` |
Expand Down Expand Up @@ -294,8 +294,8 @@ 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 — returns adjusted exponent (= `exponent + len(digits) - 1`)
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
Expand Down Expand Up @@ -344,9 +344,9 @@ For tracking against the above:
✓ __rmod__ ✓ __rmul__ ✓ __round__ ✓ __rpow__
✓ __rsub__ ✓ __rtruediv__ ✓ __str__ ✓ __sub__
✓ __truediv__ ✓ __trunc__
adjusted ✗ as_integer_ratio ✓ as_tuple ✗ canonical
✓ compare ✗ conjugate copy_abs copy_negate
copy_sign ✓ exp ✗ fma ✗ is_canonical
adjusted ✗ as_integer_ratio ✓ as_tuple ✗ canonical
✓ compare ✗ conjugate copy_abs copy_negate
copy_sign ✓ exp ✗ fma ✗ is_canonical
✗ is_finite ✓ is_integer ✗ is_nan ✗ is_normal
✗ is_signed ✗ is_snan ✗ is_subnormal ✗ is_qnan
✓ ln ✓ log10 ✗ logb ✗ logical_and
Expand Down
77 changes: 65 additions & 12 deletions src/decimo/bigdecimal/bigdecimal.mojo
Original file line number Diff line number Diff line change
Expand Up @@ -851,8 +851,9 @@ struct BigDecimal(
`decimal.Decimal`.

Notes:
Uses truncated division (toward zero), matching Python's
`decimal.Decimal.__divmod__()` behavior.

Uses truncated division (toward zero), matching Python's
`decimal.Decimal.__divmod__()` behavior.
"""
var quotient = decimo.bigdecimal.arithmetics.truncate_divide(
self, other
Expand Down Expand Up @@ -1353,20 +1354,34 @@ struct BigDecimal(
# Other methods
# ===------------------------------------------------------------------=== #

fn exponent(self) -> Int:
"""Returns the exponent of the number in scientific notation.
fn adjusted(self) -> Int:
"""Returns the adjusted exponent, matching Python's
`decimal.Decimal.adjusted()`.

Notes:
This is the exponent of the number when written with a single leading
digit in scientific notation. Equivalently,
`as_tuple_exponent + number_of_digits - 1` where `as_tuple_exponent`
is `-scale`.

For zero, the adjusted exponent is always 0 regardless of scale,
matching Python's behavior.

Examples:

123.45 (coefficient = 12345, scale = 2) is represented as 1.2345E+2.
0.00123 (coefficient = 123, scale = 5) is represented as 1.23E-3.
123000 (coefficient = 123, scale = -3) is represented as 1.23E+5.
```
BigDecimal("123.45").adjusted() # 2 (1.2345E+2)
BigDecimal("0.00123").adjusted() # -3 (1.23E-3)
BigDecimal("100").adjusted() # 2 (1E+2)
BigDecimal("1").adjusted() # 0 (1E0)
BigDecimal("0.00").adjusted() # 0 (zero has no order of magnitude)
```
"""
if self.coefficient.is_zero():
return 0
return self.coefficient.number_of_digits() - 1 - self.scale
Comment thread
forfudan marked this conversation as resolved.

Comment thread
forfudan marked this conversation as resolved.
fn as_tuple(self) -> Tuple[Bool, List[UInt8], Int]:
"""Returns a 3-tuple `(sign, digits, exponent)` matching Python's
`decimal.Decimal.as_tuple()` convention.
"""Returns a 3-tuple `(sign, digits, exponent)`.

The components are:
- `sign`: `False` for positive/zero, `True` for negative.
Expand Down Expand Up @@ -1395,8 +1410,13 @@ struct BigDecimal(
```

Notes:
To reconstruct the value from the tuple, join the digits into a
coefficient string, parse as BigUInt, then apply sign and exponent.

To reconstruct the value from the tuple, join the digits into a
coefficient string, parse as BigUInt, then apply sign and exponent.

This method is designed to match Python's `decimal.Decimal.as_tuple()`
output, except that the `digits` are returned as a `List[UInt8]`
instead of a `Tuple[int]` for better performance in Mojo.
"""
var coef_str = self.coefficient.to_string()
var cb = coef_str.as_string_slice().as_bytes()
Expand All @@ -1407,6 +1427,39 @@ struct BigDecimal(
digits.append(ptr[i] - 48) # 48 == ord('0')
return (self.sign, digits^, -self.scale)

@always_inline
fn copy_abs(self) -> Self:
"""Returns a copy with the sign set to positive.

Equivalent to `abs(self)`. Matches Python's
`decimal.Decimal.copy_abs()`.
"""
return self.__abs__()

@always_inline
fn copy_negate(self) -> Self:
"""Returns a copy with the sign inverted.

Equivalent to `-self`. Matches Python's
`decimal.Decimal.copy_negate()`.
"""
return self.__neg__()

@always_inline
fn copy_sign(self, other: Self) -> Self:
"""Returns a copy of `self` with the sign of `other`.

Matches Python's `decimal.Decimal.copy_sign(other)`.

Args:
other: The BigDecimal whose sign to copy.
"""
return Self(
coefficient=self.coefficient,
scale=self.scale,
sign=other.sign,
)

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
26 changes: 13 additions & 13 deletions src/decimo/bigdecimal/exponential.mojo
Original file line number Diff line number Diff line change
Expand Up @@ -623,7 +623,7 @@ fn integer_root(

# Initial guess using Float64 approximation
# Use exponent to get log10(x), then compute 10^(log10(x)/n)
var x_exp = abs_x.exponent() # floor(log10(x))
var x_exp = abs_x.adjusted() # floor(log10(x))

# Extract leading digits for a more precise Float64 approximation
var top_word = Float64(abs_x.coefficient.words[-1])
Expand Down Expand Up @@ -1020,7 +1020,7 @@ fn fast_isqrt(c: BigUInt, working_digits: Int) raises -> BigUInt:
var c_bd = BigDecimal(c.copy(), 0, False)

# --- Normalization ---
var c_exp = c_bd.exponent()
var c_exp = c_bd.adjusted()
var norm_shift: Int
if c_exp >= 0:
norm_shift = (c_exp // 2) * 2
Expand All @@ -1045,7 +1045,7 @@ fn fast_isqrt(c: BigUInt, working_digits: Int) raises -> BigUInt:
Float64(10.0) ** Float64(digits_in_top + 8)
)

var c_norm_exp = c_norm.exponent()
var c_norm_exp = c_norm.adjusted()
var c_norm_f64 = mantissa * Float64(10.0) ** Float64(c_norm_exp)
var r_f64 = c_norm_f64 ** (-0.5)
if r_f64 != r_f64 or r_f64 <= 0.0:
Expand Down Expand Up @@ -1333,7 +1333,7 @@ fn sqrt_reciprocal(x: BigDecimal, precision: Int) raises -> BigDecimal:
# Shift x by an even power of 10 to bring it into [0.1, 100) for a
# stable Float64 initial guess. Then sqrt(x) = sqrt(x_norm) * 10^(shift/2).
var x_norm = x.copy()
var x_exp = x_norm.exponent() # floor(log10(x))
var x_exp = x_norm.adjusted() # floor(log10(x))

# Make shift even and bring x_norm near 1
var shift: Int
Expand All @@ -1359,7 +1359,7 @@ fn sqrt_reciprocal(x: BigDecimal, precision: Int) raises -> BigDecimal:
Float64(10.0) ** Float64(digits_in_top + 8)
)

var x_norm_exp = x_norm.exponent()
var x_norm_exp = x_norm.adjusted()
var x_norm_f64 = mantissa * Float64(10.0) ** Float64(x_norm_exp)
var r_f64 = x_norm_f64 ** (-0.5) # 1/sqrt(x_norm)
if r_f64 != r_f64 or r_f64 <= 0.0: # NaN or degenerate
Expand Down Expand Up @@ -1764,11 +1764,11 @@ fn exp(x: BigDecimal, precision: Int) raises -> BigDecimal:

# For very large positive values, result will overflow BigDecimal capacity
# TODO: Use BigInt10 as scale can avoid overflow in this case
if not x.sign and x.exponent() >= 20: # x > 10^20
if not x.sign and x.adjusted() >= 20: # x > 10^20
raise Error("Error in `exp`: Result too large to represent")

# For very large negative values, result will be effectively zero
if x.sign and x.exponent() >= 20: # x < -10^20
if x.sign and x.adjusted() >= 20: # x < -10^20
return BigDecimal(BigUInt.zero(), precision, False)

# Handle negative x using identity: exp(-x) = 1/exp(x)
Expand All @@ -1789,7 +1789,7 @@ fn exp(x: BigDecimal, precision: Int) raises -> BigDecimal:
#
# For |x| ≈ 1: M ≈ √(3.322·p), giving ~2·√(3.322·p) total multiplications
# vs the old approach of ~2.5·p multiplications.
var x_exp = x.exponent() # floor(log10(x))
var x_exp = x.adjusted() # floor(log10(x))
var p_float = Float64(precision)

# Compute optimal number of halvings
Expand Down Expand Up @@ -1909,7 +1909,7 @@ fn exp_taylor_series(
# print("DEUBG: round {}, term {}, result {}".format(n, term, result))

# Check if we've reached desired precision
if term.exponent() < -minimum_precision:
if term.adjusted() < -minimum_precision:
break

result.round_to_precision(
Expand Down Expand Up @@ -1990,7 +1990,7 @@ fn ln(x: BigDecimal, precision: Int, mut cache: MathCache) raises -> BigDecimal:
var adj_power_of_2: Int = 0
var adj_power_of_5: Int = 0
# First, scale down to [0.1, 1)
var power_of_10 = m.exponent() + 1
var power_of_10 = m.adjusted() + 1
m.scale += power_of_10
# Second, scale to [0.5, 1.5)
if m < BigDecimal(BigUInt(raw_words=[135]), 3, False):
Expand Down Expand Up @@ -2241,7 +2241,7 @@ fn ln_series_expansion(
else:
result += next_term

if next_term.exponent() < -working_precision:
if next_term.adjusted() < -working_precision:
break

result.round_to_precision(
Expand Down Expand Up @@ -2294,7 +2294,7 @@ fn ln_series_expansion(

result += term

if term.exponent() < -working_precision:
if term.adjusted() < -working_precision:
break

result.round_to_precision(
Expand Down Expand Up @@ -2390,7 +2390,7 @@ fn compute_ln2(working_precision: Int) raises -> BigDecimal:
term.coefficient, k
)
k = new_k
if term.exponent() < -working_precision:
if term.adjusted() < -working_precision:
break

result.round_to_precision(
Expand Down
113 changes: 113 additions & 0 deletions tests/bigdecimal/test_bigdecimal_methods.mojo
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ Tests for BigDecimal utility methods added in v0.8.x:
- to_scientific_string() / to_eng_string()
- number_of_digits()
- as_tuple()
- copy_abs() / copy_negate() / copy_sign()
- adjusted()
"""

import testing
Expand Down Expand Up @@ -321,5 +323,116 @@ fn test_as_tuple_reconstruct() raises:
)


# ===----------------------------------------------------------------------=== #
# copy_abs() / copy_negate() / copy_sign()
# ===----------------------------------------------------------------------=== #


fn test_copy_abs_positive() raises:
"""Positive value unchanged."""
var x = BigDecimal("3.14")
testing.assert_equal(String(x.copy_abs()), "3.14")


fn test_copy_abs_negative() raises:
"""Negative value becomes positive."""
var x = BigDecimal("-3.14")
testing.assert_equal(String(x.copy_abs()), "3.14")


fn test_copy_abs_zero() raises:
"""Zero stays zero."""
testing.assert_equal(String(BigDecimal("0").copy_abs()), "0")


fn test_copy_abs_matches_abs() raises:
"""Verify copy_abs() == abs(x)."""
var x = BigDecimal("-42.5")
testing.assert_equal(String(x.copy_abs()), String(x.__abs__()))


fn test_copy_negate_positive() raises:
"""Positive becomes negative."""
testing.assert_equal(String(BigDecimal("3.14").copy_negate()), "-3.14")


fn test_copy_negate_negative() raises:
"""Negative becomes positive."""
testing.assert_equal(String(BigDecimal("-3.14").copy_negate()), "3.14")


fn test_copy_negate_zero() raises:
"""Negating zero."""
var z = BigDecimal("0").copy_negate()
# Zero negated — coefficient is still 0
testing.assert_true(z.is_zero())


fn test_copy_negate_matches_neg() raises:
"""Verify copy_negate() == -x."""
var x = BigDecimal("42.5")
testing.assert_equal(String(x.copy_negate()), String(x.__neg__()))


fn test_copy_sign_positive_to_negative() raises:
"""Copy sign of negative onto positive value."""
var x = BigDecimal("3.14")
var y = BigDecimal("-1")
testing.assert_equal(String(x.copy_sign(y)), "-3.14")


fn test_copy_sign_negative_to_positive() raises:
"""Copy sign of positive onto negative value."""
var x = BigDecimal("-3.14")
var y = BigDecimal("1")
testing.assert_equal(String(x.copy_sign(y)), "3.14")


fn test_copy_sign_same_sign() raises:
"""Same sign leaves value unchanged."""
var x = BigDecimal("3.14")
var y = BigDecimal("99")
testing.assert_equal(String(x.copy_sign(y)), "3.14")


fn test_copy_sign_zero_source() raises:
"""Zero takes sign of other."""
var x = BigDecimal("0")
var y = BigDecimal("-5")
var result = x.copy_sign(y)
# Sign bit set but coefficient is 0
testing.assert_true(result.sign)
testing.assert_true(result.is_zero())


# ===----------------------------------------------------------------------=== #
# adjusted()
# ===----------------------------------------------------------------------=== #


fn test_adjusted_basic() raises:
"""Basic adjusted exponent values."""
testing.assert_equal(BigDecimal("123.45").adjusted(), 2)
testing.assert_equal(BigDecimal("0.00123").adjusted(), -3)
testing.assert_equal(BigDecimal("100").adjusted(), 2)
testing.assert_equal(BigDecimal("1").adjusted(), 0)
testing.assert_equal(BigDecimal("10").adjusted(), 1)


fn test_adjusted_zero() raises:
"""Zero has adjusted exponent 0 regardless of scale."""
testing.assert_equal(BigDecimal("0").adjusted(), 0)
Comment thread
forfudan marked this conversation as resolved.
testing.assert_equal(BigDecimal("0.00").adjusted(), 0)
testing.assert_equal(BigDecimal("0.000000").adjusted(), 0)
testing.assert_equal(BigDecimal("0E+10").adjusted(), 0)


fn test_adjusted_scientific() raises:
"""Scientific notation inputs."""
testing.assert_equal(BigDecimal("1E+5").adjusted(), 5)
testing.assert_equal(BigDecimal("1E-5").adjusted(), -5)
testing.assert_equal(BigDecimal("1.23E+10").adjusted(), 10)


fn main() raises:
testing.TestSuite.discover_tests[__functions_in_module()]().run()