diff --git a/benches/bigdecimal/bench.mojo b/benches/bigdecimal/bench.mojo new file mode 100644 index 0000000..eb3cdcc --- /dev/null +++ b/benches/bigdecimal/bench.mojo @@ -0,0 +1,25 @@ +from bench_bigdecimal_add import main as bench_add + + +fn main() raises: + print( + """ +========================================= +This is the BigInt Benchmarks +========================================= +add: Add +all: Run all benchmarks +q: Exit +========================================= +""" + ) + var command = input("Type name of bench you want to run: ") + if command == "add": + bench_add() + elif command == "all": + bench_add() + elif command == "q": + return + else: + print("Invalid input") + main() diff --git a/benches/bigdecimal/bench_bigdecimal_add.mojo b/benches/bigdecimal/bench_bigdecimal_add.mojo new file mode 100644 index 0000000..6d28aa4 --- /dev/null +++ b/benches/bigdecimal/bench_bigdecimal_add.mojo @@ -0,0 +1,726 @@ +""" +Comprehensive benchmarks for BigDecimal addition. +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_add_" + 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_add( + name: String, + value1: String, + value2: String, + iterations: Int, + log_file: PythonObject, + mut speedup_factors: List[Float64], +) raises: + """ + Run a benchmark comparing Mojo BigDecimal addition with Python Decimal addition. + + 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 addition: " + String(mojo_time) + " ns per iteration", + log_file, + ) + log_print( + "Python addition: " + 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 Addition 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 = 1000 + 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 addition benchmarks with " + + String(iterations) + + " iterations each", + log_file, + ) + + # === BASIC DECIMAL ADDITION TESTS === + + # Case 1: Simple integer addition + run_benchmark_add( + "Simple integer addition", + "42", + "58", + iterations, + log_file, + speedup_factors, + ) + + # Case 2: Simple decimal addition + run_benchmark_add( + "Simple decimal addition", + "3.14", + "2.71", + iterations, + log_file, + speedup_factors, + ) + + # Case 3: Addition with different scales (precision alignment) + run_benchmark_add( + "Addition with different scales", + "1.2345", + "5.67", + iterations, + log_file, + speedup_factors, + ) + + # Case 4: Addition with very different scales + run_benchmark_add( + "Addition with very different scales", + "1.23456789012345678901234567", + "5.6", + iterations, + log_file, + speedup_factors, + ) + + # Case 5: Addition with zero + run_benchmark_add( + "Addition with zero", + "123.456", + "0", + iterations, + log_file, + speedup_factors, + ) + + # === SCALE AND PRECISION TESTS === + + # Case 6: Precision at decimal limit + run_benchmark_add( + "Precision at decimal limit", + "1.2345678901234567890123456789", + "9.8765432109876543210987654321", + iterations, + log_file, + speedup_factors, + ) + + # Case 7: Addition causing scale reduction + run_benchmark_add( + "Addition causing scale reduction", + "999999.999999", + "0.000001", + iterations, + log_file, + speedup_factors, + ) + + # Case 8: Addition with high precision, repeating pattern + run_benchmark_add( + "Addition with high precision, repeating pattern", + "0.33333333333333333333333333", + "0.66666666666666666666666667", + iterations, + log_file, + speedup_factors, + ) + + # Case 9: Addition resulting in exact 1.0 + run_benchmark_add( + "Addition resulting in exact 1.0", + "0.33333333333333333333333333", + "0.66666666666666666666666667", + iterations, + log_file, + speedup_factors, + ) + + # Case 10: Addition with scientific notation + run_benchmark_add( + "Addition with scientific notation", + "1.23e5", + "4.56e4", + iterations, + log_file, + speedup_factors, + ) + + # === SIGN COMBINATION TESTS === + + # Case 11: Negative + Positive (negative smaller) + run_benchmark_add( + "Negative + Positive (negative smaller)", + "-3.14", + "10", + iterations, + log_file, + speedup_factors, + ) + + # Case 12: Negative + Positive (negative larger) + run_benchmark_add( + "Negative + Positive (negative larger)", + "-10", + "3.14", + iterations, + log_file, + speedup_factors, + ) + + # Case 13: Negative + Negative + run_benchmark_add( + "Negative + Negative", + "-5.75", + "-10.25", + iterations, + log_file, + speedup_factors, + ) + + # Case 14: Addition resulting in zero (pos + neg) + run_benchmark_add( + "Addition resulting in zero (pos + neg)", + "123.456", + "-123.456", + iterations, + log_file, + speedup_factors, + ) + + # Case 15: Addition near zero (small difference) + run_benchmark_add( + "Addition near zero (small difference)", + "0.0000001", + "-0.00000005", + iterations, + log_file, + speedup_factors, + ) + + # === LARGE NUMBER TESTS === + + # Case 16: Large integer addition + run_benchmark_add( + "Large integer addition", + "9999999999999999999999999999", + "1", + iterations, + log_file, + speedup_factors, + ) + + # Case 17: Large negative + positive + run_benchmark_add( + "Large negative + positive", + "-9999999999999999999999999999", + "9999999999999999999999999998", + iterations, + log_file, + speedup_factors, + ) + + # Case 18: Very large decimal addition + run_benchmark_add( + "Very large decimal addition", + "9" * 20 + "." + "9" * 8, + "1" + "0" * 19 + "." + "0" * 7 + "1", + iterations, + log_file, + speedup_factors, + ) + + # Case 19: Very large + very small + run_benchmark_add( + "Very large + very small", + "1" + "0" * 25, + "0." + "0" * 25 + "1", + iterations, + log_file, + speedup_factors, + ) + + # Case 20: Extreme scales (large positive exponent) + run_benchmark_add( + "Extreme scales (large positive exponent)", + "1.23e20", + "4.56e19", + iterations, + log_file, + speedup_factors, + ) + + # === SMALL NUMBER TESTS === + + # Case 21: Very small positive values + run_benchmark_add( + "Very small positive values", + "0." + "0" * 25 + "1", + "0." + "0" * 25 + "2", + iterations, + log_file, + speedup_factors, + ) + + # Case 22: Very small negative values + run_benchmark_add( + "Very small negative values", + "-0." + "0" * 25 + "1", + "-0." + "0" * 25 + "2", + iterations, + log_file, + speedup_factors, + ) + + # Case 23: Small values with different scales + run_benchmark_add( + "Small values with different scales", + "0." + "0" * 10 + "1", + "0." + "0" * 20 + "1", + iterations, + log_file, + speedup_factors, + ) + + # Case 24: Extreme scales (large negative exponent) + run_benchmark_add( + "Extreme scales (large negative exponent)", + "1.23e-15", + "4.56e-20", + iterations, + log_file, + speedup_factors, + ) + + # Case 25: Addition that requires significant rescaling + run_benchmark_add( + "Addition that requires significant rescaling", + "1.23e-10", + "4.56e10", + iterations, + log_file, + speedup_factors, + ) + + # === SPECIAL VALUE TESTS === + + # Case 26: Addition of exact mathematical constants + run_benchmark_add( + "Addition of exact mathematical constants (PI + E)", + "3.14159265358979323846264338328", + "2.71828182845904523536028747135", + iterations, + log_file, + speedup_factors, + ) + + # Case 27: Addition of famous constants (Phi + sqrt(2)) + run_benchmark_add( + "Addition of famous constants (Phi + sqrt(2))", + "1.61803398874989484820458683437", + "1.41421356237309504880168872421", + iterations, + log_file, + speedup_factors, + ) + + # Case 28: Addition with repeating patterns + run_benchmark_add( + "Addition with repeating patterns", + "1.23451234512345123451234512345", + "5.67896789567895678956789567896", + iterations, + log_file, + speedup_factors, + ) + + # Case 29: Financial numbers (dollars and cents) + run_benchmark_add( + "Financial numbers (dollars and cents)", + "10542.75", + "3621.50", + iterations, + log_file, + speedup_factors, + ) + + # Case 30: Statistical data (means) + run_benchmark_add( + "Statistical data (means)", + "98.76543", + "87.65432", + iterations, + log_file, + speedup_factors, + ) + + # === PRECISION TESTS === + + # Case 31: Binary-friendly decimals + run_benchmark_add( + "Binary-friendly decimals", + "0.5", + "0.25", + iterations, + log_file, + speedup_factors, + ) + + # Case 32: Decimal-unfriendly fractions + run_benchmark_add( + "Decimal-unfriendly fractions", + "0.33333333333333333333333333", + "0.33333333333333333333333333", + iterations, + log_file, + speedup_factors, + ) + + # Case 33: Addition with many carries + run_benchmark_add( + "Addition with many carries", + "9.99999999999999999999999999", + "0.00000000000000000000000001", + iterations, + log_file, + speedup_factors, + ) + + # Case 34: Addition with trailing zeros + run_benchmark_add( + "Addition with trailing zeros", + "1.1000000000000000000000000", + "2.2000000000000000000000000", + iterations, + log_file, + speedup_factors, + ) + + # Case 35: Addition requiring precision increase + run_benchmark_add( + "Addition requiring precision increase", + "999999999999999999.9999999", + "0.0000001", + iterations, + log_file, + speedup_factors, + ) + + # === APPLICATION-SPECIFIC TESTS === + + # Case 36: Scientific measurement (physics) + run_benchmark_add( + "Scientific measurement (physics)", + "299792458.0", # Speed of light in m/s + "0.000000000000000000160217663", # Planck constant in Js + iterations, + log_file, + speedup_factors, + ) + + # Case 37: Astronomical distances + run_benchmark_add( + "Astronomical distances", + "1.496e11", # Earth-Sun distance in meters + "3.844e8", # Earth-Moon distance in meters + iterations, + log_file, + speedup_factors, + ) + + # Case 38: Chemical concentrations + run_benchmark_add( + "Chemical concentrations", + "0.00001234", # mol/L + "0.00005678", # mol/L + iterations, + log_file, + speedup_factors, + ) + + # Case 39: Financial market prices + run_benchmark_add( + "Financial market prices", + "3914.75", # Stock price + "0.05", # Price change + iterations, + log_file, + speedup_factors, + ) + + # Case 40: Interest rate calculations + run_benchmark_add( + "Interest rate calculations", + "0.0425", # 4.25% interest rate + "0.0015", # 0.15% increase + iterations, + log_file, + speedup_factors, + ) + + # === EDGE CASES AND EXTREME VALUES === + + # Case 41: Addition with maximum precision + run_benchmark_add( + "Addition with maximum precision", + "0." + "1" * 28, + "0." + "9" * 28, + iterations, + log_file, + speedup_factors, + ) + + # Case 42: Addition with extreme exponents difference + run_benchmark_add( + "Addition with extreme exponents difference", + "1e20", + "1e-20", + iterations, + log_file, + speedup_factors, + ) + + # Case 43: Addition at precision boundary + run_benchmark_add( + "Addition at precision boundary", + "9" * 28 + "." + "9" * 28, + "0." + "0" * 27 + "1", + iterations, + log_file, + speedup_factors, + ) + + # Case 44: Addition of exact fractions + run_benchmark_add( + "Addition of exact fractions", + "0.125", # 1/8 + "0.0625", # 1/16 + iterations, + log_file, + speedup_factors, + ) + + # Case 45: Addition of recurring decimals + run_benchmark_add( + "Addition of recurring decimals", + "0.142857142857142857142857", # 1/7 + "0.076923076923076923076923", # 1/13 + iterations, + log_file, + speedup_factors, + ) + + # === PRACTICAL APPLICATION TESTS === + + # Case 46: GPS coordinates addition + run_benchmark_add( + "GPS coordinates addition", + "37.7749", # San Francisco latitude + "0.0001", # Small delta + iterations, + log_file, + speedup_factors, + ) + + # Case 47: Temperature conversion factors + run_benchmark_add( + "Temperature conversion factors", + "273.15", # Kelvin offset + "32.0", # Fahrenheit offset + iterations, + log_file, + speedup_factors, + ) + + # Case 48: Transaction amounts addition + run_benchmark_add( + "Transaction amounts addition", + "156.78", # First transaction + "243.22", # Second transaction + iterations, + log_file, + speedup_factors, + ) + + # Case 49: Addition across wide range of magnitudes + run_benchmark_add( + "Addition across wide range of magnitudes", + "987654321987654321.987654321", + "0.000000000000000000000000001", + iterations, + log_file, + speedup_factors, + ) + + # Case 50: Equal but opposite values + run_benchmark_add( + "Equal but opposite values", + "12345678901234567890.1234567890", + "-12345678901234567890.1234567890", + 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 Addition Benchmark Summary ===", log_file) + log_print( + "Benchmarked: " + + String(len(speedup_factors)) + + " different addition 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/mojoproject.toml b/mojoproject.toml index 7ad1268..6c1e70f 100644 --- a/mojoproject.toml +++ b/mojoproject.toml @@ -33,4 +33,5 @@ t = "clear && magic run package && magic run mojo test tests --filter" # benches bench_decimal = "magic run package && cd benches/decimal && magic run mojo -I ../ bench.mojo && cd ../.. && magic run clean" bench_bigint = "magic run package && cd benches/bigint && magic run mojo -I ../ bench.mojo && cd ../.. && magic run clean" -bench_biguint = "magic run package && cd benches/biguint && magic run mojo -I ../ bench.mojo && cd ../.. && magic run clean" \ No newline at end of file +bench_biguint = "magic run package && cd benches/biguint && magic run mojo -I ../ bench.mojo && cd ../.. && magic run clean" +bench_bigdecimal = "magic run package && cd benches/bigdecimal && magic run mojo -I ../ bench.mojo && cd ../.. && magic run clean" \ No newline at end of file diff --git a/src/decimojo/__init__.mojo b/src/decimojo/__init__.mojo index 403fc6f..76580f4 100644 --- a/src/decimojo/__init__.mojo +++ b/src/decimojo/__init__.mojo @@ -25,10 +25,10 @@ from decimojo import Decimal, BigInt, RoundingMode """ # Core types -from .decimal.decimal import Decimal +from .decimal.decimal import Decimal, Dec from .bigint.bigint import BigInt, BInt from .biguint.biguint import BigUInt, BUInt -from .bigdecimal.bigdecimal import BigDecimal +from .bigdecimal.bigdecimal import BigDecimal, BDec from .rounding_mode import RoundingMode # Core functions diff --git a/src/decimojo/bigdecimal/arithmetics.mojo b/src/decimojo/bigdecimal/arithmetics.mojo new file mode 100644 index 0000000..82f4592 --- /dev/null +++ b/src/decimojo/bigdecimal/arithmetics.mojo @@ -0,0 +1,80 @@ +# ===----------------------------------------------------------------------=== # +# Copyright 2025 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 functions for mathematical operations on BigDecimal objects. +""" + +import time +import testing + +from decimojo.decimal.decimal import Decimal +from decimojo.rounding_mode import RoundingMode +import decimojo.utility + + +fn add(x1: BigDecimal, x2: BigDecimal) raises -> BigDecimal: + """Returns the sum of two numbers. + + Args: + x1: The first operand. + x2: The second operand. + + Returns: + The sum of x1 and x2. + """ + # 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 + + # 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) + + # Handle addition based on signs + if x1.sign == x2.sign: + # Same sign: Add coefficients, keep sign + var result_coef = coef1 + coef2 + return BigDecimal( + 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 + ) + 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 + ) + else: + # |x1| == |x2|, signs differ, result is 0 + return BigDecimal( + coefficient=BigUInt(UInt32(0)), scale=max_scale, sign=False + ) diff --git a/src/decimojo/bigdecimal/bigdecimal.mojo b/src/decimojo/bigdecimal/bigdecimal.mojo index f10647d..13a48b3 100644 --- a/src/decimojo/bigdecimal/bigdecimal.mojo +++ b/src/decimojo/bigdecimal/bigdecimal.mojo @@ -28,6 +28,8 @@ import testing from decimojo.rounding_mode import RoundingMode +alias BDec = BigDecimal + @value struct BigDecimal: @@ -76,7 +78,7 @@ struct BigDecimal: # Constructors and life time dunder methods # ===------------------------------------------------------------------=== # - fn __init__(out self, coefficient: BigUInt, scale: Int, sign: Bool) raises: + fn __init__(out self, coefficient: BigUInt, scale: Int, sign: Bool): """Constructs a BigDecimal from its components.""" self.coefficient = coefficient self.scale = scale @@ -147,8 +149,9 @@ struct BigDecimal: The BigDecimal representation of the Scalar value. Notes: - If the value is a floating-point number, it is converted to a string - with full precision before converting to BigDecimal. + + If the value is a floating-point number, it is converted to a string + with full precision before converting to BigDecimal. """ var sign = True if value < 0 else False @@ -343,6 +346,43 @@ struct BigDecimal: """ writer.write(String(self)) + # ===------------------------------------------------------------------=== # + # Basic unary operation dunders + # neg + # ===------------------------------------------------------------------=== # + + @always_inline + fn __abs__(self) -> Self: + """Returns the absolute value of this number. + See `absolute()` for more information. + """ + return Self( + coefficient=self.coefficient, + scale=self.scale, + sign=False, + ) + + @always_inline + fn __neg__(self) -> Self: + """Returns the negation of this number. + See `negative()` for more information. + """ + return Self( + coefficient=self.coefficient, + scale=self.scale, + sign=not self.sign, + ) + + # ===------------------------------------------------------------------=== # + # Basic binary arithmetic operation dunders + # These methods are called to implement the binary arithmetic operations + # (+, -, *, @, /, //, %, divmod(), pow(), **, <<, >>, &, ^, |) + # ===------------------------------------------------------------------=== # + + @always_inline + fn __add__(self, other: Self) raises -> Self: + return decimojo.bigdecimal.arithmetics.add(self, other) + # ===------------------------------------------------------------------=== # # Other methods # ===------------------------------------------------------------------=== # @@ -351,3 +391,68 @@ struct BigDecimal: fn is_zero(self) -> Bool: """Returns True if this number represents zero.""" return self.coefficient.is_zero() + + 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 + + fn normalize(self) raises -> BigDecimal: + """Removes trailing zeros from coefficient while adjusting scale. + + Notes: + + Only call it when necessary. Do not normalize after every operation. + """ + 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_words_to_remove = number_of_digits_to_remove // 9 + var number_of_remaining_digits_to_remove = number_of_digits_to_remove % 9 + + var words: List[UInt32] = List[UInt32]() + words = self.coefficient.words[number_of_words_to_remove:] + var coefficient = BigUInt(words^) + + if number_of_remaining_digits_to_remove == 0: + pass + elif number_of_remaining_digits_to_remove == 1: + coefficient = coefficient // BigUInt(UInt32(10)) + elif number_of_remaining_digits_to_remove == 2: + coefficient = coefficient // BigUInt(UInt32(100)) + elif number_of_remaining_digits_to_remove == 3: + coefficient = coefficient // BigUInt(UInt32(1_000)) + elif number_of_remaining_digits_to_remove == 4: + coefficient = coefficient // BigUInt(UInt32(10_000)) + elif number_of_remaining_digits_to_remove == 5: + coefficient = coefficient // BigUInt(UInt32(100_000)) + elif number_of_remaining_digits_to_remove == 6: + coefficient = coefficient // BigUInt(UInt32(1_000_000)) + elif number_of_remaining_digits_to_remove == 7: + coefficient = coefficient // BigUInt(UInt32(10_000_000)) + else: # number_of_remaining_digits_to_remove == 8 + coefficient = coefficient // BigUInt(UInt32(100_000_000)) + + return BigDecimal( + coefficient, + self.scale - number_of_digits_to_remove, + self.sign, + ) diff --git a/src/decimojo/biguint/arithmetics.mojo b/src/decimojo/biguint/arithmetics.mojo index 39c83dc..0785b6e 100644 --- a/src/decimojo/biguint/arithmetics.mojo +++ b/src/decimojo/biguint/arithmetics.mojo @@ -839,6 +839,55 @@ fn multiply_toom_cook_3(x1: BigUInt, x2: BigUInt) raises -> BigUInt: return result +fn multiply_by_power_of_10(x: BigUInt, n: Int) raises -> BigUInt: + """Multiplies a BigUInt by 10^n. + + Args: + x: The BigUInt value to multiply. + n: The power of 10 to multiply by. + + Returns: + A new BigUInt containing the result of the multiplication. + """ + if n == 0: + return x + + var number_of_zero_words = n // 9 + var number_of_remaining_digits = n % 9 + + var result: BigUInt = x + if number_of_remaining_digits == 0: + pass + elif number_of_remaining_digits == 1: + result = multiply(result, BigUInt(UInt32(10))) + elif number_of_remaining_digits == 2: + result = multiply(result, BigUInt(UInt32(100))) + elif number_of_remaining_digits == 3: + result = multiply(result, BigUInt(UInt32(1000))) + elif number_of_remaining_digits == 4: + result = multiply(result, BigUInt(UInt32(10000))) + elif number_of_remaining_digits == 5: + result = multiply(result, BigUInt(UInt32(100000))) + elif number_of_remaining_digits == 6: + result = multiply(result, BigUInt(UInt32(1000000))) + elif number_of_remaining_digits == 7: + result = multiply(result, BigUInt(UInt32(10000000))) + else: # number_of_remaining_digits == 8 + result = multiply(result, BigUInt(UInt32(100000000))) + + if number_of_zero_words > 0: + var words = List[UInt32]( + capacity=number_of_zero_words + len(result.words) + ) + for _ in range(number_of_zero_words): + words.append(UInt32(0)) + for i in range(len(result.words)): + words.append(result.words[i]) + result.words = words^ + + return result^ + + # ===----------------------------------------------------------------------=== # # Division Algorithms # floor_divide_general, floor_divide_inplace_by_2 diff --git a/src/decimojo/biguint/biguint.mojo b/src/decimojo/biguint/biguint.mojo index abce3ee..3212eb3 100644 --- a/src/decimojo/biguint/biguint.mojo +++ b/src/decimojo/biguint/biguint.mojo @@ -671,6 +671,14 @@ struct BigUInt(Absable, IntableRaising, Writable): """ return decimojo.biguint.arithmetics.ceil_modulo(self, other) + fn multiply_by_power_of_10(self, exponent: Int) raises -> Self: + """Returns the result of multiplying this number by 10^exponent. + See `multiply_by_power_of_10()` for more information. + """ + return decimojo.biguint.arithmetics.multiply_by_power_of_10( + self, exponent + ) + fn power(self, exponent: Int) raises -> Self: """Returns the result of raising this number to the power of `exponent`. @@ -799,6 +807,7 @@ struct BigUInt(Absable, IntableRaising, Writable): """Returns True if the BigUInt is uninitialized.""" return len(self.words) == 0 + # FIXME: This method is incorrect fn remove_trailing_zeros(mut number: BigUInt): """Removes trailing zeros from the BigUInt.""" while len(number.words) > 1 and number.words[-1] == 0: diff --git a/src/decimojo/decimal/decimal.mojo b/src/decimojo/decimal/decimal.mojo index 1d2c6ff..549f869 100644 --- a/src/decimojo/decimal/decimal.mojo +++ b/src/decimojo/decimal/decimal.mojo @@ -37,6 +37,8 @@ import decimojo.decimal.rounding from decimojo.rounding_mode import RoundingMode import decimojo.utility +alias Dec = Decimal + @register_passable("trivial") struct Decimal( diff --git a/tests/bigdecimal/test_bigdecimal_arithmetics.mojo b/tests/bigdecimal/test_bigdecimal_arithmetics.mojo new file mode 100644 index 0000000..f4fbbd7 --- /dev/null +++ b/tests/bigdecimal/test_bigdecimal_arithmetics.mojo @@ -0,0 +1,94 @@ +""" +Test BigDecimal arithmetic operations including addition, subtraction, multiplication and division. +""" + +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" + + +fn load_test_cases( + file_path: String, table_name: String +) raises -> List[TestCase]: + """Load test cases from a TOML file for a specific table.""" + var toml = parse_file(file_path) + var test_cases = List[TestCase]() + + # Get array of test cases + var cases_array = toml.get_array_of_tables(table_name) + + for i in range(len(cases_array)): + var case_table = cases_array[i] + test_cases.append( + TestCase( + case_table["a"].as_string(), + case_table["b"].as_string(), + case_table["expected"].as_string(), + case_table["description"].as_string(), + ) + ) + + return test_cases + + +fn test_add() raises: + """Test BigDecimal addition with various test cases.""" + print("------------------------------------------------------") + print("Testing BigDecimal addition...") + + # 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") + + # 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 + ) + # print("✓ Case", i + 1, ":", 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 Error:", + String(e), + ) + failed += 1 + + print("BigDecimal addition tests:", passed, "passed,", failed, "failed") + testing.assert_equal(failed, 0, "All addition tests should pass") + + +fn main() raises: + print("Running BigDecimal arithmetic tests") + + # Run addition tests + test_add() + + print("All BigDecimal arithmetic tests passed!") diff --git a/tests/bigdecimal/test_data/bigdecimal_arithmetics.toml b/tests/bigdecimal/test_data/bigdecimal_arithmetics.toml new file mode 100644 index 0000000..883446b --- /dev/null +++ b/tests/bigdecimal/test_data/bigdecimal_arithmetics.toml @@ -0,0 +1,310 @@ +# === BASIC ADDITION TESTS === +[[addition_tests]] +a = "42" +b = "58" +expected = "100" +description = "Simple integer addition" + +[[addition_tests]] +a = "3.14" +b = "2.71" +expected = "5.85" +description = "Simple decimal addition" + +[[addition_tests]] +a = "0" +b = "0" +expected = "0" +description = "Zero plus zero" + +[[addition_tests]] +a = "1" +b = "0" +expected = "1" +description = "Addition with zero" + +[[addition_tests]] +a = "123.456" +b = "0" +expected = "123.456" +description = "Decimal plus zero" + +# === DIFFERENT SCALE TESTS === +[[addition_tests]] +a = "1.2345" +b = "5.67" +expected = "6.9045" +description = "Addition with different scales" + +[[addition_tests]] +a = "1.23456789012345678901234567" +b = "5.6" +expected = "6.83456789012345678901234567" +description = "Addition with very different scales" + +[[addition_tests]] +a = "9.999" +b = "0.001" +expected = "10.000" +description = "Addition with carry" + +[[addition_tests]] +a = "999999.999999" +b = "0.000001" +expected = "1000000.000000" +description = "Addition causing scale reduction" + +[[addition_tests]] +a = "1.000000000000000000000000001" +b = "2.000000000000000000000000002" +expected = "3.000000000000000000000000003" +description = "Addition with high precision" + +# === SIGN COMBINATION TESTS === +[[addition_tests]] +a = "-1" +b = "-2" +expected = "-3" +description = "Negative plus negative" + +[[addition_tests]] +a = "-3.14" +b = "10" +expected = "6.86" +description = "Negative plus positive (negative smaller)" + +[[addition_tests]] +a = "-10" +b = "3.14" +expected = "-6.86" +description = "Negative plus positive (negative larger)" + +[[addition_tests]] +a = "-5.75" +b = "-10.25" +expected = "-16.00" +description = "Negative plus negative" + +[[addition_tests]] +a = "123.456" +b = "-123.456" +expected = "0.000" +description = "Addition resulting in zero (pos + neg)" + +[[addition_tests]] +a = "0.0000001" +b = "-0.00000005" +expected = "0.00000005" +description = "Addition near zero (small difference)" + +# === LARGE NUMBER TESTS === +[[addition_tests]] +a = "9999999999999999999999999999" +b = "1" +expected = "10000000000000000000000000000" +description = "Large integer addition" + +[[addition_tests]] +a = "-9999999999999999999999999999" +b = "9999999999999999999999999998" +expected = "-1" +description = "Large negative plus positive" + +[[addition_tests]] +a = "99999999999999999999.99999999" +b = "0.00000001" +expected = "100000000000000000000.00000000" +description = "Very large decimal addition with carry" + +[[addition_tests]] +a = "10000000000000000000000000000" +b = "0.00000000000000000000000001" +expected = "10000000000000000000000000000.00000000000000000000000001" +description = "Very large plus very small" + +# === SMALL NUMBER TESTS === +[[addition_tests]] +a = "0.0000000000000000000000001" +b = "0.0000000000000000000000002" +expected = "0.0000000000000000000000003" +description = "Very small positive values" + +[[addition_tests]] +a = "-0.0000000000000000000000001" +b = "-0.0000000000000000000000002" +expected = "-0.0000000000000000000000003" +description = "Very small negative values" + +[[addition_tests]] +a = "0.0000000000000001" +b = "0.00000000000000000000000001" +expected = "0.00000000000000010000000001" +description = "Small values with different scales" + +# === SCIENTIFIC NOTATION TESTS === +[[addition_tests]] +a = "1.23e5" +b = "4.56e4" +expected = "168600" +description = "Addition with scientific notation" + +[[addition_tests]] +a = "1.23e-10" +b = "4.56e-11" +expected = "0.0000000001686" +description = "Addition with negative exponents" + +[[addition_tests]] +a = "1.23e-10" +b = "4.56e10" +expected = "45600000000.000000000123" +description = "Addition with extreme exponent difference" + +# === SPECIAL CASES === +[[addition_tests]] +a = "3.14159265358979323846" +b = "2.71828182845904523536" +expected = "5.85987448204883847382" +description = "Addition of mathematical constants (PI + E)" + +[[addition_tests]] +a = "0.33333333333333333333333333" +b = "0.66666666666666666666666667" +expected = "1.00000000000000000000000000" +description = "Addition of repeating patterns" + +[[addition_tests]] +a = "0.499999999999999999" +b = "0.500000000000000001" +expected = "1.000000000000000000" +description = "Addition resulting in exact integer" + +[[addition_tests]] +a = "9.99999999999999999999999999" +b = "0.00000000000000000000000001" +expected = "10.00000000000000000000000000" +description = "Addition at precision limit with carry" + +# === FINANCIAL SCENARIOS === +[[addition_tests]] +a = "10542.75" +b = "3621.50" +expected = "14164.25" +description = "Financial numbers (dollars and cents)" + +[[addition_tests]] +a = "0.09" +b = "0.01" +expected = "0.10" +description = "Financial numbers (cents)" + +[[addition_tests]] +a = "99.99" +b = "0.01" +expected = "100.00" +description = "Financial addition with carry" + +# === PRECISION BOUNDARY TESTS === +[[addition_tests]] +a = "999999999999999999.9999999" +b = "0.0000001" +expected = "1000000000000000000.0000000" +description = "Addition with rounding at precision boundary" + +[[addition_tests]] +a = "1.1000000000000000000000000" +b = "2.2000000000000000000000000" +expected = "3.3000000000000000000000000" +description = "Addition with trailing zeros" + +[[addition_tests]] +a = "0.125" +b = "0.0625" +expected = "0.1875" +description = "Addition of binary-friendly values (1/8 + 1/16)" + +[[addition_tests]] +a = "0.1" +b = "0.2" +expected = "0.3" +description = "Simple tenths addition" + +# === ADDITIONAL EDGE CASES === +[[addition_tests]] +a = "0.000000000000000000000000009" +b = "0.000000000000000000000000001" +expected = "0.000000000000000000000000010" +description = "Addition near zero with precision limit" + +[[addition_tests]] +a = "0.0" +b = "0.0" +expected = "0.0" +description = "Zero plus zero with decimal point" + +[[addition_tests]] +a = "0.142857142857142857142857" +b = "0.076923076923076923076923" +expected = "0.219780219780219780219780" +description = "Addition of recurring decimals (1/7 + 1/13)" + +[[addition_tests]] +a = "-0" +b = "0" +expected = "0" +description = "Addition of negative zero and zero" + +[[addition_tests]] +a = "1E6" +b = "2000000" +expected = "3000000" +description = "Addition with E notation" + +[[addition_tests]] +a = "1.79769313486231570E+308" +b = "10" +expected = "179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010" +description = "Addition near max double precision" + +[[addition_tests]] +a = "-9.9999999999999999999999" +b = "9.9999999999999999999999" +expected = "0.0000000000000000000000" +description = "Exact cancellation of large numbers" + +[[addition_tests]] +a = "123456789012345678.987654321012345678" +b = "987654321098765432.123456789098765432" +expected = "1111111110111111111.111111110111111110" +description = "Addition with digit carryover throughout" + +# === SPECIFIC APPLICATION DOMAINS === +[[addition_tests]] +a = "37.7749" +b = "0.0001" +expected = "37.7750" +description = "GPS coordinates (latitude + delta)" + +[[addition_tests]] +a = "98.6" +b = "1.2" +expected = "99.8" +description = "Body temperature in Fahrenheit" + +[[addition_tests]] +a = "273.15" +b = "32.0" +expected = "305.15" +description = "Temperature conversion constants (K offset + F offset)" + +[[addition_tests]] +a = "987654321987654321.987654321" +b = "0.000000000000000000000000001" +expected = "987654321987654321.987654321000000000000000001" +description = "Addition with extreme scale difference" + +[[addition_tests]] +a = "0.0425" +b = "0.0015" +expected = "0.0440" +description = "Interest rate calculation"