diff --git a/benches/bench.mojo b/benches/bench.mojo index d7a6c23..fe2a245 100644 --- a/benches/bench.mojo +++ b/benches/bench.mojo @@ -10,6 +10,7 @@ from bench_round import main as bench_round from bench_comparison import main as bench_comparison from bench_exp import main as bench_exp from bench_ln import main as bench_ln +from bench_log10 import main as bench_log10 from bench_power import main as bench_power from bench_root import main as bench_root @@ -27,5 +28,6 @@ fn main() raises: bench_comparison() bench_exp() bench_ln() + bench_log10() bench_power() bench_root() diff --git a/benches/bench_log10.mojo b/benches/bench_log10.mojo new file mode 100644 index 0000000..8702eb4 --- /dev/null +++ b/benches/bench_log10.mojo @@ -0,0 +1,337 @@ +""" +Comprehensive benchmarks for Decimal log10() function. +Compares performance against Python's decimal module with diverse test cases. +""" + +from decimojo.prelude import dm, Decimal, 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_log10_" + 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_log10( + name: String, + input_value: String, + iterations: Int, + log_file: PythonObject, + mut speedup_factors: List[Float64], +) raises: + """ + Run a benchmark comparing Mojo Decimal.log10 with Python Decimal.log10. + + Args: + name: Name of the benchmark case. + input_value: String representation of the value to compute log10 of. + 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("Input value: " + input_value, log_file) + + # Set up Mojo and Python values + var mojo_decimal = Decimal(input_value) + var pydecimal = Python.import_module("decimal") + var py_decimal = pydecimal.Decimal(input_value) + + # Execute the operations once to verify correctness + var mojo_result = mojo_decimal.log10() + var py_result = py_decimal.log10() + + # 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_decimal.log10() + 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_decimal.log10() + 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 log10(): " + String(mojo_time) + " ns per iteration", + log_file, + ) + log_print( + "Python log10(): " + String(python_time) + " ns per iteration", + log_file, + ) + log_print("Speedup factor: " + String(speedup), 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 log10() 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 = 100 + 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: " + String(Decimal.MAX_SCALE), log_file) + + # Define benchmark cases + log_print( + "\nRunning log10() function benchmarks with " + + String(iterations) + + " iterations each", + log_file, + ) + + # Case 1: Exact power of 10 (10^0) + run_benchmark_log10( + "Power of 10 (10^0)", "1", iterations, log_file, speedup_factors + ) + + # Case 2: Exact power of 10 (10^1) + run_benchmark_log10( + "Power of 10 (10^1)", "10", iterations, log_file, speedup_factors + ) + + # Case 3: Exact power of 10 (10^2) + run_benchmark_log10( + "Power of 10 (10^2)", "100", iterations, log_file, speedup_factors + ) + + # Case 4: Exact power of 10 (10^-1) + run_benchmark_log10( + "Power of 10 (10^-1)", "0.1", iterations, log_file, speedup_factors + ) + + # Case 5: Exact power of 10 (10^-2) + run_benchmark_log10( + "Power of 10 (10^-2)", "0.01", iterations, log_file, speedup_factors + ) + + # Case 6: Number between powers of 10 (middle) + run_benchmark_log10( + "Between powers - middle", "5", iterations, log_file, speedup_factors + ) + + # Case 7: Number between powers of 10 (closer to lower) + run_benchmark_log10( + "Between powers - closer to lower", + "2", + iterations, + log_file, + speedup_factors, + ) + + # Case 8: Number between powers of 10 (closer to upper) + run_benchmark_log10( + "Between powers - closer to upper", + "9", + iterations, + log_file, + speedup_factors, + ) + + # Case 9: Decimal number with many digits + run_benchmark_log10( + "Decimal with many digits", + "3.1415926535897932384626433832795", + iterations, + log_file, + speedup_factors, + ) + + # Case 10: Small number (close to 0) + run_benchmark_log10( + "Small number close to 0", + "0.0000001", + iterations, + log_file, + speedup_factors, + ) + + # Case 11: Large number + run_benchmark_log10( + "Large number", "1000000000000", iterations, log_file, speedup_factors + ) + + # Case 12: Number close to 1 (slightly above) + run_benchmark_log10( + "Close to 1 (above)", + "1.00000001", + iterations, + log_file, + speedup_factors, + ) + + # Case 13: Number close to 1 (slightly below) + run_benchmark_log10( + "Close to 1 (below)", + "0.99999999", + iterations, + log_file, + speedup_factors, + ) + + # Case 14: Non-integer power of 10 (10^1.5) + run_benchmark_log10( + "Non-integer power of 10 (10^1.5)", + "31.62277660168", + iterations, + log_file, + speedup_factors, + ) + + # Case 15: Repeating pattern number + run_benchmark_log10( + "Repeating pattern", + "1.234567890123456789", + iterations, + log_file, + speedup_factors, + ) + + # Case 16: Very precise input requiring full precision + run_benchmark_log10( + "Very precise input", + "2.718281828459045235360287471352662", + iterations, + log_file, + speedup_factors, + ) + + # Case 17: Integer with many digits + run_benchmark_log10( + "Integer with many digits", + "123456789012345678901234", + iterations, + log_file, + speedup_factors, + ) + + # Case 18: The square root of 10 + run_benchmark_log10( + "Square root of 10", + "3.16227766017", + iterations, + log_file, + speedup_factors, + ) + + # Case 19: Scientific calculation value + run_benchmark_log10( + "Scientific calculation value", + "299792458", + iterations, + log_file, + speedup_factors, + ) + + # Case 20: Random decimal value + run_benchmark_log10( + "Random decimal value", + "4.2857142857", + iterations, + log_file, + speedup_factors, + ) + + # Calculate average speedup factor + 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=== log10() Function Benchmark Summary ===", log_file) + log_print("Benchmarked: 20 different log10() 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, + ) + + # Close the log file + log_file.close() + print("Benchmark completed. Log file closed.") diff --git a/mojoproject.toml b/mojoproject.toml index 03b1ce4..1c4c2be 100644 --- a/mojoproject.toml +++ b/mojoproject.toml @@ -43,6 +43,8 @@ test_comparison = "magic run package && magic run mojo test tests/test_compariso test_factorial = "magic run package && magic run mojo test tests/test_factorial.mojo && magic run delete_package" test_exp = "magic run package && magic run mojo test tests/test_exp.mojo && magic run delete_package" test_ln = "magic run package && magic run mojo test tests/test_ln.mojo && magic run delete_package" +test_log = "magic run package && magic run mojo test tests/test_log.mojo && magic run delete_package" +test_log10 = "magic run package && magic run mojo test tests/test_log10.mojo && magic run delete_package" test_power = "magic run package && magic run mojo test tests/test_power.mojo && magic run delete_package" # benches @@ -59,6 +61,7 @@ bench_from_int = "magic run package && cd benches && magic run mojo bench_from_i bench_comparison = "magic run package && cd benches && magic run mojo bench_comparison.mojo && cd .. && magic run delete_package" bench_exp = "magic run package && cd benches && magic run mojo bench_exp.mojo && cd .. && magic run delete_package" bench_ln = "magic run package && cd benches && magic run mojo bench_ln.mojo && cd .. && magic run delete_package" +bench_log10 = "magic run package && cd benches && magic run mojo bench_log10.mojo && cd .. && magic run delete_package" bench_power = "magic run package && cd benches && magic run mojo bench_power.mojo && cd .. && magic run delete_package" # before commit diff --git a/src/decimojo/__init__.mojo b/src/decimojo/__init__.mojo index a1eedca..3318eb6 100644 --- a/src/decimojo/__init__.mojo +++ b/src/decimojo/__init__.mojo @@ -49,7 +49,7 @@ from .comparison import ( not_equal, ) -from .exponential import power, root, sqrt, exp, ln +from .exponential import power, root, sqrt, exp, ln, log, log10 from .rounding import round diff --git a/src/decimojo/decimal.mojo b/src/decimojo/decimal.mojo index 85838d4..4208cab 100644 --- a/src/decimojo/decimal.mojo +++ b/src/decimojo/decimal.mojo @@ -132,6 +132,7 @@ struct Decimal( # when Mojo support global variables in the future. # Special values + @always_inline @staticmethod fn INFINITY() -> Decimal: """Returns a Decimal representing positive infinity. @@ -139,6 +140,7 @@ struct Decimal( """ return Decimal(0, 0, 0, 0x00000001) + @always_inline @staticmethod fn NEGATIVE_INFINITY() -> Decimal: """Returns a Decimal representing negative infinity. @@ -146,6 +148,7 @@ struct Decimal( """ return Decimal(0, 0, 0, 0x80000001) + @always_inline @staticmethod fn NAN() -> Decimal: """Returns a Decimal representing Not a Number (NaN). @@ -153,6 +156,7 @@ struct Decimal( """ return Decimal(0, 0, 0, 0x00000010) + @always_inline @staticmethod fn NEGATIVE_NAN() -> Decimal: """Returns a Decimal representing negative Not a Number. @@ -160,16 +164,19 @@ struct Decimal( """ return Decimal(0, 0, 0, 0x80000010) + @always_inline @staticmethod fn ZERO() -> Decimal: """Returns a Decimal representing 0.""" return Decimal(0, 0, 0, 0) + @always_inline @staticmethod fn ONE() -> Decimal: """Returns a Decimal representing 1.""" return Decimal(1, 0, 0, 0) + @always_inline @staticmethod fn MAX() -> Decimal: """ @@ -178,6 +185,7 @@ struct Decimal( """ return Decimal(0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0) + @always_inline @staticmethod fn MIN() -> Decimal: """Returns the minimum possible Decimal value (negative of MAX). @@ -185,11 +193,13 @@ struct Decimal( """ return Decimal(0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, Decimal.SIGN_MASK) + @always_inline @staticmethod fn PI() -> Decimal: """Returns the value of pi (π) as a Decimal.""" return decimojo.constants.PI() + @always_inline @staticmethod fn E() -> Decimal: """Returns the value of Euler's number (e) as a Decimal.""" @@ -866,10 +876,12 @@ struct Decimal( # Return both the significant digits and the scale return Decimal(low, mid, high, scale, is_negative) + @always_inline fn copy(self) -> Self: """Returns a copy of the Decimal.""" return Decimal(self.low, self.mid, self.high, self.flags) + @always_inline fn clone(self) -> Self: """Returns a copy of the Decimal.""" return Decimal(self.low, self.mid, self.high, self.flags) @@ -1121,22 +1133,18 @@ struct Decimal( # neg # ===------------------------------------------------------------------=== # + @always_inline fn __abs__(self) -> Self: """Returns the absolute value of this Decimal. - - Returns: - The absolute value of this Decimal. + See `absolute()` for more information. """ - return decimojo.arithmetics.absolute(self) + @always_inline fn __neg__(self) -> Self: """Returns the negation of this Decimal. - - Returns: - The negation of this Decimal. + See `negative()` for more information. """ - return decimojo.arithmetics.negative(self) # ===------------------------------------------------------------------=== # @@ -1144,65 +1152,46 @@ struct Decimal( # These methods are called to implement the binary arithmetic operations # (+, -, *, @, /, //, %, divmod(), pow(), **, <<, >>, &, ^, |) # ===------------------------------------------------------------------=== # + + @always_inline fn __add__(self, other: Self) raises -> Self: - try: - return decimojo.arithmetics.add(self, other) - except e: - raise Error("Error in `__add__()`: ", e) + return decimojo.arithmetics.add(self, other) + @always_inline fn __add__(self, other: Int) raises -> Self: - try: - return decimojo.arithmetics.add(self, Decimal(other)) - except e: - raise Error("Error in `__add__()`: ", e) + return decimojo.arithmetics.add(self, Decimal(other)) + @always_inline fn __sub__(self, other: Self) raises -> Self: - try: - return decimojo.arithmetics.subtract(self, other) - except e: - raise Error("Error in `__sub__()`: ", e) + return decimojo.arithmetics.subtract(self, other) + @always_inline fn __sub__(self, other: Int) raises -> Self: - try: - return decimojo.arithmetics.subtract(self, Decimal(other)) - except e: - raise Error("Error in `__sub__()`: ", e) + return decimojo.arithmetics.subtract(self, Decimal(other)) + @always_inline fn __mul__(self, other: Self) raises -> Self: - try: - return decimojo.arithmetics.multiply(self, other) - except e: - raise Error("Error in `__mul__()`: ", e) + return decimojo.arithmetics.multiply(self, other) + @always_inline fn __mul__(self, other: Int) raises -> Self: - try: - return decimojo.arithmetics.multiply(self, Decimal(other)) - except e: - raise Error("Error in `__mul__()`: ", e) + return decimojo.arithmetics.multiply(self, Decimal(other)) + @always_inline fn __truediv__(self, other: Self) raises -> Self: - try: - return decimojo.arithmetics.true_divide(self, other) - except e: - raise Error("Error in `__truediv__()`: ", e) + return decimojo.arithmetics.true_divide(self, other) + @always_inline fn __truediv__(self, other: Int) raises -> Self: - try: - return decimojo.arithmetics.true_divide(self, Decimal(other)) - except e: - raise Error("Error in `__truediv__()`: ", e) + return decimojo.arithmetics.true_divide(self, Decimal(other)) + @always_inline fn __pow__(self, exponent: Self) raises -> Self: - try: - return decimal.power(self, exponent) - except e: - raise Error("Error in `__pow__()`: ", e) + return decimal.power(self, exponent) + @always_inline fn __pow__(self, exponent: Int) raises -> Self: - try: - return decimal.power(self, exponent) - except e: - raise Error("Error in `__pow__()`: ", e) + return decimal.power(self, exponent) # ===------------------------------------------------------------------=== # # Basic binary arithmetic operation dunders with reflected operands @@ -1211,24 +1200,28 @@ struct Decimal( # (+, -, *, @, /, //, %, divmod(), pow(), **, <<, >>, &, ^, |) # ===------------------------------------------------------------------=== # + @always_inline fn __radd__(self, other: Int) raises -> Self: try: return decimojo.arithmetics.add(Decimal(other), self) except e: raise Error("Error in `__radd__()`: ", e) + @always_inline fn __rsub__(self, other: Int) raises -> Self: try: return decimojo.arithmetics.subtract(Decimal(other), self) except e: raise Error("Error in `__rsub__()`: ", e) + @always_inline fn __rmul__(self, other: Int) raises -> Self: try: return decimojo.arithmetics.multiply(Decimal(other), self) except e: raise Error("Error in `__rmul__()`: ", e) + @always_inline fn __rtruediv__(self, other: Int) raises -> Self: try: return decimojo.arithmetics.true_divide(Decimal(other), self) @@ -1242,122 +1235,82 @@ struct Decimal( # (+=, -=, *=, @=, /=, //=, %=, **=, <<=, >>=, &=, ^=, |=) # ===------------------------------------------------------------------=== # + @always_inline fn __iadd__(mut self, other: Self) raises: - try: - self = decimojo.arithmetics.add(self, other) - except e: - raise Error("Error in `__iadd__()`: ", e) + self = decimojo.arithmetics.add(self, other) + @always_inline fn __iadd__(mut self, other: Int) raises: - try: - self = decimojo.arithmetics.add(self, Decimal(other)) - except e: - raise Error("Error in `__iadd__()`: ", e) + self = decimojo.arithmetics.add(self, Decimal(other)) + @always_inline fn __isub__(mut self, other: Self) raises: - try: - self = decimojo.arithmetics.subtract(self, other) - except e: - raise Error("Error in `__isub__()`: ", e) + self = decimojo.arithmetics.subtract(self, other) + @always_inline fn __isub__(mut self, other: Int) raises: - try: - self = decimojo.arithmetics.subtract(self, Decimal(other)) - except e: - raise Error("Error in `__isub__()`: ", e) + self = decimojo.arithmetics.subtract(self, Decimal(other)) + @always_inline fn __imul__(mut self, other: Self) raises: - try: - self = decimojo.arithmetics.multiply(self, other) - except e: - raise Error("Error in `__imul__()`: ", e) + self = decimojo.arithmetics.multiply(self, other) + @always_inline fn __imul__(mut self, other: Int) raises: - try: - self = decimojo.arithmetics.multiply(self, Decimal(other)) - except e: - raise Error("Error in `__imul__()`: ", e) + self = decimojo.arithmetics.multiply(self, Decimal(other)) + @always_inline fn __itruediv__(mut self, other: Self) raises: - try: - self = decimojo.arithmetics.true_divide(self, other) - except e: - raise Error("Error in `__itruediv__()`: ", e) + self = decimojo.arithmetics.true_divide(self, other) + @always_inline fn __itruediv__(mut self, other: Int) raises: - try: - self = decimojo.arithmetics.true_divide(self, Decimal(other)) - except e: - raise Error("Error in `__itruediv__()`: ", e) + self = decimojo.arithmetics.true_divide(self, Decimal(other)) # ===------------------------------------------------------------------=== # # Basic binary comparison operation dunders # __gt__, __ge__, __lt__, __le__, __eq__, __ne__ # ===------------------------------------------------------------------=== # + @always_inline fn __gt__(self, other: Decimal) -> Bool: """Greater than comparison operator. - - Args: - other: The Decimal to compare with. - - Returns: - True if self is greater than other, False otherwise. + See `greater()` for more information. """ return decimojo.comparison.greater(self, other) + @always_inline fn __lt__(self, other: Decimal) -> Bool: """Less than comparison operator. - - Args: - other: The Decimal to compare with. - - Returns: - True if self is less than other, False otherwise. + See `less()` for more information. """ return decimojo.comparison.less(self, other) + @always_inline fn __ge__(self, other: Decimal) -> Bool: """Greater than or equal comparison operator. - - Args: - other: The Decimal to compare with. - - Returns: - True if self is greater than or equal to other, False otherwise. + See `greater_equal()` for more information. """ return decimojo.comparison.greater_equal(self, other) + @always_inline fn __le__(self, other: Decimal) -> Bool: """Less than or equal comparison operator. - - Args: - other: The Decimal to compare with. - - Returns: - True if self is less than or equal to other, False otherwise. + See `less_equal()` for more information. """ return decimojo.comparison.less_equal(self, other) + @always_inline fn __eq__(self, other: Decimal) -> Bool: """Equality comparison operator. - - Args: - other: The Decimal to compare with. - - Returns: - True if self is equal to other, False otherwise. + See `equal()` for more information. """ return decimojo.comparison.equal(self, other) + @always_inline fn __ne__(self, other: Decimal) -> Bool: """Inequality comparison operator. - - Args: - other: The Decimal to compare with. - - Returns: - True if self is not equal to other, False otherwise. + See `not_equal()` for more information. """ return decimojo.comparison.not_equal(self, other) @@ -1366,6 +1319,7 @@ struct Decimal( # round # ===------------------------------------------------------------------=== # + @always_inline fn __round__(self, ndigits: Int) -> Self: """Rounds this Decimal to the specified number of decimal places. If `ndigits` is not given, rounds to 0 decimal places. @@ -1374,7 +1328,6 @@ struct Decimal( raises: Error: Calling `round()` failed. """ - try: return decimojo.rounding.round( self, ndigits=ndigits, rounding_mode=RoundingMode.HALF_EVEN() @@ -1382,35 +1335,22 @@ struct Decimal( except e: return self + @always_inline fn __round__(self) -> Self: """**OVERLOAD**.""" - - return self.__round__(ndigits=0) + try: + return decimojo.rounding.round( + self, ndigits=0, rounding_mode=RoundingMode.HALF_EVEN() + ) + except e: + return self # ===------------------------------------------------------------------=== # # Mathematical methods that do not implement a trait (not a dunder) # exp, ln, round, sqrt # ===------------------------------------------------------------------=== # - fn exp(self) raises -> Self: - """Calculates the exponential of this Decimal. - See `exp()` for more information. - """ - - try: - return decimojo.exponential.exp(self) - except e: - raise Error("Error in `Decimal.exp()`: ", e) - - fn ln(self) raises -> Self: - """Calculates the natural logarithm of this Decimal. - See `ln()` for more information. - """ - try: - return decimojo.exponential.ln(self) - except e: - raise Error("Error in `Decimal.ln()`: ", e) - + @always_inline fn round( self, ndigits: Int = 0, @@ -1433,36 +1373,63 @@ struct Decimal( Raises: Error: If calling `round()` failed. """ + return decimojo.rounding.round( + self, ndigits=ndigits, rounding_mode=rounding_mode + ) - try: - return decimojo.rounding.round( - self, ndigits=ndigits, rounding_mode=rounding_mode - ) - except e: - raise Error("Error in `Decimal.round()`: ", e) + @always_inline + fn exp(self) raises -> Self: + """Calculates the exponential of this Decimal. + See `exp()` for more information. + """ + return decimojo.exponential.exp(self) + + @always_inline + fn ln(self) raises -> Self: + """Calculates the natural logarithm of this Decimal. + See `ln()` for more information. + """ + return decimojo.exponential.ln(self) + + @always_inline + fn log10(self) raises -> Decimal: + """Computes the base-10 logarithm of this Decimal.""" + return decimojo.exponential.log10(self) + + @always_inline + fn log(self, base: Decimal) raises -> Decimal: + """Computes the logarithm of this Decimal with an arbitrary base.""" + return decimojo.exponential.log(self, base) + + @always_inline + fn power(self, exponent: Int) raises -> Decimal: + """Raises this Decimal to the power of an integer.""" + return decimojo.exponential.power(self, Decimal(exponent)) + @always_inline + fn power(self, exponent: Decimal) raises -> Decimal: + """Raises this Decimal to the power of another Decimal.""" + return decimojo.exponential.power(self, exponent) + + @always_inline fn root(self, n: Int) raises -> Self: """Calculates the n-th root of this Decimal. See `root()` for more information. """ - try: - return decimojo.exponential.root(self, n) - except e: - raise Error("Error in `Decimal.root()`: ", e) + return decimojo.exponential.root(self, n) + @always_inline fn sqrt(self) raises -> Self: """Calculates the square root of this Decimal. See `sqrt()` for more information. """ - try: - return decimojo.exponential.sqrt(self) - except e: - raise Error("Error in `Decimal.sqrt()`: ", e) + return decimojo.exponential.sqrt(self) # ===------------------------------------------------------------------=== # # Other methods # ===------------------------------------------------------------------=== # + @always_inline fn coefficient(self) -> UInt128: """Returns the unscaled integer coefficient as an UInt128 value. This is the absolute value of the decimal digits without considering @@ -1486,6 +1453,7 @@ struct Decimal( # | UInt128(self.low) # ) + @always_inline fn is_integer(self) -> Bool: """Determines whether this Decimal value represents an integer. A Decimal represents an integer when it has no fractional part @@ -1513,10 +1481,12 @@ struct Decimal( % decimojo.utility.power_of_10[DType.uint128](scale) ) == 0 + @always_inline fn is_negative(self) -> Bool: """Returns True if this Decimal is negative.""" return (self.flags & Self.SIGN_MASK) != 0 + @always_inline fn is_one(self) -> Bool: """Returns True if this Decimal represents the value 1. If 10^scale == coefficient, then it's one. @@ -1536,6 +1506,7 @@ struct Decimal( return False + @always_inline fn is_zero(self) -> Bool: """Returns True if this Decimal represents zero. A decimal is zero when all coefficient parts (low, mid, high) are zero, @@ -1543,18 +1514,22 @@ struct Decimal( """ return self.low == 0 and self.mid == 0 and self.high == 0 + @always_inline fn is_infinity(self) -> Bool: """Returns True if this Decimal is positive or negative infinity.""" return (self.flags & Self.INFINITY_MASK) != 0 + @always_inline fn is_nan(self) -> Bool: """Returns True if this Decimal is NaN (Not a Number).""" return (self.flags & Self.NAN_MASK) != 0 + @always_inline fn scale(self) -> Int: """Returns the scale (number of decimal places) of this Decimal.""" return Int((self.flags & Self.SCALE_MASK) >> Self.SCALE_SHIFT) + @always_inline fn scientific_exponent(self) -> Int: """ Calculates the exponent for scientific notation representation of a Decimal. @@ -1566,6 +1541,7 @@ struct Decimal( return self.number_of_significant_digits() - 1 - self.scale() + @always_inline fn number_of_significant_digits(self) -> Int: """Returns the number of significant digits in the Decimal. The number of significant digits is the total number of digits in the @@ -1583,7 +1559,6 @@ struct Decimal( print(Decimal("0.0001234500").number_of_significant_digits()) # 7 ``` - End of example. """ @@ -1592,7 +1567,6 @@ struct Decimal( # Special case for zero if coef == 0: return 0 # Zero has zero significant digit - else: return decimojo.utility.number_of_digits(coef) diff --git a/src/decimojo/exponential.mojo b/src/decimojo/exponential.mojo index f961212..5a04e7d 100644 --- a/src/decimojo/exponential.mojo +++ b/src/decimojo/exponential.mojo @@ -16,18 +16,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # ===----------------------------------------------------------------------=== # -# -# Implements exponential functions for the Decimal type -# -# ===----------------------------------------------------------------------=== # -# -# List of functions in this module: -# -# power(base: Decimal, exponent: Decimal): Raises base to the power of exponent (integer exponents only) -# power(base: Decimal, exponent: Int): Convenience method for integer exponents -# sqrt(x: Decimal): Computes the square root of x using Newton-Raphson method -# -# ===----------------------------------------------------------------------=== # + +"""Implements exponential functions for the Decimal type.""" import math as builtin_math import testing @@ -37,6 +27,10 @@ import decimojo.constants import decimojo.special import decimojo.utility +# ===----------------------------------------------------------------------=== # +# Power and root functions +# ===----------------------------------------------------------------------=== # + fn power(base: Decimal, exponent: Decimal) raises -> Decimal: """Raises a Decimal base to an arbitrary Decimal exponent power. @@ -439,6 +433,11 @@ fn sqrt(x: Decimal) raises -> Decimal: return guess +# ===----------------------------------------------------------------------=== # +# Exponential functions +# ===----------------------------------------------------------------------=== # + + fn exp(x: Decimal) raises -> Decimal: """Calculates e^x for any Decimal value using optimized range reduction. x should be no greater than 66 to avoid overflow. @@ -633,6 +632,11 @@ fn exp_series(x: Decimal) raises -> Decimal: return result +# ===----------------------------------------------------------------------=== # +# Logarithmic functions +# ===----------------------------------------------------------------------=== # + + fn ln(x: Decimal) raises -> Decimal: """Calculates the natural logarithm (ln) of a Decimal value. @@ -894,3 +898,116 @@ fn ln_series(z: Decimal) raises -> Decimal: # print("DEBUG: result =", result) return result + + +fn log(x: Decimal, base: Decimal) raises -> Decimal: + """Calculates the logarithm of a Decimal with respect to an arbitrary base. + + Args: + x: The Decimal value to compute the logarithm of. + base: The base of the logarithm (must be positive and not equal to 1). + + Returns: + A Decimal approximation of log_base(x). + + Raises: + Error: If x is less than or equal to zero. + Error: If base is less than or equal to zero or equal to 1. + + Notes: + + This implementation uses the identity log_base(x) = ln(x) / ln(base). + """ + # Special cases: x <= 0 + if x.is_negative() or x.is_zero(): + raise Error( + "Error in log(): Cannot compute logarithm of a non-positive number" + ) + + # Special cases: base <= 0 + if base.is_negative() or base.is_zero(): + raise Error( + "Error in log(): Cannot use non-positive base for logarithm" + ) + + # Special case: base = 1 + if base.is_one(): + raise Error("Error in log(): Cannot use base 1 for logarithm") + + # Special case: x = 1 + # log_base(1) = 0 for any valid base + if x.is_one(): + return Decimal.ZERO() + + # Special case: x = base + # log_base(base) = 1 for any valid base + if x == base: + return Decimal.ONE() + + # Special case: base = 10 + if base == Decimal(10, 0, 0, 0): + return log10(x) + + # Use the identity: log_base(x) = ln(x) / ln(base) + var ln_x = ln(x) + var ln_base = ln(base) + + return ln_x / ln_base + + +fn log10(x: Decimal) raises -> Decimal: + """Calculates the base-10 logarithm (log10) of a Decimal value. + + Args: + x: The Decimal value to compute the base-10 logarithm of. + + Returns: + A Decimal approximation of log10(x). + + Raises: + Error: If x is less than or equal to zero. + + Notes: + This implementation uses the identity log10(x) = ln(x) / ln(10). + """ + # Special cases: x <= 0 + if x.is_negative() or x.is_zero(): + raise Error( + "Error in log10(): Cannot compute logarithm of a non-positive" + " number" + ) + + var x_scale = x.scale() + var x_coef = x.coefficient() + + # Sepcial case: x = 10^(-n) + if x_coef == 1: + # Special case: x = 1 + if x_scale == 0: + return Decimal.ZERO() + else: + return Decimal(x_scale, 0, 0, 0x8000_0000) + + var ten_to_power_of_scale = decimojo.utility.power_of_10[DType.uint128]( + x_scale + ) + + # Special case: x = 1.00...0 + if x_coef == ten_to_power_of_scale: + return Decimal.ZERO() + + # Special case: x = 10^n + # First get the integral part of x + if x_coef % ten_to_power_of_scale == 0: + var integeral_part = x_coef // ten_to_power_of_scale + var exponent = 0 + while integeral_part % 10 == 0: + integeral_part //= 10 + exponent += 1 + if integeral_part == 1: + return Decimal(exponent, 0, 0, 0) + else: + pass + + # Use the identity: log10(x) = ln(x) / ln(10) + return ln(x) / decimojo.constants.LN10() diff --git a/tests/test_log.mojo b/tests/test_log.mojo new file mode 100644 index 0000000..302ff79 --- /dev/null +++ b/tests/test_log.mojo @@ -0,0 +1,438 @@ +""" +Comprehensive tests for the log() function of the Decimal type. +Tests various scenarios to ensure proper calculation of logarithms with arbitrary bases. +""" + +import testing +from decimojo.prelude import dm, Decimal, RoundingMode + + +fn test_basic_log() raises: + """Test basic logarithm calculations with common bases.""" + print("Testing basic logarithm calculations...") + + # Test case 1: log_2(8) = 3 + var val1 = Decimal(8) + var base1 = Decimal(2) + var result1 = val1.log(base1) + testing.assert_equal( + String(result1), "3", "log_2(8) should be 3, got " + String(result1) + ) + + # Test case 2: log_3(27) = 3 + var val2 = Decimal(27) + var base2 = Decimal(3) + var result2 = val2.log(base2) + testing.assert_equal( + String(result2.round()), + "3", + "log_3(27) should be 3, got " + String(result2), + ) + + # Test case 3: log_5(125) = 3 + var val3 = Decimal(125) + var base3 = Decimal(5) + var result3 = val3.log(base3) + testing.assert_equal( + String(result3.round()), + "3", + "log_5(125) should be 3, got " + String(result3), + ) + + # Test case 4: log_10(1000) = 3 + var val4 = Decimal(1000) + var base4 = Decimal(10) + var result4 = val4.log(base4) + testing.assert_equal( + String(result4), "3", "log_10(1000) should be 3, got " + String(result4) + ) + + # Test case 5: log_e(e) = 1 (natural log of e) + var val5 = Decimal.E() + var base5 = Decimal.E() + var result5 = val5.log(base5) + testing.assert_equal( + String(result5), "1", "log_e(e) should be 1, got " + String(result5) + ) + + print("✓ Basic logarithm calculations tests passed!") + + +fn test_non_integer_results() raises: + """Test logarithm calculations that result in non-integer values.""" + print("Testing logarithm calculations with non-integer results...") + + # Test case 1: log_2(10) + var val1 = Decimal(10) + var base1 = Decimal(2) + var result1 = val1.log(base1) + testing.assert_true( + String(result1).startswith("3.321928094887362347"), + "log_2(10) should be approximately 3.32192809, got " + String(result1), + ) + + # Test case 2: log_3(10) + var val2 = Decimal(10) + var base2 = Decimal(3) + var result2 = val2.log(base2) + testing.assert_true( + String(result2).startswith("2.0959032742893846"), + "log_3(10) should be approximately 2.0959032742893846, got " + + String(result2), + ) + + # Test case 3: log_10(2) + var val3 = Decimal(2) + var base3 = Decimal(10) + var result3 = val3.log(base3) + testing.assert_true( + String(result3).startswith("0.301029995663981195"), + "log_10(2) should be approximately 0.30102999, got " + String(result3), + ) + + # Test case 4: log_e(10) + var val4 = Decimal(10) + var base4 = Decimal.E() + var result4 = val4.log(base4) + testing.assert_true( + String(result4).startswith("2.302585092994045684"), + "log_e(10) should be approximately 2.30258509, got " + String(result4), + ) + + # Test case 5: log_7(19) + var val5 = Decimal(19) + var base5 = Decimal(7) + var result5 = val5.log(base5) + testing.assert_true( + String(result5).startswith("1.5131423106"), + "log_7(19) should be approximately 1.5131423106, got " + + String(result5), + ) + + print("✓ Non-integer result logarithm tests passed!") + + +fn test_fractional_inputs() raises: + """Test logarithm calculations with fractional inputs.""" + print("Testing logarithm calculations with fractional inputs...") + + # Test case 1: log_2(0.5) = -1 + var val1 = Decimal("0.5") + var base1 = Decimal(2) + var result1 = val1.log(base1) + testing.assert_equal( + String(result1), "-1", "log_2(0.5) should be -1, got " + String(result1) + ) + + # Test case 2: log_3(0.125) + var val2 = Decimal("0.125") + var base2 = Decimal(3) + var result2 = val2.log(base2) + testing.assert_true( + String(result2).startswith("-1.89278926"), + "log_3(0.125) should be approximately -1.89278926, got " + + String(result2), + ) + + # Test case 3: log_0.5(2) = -1 (logarithm with fractional base) + var val3 = Decimal(2) + var base3 = Decimal("0.5") + var result3 = val3.log(base3) + testing.assert_equal( + String(result3), "-1", "log_0.5(2) should be -1, got " + String(result3) + ) + + # Test case 4: log_0.1(0.001) = 3 + var val4 = Decimal("0.001") + var base4 = Decimal("0.1") + var result4 = val4.log(base4) + testing.assert_equal( + String(result4.round()), + "3", + "log_0.1(0.001) should be 3, got " + String(result4), + ) + + # Test case 5: log with fractional base and value + var val5 = Decimal("1.5") + var base5 = Decimal("2.5") + var result5 = val5.log(base5) + testing.assert_true( + String(result5).startswith("0.4425070493497599"), + "log_2.5(1.5) should be approximately 0.4425070493497599, got " + + String(result5), + ) + + print("✓ Fractional input logarithm tests passed!") + + +fn test_edge_cases() raises: + """Test edge cases for the logarithm function.""" + print("Testing logarithm edge cases...") + + # Test case 1: log of a negative number (should raise error) + var val1 = Decimal(-10) + var base1 = Decimal(10) + var exception_caught = False + try: + var _result1 = val1.log(base1) + testing.assert_equal( + True, False, "log of negative number should raise error" + ) + except: + exception_caught = True + testing.assert_equal( + exception_caught, True, "log of negative number should raise error" + ) + + # Test case 2: log of zero (should raise error) + var val2 = Decimal(0) + var base2 = Decimal(10) + exception_caught = False + try: + var _result2 = val2.log(base2) + testing.assert_equal(True, False, "log of zero should raise error") + except: + exception_caught = True + testing.assert_equal( + exception_caught, True, "log of zero should raise error" + ) + + # Test case 3: log with base 1 (should raise error) + var val3 = Decimal(10) + var base3 = Decimal(1) + exception_caught = False + try: + var _result3 = val3.log(base3) + testing.assert_equal(True, False, "log with base 1 should raise error") + except: + exception_caught = True + testing.assert_equal( + exception_caught, True, "log with base 1 should raise error" + ) + + # Test case 4: log with base 0 (should raise error) + var val4 = Decimal(10) + var base4 = Decimal(0) + exception_caught = False + try: + var _result4 = val4.log(base4) + testing.assert_equal(True, False, "log with base 0 should raise error") + except: + exception_caught = True + testing.assert_equal( + exception_caught, True, "log with base 0 should raise error" + ) + + # Test case 5: log with negative base (should raise error) + var val5 = Decimal(10) + var base5 = Decimal(-2) + exception_caught = False + try: + var _result5 = val5.log(base5) + testing.assert_equal( + True, False, "log with negative base should raise error" + ) + except: + exception_caught = True + testing.assert_equal( + exception_caught, True, "log with negative base should raise error" + ) + + # Test case 6: log_b(1) = 0 for any base b + var val6 = Decimal(1) + var base6 = Decimal(7.5) # Any base should give result 0 + var result6 = val6.log(base6) + testing.assert_equal( + String(result6), + "0", + "log_b(1) should be 0 for any base, got " + String(result6), + ) + + # Test case 7: log_b(b) = 1 for any base b + var base7 = Decimal("3.14159") + var val7 = base7 # Value same as base should give result 1 + var result7 = val7.log(base7) + testing.assert_equal( + String(result7), + "1", + "log_b(b) should be 1 for any base, got " + String(result7), + ) + + print("✓ Edge cases tests passed!") + + +fn test_precision() raises: + """Test precision of logarithm calculations.""" + print("Testing logarithm precision...") + + # Test case 1: High precision logarithm + var val1 = Decimal( + "2.718281828459045235360287471352" + ) # e to high precision + var base1 = Decimal(10) + var result1 = val1.log(base1) + testing.assert_true( + String(result1).startswith("0.434294481903251827651"), + "log_10(e) should have sufficient precision, got " + String(result1), + ) + + # Test case 2: log_2(1024) = 10 exactly + var val2 = Decimal(1024) + var base2 = Decimal(2) + var result2 = val2.log(base2) + testing.assert_equal( + String(result2.round()), + "10", + "log_2(1024) should be exactly 10, got " + String(result2), + ) + + # Test case 3: Small difference in value + var val3a = Decimal("1.000001") + var val3b = Decimal("1.000002") + var base3 = Decimal(10) + var result3a = val3a.log(base3) + var result3b = val3b.log(base3) + testing.assert_true( + result3a < result3b, + "log(1.000001) should be less than log(1.000002)", + ) + + # Test case 4: Verify log precision with a known value + var val4 = Decimal(2) + var base4 = Decimal.E() + var result4 = val4.log(base4) + testing.assert_true( + String(result4).startswith("0.693147180559945309"), + "log_e(2) should match known value 0.69314718..., got " + + String(result4), + ) + + print("✓ Precision tests passed!") + + +fn test_mathematical_properties() raises: + """Test mathematical properties of logarithms.""" + print("Testing mathematical properties of logarithms...") + + # Test case 1: log_a(x*y) = log_a(x) + log_a(y) + var x1 = Decimal(3) + var y1 = Decimal(4) + var a1 = Decimal(5) + var log_product1 = (x1 * y1).log(a1) + var sum_logs1 = x1.log(a1) + y1.log(a1) + testing.assert_true( + abs(log_product1 - sum_logs1) < Decimal("0.000000000001"), + "log_a(x*y) should equal log_a(x) + log_a(y)", + ) + + # Test case 2: log_a(x/y) = log_a(x) - log_a(y) + var x2 = Decimal(20) + var y2 = Decimal(5) + var a2 = Decimal(2) + var log_quotient2 = (x2 / y2).log(a2) + var diff_logs2 = x2.log(a2) - y2.log(a2) + testing.assert_true( + abs(log_quotient2 - diff_logs2) < Decimal("0.000000000001"), + "log_a(x/y) should equal log_a(x) - log_a(y)", + ) + + # Test case 3: log_a(x^n) = n * log_a(x) + var x3 = Decimal(3) + var n3 = 4 + var a3 = Decimal(7) + var log_power3 = (x3**n3).log(a3) + var n_times_log3 = Decimal(n3) * x3.log(a3) + testing.assert_true( + abs(log_power3 - n_times_log3) < Decimal("0.000000000001"), + "log_a(x^n) should equal n * log_a(x)", + ) + + # Test case 4: log_a(1/x) = -log_a(x) + var x4 = Decimal(7) + var a4 = Decimal(3) + var log_inverse4 = (Decimal(1) / x4).log(a4) + var neg_log4 = -x4.log(a4) + testing.assert_true( + abs(log_inverse4 - neg_log4) < Decimal("0.000000000001"), + "log_a(1/x) should equal -log_a(x)", + ) + + # Test case 5: log_a(b) = log_c(b) / log_c(a) (change of base formula) + var b5 = Decimal(7) + var a5 = Decimal(3) + var c5 = Decimal(10) # Arbitrary third base + var direct_log5 = b5.log(a5) + var change_base5 = b5.log(c5) / a5.log(c5) + testing.assert_true( + abs(direct_log5 - change_base5) < Decimal("0.000000000001"), + "log_a(b) should equal log_c(b) / log_c(a)", + ) + + print("✓ Mathematical properties tests passed!") + + +fn test_consistency_with_other_logarithms() raises: + """Test consistency between log(base) and other logarithm functions.""" + print("Testing consistency with other logarithm functions...") + + # Test case 1: log_10(x) == log10(x) + var val1 = Decimal(7) + var log_base10_val1 = val1.log(Decimal(10)) + var log10_val1 = val1.log10() + testing.assert_true( + abs(log_base10_val1 - log10_val1) < Decimal("0.000000000001"), + "log(x, 10) should equal log10(x)", + ) + + # Test case 2: log_e(x) == ln(x) + var val2 = Decimal(5) + var log_base_e_val2 = val2.log(Decimal.E()) + var ln_val2 = val2.ln() + testing.assert_true( + abs(log_base_e_val2 - ln_val2) < Decimal("0.000000000001"), + "log(x, e) should equal ln(x)", + ) + + print("✓ Consistency with other logarithms tests passed!") + + +fn run_test_with_error_handling( + test_fn: fn () raises -> None, test_name: String +) raises: + """Helper function to run a test function with error handling and reporting. + """ + try: + print("\n" + "=" * 50) + print("RUNNING: " + test_name) + print("=" * 50) + test_fn() + print("\n✓ " + test_name + " passed\n") + except e: + print("\n✗ " + test_name + " FAILED!") + print("Error message: " + String(e)) + raise e + + +fn main() raises: + print("=========================================") + print("Running log() Function Tests") + print("=========================================") + + run_test_with_error_handling(test_basic_log, "Basic logarithm test") + run_test_with_error_handling( + test_non_integer_results, "Non-integer results test" + ) + run_test_with_error_handling( + test_fractional_inputs, "Fractional inputs test" + ) + run_test_with_error_handling(test_edge_cases, "Edge cases test") + run_test_with_error_handling(test_precision, "Precision test") + run_test_with_error_handling( + test_mathematical_properties, "Mathematical properties test" + ) + run_test_with_error_handling( + test_consistency_with_other_logarithms, + "Consistency with other logarithms test", + ) + + print("All log() function tests passed!") diff --git a/tests/test_log10.mojo b/tests/test_log10.mojo new file mode 100644 index 0000000..55be121 --- /dev/null +++ b/tests/test_log10.mojo @@ -0,0 +1,328 @@ +""" +Comprehensive tests for the log10() function of the Decimal type. +Tests various scenarios to ensure proper calculation of base-10 logarithms. +""" + +import testing +from decimojo.prelude import dm, Decimal, RoundingMode + + +fn test_basic_log10() raises: + """Test basic logarithm base 10 calculations.""" + print("Testing basic log10 calculations...") + + # Test case 1: log10(1) = 0 + var val1 = Decimal(1) + var result1 = val1.log10() + testing.assert_equal( + String(result1), "0", "log10(1) should be 0, got " + String(result1) + ) + + # Test case 2: log10(10) = 1 + var val2 = Decimal(10) + var result2 = val2.log10() + testing.assert_equal( + String(result2), "1", "log10(10) should be 1, got " + String(result2) + ) + + # Test case 3: log10(100) = 2 + var val3 = Decimal(100) + var result3 = val3.log10() + testing.assert_equal( + String(result3), "2", "log10(100) should be 2, got " + String(result3) + ) + + # Test case 4: log10(1000) = 3 + var val4 = Decimal(1000) + var result4 = val4.log10() + testing.assert_equal( + String(result4), "3", "log10(1000) should be 3, got " + String(result4) + ) + + # Test case 5: log10(0.1) = -1 + var val5 = Decimal("0.1") + var result5 = val5.log10() + testing.assert_equal( + String(result5), "-1", "log10(0.1) should be -1, got " + String(result5) + ) + + # Test case 6: log10(0.01) = -2 + var val6 = Decimal("0.01") + var result6 = val6.log10() + testing.assert_equal( + String(result6), + "-2", + "log10(0.01) should be -2, got " + String(result6), + ) + + print("✓ Basic log10 calculations tests passed!") + + +fn test_non_powers_of_ten() raises: + """Test logarithm base 10 of numbers that are not exact powers of 10.""" + print("Testing log10 of non-powers of 10...") + + # Test case 1: log10(2) + var val1 = Decimal(2) + var result1 = val1.log10() + testing.assert_true( + String(result1).startswith("0.301029995663981"), + "log10(2) should be approximately 0.301029995663981, got " + + String(result1), + ) + + # Test case 2: log10(5) + var val2 = Decimal(5) + var result2 = val2.log10() + testing.assert_true( + String(result2).startswith("0.698970004336018"), + "log10(5) should be approximately 0.698970004336018, got " + + String(result2), + ) + + # Test case 3: log10(3) + var val3 = Decimal(3) + var result3 = val3.log10() + testing.assert_true( + String(result3).startswith("0.477121254719662"), + "log10(3) should be approximately 0.477121254719662, got " + + String(result3), + ) + + # Test case 4: log10(7) + var val4 = Decimal(7) + var result4 = val4.log10() + testing.assert_true( + String(result4).startswith("0.845098040014256"), + "log10(7) should be approximately 0.845098040014256, got " + + String(result4), + ) + + # Test case 5: log10(0.5) + var val5 = Decimal("0.5") + var result5 = val5.log10() + testing.assert_true( + String(result5).startswith("-0.301029995663981"), + "log10(0.5) should be approximately -0.301029995663981, got " + + String(result5), + ) + + print("✓ Non-powers of 10 log10 calculations tests passed!") + + +fn test_edge_cases() raises: + """Test edge cases for logarithm base 10.""" + print("Testing log10 edge cases...") + + # Test case 1: log10 of a negative number (should raise error) + var val1 = Decimal(-10) + var exception_caught = False + try: + var _result1 = val1.log10() + testing.assert_equal( + True, False, "log10 of negative number should raise error" + ) + except: + exception_caught = True + testing.assert_equal( + exception_caught, True, "log10 of negative number should raise error" + ) + + # Test case 2: log10 of zero (should raise error) + var val2 = Decimal(0) + exception_caught = False + try: + var _result2 = val2.log10() + testing.assert_equal(True, False, "log10 of zero should raise error") + except: + exception_caught = True + testing.assert_equal( + exception_caught, True, "log10 of zero should raise error" + ) + + # Test case 3: log10 of value very close to 1 + var val3 = Decimal("1.0000000001") + var result3 = val3.log10() + testing.assert_true( + abs(result3) < Decimal("0.0000001"), + "log10 of value very close to 1 should be very close to 0", + ) + + # Test case 4: log10 of a very large number + var val4 = Decimal("1" + "0" * 20) # 10^20 + var result4 = val4.log10() + testing.assert_equal( + String(result4), + "20", + "log10(10^20) should be 20, got " + String(result4), + ) + + # Test case 5: log10 of a very small number + var val5 = Decimal("0." + "0" * 19 + "1") # 10^-20 + var result5 = val5.log10() + testing.assert_equal( + String(result5), + "-20", + "log10(10^-20) should be -20, got " + String(result5), + ) + + print("✓ Edge cases tests passed!") + + +fn test_precision() raises: + """Test precision of logarithm base 10 calculations.""" + print("Testing log10 precision...") + + # Test case 1: High precision decimal + var val1 = Decimal("3.14159265358979323846") + var result1 = val1.log10() + testing.assert_true( + String(result1).startswith("0.497149872694133"), + "log10(π) should have sufficient precision", + ) + + # Test case 2: Special value - e + var val2 = Decimal.E() + var result2 = val2.log10() + testing.assert_true( + String(result2).startswith("0.434294481903251"), + "log10(e) should be approximately 0.434294481903251, got " + + String(result2), + ) + + # Test case 3: Check against known value + var val3 = Decimal(2) + var result3 = val3.log10() + testing.assert_true( + abs(result3 - Decimal("0.301029995663981")) + < Decimal("0.000000000000001"), + "log10(2) should match high precision value", + ) + + # Test case 4: Check precision with a number close to a power of 10 + var val4 = Decimal("9.999999999") + var result4 = val4.log10() + testing.assert_true( + String(result4).startswith("0.999999999"), + "log10(9.999999999) should be precise, got " + String(result4), + ) + + print("✓ Precision tests passed!") + + +fn test_mathematical_properties() raises: + """Test mathematical properties of logarithm base 10.""" + print("Testing mathematical properties of log10...") + + # Test case 1: log10(a*b) = log10(a) + log10(b) + var a1 = Decimal(2) + var b1 = Decimal(5) + var product1 = a1 * b1 + var log_product1 = product1.log10() + var sum_logs1 = a1.log10() + b1.log10() + testing.assert_true( + abs(log_product1 - sum_logs1) < Decimal("0.000000000001"), + "log10(a*b) should equal log10(a) + log10(b)", + ) + + # Test case 2: log10(a/b) = log10(a) - log10(b) + var a2 = Decimal(8) + var b2 = Decimal(2) + var quotient2 = a2 / b2 + var log_quotient2 = quotient2.log10() + var diff_logs2 = a2.log10() - b2.log10() + testing.assert_true( + abs(log_quotient2 - diff_logs2) < Decimal("0.000000000001"), + "log10(a/b) should equal log10(a) - log10(b)", + ) + + # Test case 3: log10(a^n) = n * log10(a) + var a3 = Decimal(3) + var n3 = 4 + var power3 = a3**n3 + var log_power3 = power3.log10() + var n_times_log3 = Decimal(n3) * a3.log10() + testing.assert_true( + abs(log_power3 - n_times_log3) < Decimal("0.000000000001"), + "log10(a^n) should equal n * log10(a)", + ) + + # Test case 4: log10(1/a) = -log10(a) + var a4 = Decimal(7) + var inverse4 = Decimal(1) / a4 + var log_inverse4 = inverse4.log10() + var neg_log4 = -a4.log10() + testing.assert_true( + abs(log_inverse4 - neg_log4) < Decimal("0.000000000001"), + "log10(1/a) should equal -log10(a)", + ) + + print("✓ Mathematical properties tests passed!") + + +fn test_consistency_with_other_logarithms() raises: + """Test consistency between log10 and other logarithm functions.""" + print("Testing consistency with other logarithm functions...") + + # Test case 1: log10(x) = ln(x) / ln(10) + var val1 = Decimal(7) + var log10_val1 = val1.log10() + var ln_val1 = val1.ln() + var ln_10 = Decimal(10).ln() + var ln_ratio1 = ln_val1 / ln_10 + testing.assert_true( + abs(log10_val1 - ln_ratio1) < Decimal("0.000000000001"), + "log10(x) should equal ln(x) / ln(10)", + ) + + # Test case 2: log10(x) = log(x, 10) [using base 10 in generic log function] + var val2 = Decimal(5) + var log10_val2 = val2.log10() + var log_base10_val2 = val2.log(Decimal(10)) + testing.assert_true( + abs(log10_val2 - log_base10_val2) < Decimal("0.000000000001"), + "log10(x) should equal log(x, 10)", + ) + + print("✓ Consistency with other logarithms tests passed!") + + +fn run_test_with_error_handling( + test_fn: fn () raises -> None, test_name: String +) raises: + """Helper function to run a test function with error handling and reporting. + """ + try: + print("\n" + "=" * 50) + print("RUNNING: " + test_name) + print("=" * 50) + test_fn() + print("\n✓ " + test_name + " passed\n") + except e: + print("\n✗ " + test_name + " FAILED!") + print("Error message: " + String(e)) + raise e + + +fn main() raises: + print("=========================================") + print("Running log10() Function Tests") + print("=========================================") + + run_test_with_error_handling( + test_basic_log10, "Basic log10 calculations test" + ) + run_test_with_error_handling( + test_non_powers_of_ten, "Non-powers of 10 test" + ) + run_test_with_error_handling(test_edge_cases, "Edge cases test") + run_test_with_error_handling(test_precision, "Precision test") + run_test_with_error_handling( + test_mathematical_properties, "Mathematical properties test" + ) + run_test_with_error_handling( + test_consistency_with_other_logarithms, + "Consistency with other logarithms test", + ) + + print("All log10() function tests passed!")