diff --git a/README.md b/README.md index 08a8696..d9d305b 100644 --- a/README.md +++ b/README.md @@ -12,11 +12,17 @@ The core types are: - A 128-bit fixed-point decimal implementation (`Decimal`) supporting up to 29 significant digits with a maximum of 28 decimal places[^fixed]. It features a complete set of mathematical functions including logarithms, exponentiation, roots, and trigonometric operations. - A base-10 arbitrary-precision signed integer type (`BigInt`) and a base-10 arbitrary-precision unsigned integer type (`BigUInt`) supporting unlimited digits[^integer]. It features comprehensive arithmetic operations, comparison functions, and supports extremely large integer calculations efficiently. - -The library is expanding to include `BigDecimal` types that support arbitrary precision[^arbitrary], allowing for calculations with unlimited digits and decimal places. These extensions are currently under active development. +- An arbitrary-precision decimal implementation `BigDecimal` allowing for calculations with unlimited digits and decimal places[^arbitrary]. It is currently under active development. This repository includes [TOMLMojo](https://github.com/forfudan/decimojo/tree/main/src/tomlmojo), a lightweight TOML parser in pure Mojo. It parses configuration files and test data, supporting basic types, arrays, and nested tables. While created for DeciMojo's testing framework, it offers general-purpose structured data parsing with a clean, simple API. +| type | information | internal representation | +| ------------ | ------------------------------------ | ------------------------ | +| `BigUInt` | arbitrary-precision unsigned integer | `List[UInt32]` | +| `BigInt` | arbitrary-precision integer | `BigUInt`, `Bool` | +| `Decimal` | 128-bit fixed-precision decimal | 4 `UInt32` words | +| `BigDecimal` | arbitrary-precision decimal | `BigUInt`, `Int`, `Bool` | + ## Installation DeciMojo is available in the [modular-community](https://repo.prefix.dev/modular-community) package repository. You can install it using any of these methods: diff --git a/benches/bigdecimal/bench.mojo b/benches/bigdecimal/bench.mojo index 1248083..7819b10 100644 --- a/benches/bigdecimal/bench.mojo +++ b/benches/bigdecimal/bench.mojo @@ -1,5 +1,6 @@ from bench_bigdecimal_add import main as bench_add from bench_bigdecimal_subtract import main as bench_sub +from bench_bigdecimal_multiply import main as bench_multiply fn main() raises: @@ -10,6 +11,7 @@ This is the BigInt Benchmarks ========================================= add: Add sub: Subtract +mul: Multiply all: Run all benchmarks q: Exit ========================================= @@ -20,9 +22,12 @@ q: Exit bench_add() elif command == "sub": bench_sub() + elif command == "mul": + bench_multiply() elif command == "all": bench_add() bench_sub() + bench_multiply() elif command == "q": return else: diff --git a/benches/bigdecimal/bench_bigdecimal_multiply.mojo b/benches/bigdecimal/bench_bigdecimal_multiply.mojo new file mode 100644 index 0000000..36ce00e --- /dev/null +++ b/benches/bigdecimal/bench_bigdecimal_multiply.mojo @@ -0,0 +1,730 @@ +""" +Comprehensive benchmarks for BigDecimal multiplication. +Compares performance against Python's decimal module with 50 diverse test cases. +""" + +from decimojo import BigDecimal, RoundingMode +from python import Python, PythonObject +from time import perf_counter_ns +import time +import os +from collections import List + + +fn open_log_file() raises -> PythonObject: + """ + Creates and opens a log file with a timestamp in the filename. + + Returns: + A file object opened for writing. + """ + var python = Python.import_module("builtins") + var datetime = Python.import_module("datetime") + + # Create logs directory if it doesn't exist + var log_dir = "./logs" + if not os.path.exists(log_dir): + os.makedirs(log_dir) + + # Generate a timestamp for the filename + var timestamp = String(datetime.datetime.now().isoformat()) + var log_filename = log_dir + "/benchmark_bigdecimal_multiply_" + timestamp + ".log" + + print("Saving benchmark results to:", log_filename) + return python.open(log_filename, "w") + + +fn log_print(msg: String, log_file: PythonObject) raises: + """ + Prints a message to both the console and the log file. + + Args: + msg: The message to print. + log_file: The file object to write to. + """ + print(msg) + log_file.write(msg + "\n") + log_file.flush() # Ensure the message is written immediately + + +fn run_benchmark_multiply( + name: String, + value1: String, + value2: String, + iterations: Int, + log_file: PythonObject, + mut speedup_factors: List[Float64], +) raises: + """ + Run a benchmark comparing Mojo BigDecimal multiplication with Python Decimal multiplication. + + Args: + name: Name of the benchmark case. + value1: String representation of first operand. + value2: String representation of second operand. + iterations: Number of iterations to run. + log_file: File object for logging results. + speedup_factors: Mojo List to store speedup factors for averaging. + """ + log_print("\nBenchmark: " + name, log_file) + log_print("First operand: " + value1, log_file) + log_print("Second operand: " + value2, log_file) + + # Set up Mojo and Python values + var mojo_value1 = BigDecimal(value1) + var mojo_value2 = BigDecimal(value2) + var pydecimal = Python.import_module("decimal") + var py_value1 = pydecimal.Decimal(value1) + var py_value2 = pydecimal.Decimal(value2) + + # Execute the operations once to verify correctness + try: + var mojo_result = mojo_value1 * mojo_value2 + var py_result = py_value1 * py_value2 + + # Display results for verification + log_print("Mojo result: " + String(mojo_result), log_file) + log_print("Python result: " + String(py_result), log_file) + + # Benchmark Mojo implementation + var t0 = perf_counter_ns() + for _ in range(iterations): + _ = mojo_value1 * mojo_value2 + var mojo_time = (perf_counter_ns() - t0) / iterations + if mojo_time == 0: + mojo_time = 1 # Prevent division by zero + + # Benchmark Python implementation + t0 = perf_counter_ns() + for _ in range(iterations): + _ = py_value1 * py_value2 + var python_time = (perf_counter_ns() - t0) / iterations + + # Calculate speedup factor + var speedup = python_time / mojo_time + speedup_factors.append(Float64(speedup)) + + # Print results with speedup comparison + log_print( + "Mojo multiplication: " + String(mojo_time) + " ns per iteration", + log_file, + ) + log_print( + "Python multiplication: " + + String(python_time) + + " ns per iteration", + log_file, + ) + log_print("Speedup factor: " + String(speedup), log_file) + except e: + log_print("Error occurred during benchmark: " + String(e), log_file) + log_print("Skipping this benchmark case", log_file) + + +fn main() raises: + # Open log file + var log_file = open_log_file() + var datetime = Python.import_module("datetime") + + # Create a Mojo List to store speedup factors for averaging later + var speedup_factors = List[Float64]() + + # Display benchmark header with system information + log_print("=== DeciMojo BigDecimal Multiplication Benchmark ===", log_file) + log_print("Time: " + String(datetime.datetime.now().isoformat()), log_file) + + # Try to get system info + try: + var platform = Python.import_module("platform") + log_print( + "System: " + + String(platform.system()) + + " " + + String(platform.release()), + log_file, + ) + log_print("Processor: " + String(platform.processor()), log_file) + log_print( + "Python version: " + String(platform.python_version()), log_file + ) + except: + log_print("Could not retrieve system information", log_file) + + var iterations = 500 # Fewer iterations for multiplication (may be slower) + var pydecimal = Python().import_module("decimal") + + # Set Python decimal precision to match Mojo's + pydecimal.getcontext().prec = 28 + log_print( + "Python decimal precision: " + String(pydecimal.getcontext().prec), + log_file, + ) + log_print("Mojo decimal precision: 28", log_file) + + # Define benchmark cases + log_print( + "\nRunning decimal multiplication benchmarks with " + + String(iterations) + + " iterations each", + log_file, + ) + + # === BASIC DECIMAL MULTIPLICATION TESTS === + + # Case 1: Simple integer multiplication + run_benchmark_multiply( + "Simple integer multiplication", + "7", + "6", + iterations, + log_file, + speedup_factors, + ) + + # Case 2: Simple decimal multiplication + run_benchmark_multiply( + "Simple decimal multiplication", + "3.5", + "2.5", + iterations, + log_file, + speedup_factors, + ) + + # Case 3: Multiplication with different scales + run_benchmark_multiply( + "Multiplication with different scales", + "1.5", + "0.25", + iterations, + log_file, + speedup_factors, + ) + + # Case 4: Multiplication with very different scales + run_benchmark_multiply( + "Multiplication with very different scales", + "1.23456789", + "0.01", + iterations, + log_file, + speedup_factors, + ) + + # Case 5: Multiplication by zero + run_benchmark_multiply( + "Multiplication by zero", + "123.456", + "0", + iterations, + log_file, + speedup_factors, + ) + + # Case 6: Multiplication by one + run_benchmark_multiply( + "Multiplication by one", + "123.456", + "1", + iterations, + log_file, + speedup_factors, + ) + + # Case 7: Multiplication by negative one + run_benchmark_multiply( + "Multiplication by negative one", + "123.456", + "-1", + iterations, + log_file, + speedup_factors, + ) + + # === SCALE AND PRECISION TESTS === + + # Case 8: Precision at decimal limit + run_benchmark_multiply( + "Precision at decimal limit", + "1.2345678901234567890123456789", + "9.8765432109876543210987654321", + iterations, + log_file, + speedup_factors, + ) + + # Case 9: Multiplication resulting in scale increase + run_benchmark_multiply( + "Multiplication resulting in scale increase", + "123.456789", + "987.654321", + iterations, + log_file, + speedup_factors, + ) + + # Case 10: Multiplication with high precision + run_benchmark_multiply( + "Multiplication with high precision", + "0.33333333333333333333333333", + "3", + iterations, + log_file, + speedup_factors, + ) + + # Case 11: Multiplication resulting in exact integer + run_benchmark_multiply( + "Multiplication resulting in exact integer", + "0.5", + "2", + iterations, + log_file, + speedup_factors, + ) + + # Case 12: Multiplication with scientific notation + run_benchmark_multiply( + "Multiplication with scientific notation", + "1.23e5", + "4.56e2", + iterations, + log_file, + speedup_factors, + ) + + # === SIGN COMBINATION TESTS === + + # Case 13: Negative * Positive + run_benchmark_multiply( + "Negative * Positive", + "-3.14", + "10", + iterations, + log_file, + speedup_factors, + ) + + # Case 14: Positive * Negative + run_benchmark_multiply( + "Positive * Negative", + "10", + "-3.14", + iterations, + log_file, + speedup_factors, + ) + + # Case 15: Negative * Negative + run_benchmark_multiply( + "Negative * Negative", + "-5.75", + "-10.25", + iterations, + log_file, + speedup_factors, + ) + + # Case 16: Zero * Negative + run_benchmark_multiply( + "Zero * Negative", + "0", + "-123.456", + iterations, + log_file, + speedup_factors, + ) + + # Case 17: Negative * Zero + run_benchmark_multiply( + "Negative * Zero", + "-123.456", + "0", + iterations, + log_file, + speedup_factors, + ) + + # === LARGE NUMBER TESTS === + + # Case 18: Large integer multiplication + run_benchmark_multiply( + "Large integer multiplication", + "9999999", + "9999999", + iterations, + log_file, + speedup_factors, + ) + + # Case 19: Large negative * positive + run_benchmark_multiply( + "Large negative * positive", + "-9999999999", + "1234567890", + iterations, + log_file, + speedup_factors, + ) + + # Case 20: Large decimal multiplication + run_benchmark_multiply( + "Large decimal multiplication", + "12345.6789", + "98765.4321", + iterations, + log_file, + speedup_factors, + ) + + # Case 21: Very large * very small + run_benchmark_multiply( + "Very large * very small", + "1" + "0" * 20, # 10^20 + "0." + "0" * 20 + "1", # 10^-21 + iterations, + log_file, + speedup_factors, + ) + + # Case 22: Extreme scales (large positive exponents) + run_benchmark_multiply( + "Extreme scales (large positive exponents)", + "1.23e10", + "4.56e10", + iterations, + log_file, + speedup_factors, + ) + + # === SMALL NUMBER TESTS === + + # Case 23: Very small positive values + run_benchmark_multiply( + "Very small positive values", + "0." + "0" * 15 + "1", + "0." + "0" * 15 + "2", + iterations, + log_file, + speedup_factors, + ) + + # Case 24: Very small negative values + run_benchmark_multiply( + "Very small negative values", + "-0." + "0" * 15 + "1", + "-0." + "0" * 15 + "2", + iterations, + log_file, + speedup_factors, + ) + + # Case 25: Small values with different scales + run_benchmark_multiply( + "Small values with different scales", + "0." + "0" * 10 + "1", + "0." + "0" * 5 + "1", + iterations, + log_file, + speedup_factors, + ) + + # Case 26: Extreme scales (large negative exponents) + run_benchmark_multiply( + "Extreme scales (large negative exponents)", + "1.23e-10", + "4.56e-10", + iterations, + log_file, + speedup_factors, + ) + + # Case 27: Multiplication with extreme exponent difference + run_benchmark_multiply( + "Multiplication with extreme exponent difference", + "1.23e-10", + "4.56e10", + iterations, + log_file, + speedup_factors, + ) + + # === SPECIAL VALUE TESTS === + + # Case 28: Multiplication of exact mathematical constants + run_benchmark_multiply( + "Multiplication of exact mathematical constants (PI * E)", + "3.14159265358979323846264338328", + "2.71828182845904523536028747135", + iterations, + log_file, + speedup_factors, + ) + + # Case 29: Multiply by 0.1 (recurring binary) + run_benchmark_multiply( + "Multiply by 0.1 (recurring binary)", + "42.5", + "0.1", + iterations, + log_file, + speedup_factors, + ) + + # Case 30: Multiply by 0.01 (recurring binary) + run_benchmark_multiply( + "Multiply by 0.01 (recurring binary)", + "42.5", + "0.01", + iterations, + log_file, + speedup_factors, + ) + + # Case 31: Multiplication with repeating decimals + run_benchmark_multiply( + "Multiplication with repeating decimals", + "0.333333333333333333333333333", + "0.666666666666666666666666667", + iterations, + log_file, + speedup_factors, + ) + + # Case 32: Multiplication by 10 (shift operation) + run_benchmark_multiply( + "Multiplication by 10 (shift operation)", + "123.456", + "10", + iterations, + log_file, + speedup_factors, + ) + + # === PRECISION BOUNDARY TESTS === + + # Case 33: Multiplication requiring rounding + run_benchmark_multiply( + "Multiplication requiring rounding", + "9.9999999999999999999999999", + "9.9999999999999999999999999", + iterations, + log_file, + speedup_factors, + ) + + # Case 34: Multiplication with trailing zeros + run_benchmark_multiply( + "Multiplication with trailing zeros", + "1.5000000000000000000000000", + "2.0000000000000000000000000", + iterations, + log_file, + speedup_factors, + ) + + # Case 35: Binary-friendly multiplication + run_benchmark_multiply( + "Binary-friendly multiplication", + "0.125", # 1/8 + "8", + iterations, + log_file, + speedup_factors, + ) + + # Case 36: Decimal-friendly multiplication + run_benchmark_multiply( + "Decimal-friendly multiplication", + "0.2", + "5", + iterations, + log_file, + speedup_factors, + ) + + # Case 37: Multiplication at precision boundary + run_benchmark_multiply( + "Multiplication at precision boundary", + "1" + "0" * 14, # 10^14 + "1" + "0" * 14, # 10^14 + iterations, + log_file, + speedup_factors, + ) + + # === APPLICATION-SPECIFIC TESTS === + + # Case 38: Financial calculation (price * quantity) + run_benchmark_multiply( + "Financial calculation (price * quantity)", + "19.99", # Price + "12", # Quantity + iterations, + log_file, + speedup_factors, + ) + + # Case 39: Scientific measurement (unit conversion) + run_benchmark_multiply( + "Scientific measurement (unit conversion)", + "299792458", # Speed of light in m/s + "0.000000001", # Convert to km/µs + iterations, + log_file, + speedup_factors, + ) + + # Case 40: Area calculation (length * width) + run_benchmark_multiply( + "Area calculation (length * width)", + "14.75", # Length in meters + "8.25", # Width in meters + iterations, + log_file, + speedup_factors, + ) + + # Case 41: Financial percentage (principal * rate) + run_benchmark_multiply( + "Financial percentage (principal * rate)", + "10000.00", # Principal + "0.0425", # Interest rate (4.25%) + iterations, + log_file, + speedup_factors, + ) + + # Case 42: Physics calculation (E = mc²) + run_benchmark_multiply( + "Physics calculation (E = mc²)", + "1.5", # Mass in kg + "8.98755178736817e16", # c² in m²/s² + iterations, + log_file, + speedup_factors, + ) + + # === EDGE CASES AND EXTREME VALUES === + + # Case 43: Multiplication with maximum precision + run_benchmark_multiply( + "Multiplication with maximum precision", + "1." + "1" * 27, # Lots of 1s after decimal + "1." + "9" * 27, # Lots of 9s after decimal + iterations, + log_file, + speedup_factors, + ) + + # Case 44: Multiplication with extreme scale separation + run_benchmark_multiply( + "Multiplication with extreme scale separation", + "1e+20", + "1e-20", + iterations, + log_file, + speedup_factors, + ) + + # Case 45: Square operation + run_benchmark_multiply( + "Square operation", + "123456789.987654321", + "123456789.987654321", + iterations, + log_file, + speedup_factors, + ) + + # Case 46: Multiplication requiring significant rounding + run_benchmark_multiply( + "Multiplication requiring significant rounding", + "0.333333333333333333333333333", + "3.333333333333333333333333333", + iterations, + log_file, + speedup_factors, + ) + + # Case 47: Multiplication of near-zero values + run_benchmark_multiply( + "Multiplication of near-zero values", + "0." + "0" * 25 + "1", + "0." + "0" * 25 + "1", + iterations, + log_file, + speedup_factors, + ) + + # === REAL-WORLD APPLICATION TESTS === + + # Case 48: Currency conversion + run_benchmark_multiply( + "Currency conversion", + "1250.75", # Amount in USD + "0.92", # USD to EUR conversion rate + iterations, + log_file, + speedup_factors, + ) + + # Case 49: Volume calculation (length * width * height) + run_benchmark_multiply( + "Volume calculation (partial - length * width)", + "25.75", # Length + "10.5", # Width + iterations, + log_file, + speedup_factors, + ) + + # Case 50: Scale calculation (multiplying by factor) + run_benchmark_multiply( + "Scale calculation (multiplying by factor)", + "123.456789", # Original value + "1000", # Scale factor + iterations, + log_file, + speedup_factors, + ) + + # Calculate average speedup factor (ignoring any cases that might have failed) + if len(speedup_factors) > 0: + var sum_speedup: Float64 = 0.0 + for i in range(len(speedup_factors)): + sum_speedup += speedup_factors[i] + var average_speedup = sum_speedup / Float64(len(speedup_factors)) + + # Display summary + log_print( + "\n=== BigDecimal Multiplication Benchmark Summary ===", log_file + ) + log_print( + "Benchmarked: " + + String(len(speedup_factors)) + + " different multiplication cases", + log_file, + ) + log_print( + "Each case ran: " + String(iterations) + " iterations", log_file + ) + log_print( + "Average speedup: " + String(average_speedup) + "×", log_file + ) + + # List all speedup factors + log_print("\nIndividual speedup factors:", log_file) + for i in range(len(speedup_factors)): + log_print( + String("Case {}: {}×").format( + i + 1, round(speedup_factors[i], 2) + ), + log_file, + ) + else: + log_print("\nNo valid benchmark cases were completed", log_file) + + # Close the log file + log_file.close() + print("Benchmark completed. Log file closed.") diff --git a/src/decimojo/bigdecimal/arithmetics.mojo b/src/decimojo/bigdecimal/arithmetics.mojo index fa39d98..6b601f5 100644 --- a/src/decimojo/bigdecimal/arithmetics.mojo +++ b/src/decimojo/bigdecimal/arithmetics.mojo @@ -38,23 +38,28 @@ fn add(x1: BigDecimal, x2: BigDecimal) raises -> BigDecimal: Notes: - 1. This function always return the exact result of the addition. - 2. The result's scale is the maximum of the two operands' scales. - 3. The result's sign is determined by the signs of the operands. + Rules for addition: + - This function always return the exact result of the addition. + - The result's scale is the maximum of the two operands' scales. + - The result's sign is determined by the signs of the operands. """ - # Handle zero operands as special cases for efficiency - if x1.coefficient.is_zero(): - return x2 - if x2.coefficient.is_zero(): - return x1 - - # Ensure operands have the same scale (needed to align decimal points) var max_scale = max(x1.scale, x2.scale) - - # Scale adjustment factors var scale_factor1 = (max_scale - x1.scale) if x1.scale < max_scale else 0 var scale_factor2 = (max_scale - x2.scale) if x2.scale < max_scale else 0 + # Handle zero operands as special cases for efficiency + if x1.coefficient.is_zero(): + if x2.coefficient.is_zero(): + return BigDecimal( + coefficient=BigUInt.ZERO, + scale=max_scale, + sign=False, + ) + else: + return x2.extend_precision(scale_factor2) + if x2.coefficient.is_zero(): + return x1.extend_precision(scale_factor1) + # Scale coefficients to match var coef1 = x1.coefficient.multiply_by_power_of_10(scale_factor1) var coef2 = x2.coefficient.multiply_by_power_of_10(scale_factor2) @@ -64,20 +69,20 @@ fn add(x1: BigDecimal, x2: BigDecimal) raises -> BigDecimal: # Same sign: Add coefficients, keep sign var result_coef = coef1 + coef2 return BigDecimal( - coefficient=result_coef, scale=max_scale, sign=x1.sign + coefficient=result_coef^, scale=max_scale, sign=x1.sign ) # Different signs: Subtract smaller coefficient from larger if coef1 > coef2: # |x1| > |x2|, result sign is x1's sign var result_coef = coef1 - coef2 return BigDecimal( - coefficient=result_coef, scale=max_scale, sign=x1.sign + coefficient=result_coef^, scale=max_scale, sign=x1.sign ) elif coef2 > coef1: # |x2| > |x1|, result sign is x2's sign var result_coef = coef2 - coef1 return BigDecimal( - coefficient=result_coef, scale=max_scale, sign=x2.sign + coefficient=result_coef^, scale=max_scale, sign=x2.sign ) else: # |x1| == |x2|, signs differ, result is 0 @@ -98,26 +103,30 @@ fn subtract(x1: BigDecimal, x2: BigDecimal) raises -> BigDecimal: Notes: - 1. This function always return the exact result of the subtraction. - 2. The result's scale is the maximum of the two operands' scales. - 3. The result's sign is determined by the signs of the operands. + - This function always return the exact result of the subtraction. + - The result's scale is the maximum of the two operands' scales. + - The result's sign is determined by the signs of the operands. """ - # Handle zero operands as special cases for efficiency - if x2.coefficient.is_zero(): - return x1 - if x1.coefficient.is_zero(): - # Subtraction from zero negates the sign - return BigDecimal( - coefficient=x2.coefficient, scale=x2.scale, sign=not x2.sign - ) - # Ensure operands have the same scale (needed to align decimal points) var max_scale = max(x1.scale, x2.scale) - - # Scale adjustment factors var scale_factor1 = (max_scale - x1.scale) if x1.scale < max_scale else 0 var scale_factor2 = (max_scale - x2.scale) if x2.scale < max_scale else 0 + # Handle zero operands as special cases for efficiency + if x2.coefficient.is_zero(): + if x1.coefficient.is_zero(): + return BigDecimal( + coefficient=BigUInt.ZERO, + scale=max_scale, + sign=False, + ) + else: + return x1.extend_precision(scale_factor1) + if x1.coefficient.is_zero(): + var result = x2.extend_precision(scale_factor2) + result.sign = not result.sign + return result^ + # Scale coefficients to match var coef1 = x1.coefficient.multiply_by_power_of_10(scale_factor1) var coef2 = x2.coefficient.multiply_by_power_of_10(scale_factor2) @@ -127,7 +136,7 @@ fn subtract(x1: BigDecimal, x2: BigDecimal) raises -> BigDecimal: # Different signs: x1 - (-x2) = x1 + x2, or (-x1) - x2 = -(x1 + x2) var result_coef = coef1 + coef2 return BigDecimal( - coefficient=result_coef, scale=max_scale, sign=x1.sign + coefficient=result_coef^, scale=max_scale, sign=x1.sign ) # Same signs: Must perform actual subtraction @@ -135,16 +144,45 @@ fn subtract(x1: BigDecimal, x2: BigDecimal) raises -> BigDecimal: # |x1| > |x2|, result sign is x1's sign var result_coef = coef1 - coef2 return BigDecimal( - coefficient=result_coef, scale=max_scale, sign=x1.sign + coefficient=result_coef^, scale=max_scale, sign=x1.sign ) elif coef2 > coef1: # |x1| < |x2|, result sign is opposite of x1's sign var result_coef = coef2 - coef1 return BigDecimal( - coefficient=result_coef, scale=max_scale, sign=not x1.sign + coefficient=result_coef^, scale=max_scale, sign=not x1.sign ) else: # |x1| == |x2|, result is 0 + return BigDecimal(coefficient=BigUInt.ZERO, scale=max_scale, sign=False) + + +fn multiply(x1: BigDecimal, x2: BigDecimal) raises -> BigDecimal: + """Returns the product of two numbers. + + Args: + x1: The first operand (multiplicand). + x2: The second operand (multiplier). + + Returns: + The product of x1 and x2. + + Notes: + + - This function always returns the exact result of the multiplication. + - The result's scale is the sum of the two operands' scales (except for zero). + - The result's sign follows the standard sign rules for multiplication. + """ + # Handle zero operands as special cases for efficiency + if x1.coefficient.is_zero() or x2.coefficient.is_zero(): return BigDecimal( - coefficient=BigUInt(UInt32(0)), scale=max_scale, sign=False + coefficient=BigUInt.ZERO, + scale=x1.scale + x2.scale, + sign=x1.sign != x2.sign, ) + + return BigDecimal( + coefficient=x1.coefficient * x2.coefficient, + scale=x1.scale + x2.scale, + sign=x1.sign != x2.sign, + ) diff --git a/src/decimojo/bigdecimal/bigdecimal.mojo b/src/decimojo/bigdecimal/bigdecimal.mojo index b9139a0..12609af 100644 --- a/src/decimojo/bigdecimal/bigdecimal.mojo +++ b/src/decimojo/bigdecimal/bigdecimal.mojo @@ -298,8 +298,17 @@ struct BigDecimal: # Type-transfer or output methods that are not dunders # ===------------------------------------------------------------------=== # - fn to_string(self) -> String: - """Returns string representation of the number.""" + fn to_string(self, line_width: Int = 0) -> String: + """Returns string representation of the number. + + Args: + line_width: The maximum line width for the string representation. + If 0, the string is returned as a single line. + If greater than 0, the string is split into multiple lines. + + Returns: + A string representation of the number. + """ if self.coefficient.is_unitialized(): return String("Unitilialized maginitude of BigDecimal") @@ -334,6 +343,17 @@ struct BigDecimal: result += coefficient_string result += "0" * (-self.scale) + if line_width > 0: + var start = 0 + var end = line_width + var lines = List[String](capacity=len(result) // line_width + 1) + while end < len(result): + lines.append(result[start:end]) + start = end + end += line_width + lines.append(result[start:]) + result = String("\n").join(lines^) + return result^ # ===------------------------------------------------------------------=== # @@ -387,33 +407,118 @@ struct BigDecimal: fn __sub__(self, other: Self) raises -> Self: return decimojo.bigdecimal.arithmetics.subtract(self, other) + @always_inline + fn __mul__(self, other: Self) raises -> Self: + return decimojo.bigdecimal.arithmetics.multiply(self, other) + # ===------------------------------------------------------------------=== # # Other methods # ===------------------------------------------------------------------=== # - @always_inline - fn is_zero(self) -> Bool: - """Returns True if this number represents zero.""" - return self.coefficient.is_zero() + fn extend_precision(self, precision_diff: Int) raises -> BigDecimal: + """Returns a number with additional decimal places (trailing zeros). + This multiplies the coefficient by 10^precision_diff and increases + the scale accordingly, preserving the numeric value. - fn number_of_trailing_zeros(self) -> Int: - """Returns the number of trailing zeros in the coefficient.""" - if self.coefficient.is_zero(): - return 0 + Args: + precision_diff: The number of decimal places to add. - # Count trailing zero words - var number_of_zero_words = 0 - while self.coefficient.words[number_of_zero_words] == UInt32(0): - number_of_zero_words += 1 + Returns: + A new BigDecimal with increased precision. + + Examples: + ``` + print(BigDecimal("123.456).scale_up(5)) # Output: 123.45600000 + print(BigDecimal("123456").scale_up(3)) # Output: 123456.000 + print(BigDecimal("123456").scale_up(-1)) # Error! + ``` + End of examples. + """ + if precision_diff < 0: + raise Error( + "Error in `extend_precision()`: " + "Cannot extend precision with negative value" + ) - # Count trailing zeros in the last non-zero word - var number_of_trailing_zeros = 0 - var last_non_zero_word = self.coefficient.words[number_of_zero_words] - while (last_non_zero_word % UInt32(10)) == 0: - last_non_zero_word = last_non_zero_word // UInt32(10) - number_of_trailing_zeros += 1 + if precision_diff == 0: + return self - return number_of_zero_words * 9 + number_of_trailing_zeros + var number_of_words_to_add = precision_diff // 9 + var number_of_remaining_digits_to_add = precision_diff % 9 + + var coefficient = self.coefficient + + if number_of_remaining_digits_to_add == 0: + pass + elif number_of_remaining_digits_to_add == 1: + coefficient = coefficient * BigUInt(UInt32(10)) + elif number_of_remaining_digits_to_add == 2: + coefficient = coefficient * BigUInt(UInt32(100)) + elif number_of_remaining_digits_to_add == 3: + coefficient = coefficient * BigUInt(UInt32(1_000)) + elif number_of_remaining_digits_to_add == 4: + coefficient = coefficient * BigUInt(UInt32(10_000)) + elif number_of_remaining_digits_to_add == 5: + coefficient = coefficient * BigUInt(UInt32(100_000)) + elif number_of_remaining_digits_to_add == 6: + coefficient = coefficient * BigUInt(UInt32(1_000_000)) + elif number_of_remaining_digits_to_add == 7: + coefficient = coefficient * BigUInt(UInt32(10_000_000)) + else: # number_of_remaining_digits_to_add == 8 + coefficient = coefficient * BigUInt(UInt32(100_000_000)) + + var words: List[UInt32] = List[UInt32]() + for _ in range(number_of_words_to_add): + words.append(UInt32(0)) + words.extend(coefficient.words) + + return BigDecimal( + BigUInt(words^), + self.scale + precision_diff, + self.sign, + ) + + @always_inline + fn internal_representation(self) raises: + """Prints the internal representation of the BigDecimal.""" + var line_width = 30 + var string_of_number = self.to_string(line_width=line_width).split("\n") + var string_of_coefficient = self.coefficient.to_string( + line_width=line_width + ).split("\n") + print("\nInternal Representation Details of BigDecimal") + print("----------------------------------------------") + print("number: ", end="") + for i in range(0, len(string_of_number)): + if i > 0: + print(" " * 16, end="") + print(string_of_number[i]) + print("coefficient: ", end="") + for i in range(0, len(string_of_coefficient)): + if i > 0: + print(" " * 16, end="") + print(String(string_of_coefficient[i])) + print("negative: ", self.sign) + print("scale: ", self.scale) + for i in range(len(self.coefficient.words)): + var ndigits = 1 + if i < 10: + pass + elif i < 100: + ndigits = 2 + else: + ndigits = 3 + print( + "word {}:{}{}".format( + i, " " * (10 - ndigits), String(self.coefficient.words[i]) + ).rjust(9, fillchar="0") + ) + print("----------------------------------------------") + + @always_inline + fn is_zero(self) -> Bool: + """Returns True if this number represents zero.""" + return self.coefficient.is_zero() fn normalize(self) raises -> BigDecimal: """Removes trailing zeros from coefficient while adjusting scale. @@ -425,9 +530,7 @@ struct BigDecimal: if self.coefficient.is_zero(): return BigDecimal(BigUInt(UInt32(0)), 0, False) - var number_of_digits_to_remove = min( - self.number_of_trailing_zeros(), self.scale - ) + var number_of_digits_to_remove = self.number_of_trailing_zeros() var number_of_words_to_remove = number_of_digits_to_remove // 9 var number_of_remaining_digits_to_remove = number_of_digits_to_remove % 9 @@ -460,3 +563,22 @@ struct BigDecimal: self.scale - number_of_digits_to_remove, self.sign, ) + + fn number_of_trailing_zeros(self) -> Int: + """Returns the number of trailing zeros in the coefficient.""" + if self.coefficient.is_zero(): + return 0 + + # Count trailing zero words + var number_of_zero_words = 0 + while self.coefficient.words[number_of_zero_words] == UInt32(0): + number_of_zero_words += 1 + + # Count trailing zeros in the last non-zero word + var number_of_trailing_zeros = 0 + var last_non_zero_word = self.coefficient.words[number_of_zero_words] + while (last_non_zero_word % UInt32(10)) == 0: + last_non_zero_word = last_non_zero_word // UInt32(10) + number_of_trailing_zeros += 1 + + return number_of_zero_words * 9 + number_of_trailing_zeros diff --git a/src/decimojo/bigint/bigint.mojo b/src/decimojo/bigint/bigint.mojo index 7e895c0..69df52e 100644 --- a/src/decimojo/bigint/bigint.mojo +++ b/src/decimojo/bigint/bigint.mojo @@ -53,13 +53,6 @@ struct BigInt(Absable, IntableRaising, Writable): var sign: Bool """Sign information.""" - # ===------------------------------------------------------------------=== # - # Constants - # ===------------------------------------------------------------------=== # - - alias MAX_OF_WORD = UInt32(999_999_999) - alias BASE_OF_WORD = UInt32(1_000_000_000) - # ===------------------------------------------------------------------=== # # Constructors and life time dunder methods # @@ -195,7 +188,7 @@ struct BigInt(Absable, IntableRaising, Writable): # Check if the words are valid for word in words: - if word > Self.MAX_OF_WORD: + if word > UInt32(999_999_999): raise Error( "Error in `BigInt.__init__()`: Word value exceeds maximum" " value of 999_999_999" @@ -343,8 +336,16 @@ struct BigInt(Absable, IntableRaising, Writable): return Int(value) - fn to_string(self) -> String: - """Returns string representation of the BigInt.""" + fn to_string(self, line_width: Int = 0) -> String: + """Returns string representation of the BigInt. + + Args: + line_width: The maximum line width for the string representation. + Default is 0, which means no line width limit. + + Returns: + The string representation of the BigInt. + """ if self.magnitude.is_unitialized(): return String("Unitilialized BigInt") @@ -355,6 +356,17 @@ struct BigInt(Absable, IntableRaising, Writable): var result = String("-") if self.sign else String("") result += self.magnitude.to_string() + if line_width > 0: + var start = 0 + var end = line_width + var lines = List[String](capacity=len(result) // line_width + 1) + while end < len(result): + lines.append(result[start:end]) + start = end + end += line_width + lines.append(result[start:]) + result = String("\n").join(lines^) + return result^ fn to_string_with_separators(self, separator: String = "_") -> String: @@ -370,10 +382,14 @@ struct BigInt(Absable, IntableRaising, Writable): var result = self.to_string() var end = len(result) var start = end - 3 + var blocks = List[String](capacity=len(result) // 3 + 1) while start > 0: - result = result[:start] + separator + result[start:] + blocks.append(result[start:end]) end = start start = end - 3 + blocks.append(result[0:end]) + blocks.reverse() + result = separator.join(blocks) return result^ @@ -585,18 +601,27 @@ struct BigInt(Absable, IntableRaising, Writable): # Internal methods # ===------------------------------------------------------------------=== # - fn internal_representation(self): + fn internal_representation(self) raises: """Prints the internal representation details of a BigInt.""" + var string_of_number = self.to_string(line_width=30).split("\n") print("\nInternal Representation Details of BigInt") - print("-----------------------------------------") - print("number: ", self) - print(" ", self.to_string_with_separators()) - print("negative: ", self.sign) + print("----------------------------------------------") + print("number: ", end="") + for i in range(0, len(string_of_number)): + if i > 0: + print(" " * 16, end="") + print(string_of_number[i]) for i in range(len(self.magnitude.words)): + var ndigits = 1 + if i < 10: + pass + elif i < 100: + ndigits = 2 + else: + ndigits = 3 print( - "word", - i, - ": ", - String(self.magnitude.words[i]).rjust(width=9, fillchar="0"), + "word {}:{}{}".format( + i, " " * (10 - ndigits), String(self.magnitude.words[i]) + ).rjust(9, fillchar="0") ) - print("--------------------------------") + print("----------------------------------------------") diff --git a/src/decimojo/biguint/arithmetics.mojo b/src/decimojo/biguint/arithmetics.mojo index 492a438..fef07ac 100644 --- a/src/decimojo/biguint/arithmetics.mojo +++ b/src/decimojo/biguint/arithmetics.mojo @@ -841,7 +841,7 @@ fn multiply_toom_cook_3(x1: BigUInt, x2: BigUInt) raises -> BigUInt: fn multiply_by_power_of_10(x: BigUInt, n: Int) raises -> BigUInt: - """Multiplies a BigUInt by 10^n. + """Multiplies a BigUInt by 10^n (n>=0). Args: x: The BigUInt value to multiply. diff --git a/src/decimojo/biguint/biguint.mojo b/src/decimojo/biguint/biguint.mojo index 3212eb3..57c0615 100644 --- a/src/decimojo/biguint/biguint.mojo +++ b/src/decimojo/biguint/biguint.mojo @@ -61,8 +61,20 @@ struct BigUInt(Absable, IntableRaising, Writable): # Constants # ===------------------------------------------------------------------=== # - alias MAX_OF_WORD = UInt32(999_999_999) - alias BASE_OF_WORD = UInt32(1_000_000_000) + alias ZERO = Self.zero() + alias ONE = Self.one() + + @always_inline + @staticmethod + fn zero() -> Self: + """Returns a BigUInt with value 0.""" + return Self(words=List[UInt32](UInt32(0))) + + @always_inline + @staticmethod + fn one() -> Self: + """Returns a BigUInt with value 1.""" + return Self(words=List[UInt32](UInt32(1))) # ===------------------------------------------------------------------=== # # Constructors and life time dunder methods @@ -166,7 +178,7 @@ struct BigUInt(Absable, IntableRaising, Writable): # Check if the words are valid for word in words: - if word[] > Self.MAX_OF_WORD: + if word[] > UInt32(999_999_999): raise Error( "Error in `BigUInt.from_list()`: Word value exceeds maximum" " value of 999_999_999" @@ -198,7 +210,7 @@ struct BigUInt(Absable, IntableRaising, Writable): # Check if the words are valid for word in words: - if word > Self.MAX_OF_WORD: + if word > UInt32(999_999_999): raise Error( "Error in `BigUInt.__init__()`: Word value exceeds maximum" " value of 999_999_999" @@ -422,7 +434,7 @@ struct BigUInt(Absable, IntableRaising, Writable): var value: Int128 = 0 for i in range(len(self.words)): - value += Int128(self.words[i]) * Int128(Self.BASE_OF_WORD) ** i + value += Int128(self.words[i]) * Int128(1_000_000_000) ** i if value > Int128(Int.MAX): raise Error( @@ -450,7 +462,7 @@ struct BigUInt(Absable, IntableRaising, Writable): var value: UInt128 = 0 for i in range(len(self.words)): - value += UInt128(self.words[i]) * UInt128(Self.BASE_OF_WORD) ** i + value += UInt128(self.words[i]) * UInt128(1_000_000_000) ** i if value > UInt128(UInt64.MAX): raise Error( @@ -460,8 +472,16 @@ struct BigUInt(Absable, IntableRaising, Writable): return UInt64(value) - fn to_string(self) -> String: - """Returns string representation of the BigUInt.""" + fn to_string(self, line_width: Int = 0) -> String: + """Returns string representation of the BigUInt. + + Args: + line_width: The width of each line. Default is 0, which means no + line width. + + Returns: + The string representation of the BigUInt. + """ if len(self.words) == 0: return String("Unitilialized BigUInt") @@ -477,6 +497,17 @@ struct BigUInt(Absable, IntableRaising, Writable): else: result += String(self.words[i]).rjust(width=9, fillchar="0") + if line_width > 0: + var start = 0 + var end = line_width + var lines = List[String](capacity=len(result) // line_width + 1) + while end < len(result): + lines.append(result[start:end]) + start = end + end += line_width + lines.append(result[start:]) + result = String("\n").join(lines^) + return result^ fn to_string_with_separators(self, separator: String = "_") -> String: @@ -492,10 +523,14 @@ struct BigUInt(Absable, IntableRaising, Writable): var result = self.to_string() var end = len(result) var start = end - 3 + var blocks = List[String](capacity=len(result) // 3 + 1) while start > 0: - result = result[:start] + separator + result[start:] + blocks.append(result[start:end]) end = start start = end - 3 + blocks.append(result[0:end]) + blocks.reverse() + result = separator.join(blocks) return result^ @@ -771,20 +806,30 @@ struct BigUInt(Absable, IntableRaising, Writable): """Returns the number of words in the BigInt.""" return len(self.words) - fn internal_representation(self): + fn internal_representation(self) raises: """Prints the internal representation details of a BigUInt.""" + var string_of_number = self.to_string(line_width=30).split("\n") print("\nInternal Representation Details of BigUInt") - print("-----------------------------------------") - print("number: ", self) - print(" ", self.to_string_with_separators()) + print("----------------------------------------------") + print("number: ", end="") + for i in range(0, len(string_of_number)): + if i > 0: + print(" " * 16, end="") + print(string_of_number[i]) for i in range(len(self.words)): + var ndigits = 1 + if i < 10: + pass + elif i < 100: + ndigits = 2 + else: + ndigits = 3 print( - "word", - i, - ": ", - String(self.words[i]).rjust(width=9, fillchar="0"), + "word {}:{}{}".format( + i, " " * (10 - ndigits), String(self.words[i]) + ).rjust(9, fillchar="0") ) - print("--------------------------------") + print("----------------------------------------------") fn ith_digit(self, i: Int) raises -> UInt8: """Returns the ith digit of the BigUInt.""" diff --git a/src/decimojo/decimal/decimal.mojo b/src/decimojo/decimal/decimal.mojo index 549f869..7340289 100644 --- a/src/decimojo/decimal/decimal.mojo +++ b/src/decimojo/decimal/decimal.mojo @@ -1499,6 +1499,102 @@ struct Decimal( # | UInt128(self.low) # ) + fn extend_precision(self, owned precision_diff: Int) raises -> Decimal: + """Returns a number with additional decimal places (trailing zeros). + This multiplies the coefficient by 10^precision_diff and increases + the scale accordingly, preserving the numeric value. + + Args: + precision_diff: The number of decimal places to add. + + Returns: + A new Decimal with increased precision. + + Raises: + Error: If the level is less than 0. + + Examples: + ```mojo + from decimojo import Decimal + var d1 = Decimal("5") # 5 + var d2 = d1.extend_precision(2) # Result: 5.00 (same value, different representation) + print(d1) # 5 + print(d2) # 5.00 + print(d2.scale()) # 2 + + var d3 = Decimal("123.456") # 123.456 + var d4 = d3.extend_precision(3) # Result: 123.456000 + print(d3) # 123.456 + print(d4) # 123.456000 + print(d4.scale()) # 6 + ``` + End of examples. + """ + if precision_diff < 0: + raise Error( + "Error in `scale_up()`: precision_diff must be greater than 0" + ) + + if precision_diff == 0: + return self + + var result = self + + # Update the scale in the flags + var new_scale = self.scale() + precision_diff + + # TODO: Check if multiplication by 10^level would cause overflow + # If yes, then raise an error + if new_scale > Decimal.MAX_SCALE + 1: + # Cannot scale beyond max precision, limit the scaling + precision_diff = Decimal.MAX_SCALE + 1 - self.scale() + new_scale = Decimal.MAX_SCALE + 1 + + # With UInt128, we can represent the coefficient as a single value + var coefficient = UInt128(self.high) << 64 | UInt128( + self.mid + ) << 32 | UInt128(self.low) + + # TODO: Check if multiplication by 10^level would cause overflow + # If yes, then raise an error + var max_coefficient = ~UInt128(0) / UInt128(10) ** precision_diff + if coefficient > max_coefficient: + # Handle overflow case - limit to maximum value or raise error + coefficient = ~UInt128(0) + else: + # No overflow - safe to multiply + coefficient *= UInt128(10**precision_diff) + + # Extract the 32-bit components from the UInt128 + result.low = UInt32(coefficient & 0xFFFFFFFF) + result.mid = UInt32((coefficient >> 32) & 0xFFFFFFFF) + result.high = UInt32((coefficient >> 64) & 0xFFFFFFFF) + + # Set the new scale + result.flags = (self.flags & ~Decimal.SCALE_MASK) | ( + UInt32(new_scale << Decimal.SCALE_SHIFT) & Decimal.SCALE_MASK + ) + + return result + + fn internal_representation(self): + """Prints the internal representation details of a Decimal.""" + print("\nInternal Representation Details:") + print("--------------------------------") + print("Decimal: ", self) + print("coefficient: ", self.coefficient()) + print("scale: ", self.scale()) + print("is negative: ", self.is_negative()) + print("is zero: ", self.is_zero()) + print("low: ", self.low) + print("mid: ", self.mid) + print("high: ", self.high) + print("low byte: ", hex(self.low)) + print("mid byte: ", hex(self.mid)) + print("high byte: ", hex(self.high)) + print("flags byte: ", hex(self.flags)) + print("--------------------------------") + @always_inline fn is_integer(self) -> Bool: """Determines whether this Decimal value represents an integer. @@ -1603,25 +1699,3 @@ struct Decimal( return 0 # Zero has zero significant digit else: return decimojo.utility.number_of_digits(coef) - - # ===------------------------------------------------------------------=== # - # Internal methods - # ===------------------------------------------------------------------=== # - - fn internal_representation(value: Decimal): - """Prints the internal representation details of a Decimal.""" - print("\nInternal Representation Details:") - print("--------------------------------") - print("Decimal: ", value) - print("coefficient: ", value.coefficient()) - print("scale: ", value.scale()) - print("is negative: ", value.is_negative()) - print("is zero: ", value.is_zero()) - print("low: ", value.low) - print("mid: ", value.mid) - print("high: ", value.high) - print("low byte: ", hex(value.low)) - print("mid byte: ", hex(value.mid)) - print("high byte: ", hex(value.high)) - print("flags byte: ", hex(value.flags)) - print("--------------------------------") diff --git a/src/decimojo/utility.mojo b/src/decimojo/utility.mojo index 69d4462..8b5c7f8 100644 --- a/src/decimojo/utility.mojo +++ b/src/decimojo/utility.mojo @@ -61,90 +61,6 @@ fn bitcast[dtype: DType](dec: Decimal) -> Scalar[dtype]: return result -fn scale_up(value: Decimal, owned level: Int) raises -> Decimal: - """ - Increase the scale of a Decimal while keeping the value unchanged. - Internally, this means multiplying the coefficient by 10^scale_diff - and increasing the scale by scale_diff simultaneously. - - Args: - value: The Decimal to scale up. - level: Number of decimal places to scale up by. - - Returns: - A new Decimal with the scaled up value. - - Raises: - Error: If the level is less than 0. - - Examples: - - ```mojo - from decimojo import Decimal - from decimojo.utility import scale_up - var d1 = Decimal("5") # 5 - var d2 = scale_up(d1, 2) # Result: 5.00 (same value, different representation) - print(d1) # 5 - print(d2) # 5.00 - print(d2.scale()) # 2 - - var d3 = Decimal("123.456") # 123.456 - var d4 = scale_up(d3, 3) # Result: 123.456000 - print(d3) # 123.456 - print(d4) # 123.456000 - print(d4.scale()) # 6 - ``` - . - """ - - if level < 0: - raise Error("Error in `scale_up()`: Level must be greater than 0") - - # Early return if no scaling needed - if level == 0: - return value - - var result = value - - # Update the scale in the flags - var new_scale = value.scale() + level - - # TODO: Check if multiplication by 10^level would cause overflow - # If yes, then raise an error - if new_scale > Decimal.MAX_SCALE + 1: - # Cannot scale beyond max precision, limit the scaling - level = Decimal.MAX_SCALE + 1 - value.scale() - new_scale = Decimal.MAX_SCALE + 1 - - # With UInt128, we can represent the coefficient as a single value - var coefficient = UInt128(value.high) << 64 | UInt128( - value.mid - ) << 32 | UInt128(value.low) - - # TODO: Check if multiplication by 10^level would cause overflow - # If yes, then raise an error - # - var max_coefficient = ~UInt128(0) / UInt128(10) ** level - if coefficient > max_coefficient: - # Handle overflow case - limit to maximum value or raise error - coefficient = ~UInt128(0) - else: - # No overflow - safe to multiply - coefficient *= UInt128(10**level) - - # Extract the 32-bit components from the UInt128 - result.low = UInt32(coefficient & 0xFFFFFFFF) - result.mid = UInt32((coefficient >> 32) & 0xFFFFFFFF) - result.high = UInt32((coefficient >> 64) & 0xFFFFFFFF) - - # Set the new scale - result.flags = (value.flags & ~Decimal.SCALE_MASK) | ( - UInt32(new_scale << Decimal.SCALE_SHIFT) & Decimal.SCALE_MASK - ) - - return result - - fn truncate_to_max[dtype: DType, //](value: Scalar[dtype]) -> Scalar[dtype]: """ Truncates a UInt256 or UInt128 value to be as closer to the max value of diff --git a/tests/bigdecimal/test_bigdecimal_arithmetics.mojo b/tests/bigdecimal/test_bigdecimal_arithmetics.mojo index ce7bf9e..7768390 100644 --- a/tests/bigdecimal/test_bigdecimal_arithmetics.mojo +++ b/tests/bigdecimal/test_bigdecimal_arithmetics.mojo @@ -2,9 +2,11 @@ Test BigDecimal arithmetic operations including addition, subtraction, multiplication and division. """ +from python import Python +import testing + from decimojo import BigDecimal, RoundingMode from decimojo.tests import TestCase -import testing from tomlmojo import parse_file alias file_path = "tests/bigdecimal/test_data/bigdecimal_arithmetics.toml" @@ -39,6 +41,8 @@ fn test_add() raises: print("------------------------------------------------------") print("Testing BigDecimal addition...") + var pydecimal = Python.import_module("decimal") + # Load test cases from TOML file var test_cases = load_test_cases(file_path, "addition_tests") print("Loaded", len(test_cases), "test cases for addition") @@ -76,8 +80,11 @@ fn test_add() raises: test_case.expected, "\n Got:", String(result), - "\n Error:", - String(e), + "\n Python decimal result (for reference):", + String( + pydecimal.Decimal(test_case.a) + + pydecimal.Decimal(test_case.b) + ), ) failed += 1 @@ -90,6 +97,8 @@ fn test_subtract() raises: print("------------------------------------------------------") print("Testing BigDecimal subtraction...") + var pydecimal = Python.import_module("decimal") + # Debug TOML parsing var toml = parse_file(file_path) print("TOML file loaded successfully") @@ -143,8 +152,11 @@ fn test_subtract() raises: test_case.expected, "\n Got:", String(result), - "\n Error:", - String(e), + "\n Python decimal result (for reference):", + String( + pydecimal.Decimal(test_case.a) + - pydecimal.Decimal(test_case.b) + ), ) failed += 1 @@ -152,6 +164,63 @@ fn test_subtract() raises: testing.assert_equal(failed, 0, "All subtraction tests should pass") +fn test_multiply() raises: + """Test BigDecimal multiplication with various test cases.""" + print("------------------------------------------------------") + print("Testing BigDecimal multiplication...") + + var pydecimal = Python.import_module("decimal") + + # Load test cases from TOML file + var test_cases = load_test_cases(file_path, "multiplication_tests") + print("Loaded", len(test_cases), "test cases for multiplication") + + # Track test results + var passed = 0 + var failed = 0 + + # Run all test cases in a loop + for i in range(len(test_cases)): + var test_case = test_cases[i] + var a = BigDecimal(test_case.a) + var b = BigDecimal(test_case.b) + var expected = BigDecimal(test_case.expected) + var result = a * b + + try: + # Using String comparison for easier debugging + testing.assert_equal( + String(result), String(expected), test_case.description + ) + passed += 1 + except e: + print( + "✗ Case", + i + 1, + "failed:", + test_case.description, + "\n Input:", + test_case.a, + "*", + test_case.b, + "\n Expected:", + test_case.expected, + "\n Got:", + String(result), + "\n Python decimal result (for reference):", + String( + pydecimal.Decimal(test_case.a) + * pydecimal.Decimal(test_case.b) + ), + ) + failed += 1 + + print( + "BigDecimal multiplication tests:", passed, "passed,", failed, "failed" + ) + testing.assert_equal(failed, 0, "All multiplication tests should pass") + + fn main() raises: print("Running BigDecimal arithmetic tests") @@ -161,4 +230,7 @@ fn main() raises: # Run subtraction tests test_subtract() + # Run multiplication tests + test_multiply() + print("All BigDecimal arithmetic tests passed!") diff --git a/tests/bigdecimal/test_data/bigdecimal_arithmetics.toml b/tests/bigdecimal/test_data/bigdecimal_arithmetics.toml index 219db64..063aed4 100644 --- a/tests/bigdecimal/test_data/bigdecimal_arithmetics.toml +++ b/tests/bigdecimal/test_data/bigdecimal_arithmetics.toml @@ -628,3 +628,293 @@ a = "0.0440" b = "0.0015" expected = "0.0425" description = "Interest rate calculation" + +# ===----------------------------------------------------------------------=== # +# Test cases for BigDecimal multiplication +# ===----------------------------------------------------------------------=== # +# === BASIC MULTIPLICATION TESTS === +[[multiplication_tests]] +a = "2" +b = "3" +expected = "6" +description = "Simple integer multiplication" + +[[multiplication_tests]] +a = "1.5" +b = "2" +expected = "3.0" +description = "Simple decimal by integer" + +[[multiplication_tests]] +a = "0.5" +b = "0.5" +expected = "0.25" +description = "Decimal by decimal multiplication" + +[[multiplication_tests]] +a = "0" +b = "123.456" +expected = "0.000" +description = "Multiplication by zero" + +[[multiplication_tests]] +a = "1" +b = "123.456" +expected = "123.456" +description = "Multiplication by one" + +[[multiplication_tests]] +a = "-1" +b = "123.456" +expected = "-123.456" +description = "Multiplication by negative one" + +# === DIFFERENT SCALE TESTS === +[[multiplication_tests]] +a = "1.23" +b = "4.56" +expected = "5.6088" +description = "Multiplication with scales addition" + +[[multiplication_tests]] +a = "1.2345" +b = "6.789" +expected = "8.3810205" +description = "Multiplication with different scales" + +[[multiplication_tests]] +a = "1.23456789" +b = "9.87654321" +expected = "12.1932631112635269" +description = "Multiplication with high precision" + +[[multiplication_tests]] +a = "9.999" +b = "9.999" +expected = "99.980001" +description = "Multiplication near power of 10" + +[[multiplication_tests]] +a = "0.1" +b = "0.1" +expected = "0.01" +description = "Multiplication of tenths" + +# === SIGN COMBINATION TESTS === +[[multiplication_tests]] +a = "-2" +b = "-3" +expected = "6" +description = "Negative times negative" + +[[multiplication_tests]] +a = "-3.14" +b = "2" +expected = "-6.28" +description = "Negative times positive" + +[[multiplication_tests]] +a = "10" +b = "-0.5" +expected = "-5.0" +description = "Positive times negative" + +[[multiplication_tests]] +a = "-5.75" +b = "-10.25" +expected = "58.9375" +description = "Negative times negative decimal" + +[[multiplication_tests]] +a = "0" +b = "-123.456" +expected = "-0.000" +description = "Zero times negative" + +# === LARGE NUMBER TESTS === +[[multiplication_tests]] +a = "9999999999" +b = "9999999999" +expected = "99999999980000000001" +description = "Large integer multiplication" + +[[multiplication_tests]] +a = "-9999999999" +b = "9999999999" +expected = "-99999999980000000001" +description = "Large integer with negative" + +[[multiplication_tests]] +a = "12345678901234567890" +b = "0.00000000001" +expected = "123456789.01234567890" +description = "Large integer times small decimal" + +[[multiplication_tests]] +a = "0.0000000001" +b = "0.0000000001" +expected = "0.00000000000000000001" +description = "Small decimal multiplication" + +# === SCIENTIFIC NOTATION TESTS === +[[multiplication_tests]] +a = "1.23e5" +b = "4.56e2" +expected = "56088000" +description = "Multiplication with scientific notation" + +[[multiplication_tests]] +a = "1.23e-5" +b = "4.56e-2" +expected = "0.00000056088" +description = "Multiplication with negative exponents" + +[[multiplication_tests]] +a = "1.23e5" +b = "4.56e-2" +expected = "5608.8" +description = "Multiplication with mixed exponents" + +# === SPECIAL CASES === +[[multiplication_tests]] +a = "3.14159265358979323846" +b = "2.71828182845904523536" +expected = "8.5397342226735670654554622909226073039456" +description = "Multiplication of mathematical constants (PI * E)" + +[[multiplication_tests]] +a = "0.33333333333333333333333333" +b = "3" +expected = "0.99999999999999999999999999" +description = "Multiplication resulting in almost one" + +[[multiplication_tests]] +a = "0.5" +b = "2" +expected = "1.0" +description = "Multiplication resulting in exact integer" + +[[multiplication_tests]] +a = "0.1" +b = "10" +expected = "1.0" +description = "Decimal shifted by multiplication" + +# === FINANCIAL SCENARIOS === +[[multiplication_tests]] +a = "10.99" +b = "3" +expected = "32.97" +description = "Financial multiplication (price * quantity)" + +[[multiplication_tests]] +a = "100" +b = "0.075" +expected = "7.500" +description = "Financial multiplication (principal * interest)" + +[[multiplication_tests]] +a = "9.99" +b = "1.08" +expected = "10.7892" +description = "Financial multiplication (price * tax)" + +# === PRECISION BOUNDARY TESTS === +[[multiplication_tests]] +a = "9.9999999999999999999999999" +b = "9.9999999999999999999999999" +expected = "99.99999999999999999999999800000000000000000000000001" +description = "Multiplication at precision boundary" + +[[multiplication_tests]] +a = "1.0000000000000000000000001" +b = "1.0000000000000000000000001" +expected = "1.00000000000000000000000020000000000000000000000001" +description = "Multiplication slightly above one" + +[[multiplication_tests]] +a = "0.125" +b = "8" +expected = "1.000" +description = "Binary-friendly multiplication (1/8 * 8)" + +# === ADDITIONAL EDGE CASES === +[[multiplication_tests]] +a = "0.000000000000000000000000001" +b = "1000000000000000000000000000" +expected = "1.000000000000000000000000000" +description = "Multiplication of very small and very large" + +[[multiplication_tests]] +a = "0.9" +b = "0.9" +expected = "0.81" +description = "Multiplication less than one" + +[[multiplication_tests]] +a = "1.11111111111111111111111111" +b = "9" +expected = "9.99999999999999999999999999" +description = "Multiplication of repeating decimal" + +[[multiplication_tests]] +a = "2.5" +b = "0.4" +expected = "1.00" +description = "Multiplication resulting in exact one" + +[[multiplication_tests]] +a = "0.000000000000000000000000009" +b = "0.000000000000000000000000009" +expected = "0.000000000000000000000000000000000000000000000000000081" +description = "Multiplication of very small numbers" + +# === APPLICATION SCENARIOS === +[[multiplication_tests]] +a = "299792458" +b = "0.000000001" +expected = "0.299792458" +description = "Physical unit conversion (m/s to km/μs)" + +[[multiplication_tests]] +a = "6.022e23" +b = "0.001" +expected = "6.022e20" +description = "Scientific calculation (Avogadro's number * milli-mole)" + +[[multiplication_tests]] +a = "29.92" +b = "33.8639" +expected = "1013.207888" +description = "Weather calculation (inches of mercury to hPa)" + +[[multiplication_tests]] +a = "42.195" +b = "0.621371" +expected = "26.218749345" +description = "Distance conversion (km to miles)" + +[[multiplication_tests]] +a = "180" +b = "0.017453292519943295" +expected = "3.141592653589793100" +description = "Angle conversion (degrees to radians)" + +# === ROUNDING AND PRECISION TESTS === +[[multiplication_tests]] +a = "3.333333333333333333333333333" +b = "3" +expected = "9.999999999999999999999999999" +description = "Multiplication preserving precision" + +[[multiplication_tests]] +a = "1.000000000000000000000000001" +b = "0.999999999999999999999999999" +expected = "0.999999999999999999999999999999999999999999999999999999" +description = "Multiplication with potential precision loss" + +[[multiplication_tests]] +a = "3.1415926535897932384626433832795" +b = "1.0" +expected = "3.14159265358979323846264338327950" +description = "Multiplication preserving significant digits up to precision"