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
50 changes: 41 additions & 9 deletions src/decimo/bigdecimal/bigdecimal.mojo
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import decimo.bigdecimal.comparison as bigdecimal_comparison
import decimo.bigdecimal.constants as bigdecimal_constants
import decimo.bigdecimal.exponential as bigdecimal_exponential
import decimo.bigdecimal.rounding as bigdecimal_rounding
import decimo.bigdecimal.special as bigdecimal_special
import decimo.bigdecimal.trigonometric as bigdecimal_trigonometric
import decimo.biguint.arithmetics as biguint_arithmetics

Expand Down Expand Up @@ -194,7 +195,7 @@ struct BigDecimal(

@implicit
def __init__(out self, value: BigInt10):
"""Constructs a BigDecimal from a big integer.
"""Constructs a BigDecimal from a base-10 big integer.

Args:
value: The `BigInt10` to convert.
Expand Down Expand Up @@ -1566,22 +1567,18 @@ struct BigDecimal(
return truncated^

def __trunc__(self) raises -> Self:
"""Returns self truncated toward zero (removes fractional part).
"""Returns self truncated toward zero (removes the fractional part).

Equivalent to `math.trunc()` in Python. Returns a BigDecimal
with scale 0.
Equivalent to `math.trunc()` in Python; delegates to `truncate()`.
Returns a BigDecimal with scale 0.

Returns:
The truncated integer part of this value.

Raises:
Error: If the underlying rounding operation fails.
"""
if self.scale <= 0:
return self.copy()
return bigdecimal_rounding.round(
self, ndigits=0, rounding_mode=RoundingMode.down()
)
return self.truncate()

# ===------------------------------------------------------------------=== #
# Basic arithmetic operation without dunders
Expand Down Expand Up @@ -1850,6 +1847,24 @@ struct BigDecimal(
"""
return bigdecimal_exponential.exp(self, precision)

def factorial(self, precision: Int = 0) raises -> Self:
"""Returns the factorial of this value (`self!`).

Args:
precision: Significant digits for the result. `0` (the default)
computes the exact factorial with no rounding. A positive
Comment thread
forfudan marked this conversation as resolved.
value rounds intermediate products to keep the computation
bounded and returns `precision` correct significant digits.

Returns:
`self!` (`0! == 1`). Exact when `precision == 0`.

Raises:
ValueError: If `self` is not an integer, is negative, or is
larger than 10^6.
"""
return bigdecimal_special.factorial(self, precision)

@always_inline
def ln(self, precision: Int = PRECISION) raises -> Self:
"""Returns the natural logarithm of the BigDecimal number.
Expand Down Expand Up @@ -2309,6 +2324,23 @@ struct BigDecimal(
"""
return self * a + b

def truncate(self) raises -> Self:
"""Returns self truncated toward zero (removes the fractional part).

Returns a BigDecimal with scale 0.

Returns:
The truncated integer part of this value.

Raises:
Error: If the underlying rounding operation fails.
"""
if self.scale <= 0:
return self.copy()
return bigdecimal_rounding.round(
self, ndigits=0, rounding_mode=RoundingMode.down()
)

# ===------------------------------------------------------------------=== #
# Other methods
# ===------------------------------------------------------------------=== #
Expand Down
100 changes: 100 additions & 0 deletions src/decimo/bigdecimal/special.mojo
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# ===----------------------------------------------------------------------=== #
# Copyright 2025-2026 Yuhao Zhu
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ===----------------------------------------------------------------------=== #
#
# Implements special functions for the BigDecimal type
#
# ===----------------------------------------------------------------------=== #

"""Implements functions for special operations on BigDecimal objects."""

from decimo.bigdecimal.bigdecimal import BigDecimal
from decimo.errors import ValueError

# Extra significant digits carried during a rounded factorial, on top of the
# requested precision and the digit count of `n`. Covers the rounding error
# that accumulates over the `n` intermediate products.
comptime FACTORIAL_GUARD_DIGITS = 9 # word size

# Largest argument accepted by `factorial`. Even 10^6 already needs ~10^6
# multiplications, so anything beyond it is impractical with the simple
# iterative product. The cap also keeps the value within Mojo's `Int` range,
# so an out-of-range argument raises a clear error instead of an `Int`
# overflow. (A faster algorithm, e.g. binary splitting, could lift this.)
comptime FACTORIAL_MAX_INPUT = 1_000_000


def factorial(x: BigDecimal, precision: Int = 0) raises -> BigDecimal:
"""Calculates the factorial of a non-negative integer value.

Args:
x: The non-negative integer value to take the factorial of.
precision: Significant digits for the result. `0` (the default)
computes the exact factorial with no rounding. A positive value
rounds the intermediate products to a bounded working width,
which lowers the cost for large `x`, and returns `precision`
correct significant digits.

Returns:
`x!`, the product of all positive integers up to `x` (`0! == 1`).
Exact when `precision == 0`.

Raises:
ValueError: If `x` is not an integer, is negative, or is larger than
`FACTORIAL_MAX_INPUT` (10^6).

Notes:

The value must currently fit in a Mojo `Int`. Arbitrarily large
arguments will be supported later.
"""
if not x.is_integer():
raise ValueError(
message="Factorial is only defined for integer values.",
function="factorial()",
)
if x < BigDecimal(0):
raise ValueError(
message="Factorial is not defined for negative numbers.",
function="factorial()",
)
if x > BigDecimal(FACTORIAL_MAX_INPUT):
raise ValueError(
message=(
"Factorial argument is too large to compute (must be <= 10^6)."
),
function="factorial()",
)

# `truncate` gives a scale-0 BigDecimal, so integer values written with a
# fractional part (e.g. "5.00") convert cleanly to `Int`.
var n = Int(x.truncate())
if precision <= 0:
# Exact: full-width products, no rounding.
var result = BigDecimal(1)
for i in range(2, n + 1):
result = result.multiply(BigDecimal(i))
return result^
Comment thread
forfudan marked this conversation as resolved.

# Rounded: keep every product at `precision + guard` significant digits,
# where the guard also grows with the number of digits in `n`. Round the
# final result back to `precision` (HALF_EVEN, via multiply-by-one).
var working_precision = (
precision + String(n).byte_length() + FACTORIAL_GUARD_DIGITS
)
var result = BigDecimal(1)
for i in range(2, n + 1):
result = result.multiply(BigDecimal(i), working_precision)
return result.multiply(BigDecimal(1), precision)
2 changes: 2 additions & 0 deletions src/decimo/bigint/__init__.mojo
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,6 @@ Modules:
- bitwise: AND, OR, XOR, NOT (Python two's complement semantics)
- comparison: compare, greater, less, equal
- exponential: sqrt, isqrt
- number_theory: gcd, extended_gcd, lcm, mod_pow, mod_inverse
- special: factorial (and future special functions)
"""
15 changes: 15 additions & 0 deletions src/decimo/bigint/bigint.mojo
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import decimo.bigint.bitwise as bigint_bitwise
import decimo.bigint.comparison as bigint_comparison
import decimo.bigint.exponential as bigint_exponential
import decimo.bigint.number_theory as bigint_number_theory
import decimo.bigint.special as bigint_special
import decimo.str as decimo_str
from decimo.bigint10.bigint10 import BigInt10
from decimo.biguint.biguint import BigUInt
Expand Down Expand Up @@ -219,6 +220,9 @@ struct BigInt(
# Constructing methods that are not dunders
# ===------------------------------------------------------------------=== #

# IMPORTANT:
# This function will be removed in the future when `Int` type is
# a comptime alias of `SIMD[DType.int, 1]`.
@staticmethod
def from_int(value: Int) -> Self:
"""Creates a BigInt from a Mojo Int.
Expand Down Expand Up @@ -1467,6 +1471,17 @@ struct BigInt(
"""
return bigint_exponential.sqrt(self)

def factorial(self) raises -> Self:
"""Returns the factorial of this value.

Returns:
`self!` (`0! == 1`).
Comment thread
forfudan marked this conversation as resolved.

Raises:
ValueError: If `self` is negative or larger than 10^6.
"""
return bigint_special.factorial(self)

@always_inline
def compare_magnitudes(self, other: Self) -> Int8:
"""Compares the magnitudes (absolute values) of two BigInt numbers.
Expand Down
69 changes: 69 additions & 0 deletions src/decimo/bigint/special.mojo
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# ===----------------------------------------------------------------------=== #
# Copyright 2025-2026 Yuhao Zhu
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ===----------------------------------------------------------------------=== #
#
# Implements special functions for the BigInt type
#
# ===----------------------------------------------------------------------=== #

"""Implements functions for special operations on BigInt objects."""

from decimo.bigint.bigint import BigInt
from decimo.errors import ValueError

# Largest argument accepted by `factorial`. Even 10^6 already needs ~10^6
# multiplications, so anything beyond it is impractical with the simple
# iterative product. The cap also keeps the value within Mojo's `Int` range,
# so an out-of-range argument raises a clear error instead of an `Int`
# overflow. (A faster algorithm, e.g. binary splitting, could lift this.)
comptime FACTORIAL_MAX_INPUT = 1_000_000


def factorial(x: BigInt) raises -> BigInt:
"""Calculates the factorial of a non-negative integer value.

Args:
x: The non-negative integer value to take the factorial of.

Returns:
`x!`, the product of all positive integers up to `x` (`0! == 1`).

Raises:
ValueError: If `x` is negative or larger than `FACTORIAL_MAX_INPUT`
(10^6).

Notes:

The value must currently fit in a Mojo `Int`. Arbitrarily large
arguments will be supported later.
"""
if x < BigInt.zero():
raise ValueError(
message="Factorial is not defined for negative numbers.",
function="factorial()",
)
if x > BigInt(FACTORIAL_MAX_INPUT):
raise ValueError(
message=(
"Factorial argument is too large to compute (must be <= 10^6)."
),
function="factorial()",
)

var n = Int(x)
var result = BigInt.one()
for i in range(2, n + 1):
result *= BigInt(i)
return result^
Comment thread
forfudan marked this conversation as resolved.
65 changes: 65 additions & 0 deletions tests/bigdecimal/test_bigdecimal_special.mojo
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# ===----------------------------------------------------------------------=== #
# Test BigDecimal special functions (factorial)
# ===----------------------------------------------------------------------=== #

from std import testing
from decimo.bigdecimal.bigdecimal import BigDecimal


def test_factorial_exact() raises:
"""Test exact factorial (precision == 0)."""
testing.assert_equal(String(BigDecimal(0).factorial()), "1")
testing.assert_equal(String(BigDecimal(1).factorial()), "1")
testing.assert_equal(String(BigDecimal(5).factorial()), "120")
testing.assert_equal(String(BigDecimal(10).factorial()), "3628800")
testing.assert_equal(
String(BigDecimal(30).factorial()),
"265252859812191058636308480000000",
)


def test_factorial_integer_with_scale() raises:
"""Test that an integer value written with a fractional part (e.g.
"5.00") is accepted."""
testing.assert_equal(String(BigDecimal("5.00").factorial()), "120")


def test_factorial_rounded() raises:
"""Test the rounded mode returns `precision` significant digits."""
testing.assert_equal(
String(BigDecimal(30).factorial(10)), "2.652528598E+32"
)


def test_factorial_non_integer_raises() raises:
"""Test that a non-integer argument raises."""
var raised = False
try:
_ = BigDecimal("5.5").factorial()
except:
raised = True
testing.assert_true(raised, "factorial of a non-integer should raise")


def test_factorial_negative_raises() raises:
"""Test that a negative argument raises."""
var raised = False
try:
_ = BigDecimal(-1).factorial()
except:
raised = True
testing.assert_true(raised, "factorial of a negative value should raise")


def test_factorial_too_large_raises() raises:
"""Test that an argument above the cap raises."""
var raised = False
try:
_ = BigDecimal(2_000_000).factorial() # above the 10^6 cap
except:
raised = True
testing.assert_true(raised, "factorial above the cap should raise")


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