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
10 changes: 5 additions & 5 deletions docs/plans/api_roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -298,11 +298,11 @@ BigInt has `to_string_with_separators()`. This should be extended to 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
9. **`fma(a, b)`** on BigDecimal — `self * a + b` without intermediate rounding
10. **`to_string_with_separators()`** on BigDecimal — alias for `to_string(delimiter=...)`
6. **`scaleb(n)`** on BigDecimal — adjusts scale by `n` (O(1), no coefficient change)
7. **`bit_count()`** on BigInt — popcount (Kernighan's algorithm over UInt32 words)
8. **`__float__()`** on BigInt — `float(n)` via `Float64(String(self))`; `FloatableRaising` trait added
9. **`fma(a, b)`** on BigDecimal — `self * a + b` (exact, no intermediate rounding)
10. **`to_string_with_separators()`** on BigDecimal — alias for `to_string(delimiter=...)`

### Tier 3: Nice-to-Have (Remaining)

Expand Down
8 changes: 6 additions & 2 deletions src/cli/calculator/evaluator.mojo
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,12 @@ fn evaluate_rpn(rpn: List[Token], precision: Int) raises -> BDec:
Error: On division by zero, missing operands, or other runtime
errors — with source position when available.
"""
comptime GUARD_DIGITS = 9 # Word size
var working_precision = precision + GUARD_DIGITS # working precision
# BigUInt uses base-1e9 words (~9 decimal digits per word).
# Adding 9 guard digits gives roughly one extra internal word of
# precision beyond the user-requested amount, which absorbs
# accumulated rounding errors from intermediate operations.
comptime GUARD_DIGITS = 9
var working_precision = precision + GUARD_DIGITS
var stack = List[BDec]()

for i in range(len(rpn)):
Expand Down
2 changes: 1 addition & 1 deletion src/cli/main.mojo
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ fn _run() raises:
.required()
)

# Named option: decimal precision
# Named option: number of significant digits
cmd.add_arg(
Arg("precision", help="Number of significant digits (default: 50)")
.long("precision")
Expand Down
77 changes: 77 additions & 0 deletions src/decimo/bigdecimal/bigdecimal.mojo
Original file line number Diff line number Diff line change
Expand Up @@ -753,6 +753,26 @@ struct BigDecimal(
"""
return self.to_string(engineering=True)

fn to_string_with_separators(self, separator: String = "_") -> String:
"""Returns a string with digit-group separators inserted every 3 digits.

Groups both the integer and fractional parts. Convenience alias for
`to_string(delimiter=separator)`.

Args:
separator: The separator character (default: `"_"`).

Examples:

```
BigDecimal("1234567.89").to_string_with_separators() # "1_234_567.89"
BigDecimal("1234567.89").to_string_with_separators(",") # "1,234,567.89"
BigDecimal("-9876543210.123456").to_string_with_separators()
# "-9_876_543_210.123_456"
```
"""
return self.to_string(delimiter=separator)

# ===------------------------------------------------------------------=== #
# Basic unary operation dunders
# neg
Expand Down Expand Up @@ -1350,6 +1370,34 @@ struct BigDecimal(
"""
return decimo.bigdecimal.rounding.quantize(self, exp, rounding_mode)

fn fma(self, a: Self, b: Self) raises -> Self:
"""Fused multiply-add: returns `self * a + b` with no intermediate
rounding.

Matches Python's `decimal.Decimal.fma(other, third)`.

Because BigDecimal multiplication and addition are both exact
(no precision loss), this method is semantically equivalent to
`self * a + b`. It is provided for IEEE 754 / Python `Decimal`
API compatibility and to express intent clearly in numerical
algorithms.

Args:
a: The value to multiply by.
b: The value to add after multiplication.

Returns:
The exact result of `self * a + b`.

Examples:

```
BigDecimal("2").fma(BigDecimal("3"), BigDecimal("4")) # 10 (2*3+4)
BigDecimal("1.5").fma(BigDecimal("2"), BigDecimal("0.1")) # 3.1
```
"""
return self * a + b

# ===------------------------------------------------------------------=== #
# Other methods
# ===------------------------------------------------------------------=== #
Expand Down Expand Up @@ -1480,6 +1528,35 @@ struct BigDecimal(
"""
return self.scale == other.scale

@always_inline
fn scaleb(self, n: Int) -> Self:
"""Multiplies the value by 10^n by adjusting the scale.

Matches Python's `decimal.Decimal.scaleb(other)`. The name
comes from the IEEE 754 "scaleB" (scale Binary/Base) operation.

This operation is O(1) — it only changes the exponent without
touching the coefficient digits.

Args:
n: The power of ten to scale by. Positive values make the
number larger, negative values make it smaller.

Returns:
A new BigDecimal whose value is `self * 10^n`.

Examples:

```
BigDecimal("1.23").scaleb(2) # 123
BigDecimal("1.23").scaleb(-2) # 0.0123
BigDecimal("100").scaleb(-1) # 10
```
"""
var result = self.copy()
result.scale -= n
return result^

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
39 changes: 39 additions & 0 deletions src/decimo/bigint/bigint.mojo
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ struct BigInt(
Absable,
Comparable,
Copyable,
FloatableRaising,
IntableRaising,
Movable,
Representable,
Expand Down Expand Up @@ -466,6 +467,18 @@ struct BigInt(
"""
return self.to_int()

fn __float__(self) raises -> Float64:
"""Converts the BigInt to a floating-point number.

Matches Python's `float(n)` for `int` objects.

Note: Large values may lose precision or overflow to `inf`.

Returns:
The value as a Float64.
"""
return Float64(String(self))

Comment on lines +480 to +481
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

__float__ converts via Float64(String(self)), which forces a full base-2^32 → decimal string conversion and then reparses it. For very large BigInt values this is substantially more expensive (time + memory) than converting from the binary words directly; consider a word-based Float64 conversion to avoid the intermediate string if this becomes a hotspot.

Suggested change
return Float64(String(self))
var result = 0.0
# Process words from most significant to least significant.
# Each word contributes 32 bits in base-2^32 representation.
for i in range(len(self.words) - 1, -1, -1):
result = result * 4294967296.0 + Float64(self.words[i])
# Apply sign (assuming negative values have self.sign < 0).
if self.sign < 0:
result = -result
return result

Copilot uses AI. Check for mistakes.
fn __str__(self) -> String:
"""Returns a decimal string representation of the BigInt."""
return self.to_string()
Expand Down Expand Up @@ -1238,6 +1251,32 @@ struct BigInt(

return (n_words - 1) * 32 + bits_in_msw

fn bit_count(self) -> Int:
"""Returns the number of ones in the binary representation of the
absolute value (population count).

Matches Python 3.10+ `int.bit_count()`.

Returns:
The number of set bits in the magnitude, or 0 if the value is zero.

Examples:

```
BigInt(13).bit_count() # 3 (13 = 0b1101)
BigInt(-7).bit_count() # 3 (7 = 0b111)
BigInt(0).bit_count() # 0
```
"""
var count = 0
for i in range(len(self.words)):
var w = self.words[i]
# Kernighan's bit-counting trick
while w != 0:
w &= w - 1
count += 1
return count
Comment on lines +1271 to +1278
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bit_count() iterates over all self.words. Other methods (e.g., to_string) compute an effective word count that trims most-significant zero words; if BigInt instances can contain such trailing zeros, bit_count() will do unnecessary work. Consider trimming/using an eff_words count before counting bits.

Copilot uses AI. Check for mistakes.

fn number_of_words(self) -> Int:
"""Returns the number of words in the magnitude."""
return len(self.words)
Expand Down
94 changes: 94 additions & 0 deletions tests/bigdecimal/test_bigdecimal_methods.mojo
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# Consider incorporating some of the tests to other test files in future
"""
Tests for BigDecimal utility methods added in v0.8.x:
- is_positive()
Expand All @@ -8,6 +9,9 @@ Tests for BigDecimal utility methods added in v0.8.x:
- copy_abs() / copy_negate() / copy_sign()
- adjusted()
- same_quantum()
- scaleb()
- fma()
- to_string_with_separators()
"""

import testing
Expand Down Expand Up @@ -467,5 +471,95 @@ fn test_same_quantum_zero_variants() raises:
testing.assert_false(BigDecimal("0").same_quantum(BigDecimal("0.0")))


# ===----------------------------------------------------------------------=== #
# scaleb()
# ===----------------------------------------------------------------------=== #


fn test_scaleb_positive() raises:
"""Tests scaleb with positive n multiplies value by 10^n."""
testing.assert_equal(String(BigDecimal("1.23").scaleb(2)), "123")
testing.assert_equal(String(BigDecimal("5").scaleb(3)), "5E+3")


fn test_scaleb_negative() raises:
"""Tests scaleb with negative n divides value by 10^|n|."""
testing.assert_equal(String(BigDecimal("1.23").scaleb(-2)), "0.0123")
testing.assert_equal(String(BigDecimal("100").scaleb(-1)), "10.0")


fn test_scaleb_zero() raises:
"""Tests scaleb(0) returns the same value."""
testing.assert_equal(String(BigDecimal("42.5").scaleb(0)), "42.5")


fn test_scaleb_on_zero() raises:
"""Tests scaleb on zero adjusts scale but value stays zero."""
var result = BigDecimal("0").scaleb(5)
testing.assert_true(result.is_zero())


# ===----------------------------------------------------------------------=== #
# fma()
# ===----------------------------------------------------------------------=== #


fn test_fma_basic() raises:
"""Tests fma(a, b) = self * a + b."""
var result = BigDecimal("2").fma(BigDecimal("3"), BigDecimal("4"))
testing.assert_equal(String(result), "10")


fn test_fma_decimal() raises:
"""Tests fma with fractional values."""
var result = BigDecimal("1.5").fma(BigDecimal("2"), BigDecimal("0.1"))
testing.assert_equal(String(result), "3.1")


fn test_fma_negative() raises:
"""Tests fma with negative values."""
var result = BigDecimal("-3").fma(BigDecimal("4"), BigDecimal("15"))
testing.assert_equal(String(result), "3")


fn test_fma_zero_multiplier() raises:
"""Tests fma with zero multiplier: 0 * a + b = b."""
var result = BigDecimal("0").fma(BigDecimal("999"), BigDecimal("42"))
testing.assert_equal(String(result), "42")


# ===----------------------------------------------------------------------=== #
# to_string_with_separators()
# ===----------------------------------------------------------------------=== #


fn test_to_string_with_separators_default() raises:
"""Tests to_string_with_separators with default separator."""
testing.assert_equal(
BigDecimal("1234567").to_string_with_separators(), "1_234_567"
)


fn test_to_string_with_separators_comma() raises:
"""Tests to_string_with_separators with custom comma separator."""
testing.assert_equal(
BigDecimal("1234567.89").to_string_with_separators(","),
"1,234,567.89",
)


fn test_to_string_with_separators_small() raises:
"""Tests to_string_with_separators with numbers having fewer than 4 digits.
"""
testing.assert_equal(BigDecimal("123").to_string_with_separators(), "123")


fn test_to_string_with_separators_negative() raises:
"""Tests to_string_with_separators with negative numbers."""
testing.assert_equal(
BigDecimal("-1234567").to_string_with_separators(), "-1_234_567"
)


fn main() raises:
testing.TestSuite.discover_tests[__functions_in_module()]().run()
43 changes: 43 additions & 0 deletions tests/bigint/test_bigint_bitwise.mojo
Original file line number Diff line number Diff line change
Expand Up @@ -416,5 +416,48 @@ fn test_word_boundary_values() raises:
testing.assert_equal(String(~w64), "-18446744073709551616")


# ===----------------------------------------------------------------------=== #
# Test: bit_count() — population count
# ===----------------------------------------------------------------------=== #


fn test_bit_count_zero() raises:
"""Tests bit_count(0) = 0."""
testing.assert_equal(BigInt(0).bit_count(), 0)


fn test_bit_count_one() raises:
"""Tests bit_count(1) = 1."""
testing.assert_equal(BigInt(1).bit_count(), 1)


fn test_bit_count_powers_of_two() raises:
"""Powers of 2 have exactly 1 bit set."""
testing.assert_equal(BigInt(2).bit_count(), 1)
testing.assert_equal(BigInt(4).bit_count(), 1)
testing.assert_equal(BigInt(1024).bit_count(), 1)


fn test_bit_count_small() raises:
"""Small values: 7 = 0b111 (3 bits), 13 = 0b1101 (3 bits)."""
testing.assert_equal(BigInt(7).bit_count(), 3)
testing.assert_equal(BigInt(13).bit_count(), 3)
testing.assert_equal(BigInt(15).bit_count(), 4)
testing.assert_equal(BigInt(255).bit_count(), 8)


fn test_bit_count_negative() raises:
"""Tests bit_count on negative values counts bits in the magnitude."""
testing.assert_equal(BigInt(-7).bit_count(), 3)
testing.assert_equal(BigInt(-1).bit_count(), 1)


fn test_bit_count_large() raises:
"""2^32 - 1 = 0xFFFFFFFF has 32 bits set."""
var n = BigInt(1) << 32
n = n - BigInt(1) # 2^32 - 1
testing.assert_equal(n.bit_count(), 32)


fn main() raises:
testing.TestSuite.discover_tests[__functions_in_module()]().run()
20 changes: 19 additions & 1 deletion tests/bigint/test_bigint_conversion.mojo
Original file line number Diff line number Diff line change
Expand Up @@ -267,11 +267,29 @@ fn test_from_string_non_integer_raises() raises:


fn test_from_string_plus_sign() raises:
"""Test from_string handles explicit positive sign."""
"""Tests from_string handles explicit positive sign."""
testing.assert_equal(String(BigInt("+42")), "42")
testing.assert_equal(String(BigInt("+0")), "0")
testing.assert_equal(String(BigInt("+1,000")), "1000")


# ===----------------------------------------------------------------------=== #
# Test: __float__
# ===----------------------------------------------------------------------=== #


fn test_float_small() raises:
"""Tests __float__ with small integers."""
testing.assert_equal(Float64(BigInt(0)), 0.0)
testing.assert_equal(Float64(BigInt(1)), 1.0)
testing.assert_equal(Float64(BigInt(42)), 42.0)
testing.assert_equal(Float64(BigInt(-7)), -7.0)


fn test_float_large() raises:
"""Tests __float__ with a large-ish integer."""
testing.assert_equal(Float64(BigInt(1000000)), 1000000.0)


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