diff --git a/.gitignore b/.gitignore index e0c91fa..29a8957 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,5 @@ tempCodeRunnerFile.mojo *.log # local files /test*.mojo +/test*.py local \ No newline at end of file diff --git a/benches/bigdecimal/bench_bigdecimal_add.mojo b/benches/bigdecimal/bench_bigdecimal_add.mojo index 2ae4751..168d2af 100644 --- a/benches/bigdecimal/bench_bigdecimal_add.mojo +++ b/benches/bigdecimal/bench_bigdecimal_add.mojo @@ -1,6 +1,7 @@ """ Comprehensive benchmarks for BigDecimal addition. -Compares performance against Python's decimal module with 50 diverse test cases. +Compares performance against Python's decimal module with 60 diverse test cases, +including 10 cases with very large numbers (1000+ digits). """ from decimojo import BigDecimal, RoundingMode @@ -84,9 +85,19 @@ fn run_benchmark_add( var mojo_result = mojo_value1 + mojo_value2 var py_result = py_value1 + py_value2 + # Convert results to strings for comparison + var mojo_result_str = String(mojo_result) + var py_result_str = String(py_result) + # Display results for verification - log_print("Mojo result: " + String(mojo_result), log_file) - log_print("Python result: " + String(py_result), log_file) + log_print("Mojo result: " + mojo_result_str, log_file) + log_print("Python result: " + py_result_str, log_file) + + # Check if results match exactly + if mojo_result == BigDecimal(py_result_str): + log_print("✓ Results match exactly", log_file) + else: + log_print("⚠ WARNING: Results differ!", log_file) # Benchmark Mojo implementation var t0 = perf_counter_ns() @@ -153,19 +164,20 @@ fn main() raises: var iterations = 1000 var pydecimal = Python().import_module("decimal") - # Set Python decimal precision to match Mojo's - pydecimal.getcontext().prec = 28 + # Set Python decimal precision to handle very large numbers + pydecimal.getcontext().prec = 10000 log_print( "Python decimal precision: " + String(pydecimal.getcontext().prec), log_file, ) - log_print("Mojo decimal precision: 28", log_file) + log_print("Mojo decimal precision: dynamic (no fixed limit)", log_file) # Define benchmark cases log_print( "\nRunning decimal addition benchmarks with " + String(iterations) - + " iterations each", + + " iterations each (60 test cases including 10 very large number" + " cases)", log_file, ) @@ -689,6 +701,184 @@ fn main() raises: speedup_factors, ) + # === VERY LARGE NUMBER TESTS (1000+ digits) === + + # Case 51: Addition of 1000-digit decimals + var big_dec_1000_a = ( + "1" + "23456789" * 62 + "123456." + "789012345" * 62 + "789012345" + ) # ~1000 digits + var big_dec_1000_b = ( + "9" + "87654321" * 62 + "987654." + "321098765" * 62 + "321098765" + ) # ~1000 digits + run_benchmark_add( + "Addition of 1000-digit decimals", + big_dec_1000_a, + big_dec_1000_b, + iterations, + log_file, + speedup_factors, + ) + + # Case 52: Addition of 1500-digit decimals with large fractional parts + var big_dec_1500_a = ( + "1" + "23456789" * 93 + "123456." + "789012345" * 93 + "789012345" + ) # ~1500 digits + var big_dec_1500_b = ( + "9" + "87654321" * 93 + "987654." + "321098765" * 93 + "321098765" + ) # ~1500 digits + run_benchmark_add( + "Addition of 1500-digit decimals with large fractional parts", + big_dec_1500_a, + big_dec_1500_b, + iterations, + log_file, + speedup_factors, + ) + + # Case 53: Addition of 2000-digit decimals with carries + var big_dec_2000_a = ( + "9" * 1000 + "." + "9" * 1000 + ) # 2000 digits, all 9s to force maximum carries + var big_dec_2000_b = ( + "0." + "0" * 999 + "1" + ) # Adding small amount to trigger cascading carries + run_benchmark_add( + "Addition of 2000-digit decimals with carries", + big_dec_2000_a, + big_dec_2000_b, + iterations, + log_file, + speedup_factors, + ) + + # Case 54: Large decimal addition with scientific notation (1200 digits) + var big_dec_sci_a = ( + "1." + "23456789" * 149 + "123456789e+500" + ) # ~1200 significant digits + var big_dec_sci_b = ( + "9." + "87654321" * 149 + "987654321e+495" + ) # ~1200 significant digits + run_benchmark_add( + "Large decimal addition with scientific notation (1200 digits)", + big_dec_sci_a, + big_dec_sci_b, + iterations, + log_file, + speedup_factors, + ) + + # Case 55: Very large positive + very large negative decimals (1800 digits) + var big_dec_1800_pos = ( + "1" + "23456789" * 112 + "1234567." + "890123456" * 112 + "890123456" + ) # ~1800 digits positive + var big_dec_1800_neg = ( + "-1" + "23456789" * 112 + "1234566." + "890123456" * 112 + "890123455" + ) # ~1800 digits negative (slightly smaller) + run_benchmark_add( + "Very large positive + very large negative decimals (1800 digits)", + big_dec_1800_pos, + big_dec_1800_neg, + iterations, + log_file, + speedup_factors, + ) + + # Case 56: Addition of decimals with vastly different scales (2500 digits total) + var big_dec_large = ( + "1" + "23456789" * 124 + "1234567." + "890123456" * 124 + "890123456" + ) # ~2500 digits + var big_dec_small_frac = ( + "0." + "0" * 1200 + "1" + "23456789" * 37 + "123456789" + ) # ~1300 decimal places + run_benchmark_add( + "Addition of decimals with vastly different scales (2500 digits total)", + big_dec_large, + big_dec_small_frac, + iterations, + log_file, + speedup_factors, + ) + + # Case 57: Fibonacci-like large decimal addition (1100 digits each) + var fib_dec_a = ( + "1" + + "12358132134" * 49 + + "1123581321." + + "34455891442" * 49 + + "34455891442" + ) # ~1100 digits with Fibonacci pattern + var fib_dec_b = ( + "2" + + "35813213455" * 49 + + "3358132134." + + "55891442334" * 49 + + "55891442334" + ) # ~1100 digits with Fibonacci pattern + run_benchmark_add( + "Fibonacci-like large decimal addition (1100 digits each)", + fib_dec_a, + fib_dec_b, + iterations, + log_file, + speedup_factors, + ) + + # Case 58: Prime-like large decimal addition (1300 digits) + var prime_dec_a = ( + "2" + + "357111317192329" * 43 + + "23571113171923." + + "41434751617379" * 43 + + "41434751617379" + ) # ~1300 digits with prime pattern + var prime_dec_b = ( + "4" + + "143717919293137" * 43 + + "14137171929313." + + "17918389719497" * 43 + + "17918389719497" + ) # ~1300 digits + run_benchmark_add( + "Prime-like large decimal addition (1300 digits)", + prime_dec_a, + prime_dec_b, + iterations, + log_file, + speedup_factors, + ) + + # Case 59: Very large decimal with maximum fractional precision (2000+ digits) + var max_prec_dec_a = ( + "1" + "23456789" * 62 + "1234567." + "890123456" * 186 + "890123456" + ) # ~2000 digits + var max_prec_dec_b = ( + "0." + "0" * 500 + "1" + "987654321" * 186 + "987654321" + ) # ~2000 total digits + run_benchmark_add( + "Very large decimal with maximum fractional precision (2000+ digits)", + max_prec_dec_a, + max_prec_dec_b, + iterations, + log_file, + speedup_factors, + ) + + # Case 60: Extreme scale difference with large decimals (3000+ digits combined) + var extreme_large_dec = ( + "9" + "87654321" * 187 + "987654321." + "123456789" * 187 + "123456789" + ) # ~3000 digits + var extreme_small_dec = ( + "0." + "0" * 1500 + "1" + "23456789" * 93 + "123456789" + ) # ~3000 total precision + run_benchmark_add( + "Extreme scale difference with large decimals (3000+ digits combined)", + extreme_large_dec, + extreme_small_dec, + 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 @@ -701,7 +891,7 @@ fn main() raises: log_print( "Benchmarked: " + String(len(speedup_factors)) - + " different addition cases", + + " different addition cases (out of 60 total)", log_file, ) log_print( diff --git a/benches/bigdecimal/bench_bigdecimal_divide.mojo b/benches/bigdecimal/bench_bigdecimal_divide.mojo index 399255d..53a1076 100644 --- a/benches/bigdecimal/bench_bigdecimal_divide.mojo +++ b/benches/bigdecimal/bench_bigdecimal_divide.mojo @@ -10,6 +10,9 @@ import time import os from collections import List +alias PRECISION = 4096 +alias ITERATIONS = 100 + fn open_log_file() raises -> PythonObject: """ @@ -63,8 +66,8 @@ fn run_benchmark_divide( 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) + log_print("Dividend: " + dividend[:500] + "...", log_file) + log_print("Divisor: " + divisor[:500] + "...", log_file) # Set up Mojo and Python values var mojo_dividend = BigDecimal(dividend) @@ -75,12 +78,22 @@ fn run_benchmark_divide( # Execute the operations once to verify correctness try: - var mojo_result = mojo_dividend / mojo_divisor + var mojo_result = mojo_dividend.true_divide( + mojo_divisor, precision=PRECISION + ) var py_result = py_dividend / py_divisor + var mojo_result_str = String(mojo_result) + var py_result_str = String(py_result) # Display results for verification - log_print("Mojo result: " + String(mojo_result), log_file) - log_print("Python result: " + String(py_result), log_file) + log_print("Mojo result: " + mojo_result_str[:100] + "...", log_file) + log_print("Python result: " + py_result_str[:100] + "...", log_file) + + # Check if results match exactly + if mojo_result == BigDecimal(py_result_str): + log_print("✓ Results match exactly", log_file) + else: + log_print("⚠ WARNING: Results differ!", log_file) # Benchmark Mojo implementation var t0 = perf_counter_ns() @@ -119,6 +132,8 @@ fn main() raises: # Open log file var log_file = open_log_file() var datetime = Python.import_module("datetime") + var pysys = Python.import_module("sys") + pysys.set_int_max_str_digits(10000000) # Create a Mojo List to store speedup factors for averaging later var speedup_factors = List[Float64]() @@ -144,16 +159,16 @@ fn main() raises: except: log_print("Could not retrieve system information", log_file) - var iterations = 1000 + var iterations = ITERATIONS var pydecimal = Python().import_module("decimal") # Set Python decimal precision to match Mojo's - pydecimal.getcontext().prec = 28 + pydecimal.getcontext().prec = PRECISION log_print( "Python decimal precision: " + String(pydecimal.getcontext().prec), log_file, ) - log_print("Mojo decimal precision: 28", log_file) + log_print("Mojo decimal precision: " + String(PRECISION), log_file) # Define benchmark cases log_print( @@ -683,6 +698,66 @@ fn main() raises: speedup_factors, ) + # Case 51: Division 1024 words / 1024 words + run_benchmark_divide( + "Division 1024 words / 1024 words", + "123456789" * 512 + "." + "123456789" * 512, + "987654321" * 512 + "." + "987654321" * 512, + iterations, + log_file, + speedup_factors, + ) + + # Case 52: Division 2048 words / 2048 words + run_benchmark_divide( + "Division 2048 words / 2048 words", + "123456789" * 1024 + "." + "123456789" * 1024, + "987654321" * 1024 + "." + "987654321" * 1024, + iterations, + log_file, + speedup_factors, + ) + + # Case 53: Division 4096 words / 4096 words + run_benchmark_divide( + "Division 4096 words / 4096 words", + "123456789" * 2048 + "." + "123456789" * 2048, + "987654321" * 2048 + "." + "987654321" * 2048, + iterations, + log_file, + speedup_factors, + ) + + # Case 54: Division 8192 words / 8192 words + run_benchmark_divide( + "Division 8192 words / 8192 words", + "123456789" * 4096 + "." + "123456789" * 4096, + "987654321" * 4096 + "." + "987654321" * 4096, + iterations, + log_file, + speedup_factors, + ) + + # Case 55: Division 16384 words / 16384 words + run_benchmark_divide( + "Division 16384 words / 16384 words", + "123456789" * 8192 + "." + "123456789" * 8192, + "987654321" * 8192 + "." + "987654321" * 8192, + iterations, + log_file, + speedup_factors, + ) + + # Case 56: Division 32768 words / 32768 words + run_benchmark_divide( + "Division 32768 words / 32768 words", + "123456789" * 16384 + "." + "123456789" * 16384, + "987654321" * 16384 + "." + "987654321" * 16384, + 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 diff --git a/src/decimojo/__init__.mojo b/src/decimojo/__init__.mojo index 414cbbb..e5e6744 100644 --- a/src/decimojo/__init__.mojo +++ b/src/decimojo/__init__.mojo @@ -28,7 +28,7 @@ from decimojo import Decimal, BigInt, RoundingMode from .decimal.decimal import Decimal, Dec from .bigint.bigint import BigInt, BInt from .biguint.biguint import BigUInt, BUInt -from .bigdecimal.bigdecimal import BigDecimal, BigDec, BDec +from .bigdecimal.bigdecimal import BigDecimal, BDec from .rounding_mode import ( RoundingMode, RM, diff --git a/src/decimojo/bigdecimal/arithmetics.mojo b/src/decimojo/bigdecimal/arithmetics.mojo index d601c12..d7adc54 100644 --- a/src/decimojo/bigdecimal/arithmetics.mojo +++ b/src/decimojo/bigdecimal/arithmetics.mojo @@ -75,26 +75,20 @@ fn add(x1: BigDecimal, x2: BigDecimal) raises -> BigDecimal: # Handle addition based on signs if x1.sign == x2.sign: # Same sign: Add coefficients, keep sign - coef1.add_inplace(coef2) + coef1 += coef2 return BigDecimal(coefficient=coef1^, 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 - ) + coef1 -= coef2 + return BigDecimal(coefficient=coef1^, 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 - ) + coef2 -= coef1 + return BigDecimal(coefficient=coef2^, 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 - ) + return BigDecimal(coefficient=BigUInt.ZERO, scale=max_scale, sign=False) fn subtract(x1: BigDecimal, x2: BigDecimal) raises -> BigDecimal: @@ -140,22 +134,18 @@ fn subtract(x1: BigDecimal, x2: BigDecimal) raises -> BigDecimal: # Handle subtraction based on signs if x1.sign != x2.sign: # Different signs: x1 - (-x2) = x1 + x2, or (-x1) - x2 = -(x1 + x2) - coef1.add_inplace(coef2) + coef1 += coef2 return BigDecimal(coefficient=coef1^, scale=max_scale, sign=x1.sign) # Same signs: Must perform actual subtraction 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 - ) + coef1 -= coef2 + return BigDecimal(coefficient=coef1^, scale=max_scale, sign=x1.sign) elif coef2 > coef1: # |x1| < |x2|, result sign is opposite of x1's sign - var result_coef = coef2 - coef1 - return BigDecimal( - coefficient=result_coef^, scale=max_scale, sign=not x1.sign - ) + coef2 -= coef1 + return BigDecimal(coefficient=coef2^, scale=max_scale, sign=not x1.sign) else: # |x1| == |x2|, result is 0 return BigDecimal(coefficient=BigUInt.ZERO, scale=max_scale, sign=False) diff --git a/src/decimojo/bigdecimal/bigdecimal.mojo b/src/decimojo/bigdecimal/bigdecimal.mojo index f690075..a5866e8 100644 --- a/src/decimojo/bigdecimal/bigdecimal.mojo +++ b/src/decimojo/bigdecimal/bigdecimal.mojo @@ -29,8 +29,10 @@ import testing from decimojo.rounding_mode import RoundingMode from decimojo.bigdecimal.rounding import round_to_precision -alias BigDec = BigDecimal alias BDec = BigDecimal +"""Short alias for `BigDecimal`.""" +alias bdec = BigDecimal +"""Short alias for `BigDecimal` constructor.""" @value @@ -903,72 +905,86 @@ struct BigDecimal( """ return self.coefficient.number_of_digits() - 1 - self.scale - fn extend_precision(self, precision_diff: Int) raises -> BigDecimal: + fn extend_precision(self, precision_diff: Int) -> BigDecimal: """Returns a number with additional decimal places (trailing zeros). This multiplies the coefficient by 10^precision_diff and increases the scale accordingly, preserving the numeric value. + If `precision_diff` is negative, nothing is done and the + original number is returned. Args: - precision_diff: The number of decimal places to add. + precision_diff: The number of decimal places to add, which must be + non-negative. Returns: A new BigDecimal with increased precision. - Raises: - Error: If `precision_diff` is negative. + Notes: + + In debug mode, negative `precision_diff` raises an assertion error. Examples: ``` print(BigDecimal("123.456).scale_up(5)) # Output: 123.45600000 print(BigDecimal("123456").scale_up(3)) # Output: 123456.000 - print(BigDecimal("123456").scale_up(-1)) # Error! + print(BigDecimal("123456").scale_up(-1)) # Output: 123456 (no change) ``` - End of examples. """ - if precision_diff < 0: - raise Error( - "`extend_precision()`: " - "Cannot extend precision with negative value" - ) + debug_assert( + precision_diff >= 0, + "bigdecimal.BigDecimal.extend_precision(): ", + "precision_diff must be non-negative, got: ", + precision_diff, + ) - if precision_diff == 0: + if precision_diff <= 0: return self - var number_of_words_to_add = precision_diff // 9 - var number_of_remaining_digits_to_add = precision_diff % 9 - - var coefficient = self.coefficient - - if number_of_remaining_digits_to_add == 0: - pass - elif number_of_remaining_digits_to_add == 1: - coefficient = coefficient * BigUInt(UInt32(10)) - elif number_of_remaining_digits_to_add == 2: - coefficient = coefficient * BigUInt(UInt32(100)) - elif number_of_remaining_digits_to_add == 3: - coefficient = coefficient * BigUInt(UInt32(1_000)) - elif number_of_remaining_digits_to_add == 4: - coefficient = coefficient * BigUInt(UInt32(10_000)) - elif number_of_remaining_digits_to_add == 5: - coefficient = coefficient * BigUInt(UInt32(100_000)) - elif number_of_remaining_digits_to_add == 6: - coefficient = coefficient * BigUInt(UInt32(1_000_000)) - elif number_of_remaining_digits_to_add == 7: - coefficient = coefficient * BigUInt(UInt32(10_000_000)) - else: # number_of_remaining_digits_to_add == 8 - coefficient = coefficient * BigUInt(UInt32(100_000_000)) - - var words: List[UInt32] = List[UInt32]() - for _ in range(number_of_words_to_add): - words.append(UInt32(0)) - words.extend(coefficient.words) - return BigDecimal( - BigUInt(words^), + decimojo.biguint.arithmetics.multiply_by_power_of_ten( + self.coefficient, precision_diff + ), self.scale + precision_diff, self.sign, ) + fn extend_precision_inplace(mut self, precision_diff: Int): + """Add additional decimal places (trailing zeros) in-place. + This multiplies the coefficient by 10^precision_diff and increases + the scale accordingly, preserving the numeric value. + If `precision_diff` is negative, nothing is done and the + original number is returned. + + Args: + precision_diff: The number of decimal places to add, which must be + non-negative. + + Notes: + + In debug mode, negative `precision_diff` raises an assertion error. + + Examples: + ``` + print(BigDecimal("123.456).scale_up(5)) # Output: 123.45600000 + print(BigDecimal("123456").scale_up(3)) # Output: 123456.000 + print(BigDecimal("123456").scale_up(-1)) # Output: 123456 (no change) + ``` + """ + debug_assert( + precision_diff >= 0, + "bigdecimal.BigDecimal.extend_precision(): ", + "precision_diff must be non-negative, got: ", + precision_diff, + ) + + if precision_diff <= 0: + return + + decimojo.biguint.arithmetics.multiply_inplace_by_power_of_ten( + self.coefficient, precision_diff + ) + self.scale += precision_diff + @always_inline fn print_internal_representation(self): """Prints the internal representation of the BigDecimal.""" diff --git a/src/decimojo/prelude.mojo b/src/decimojo/prelude.mojo index f04c69a..c01eefd 100644 --- a/src/decimojo/prelude.mojo +++ b/src/decimojo/prelude.mojo @@ -27,7 +27,7 @@ from decimojo.prelude import * import decimojo as dm from decimojo.decimal.decimal import Decimal, Dec -from decimojo.bigdecimal.bigdecimal import BigDecimal, BDec +from decimojo.bigdecimal.bigdecimal import BigDecimal, BDec, bdec from decimojo.biguint.biguint import BigUInt, BUInt from decimojo.bigint.bigint import BigInt, BInt from decimojo.rounding_mode import ( diff --git a/src/decimojo/str.mojo b/src/decimojo/str.mojo index ca5a68f..21f0431 100644 --- a/src/decimojo/str.mojo +++ b/src/decimojo/str.mojo @@ -14,9 +14,7 @@ # limitations under the License. # ===----------------------------------------------------------------------=== # -"""String manipulation functions.""" - -import time +"""String parsing and manipulation functions.""" fn parse_numeric_string( @@ -93,13 +91,16 @@ fn parse_numeric_string( for code_ptr in value_bytes: ref code = code_ptr + # If the char is " ", skip it if code == 32: pass + # If the char is "," or "_", skip it elif code == 44 or code == 95: unexpected_end_char = True # If the char is "-" + elif code == 45: unexpected_end_char = True if exponent_sign_read: @@ -112,6 +113,7 @@ fn parse_numeric_string( else: mantissa_sign = True mantissa_sign_read = True + # If the char is "+" elif code == 43: unexpected_end_char = True @@ -124,6 +126,7 @@ fn parse_numeric_string( else: mantissa_sign_read = True # If the char is "." + elif code == 46: unexpected_end_char = False if decimal_point_read: @@ -132,6 +135,7 @@ fn parse_numeric_string( decimal_point_read = True mantissa_sign_read = True # If the char is "e" or "E" + elif code == 101 or code == 69: unexpected_end_char = True if exponent_notation_read: