diff --git a/README.md b/README.md index 5cb7d97..e6a5446 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ For the latest development version, clone the [GitHub repository](https://github | ---------- | ------ | | v0.1.0 | >=25.1 | | v0.2.0 | >=25.2 | +| v0.3.0 | >=25.2 | ## Quick start diff --git a/benches/bigdecimal/bench.mojo b/benches/bigdecimal/bench.mojo index d4cedb1..007b0e9 100644 --- a/benches/bigdecimal/bench.mojo +++ b/benches/bigdecimal/bench.mojo @@ -6,6 +6,7 @@ from bench_bigdecimal_sqrt import main as bench_sqrt from bench_bigdecimal_exp import main as bench_exp from bench_bigdecimal_ln import main as bench_ln from bench_bigdecimal_root import main as bench_root +from bench_bigdecimal_round import main as bench_round from bench_bigdecimal_scale_up_by_power_of_10 import main as bench_scale_up @@ -23,6 +24,7 @@ sqrt: Square root exp: Exponential ln: Natural logarithm root: Root +round: Round all: Run all benchmarks q: Exit ========================================= @@ -47,6 +49,8 @@ scaleup: Scale up by power of 10 bench_ln() elif command == "root": bench_root() + elif command == "round": + bench_round() elif command == "all": bench_add() bench_sub() @@ -56,6 +60,7 @@ scaleup: Scale up by power of 10 bench_exp() bench_ln() bench_root() + bench_round() elif command == "q": return elif command == "scaleup": diff --git a/benches/bigdecimal/bench_bigdecimal_round.mojo b/benches/bigdecimal/bench_bigdecimal_round.mojo new file mode 100644 index 0000000..e184173 --- /dev/null +++ b/benches/bigdecimal/bench_bigdecimal_round.mojo @@ -0,0 +1,538 @@ +""" +Comprehensive benchmarks for BigDecimal rounding functions. +Compares performance against Python's decimal module with various rounding modes and 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_round_" + 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_round( + name: String, + value: String, + decimal_places: Int, + rounding_mode_mojo: RoundingMode, + rounding_mode_python: String, + iterations: Int, + log_file: PythonObject, + mut speedup_factors: List[Float64], +) raises: + """ + Run a benchmark comparing Mojo BigDecimal round with Python Decimal quantize. + + Args: + name: Name of the benchmark case. + value: String representation of the number to round. + decimal_places: Number of decimal places to round to. + rounding_mode_mojo: Rounding mode to use for Mojo BigDecimal. + rounding_mode_python: Rounding mode string to use for Python Decimal. + 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("Value: " + value, log_file) + log_print("Decimal places: " + String(decimal_places), log_file) + log_print("Rounding mode: " + String(rounding_mode_mojo), log_file) + + # Set up Mojo and Python values + var mojo_value = BigDecimal(value) + var pydecimal = Python.import_module("decimal") + var py_value = pydecimal.Decimal(value) + + # Create Python rounding context + pydecimal.setcontext(pydecimal.Context(rounding=pydecimal.ROUND_HALF_EVEN)) + if rounding_mode_python == "ROUND_DOWN": + pydecimal.setcontext(pydecimal.Context(rounding=pydecimal.ROUND_DOWN)) + elif rounding_mode_python == "ROUND_UP": + pydecimal.setcontext(pydecimal.Context(rounding=pydecimal.ROUND_UP)) + elif rounding_mode_python == "ROUND_HALF_UP": + pydecimal.setcontext( + pydecimal.Context(rounding=pydecimal.ROUND_HALF_UP) + ) + elif rounding_mode_python == "ROUND_HALF_EVEN": + pydecimal.setcontext( + pydecimal.Context(rounding=pydecimal.ROUND_HALF_EVEN) + ) + + # Execute the operations once to verify correctness + try: + var mojo_result = mojo_value.round(decimal_places, rounding_mode_mojo) + var py_result = py_value.__round__(decimal_places) + + # 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_value.round(decimal_places, rounding_mode_mojo) + 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_value.__round__(decimal_places) + 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 round: " + String(mojo_time) + " ns per iteration", + log_file, + ) + log_print( + "Python round: " + 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 Round Function 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 round benchmarks with " + + String(iterations) + + " iterations each", + log_file, + ) + + # === ROUND_DOWN MODE TESTS === + + # Case 1: Round down to integer + run_benchmark_round( + "Round down to integer", + "12.345", + 0, + RoundingMode.ROUND_DOWN, + "ROUND_DOWN", + iterations, + log_file, + speedup_factors, + ) + + # Case 2: Round down to 1 decimal place + run_benchmark_round( + "Round down to 1 decimal place", + "12.345", + 1, + RoundingMode.ROUND_DOWN, + "ROUND_DOWN", + iterations, + log_file, + speedup_factors, + ) + + # Case 3: Round down negative to integer + run_benchmark_round( + "Round down negative to integer", + "-12.345", + 0, + RoundingMode.ROUND_DOWN, + "ROUND_DOWN", + iterations, + log_file, + speedup_factors, + ) + + # === ROUND_UP MODE TESTS === + + # Case 4: Round up to integer + run_benchmark_round( + "Round up to integer", + "12.345", + 0, + RoundingMode.ROUND_UP, + "ROUND_UP", + iterations, + log_file, + speedup_factors, + ) + + # Case 5: Round up to 1 decimal place + run_benchmark_round( + "Round up to 1 decimal place", + "12.345", + 1, + RoundingMode.ROUND_UP, + "ROUND_UP", + iterations, + log_file, + speedup_factors, + ) + + # Case 6: Round up negative to integer + run_benchmark_round( + "Round up negative to integer", + "-12.345", + 0, + RoundingMode.ROUND_UP, + "ROUND_UP", + iterations, + log_file, + speedup_factors, + ) + + # === ROUND_HALF_UP MODE TESTS === + + # Case 7: Round half up to integer (0.5 -> 1) + run_benchmark_round( + "Round half up to integer (0.5 -> 1)", + "12.5", + 0, + RoundingMode.ROUND_HALF_UP, + "ROUND_HALF_UP", + iterations, + log_file, + speedup_factors, + ) + + # Case 8: Round half up to 1 decimal place (0.05 -> 0.1) + run_benchmark_round( + "Round half up to 1 decimal place (0.05 -> 0.1)", + "12.05", + 1, + RoundingMode.ROUND_HALF_UP, + "ROUND_HALF_UP", + iterations, + log_file, + speedup_factors, + ) + + # Case 9: Round half up negative to integer (-0.5 -> -1) + run_benchmark_round( + "Round half up negative to integer (-0.5 -> -1)", + "-12.5", + 0, + RoundingMode.ROUND_HALF_UP, + "ROUND_HALF_UP", + iterations, + log_file, + speedup_factors, + ) + + # === ROUND_HALF_EVEN (BANKER'S ROUNDING) MODE TESTS === + + # Case 10: Round half even to integer (0.5 -> 0 with even digit) + run_benchmark_round( + "Round half even to integer (even digit)", + "12.5", + 0, + RoundingMode.ROUND_HALF_EVEN, + "ROUND_HALF_EVEN", + iterations, + log_file, + speedup_factors, + ) + + # Case 11: Round half even to integer (0.5 -> 1 with odd digit) + run_benchmark_round( + "Round half even to integer (odd digit)", + "13.5", + 0, + RoundingMode.ROUND_HALF_EVEN, + "ROUND_HALF_EVEN", + iterations, + log_file, + speedup_factors, + ) + + # Case 12: Round half even to 1 decimal place (0.05 -> 0.0 with even digit) + run_benchmark_round( + "Round half even to 1 decimal place (even digit)", + "12.25", + 1, + RoundingMode.ROUND_HALF_EVEN, + "ROUND_HALF_EVEN", + iterations, + log_file, + speedup_factors, + ) + + # === DECIMAL PLACES VARIATIONS === + + # Case 13: Round to higher precision (add zeros) + run_benchmark_round( + "Round to higher precision (add zeros)", + "12.345", + 5, + RoundingMode.ROUND_HALF_EVEN, + "ROUND_HALF_EVEN", + iterations, + log_file, + speedup_factors, + ) + + # Case 14: Negative decimal places (round to tens) + run_benchmark_round( + "Negative decimal places (round to tens)", + "123.456", + -1, + RoundingMode.ROUND_HALF_EVEN, + "ROUND_HALF_EVEN", + iterations, + log_file, + speedup_factors, + ) + + # Case 15: Negative decimal places (round to hundreds) + run_benchmark_round( + "Negative decimal places (round to hundreds)", + "123.456", + -2, + RoundingMode.ROUND_HALF_EVEN, + "ROUND_HALF_EVEN", + iterations, + log_file, + speedup_factors, + ) + + # === SPECIAL VALUES AND EDGE CASES === + + # Case 16: Round zero + run_benchmark_round( + "Round zero", + "0", + 2, + RoundingMode.ROUND_HALF_EVEN, + "ROUND_HALF_EVEN", + iterations, + log_file, + speedup_factors, + ) + + # Case 17: Round very small number + run_benchmark_round( + "Round very small number", + "0.0000000000000000000000001", + 10, + RoundingMode.ROUND_HALF_EVEN, + "ROUND_HALF_EVEN", + iterations, + log_file, + speedup_factors, + ) + + # Case 18: Round very large number + run_benchmark_round( + "Round very large number", + "9999999999.99999", + 2, + RoundingMode.ROUND_HALF_EVEN, + "ROUND_HALF_EVEN", + iterations, + log_file, + speedup_factors, + ) + + # Case 19: Round with carry over (9.9999 -> 10) + run_benchmark_round( + "Round with carry over (9.9999 -> 10)", + "9.9999", + 0, + RoundingMode.ROUND_HALF_EVEN, + "ROUND_HALF_EVEN", + iterations, + log_file, + speedup_factors, + ) + + # Case 20: Round exactly half with alternate modes + run_benchmark_round( + "Round exactly half (down)", + "10.5", + 0, + RoundingMode.ROUND_DOWN, + "ROUND_DOWN", + iterations, + log_file, + speedup_factors, + ) + + run_benchmark_round( + "Round exactly half (up)", + "10.5", + 0, + RoundingMode.ROUND_UP, + "ROUND_UP", + iterations, + log_file, + speedup_factors, + ) + + run_benchmark_round( + "Round exactly half (half up)", + "10.5", + 0, + RoundingMode.ROUND_HALF_UP, + "ROUND_HALF_UP", + iterations, + log_file, + speedup_factors, + ) + + run_benchmark_round( + "Round exactly half (half even)", + "10.5", + 0, + RoundingMode.ROUND_HALF_EVEN, + "ROUND_HALF_EVEN", + iterations, + log_file, + speedup_factors, + ) + + # === SCIENTIFIC NOTATION INPUTS === + + # Case 24: Round scientific notation value + run_benchmark_round( + "Round scientific notation value", + "1.2345e5", + 2, + RoundingMode.ROUND_HALF_EVEN, + "ROUND_HALF_EVEN", + iterations, + log_file, + speedup_factors, + ) + + # Case 25: Round small scientific notation value + run_benchmark_round( + "Round small scientific notation value", + "1.2345e-5", + 8, + RoundingMode.ROUND_HALF_EVEN, + "ROUND_HALF_EVEN", + 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 Round Function Benchmark Summary ===", log_file + ) + log_print( + "Benchmarked: " + + String(len(speedup_factors)) + + " different rounding 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 0aa3a88..cc18709 100644 --- a/src/decimojo/bigdecimal/arithmetics.mojo +++ b/src/decimojo/bigdecimal/arithmetics.mojo @@ -25,6 +25,15 @@ from decimojo.decimal.decimal import Decimal from decimojo.rounding_mode import RoundingMode import decimojo.utility +# ===----------------------------------------------------------------------=== # +# Arithmetic operations on BigDecimal objects +# add(x1, x2) +# subtract(x1, x2) +# multiply(x1, x2) +# true_divide(x1, x2, precision) +# true_divide_inexact(x1, x2, number_of_significant_digits) +# ===----------------------------------------------------------------------=== # + fn add(x1: BigDecimal, x2: BigDecimal) raises -> BigDecimal: """Returns the sum of two numbers. @@ -399,3 +408,73 @@ fn true_divide_inexact( scale=result_scale, sign=x1.sign != x2.sign, ) + + +fn truncate_divide(x1: BigDecimal, x2: BigDecimal) raises -> BigDecimal: + """Returns the quotient of two numbers truncated to zeros. + + Args: + x1: The first operand (dividend). + x2: The second operand (divisor). + + Returns: + The quotient of x1 and x2, truncated to zeros. + + Raises: + Error: If division by zero is attempted. + + Notes: + This function performs integer division that truncates toward zero. + For example: 7//4 = 1, -7//4 = -1, 7//(-4) = -1, (-7)//(-4) = 1. + """ + # 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(BigUInt.ZERO, 0, False) + + # Calculate adjusted scales to align decimal points + var scale_diff = x1.scale - x2.scale + + # If scale_diff is positive, we need to scale up the dividend + # If scale_diff is negative, we need to scale up the divisor + if scale_diff > 0: + var divisor = x2.coefficient.scale_up_by_power_of_10(scale_diff) + var quotient = x1.coefficient.truncate_divide(divisor) + return BigDecimal(quotient^, 0, x1.sign != x2.sign) + + else: # scale_diff < 0 + var dividend = x1.coefficient.scale_up_by_power_of_10(-scale_diff) + var quotient = dividend.truncate_divide(x2.coefficient) + return BigDecimal(quotient^, 0, x1.sign != x2.sign) + + +fn truncate_modulo( + x1: BigDecimal, x2: BigDecimal, precision: Int +) raises -> BigDecimal: + """Returns the trucated modulo of two numbers. + + Args: + x1: The first operand (dividend). + x2: The second operand (divisor). + precision: The number of significant digits in the result. + + Returns: + The truncated modulo of x1 and x2. + + Raises: + Error: If division by zero is attempted. + """ + # Check for division by zero + if x2.coefficient.is_zero(): + raise Error("Division by zero") + + return subtract( + x1, + multiply( + truncate_divide(x1, x2), + x2, + ), + ) diff --git a/src/decimojo/bigdecimal/bigdecimal.mojo b/src/decimojo/bigdecimal/bigdecimal.mojo index 73f9eaf..86008b0 100644 --- a/src/decimojo/bigdecimal/bigdecimal.mojo +++ b/src/decimojo/bigdecimal/bigdecimal.mojo @@ -34,7 +34,7 @@ alias BDec = BigDecimal @value -struct BigDecimal: +struct BigDecimal(Absable, Comparable, IntableRaising, Roundable, Writable): """Represents a arbitrary-precision decimal. Notes: @@ -463,6 +463,13 @@ struct BigDecimal: self, other, precision=28 ) + @always_inline + fn __floordiv__(self, other: Self) raises -> Self: + """Returns the result of floor division. + See `arithmetics.truncate_divide()` for more information. + """ + return decimojo.bigdecimal.arithmetics.truncate_divide(self, other) + @always_inline fn __pow__(self, exponent: Self) raises -> Self: """Returns the result of exponentiation.""" @@ -501,35 +508,68 @@ struct BigDecimal: # ===------------------------------------------------------------------=== # @always_inline - fn __gt__(self, other: BigDecimal) raises -> Bool: + fn __gt__(self, other: BigDecimal) -> Bool: """Returns whether self is greater than other.""" return decimojo.bigdecimal.comparison.compare(self, other) > 0 @always_inline - fn __ge__(self, other: BigDecimal) raises -> Bool: + fn __ge__(self, other: BigDecimal) -> Bool: """Returns whether self is greater than or equal to other.""" return decimojo.bigdecimal.comparison.compare(self, other) >= 0 @always_inline - fn __lt__(self, other: BigDecimal) raises -> Bool: + fn __lt__(self, other: BigDecimal) -> Bool: """Returns whether self is less than other.""" return decimojo.bigdecimal.comparison.compare(self, other) < 0 @always_inline - fn __le__(self, other: BigDecimal) raises -> Bool: + fn __le__(self, other: BigDecimal) -> Bool: """Returns whether self is less than or equal to other.""" return decimojo.bigdecimal.comparison.compare(self, other) <= 0 @always_inline - fn __eq__(self, other: BigDecimal) raises -> Bool: + fn __eq__(self, other: BigDecimal) -> Bool: """Returns whether self equals other.""" return decimojo.bigdecimal.comparison.compare(self, other) == 0 @always_inline - fn __ne__(self, other: BigDecimal) raises -> Bool: + fn __ne__(self, other: BigDecimal) -> Bool: """Returns whether self does not equal other.""" return decimojo.bigdecimal.comparison.compare(self, other) != 0 + # ===------------------------------------------------------------------=== # + # Other dunders that implements traits + # round + # ===------------------------------------------------------------------=== # + + @always_inline + fn __round__(self, ndigits: Int) -> Self: + """Rounds the number to the specified number of decimal places. + If `ndigits` is not given, rounds to 0 decimal places. + If rounding causes errors, returns the value itself. + """ + try: + return decimojo.bigdecimal.rounding.round( + self, + ndigits=ndigits, + rounding_mode=RoundingMode.ROUND_HALF_EVEN, + ) + except e: + return self + + @always_inline + fn __round__(self) -> Self: + """Rounds the number to the specified number of decimal places. + If `ndigits` is not given, rounds to 0 decimal places. + If rounding causes errors, returns the value itself. + """ + try: + return decimojo.bigdecimal.rounding.round( + self, ndigits=0, rounding_mode=RoundingMode.ROUND_HALF_EVEN + ) + except e: + return self + # ===------------------------------------------------------------------=== # # Mathematical methods that do not implement a trait (not a dunder) # ===------------------------------------------------------------------=== # @@ -609,6 +649,13 @@ struct BigDecimal: self, other, number_of_significant_digits ) + @always_inline + fn truncate_divide(self, other: Self) raises -> Self: + """Returns the result of truncating division of two BigDecimal numbers. + See `arithmetics.truncate_divide()` for more information. + """ + return decimojo.bigdecimal.arithmetics.truncate_divide(self, other) + @always_inline fn power(self, exponent: Self, precision: Int) raises -> Self: """Returns the result of exponentiation with the given precision. @@ -616,6 +663,13 @@ struct BigDecimal: """ return decimojo.bigdecimal.exponential.power(self, exponent, precision) + @always_inline + fn round(self, ndigits: Int, rounding_mode: RoundingMode) raises -> Self: + """Rounds the number to the specified precision. + See `bigdecimal.rounding.round()` for more information. + """ + return decimojo.bigdecimal.rounding.round(self, ndigits, rounding_mode) + @always_inline fn round_to_precision( mut self, diff --git a/src/decimojo/bigdecimal/comparison.mojo b/src/decimojo/bigdecimal/comparison.mojo index 0bf4115..032cc75 100644 --- a/src/decimojo/bigdecimal/comparison.mojo +++ b/src/decimojo/bigdecimal/comparison.mojo @@ -21,7 +21,7 @@ Implements functions for comparison operations on BigDecimal objects. from decimojo.bigdecimal.bigdecimal import BigDecimal -fn compare_absolute(x1: BigDecimal, x2: BigDecimal) raises -> Int8: +fn compare_absolute(x1: BigDecimal, x2: BigDecimal) -> Int8: """Compares the absolute values of two numbers. Args: @@ -71,7 +71,7 @@ fn compare_absolute(x1: BigDecimal, x2: BigDecimal) raises -> Int8: return scaled_x1.compare(x2.coefficient) -fn compare(x1: BigDecimal, x2: BigDecimal) raises -> Int8: +fn compare(x1: BigDecimal, x2: BigDecimal) -> Int8: """Compares two BigDecimal numbers. Args: @@ -110,44 +110,44 @@ fn compare(x1: BigDecimal, x2: BigDecimal) raises -> Int8: return abs_comparison -fn equals(x1: BigDecimal, x2: BigDecimal) raises -> Bool: +fn equals(x1: BigDecimal, x2: BigDecimal) -> Bool: """Returns whether x1 equals x2.""" return compare(x1, x2) == 0 -fn not_equals(x1: BigDecimal, x2: BigDecimal) raises -> Bool: +fn not_equals(x1: BigDecimal, x2: BigDecimal) -> Bool: """Returns whether x1 does not equal x2.""" return compare(x1, x2) != 0 -fn less_than(x1: BigDecimal, x2: BigDecimal) raises -> Bool: +fn less_than(x1: BigDecimal, x2: BigDecimal) -> Bool: """Returns whether x1 is less than x2.""" return compare(x1, x2) < 0 -fn less_than_or_equal(x1: BigDecimal, x2: BigDecimal) raises -> Bool: +fn less_than_or_equal(x1: BigDecimal, x2: BigDecimal) -> Bool: """Returns whether x1 is less than or equal to x2.""" return compare(x1, x2) <= 0 -fn greater_than(x1: BigDecimal, x2: BigDecimal) raises -> Bool: +fn greater_than(x1: BigDecimal, x2: BigDecimal) -> Bool: """Returns whether x1 is greater than x2.""" return compare(x1, x2) > 0 -fn greater_than_or_equal(x1: BigDecimal, x2: BigDecimal) raises -> Bool: +fn greater_than_or_equal(x1: BigDecimal, x2: BigDecimal) -> Bool: """Returns whether x1 is greater than or equal to x2.""" return compare(x1, x2) >= 0 -fn max(x1: BigDecimal, x2: BigDecimal) raises -> BigDecimal: +fn max(x1: BigDecimal, x2: BigDecimal) -> BigDecimal: """Returns the maximum of x1 and x2.""" if compare(x1, x2) >= 0: return x1 return x2 -fn min(x1: BigDecimal, x2: BigDecimal) raises -> BigDecimal: +fn min(x1: BigDecimal, x2: BigDecimal) -> BigDecimal: """Returns the minimum of x1 and x2.""" if compare(x1, x2) <= 0: return x1 diff --git a/src/decimojo/bigdecimal/rounding.mojo b/src/decimojo/bigdecimal/rounding.mojo index c8152b2..01f921f 100644 --- a/src/decimojo/bigdecimal/rounding.mojo +++ b/src/decimojo/bigdecimal/rounding.mojo @@ -25,6 +25,63 @@ from decimojo.bigdecimal.bigdecimal import BigDecimal # ===------------------------------------------------------------------------===# +fn round( + number: BigDecimal, + ndigits: Int, + rounding_mode: RoundingMode, +) raises -> BigDecimal: + """Rounds the number to the specified number of decimal places. + + Args: + number: The number to round. + ndigits: Number of decimal places to round to. + rounding_mode: Rounding mode to use. + RoundingMode.ROUND_DOWN: Round down. + RoundingMode.ROUND_UP: Round up. + RoundingMode.ROUND_HALF_UP: Round half up. + RoundingMode.ROUND_HALF_EVEN: Round half even. + + Notes: + + If `ndigits` is negative, the last `ndigits` digits of the integer part of + the number will be dropped and the scale will be `ndigits`. + Example: + round(123.456, 2) -> 123.46 + round(123.456, -1) -> 12E+1 + round(123.456, -2) -> 1E+2 + round(123.456, -3) -> 0E+3 + round(678.890, -3) -> 1E+3 + """ + var ndigits_to_remove = number.scale - ndigits + if ndigits_to_remove == 0: + return number + if ndigits_to_remove < 0: + # Add trailing zeros to the number + return number.extend_precision(precision_diff=-ndigits_to_remove) + else: # ndigits_to_remove > 0 + # Remove trailing digits from the number + if ndigits_to_remove > number.coefficient.number_of_digits(): + # If the number of digits to remove is greater than + # the number of digits in the coefficient, return 0. + return BigDecimal( + coefficient=BigUInt.ZERO, + scale=ndigits, + sign=number.sign, + ) + var coefficient = ( + number.coefficient.remove_trailing_digits_with_rounding( + ndigits=ndigits_to_remove, + rounding_mode=rounding_mode, + remove_extra_digit_due_to_rounding=False, + ) + ) + return BigDecimal( + coefficient=coefficient, + scale=ndigits, + sign=number.sign, + ) + + fn round_to_precision( mut number: BigDecimal, precision: Int, diff --git a/src/decimojo/biguint/arithmetics.mojo b/src/decimojo/biguint/arithmetics.mojo index 6568411..8b46e4d 100644 --- a/src/decimojo/biguint/arithmetics.mojo +++ b/src/decimojo/biguint/arithmetics.mojo @@ -604,8 +604,8 @@ fn divmod(x1: BigUInt, x2: BigUInt) raises -> Tuple[BigUInt, BigUInt]: # ===----------------------------------------------------------------------=== # -fn scale_up_by_power_of_10(x: BigUInt, n: Int) raises -> BigUInt: - """Multiplies a BigUInt by 10^n (n>=0). +fn scale_up_by_power_of_10(x: BigUInt, n: Int) -> BigUInt: + """Multiplies a BigUInt by 10^n if n > 0, otherwise doing nothing. Args: x: The BigUInt value to multiply. @@ -614,12 +614,7 @@ fn scale_up_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: + if n <= 0: return x var number_of_zero_words = n // 9 @@ -653,10 +648,6 @@ fn scale_up_by_power_of_10(x: BigUInt, n: Int) raises -> BigUInt: multiplier = UInt64(10_000_000) else: # number_of_remaining_digits == 8 multiplier = UInt64(100_000_000) - debug_assert( - number_of_remaining_digits >= 9, - "number_of_remaining_digits must be less than 9", - ) for i in range(len(x.words)): var product = UInt64(x.words[i]) * multiplier + carry diff --git a/src/decimojo/biguint/biguint.mojo b/src/decimojo/biguint/biguint.mojo index 68c065c..cc050a1 100644 --- a/src/decimojo/biguint/biguint.mojo +++ b/src/decimojo/biguint/biguint.mojo @@ -737,7 +737,7 @@ struct BigUInt(Absable, IntableRaising, Writable): decimojo.biguint.arithmetics.floor_divide_inplace_by_2(self) @always_inline - fn scale_up_by_power_of_10(self, n: Int) raises -> Self: + fn scale_up_by_power_of_10(self, n: Int) -> Self: """Returns the result of multiplying this number by 10^n (n>=0). See `scale_up_by_power_of_10()` for more information. """ diff --git a/src/decimojo/rounding_mode.mojo b/src/decimojo/rounding_mode.mojo index a7013c3..b972bd8 100644 --- a/src/decimojo/rounding_mode.mojo +++ b/src/decimojo/rounding_mode.mojo @@ -79,7 +79,7 @@ struct RoundingMode: fn __str__(self) -> String: if self == Self.ROUND_DOWN: return "ROUND_DOWN" - elif self == Self.ROUND_UP: + elif self == Self.ROUND_HALF_UP: return "ROUND_HALF_UP" elif self == Self.ROUND_HALF_EVEN: return "ROUND_HALF_EVEN" diff --git a/tests/bigdecimal/test_bigdecimal_rounding.mojo b/tests/bigdecimal/test_bigdecimal_rounding.mojo new file mode 100644 index 0000000..fe9788d --- /dev/null +++ b/tests/bigdecimal/test_bigdecimal_rounding.mojo @@ -0,0 +1,578 @@ +""" +Test BigDecimal rounding operations with various rounding modes and precision values. +""" + +from python import Python +import testing + +from decimojo import BigDecimal, RoundingMode +from decimojo.tests import TestCase +from tomlmojo import parse_file + +alias rounding_file_path = "tests/bigdecimal/test_data/bigdecimal_rounding.toml" + + +fn load_test_cases_two_arguments( + 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(), # Value to round + case_table["b"].as_string(), # Decimal places + case_table["expected"].as_string(), # Expected result + case_table["description"].as_string(), # Test description + ) + ) + return test_cases^ + + +fn test_round_down() raises: + """Test BigDecimal rounding with ROUND_DOWN mode.""" + print("------------------------------------------------------") + print("Testing BigDecimal ROUND_DOWN mode...") + + var pydecimal = Python.import_module("decimal") + + # Load test cases from TOML file + var test_cases = load_test_cases_two_arguments( + rounding_file_path, "round_down_tests" + ) + print("Loaded", len(test_cases), "test cases for ROUND_DOWN") + + # 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 value = BigDecimal(test_case.a) + var decimal_places = Int(test_case.b) + var expected = BigDecimal(test_case.expected) + + # Perform rounding + var result = value.round(decimal_places, RoundingMode.ROUND_DOWN) + + try: + # Using String comparison for easier debugging + testing.assert_equal( + String(result), String(expected), test_case.description + ) + passed += 1 + except e: + print( + "=" * 50, + "\n", + i + 1, + "failed:", + test_case.description, + "\n Value:", + test_case.a, + "\n Decimal places:", + test_case.b, + "\n Expected:", + test_case.expected, + "\n Got:", + String(result), + "\n Python decimal result (for reference):", + String( + pydecimal.Decimal(test_case.a).__round__(Int(test_case.b)) + ), + ) + failed += 1 + + print("BigDecimal ROUND_DOWN tests:", passed, "passed,", failed, "failed") + testing.assert_equal(failed, 0, "All ROUND_DOWN tests should pass") + + +fn test_round_up() raises: + """Test BigDecimal rounding with ROUND_UP mode.""" + print("------------------------------------------------------") + print("Testing BigDecimal ROUND_UP mode...") + + pydecimal = Python.import_module("decimal") + + # Load test cases from TOML file + var test_cases = load_test_cases_two_arguments( + rounding_file_path, "round_up_tests" + ) + print("Loaded", len(test_cases), "test cases for ROUND_UP") + + # 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 value = BigDecimal(test_case.a) + var decimal_places = Int(test_case.b) + var expected = BigDecimal(test_case.expected) + + # Perform rounding + var result = value.round(decimal_places, RoundingMode.ROUND_UP) + + try: + # Using String comparison for easier debugging + testing.assert_equal( + String(result), String(expected), test_case.description + ) + passed += 1 + except e: + print( + "=" * 50, + "\n", + i + 1, + "failed:", + test_case.description, + "\n Value:", + test_case.a, + "\n Decimal places:", + test_case.b, + "\n Expected:", + test_case.expected, + "\n Got:", + String(result), + "\n Python decimal result (for reference):", + String( + pydecimal.Decimal(test_case.a).__round__(Int(test_case.b)) + ), + ) + failed += 1 + + print("BigDecimal ROUND_UP tests:", passed, "passed,", failed, "failed") + testing.assert_equal(failed, 0, "All ROUND_UP tests should pass") + + +fn test_round_half_up() raises: + """Test BigDecimal rounding with ROUND_HALF_UP mode.""" + print("------------------------------------------------------") + print("Testing BigDecimal ROUND_HALF_UP mode...") + + pydecimal = Python.import_module("decimal") + + # Load test cases from TOML file + var test_cases = load_test_cases_two_arguments( + rounding_file_path, "round_half_up_tests" + ) + print("Loaded", len(test_cases), "test cases for ROUND_HALF_UP") + + # 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 value = BigDecimal(test_case.a) + var decimal_places = Int(test_case.b) + var expected = BigDecimal(test_case.expected) + + # Perform rounding + var result = value.round(decimal_places, RoundingMode.ROUND_HALF_UP) + + try: + # Using String comparison for easier debugging + testing.assert_equal( + String(result), String(expected), test_case.description + ) + passed += 1 + except e: + print( + "=" * 50, + "\n", + i + 1, + "failed:", + test_case.description, + "\n Value:", + test_case.a, + "\n Decimal places:", + test_case.b, + "\n Expected:", + test_case.expected, + "\n Got:", + String(result), + "\n Python decimal result (for reference):", + String( + pydecimal.Decimal(test_case.a).__round__(Int(test_case.b)) + ), + ) + failed += 1 + + print( + "BigDecimal ROUND_HALF_UP tests:", passed, "passed,", failed, "failed" + ) + testing.assert_equal(failed, 0, "All ROUND_HALF_UP tests should pass") + + +fn test_round_half_even() raises: + """Test BigDecimal rounding with ROUND_HALF_EVEN (banker's rounding) mode. + """ + print("------------------------------------------------------") + print("Testing BigDecimal ROUND_HALF_EVEN mode...") + + pydecimal = Python.import_module("decimal") + + # Load test cases from TOML file + var test_cases = load_test_cases_two_arguments( + rounding_file_path, "round_half_even_tests" + ) + print("Loaded", len(test_cases), "test cases for ROUND_HALF_EVEN") + + # 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 value = BigDecimal(test_case.a) + var decimal_places = Int(test_case.b) + var expected = BigDecimal(test_case.expected) + + # Perform rounding + var result = value.round(decimal_places, RoundingMode.ROUND_HALF_EVEN) + + try: + # Using String comparison for easier debugging + testing.assert_equal( + String(result), String(expected), test_case.description + ) + passed += 1 + except e: + print( + "=" * 50, + "\n", + i + 1, + "failed:", + test_case.description, + "\n Value:", + test_case.a, + "\n Decimal places:", + test_case.b, + "\n Expected:", + test_case.expected, + "\n Got:", + String(result), + "\n Python decimal result (for reference):", + String( + pydecimal.Decimal(test_case.a).__round__(Int(test_case.b)) + ), + ) + failed += 1 + + print( + "BigDecimal ROUND_HALF_EVEN tests:", passed, "passed,", failed, "failed" + ) + testing.assert_equal(failed, 0, "All ROUND_HALF_EVEN tests should pass") + + +fn test_extreme_values() raises: + """Test BigDecimal rounding with extreme values.""" + print("------------------------------------------------------") + print("Testing BigDecimal rounding with extreme values...") + + pydecimal = Python.import_module("decimal") + pydecimal.getcontext().prec = 100 + + # Load test cases from TOML file + var test_cases = load_test_cases_two_arguments( + rounding_file_path, "extreme_value_tests" + ) + print("Loaded", len(test_cases), "test cases for extreme values") + + # 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 value = BigDecimal(test_case.a) + var decimal_places = Int(test_case.b) + var expected = BigDecimal(test_case.expected) + + # Perform rounding with default ROUND_HALF_EVEN mode + var result = value.round(decimal_places, RoundingMode.ROUND_HALF_EVEN) + + try: + # Using String comparison for easier debugging + testing.assert_equal( + String(result), String(expected), test_case.description + ) + passed += 1 + except e: + print( + "=" * 50, + "\n", + i + 1, + "failed:", + test_case.description, + "\n Value:", + test_case.a, + "\n Decimal places:", + test_case.b, + "\n Expected:", + test_case.expected, + "\n Got:", + String(result), + "\n Python decimal result (for reference):", + String( + pydecimal.Decimal(test_case.a).__round__(decimal_places) + ), + ) + failed += 1 + + print( + "BigDecimal extreme value tests:", passed, "passed,", failed, "failed" + ) + testing.assert_equal(failed, 0, "All extreme value tests should pass") + + +fn test_edge_cases() raises: + """Test BigDecimal rounding with special edge cases.""" + print("------------------------------------------------------") + print("Testing BigDecimal rounding with special edge cases...") + + pydecimal = Python.import_module("decimal") + + # Load test cases from TOML file + var test_cases = load_test_cases_two_arguments( + rounding_file_path, "edge_case_tests" + ) + print("Loaded", len(test_cases), "test cases for edge cases") + + # 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 value = BigDecimal(test_case.a) + var decimal_places = Int(test_case.b) + + # Perform rounding with default ROUND_HALF_EVEN mode + var result = value.round(decimal_places, RoundingMode.ROUND_HALF_EVEN) + + try: + # Using String comparison for easier debugging + testing.assert_equal( + String(result), test_case.expected, test_case.description + ) + passed += 1 + except e: + print( + "=" * 50, + "\n", + i + 1, + "failed:", + test_case.description, + "\n Value:", + test_case.a, + "\n Decimal places:", + test_case.b, + "\n Expected:", + test_case.expected, + "\n Got:", + String(result), + "\n Python decimal result (for reference):", + String( + pydecimal.Decimal(test_case.a).__round__(Int(test_case.b)) + ), + ) + failed += 1 + + print("BigDecimal edge case tests:", passed, "passed,", failed, "failed") + testing.assert_equal(failed, 0, "All edge case tests should pass") + + +fn test_precision_conversions() raises: + """Test BigDecimal rounding with negative precision (rounding to tens, hundreds, etc.). + """ + print("------------------------------------------------------") + print("Testing BigDecimal rounding with precision conversions...") + + pydecimal = Python.import_module("decimal") + + # Load test cases from TOML file + var test_cases = load_test_cases_two_arguments( + rounding_file_path, "precision_tests" + ) + print("Loaded", len(test_cases), "test cases for precision conversions") + + # 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 value = BigDecimal(test_case.a) + var decimal_places = Int(test_case.b) + var expected = BigDecimal(test_case.expected) + + # Perform rounding with default ROUND_HALF_EVEN mode + var result = value.round(decimal_places, RoundingMode.ROUND_HALF_EVEN) + + try: + # Using String comparison for easier debugging + testing.assert_equal( + String(result), String(expected), test_case.description + ) + passed += 1 + except e: + print( + "=" * 50, + "\n", + i + 1, + "failed:", + test_case.description, + "\n Value:", + test_case.a, + "\n Decimal places:", + test_case.b, + "\n Expected:", + test_case.expected, + "\n Got:", + String(result), + "\n Python decimal result (for reference):", + String( + pydecimal.Decimal(test_case.a).__round__(Int(test_case.b)) + ), + ) + failed += 1 + + print( + "BigDecimal precision conversion tests:", + passed, + "passed,", + failed, + "failed", + ) + testing.assert_equal( + failed, 0, "All precision conversion tests should pass" + ) + + +fn test_scientific_notation() raises: + """Test BigDecimal rounding with scientific notation inputs.""" + print("------------------------------------------------------") + print("Testing BigDecimal rounding with scientific notation inputs...") + + pydecimal = Python.import_module("decimal") + + # Load test cases from TOML file + var test_cases = load_test_cases_two_arguments( + rounding_file_path, "scientific_tests" + ) + print("Loaded", len(test_cases), "test cases for scientific notation") + + # 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 value = BigDecimal(test_case.a) + var decimal_places = Int(test_case.b) + var expected = BigDecimal(test_case.expected) + + # Perform rounding with default ROUND_HALF_EVEN mode + var result = value.round(decimal_places, RoundingMode.ROUND_HALF_EVEN) + + try: + # Using String comparison for easier debugging + testing.assert_equal( + String(result), String(expected), test_case.description + ) + passed += 1 + except e: + print( + "=" * 50, + "\n", + i + 1, + "failed:", + test_case.description, + "\n Value:", + test_case.a, + "\n Decimal places:", + test_case.b, + "\n Expected:", + test_case.expected, + "\n Got:", + String(result), + "\n Python decimal result (for reference):", + String( + pydecimal.Decimal(test_case.a).__round__(Int(test_case.b)) + ), + ) + failed += 1 + + print( + "BigDecimal scientific notation tests:", + passed, + "passed,", + failed, + "failed", + ) + testing.assert_equal(failed, 0, "All scientific notation tests should pass") + + +fn test_default_rounding_mode() raises: + """Test that the default rounding mode is ROUND_HALF_EVEN.""" + print("------------------------------------------------------") + print("Testing BigDecimal default rounding mode...") + + var value = BigDecimal("2.5") + var result = value.round(0, RoundingMode.ROUND_HALF_EVEN) + var expected = BigDecimal("2") # HALF_EVEN rounds 2.5 to 2 (nearest even) + + testing.assert_equal( + String(result), + String(expected), + "Default rounding mode should be ROUND_HALF_EVEN", + ) + + value = BigDecimal("3.5") + result = round(value, 0) # No rounding mode specified + expected = BigDecimal("4") # HALF_EVEN rounds 3.5 to 4 (nearest even) + + testing.assert_equal( + String(result), + String(expected), + "Default rounding mode should be ROUND_HALF_EVEN", + ) + + print("✓ Default rounding mode tests passed") + + +fn main() raises: + print("Running BigDecimal rounding tests") + + # Test different rounding modes + test_round_down() + test_round_up() + test_round_half_up() + test_round_half_even() + + # Test special cases + test_extreme_values() + test_edge_cases() + test_precision_conversions() + test_scientific_notation() + + # Test default rounding mode + test_default_rounding_mode() + + print("All BigDecimal rounding tests passed!") diff --git a/tests/bigdecimal/test_data/bigdecimal_rounding.toml b/tests/bigdecimal/test_data/bigdecimal_rounding.toml new file mode 100644 index 0000000..3656a50 --- /dev/null +++ b/tests/bigdecimal/test_data/bigdecimal_rounding.toml @@ -0,0 +1,349 @@ +# === ROUND DOWN (TRUNCATE) TESTS === +[[round_down_tests]] +a = "12.345" +b = "0" +expected = "12" +description = "Round down to integer" + +[[round_down_tests]] +a = "12.345" +b = "1" +expected = "12.3" +description = "Round down to 1 decimal place" + +[[round_down_tests]] +a = "12.345" +b = "2" +expected = "12.34" +description = "Round down to 2 decimal places" + +[[round_down_tests]] +a = "12.345" +b = "3" +expected = "12.345" +description = "Round to same precision" + +[[round_down_tests]] +a = "12.345" +b = "4" +expected = "12.3450" +description = "Round to higher precision" + +[[round_down_tests]] +a = "-12.345" +b = "0" +expected = "-12" +description = "Round down negative to integer" + +[[round_down_tests]] +a = "-12.345" +b = "1" +expected = "-12.3" +description = "Round down negative to 1 decimal place" + +[[round_down_tests]] +a = "-12.345" +b = "2" +expected = "-12.34" +description = "Round down negative to 2 decimal places" + +[[round_down_tests]] +a = "0.9999" +b = "0" +expected = "0" +description = "Round down near-1 value to integer" + +[[round_down_tests]] +a = "9.9999" +b = "0" +expected = "9" +description = "Round down near-10 value to integer" + +# === ROUND UP TESTS === +[[round_up_tests]] +a = "12.345" +b = "0" +expected = "13" +description = "Round up to integer" + +[[round_up_tests]] +a = "12.345" +b = "1" +expected = "12.4" +description = "Round up to 1 decimal place" + +[[round_up_tests]] +a = "12.345" +b = "2" +expected = "12.35" +description = "Round up to 2 decimal places" + +[[round_up_tests]] +a = "12.345" +b = "3" +expected = "12.345" +description = "Round to same precision" + +[[round_up_tests]] +a = "-12.345" +b = "0" +expected = "-13" +description = "Round up negative to integer" + +[[round_up_tests]] +a = "-12.345" +b = "1" +expected = "-12.4" +description = "Round up negative to 1 decimal place" + +[[round_up_tests]] +a = "0.001" +b = "0" +expected = "0" +description = "Round up small positive value to integer" + +[[round_up_tests]] +a = "-0.001" +b = "0" +expected = "-0" +description = "Round up small negative value to integer" + +# === ROUND HALF UP TESTS === +[[round_half_up_tests]] +a = "12.5" +b = "0" +expected = "13" +description = "Round half up to integer" + +[[round_half_up_tests]] +a = "12.4" +b = "0" +expected = "12" +description = "Round half up where less than .5" + +[[round_half_up_tests]] +a = "12.25" +b = "1" +expected = "12.3" +description = "Round half up to 1 decimal place" + +[[round_half_up_tests]] +a = "12.35" +b = "1" +expected = "12.4" +description = "Round half up exactly .5 to 1 decimal place" + +[[round_half_up_tests]] +a = "-12.5" +b = "0" +expected = "-13" +description = "Round half up negative to integer" + +[[round_half_up_tests]] +a = "-12.45" +b = "1" +expected = "-12.5" +description = "Round half up negative to 1 decimal place" + +[[round_half_up_tests]] +a = "0.5" +b = "0" +expected = "1" +description = "Round half up exactly 0.5 to integer" + +[[round_half_up_tests]] +a = "-0.5" +b = "0" +expected = "-1" +description = "Round half up exactly -0.5 to integer" + +# === ROUND HALF EVEN TESTS (BANKER'S ROUNDING) === +[[round_half_even_tests]] +a = "12.5" +b = "0" +expected = "12" +description = "Round half even to integer (toward even digit)" + +[[round_half_even_tests]] +a = "13.5" +b = "0" +expected = "14" +description = "Round half even to integer (toward even digit)" + +[[round_half_even_tests]] +a = "12.25" +b = "1" +expected = "12.2" +description = "Round half even to 1 decimal place (toward even digit)" + +[[round_half_even_tests]] +a = "12.35" +b = "1" +expected = "12.4" +description = "Round half even to 1 decimal place (toward even digit)" + +[[round_half_even_tests]] +a = "12.65" +b = "1" +expected = "12.6" +description = "Round half even to 1 decimal place (toward even digit)" + +[[round_half_even_tests]] +a = "12.75" +b = "1" +expected = "12.8" +description = "Round half even to 1 decimal place (toward even digit)" + +[[round_half_even_tests]] +a = "-12.5" +b = "0" +expected = "-12" +description = "Round half even negative to integer (toward even digit)" + +[[round_half_even_tests]] +a = "-13.5" +b = "0" +expected = "-14" +description = "Round half even negative to integer (toward even digit)" + +[[round_half_even_tests]] +a = "0.5" +b = "0" +expected = "0" +description = "Round half even exactly 0.5 to integer (0 is even)" + +[[round_half_even_tests]] +a = "-0.5" +b = "0" +expected = "-0" +description = "Round half even exactly -0.5 to integer (0 is even)" + +# === EXTREME VALUES TESTS === +[[extreme_value_tests]] +a = "0.00000000000000000000000001" +b = "10" +expected = "0.0000000000" +description = "Rounding very small number" + +[[extreme_value_tests]] +a = "9999999999999999999999999999.99999" +b = "2" +expected = "1.000000000000000000000000000000E+28" +description = "Rounding very large number" + +[[extreme_value_tests]] +a = "0.5555555555555555555555555" +b = "10" +expected = "0.5555555556" +description = "Rounding repeated digits" + +[[extreme_value_tests]] +a = "1.000000000000000000000000001" +b = "10" +expected = "1.0000000000" +description = "Rounding number very close to whole" + +[[extreme_value_tests]] +a = "-0.000000000000000000000000001" +b = "10" +expected = "-0E-10" +description = "Rounding very small negative number (becomes zero)" + +# === SPECIAL EDGE CASES === +[[edge_case_tests]] +a = "9.9999999999999999999999999" +b = "2" +expected = "10.00" +description = "Rounding causes carrying over to next digit" + +[[edge_case_tests]] +a = "999.9999999999999999999999999" +b = "0" +expected = "1000" +description = "Rounding causes carrying over to next 10s place" + +[[edge_case_tests]] +a = "0.00000000000000000000000005" +b = "28" +expected = "5.00E-26" +description = "Rounding at maximum precision boundary" + +[[edge_case_tests]] +a = "0" +b = "10" +expected = "0.0000000000" +description = "Rounding zero" + +[[edge_case_tests]] +a = "-0" +b = "10" +expected = "-0.0000000000" +description = "Rounding negative zero" + +[[edge_case_tests]] +a = "0.499999999999999999999999999" +b = "0" +expected = "0" +description = "Rounding just under half" + +[[edge_case_tests]] +a = "0.500000000000000000000000001" +b = "0" +expected = "1" +description = "Rounding just over half" + +# === PRECISION CONVERSIONS === +[[precision_tests]] +a = "123.456" +b = "-2" +expected = "1E+2" +description = "Rounding to negative precision (hundreds)" + +[[precision_tests]] +a = "1234.56" +b = "-1" +expected = "1.23E+3" +description = "Rounding to negative precision (tens)" + +[[precision_tests]] +a = "1234.56" +b = "-3" +expected = "1E+3" +description = "Rounding to negative precision (thousands)" + +[[precision_tests]] +a = "9999.99" +b = "-3" +expected = "1.0E+4" +description = "Rounding to negative precision with carry" + +[[precision_tests]] +a = "0.000123456" +b = "5" +expected = "0.00012" +description = "Rounding where leading zeros matter" + +# === SCIENTIFIC NOTATION INPUTS === +[[scientific_tests]] +a = "1.2345e5" +b = "2" +expected = "123450.00" +description = "Rounding scientific notation value" + +[[scientific_tests]] +a = "1.2345e-5" +b = "8" +expected = "0.00001234" +description = "Rounding small scientific notation value" + +[[scientific_tests]] +a = "9.9999e20" +b = "0" +expected = "999990000000000000000" +description = "Rounding large scientific notation value" + +[[scientific_tests]] +a = "-1.2345e-10" +b = "12" +expected = "-0.000000000123" +description = "Rounding negative scientific notation value"