diff --git a/.github/workflows/run_tests.yaml b/.github/workflows/run_tests.yaml index 36f583e7..ea8eb281 100644 --- a/.github/workflows/run_tests.yaml +++ b/.github/workflows/run_tests.yaml @@ -48,15 +48,3 @@ jobs: run: | magic install magic run mojo test tests -I . - - - name: Run formating checks - run: | - magic install - cp -r ./ /tmp/decimojo-original - magic run mojo format ./ - if ! diff -r --exclude=.git --exclude=.github --exclude=venv ./ /tmp/decimojo-original > /dev/null; then - echo "::error::Formatting issues detected. Run 'mojo format' locally to fix." - exit 1 - else - echo "No formatting issues detected." - fi diff --git a/.github/workflows/test_pre_commit.yaml b/.github/workflows/test_pre_commit.yaml new file mode 100644 index 00000000..b75e1a30 --- /dev/null +++ b/.github/workflows/test_pre_commit.yaml @@ -0,0 +1,51 @@ +name: Run pre-commit +on: + # Run pre-commit on pull requests + pull_request: + # Add a workflow_dispatch event to run pre-commit manually + workflow_dispatch: + +permissions: + contents: read + pull-requests: read + +jobs: + lint: + runs-on: "ubuntu-22.04" + timeout-minutes: 30 + + defaults: + run: + shell: bash + env: + DEBIAN_FRONTEND: noninteractive + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Install magic + run: | + curl -ssL https://magic.modular.com/deb181c4-455c-4abe-a263-afcff49ccf67 | bash + + - name: Add path + run: | + echo "MODULAR_HOME=$HOME/.modular" >> $GITHUB_ENV + echo "$HOME/.modular/bin" >> $GITHUB_PATH + echo "$HOME/.modular/pkg/packages.modular.com_mojo/bin" >> $GITHUB_PATH + + - name: Activate virtualenv + run: | + python3 -m venv $HOME/venv/ + . $HOME/venv/bin/activate + echo PATH=$PATH >> $GITHUB_ENV + + - name: Install pre-commit + run: | + pip install pre-commit + pre-commit install + + - name: Run pre-commit + run: | + magic install + pre-commit run --all-files \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..6a72c9e9 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,9 @@ +repos: + - repo: local + hooks: + - id: mojo-format + name: mojo-format + entry: magic run mojo format + language: system + files: '\.(mojo|🔥|py)$' + stages: [pre-commit] diff --git a/decimojo/__init__.mojo b/decimojo/__init__.mojo index e543b9cc..811a0095 100644 --- a/decimojo/__init__.mojo +++ b/decimojo/__init__.mojo @@ -10,5 +10,5 @@ DeciMojo - Correctly-rounded, fixed-point Decimal library for Mojo. from .decimal import Decimal from .rounding_mode import RoundingMode -from .mathematics import round, power +from .mathematics import power, sqrt, round, absolute from .logic import greater, greater_equal, less, less_equal, equal, not_equal diff --git a/decimojo/decimal.mojo b/decimojo/decimal.mojo index d9aae82b..daf608b5 100644 --- a/decimojo/decimal.mojo +++ b/decimojo/decimal.mojo @@ -22,7 +22,6 @@ Implements basic object methods for working with decimal numbers. """ -import math.math as mt from .rounding_mode import RoundingMode @@ -354,6 +353,51 @@ struct Decimal(Roundable, Writable): # Move decimal point left (increase scale) scale += -exponent + # STEP 2: If scale > max_precision, + # round the coefficient string after truncating + # and re-calculate the scale + if scale > Self.MAX_PRECISION: + var diff_scale = scale - Self.MAX_PRECISION + var kept_digits = len(string_of_coefficient) - diff_scale + + # Truncate the coefficient string to 29 digits + if kept_digits < 0: + string_of_coefficient = String("0") + else: + string_of_coefficient = string_of_coefficient[:kept_digits] + + # Apply rounding if needed + if kept_digits < len(string_of_coefficient): + if string_of_coefficient[kept_digits] >= String("5"): + # Same rounding logic as above + var carry = 1 + var result_chars = List[String]() + + for i in range(len(string_of_coefficient)): + result_chars.append(string_of_coefficient[i]) + + var pos = Self.MAX_PRECISION + while pos >= 0 and carry > 0: + var digit = ord(result_chars[pos]) - ord(String("0")) + digit += carry + + if digit < 10: + result_chars[pos] = chr(digit + ord(String("0"))) + carry = 0 + else: + result_chars[pos] = String("0") + carry = 1 + pos -= 1 + + if carry > 0: + result_chars.insert(0, String("1")) + + string_of_coefficient = String("") + for ch in result_chars: + string_of_coefficient += ch[] + + scale = Self.MAX_PRECISION + # STEP 2: Check for overflow # Check if the integral part of the coefficient is too large var string_of_integral_part: String @@ -364,23 +408,36 @@ struct Decimal(Roundable, Writable): else: string_of_integral_part = String("0") - if len(string_of_integral_part) > Decimal.LEN_OF_MAX_VALUE: - raise Error("Decimal value too large: " + s) - elif len(string_of_integral_part) == Decimal.LEN_OF_MAX_VALUE and ( - string_of_integral_part > Self.MAX_AS_STRING + if (len(string_of_integral_part) > Decimal.LEN_OF_MAX_VALUE) or ( + len(string_of_integral_part) == Decimal.LEN_OF_MAX_VALUE + and (string_of_integral_part > Self.MAX_AS_STRING) ): - raise Error("Decimal value too large: " + s) + raise Error( + "\nError in init from string: Integral part of the Decimal" + " value too large: " + + s + ) # Check if the coefficient is too large - var raw_length_of_coefficient = len(string_of_coefficient) - if raw_length_of_coefficient > Decimal.LEN_OF_MAX_VALUE: - # Need to truncate to Decimal.LEN_OF_MAX_VALUE digits - var rounding_digit = string_of_coefficient[Decimal.LEN_OF_MAX_VALUE] + # Recursively re-calculate the coefficient string after truncating and rounding + # until it fits within the Decimal limits + while (len(string_of_coefficient) > Decimal.LEN_OF_MAX_VALUE) or ( + len(string_of_coefficient) == Decimal.LEN_OF_MAX_VALUE + and (string_of_coefficient > Self.MAX_AS_STRING) + ): + var raw_length_of_coefficient = len(string_of_coefficient) + + # If string_of_coefficient has more than 29 digits, truncate it to 29. + # If string_of_coefficient has 29 digits and larger than MAX_AS_STRING, truncate it to 28. + var rounding_digit = string_of_coefficient[ + min(Decimal.LEN_OF_MAX_VALUE, len(string_of_coefficient) - 1) + ] string_of_coefficient = string_of_coefficient[ - : Decimal.LEN_OF_MAX_VALUE + : min(Decimal.LEN_OF_MAX_VALUE, len(string_of_coefficient) - 1) ] + scale = scale - ( - raw_length_of_coefficient - Decimal.LEN_OF_MAX_VALUE + raw_length_of_coefficient - len(string_of_coefficient) ) # Apply rounding if needed @@ -429,9 +486,13 @@ struct Decimal(Roundable, Writable): break if is_greater: - raise Error("Decimal value too large: " + s) + raise Error( + "\nError in init from string: Decimal value too large: " + s + ) elif len(string_of_coefficient) > len(Self.MAX_AS_STRING): - raise Error("Decimal value too large: " + s) + raise Error( + "\nError in init from string: Decimal value too large: " + s + ) # Step 3: Convert the coefficient string to low/mid/high parts var low: UInt32 = 0 @@ -621,49 +682,14 @@ struct Decimal(Roundable, Writable): """ Adds two Decimal values and returns a new Decimal containing the sum. """ - ############################################################ # Special case for zero - ############################################################ if self.is_zero(): return other if other.is_zero(): return self - ############################################################ - # Check for operands that cancel each other out - # (same absolute value but opposite signs) - ############################################################ - if self.is_negative() != other.is_negative(): - # Different signs - check if absolute values are equal - # First normalize both to same scale for comparison - var max_scale = max(self.scale(), other.scale()) - var self_copy = self - var other_copy = other - - # Scale both up to the maximum scale for proper comparison - if self.scale() < max_scale: - self_copy = self._scale_up(max_scale - self.scale()) - if other.scale() < max_scale: - other_copy = other._scale_up(max_scale - other.scale()) - - # Compare absolute values (ignoring sign) - if ( - self_copy.low == other_copy.low - and self_copy.mid == other_copy.mid - and self_copy.high == other_copy.high - ): - # Numbers cancel out, return zero with proper scale - var result = Decimal.ZERO() - # Use the larger scale for the result - result.flags = UInt32( - (max_scale << Self.SCALE_SHIFT) & Self.SCALE_MASK - ) - return result - - ############################################################ - # Integer addition with same scale - ############################################################ - if self.scale() == other.scale(): + # Integer addition + if (self.scale() == 0) and (other.scale() == 0): var result = Decimal() result.flags = UInt32( (self.scale() << Self.SCALE_SHIFT) & Self.SCALE_MASK @@ -701,172 +727,10 @@ struct Decimal(Roundable, Writable): return result - ############################################################ # Float addition which may be with different scales - ############################################################ - - # Determine which decimal has larger absolute value - var larger_decimal: Decimal - var smaller_decimal: Decimal - var larger_scale: Int - var smaller_scale: Int - - if self._abs_compare(other) >= 0: - larger_decimal = self - smaller_decimal = other - larger_scale = self.scale() - smaller_scale = other.scale() - else: - larger_decimal = other - smaller_decimal = self - larger_scale = other.scale() - smaller_scale = self.scale() - - # Calculate how much we can safely scale up the larger number - var larger_coef = larger_decimal.coefficient() - var significant_digits = len(larger_coef) - var max_safe_scale_increase = Self.MAX_PRECISION - significant_digits - - # If the scales are too different, we need special handling - if smaller_scale > larger_scale + max_safe_scale_increase: - # We need to determine the effective position where the smaller number would contribute - var scale_diff = smaller_scale - larger_scale - - # If beyond our max safe scale, we need to round - if scale_diff > max_safe_scale_increase: - # Get smallest significant digit position in the smaller number - var smaller_coef = smaller_decimal.coefficient() - - # Find first non-zero digit in the smaller number - var first_digit_pos = 0 - for i in range(len(smaller_coef)): - if smaller_coef[i] != "0": - first_digit_pos = i - break - - # Calculate total effective position - var effective_pos = scale_diff + first_digit_pos - - # If still beyond max safe scale, determine if rounding is needed - if effective_pos > max_safe_scale_increase: - # If way beyond precision limit, just return the larger number - if effective_pos > max_safe_scale_increase + 1: - return larger_decimal - - # If exactly at rounding boundary, use first digit to determine rounding - var first_digit = ord(smaller_coef[first_digit_pos]) - ord( - "0" - ) - - # Round up if 5 or greater (using half-up rounding) - if first_digit >= 5: - # Create a small decimal for rounding adjustment - var round_value = Decimal( - "0." + "0" * max_safe_scale_increase + "1" - ) - - # Apply rounding based on signs - if ( - smaller_decimal.is_negative() - != larger_decimal.is_negative() - ): - return larger_decimal + -round_value - else: - return larger_decimal + round_value - else: - # Round down - just return larger number - return larger_decimal - - # If we get here, we can align to max safe scale - var safe_scale = larger_scale + max_safe_scale_increase - var scale_reduction = smaller_scale - safe_scale - smaller_decimal = smaller_decimal._scale_down( - scale_reduction, RoundingMode.HALF_EVEN() - ) - - # Standard addition with aligned scales - var result = Decimal() - var target_scale = max(larger_scale, smaller_decimal.scale()) - - # Scale up if needed - var larger_copy = larger_decimal - var smaller_copy = smaller_decimal - - if larger_scale < target_scale: - larger_copy = larger_decimal._scale_up(target_scale - larger_scale) - if smaller_decimal.scale() < target_scale: - smaller_copy = smaller_decimal._scale_up( - target_scale - smaller_decimal.scale() - ) - - # Set result scale - result.flags = UInt32( - (target_scale << Self.SCALE_SHIFT) & Self.SCALE_MASK - ) - - # Now perform the actual addition - if larger_copy.is_negative() == smaller_copy.is_negative(): - # Same sign: add absolute values and keep the sign - if larger_copy.is_negative(): - result.flags |= Self.SIGN_MASK - - # Add with carry - var carry: UInt32 = 0 - - # Add low parts - var sum_low = UInt64(larger_copy.low) + UInt64(smaller_copy.low) - result.low = UInt32(sum_low & 0xFFFFFFFF) - carry = UInt32(sum_low >> 32) - - # Add mid parts with carry - var sum_mid = UInt64(larger_copy.mid) + UInt64( - smaller_copy.mid - ) + UInt64(carry) - result.mid = UInt32(sum_mid & 0xFFFFFFFF) - carry = UInt32(sum_mid >> 32) - - # Add high parts with carry - var sum_high = UInt64(larger_copy.high) + UInt64( - smaller_copy.high - ) + UInt64(carry) - result.high = UInt32(sum_high & 0xFFFFFFFF) - - # Check for overflow - if (sum_high >> 32) > 0: - raise Error("Decimal overflow in addition") - else: - # Different signs: subtract smaller absolute value from larger - # We already know larger_copy has larger absolute value - var borrow: UInt32 = 0 - - # Subtract low parts with borrow - if larger_copy.low >= smaller_copy.low: - result.low = larger_copy.low - smaller_copy.low - borrow = 0 - else: - result.low = UInt32( - 0x100000000 + larger_copy.low - smaller_copy.low - ) - borrow = 1 - - # Subtract mid parts with borrow - if larger_copy.mid >= smaller_copy.mid + borrow: - result.mid = larger_copy.mid - smaller_copy.mid - borrow - borrow = 0 - else: - result.mid = UInt32( - 0x100000000 + larger_copy.mid - smaller_copy.mid - borrow - ) - borrow = 1 + # Use string-based approach - # Subtract high parts with borrow - result.high = larger_copy.high - smaller_copy.high - borrow - - # Set sign based on which had larger absolute value - if larger_copy.is_negative(): - result.flags |= Self.SIGN_MASK - - return result + return Decimal(decimojo.mathematics._addition_string_based(self, other)) fn __sub__(self, other: Decimal) raises -> Self: """ @@ -1023,7 +887,9 @@ struct Decimal(Roundable, Writable): fn __truediv__(self, other: Decimal) raises -> Self: """ Divides self by other and returns a new Decimal containing the quotient. + Uses a simpler string-based long division approach. """ + # Check for division by zero if other.is_zero(): raise Error("Division by zero") @@ -1039,125 +905,149 @@ struct Decimal(Roundable, Writable): # If dividing identical numbers, return 1 if ( - self.coefficient() == other.coefficient() + self.low == other.low + and self.mid == other.mid + and self.high == other.high and self.scale() == other.scale() ): - return Decimal("1") + return Decimal.ONE() - # Determine sign of result + # Determine sign of result (positive if signs are the same, negative otherwise) var result_is_negative = self.is_negative() != other.is_negative() - # Get coefficients - var numerator = self.coefficient() - var denominator = other.coefficient() + # Get coefficients as strings (absolute values) + var dividend_coef = _remove_trailing_zeros(self.coefficient()) + var divisor_coef = _remove_trailing_zeros(other.coefficient()) - # Calculate natural scale (difference in scales) - var dividend_scale = self.scale() - var divisor_scale = other.scale() - var natural_scale = dividend_scale - divisor_scale + # Use string-based division to avoid overflow with large numbers - # Add working precision zeros to numerator for division - var working_precision = Self.MAX_PRECISION - numerator += "0" * working_precision + # Determine precision needed for calculation + var working_precision = Self.LEN_OF_MAX_VALUE + 1 # +1 for potential rounding - # Convert denominator to integer - var denom_value = UInt64(0) - for i in range(len(denominator)): - denom_value = denom_value * 10 + UInt64( - ord(denominator[i]) - ord("0") - ) + # Perform long division algorithm + var quotient = String("") + var remainder = String("") + var digit = 0 + var current_pos = 0 + var processed_all_dividend = False + var significant_digits_of_quotient = 0 - # Perform long division - var quotient_digits = String("") - var remainder = UInt64(0) + while significant_digits_of_quotient < working_precision: + # Grab next digit from dividend if available + if current_pos < len(dividend_coef): + remainder += dividend_coef[current_pos] + current_pos += 1 + else: + # If we've processed all dividend digits, add a zero + if not processed_all_dividend: + processed_all_dividend = True + remainder += "0" + + # Remove leading zeros from remainder for cleaner comparison + var remainder_start = 0 + while ( + remainder_start < len(remainder) - 1 + and remainder[remainder_start] == "0" + ): + remainder_start += 1 + remainder = remainder[remainder_start:] - for i in range(len(numerator)): - remainder = remainder * 10 + UInt64(ord(numerator[i]) - ord("0")) - var digit = remainder / denom_value - quotient_digits += String(digit) - remainder = remainder % denom_value + # Compare remainder with divisor to determine next quotient digit + digit = 0 + var can_subtract = False + + # Check if remainder >= divisor_coef + if len(remainder) > len(divisor_coef) or ( + len(remainder) == len(divisor_coef) + and remainder >= divisor_coef + ): + can_subtract = True + + if can_subtract: + # Find how many times divisor goes into remainder + while True: + # Try to subtract divisor from remainder + var new_remainder = _subtract_strings( + remainder, divisor_coef + ) + if ( + new_remainder[0] == "-" + ): # Negative result means we've gone too far + break + remainder = new_remainder + digit += 1 + + # Add digit to quotient + quotient += String(digit) + significant_digits_of_quotient = len( + _remove_leading_zeros(quotient) + ) + + # Check if division is exact + var is_exact = remainder == "0" and current_pos >= len(dividend_coef) # Remove leading zeros - var start_pos = 0 - while ( - start_pos < len(quotient_digits) - 1 - and quotient_digits[start_pos] == "0" - ): - start_pos += 1 - quotient_digits = quotient_digits[start_pos:] - - # Check if the division is exact - var is_exact = remainder == 0 - - # For exact division, trim unnecessary trailing zeros - var trailing_zeros_removed = 0 - if is_exact: - # Count trailing zeros - var trailing_zeros = 0 - for i in range( - len(quotient_digits) - 1, 0, -1 - ): # Don't remove last digit - if quotient_digits[i] == "0": + var leading_zeros = 0 + for i in range(len(quotient)): + if quotient[i] == "0": + leading_zeros += 1 + else: + break + + if leading_zeros == len(quotient): + # All zeros, keep just one + quotient = "0" + elif leading_zeros > 0: + quotient = quotient[leading_zeros:] + + # Handle trailing zeros for exact division + var trailing_zeros = 0 + if is_exact and len(quotient) > 1: # Don't remove single digit + for i in range(len(quotient) - 1, 0, -1): + if quotient[i] == "0": trailing_zeros += 1 else: break - # For exact division, trim all trailing zeros if trailing_zeros > 0: - quotient_digits = quotient_digits[ - : len(quotient_digits) - trailing_zeros - ] - trailing_zeros_removed = trailing_zeros - - # Create the result with the correct decimal point position - var actual_value_str = String("") - - # Handle exact division correctly with proper decimal point placement - if is_exact: - # The scale should be adjusted for trailing zeros removed - var effective_scale = natural_scale + working_precision - trailing_zeros_removed - - if effective_scale <= 0: - # For negative effective scale, we need to add zeros - actual_value_str = quotient_digits + "0" * (-effective_scale) - else: - # Need to place decimal point with effective_scale digits after it - if len(quotient_digits) <= effective_scale: - # Number < 1, needs leading zeros - actual_value_str = ( - "0." - + "0" * (effective_scale - len(quotient_digits)) - + quotient_digits - ) - else: - # Place decimal point from right to left - var decimal_pos = len(quotient_digits) - effective_scale - actual_value_str = ( - quotient_digits[:decimal_pos] - + "." - + quotient_digits[decimal_pos:] - ) + quotient = quotient[: len(quotient) - trailing_zeros] + + # Calculate decimal point position + var dividend_scientific_exponent = self.scientific_exponent() + var divisor_scientific_exponent = other.scientific_exponent() + var result_scientific_exponent = dividend_scientific_exponent - divisor_scientific_exponent + + if dividend_coef < divisor_coef: + # If dividend < divisor, result < 1 + result_scientific_exponent -= 1 + + var decimal_pos = result_scientific_exponent + 1 + + # Format result with decimal point + var result_str = String("") + + if decimal_pos <= 0: + # decimal_pos <= 0, needs leading zeros + # For example, decimal_pos = -1 + # 1234 -> 0.1234 + result_str = "0." + "0" * (-decimal_pos) + quotient + elif decimal_pos >= len(quotient): + # All digits are to the left of the decimal point + # For example, decimal_pos = 5 + # 1234 -> 12340 + result_str = quotient + "0" * (decimal_pos - len(quotient)) else: - # For inexact division, position decimal based on working_precision - var decimal_pos = len(quotient_digits) - working_precision - - if decimal_pos <= 0: - # Number < 1, needs leading zeros - actual_value_str = "0." + "0" * (-decimal_pos) + quotient_digits - else: - # Insert decimal at the appropriate position - actual_value_str = ( - quotient_digits[:decimal_pos] - + "." - + quotient_digits[decimal_pos:] - ) - - # Create result from formatted string - var result = Decimal(actual_value_str) + # Insert decimal point within the digits + # For example, decimal_pos = 2 + # 1234 -> 12.34 + result_str = quotient[:decimal_pos] + "." + quotient[decimal_pos:] # Apply sign - if result_is_negative: - result = -result + if result_is_negative and result_str != "0": + result_str = "-" + result_str + + # Convert to Decimal and return + var result = Decimal(result_str) return result @@ -1440,11 +1330,44 @@ struct Decimal(Roundable, Writable): """Returns the scale (number of decimal places) of this Decimal.""" return Int((self.flags & Self.SCALE_MASK) >> Self.SCALE_SHIFT) + fn scientific_exponent(self) -> Int: + """ + Calculates the exponent for scientific notation representation of a Decimal. + The exponent is the power of 10 needed to represent the value in scientific notation. + """ + + # Get the coefficient as a string + var coef = self.coefficient() + + return len(coef) - 1 - self.scale() + + fn significant_digits(self) -> Int: + """ + Returns the number of significant digits in this Decimal. + The number of significant digits is the total number of digits in the coefficient, + excluding leading and trailing zeros. + """ + + # Get the coefficient as a string + var coef = self.coefficient() + + # Count significant digits + var count = 0 + var found_non_zero = False + + for i in range(len(coef)): + if coef[i] != "0": + found_non_zero = True + if found_non_zero: + count += 1 + + return count + # ===------------------------------------------------------------------=== # # Internal methods # ===------------------------------------------------------------------=== # - fn _abs_compare(self, other: Decimal) -> Int: + fn _abs_compare(self, other: Decimal) raises -> Int: """ Compares absolute values of two Decimal numbers, ignoring signs. @@ -1453,41 +1376,15 @@ struct Decimal(Roundable, Writable): - Zero if |self| = |other| - Negative value if |self| < |other| """ - # Create temporary copies with same scale for comparison - var self_copy = self - var other_copy = other + var abs_self = decimojo.absolute(self) + var abs_other = decimojo.absolute(other) - # Get scales - var self_scale = self.scale() - var other_scale = other.scale() - - # Scale up the one with smaller scale to match - if self_scale < other_scale: - self_copy = self_copy._scale_up(other_scale - self_scale) - elif other_scale < self_scale: - other_copy = other_copy._scale_up(self_scale - other_scale) - - # Now both have the same scale, compare coefficients - # Start with highest significance (high) - if self_copy.high > other_copy.high: + if abs_self > abs_other: return 1 - if self_copy.high < other_copy.high: + elif abs_self < abs_other: return -1 - - # High parts equal, compare mid parts - if self_copy.mid > other_copy.mid: - return 1 - if self_copy.mid < other_copy.mid: - return -1 - - # Mid parts equal, compare low parts - if self_copy.low > other_copy.low: - return 1 - if self_copy.low < other_copy.low: - return -1 - - # All parts equal, numbers are equal - return 0 + else: + return 0 fn _internal_representation(value: Decimal): # Show internal representation details @@ -1536,9 +1433,6 @@ struct Decimal(Roundable, Writable): # Collect the digits to be removed (we need all of them for proper rounding) for i in range(scale_diff): - var last_digit = temp.low % 10 - removed_digits = String(last_digit) + removed_digits - # Divide by 10 without any rounding at this stage var high64 = UInt64(temp.high) var mid64 = UInt64(temp.mid) @@ -1557,6 +1451,9 @@ struct Decimal(Roundable, Writable): var low_with_remainder = low64 + (remainder_m << 32) var new_low = low_with_remainder // 10 + var last_digit = low_with_remainder % 10 + removed_digits = String(last_digit) + removed_digits + # Update temp values temp.low = UInt32(new_low) temp.mid = UInt32(new_mid) @@ -1626,7 +1523,9 @@ struct Decimal(Roundable, Writable): fn _scale_up(self, owned scale_diff: Int) -> Decimal: """ - Internal method to scale up a decimal by multiplying by 10^scale_diff. + Internal method to scale up a decimal by: + - multiplying coefficient by 10^scale_diff. + - imcrease the scale by scale_diff. Args: scale_diff: Number of decimal places to scale up by @@ -1642,17 +1541,17 @@ struct Decimal(Roundable, Writable): # Update the scale in the flags var new_scale = self.scale() + scale_diff - if new_scale > Self.MAX_PRECISION: + if new_scale > Self.MAX_PRECISION + 1: # Cannot scale beyond max precision, limit the scaling - scale_diff = Self.MAX_PRECISION - self.scale() - new_scale = Self.MAX_PRECISION + scale_diff = Self.MAX_PRECISION + 1 - self.scale() + new_scale = Self.MAX_PRECISION + 1 result.flags = (result.flags & ~Self.SCALE_MASK) | ( UInt32(new_scale << Self.SCALE_SHIFT) & Self.SCALE_MASK ) # Scale up by multiplying by powers of 10 - for _ in range(scale_diff): + for i in range(scale_diff): # Check for potential overflow before multiplying if result.high > 0xFFFFFFFF // 10 or ( result.high == 0xFFFFFFFF // 10 and result.low > 0xFFFFFFFF % 10 @@ -1714,3 +1613,60 @@ fn _float_to_decimal_str(value: Float64, precision: Int) -> String: result = "-" + result return result + + +fn _subtract_strings(a: String, b: String) -> String: + """Subtracts string b from string a and returns the result as a string.""" + # Ensure a is longer or equal to b by padding with zeros + var a_padded = a + var b_padded = b + + if len(a) < len(b): + a_padded = "0" * (len(b) - len(a)) + a + elif len(b) < len(a): + b_padded = "0" * (len(a) - len(b)) + b + + var result = String("") + var borrow = 0 + + # Perform subtraction digit by digit from right to left + for i in range(len(a_padded) - 1, -1, -1): + var digit_a = ord(a_padded[i]) - ord("0") + var digit_b = ord(b_padded[i]) - ord("0") + borrow + + if digit_a < digit_b: + digit_a += 10 + borrow = 1 + else: + borrow = 0 + + result = String(digit_a - digit_b) + result + + # Check if result is negative + if borrow > 0: + return "-" + result + + # Remove leading zeros + var start_idx = 0 + while start_idx < len(result) - 1 and result[start_idx] == "0": + start_idx += 1 + + return result[start_idx:] + + +fn _remove_leading_zeros(value: String) -> String: + """Removes leading zeros from a string.""" + var start_idx = 0 + while start_idx < len(value) - 1 and value[start_idx] == "0": + start_idx += 1 + + return value[start_idx:] + + +fn _remove_trailing_zeros(value: String) -> String: + """Removes trailing zeros from a string.""" + var end_idx = len(value) + while end_idx > 0 and value[end_idx - 1] == "0": + end_idx -= 1 + + return value[:end_idx] diff --git a/decimojo/mathematics.mojo b/decimojo/mathematics.mojo index fad8af80..bec90c48 100644 --- a/decimojo/mathematics.mojo +++ b/decimojo/mathematics.mojo @@ -14,10 +14,11 @@ # 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 -# root(x: Decimal, n: Int): Computes the nth root of x using Newton's method +# round(x: Decimal, places: Int, mode: RoundingMode): Rounds x to specified decimal places # # TODO Additional functions planned for future implementation: # +# root(x: Decimal, n: Int): Computes the nth root of x using Newton's method # exp(x: Decimal): Computes e raised to the power of x # ln(x: Decimal): Computes the natural logarithm of x # log10(x: Decimal): Computes the base-10 logarithm of x @@ -25,7 +26,6 @@ # cos(x: Decimal): Computes the cosine of x (in radians) # tan(x: Decimal): Computes the tangent of x (in radians) # abs(x: Decimal): Returns the absolute value of x -# round(x: Decimal, places: Int, mode: RoundingMode): Rounds x to specified decimal places # floor(x: Decimal): Returns the largest integer <= x # ceil(x: Decimal): Returns the smallest integer >= x # gcd(a: Decimal, b: Decimal): Returns greatest common divisor of a and b @@ -44,6 +44,326 @@ from decimojo.rounding_mode import RoundingMode # ===----------------------------------------------------------------------=== # +fn _addition_string_based(a: Decimal, b: Decimal) -> String: + """ + Performs addition of two Decimals using a string-based approach. + Preserves decimal places to match the inputs. + + Args: + a: First Decimal operand. + b: Second Decimal operand. + + Returns: + A string representation of the sum with decimal places preserved. + """ + # Special case: if either number is zero, return the other + if a.is_zero(): + return String(b) + if b.is_zero(): + return String(a) + + # Handle different signs + if a.is_negative() != b.is_negative(): + # If signs differ, we need subtraction + if a.is_negative(): + # -a + b = b - |a| + return _subtraction_string_based(b, -a) + else: + # a + (-b) = a - |b| + return _subtraction_string_based(a, -b) + + # Determine the number of decimal places to preserve + # We need to examine the original string representation of a and b + var a_str = String(a) + var b_str = String(b) + var a_decimal_places = 0 + var b_decimal_places = 0 + + # Count decimal places in a + var a_decimal_pos = a_str.find(".") + if a_decimal_pos >= 0: + a_decimal_places = len(a_str) - a_decimal_pos - 1 + + # Count decimal places in b + var b_decimal_pos = b_str.find(".") + if b_decimal_pos >= 0: + b_decimal_places = len(b_str) - b_decimal_pos - 1 + + # Determine target decimal places (maximum of both inputs) + var target_decimal_places = max(a_decimal_places, b_decimal_places) + + # At this point, both numbers have the same sign + var is_negative = a.is_negative() # and b.is_negative() is the same + + # Step 1: Get coefficient strings (absolute values) + var a_coef = a.coefficient() + var b_coef = b.coefficient() + var a_scale = a.scale() + var b_scale = b.scale() + + # Step 2: Align decimal points + var max_scale = max(a_scale, b_scale) + + # Pad coefficients with trailing zeros to align decimal points + if a_scale < max_scale: + a_coef += "0" * (max_scale - a_scale) + if b_scale < max_scale: + b_coef += "0" * (max_scale - b_scale) + + # Ensure both strings are the same length by padding with leading zeros + var max_length = max(len(a_coef), len(b_coef)) + a_coef = "0" * (max_length - len(a_coef)) + a_coef + b_coef = "0" * (max_length - len(b_coef)) + b_coef + + # Step 3: Perform addition from right to left + var result = String("") + var carry = 0 + + for i in range(len(a_coef) - 1, -1, -1): + var digit_a = ord(a_coef[i]) - ord("0") + var digit_b = ord(b_coef[i]) - ord("0") + + var digit_sum = digit_a + digit_b + carry + carry = digit_sum // 10 + result = String(digit_sum % 10) + result + + # Handle final carry + if carry > 0: + result = String(carry) + result + + # Step 4: Insert decimal point at correct position + var final_result = String("") + + if max_scale == 0: + # No decimal places, just return the result + final_result = result + # Add decimal point and zeros if needed to match target decimal places + if target_decimal_places > 0: + final_result += "." + "0" * target_decimal_places + else: + var decimal_pos = len(result) - max_scale + + if decimal_pos <= 0: + # Result is less than 1, need leading zeros + final_result = "0." + "0" * (0 - decimal_pos) + result + + # Ensure we have enough decimal places to match target + var current_decimals = len(result) + (0 - decimal_pos) + if current_decimals < target_decimal_places: + final_result += "0" * (target_decimal_places - current_decimals) + else: + # Insert decimal point + final_result = result[:decimal_pos] + "." + result[decimal_pos:] + + # Ensure we have enough decimal places to match target + var current_decimals = len(result) - decimal_pos + if current_decimals < target_decimal_places: + final_result += "0" * (target_decimal_places - current_decimals) + + # Add negative sign if needed + if ( + is_negative + and final_result != "0" + and final_result != "0." + "0" * target_decimal_places + ): + final_result = "-" + final_result + + return final_result + + +fn _subtraction_string_based(owned a: Decimal, owned b: Decimal) -> String: + """ + Helper function to perform subtraction of b from a. + Handles cases for all sign combinations and preserves decimal places. + + Args: + a: First Decimal operand (minuend). + b: Second Decimal operand (subtrahend). + + Returns: + A string representation of the difference with decimal places preserved. + """ + # Determine the number of decimal places to preserve + # We need to examine the original string representation of a and b + var a_str = String(a) + var b_str = String(b) + var a_decimal_places = 0 + var b_decimal_places = 0 + + # Count decimal places in a + var a_decimal_pos = a_str.find(".") + if a_decimal_pos >= 0: + a_decimal_places = len(a_str) - a_decimal_pos - 1 + + # Count decimal places in b + var b_decimal_pos = b_str.find(".") + if b_decimal_pos >= 0: + b_decimal_places = len(b_str) - b_decimal_pos - 1 + + # Determine target decimal places (maximum of both inputs) + var target_decimal_places = max(a_decimal_places, b_decimal_places) + + # Handle different signs + if a.is_negative() != b.is_negative(): + # When signs differ, subtraction becomes addition + if a.is_negative(): + # -a - b = -(a + b) + var sum_result = _addition_string_based(-a, b) + if sum_result == "0": + return "0." + "0" * target_decimal_places + return "-" + sum_result + else: + # a - (-b) = a + b + return _addition_string_based(a, -b) + + # At this point, both numbers have the same sign + var is_negative = a.is_negative() # Both a and b have the same sign + + # Compare absolute values to determine which is larger + var a_larger = True + var a_coef = a.coefficient() + var b_coef = b.coefficient() + var a_scale = a.scale() + var b_scale = b.scale() + + # First compare by number of digits before decimal point + var a_int_digits = len(a_coef) - a_scale + var b_int_digits = len(b_coef) - b_scale + + if a_int_digits < b_int_digits: + a_larger = False + elif a_int_digits == b_int_digits: + # If same number of integer digits, align decimal points and compare + var max_scale = max(a_scale, b_scale) + + # Pad coefficients with trailing zeros to align + var a_padded = a_coef + "0" * (max_scale - a_scale) + var b_padded = b_coef + "0" * (max_scale - b_scale) + + # Ensure both are the same length + var max_length = max(len(a_padded), len(b_padded)) + a_padded = "0" * (max_length - len(a_padded)) + a_padded + b_padded = "0" * (max_length - len(b_padded)) + b_padded + + # Compare digit by digit + a_larger = a_padded >= b_padded + + # Determine sign of result based on comparison and original signs + var result_is_negative = is_negative + if not a_larger: + # If |a| < |b|, then: + # For positive numbers: a - b = -(b - a) + # For negative numbers: -a - (-b) = -a + b = b - a = -(-(b - a)) = -(b - a) + # So result sign is flipped from original + result_is_negative = not result_is_negative + + # Swap a and b so we always subtract smaller from larger + a, b = b, a + + # Now |a| is guaranteed to be >= |b| + # Align decimal points again for the actual subtraction + var max_scale = max(a.scale(), b.scale()) + + # Get coefficients again (after possible swap) + a_coef = a.coefficient() + b_coef = b.coefficient() + a_scale = a.scale() + b_scale = b.scale() + + # Pad coefficients with trailing zeros to align decimal points + a_coef += "0" * (max_scale - a_scale) + b_coef += "0" * (max_scale - b_scale) + + # Ensure both strings are the same length + var max_length = max(len(a_coef), len(b_coef)) + a_coef = "0" * (max_length - len(a_coef)) + a_coef + b_coef = "0" * (max_length - len(b_coef)) + b_coef + + # Perform subtraction from right to left + var result = String("") + var borrow = 0 + + for i in range(len(a_coef) - 1, -1, -1): + var digit_a = ord(a_coef[i]) - ord("0") - borrow + var digit_b = ord(b_coef[i]) - ord("0") + + if digit_a < digit_b: + digit_a += 10 + borrow = 1 + else: + borrow = 0 + + var digit_diff = digit_a - digit_b + result = String(digit_diff) + result + + # Remove leading zeros (but keep at least one digit before decimal point) + var start_idx = 0 + while start_idx < len(result) - max_scale - 1 and result[start_idx] == "0": + start_idx += 1 + + result = result[start_idx:] + + # Insert decimal point + var final_result = String("") + + if max_scale == 0: + # No decimal places in calculation, but we still need to consider target decimal places + final_result = result + if target_decimal_places > 0: + final_result += "." + "0" * target_decimal_places + else: + var decimal_pos = len(result) - max_scale + + if decimal_pos <= 0: + # Result is less than 1 + final_result = "0." + "0" * (0 - decimal_pos) + result + + # Ensure we have enough decimal places to match target + var current_decimals = len(result) + (0 - decimal_pos) + if current_decimals < target_decimal_places: + final_result += "0" * (target_decimal_places - current_decimals) + else: + # Insert decimal point + final_result = result[:decimal_pos] + "." + result[decimal_pos:] + + # Ensure we have enough decimal places to match target + var current_decimals = len(result) - decimal_pos + if current_decimals < target_decimal_places: + final_result += "0" * (target_decimal_places - current_decimals) + + # Handle case where result is zero + if final_result == "0" or final_result.startswith("0."): + # Check if the result contains only zeros and decimal point + var is_all_zero = True + for i in range(len(final_result)): + if final_result[i] != "0" and final_result[i] != ".": + is_all_zero = False + break + + if is_all_zero: + return ( + "0." + "0" * target_decimal_places if target_decimal_places + > 0 else "0" + ) + + var is_zero_point = True + for i in range(len(final_result)): + if final_result[i] != "0" and final_result[i] != ".": + is_zero_point = False + break + + # Add negative sign if needed + if result_is_negative and not ( + final_result == "0" + or + # Check if string starts with "0." and contains only zeros and decimal point + (final_result.startswith("0.") and (is_zero_point)) + ): + final_result = "-" + final_result + + return final_result + + fn power(base: Decimal, exponent: Decimal) raises -> Decimal: """ Raises base to the power of exponent and returns a new Decimal. @@ -162,3 +482,120 @@ fn round( # Otherwise, scale down with the specified rounding mode return number._scale_down(current_scale - decimal_places, rounding_mode) + + +# ===------------------------------------------------------------------------===# +# Rounding +# ===------------------------------------------------------------------------===# + + +fn absolute(x: Decimal) raises -> Decimal: + """ + Returns the absolute value of a Decimal number. + + Args: + x: The Decimal value to compute the absolute value of. + + Returns: + A new Decimal containing the absolute value of x. + """ + if x.is_negative(): + return -x + return x + + +fn sqrt(x: Decimal) raises -> Decimal: + """ + Computes the square root of a Decimal value using Newton-Raphson method. + + Args: + x: The Decimal value to compute the square root of. + + Returns: + A new Decimal containing the square root of x. + + Raises: + Error: If x is negative. + """ + # Special cases + if x.is_negative(): + raise Error("Cannot compute square root of negative number") + + if x.is_zero(): + return Decimal.ZERO() + + if x == Decimal.ONE(): + return Decimal.ONE() + + # Working precision - we'll compute with extra digits and round at the end + var working_precision = UInt32(x.scale() * 2) + working_precision = max(working_precision, UInt32(10)) # At least 10 digits + + # Initial guess - a good guess helps converge faster + # For numbers near 1, use the number itself + # For very small or large numbers, scale appropriately + var guess: Decimal + var exponent = len(x.coefficient()) - x.scale() + + if exponent >= 0 and exponent <= 3: + # For numbers between 0.1 and 1000, start with x/2 + 0.5 + try: + var half_x = x / Decimal("2") + guess = half_x + Decimal("0.5") + except e: + raise e + else: + # For larger/smaller numbers, make a smarter guess + # This scales based on the magnitude of the number + var shift: Int + if exponent % 2 != 0: + # For odd exponents, adjust + shift = (exponent + 1) // 2 + else: + shift = exponent // 2 + + try: + # Use an approximation based on the exponent + if exponent > 0: + guess = Decimal("10") ** shift + else: + guess = Decimal("0.1") ** (-shift) + + except e: + raise e + + # Newton-Raphson iterations + # x_n+1 = (x_n + S/x_n) / 2 + var prev_guess = Decimal.ZERO() + var iteration_count = 0 + var max_iterations = 100 # Prevent infinite loops + + while guess != prev_guess and iteration_count < max_iterations: + prev_guess = guess + + try: + var division_result = x / guess + var sum_result = guess + division_result + guess = sum_result / Decimal("2") + except e: + raise e + + iteration_count += 1 + + # Round to appropriate precision - typically half the working precision + var result_precision = x.scale() + if result_precision % 2 == 1: + # For odd scales, add 1 to ensure proper rounding + result_precision += 1 + + # The result scale should be approximately half the input scale + result_precision = result_precision // 2 + + # Format to the appropriate number of decimal places + var result_str = String(guess) + + try: + var rounded_result = Decimal(result_str) + return rounded_result + except e: + raise e diff --git a/tests/test_decimal_arithmetics.mojo b/tests/test_decimal_arithmetics.mojo index 0148b982..7769c7d1 100644 --- a/tests/test_decimal_arithmetics.mojo +++ b/tests/test_decimal_arithmetics.mojo @@ -2,7 +2,7 @@ Test Decimal arithmetic operations including addition, subtraction, and negation. """ from decimojo import Decimal -from decimojo.mathematics import round +from decimojo.mathematics import round, absolute import testing @@ -669,6 +669,207 @@ fn test_division() raises: except: testing.assert_equal(True, True, "Division by zero correctly rejected") + # ============= ADDITIONAL DIVISION TEST CASES ============= + print("\nTesting additional division scenarios...") + + # Test case 16: Division with very large number by very small number + var a16 = Decimal("1000000000") + var b16 = Decimal("0.0001") + var result16 = a16 / b16 + testing.assert_equal( + String(result16), + "10000000000000", + "Large number divided by small number", + ) + + # Test case 17: Division with very small number by very large number + var a17 = Decimal("0.0001") + var b17 = Decimal("1000000000") + var result17 = a17 / b17 + testing.assert_true( + String(result17).startswith("0.0000000000001"), + "Small number divided by large number", + ) + + # Test case 18: Division resulting in repeating decimal + var a18 = Decimal("1") + var b18 = Decimal("3") + var result18 = a18 / b18 + testing.assert_true( + String(result18).startswith("0.33333333"), + "Division resulting in repeating decimal (1/3)", + ) + + # Test case 19: Division by powers of 10 + var a19 = Decimal("123.456") + var b19 = Decimal("10") + var result19 = a19 / b19 + testing.assert_equal( + String(result19), + "12.3456", + "Division by power of 10", + ) + + # Test case 20: Division by powers of 10 (another case) + var a20 = Decimal("123.456") + var b20 = Decimal("0.01") + var result20 = a20 / b20 + testing.assert_equal( + String(result20), + "12345.6", + "Division by 0.01 (multiply by 100)", + ) + + # Test case 21: Division of nearly equal numbers + var a21 = Decimal("1.000001") + var b21 = Decimal("1") + var result21 = a21 / b21 + testing.assert_equal( + String(result21), + "1.000001", + "Division of nearly equal numbers", + ) + + # Test case 22: Division resulting in a number with many trailing zeros + var a22 = Decimal("1") + var b22 = Decimal("8") + var result22 = a22 / b22 + testing.assert_true( + String(result22).startswith("0.125"), + "Division resulting in an exact decimal with trailing zeros", + ) + + # Test case 23: Division with negative numerator + var a23 = Decimal("-50") + var b23 = Decimal("10") + var result23 = a23 / b23 + testing.assert_equal( + String(result23), + "-5", + "Division with negative numerator", + ) + + # Test case 24: Division with negative denominator + var a24 = Decimal("50") + var b24 = Decimal("-10") + var result24 = a24 / b24 + testing.assert_equal( + String(result24), + "-5", + "Division with negative denominator", + ) + + # Test case 25: Division with both negative + var a25 = Decimal("-50") + var b25 = Decimal("-10") + var result25 = a25 / b25 + testing.assert_equal( + String(result25), + "5", + "Division with both negative numbers", + ) + + # Test case 26: Division resulting in exact integer + var a26 = Decimal("96.75") + var b26 = Decimal("4.5") + var result26 = a26 / b26 + testing.assert_equal( + String(result26), + "21.5", + "Division resulting in exact value", + ) + + # Test case 27: Division with high precision numbers + var a27 = Decimal("0.123456789012345678901234567") + var b27 = Decimal("0.987654321098765432109876543") + var result27 = a27 / b27 + testing.assert_true( + String(result27).startswith("0.12499"), + "Division of high precision numbers", + ) + + # Test case 28: Division with extreme digit patterns + var a28 = Decimal("9" * 15) # 999999999999999 + var b28 = Decimal("9" * 5) # 99999 + var result28 = a28 / b28 + testing.assert_equal( + String(result28), + "10000100001", + "Division with extreme digit patterns (all 9's)", + ) + + # Test case 29: Division where result is zero + var a29 = Decimal("0") + var b29 = Decimal("123.45") + var result29 = a29 / b29 + testing.assert_equal( + String(result29), + "0", + "Division where result is zero", + ) + + # Test case 30: Division where numerator is smaller than denominator + var a30 = Decimal("1") + var b30 = Decimal("10000") + var result30 = a30 / b30 + testing.assert_equal( + String(result30), + "0.0001", + "Division where numerator is smaller than denominator", + ) + + # Test case 31: Division resulting in scientific notation range + var a31 = Decimal("1") + var b31 = Decimal("1" + "0" * 20) # 10^20 + var result31 = a31 / b31 + testing.assert_true( + String(result31).startswith("0.00000000000000000001"), + "Division resulting in very small number", + ) + + # Test case 32: Division with mixed precision + var a32 = Decimal("1") + var b32 = Decimal("3.33333333333333333333333333") + var result32 = a32 / b32 + testing.assert_true( + String(result32).startswith("0.3"), + "Division with mixed precision numbers", + ) + + # Test case 33: Division by fractional power of 10 + var a33 = Decimal("5.5") + var b33 = Decimal("0.055") + var result33 = a33 / b33 + testing.assert_equal( + String(result33), + "100", + "Division by fractional power of 10", + ) + + # Test case 34: Division with rounding at precision boundary + var a34 = Decimal("2") + var b34 = Decimal("3") + var result34 = a34 / b34 + # Result should be about 0.66666... + var expected34 = Decimal("0.66666666666666666666666666667") + print(result34) + testing.assert_equal( + result34, + expected34, + "Division with rounding at precision boundary", + ) + + # Test case 35: Division by value very close to zero + var a35 = Decimal("1") + var b35 = Decimal("0." + "0" * 26 + "1") # 0.000...0001 (27 zeros) + var result35 = a35 / b35 + testing.assert_true( + String(result35).startswith("1" + "0" * 27), + "Division by value very close to zero", + ) + + print("Additional division tests passed!") + print("Decimal division tests passed!") diff --git a/tests/test_decimal_mathematics.mojo b/tests/test_decimal_mathematics.mojo index e69de29b..231f1312 100644 --- a/tests/test_decimal_mathematics.mojo +++ b/tests/test_decimal_mathematics.mojo @@ -0,0 +1,857 @@ +""" +Comprehensive tests for the sqrt function of the Decimal type. +""" +from decimojo import Decimal +from decimojo.mathematics import sqrt, round +from decimojo.rounding_mode import RoundingMode +import testing + + +fn test_perfect_squares() raises: + print("Testing square root of perfect squares...") + + # Test case 1 + try: + var d1 = Decimal("1") + var expected1 = "1" + var result1 = sqrt(d1) + testing.assert_equal( + String(result1), + expected1, + "sqrt(" + String(d1) + ") should be " + expected1, + ) + except e: + print("ERROR in test case 1: sqrt(1) = 1") + raise e + + # Test case 2 + try: + var d2 = Decimal("4") + var expected2 = "2" + var result2 = sqrt(d2) + testing.assert_equal( + String(result2), + expected2, + "sqrt(" + String(d2) + ") should be " + expected2, + ) + except e: + print("ERROR in test case 2: sqrt(4) = 2") + raise e + + # Test case 3 + var d3 = Decimal("9") + var expected3 = "3" + var result3 = sqrt(d3) + testing.assert_equal( + String(result3), + expected3, + "sqrt(" + String(d3) + ") should be " + expected3, + ) + + # Test case 4 + var d4 = Decimal("16") + var expected4 = "4" + var result4 = sqrt(d4) + testing.assert_equal( + String(result4), + expected4, + "sqrt(" + String(d4) + ") should be " + expected4, + ) + + # Test case 5 + var d5 = Decimal("25") + var expected5 = "5" + var result5 = sqrt(d5) + testing.assert_equal( + String(result5), + expected5, + "sqrt(" + String(d5) + ") should be " + expected5, + ) + + # Test case 6 + var d6 = Decimal("36") + var expected6 = "6" + var result6 = sqrt(d6) + testing.assert_equal( + String(result6), + expected6, + "sqrt(" + String(d6) + ") should be " + expected6, + ) + + # Test case 7 + var d7 = Decimal("49") + var expected7 = "7" + var result7 = sqrt(d7) + testing.assert_equal( + String(result7), + expected7, + "sqrt(" + String(d7) + ") should be " + expected7, + ) + + # Test case 8 + var d8 = Decimal("64") + var expected8 = "8" + var result8 = sqrt(d8) + testing.assert_equal( + String(result8), + expected8, + "sqrt(" + String(d8) + ") should be " + expected8, + ) + + # Test case 9 + var d9 = Decimal("81") + var expected9 = "9" + var result9 = sqrt(d9) + testing.assert_equal( + String(result9), + expected9, + "sqrt(" + String(d9) + ") should be " + expected9, + ) + + # Test case 10 + var d10 = Decimal("100") + var expected10 = "10" + var result10 = sqrt(d10) + testing.assert_equal( + String(result10), + expected10, + "sqrt(" + String(d10) + ") should be " + expected10, + ) + + # Test case 11 + var d11 = Decimal("10000") + var expected11 = "100" + var result11 = sqrt(d11) + testing.assert_equal( + String(result11), + expected11, + "sqrt(" + String(d11) + ") should be " + expected11, + ) + + # Test case 12 + var d12 = Decimal("1000000") + var expected12 = "1000" + var result12 = sqrt(d12) + testing.assert_equal( + String(result12), + expected12, + "sqrt(" + String(d12) + ") should be " + expected12, + ) + + print("Perfect square tests passed!") + + +fn test_non_perfect_squares() raises: + print("Testing square root of non-perfect squares...") + + # Test case 1 + try: + var d1 = Decimal("2") + var expected_prefix1 = "1.414" + var result1 = sqrt(d1) + var result_str1 = String(result1) + testing.assert_true( + result_str1.startswith(expected_prefix1), + "sqrt(" + + String(d1) + + ") should start with " + + expected_prefix1 + + ", got " + + result_str1, + ) + except e: + print("ERROR in test_non_perfect_squares case 1: sqrt(2) ≈ 1.414...") + raise e + + # Test case 2 + var d2 = Decimal("3") + var expected_prefix2 = "1.732" + var result2 = sqrt(d2) + var result_str2 = String(result2) + testing.assert_true( + result_str2.startswith(expected_prefix2), + "sqrt(" + + String(d2) + + ") should start with " + + expected_prefix2 + + ", got " + + result_str2, + ) + + # Test case 3 + var d3 = Decimal("5") + var expected_prefix3 = "2.236" + var result3 = sqrt(d3) + var result_str3 = String(result3) + testing.assert_true( + result_str3.startswith(expected_prefix3), + "sqrt(" + + String(d3) + + ") should start with " + + expected_prefix3 + + ", got " + + result_str3, + ) + + # Test case 4 + var d4 = Decimal("10") + var expected_prefix4 = "3.162" + var result4 = sqrt(d4) + var result_str4 = String(result4) + testing.assert_true( + result_str4.startswith(expected_prefix4), + "sqrt(" + + String(d4) + + ") should start with " + + expected_prefix4 + + ", got " + + result_str4, + ) + + # Test case 5 + var d5 = Decimal("50") + var expected_prefix5 = "7.071" + var result5 = sqrt(d5) + var result_str5 = String(result5) + testing.assert_true( + result_str5.startswith(expected_prefix5), + "sqrt(" + + String(d5) + + ") should start with " + + expected_prefix5 + + ", got " + + result_str5, + ) + + # Test case 6 + var d6 = Decimal("99") + var expected_prefix6 = "9.949" + var result6 = sqrt(d6) + var result_str6 = String(result6) + testing.assert_true( + result_str6.startswith(expected_prefix6), + "sqrt(" + + String(d6) + + ") should start with " + + expected_prefix6 + + ", got " + + result_str6, + ) + + # Test case 7 + var d7 = Decimal("999") + var expected_prefix7 = "31.60" + var result7 = sqrt(d7) + var result_str7 = String(result7) + testing.assert_true( + result_str7.startswith(expected_prefix7), + "sqrt(" + + String(d7) + + ") should start with " + + expected_prefix7 + + ", got " + + result_str7, + ) + + print("Non-perfect square tests passed!") + + +fn test_decimal_values() raises: + print("Testing square root of decimal values...") + + # Test case 1 + try: + var d1 = Decimal("0.25") + var expected1 = "0.5" + var result1 = sqrt(d1) + testing.assert_equal( + String(result1), + expected1, + "sqrt(" + String(d1) + ") should be " + expected1, + ) + except e: + print("ERROR in test_decimal_values case 1: sqrt(0.25) = 0.5") + raise e + + # Test case 2 + var d2 = Decimal("0.09") + var expected2 = "0.3" + var result2 = sqrt(d2) + testing.assert_equal( + String(result2), + expected2, + "sqrt(" + String(d2) + ") should be " + expected2, + ) + + # Test case 3 + var d3 = Decimal("0.04") + var expected3 = "0.2" + var result3 = sqrt(d3) + testing.assert_equal( + String(result3), + expected3, + "sqrt(" + String(d3) + ") should be " + expected3, + ) + + # Test case 4 + var d4 = Decimal("0.01") + var expected4 = "0.1" + var result4 = sqrt(d4) + testing.assert_equal( + String(result4), + expected4, + "sqrt(" + String(d4) + ") should be " + expected4, + ) + + # Test case 5 + var d5 = Decimal("1.44") + var expected5 = "1.2" + var result5 = sqrt(d5) + testing.assert_equal( + String(result5), + expected5, + "sqrt(" + String(d5) + ") should be " + expected5, + ) + + # Test case 6 + var d6 = Decimal("2.25") + var expected6 = "1.5" + var result6 = sqrt(d6) + testing.assert_equal( + String(result6), + expected6, + "sqrt(" + String(d6) + ") should be " + expected6, + ) + + # Test case 7 + var d7 = Decimal("6.25") + var expected7 = "2.5" + var result7 = sqrt(d7) + testing.assert_equal( + String(result7), + expected7, + "sqrt(" + String(d7) + ") should be " + expected7, + ) + + print("Decimal value tests passed!") + + +fn test_edge_cases() raises: + print("Testing edge cases...") + + # Test sqrt(0) = 0 + try: + var zero = Decimal("0") + var result_zero = sqrt(zero) + testing.assert_equal(String(result_zero), "0", "sqrt(0) should be 0") + except e: + print("ERROR in test_edge_cases: sqrt(0) = 0") + raise e + + # Test sqrt(1) = 1 + try: + var one = Decimal("1") + var result_one = sqrt(one) + testing.assert_equal(String(result_one), "1", "sqrt(1) should be 1") + except e: + print("ERROR in test_edge_cases: sqrt(1) = 1") + raise e + + # Test very small positive number + try: + var very_small = Decimal( + "0." + "0" * 27 + "1" + ) # Smallest possible positive decimal + var result_small = sqrt(very_small) + testing.assert_true( + String(result_small).startswith("0.00000000000001"), + String( + "sqrt of very small number should be positive and smaller," + " very_small={}, result_small={}" + ).format(String(very_small), String(result_small)), + ) + except e: + print("ERROR in test_edge_cases: sqrt of very small number") + raise e + + # Test very large number + try: + var very_large = Decimal("1" + "0" * 27) # Large decimal + var result_large = sqrt(very_large) + testing.assert_true( + String(result_large).startswith("3162277"), + "sqrt of 10^27 should start with 3162277...", + ) + except e: + print("ERROR in test_edge_cases: sqrt of very large number (10^27)") + raise e + + # Test negative number exception + var negative = Decimal("-1") + var negative_exception_caught = False + try: + var result_negative = sqrt(negative) + testing.assert_equal( + True, False, "sqrt() of negative should raise exception" + ) + except: + negative_exception_caught = True + + try: + testing.assert_equal( + negative_exception_caught, + True, + "sqrt() of negative correctly raised exception", + ) + except e: + print( + "ERROR in test_edge_cases: sqrt of negative number didn't raise" + " exception properly" + ) + raise e + + print("Edge cases tests passed!") + + +fn test_precision() raises: + print("Testing precision of square root calculations...") + var expected_sqrt2 = "1.4142135623" # First 10 decimal places of sqrt(2) + + # Test precision for irrational numbers + try: + var two = Decimal("2") + var result = sqrt(two) + + # Check at least 10 decimal places (should be enough for most applications) + testing.assert_true( + String(result).startswith(expected_sqrt2), + "sqrt(2) should start with " + expected_sqrt2, + ) + except e: + print("ERROR in test_precision: sqrt(2) precision check") + raise e + + # Test high precision values + try: + var precise_value = Decimal("2.0000000000000000000000000") + var precise_result = sqrt(precise_value) + testing.assert_true( + String(precise_result).startswith(expected_sqrt2), + "sqrt of high precision 2 should start with " + expected_sqrt2, + ) + except e: + print("ERROR in test_precision: high precision sqrt(2)") + raise e + + # Check that results are appropriately rounded + try: + var d = Decimal("10") + var sqrt_d = sqrt(d) + var expected_places = 7 # Typical minimum precision + testing.assert_true( + sqrt_d.scale() >= expected_places, + "sqrt(10) should have at least " + + String(expected_places) + + " decimal places", + ) + except e: + print("ERROR in test_precision: sqrt(10) scale check") + raise e + + print("Precision tests passed!") + + +fn test_mathematical_identities() raises: + print("Testing mathematical identities...") + + # Test that sqrt(x)² = x - Expanded for each test number + # Test number 1 + try: + var num1 = Decimal("2") + var sqrt_num1 = sqrt(num1) + var squared1 = sqrt_num1 * sqrt_num1 + var original_rounded1 = round(num1, 10) + var squared_rounded1 = round(squared1, 10) + testing.assert_true( + original_rounded1 == squared_rounded1, + "sqrt(" + + String(num1) + + ")² should approximately equal " + + String(num1) + + ", but got " + + String(squared_rounded1), + ) + except e: + print("ERROR in test_mathematical_identities: sqrt(2)² = 2") + raise e + + # Test number 2 + var num2 = Decimal("3") + var sqrt_num2 = sqrt(num2) + var squared2 = sqrt_num2 * sqrt_num2 + var original_rounded2 = round(num2, 10) + var squared_rounded2 = round(squared2, 10) + testing.assert_true( + original_rounded2 == squared_rounded2, + "sqrt(" + + String(num2) + + ")² should approximately equal " + + String(num2), + ) + + # Test number 3 + var num3 = Decimal("5") + var sqrt_num3 = sqrt(num3) + var squared3 = sqrt_num3 * sqrt_num3 + var original_rounded3 = round(num3, 10) + var squared_rounded3 = round(squared3, 10) + testing.assert_true( + original_rounded3 == squared_rounded3, + "sqrt(" + + String(num3) + + ")² should approximately equal " + + String(num3), + ) + + # Test number 4 + var num4 = Decimal("7") + var sqrt_num4 = sqrt(num4) + var squared4 = sqrt_num4 * sqrt_num4 + var original_rounded4 = round(num4, 10) + var squared_rounded4 = round(squared4, 10) + testing.assert_true( + original_rounded4 == squared_rounded4, + "sqrt(" + + String(num4) + + ")² should approximately equal " + + String(num4), + ) + + # Test number 5 + var num5 = Decimal("10") + var sqrt_num5 = sqrt(num5) + var squared5 = sqrt_num5 * sqrt_num5 + var original_rounded5 = round(num5, 10) + var squared_rounded5 = round(squared5, 10) + testing.assert_true( + original_rounded5 == squared_rounded5, + "sqrt(" + + String(num5) + + ")² should approximately equal " + + String(num5), + ) + + # Test number 6 + var num6 = Decimal("0.5") + var sqrt_num6 = sqrt(num6) + var squared6 = sqrt_num6 * sqrt_num6 + var original_rounded6 = round(num6, 10) + var squared_rounded6 = round(squared6, 10) + testing.assert_true( + original_rounded6 == squared_rounded6, + "sqrt(" + + String(num6) + + ")² should approximately equal " + + String(num6), + ) + + # Test number 7 + var num7 = Decimal("0.25") + var sqrt_num7 = sqrt(num7) + var squared7 = sqrt_num7 * sqrt_num7 + var original_rounded7 = round(num7, 10) + var squared_rounded7 = round(squared7, 10) + testing.assert_true( + original_rounded7 == squared_rounded7, + "sqrt(" + + String(num7) + + ")² should approximately equal " + + String(num7), + ) + + # Test number 8 + var num8 = Decimal("1.44") + var sqrt_num8 = sqrt(num8) + var squared8 = sqrt_num8 * sqrt_num8 + var original_rounded8 = round(num8, 10) + var squared_rounded8 = round(squared8, 10) + testing.assert_true( + original_rounded8 == squared_rounded8, + "sqrt(" + + String(num8) + + ")² should approximately equal " + + String(num8), + ) + + # Test that sqrt(x*y) = sqrt(x) * sqrt(y) - Expanded for each pair + # Pair 1: 4 and 9 + try: + var x1 = Decimal("4") + var y1 = Decimal("9") + var product1 = x1 * y1 + var sqrt_product1 = sqrt(product1) + var sqrt_x1 = sqrt(x1) + var sqrt_y1 = sqrt(y1) + var sqrt_product_separate1 = sqrt_x1 * sqrt_y1 + var sqrt_product_rounded1 = round(sqrt_product1, 10) + var sqrt_product_separate_rounded1 = round(sqrt_product_separate1, 10) + testing.assert_true( + sqrt_product_rounded1 == sqrt_product_separate_rounded1, + "sqrt(" + + String(x1) + + "*" + + String(y1) + + ") should equal sqrt(" + + String(x1) + + ") * sqrt(" + + String(y1) + + ")", + ) + except e: + print( + "ERROR in test_mathematical_identities: sqrt(4*9) = sqrt(4) *" + " sqrt(9)" + ) + raise e + + # Pair 2: 16 and 25 + var x2 = Decimal("16") + var y2 = Decimal("25") + var product2 = x2 * y2 + var sqrt_product2 = sqrt(product2) + var sqrt_x2 = sqrt(x2) + var sqrt_y2 = sqrt(y2) + var sqrt_product_separate2 = sqrt_x2 * sqrt_y2 + var sqrt_product_rounded2 = round(sqrt_product2, 10) + var sqrt_product_separate_rounded2 = round(sqrt_product_separate2, 10) + testing.assert_true( + sqrt_product_rounded2 == sqrt_product_separate_rounded2, + "sqrt(" + + String(x2) + + "*" + + String(y2) + + ") should equal sqrt(" + + String(x2) + + ") * sqrt(" + + String(y2) + + ")", + ) + + # Pair 3: 2 and 8 + var x3 = Decimal("2") + var y3 = Decimal("8") + var product3 = x3 * y3 + var sqrt_product3 = sqrt(product3) + var sqrt_x3 = sqrt(x3) + var sqrt_y3 = sqrt(y3) + var sqrt_product_separate3 = sqrt_x3 * sqrt_y3 + var sqrt_product_rounded3 = round(sqrt_product3, 10) + var sqrt_product_separate_rounded3 = round(sqrt_product_separate3, 10) + testing.assert_true( + sqrt_product_rounded3 == sqrt_product_separate_rounded3, + "sqrt(" + + String(x3) + + "*" + + String(y3) + + ") should equal sqrt(" + + String(x3) + + ") * sqrt(" + + String(y3) + + ")", + ) + + print("Mathematical identity tests passed!") + + +fn test_sqrt_performance() raises: + print("Testing square root performance and convergence...") + + # Test case 1 + try: + var num1 = Decimal("0.0001") + var result1 = sqrt(num1) + var squared1 = result1 * result1 + var diff1 = squared1 - num1 + diff1 = -diff1 if diff1.is_negative() else diff1 + var rel_diff1 = diff1 / num1 + var diff_float1 = Float64(String(rel_diff1)) + testing.assert_true( + diff_float1 < 0.00001, + "Square root calculation for " + + String(num1) + + " should be accurate within 0.001%", + ) + except e: + print("ERROR in test_sqrt_performance case 1: small number 0.0001") + raise e + + # Test case 2 + var num2 = Decimal("0.01") + var result2 = sqrt(num2) + var squared2 = result2 * result2 + var diff2 = squared2 - num2 + diff2 = -diff2 if diff2.is_negative() else diff2 + var rel_diff2 = diff2 / num2 + var diff_float2 = Float64(String(rel_diff2)) + testing.assert_true( + diff_float2 < 0.00001, + "Square root calculation for " + + String(num2) + + " should be accurate within 0.001%", + ) + + # Test case 3 + var num3 = Decimal("1") + var result3 = sqrt(num3) + var squared3 = result3 * result3 + var diff3 = squared3 - num3 + diff3 = -diff3 if diff3.is_negative() else diff3 + var rel_diff3 = diff3 / num3 + var diff_float3 = Float64(String(rel_diff3)) + testing.assert_true( + diff_float3 < 0.00001, + "Square root calculation for " + + String(num3) + + " should be accurate within 0.001%", + ) + + # Test case 4 + var num4 = Decimal("10") + var result4 = sqrt(num4) + var squared4 = result4 * result4 + var diff4 = squared4 - num4 + diff4 = -diff4 if diff4.is_negative() else diff4 + var rel_diff4 = diff4 / num4 + var diff_float4 = Float64(String(rel_diff4)) + testing.assert_true( + diff_float4 < 0.00001, + "Square root calculation for " + + String(num4) + + " should be accurate within 0.001%", + ) + + # Test case 5 + var num5 = Decimal("10000") + var result5 = sqrt(num5) + var squared5 = result5 * result5 + var diff5 = squared5 - num5 + diff5 = -diff5 if diff5.is_negative() else diff5 + var rel_diff5 = diff5 / num5 + var diff_float5 = Float64(String(rel_diff5)) + testing.assert_true( + diff_float5 < 0.00001, + "Square root calculation for " + + String(num5) + + " should be accurate within 0.001%", + ) + + # Test case 6 + var num6 = Decimal("10000000000") + var result6 = sqrt(num6) + var squared6 = result6 * result6 + var diff6 = squared6 - num6 + diff6 = -diff6 if diff6.is_negative() else diff6 + var rel_diff6 = diff6 / num6 + var diff_float6 = Float64(String(rel_diff6)) + testing.assert_true( + diff_float6 < 0.00001, + "Square root calculation for " + + String(num6) + + " should be accurate within 0.001%", + ) + + # Test case 7 + var num7 = Decimal("0.999999999") + var result7 = sqrt(num7) + var squared7 = result7 * result7 + var diff7 = squared7 - num7 + diff7 = -diff7 if diff7.is_negative() else diff7 + var rel_diff7 = diff7 / num7 + var diff_float7 = Float64(String(rel_diff7)) + testing.assert_true( + diff_float7 < 0.00001, + "Square root calculation for " + + String(num7) + + " should be accurate within 0.001%", + ) + + # Test case 8 + var num8 = Decimal("1.000000001") + var result8 = sqrt(num8) + var squared8 = result8 * result8 + var diff8 = squared8 - num8 + diff8 = -diff8 if diff8.is_negative() else diff8 + var rel_diff8 = diff8 / num8 + var diff_float8 = Float64(String(rel_diff8)) + testing.assert_true( + diff_float8 < 0.00001, + "Square root calculation for " + + String(num8) + + " should be accurate within 0.001%", + ) + + # Test case 9 + var num9 = Decimal("3.999999999") + var result9 = sqrt(num9) + var squared9 = result9 * result9 + var diff9 = squared9 - num9 + diff9 = -diff9 if diff9.is_negative() else diff9 + var rel_diff9 = diff9 / num9 + var diff_float9 = Float64(String(rel_diff9)) + testing.assert_true( + diff_float9 < 0.00001, + "Square root calculation for " + + String(num9) + + " should be accurate within 0.001%", + ) + + # Test case 10 + var num10 = Decimal("4.000000001") + var result10 = sqrt(num10) + var squared10 = result10 * result10 + # Using manual absolute difference calculation + var diff10 = squared10 - num10 + diff10 = -diff10 if diff10.is_negative() else diff10 + var rel_diff10 = diff10 / num10 + testing.assert_true( + rel_diff10 < Decimal("0.00001"), + "Square root calculation for " + + String(num10) + + " should be accurate within 0.001%", + ) + + print("Performance and convergence 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: + test_fn() + print("✓ " + test_name + " passed\n") + except e: + print("✗ " + test_name + " FAILED!") + print("Error message: " + String(e)) + raise e + + +fn main() raises: + print("Running comprehensive Decimal square root tests") + + run_test_with_error_handling(test_perfect_squares, "Perfect squares test") + run_test_with_error_handling( + test_non_perfect_squares, "Non-perfect squares test" + ) + run_test_with_error_handling(test_decimal_values, "Decimal values 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_identities, "Mathematical identities test" + ) + run_test_with_error_handling( + test_sqrt_performance, "Performance and convergence test" + ) + + print("All square root tests passed!") diff --git a/tests/test_string_based_operations.mojo b/tests/test_string_based_operations.mojo new file mode 100644 index 00000000..05ff94fd --- /dev/null +++ b/tests/test_string_based_operations.mojo @@ -0,0 +1,630 @@ +""" +Comprehensive tests for string-based decimal operations. +Tests addition and subtraction functions with 50 test cases each. +""" +from decimojo import Decimal +from decimojo.mathematics import _addition_string_based as addition +from decimojo.mathematics import _subtraction_string_based as subtraction +import testing + + +fn test_addition_function() raises: + print("Testing string-based addition function...") + + # Array to store all test cases + var test_cases = List[Tuple[Decimal, Decimal, String]]() + + # Category 1: Basic addition with simple positive numbers + test_cases.append((Decimal(String("1")), Decimal(String("2")), String("3"))) + test_cases.append( + (Decimal(String("10")), Decimal(String("20")), String("30")) + ) + test_cases.append( + (Decimal(String("123.45")), Decimal(String("67.89")), String("191.34")) + ) + test_cases.append( + (Decimal(String("999")), Decimal(String("1")), String("1000")) + ) + test_cases.append( + (Decimal(String("0.5")), Decimal(String("0.5")), String("1.0")) + ) + + # Category 2: Addition with negative numbers + test_cases.append( + (Decimal(String("-1")), Decimal(String("2")), String("1")) + ) + test_cases.append( + (Decimal(String("1")), Decimal(String("-2")), String("-1")) + ) + test_cases.append( + (Decimal(String("-10")), Decimal(String("-20")), String("-30")) + ) + test_cases.append( + ( + Decimal(String("-123.45")), + Decimal(String("-67.89")), + String("-191.34"), + ) + ) + test_cases.append( + (Decimal(String("-0.5")), Decimal(String("0.5")), String("0.0")) + ) + + # Category 3: Addition with zeros + test_cases.append((Decimal(String("0")), Decimal(String("0")), String("0"))) + test_cases.append( + (Decimal(String("0")), Decimal(String("123.45")), String("123.45")) + ) + test_cases.append( + (Decimal(String("123.45")), Decimal(String("0")), String("123.45")) + ) + test_cases.append( + (Decimal(String("0")), Decimal(String("-123.45")), String("-123.45")) + ) + test_cases.append( + (Decimal(String("-123.45")), Decimal(String("0")), String("-123.45")) + ) + + # Category 4: Addition with different scales/decimal places + test_cases.append( + (Decimal(String("1.23")), Decimal(String("4.567")), String("5.797")) + ) + test_cases.append( + (Decimal(String("10.1")), Decimal(String("0.01")), String("10.11")) + ) + test_cases.append( + (Decimal(String("0.001")), Decimal(String("0.002")), String("0.003")) + ) + test_cases.append( + (Decimal(String("123.4")), Decimal(String("5.67")), String("129.07")) + ) + test_cases.append( + (Decimal(String("1.000")), Decimal(String("2.00")), String("3.000")) + ) + + # Category 5: Addition with very large numbers + test_cases.append( + ( + Decimal(String("1000000000")), + Decimal(String("2000000000")), + String("3000000000"), + ) + ) + test_cases.append( + ( + Decimal(String("9") * 20), + Decimal(String("1")), + String("1") + String("0") * 20, + ) + ) + test_cases.append( + ( + Decimal(String("999999999999")), + Decimal(String("1")), + String("1000000000000"), + ) + ) + test_cases.append( + ( + Decimal(String("9999999999")), + Decimal(String("9999999999")), + String("19999999998"), + ) + ) + test_cases.append( + ( + Decimal(String("123456789012345")), + Decimal(String("987654321098765")), + String("1111111110111110"), + ) + ) + + # Category 6: Addition with very small numbers + test_cases.append( + ( + Decimal(String("0.000000001")), + Decimal(String("0.000000002")), + String("0.000000003"), + ) + ) + test_cases.append( + ( + Decimal(String("0.") + String("0") * 20 + String("1")), + Decimal(String("0.") + String("0") * 20 + String("2")), + String("0.") + String("0") * 20 + String("3"), + ) + ) + test_cases.append( + ( + Decimal(String("0.000000001")), + Decimal(String("1")), + String("1.000000001"), + ) + ) + test_cases.append( + ( + Decimal(String("0.") + String("0") * 27 + String("1")), + Decimal(String("0.") + String("0") * 27 + String("9")), + String("0.0000000000000000000000000010"), + ) + ) + test_cases.append( + ( + Decimal(String("0.") + String("0") * 10 + String("1")), + Decimal(String("0.") + String("0") * 15 + String("1")), + String("0.0000000000100001"), + ) + ) + + # Category 7: Addition with numbers that have many digits + test_cases.append( + ( + Decimal(String("1.23456789")), + Decimal(String("9.87654321")), + String("11.11111110"), + ) + ) + test_cases.append( + ( + Decimal(String("1.111111111111111")), + Decimal(String("2.222222222222222")), + String("3.333333333333333"), + ) + ) + test_cases.append( + ( + Decimal(String("3.14159265358979323846")), + Decimal(String("2.71828182845904523536")), + String("5.85987448204883847382"), + ) + ) + test_cases.append( + ( + Decimal(String("1.234567890123456789")), + Decimal(String("9.876543210987654321")), + String("11.111111101111111110"), + ) + ) + test_cases.append( + ( + Decimal(String("0.1234567890123456789")), + Decimal(String("0.9876543210987654321")), + String("1.1111111101111111110"), + ) + ) + + # Category 8: Addition where results require carries + test_cases.append( + (Decimal(String("9.9")), Decimal(String("0.1")), String("10.0")) + ) + test_cases.append( + (Decimal(String("9.99")), Decimal(String("0.01")), String("10.00")) + ) + test_cases.append( + ( + Decimal(String("9") * 10), + Decimal(String("1")), + String("10000000000"), + ) + ) + test_cases.append( + ( + Decimal(String("9.99999")), + Decimal(String("0.00001")), + String("10.00000"), + ) + ) + test_cases.append( + ( + Decimal(String("999.999")), + Decimal(String("0.001")), + String("1000.000"), + ) + ) + + # Category 9: Edge cases and boundary values + test_cases.append( + ( + Decimal(String("0.0000000000000000000000000001")), + Decimal(String("0.0000000000000000000000000009")), + String("0.0000000000000000000000000010"), + ) + ) + test_cases.append( + ( + Decimal(String("0.49999999")), + Decimal(String("0.50000001")), + String("1.00000000"), + ) + ) + test_cases.append( + ( + Decimal(String("1") + String("0") * 20), + Decimal(String("0.") + String("0") * 20 + String("1")), + String("1") + + String("0") * 20 + + String(".") + + String("0") * 20 + + String("1"), + ) + ) + test_cases.append( + ( + Decimal(String("9") * 10 + String(".") + String("9") * 10), + Decimal(String("0.") + String("0") * 9 + String("1")), + String("10000000000.0000000000"), + ) + ) + + # Category 10: Addition where sign changes + test_cases.append( + (Decimal(String("-1")), Decimal(String("1")), String("0")) + ) + test_cases.append( + (Decimal(String("-10")), Decimal(String("20")), String("10")) + ) + test_cases.append( + (Decimal(String("-100")), Decimal(String("50")), String("-50")) + ) + test_cases.append( + (Decimal(String("-0.001")), Decimal(String("0.002")), String("0.001")) + ) + test_cases.append( + (Decimal(String("-9.99")), Decimal(String("10")), String("0.01")) + ) + + # Execute all test cases + var passed_count = 0 + var failed_count = 0 + + for i in range(len(test_cases)): + var test_case = test_cases[i] + var a = test_case[0] + var b = test_case[1] + var expected = test_case[2] + var result = addition(a, b) + + try: + if result == expected: + passed_count += 1 + else: + failed_count += 1 + print(String("❌ Addition test case {} failed:").format(i + 1)) + print(a, " + ", b) + print(String(" Expected: {}").format(expected)) + print(String(" Got: {}").format(result)) + except e: + failed_count += 1 + print( + String( + "❌ Addition test case {} raised an exception: {}" + ).format(i + 1, e) + ) + + print( + String("Addition tests: {} passed, {} failed").format( + passed_count, failed_count + ) + ) + testing.assert_equal( + failed_count, 0, String("All addition tests should pass") + ) + + +fn test_subtraction_function() raises: + print(String("Testing string-based subtraction function...")) + + # Array to store all test cases + var test_cases = List[Tuple[Decimal, Decimal, String]]() + + # Category 1: Basic subtraction with simple positive numbers + test_cases.append((Decimal(String("3")), Decimal(String("2")), String("1"))) + test_cases.append( + (Decimal(String("10")), Decimal(String("5")), String("5")) + ) + test_cases.append( + (Decimal(String("123.45")), Decimal(String("23.45")), String("100.00")) + ) + test_cases.append( + (Decimal(String("1000")), Decimal(String("1")), String("999")) + ) + test_cases.append( + (Decimal(String("5.5")), Decimal(String("0.5")), String("5.0")) + ) + + # Category 2: Subtraction with negative numbers + test_cases.append( + (Decimal(String("-1")), Decimal(String("2")), String("-3")) + ) + test_cases.append( + (Decimal(String("1")), Decimal(String("-2")), String("3")) + ) + test_cases.append( + (Decimal(String("-10")), Decimal(String("-5")), String("-5")) + ) + test_cases.append( + (Decimal(String("-10")), Decimal(String("-20")), String("10")) + ) + test_cases.append( + (Decimal(String("-100")), Decimal(String("-50")), String("-50")) + ) + + # Category 3: Subtraction with zeros + test_cases.append((Decimal(String("0")), Decimal(String("0")), String("0"))) + test_cases.append( + (Decimal(String("0")), Decimal(String("123.45")), String("-123.45")) + ) + test_cases.append( + (Decimal(String("123.45")), Decimal(String("0")), String("123.45")) + ) + test_cases.append( + (Decimal(String("0")), Decimal(String("-123.45")), String("123.45")) + ) + test_cases.append( + (Decimal(String("-123.45")), Decimal(String("0")), String("-123.45")) + ) + + # Category 4: Subtraction with different scales/decimal places + test_cases.append( + (Decimal(String("5.67")), Decimal(String("1.2")), String("4.47")) + ) + test_cases.append( + (Decimal(String("10.1")), Decimal(String("0.01")), String("10.09")) + ) + test_cases.append( + (Decimal(String("0.003")), Decimal(String("0.002")), String("0.001")) + ) + test_cases.append( + (Decimal(String("123.4")), Decimal(String("0.4")), String("123.0")) + ) + test_cases.append( + (Decimal(String("1.000")), Decimal(String("0.999")), String("0.001")) + ) + + # Category 5: Subtraction with very large numbers + test_cases.append( + ( + Decimal(String("3000000000")), + Decimal(String("1000000000")), + String("2000000000"), + ) + ) + test_cases.append( + ( + Decimal(String("1") + String("0") * 20), + Decimal(String("1")), + String("9") * 19 + String("9"), + ) + ) + test_cases.append( + ( + Decimal(String("10000000000")), + Decimal(String("1")), + String("9999999999"), + ) + ) + test_cases.append( + ( + Decimal(String("19999999998")), + Decimal(String("9999999999")), + String("9999999999"), + ) + ) + test_cases.append( + ( + Decimal(String("10000000000")), + Decimal(String("1")), + String("9999999999"), + ) + ) + + # Category 6: Subtraction with very small numbers + test_cases.append( + ( + Decimal(String("0.000000003")), + Decimal(String("0.000000001")), + String("0.000000002"), + ) + ) + test_cases.append( + ( + Decimal(String("0.") + String("0") * 20 + String("3")), + Decimal(String("0.") + String("0") * 20 + String("1")), + String("0.") + String("0") * 20 + String("2"), + ) + ) + test_cases.append( + ( + Decimal(String("1.000000001")), + Decimal(String("0.000000001")), + String("1.000000000"), + ) + ) + test_cases.append( + ( + Decimal(String("0.") + String("0") * 27 + String("9")), + Decimal(String("0.") + String("0") * 27 + String("1")), + String("0.") + String("0") * 27 + String("8"), + ) + ) + test_cases.append( + ( + Decimal(String("0.") + String("0") * 10 + String("5")), + Decimal(String("0.") + String("0") * 15 + String("1")), + String("0.0000000000499999"), + ) + ) + + # Category 7: Subtraction where results require borrows + test_cases.append( + (Decimal(String("10")), Decimal(String("0.1")), String("9.9")) + ) + test_cases.append( + (Decimal(String("10")), Decimal(String("0.01")), String("9.99")) + ) + test_cases.append( + ( + Decimal(String("1") + String("0") * 10), + Decimal(String("1")), + String("9999999999"), + ) + ) + test_cases.append( + (Decimal(String("10")), Decimal(String("0.00001")), String("9.99999")) + ) + test_cases.append( + (Decimal(String("1000")), Decimal(String("0.001")), String("999.999")) + ) + + # Category 8: Cases where the result changes sign + test_cases.append( + (Decimal(String("1")), Decimal(String("2")), String("-1")) + ) + test_cases.append( + (Decimal(String("0.5")), Decimal(String("1.5")), String("-1.0")) + ) + test_cases.append( + (Decimal(String("100")), Decimal(String("200")), String("-100")) + ) + test_cases.append( + (Decimal(String("0.001")), Decimal(String("0.002")), String("-0.001")) + ) + test_cases.append( + (Decimal(String("9.99")), Decimal(String("10")), String("-0.01")) + ) + + # Category 9: Edge cases and boundary values + test_cases.append( + ( + Decimal(String("0.0000000000000000000000000009")), + Decimal(String("0.0000000000000000000000000001")), + String("0.0000000000000000000000000008"), + ) + ) + test_cases.append( + ( + Decimal(String("1.00000000")), + Decimal(String("0.49999999")), + String("0.50000001"), + ) + ) + test_cases.append( + ( + Decimal( + String("1") + + String("0") * 20 + + String(".") + + String("0") * 20 + + String("1") + ), + Decimal(String("0.") + String("0") * 20 + String("1")), + String("99999999999999999999.999999999999999999999"), + ) + ) + test_cases.append( + ( + Decimal( + String("1") + + String("0") * 9 + + String(".") + + String("0") * 9 + + String("1") + ), + Decimal(String("0.") + String("0") * 9 + String("1")), + String("1000000000.0000000000"), + ) + ) + + # Category 10: Subtracting nearly equal numbers + test_cases.append((Decimal(String("1")), Decimal(String("1")), String("0"))) + test_cases.append( + (Decimal(String("1.0001")), Decimal(String("1")), String("0.0001")) + ) + test_cases.append( + ( + Decimal(String("1.0000001")), + Decimal(String("1")), + String("0.0000001"), + ) + ) + test_cases.append( + ( + Decimal(String("123456789.000000001")), + Decimal(String("123456789")), + String("0.000000001"), + ) + ) + test_cases.append( + ( + Decimal(String("0.000000002")), + Decimal(String("0.000000001")), + String("0.000000001"), + ) + ) + + # Execute all test cases + var passed_count = 0 + var failed_count = 0 + + for i in range(len(test_cases)): + var test_case = test_cases[i] + var a = test_case[0] + var b = test_case[1] + var expected = test_case[2] + var result = subtraction(a, b) + + try: + if result == expected: + passed_count += 1 + else: + failed_count += 1 + print( + String("❌ Subtraction test case {} failed:").format(i + 1) + ) + print(a, " - ", b) + print(String(" Expected: {}").format(expected)) + print(String(" Got: {}").format(result)) + except e: + failed_count += 1 + print( + String( + "❌ Subtraction test case {} raised an exception: {}".format( + i + 1, e + ) + ) + ) + + print( + String( + "Subtraction tests: {} passed, {} failed".format( + passed_count, failed_count + ) + ) + ) + testing.assert_equal( + failed_count, 0, String("All subtraction tests should pass") + ) + + +fn main() raises: + print( + String( + "Running comprehensive tests for string-based decimal operations" + ) + ) + print( + String( + "=============================================================\n" + ) + ) + + test_addition_function() + print(String("\n")) + test_subtraction_function() + + print( + String( + "\n=============================================================" + ) + ) + print(String("All tests completed!"))