diff --git a/benches/bigdecimal/bench.mojo b/benches/bigdecimal/bench.mojo index 7819b10..1cdb264 100644 --- a/benches/bigdecimal/bench.mojo +++ b/benches/bigdecimal/bench.mojo @@ -1,6 +1,7 @@ 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 +from bench_bigdecimal_divide import main as bench_divide fn main() raises: @@ -12,6 +13,7 @@ This is the BigInt Benchmarks add: Add sub: Subtract mul: Multiply +div: Divide (true divide) all: Run all benchmarks q: Exit ========================================= @@ -24,10 +26,13 @@ q: Exit bench_sub() elif command == "mul": bench_multiply() + elif command == "div": + bench_divide() elif command == "all": bench_add() bench_sub() bench_multiply() + bench_divide() elif command == "q": return else: diff --git a/benches/bigdecimal/bench_bigdecimal_divide.mojo b/benches/bigdecimal/bench_bigdecimal_divide.mojo new file mode 100644 index 0000000..5c01e88 --- /dev/null +++ b/benches/bigdecimal/bench_bigdecimal_divide.mojo @@ -0,0 +1,720 @@ +""" +Comprehensive benchmarks for BigDecimal division. +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_divide_" + 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.""" + print(msg) + log_file.write(msg + "\n") + log_file.flush() # Ensure the message is written immediately + + +fn run_benchmark_divide( + name: String, + dividend: String, + divisor: String, + iterations: Int, + log_file: PythonObject, + mut speedup_factors: List[Float64], +) raises: + """ + Run a benchmark comparing Mojo BigDecimal division with Python Decimal division. + + Args: + name: Name of the benchmark case. + dividend: String representation of the dividend. + divisor: String representation of the divisor. + 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("Dividend: " + dividend, log_file) + log_print("Divisor: " + divisor, log_file) + + # Set up Mojo and Python values + var mojo_dividend = BigDecimal(dividend) + var mojo_divisor = BigDecimal(divisor) + var pydecimal = Python.import_module("decimal") + var py_dividend = pydecimal.Decimal(dividend) + var py_divisor = pydecimal.Decimal(divisor) + + # Execute the operations once to verify correctness + try: + var mojo_result = mojo_dividend / mojo_divisor + var py_result = py_dividend / py_divisor + + # 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_dividend / mojo_divisor + 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_dividend / py_divisor + 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 division: " + String(mojo_time) + " ns per iteration", + log_file, + ) + log_print( + "Python division: " + 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 Division 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 division benchmarks with " + + String(iterations) + + " iterations each", + log_file, + ) + + # === BASIC DECIMAL DIVISION TESTS === + + # Case 1: Simple integer division + run_benchmark_divide( + "Simple integer division", + "100", + "4", + iterations, + log_file, + speedup_factors, + ) + + # Case 2: Simple decimal division + run_benchmark_divide( + "Simple decimal division", + "10.5", + "2.5", + iterations, + log_file, + speedup_factors, + ) + + # Case 3: Division with different scales (precision alignment) + run_benchmark_divide( + "Division with different scales", + "10.2345", + "5.67", + iterations, + log_file, + speedup_factors, + ) + + # Case 4: Division with very different scales + run_benchmark_divide( + "Division with very different scales", + "5.23456789012345678901234567", + "1.6", + iterations, + log_file, + speedup_factors, + ) + + # Case 5: Division with one + run_benchmark_divide( + "Division with one (a / 1)", + "123.456", + "1", + iterations, + log_file, + speedup_factors, + ) + + # === SCALE AND PRECISION TESTS === + + # Case 6: Precision at decimal limit + run_benchmark_divide( + "Precision at decimal limit", + "9.8765432109876543210987654321", + "1.2345678901234567890123456789", + iterations, + log_file, + speedup_factors, + ) + + # Case 7: Division causing scale increase + run_benchmark_divide( + "Division causing scale increase", + "1", + "0.000001", + iterations, + log_file, + speedup_factors, + ) + + # Case 8: Division with high precision, repeating pattern + run_benchmark_divide( + "Division with high precision, repeating pattern", + "1.00000000000000000000000000", + "0.33333333333333333333333333", + iterations, + log_file, + speedup_factors, + ) + + # Case 9: Division resulting in exact 0.0 + run_benchmark_divide( + "Division resulting in exact 0.0", + "0", + "0.33333333333333333333333333", + iterations, + log_file, + speedup_factors, + ) + + # Case 10: Division with scientific notation + run_benchmark_divide( + "Division with scientific notation", + "1.23e5", + "4.56e4", + iterations, + log_file, + speedup_factors, + ) + + # === SIGN COMBINATION TESTS === + + # Case 11: Positive / Negative + run_benchmark_divide( + "Positive / Negative", + "10", + "-3.14", + iterations, + log_file, + speedup_factors, + ) + + # Case 12: Negative / Positive + run_benchmark_divide( + "Negative / Positive", + "-10", + "3.14", + iterations, + log_file, + speedup_factors, + ) + + # Case 13: Negative / Negative + run_benchmark_divide( + "Negative / Negative", + "-5.75", + "-10.25", + iterations, + log_file, + speedup_factors, + ) + + # Case 14: Division resulting in sign change + run_benchmark_divide( + "Division resulting in sign change", + "50", + "-60.5", + iterations, + log_file, + speedup_factors, + ) + + # Case 15: Division near zero (small difference) + run_benchmark_divide( + "Division near zero (small difference)", + "0.0000001", + "0.00000005", + iterations, + log_file, + speedup_factors, + ) + + # === LARGE NUMBER TESTS === + + # Case 16: Large integer division + run_benchmark_divide( + "Large integer division", + "10000000000000000000000000000", + "1", + iterations, + log_file, + speedup_factors, + ) + + # Case 17: Large negative / positive + run_benchmark_divide( + "Large negative / positive", + "-9999999999999999999999999999", + "1", + iterations, + log_file, + speedup_factors, + ) + + # Case 18: Very large decimal division with borrow + run_benchmark_divide( + "Very large decimal division with borrow", + "100000000000000000000.00000000", + "0.00000001", + iterations, + log_file, + speedup_factors, + ) + + # Case 19: Very large / very small + run_benchmark_divide( + "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_divide( + "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_divide( + "Very small positive values", + "0." + "0" * 25 + "3", + "0." + "0" * 25 + "2", + iterations, + log_file, + speedup_factors, + ) + + # Case 22: Very small negative values + run_benchmark_divide( + "Very small negative values", + "-0." + "0" * 25 + "3", + "-0." + "0" * 25 + "2", + iterations, + log_file, + speedup_factors, + ) + + # Case 23: Small values with different scales + run_benchmark_divide( + "Small values with different scales", + "0." + "0" * 10 + "3", + "0." + "0" * 20 + "1", + iterations, + log_file, + speedup_factors, + ) + + # Case 24: Extreme scales (large negative exponent) + run_benchmark_divide( + "Extreme scales (large negative exponent)", + "1.23e-15", + "4.56e-20", + iterations, + log_file, + speedup_factors, + ) + + # Case 25: Division that requires significant rescaling + run_benchmark_divide( + "Division that requires significant rescaling", + "4.56e10", + "1.23e-10", + iterations, + log_file, + speedup_factors, + ) + + # === SPECIAL VALUE TESTS === + + # Case 26: Division of exact mathematical constants + run_benchmark_divide( + "Division of exact mathematical constants (PI / E)", + "3.14159265358979323846264338328", + "2.71828182845904523536028747135", + iterations, + log_file, + speedup_factors, + ) + + # Case 27: Division of famous constants (Phi / sqrt(2)) + run_benchmark_divide( + "Division of famous constants (Phi / sqrt(2))", + "1.61803398874989484820458683437", + "1.41421356237309504880168872421", + iterations, + log_file, + speedup_factors, + ) + + # Case 28: Division with repeating patterns + run_benchmark_divide( + "Division with repeating patterns", + "5.67896789567895678956789567896", + "1.23451234512345123451234512345", + iterations, + log_file, + speedup_factors, + ) + + # Case 29: Financial numbers (dollars and cents) + run_benchmark_divide( + "Financial numbers (dollars and cents)", + "10542.75", + "3621.50", + iterations, + log_file, + speedup_factors, + ) + + # Case 30: Statistical data (deviations)", + run_benchmark_divide( + "Statistical data (means)", + "98.76543", + "87.65432", + iterations, + log_file, + speedup_factors, + ) + + # === PRECISION TESTS === + + # Case 31: Binary-friendly decimals + run_benchmark_divide( + "Binary-friendly decimals", + "0.5", + "0.25", + iterations, + log_file, + speedup_factors, + ) + + # Case 32: Decimal-unfriendly fractions + run_benchmark_divide( + "Decimal-unfriendly fractions", + "0.66666666666666666666666666", + "0.33333333333333333333333333", + iterations, + log_file, + speedup_factors, + ) + + # Case 33: Division with many borrows + run_benchmark_divide( + "Division with many borrows", + "10.00000000000000000000000000", + "9.99999999999999999999999999", + iterations, + log_file, + speedup_factors, + ) + + # Case 34: Division with trailing zeros + run_benchmark_divide( + "Division with trailing zeros", + "3.3000000000000000000000000", + "2.2000000000000000000000000", + iterations, + log_file, + speedup_factors, + ) + + # Case 35: Division requiring precision increase + run_benchmark_divide( + "Division requiring precision increase", + "1000000000000000000.0000000", + "0.0000001", + iterations, + log_file, + speedup_factors, + ) + + # === APPLICATION-SPECIFIC TESTS === + + # Case 36: Scientific measurement (physics) + run_benchmark_divide( + "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_divide( + "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_divide( + "Chemical concentrations", + "0.00005678", # mol/L + "0.00001234", # mol/L + iterations, + log_file, + speedup_factors, + ) + + # Case 39: Financial market price changes + run_benchmark_divide( + "Financial market price changes", + "3914.75", # Current price + "3914.70", # Previous price + iterations, + log_file, + speedup_factors, + ) + + # Case 40: Interest rate calculations + run_benchmark_divide( + "Interest rate calculations", + "0.0440", # 4.40% interest rate + "0.0015", # 0.15% decrease + iterations, + log_file, + speedup_factors, + ) + + # === EDGE CASES AND EXTREME VALUES === + + # Case 41: Division with maximum precision + run_benchmark_divide( + "Division with maximum precision", + "1." + "0" * 28, + "0." + "9" * 28, + iterations, + log_file, + speedup_factors, + ) + + # Case 42: Division with extreme exponents difference + run_benchmark_divide( + "Division with extreme exponents difference", + "1e20", + "1e-20", + iterations, + log_file, + speedup_factors, + ) + + # Case 43: Division at precision boundary + run_benchmark_divide( + "Division at precision boundary", + "9" * 28 + "." + "9" * 28, + "0." + "0" * 27 + "1", + iterations, + log_file, + speedup_factors, + ) + + # Case 44: Division of exact fractions + run_benchmark_divide( + "Division of exact fractions", + "0.125", # 1/8 + "0.0625", # 1/16 + iterations, + log_file, + speedup_factors, + ) + + # Case 45: Division of recurring decimals + run_benchmark_divide( + "Division of recurring decimals", + "0.142857142857142857142857", # 1/7 + "0.076923076923076923076923", # 1/13 + iterations, + log_file, + speedup_factors, + ) + + # === PRACTICAL APPLICATION TESTS === + + # Case 46: GPS coordinates division + run_benchmark_divide( + "GPS coordinates division", + "37.7749", # Current latitude + "37.7748", # Previous latitude + iterations, + log_file, + speedup_factors, + ) + + # Case 47: Temperature difference calculation + run_benchmark_divide( + "Temperature difference calculation", + "98.6", # Fahrenheit temperature + "37.0", # Celsius equivalent + iterations, + log_file, + speedup_factors, + ) + + # Case 48: Bank balance calculation + run_benchmark_divide( + "Bank balance calculation", + "1000.50", # Initial balance + "243.22", # Withdrawal + iterations, + log_file, + speedup_factors, + ) + + # Case 49: Division across wide range of magnitudes + run_benchmark_divide( + "Division across wide range of magnitudes", + "987654321987654321.987654321", + "0.000000000000000000000000001", + iterations, + log_file, + speedup_factors, + ) + + # Case 50: Division resulting in negative zero + run_benchmark_divide( + "Division resulting in negative zero", + "0.0", + "1", + 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 Division Benchmark Summary ===", log_file) + log_print( + "Benchmarked: " + + String(len(speedup_factors)) + + " different division 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 6b601f5..4a1a505 100644 --- a/src/decimojo/bigdecimal/arithmetics.mojo +++ b/src/decimojo/bigdecimal/arithmetics.mojo @@ -61,8 +61,8 @@ fn add(x1: BigDecimal, x2: BigDecimal) raises -> BigDecimal: 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) + var coef1 = x1.coefficient.scale_up_by_power_of_10(scale_factor1) + var coef2 = x2.coefficient.scale_up_by_power_of_10(scale_factor2) # Handle addition based on signs if x1.sign == x2.sign: @@ -128,8 +128,8 @@ fn subtract(x1: BigDecimal, x2: BigDecimal) raises -> BigDecimal: 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) + var coef1 = x1.coefficient.scale_up_by_power_of_10(scale_factor1) + var coef2 = x2.coefficient.scale_up_by_power_of_10(scale_factor2) # Handle subtraction based on signs if x1.sign != x2.sign: @@ -186,3 +186,152 @@ fn multiply(x1: BigDecimal, x2: BigDecimal) raises -> BigDecimal: scale=x1.scale + x2.scale, sign=x1.sign != x2.sign, ) + + +fn true_divide( + x1: BigDecimal, x2: BigDecimal, max_precision: Int = 28 +) raises -> BigDecimal: + """Returns the quotient of two numbers. + + Args: + x1: The first operand (dividend). + x2: The second operand (divisor). + max_precision: The maximum precision for the result. It should be + non-negative. + + Returns: + The quotient of x1 and x2, with precision up to max_precision. + + Notes: + + - If the coefficients can be divided exactly, the number of digits after + the decimal point is the difference of the scales of the two operands. + - If the coefficients cannot be divided exactly, the number of digits after + the decimal point is max_precision. + - If the division is not exact, the number of digits after the decimal + point is calcuated to max_precision + BUFFER_DIGITS, and the result is + rounded to max_precision according to the specified rules. + """ + alias BUFFER_DIGITS = 2 # Buffer digits for rounding + + # Check for division by zero + if x2.coefficient.is_zero(): + raise Error("Division by zero") + + # Handle dividend of zero + if x1.coefficient.is_zero(): + return BigDecimal( + coefficient=BigUInt(UInt32(0)), + scale=max(0, x1.scale - x2.scale), + sign=x1.sign != x2.sign, + ) + + # TODO: Divided by power of 10 + + # Check whether the coefficients can be divided exactly + # If division is exact, return the result immediately + if len(x1.coefficient.words) >= len(x2.coefficient.words): + # Check if x1 is divisible by x2 + var quotient: BigUInt + var remainder: BigUInt + quotient, remainder = x1.coefficient.divmod(x2.coefficient) + if remainder.is_zero(): + return BigDecimal( + coefficient=quotient, + scale=x1.scale - x2.scale, + sign=x1.sign != x2.sign, + ) + + # Calculate how many extra digits we need to scale x1 by + # We want (max_precision + BUFFER_DIGITS) decimal places in the result + var desired_result_scale = max_precision + BUFFER_DIGITS + var current_result_scale = x1.scale - x2.scale + var scale_factor = max(0, desired_result_scale - current_result_scale) + + # Scale the dividend coefficient + var scaled_x1_coefficient = x1.coefficient + if scale_factor > 0: + scaled_x1_coefficient = x1.coefficient.scale_up_by_power_of_10( + scale_factor + ) + + # Perform the division and get remainder + var result_coefficient: BigUInt + var remainder: BigUInt + result_coefficient, remainder = scaled_x1_coefficient.divmod(x2.coefficient) + var result_scale = x1.scale + scale_factor - x2.scale + + # If the division is exact + # we may need to remove the extra trailing zeros. + # TODO: Think about the behavior, whether division should always return the + # maximum precision even if the result scale is less than max_precision. + # Example: 1 / 1 = 1.0000000000000000000000000000 + if remainder.is_zero(): + # result_scale == scale_factor + (x1.scale - x2.scale) + var number_of_trailing_zeros = result_coefficient.number_of_trailing_zeros() + + # If number_of_trailing_zeros <= scale_factor: + # Just remove the trailing zeros, the scale is larger than expected + # scale (x1.scale - x2.scale) because the division is exact but with + # fractional part. + # If number_of_trailing_zeros > scale_factor: + # We can remove at most scale_factor digits because the result scale + # should be no less than expected scale + var number_of_zeros_to_remove = min( + number_of_trailing_zeros, scale_factor + ) + result_coefficient = result_coefficient.scale_down_by_power_of_10( + number_of_zeros_to_remove + ) + + return BigDecimal( + coefficient=result_coefficient^, + scale=result_scale - number_of_zeros_to_remove, + sign=x1.sign != x2.sign, + ) + + # Otherwise, the division is not exact or have too many digits + # round to max_precision + # TODO: Use round() function when it is available + var digits_to_remove = result_scale - max_precision + if digits_to_remove > BUFFER_DIGITS: + print( + "Warning: Remove (={}) more than BUFFER_DIGITS digits (={}), the" + " algorithm may not be optimal.".format( + digits_to_remove, BUFFER_DIGITS + ) + ) + + # Extract the digits to be rounded + # Example: 2 digits to remove + # divisor = 100 + # half_divisor = 50 + # rounding_digits = 123456 % 100 = 56 + # result_coefficient = 123456 // 100 = 1234 + # If rounding_digits > half_divisor, round up + # If rounding_digits == half_divisor, round up if the last digit of + # result_coefficient is odd + # If rounding_digits < half_divisor, round down + var divisor = BigUInt.ONE.scale_up_by_power_of_10(digits_to_remove) + var half_divisor = divisor // BigUInt(2) + var rounding_digits: BigUInt + result_coefficient, rounding_digits = result_coefficient.divmod(divisor) + + # Apply rounding rules + var round_up = False + if rounding_digits > half_divisor: + round_up = True + elif rounding_digits == half_divisor: + round_up = result_coefficient.words[0] % 2 == 1 + + if round_up: + result_coefficient += BigUInt(1) + + # Update scale + result_scale = max_precision + + return BigDecimal( + coefficient=result_coefficient^, + scale=result_scale, + sign=x1.sign != x2.sign, + ) diff --git a/src/decimojo/bigdecimal/bigdecimal.mojo b/src/decimojo/bigdecimal/bigdecimal.mojo index 12609af..2030ac0 100644 --- a/src/decimojo/bigdecimal/bigdecimal.mojo +++ b/src/decimojo/bigdecimal/bigdecimal.mojo @@ -411,6 +411,10 @@ struct BigDecimal: fn __mul__(self, other: Self) raises -> Self: return decimojo.bigdecimal.arithmetics.multiply(self, other) + @always_inline + fn __truediv__(self, other: Self) raises -> Self: + return decimojo.bigdecimal.arithmetics.true_divide(self, other) + # ===------------------------------------------------------------------=== # # Other methods # ===------------------------------------------------------------------=== # diff --git a/src/decimojo/biguint/arithmetics.mojo b/src/decimojo/biguint/arithmetics.mojo index fef07ac..ecda649 100644 --- a/src/decimojo/biguint/arithmetics.mojo +++ b/src/decimojo/biguint/arithmetics.mojo @@ -177,7 +177,7 @@ fn subtract(x1: BigUInt, x2: BigUInt) raises -> BigUInt: ith += 1 var result = BigUInt(words=words^) - result.remove_trailing_zeros() + result.remove_leading_empty_words() return result^ @@ -251,10 +251,6 @@ fn multiply(x1: BigUInt, x2: BigUInt) raises -> BigUInt: carry = UInt64(0) for j in range(len(x2.words)): - # Skip if the word is zero - if x2.words[j] == 0: - continue - # Calculate the product of the current words # plus the carry from the previous multiplication # plus the value already at this position in the result @@ -272,7 +268,7 @@ fn multiply(x1: BigUInt, x2: BigUInt) raises -> BigUInt: words[i + len(x2.words)] += UInt32(carry) var result = BigUInt(words=words^) - result.remove_trailing_zeros() + result.remove_leading_empty_words() return result^ @@ -335,12 +331,6 @@ fn floor_divide(x1: BigUInt, x2: BigUInt) raises -> BigUInt: floor_divide_inplace_by_double_words(result, x2) return result^ - # CASE: Dividend is quadruple-word (<= 40 digits) - if len(x2.words) == 4: - var result = x1 - floor_divide_inplace_by_quad_words(result, x2) - return result^ - # CASE: Dividend is zero if x1.is_zero(): return BigUInt() # Return zero @@ -355,7 +345,7 @@ fn floor_divide(x1: BigUInt, x2: BigUInt) raises -> BigUInt: # CASE: Divisor is 10^n # First remove the last words (10^9) and then shift the rest - if BigUInt.is_power_of_10(x2): + if x2.is_power_of_10(): var result: BigUInt if len(x2.words) == 1: result = x1 @@ -380,10 +370,7 @@ fn floor_divide(x1: BigUInt, x2: BigUInt) raises -> BigUInt: result.words[i] = quot + carry * power_of_carry carry = rem - # Remove leading zeros - while len(result.words) > 1 and result.words[-1] == 0: - result.words.resize(len(result.words) - 1) - + result.remove_leading_empty_words() return result^ # CASE: division of very, very large numbers @@ -553,138 +540,9 @@ fn divmod(x1: BigUInt, x2: BigUInt) raises -> Tuple[BigUInt, BigUInt]: It is equal to truncated division for positive numbers. """ - # CASE: x2 is single word - if len(x2.words) == 1: - # SUB-CASE: Division by zero - if x2.words[0] == 0: - raise Error("Error in `truncate_divide`: Division by zero") - - # SUB-CASE: Division by one - if x2.words[0] == 1: - return Tuple(x1, BigUInt(UInt32(0))) - - # SUB-CASE: Division by two - if x2.words[0] == 2: - var result = x1 - floor_divide_inplace_by_2(result) - if x2.words[0] & 1 == 0: - return Tuple(result, BigUInt(UInt32(0))) - else: - return Tuple(result^, BigUInt(UInt32(1))) - - # SUB-CASE: Single word // single word - if len(x1.words) == 1: - var result = BigUInt(UInt32(x1.words[0] // x2.words[0])) - var remainder = BigUInt(UInt32(x1.words[0] % x2.words[0])) - return Tuple(result^, remainder^) - - # SUB-CASE: Divisor is single word and is power of 2 - if (x2.words[0] & (x2.words[0] - 1)) == 0: - var quotient = x1 # Make a copy - var divisor = x2.words[0] - # Calculate quotient by repeated division by 2 - while divisor > 1: - floor_divide_inplace_by_2(quotient) - divisor >>= 1 - # Calculate remainder: remainder = x1 - quotient * x2 - var remainder = subtract(x1, multiply(quotient, x2)) - return Tuple(quotient^, remainder^) - - # CASE: Dividend is zero - if x1.is_zero(): - return Tuple(BigUInt(), BigUInt()) # Return zero quotient and remainder - - var comparison_result: Int8 = x1.compare(x2) - # CASE: dividend < divisor - if comparison_result < 0: - return Tuple( - BigUInt(), x1 - ) # Return zero quotient and dividend as remainder - # CASE: dividend == divisor - if comparison_result == 0: - return ( - BigUInt(UInt32(1)), - BigUInt(), - ) # Return one quotient and zero remainder - - # CASE: Duo words division by means of UInt64 - if len(x1.words) <= 2 and len(x2.words) <= 2: - var result = BigUInt.from_scalar(x1.to_uint64() // x2.to_uint64()) - var remainder = BigUInt.from_scalar(x1.to_uint64() % x2.to_uint64()) - return Tuple(result^, remainder^) - - # CASE: Divisor is 10^n - # First remove the last words (10^9) and then shift the rest - if BigUInt.is_power_of_10(x2): - var result = BigUInt(List[UInt32]()) - var remainder = BigUInt(List[UInt32]()) - - if len(x2.words) == 1: - result = x1 - else: - var word_shift = len(x2.words) - 1 - # If we need to drop more words than exists, result is zero - if word_shift >= len(x1.words): - return Tuple(BigUInt(), x1) - # Create result with the remaining words - for i in range(word_shift, len(x1.words)): - result.words.append(x1.words[i]) - for i in range(min(word_shift, len(x1.words))): - remainder.words.append(x1.words[i]) - - # Get the last word of the divisor - var x2_word = x2.words[len(x2.words) - 1] - var carry = UInt32(0) - var power_of_carry = UInt32(1_000_000_000) // x2_word - for i in range(len(result.words) - 1, -1, -1): - var quot = result.words[i] // x2_word - var rem = result.words[i] % x2_word - result.words[i] = quot + carry * power_of_carry - carry = rem - - # Add the final remainder from the digit-wise division - if carry > 0: - # If we already have words in the remainder, we need to add this carry - if len(remainder.words) > 0: - # Create a BigUInt with the carry and multiply by appropriate power of 10 - var carry_biguint = BigUInt(carry) - if len(x2.words) > 1: - for _ in range(len(x2.words) - 1): - # Multiply by 10^9 for each word position - carry_biguint = multiply( - carry_biguint, BigUInt(1_000_000_000) - ) - remainder = add(remainder, carry_biguint) - else: - remainder.words.append(carry) - - result.remove_trailing_zeros() - remainder.remove_trailing_zeros() - - return Tuple(result^, remainder^) - - # CASE: division of very, very large numbers - # Use Newton-Raphson division for large numbers? - - # CASE: all other situations - # Normalize divisor to improve quotient estimation - var normalized_x1 = x1 - var normalized_x2 = x2 - var normalization_factor: UInt32 = 1 - - # Calculate normalization factor to make leading digit of divisor large - var msw = x2.words[len(x2.words) - 1] - if msw < 500_000_000: - while msw < 100_000_000: # Ensure leading digit is significant - msw *= 10 - normalization_factor *= 10 - - # Apply normalization - if normalization_factor > 1: - normalized_x1 = multiply(x1, BigUInt(normalization_factor)) - normalized_x2 = multiply(x2, BigUInt(normalization_factor)) - - return divmod_general(normalized_x1, normalized_x2) + var quotient = floor_divide(x1, x2) + var remainder = subtract(x1, multiply(x2, quotient)) + return (quotient^, remainder^) # ===----------------------------------------------------------------------=== # @@ -756,12 +614,12 @@ fn multiply_toom_cook_3(x1: BigUInt, x2: BigUInt) raises -> BigUInt: b2 = BigUInt.from_list(b2_words^) # Remove trailing zeros - a0.remove_trailing_zeros() - a1.remove_trailing_zeros() - a2.remove_trailing_zeros() - b0.remove_trailing_zeros() - b1.remove_trailing_zeros() - b2.remove_trailing_zeros() + a0.remove_leading_empty_words() + a1.remove_leading_empty_words() + a2.remove_leading_empty_words() + b0.remove_leading_empty_words() + b1.remove_leading_empty_words() + b2.remove_leading_empty_words() print("DEBUG: a0 =", a0) print("DEBUG: a1 =", a1) @@ -840,7 +698,7 @@ 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: +fn scale_up_by_power_of_10(x: BigUInt, n: Int) raises -> BigUInt: """Multiplies a BigUInt by 10^n (n>=0). Args: @@ -850,6 +708,11 @@ fn multiply_by_power_of_10(x: BigUInt, n: Int) raises -> BigUInt: Returns: A new BigUInt containing the result of the multiplication. """ + if n < 0: + raise Error( + "Error in `multiply_by_power_of_10`: n must be non-negative" + ) + if n == 0: return x @@ -961,7 +824,7 @@ fn floor_divide_general(x1: BigUInt, x2: BigUInt) raises -> BigUInt: remainder = subtract(remainder, shifted_product) j -= 1 - result.remove_trailing_zeros() + result.remove_leading_empty_words() return result^ @@ -1029,7 +892,7 @@ fn floor_divide_partition(x1: BigUInt, x2: BigUInt) raises -> BigUInt: remainder = subtract(dividend, multiply(quotient, x2)) number_of_words_remainder = len(remainder.words) - result.remove_trailing_zeros() + result.remove_leading_empty_words() return result^ @@ -1052,7 +915,7 @@ fn floor_divide_inplace_by_single_word(mut x1: BigUInt, x2: BigUInt) raises: var dividend = carry * UInt64(1_000_000_000) + UInt64(x1.words[i]) x1.words[i] = UInt32(dividend // x2_value) carry = dividend % x2_value - x1.remove_trailing_zeros() + x1.remove_leading_empty_words() fn floor_divide_inplace_by_double_words(mut x1: BigUInt, x2: BigUInt) raises: @@ -1086,85 +949,7 @@ fn floor_divide_inplace_by_double_words(mut x1: BigUInt, x2: BigUInt) raises: x1.words[i - 1] = UInt32(quotient % UInt128(1_000_000_000)) carry = dividend % x2_value - x1.remove_trailing_zeros() - return - - -fn floor_divide_inplace_by_quad_words(mut x1: BigUInt, x2: BigUInt) raises: - """Divides a BigUInt by quad-word divisor in-place. - - Args: - x1: The BigUInt value to divide by the divisor. - x2: The double-word divisor. - - Notes: - - The improvement in this algorithm is marginal. - """ - if x2.is_zero(): - raise Error( - "Error in `floor_divide_inplace_by_double_words`: Division by zero" - ) - - # CASE: all other situations - var x2_value = UInt256(x2.words[3]) * UInt256( - 1_000_000_000_000_000_000_000_000_000 - ) + UInt256(x2.words[2]) * UInt256(1_000_000_000_000_000_000) + UInt256( - x2.words[1] - ) * UInt256( - 1_000_000_000 - ) + UInt256( - x2.words[0] - ) - - var carry = UInt256(0) - if len(x1.words) % 4 == 1: - carry = UInt256(x1.words[-1]) - x1.words.resize(len(x1.words) - 1) - if len(x1.words) % 4 == 2: - carry = UInt256(x1.words[-1]) * UInt256(1_000_000_000) + UInt256( - x1.words[-2] - ) - x1.words.resize(len(x1.words) - 2) - if len(x1.words) % 4 == 3: - carry = ( - UInt256(x1.words[-1]) * UInt256(1_000_000_000_000_000_000) - + UInt256(x1.words[-2]) * UInt256(1_000_000_000) - + UInt256(x1.words[-3]) - ) - x1.words.resize(len(x1.words) - 3) - - for i in range(len(x1.words) - 1, -1, -4): - var dividend = carry * UInt256( - 1_000_000_000_000_000_000_000_000_000_000_000_000 - ) + UInt256(x1.words[i]) * UInt256( - 1_000_000_000_000_000_000_000_000_000 - ) + UInt256( - x1.words[i - 1] - ) * UInt256( - 1_000_000_000_000_000_000 - ) + UInt256( - x1.words[i - 2] - ) * UInt256( - 1_000_000_000 - ) + UInt256( - x1.words[i - 3] - ) - var quotient = dividend // x2_value - carry = dividend % x2_value - - var quot = quotient // (UInt256(1_000_000_000_000_000_000_000_000_000)) - x1.words[i] = UInt32(quot) - var rem = quotient % (UInt256(1_000_000_000_000_000_000_000_000_000)) - quot = rem // UInt256(1_000_000_000_000_000_000) - x1.words[i - 1] = UInt32(quot) - rem = rem % UInt256(1_000_000_000_000_000_000) - quot = rem // UInt256(1_000_000_000) - x1.words[i - 2] = UInt32(quot) - rem = rem % UInt256(1_000_000_000) - x1.words[i - 3] = UInt32(rem) - - x1.remove_trailing_zeros() + x1.remove_leading_empty_words() return @@ -1190,74 +975,71 @@ fn floor_divide_inplace_by_2(mut x: BigUInt): x.words.resize(len(x.words) - 1) -fn divmod_general(x1: BigUInt, x2: BigUInt) raises -> Tuple[BigUInt, BigUInt]: - """General divmod algorithm for BigInt numbers. +fn scale_down_by_power_of_10(x: BigUInt, n: Int) raises -> BigUInt: + """Floor divide a BigUInt by 10^n (n>=0). + It is equal to removing the last n digits of the number. Args: - x1: The dividend. - x2: The divisor. + x: The BigUInt value to multiply. + n: The power of 10 to multiply by. Returns: - The quotient of x1 // x2 and the remainder of x1 % x2. - - Raises: - ValueError: If the divisor is zero. + A new BigUInt containing the result of the multiplication. """ + if n < 0: + raise Error( + "Error in `scale_down_by_power_of_10`: n must be non-negative" + ) + if n == 0: + return x - if x2.is_zero(): - raise Error("Error in `divmod_general`: Division by zero") - - # Initialize result and remainder - var result = BigUInt(List[UInt32](capacity=len(x1.words))) - var remainder = x1 - - # Calculate significant digits - var n = len(remainder.words) - var m = len(x2.words) - - # Shift and initialize - var d = n - m - for _ in range(d + 1): - result.words.append(0) - - # Main division loop - var j = d - while j >= 0: - # OPTIMIZATION: Better quotient estimation - var q = estimate_quotient(remainder, x2, j, m) - - # Calculate trial product - var trial_product = x2 * BigUInt(UInt32(q)) - var shifted_product = shift_words_left(trial_product, j) - - # OPTIMIZATION: Binary search for adjustment - if shifted_product.compare(remainder) > 0: - var low: UInt64 = 0 - var high: UInt64 = q - 1 - - while low <= high: - var mid = (low + high) / 2 - - # Recalculate with new q - trial_product = x2 * BigUInt(UInt32(mid)) - shifted_product = shift_words_left(trial_product, j) - - if shifted_product.compare(remainder) <= 0: - q = mid # This works - low = mid + 1 - else: - high = mid - 1 - - # Final recalculation with best q - trial_product = x2 * BigUInt(UInt32(q)) - shifted_product = shift_words_left(trial_product, j) - - result.words[j] = UInt32(q) - remainder = subtract(remainder, shifted_product) - j -= 1 - - result.remove_trailing_zeros() - return Tuple(result, remainder) + # First remove the last words (10^9) + var result: BigUInt + if len(x.words) == 1: + result = x + else: + var word_shift = n // 9 + # If we need to drop more words than exists, result is zero + if word_shift >= len(x.words): + return BigUInt.ZERO + # Create result with the remaining words + words = List[UInt32]() + for i in range(word_shift, len(x.words)): + words.append(x.words[i]) + result = BigUInt(words=words^) + + # Then shift the remaining words right + # Get the last word of the divisor + var digit_shift = n % 9 + var carry = UInt32(0) + var divisor: UInt32 + if digit_shift == 0: + divisor = UInt32(1) + elif digit_shift == 1: + divisor = UInt32(10) + elif digit_shift == 2: + divisor = UInt32(100) + elif digit_shift == 3: + divisor = UInt32(1000) + elif digit_shift == 4: + divisor = UInt32(10000) + elif digit_shift == 5: + divisor = UInt32(100000) + elif digit_shift == 6: + divisor = UInt32(1000000) + elif digit_shift == 7: + divisor = UInt32(10000000) + else: # digit_shift == 8 + divisor = UInt32(100000000) + var power_of_carry = UInt32(1_000_000_000) // divisor + for i in range(len(result.words) - 1, -1, -1): + var quot = result.words[i] // divisor + var rem = result.words[i] % divisor + result.words[i] = quot + carry * power_of_carry + carry = rem + + result.remove_leading_empty_words() + return result^ # ===----------------------------------------------------------------------=== # diff --git a/src/decimojo/biguint/biguint.mojo b/src/decimojo/biguint/biguint.mojo index 57c0615..8b096bd 100644 --- a/src/decimojo/biguint/biguint.mojo +++ b/src/decimojo/biguint/biguint.mojo @@ -588,6 +588,10 @@ struct BigUInt(Absable, IntableRaising, Writable): fn __mod__(self, other: Self) raises -> Self: return decimojo.biguint.arithmetics.floor_modulo(self, other) + @always_inline + fn __divmod__(self, other: Self) raises -> Tuple[Self, Self]: + return decimojo.biguint.arithmetics.divmod(self, other) + @always_inline fn __pow__(self, exponent: Self) raises -> Self: return self.power(exponent) @@ -706,13 +710,27 @@ 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. + @always_inline + fn divmod(self, other: Self) raises -> Tuple[Self, Self]: + """Returns the result of divmod this number by `other`. + See `divmod()` for more information. + """ + return decimojo.biguint.arithmetics.divmod(self, other) + + @always_inline + fn scale_up_by_power_of_10(self, n: Int) raises -> Self: + """Returns the result of multiplying this number by 10^n (n>=0). + See `scale_up_by_power_of_10()` for more information. """ - return decimojo.biguint.arithmetics.multiply_by_power_of_10( - self, exponent - ) + return decimojo.biguint.arithmetics.scale_up_by_power_of_10(self, n) + + @always_inline + fn scale_down_by_power_of_10(self, n: Int) raises -> Self: + """Returns the result of floored dividing this number by 10^n (n>=0). + It is equal to removing the last n digits of the number. + See `scale_down_by_power_of_10()` for more information. + """ + return decimojo.biguint.arithmetics.scale_down_by_power_of_10(self, n) fn power(self, exponent: Int) raises -> Self: """Returns the result of raising this number to the power of `exponent`. @@ -766,6 +784,31 @@ struct BigUInt(Absable, IntableRaising, Writable): # Other methods # ===------------------------------------------------------------------=== # + 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: ", 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 {}:{}{}".format( + i, " " * (10 - ndigits), String(self.words[i]) + ).rjust(9, fillchar="0") + ) + print("----------------------------------------------") + @always_inline fn is_zero(self) -> Bool: """Returns True if this BigUInt represents zero.""" @@ -781,6 +824,7 @@ struct BigUInt(Absable, IntableRaising, Writable): """Returns True if this BigUInt represents two.""" return len(self.words) == 1 and self.words[0] == 2 + @always_inline fn is_power_of_10(x: BigUInt) -> Bool: """Check if x is a power of 10.""" for i in range(len(x.words) - 1): @@ -802,35 +846,11 @@ struct BigUInt(Absable, IntableRaising, Writable): return False @always_inline - fn number_of_words(self) -> Int: - """Returns the number of words in the BigInt.""" - return len(self.words) - - 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: ", 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 {}:{}{}".format( - i, " " * (10 - ndigits), String(self.words[i]) - ).rjust(9, fillchar="0") - ) - print("----------------------------------------------") + fn is_unitialized(self) -> Bool: + """Returns True if the BigUInt is uninitialized.""" + return len(self.words) == 0 + @always_inline fn ith_digit(self, i: Int) raises -> UInt8: """Returns the ith digit of the BigUInt.""" if i < 0: @@ -848,12 +868,28 @@ struct BigUInt(Absable, IntableRaising, Writable): digit = word % 10 return UInt8(digit) - fn is_unitialized(self) -> Bool: - """Returns True if the BigUInt is uninitialized.""" - return len(self.words) == 0 + @always_inline + fn number_of_words(self) -> Int: + """Returns the number of words in the BigInt.""" + return len(self.words) + + @always_inline + fn number_of_trailing_zeros(self) -> Int: + """Returns the number of trailing zeros in the BigUInt.""" + var result: Int = 0 + for i in range(len(self.words)): + if self.words[i] == 0: + result += 9 + else: + var word = self.words[i] + while word % 10 == 0: + result += 1 + word = word // 10 + break + return result - # 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: - number.words.resize(len(number.words) - 1) + @always_inline + fn remove_leading_empty_words(mut self): + """Removes leading words of 0 from BigUInt's internal representation.""" + while len(self.words) > 1 and self.words[-1] == 0: + self.words.resize(len(self.words) - 1) diff --git a/tests/bigdecimal/test_bigdecimal_divide.mojo b/tests/bigdecimal/test_bigdecimal_divide.mojo new file mode 100644 index 0000000..2fce93a --- /dev/null +++ b/tests/bigdecimal/test_bigdecimal_divide.mojo @@ -0,0 +1,130 @@ +""" +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 +from tomlmojo import parse_file + +alias division_file_path = "tests/bigdecimal/test_data/bigdecimal_divide.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_true_divide() raises: + """Test BigDecimal division with various test cases.""" + print("------------------------------------------------------") + print("Testing BigDecimal division...") + + var pydecimal = Python.import_module("decimal") + + # Load test cases from TOML file + var test_cases = load_test_cases(division_file_path, "division_tests") + print("Loaded", len(test_cases), "test cases for division") + + # 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) + + # Special case: Check if divisor is zero + if String(b) == "0": + print("Skipping division by zero test (would cause error)") + continue + + 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 division tests:", passed, "passed,", failed, "failed") + testing.assert_equal(failed, 0, "All division tests should pass") + + +fn test_division_by_zero() raises: + """Test that division by zero raises an error.""" + print("------------------------------------------------------") + print("Testing BigDecimal division by zero...") + + var a = BigDecimal("1") + var b = BigDecimal("0") + + var exception_caught = False + try: + _ = a / b + exception_caught = False + except: + exception_caught = True + + testing.assert_true( + exception_caught, "Division by zero should raise an error" + ) + print("✓ Division by zero correctly raises an error") + + +fn main() raises: + print("Running BigDecimal arithmetic tests") + + # Run division tests + test_true_divide() + + # Test division by zero + test_division_by_zero() + + print("All BigDecimal arithmetic tests passed!") diff --git a/tests/bigdecimal/test_data/bigdecimal_divide.toml b/tests/bigdecimal/test_data/bigdecimal_divide.toml new file mode 100644 index 0000000..6229ad0 --- /dev/null +++ b/tests/bigdecimal/test_data/bigdecimal_divide.toml @@ -0,0 +1,323 @@ +# === BASIC DIVISION TESTS === +[[division_tests]] +a = "10" +b = "2" +expected = "5" +description = "Simple integer division" + +[[division_tests]] +a = "10" +b = "4" +expected = "2.5" +description = "Integer division resulting in decimal" + +[[division_tests]] +a = "1" +b = "3" +expected = "0.3333333333333333333333333333" +description = "Division resulting in repeating decimal" + +[[division_tests]] +a = "10.5" +b = "2.5" +expected = "4.2" +description = "Decimal division resulting in exact decimal" + +[[division_tests]] +a = "0" +b = "5" +expected = "0" +description = "Zero divided by something" + +# === DIVISION WITH DIFFERENT SCALES === +[[division_tests]] +a = "1.23456789" +b = "0.001" +expected = "1234.56789" +description = "Division by small decimal (scale increase)" + +[[division_tests]] +a = "0.001" +b = "100" +expected = "0.00001" +description = "Small number divided by large (scale increase)" + +[[division_tests]] +a = "1.234" +b = "10" +expected = "0.1234" +description = "Division resulting in scale increase" + +[[division_tests]] +a = "5.75" +b = "0.1" +expected = "57.5" +description = "Division by 0.1 (scale shift)" + +[[division_tests]] +a = "5.75" +b = "0.01" +expected = "575" +description = "Division by 0.01 (scale shift)" + +# === SIGN COMBINATION TESTS === +[[division_tests]] +a = "-10" +b = "2" +expected = "-5" +description = "Negative divided by positive" + +[[division_tests]] +a = "10" +b = "-2" +expected = "-5" +description = "Positive divided by negative" + +[[division_tests]] +a = "-10" +b = "-2" +expected = "5" +description = "Negative divided by negative" + +[[division_tests]] +a = "-0" +b = "5" +expected = "-0" +description = "Negative zero divided by positive" + +[[division_tests]] +a = "0" +b = "-5" +expected = "-0" +description = "Zero divided by negative" + +# === ROUNDING TESTS === +[[division_tests]] +a = "1" +b = "7" +expected = "0.1428571428571428571428571429" +description = "Division with repeating decimal (1/7)" + +[[division_tests]] +a = "2" +b = "3" +expected = "0.6666666666666666666666666667" +description = "Division with repeating decimal (2/3)" + +[[division_tests]] +a = "10" +b = "6" +expected = "1.6666666666666666666666666667" +description = "Division with repeating decimal (10/6)" + +[[division_tests]] +a = "1" +b = "9" +expected = "0.1111111111111111111111111111" +description = "Division with repeating digit (1/9)" + +[[division_tests]] +a = "100" +b = "3" +expected = "33.3333333333333333333333333333" +description = "Large repeating division" + +# === LARGE AND SMALL NUMBER TESTS === +[[division_tests]] +a = "9999999999999999999999999999" +b = "3" +expected = "3333333333333333333333333333" +description = "Large number simple division" + +[[division_tests]] +a = "1" +b = "9999999999999999999999999999" +expected = "0.0000000000000000000000000001" +description = "One divided by large number" + +[[division_tests]] +a = "0.0000000000000000000000000001" +b = "0.0000000000000000000000000003" +expected = "0.3333333333333333333333333333" +description = "Small number division" + +[[division_tests]] +a = "1000000000000000000000000000000" +b = "0.0000000000000000000000000001" +expected = "10000000000000000000000000000000000000000000000000000000000" +description = "Large divided by small" + +[[division_tests]] +a = "0.0000000000000000000000000001" +b = "1000000000000000000000000000000" +expected = "0.0000000000000000000000000000000000000000000000000000000001" +description = "Small divided by large" + +# === SCIENTIFIC NOTATION TESTS === +[[division_tests]] +a = "1.23e5" +b = "4.56e2" +expected = "269.7368421052631578947368421053" +description = "Division with scientific notation" + +[[division_tests]] +a = "1.23e-5" +b = "4.56e-2" +expected = "0.0002697368421052631578947368" +description = "Division with negative exponents" + +[[division_tests]] +a = "1.23e5" +b = "4.56e-2" +expected = "2697368.4210526315789473684210526316" +description = "Division with mixed exponents" + +# === SPECIAL CASES === +[[division_tests]] +a = "3.14159265358979323846" +b = "2.71828182845904523536" +expected = "1.1557273497909217179092429607" +description = "Division of mathematical constants (PI / E)" + +[[division_tests]] +a = "1" +b = "1" +expected = "1" +description = "Division by one" + +[[division_tests]] +a = "0.33333333333333333333333333" +b = "3" +expected = "0.11111111111111111111111111" +description = "Repeating decimal divided by integer" + +[[division_tests]] +a = "5" +b = "10" +expected = "0.5" +description = "Division resulting in exact fraction" + +# === DECIMAL PLACE SHIFTS === +[[division_tests]] +a = "123.456789" +b = "10" +expected = "12.3456789" +description = "Division by 10 (decimal shift left)" + +[[division_tests]] +a = "123.456789" +b = "100" +expected = "1.23456789" +description = "Division by 100 (decimal shift left)" + +[[division_tests]] +a = "123.456789" +b = "1000" +expected = "0.123456789" +description = "Division by 1000 (decimal shift left)" + +[[division_tests]] +a = "123.456789" +b = "0.1" +expected = "1234.56789" +description = "Division by 0.1 (decimal shift right)" + +[[division_tests]] +a = "123.456789" +b = "0.01" +expected = "12345.6789" +description = "Division by 0.01 (decimal shift right)" + +# === PRECISION BOUNDARY TESTS === +[[division_tests]] +a = "1" +b = "3" +expected = "0.3333333333333333333333333333" +description = "Division at precision boundary (1/3)" + +[[division_tests]] +a = "2" +b = "3" +expected = "0.6666666666666666666666666667" +description = "Division at precision boundary (2/3)" + +[[division_tests]] +a = "9.9999999999999999999999999" +b = "9.9999999999999999999999999" +expected = "1" +description = "Division of equal values at precision limit" + +[[division_tests]] +a = "0.0000000000000000000000001" +b = "0.0000000000000000000000001" +expected = "1" +description = "Division of equal small values" + +# === FINANCIAL SCENARIOS === +[[division_tests]] +a = "100.00" +b = "4" +expected = "25.00" +description = "Financial division (dollars)" + +[[division_tests]] +a = "100.00" +b = "3" +expected = "33.3333333333333333333333333333" +description = "Financial division with repeating result" + +[[division_tests]] +a = "156.48" +b = "12" +expected = "13.04" +description = "Financial calculation (price per item)" + +# === APPLICATION SCENARIOS === +[[division_tests]] +a = "360" +b = "12" +expected = "30" +description = "Circle division (degrees in a circle / months)" + +[[division_tests]] +a = "1000" +b = "3" +expected = "333.3333333333333333333333333333" +description = "Division for equal distribution" + +[[division_tests]] +a = "2.54" # cm +b = "0.01" # convert to m +expected = "254" +description = "Unit conversion (cm to m)" + +[[division_tests]] +a = "1234.56" +b = "51.44" +expected = "24" +description = "Division resulting in exact integer" + +# === EDGE CASES === +[[division_tests]] +a = "0.0000000000000000000000000009" +b = "0.0000000000000000000000000003" +expected = "3" +description = "Division of very small numbers" + +[[division_tests]] +a = "1" +b = "0.000000000000000000000000001" +expected = "1000000000000000000000000000" +description = "One divided by very small number" + +[[division_tests]] +a = "9999999999999999999999999999.9999999999999999999999999999" +b = "9999999999999999999999999999.9999999999999999999999999999" +expected = "1" +description = "Division of very large equal numbers" + +[[division_tests]] +a = "0" +b = "1" +expected = "0" +description = "Division of zero"