From 293f1ed3774286fa57744ba5db8a13b8e7305f86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?ZHU=20Yuhao=20=E6=9C=B1=E5=AE=87=E6=B5=A9?= Date: Fri, 28 Mar 2025 20:01:58 +0100 Subject: [PATCH 1/2] BigDecimal --- src/decimojo/__init__.mojo | 3 +- src/decimojo/bigdecimal/__init__.mojo | 0 src/decimojo/bigdecimal/bigdecimal.mojo | 179 ++++++++++++++++++++++++ src/decimojo/bigint/bigint.mojo | 41 ++---- src/decimojo/biguint/biguint.mojo | 15 +- 5 files changed, 198 insertions(+), 40 deletions(-) create mode 100644 src/decimojo/bigdecimal/__init__.mojo create mode 100644 src/decimojo/bigdecimal/bigdecimal.mojo diff --git a/src/decimojo/__init__.mojo b/src/decimojo/__init__.mojo index de599c23..403fc6f4 100644 --- a/src/decimojo/__init__.mojo +++ b/src/decimojo/__init__.mojo @@ -27,7 +27,8 @@ from decimojo import Decimal, BigInt, RoundingMode # Core types from .decimal.decimal import Decimal from .bigint.bigint import BigInt, BInt -from .biguint.biguint import BigUInt +from .biguint.biguint import BigUInt, BUInt +from .bigdecimal.bigdecimal import BigDecimal from .rounding_mode import RoundingMode # Core functions diff --git a/src/decimojo/bigdecimal/__init__.mojo b/src/decimojo/bigdecimal/__init__.mojo new file mode 100644 index 00000000..e69de29b diff --git a/src/decimojo/bigdecimal/bigdecimal.mojo b/src/decimojo/bigdecimal/bigdecimal.mojo new file mode 100644 index 00000000..0972e66b --- /dev/null +++ b/src/decimojo/bigdecimal/bigdecimal.mojo @@ -0,0 +1,179 @@ +# ===----------------------------------------------------------------------=== # +# Copyright 2025 Yuhao Zhu +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ===----------------------------------------------------------------------=== # + +"""Implements basic object methods for the BigDecimal type. + +This module contains the basic object methods for the BigDecimal type. +These methods include constructors, life time methods, output dunders, +type-transfer dunders, basic arithmetic operation dunders, comparison +operation dunders, and other dunders that implement traits, as well as +mathematical methods that do not implement a trait. +""" + +from memory import UnsafePointer +import testing + +from decimojo.rounding_mode import RoundingMode + + +@value +struct BigDecimal: + """Represents a arbitrary-precision decimal. + + Notes: + + Internal Representation: + + - A base-10 unsigned integer (BigUInt) for magnitude. + - A Int value for the scale + - A Bool value for the sign. + + Final value: + (-1)**sign * magnitude * 10^(-scale) + """ + + # ===------------------------------------------------------------------=== # + # Organization of fields and methods: + # - Internal representation fields + # - Constants (aliases) + # - Special values (methods) + # - Constructors and life time methods + # - Constructing methods that are not dunders + # - Output dunders, type-transfer dunders, and other type-transfer methods + # - Basic unary arithmetic operation dunders + # - Basic binary arithmetic operation dunders + # - Basic binary arithmetic operation dunders with reflected operands + # - Basic binary augmented arithmetic operation dunders + # - Basic comparison operation dunders + # - Other dunders that implements traits + # - Mathematical methods that do not implement a trait (not a dunder) + # - Other methods + # - Internal methods + # ===------------------------------------------------------------------=== # + + # Internal representation fields + var magnitude: BigUInt + """The magnitude of the BigDecimal.""" + var scale: Int + """The scale of the BigDecimal.""" + var sign: Bool + """Sign information.""" + + # ===------------------------------------------------------------------=== # + # Constructors and life time dunder methods + # ===------------------------------------------------------------------=== # + + fn __init__(out self, magnitude: BigUInt, scale: Int, sign: Bool) raises: + """Constructs a BigDecimal from its components.""" + self.magnitude = magnitude + self.scale = scale + self.sign = sign + + # ===------------------------------------------------------------------=== # + # Constructing methods that are not dunders + # from_string(value: String) -> Self + # ===------------------------------------------------------------------=== # + + @staticmethod + fn from_string(value: String) raises -> Self: + """Initializes a BigDecimal from a string representation. + The string is normalized with `deciomojo.str.parse_numeric_string()`. + + Args: + value: The string representation of the BigDecimal. + + Returns: + The BigDecimal representation of the string. + """ + var coef: List[UInt8] + var scale: Int + var sign: Bool + coef, scale, sign = decimojo.str.parse_numeric_string(value) + + magnitude = BigUInt.from_string(value, ignore_sign=True) + + return Self(magnitude^, scale, sign) + + # ===------------------------------------------------------------------=== # + # Output dunders, type-transfer dunders + # ===------------------------------------------------------------------=== # + + fn __str__(self) -> String: + """Returns string representation of the BigDecimal. + See `to_string()` for more information. + """ + return self.to_string() + + fn __repr__(self) -> String: + """Returns a string representation of the BigDecimal.""" + return 'BigDecimal("' + self.__str__() + '")' + + # ===------------------------------------------------------------------=== # + # Type-transfer or output methods that are not dunders + # ===------------------------------------------------------------------=== # + + fn to_string(self) -> String: + """Returns string representation of the number.""" + + if self.magnitude.is_unitialized(): + return String("Unitilialized maginitude of BigDecimal") + + var result = String("-") if self.sign else String("") + + var magnitude_string = self.magnitude.to_string() + + if self.scale == 0: + result += magnitude_string + + elif self.scale > 0: + if self.scale < len(magnitude_string): + # Example: 123_456 with scale 3 -> 123.456 + result += magnitude_string[: len(magnitude_string) - self.scale] + result += "." + result += magnitude_string[len(magnitude_string) - self.scale :] + else: + # Example: 123_456 with scale 6 -> 0.123_456 + # Example: 123_456 with scale 7 -> 0.012_345_6 + result += "0." + result += "0" * (self.scale - len(magnitude_string)) + result += magnitude_string + + else: + # scale < 0 + # Example: 12_345 with scale -3 -> 12_345_000 + result += magnitude_string + result += "0" * (-self.scale) + + return result^ + + # ===------------------------------------------------------------------=== # + # Type-transfer or output methods that are not dunders + # ===------------------------------------------------------------------=== # + + fn write_to[W: Writer](self, mut writer: W): + """Writes the BigDecimal to a writer. + This implement the `write` method of the `Writer` trait. + """ + writer.write(String(self)) + + # ===------------------------------------------------------------------=== # + # Other methods + # ===------------------------------------------------------------------=== # + + @always_inline + fn is_zero(self) -> Bool: + """Returns True if this number represents zero.""" + return self.magnitude.is_zero() diff --git a/src/decimojo/bigint/bigint.mojo b/src/decimojo/bigint/bigint.mojo index b6618ecc..24629e43 100644 --- a/src/decimojo/bigint/bigint.mojo +++ b/src/decimojo/bigint/bigint.mojo @@ -76,31 +76,6 @@ struct BigInt(Absable, IntableRaising, Writable): self.magnitude = BigUInt() self.sign = False - # fn __init__(out self, empty: Bool, sign: Bool): - # """Initializes an empty BigInt. - - # Args: - # empty: A Bool value indicating whether the BigInt is empty. - # If True, the BigInt is empty. - # If False, the BigInt is intialized with value 0. - # sign: The sign of the BigInt. - # """ - # self.magnitude = BigUInt(empty=empty) - # self.sign = sign - - # fn __init__(out self, empty: Bool, capacity: Int, sign: Bool): - # """Initializes an empty BigInt with a given capacity. - - # Args: - # empty: A Bool value indicating whether the BigInt is empty. - # If True, the BigInt is empty. - # If False, the BigInt is intialized with value 0. - # capacity: The capacity of the BigInt. - # sign: The sign of the BigInt. - # """ - # self.magnitude = BigUInt(empty=empty, capacity=capacity) - # self.sign = sign - fn __init__(out self, magnitude: BigUInt, sign: Bool): """Initializes a BigInt from a BigUInt and a sign. @@ -278,7 +253,7 @@ struct BigInt(Absable, IntableRaising, Writable): return Self(magnitude^, sign) @staticmethod - fn from_string(value: String) raises -> BigInt: + fn from_string(value: String) raises -> Self: """Initializes a BigInt from a string representation. The string is normalized with `deciomojo.str.parse_numeric_string()`. @@ -313,9 +288,9 @@ struct BigInt(Absable, IntableRaising, Writable): fn __str__(self) -> String: """Returns string representation of the BigInt. - See `to_str()` for more information. + See `to_string()` for more information. """ - return self.to_str() + return self.to_string() fn __repr__(self) -> String: """Returns a string representation of the BigInt.""" @@ -364,7 +339,7 @@ struct BigInt(Absable, IntableRaising, Writable): return Int(value) - fn to_str(self) -> String: + fn to_string(self) -> String: """Returns string representation of the BigInt.""" if self.magnitude.is_unitialized(): @@ -374,11 +349,11 @@ struct BigInt(Absable, IntableRaising, Writable): return String("0") var result = String("-") if self.sign else String("") - result += self.magnitude.to_str() + result += self.magnitude.to_string() return result^ - fn to_str_with_separators(self, separator: String = "_") -> String: + fn to_string_with_separators(self, separator: String = "_") -> String: """Returns string representation of the BigInt with separators. Args: @@ -388,7 +363,7 @@ struct BigInt(Absable, IntableRaising, Writable): The string representation of the BigInt with separators. """ - var result = self.to_str() + var result = self.to_string() var end = len(result) var start = end - 3 while start > 0: @@ -611,7 +586,7 @@ struct BigInt(Absable, IntableRaising, Writable): print("\nInternal Representation Details of BigInt") print("-----------------------------------------") print("number: ", self) - print(" ", self.to_str_with_separators()) + print(" ", self.to_string_with_separators()) print("negative: ", self.sign) for i in range(len(self.magnitude.words)): print( diff --git a/src/decimojo/biguint/biguint.mojo b/src/decimojo/biguint/biguint.mojo index 46e4b333..ed460aa2 100644 --- a/src/decimojo/biguint/biguint.mojo +++ b/src/decimojo/biguint/biguint.mojo @@ -31,6 +31,9 @@ import decimojo.biguint.arithmetics import decimojo.biguint.comparison import decimojo.str +# Type aliases +alias BUInt = BigUInt + @value struct BigUInt(Absable, IntableRaising, Writable): @@ -383,9 +386,9 @@ struct BigUInt(Absable, IntableRaising, Writable): fn __str__(self) -> String: """Returns string representation of the BigUInt. - See `to_str()` for more information. + See `to_string()` for more information. """ - return self.to_str() + return self.to_string() fn __repr__(self) -> String: """Returns a string representation of the BigUInt.""" @@ -460,7 +463,7 @@ struct BigUInt(Absable, IntableRaising, Writable): return UInt64(value) - fn to_str(self) -> String: + fn to_string(self) -> String: """Returns string representation of the BigUInt.""" if len(self.words) == 0: @@ -479,7 +482,7 @@ struct BigUInt(Absable, IntableRaising, Writable): return result^ - fn to_str_with_separators(self, separator: String = "_") -> String: + fn to_string_with_separators(self, separator: String = "_") -> String: """Returns string representation of the BigUInt with separators. Args: @@ -489,7 +492,7 @@ struct BigUInt(Absable, IntableRaising, Writable): The string representation of the BigUInt with separators. """ - var result = self.to_str() + var result = self.to_string() var end = len(result) var start = end - 3 while start > 0: @@ -768,7 +771,7 @@ struct BigUInt(Absable, IntableRaising, Writable): print("\nInternal Representation Details of BigUInt") print("-----------------------------------------") print("number: ", self) - print(" ", self.to_str_with_separators()) + print(" ", self.to_string_with_separators()) for i in range(len(self.words)): print( "word", From 2a1b5e182d4c6d5a98c2fe64c82c3b4f0fe84be8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?ZHU=20Yuhao=20=E6=9C=B1=E5=AE=87=E6=B5=A9?= Date: Fri, 28 Mar 2025 21:38:35 +0100 Subject: [PATCH 2/2] Add more methods --- src/decimojo/bigdecimal/bigdecimal.mojo | 210 ++++++++++++++++++++++-- src/decimojo/bigint/bigint.mojo | 14 +- src/decimojo/biguint/biguint.mojo | 5 +- 3 files changed, 202 insertions(+), 27 deletions(-) diff --git a/src/decimojo/bigdecimal/bigdecimal.mojo b/src/decimojo/bigdecimal/bigdecimal.mojo index 0972e66b..f10647df 100644 --- a/src/decimojo/bigdecimal/bigdecimal.mojo +++ b/src/decimojo/bigdecimal/bigdecimal.mojo @@ -37,12 +37,12 @@ struct BigDecimal: Internal Representation: - - A base-10 unsigned integer (BigUInt) for magnitude. + - A base-10 unsigned integer (BigUInt) for coefficient. - A Int value for the scale - A Bool value for the sign. Final value: - (-1)**sign * magnitude * 10^(-scale) + (-1)**sign * coefficient * 10^(-scale) """ # ===------------------------------------------------------------------=== # @@ -65,8 +65,8 @@ struct BigDecimal: # ===------------------------------------------------------------------=== # # Internal representation fields - var magnitude: BigUInt - """The magnitude of the BigDecimal.""" + var coefficient: BigUInt + """The coefficient of the BigDecimal.""" var scale: Int """The scale of the BigDecimal.""" var sign: Bool @@ -76,17 +76,153 @@ struct BigDecimal: # Constructors and life time dunder methods # ===------------------------------------------------------------------=== # - fn __init__(out self, magnitude: BigUInt, scale: Int, sign: Bool) raises: + fn __init__(out self, coefficient: BigUInt, scale: Int, sign: Bool) raises: """Constructs a BigDecimal from its components.""" - self.magnitude = magnitude + self.coefficient = coefficient self.scale = scale self.sign = sign + fn __init__(out self, value: String) raises: + """Constructs a BigDecimal from a string representation.""" + # The string is normalized with `deciomojo.str.parse_numeric_string()`. + self = Self.from_string(value) + + fn __init__(out self, value: Int) raises: + """Constructs a BigDecimal from an integer.""" + self = Self.from_int(value) + + fn __init__(out self, value: Scalar) raises: + """Constructs a BigDecimal from a Mojo Scalar.""" + self = Self.from_scalar(value) + # ===------------------------------------------------------------------=== # # Constructing methods that are not dunders + # from_int(value: Int) -> Self + # from_scalar(value: Scalar) -> Self # from_string(value: String) -> Self # ===------------------------------------------------------------------=== # + @staticmethod + fn from_int(value: Int) raises -> Self: + """Creates a BigDecimal from an integer.""" + if value == 0: + return Self(coefficient=BigUInt(UInt32(0)), scale=0, sign=False) + + var words = List[UInt32](capacity=2) + var sign: Bool + var remainder: Int + var quotient: Int + var is_min: Bool = False + if value < 0: + sign = True + # Handle the case of Int.MIN due to asymmetry of Int.MIN and Int.MAX + if value == Int.MIN: + is_min = True + remainder = Int.MAX + else: + remainder = -value + else: + sign = False + remainder = value + + while remainder != 0: + quotient = remainder // 1_000_000_000 + remainder = remainder % 1_000_000_000 + words.append(UInt32(remainder)) + remainder = quotient + + if is_min: + words[0] += 1 + + return Self(coefficient=BigUInt(words^), scale=0, sign=sign) + + @staticmethod + fn from_scalar[dtype: DType, //](value: Scalar[dtype]) raises -> Self: + """Initializes a BigDecimal from a Mojo Scalar. + + Args: + value: The Scalar value to be converted to BigDecimal. + + Returns: + The BigDecimal representation of the Scalar value. + + Notes: + If the value is a floating-point number, it is converted to a string + with full precision before converting to BigDecimal. + """ + var sign = True if value < 0 else False + + @parameter + if dtype.is_integral(): + var list_of_words = List[UInt32]() + var remainder: Scalar[dtype] = value + var quotient: Scalar[dtype] + var is_min = False + + if sign: + var min_value: Scalar[dtype] + var max_value: Scalar[dtype] + + # TODO: Currently Int256 is not supported due to the limitation + # of Mojo's standard library. The following part can be removed + # if `mojo/stdlib/src/utils/numerics.mojo` is updated. + @parameter + if dtype == DType.int128: + min_value = Scalar[dtype]( + -170141183460469231731687303715884105728 + ) + max_value = Scalar[dtype]( + 170141183460469231731687303715884105727 + ) + elif dtype == DType.int64: + min_value = Scalar[dtype].MIN + max_value = Scalar[dtype].MAX + elif dtype == DType.int32: + min_value = Scalar[dtype].MIN + max_value = Scalar[dtype].MAX + elif dtype == DType.int16: + min_value = Scalar[dtype].MIN + max_value = Scalar[dtype].MAX + elif dtype == DType.int8: + min_value = Scalar[dtype].MIN + max_value = Scalar[dtype].MAX + else: + raise Error( + "Error in `from_scalar()`: Unsupported integral type" + ) + + if value == min_value: + remainder = max_value + is_min = True + else: + remainder = -value + + while remainder != 0: + quotient = remainder // 1_000_000_000 + remainder = remainder % 1_000_000_000 + list_of_words.append(UInt32(remainder)) + remainder = quotient + + if is_min: + list_of_words[0] += 1 + + return Self(coefficient=BigUInt(list_of_words^), scale=0, sign=sign) + + else: # floating-point + if value != value: # Check for NaN + raise Error( + "Error in `from_scalar()`: Cannot convert NaN to BigUInt" + ) + # Convert to string with full precision + try: + return Self.from_string(String(value)) + except e: + raise Error("Error in `from_scalar()`: ", e) + + return Self( + coefficient=BigUInt(UInt32(0)), scale=0, sign=sign + ) # Default case + @staticmethod fn from_string(value: String) raises -> Self: """Initializes a BigDecimal from a string representation. @@ -103,12 +239,38 @@ struct BigDecimal: var sign: Bool coef, scale, sign = decimojo.str.parse_numeric_string(value) - magnitude = BigUInt.from_string(value, ignore_sign=True) + var number_of_digits = len(coef) + var number_of_words = number_of_digits // 9 + if number_of_digits % 9 != 0: + number_of_words += 1 + + coefficient_words = List[UInt32](capacity=number_of_words) + + var end: Int = number_of_digits + var start: Int + while end >= 9: + start = end - 9 + var word: UInt32 = 0 + for digit in coef[start:end]: + word = word * 10 + UInt32(digit[]) + coefficient_words.append(word) + end = start + if end > 0: + var word: UInt32 = 0 + for digit in coef[0:end]: + word = word * 10 + UInt32(digit[]) + coefficient_words.append(word) - return Self(magnitude^, scale, sign) + coefficient = BigUInt(coefficient_words^) + + return Self(coefficient^, scale, sign) # ===------------------------------------------------------------------=== # # Output dunders, type-transfer dunders + # __str__() + # __repr__() + # __int__() + # __float__() # ===------------------------------------------------------------------=== # fn __str__(self) -> String: @@ -121,6 +283,14 @@ struct BigDecimal: """Returns a string representation of the BigDecimal.""" return 'BigDecimal("' + self.__str__() + '")' + fn __int__(self) raises -> Int: + """Converts the BigDecimal to an integer.""" + return Int(String(self)) + + fn __float__(self) raises -> Float64: + """Converts the BigDecimal to a floating-point number.""" + return Float64(String(self)) + # ===------------------------------------------------------------------=== # # Type-transfer or output methods that are not dunders # ===------------------------------------------------------------------=== # @@ -128,33 +298,37 @@ struct BigDecimal: fn to_string(self) -> String: """Returns string representation of the number.""" - if self.magnitude.is_unitialized(): + if self.coefficient.is_unitialized(): return String("Unitilialized maginitude of BigDecimal") var result = String("-") if self.sign else String("") - var magnitude_string = self.magnitude.to_string() + var coefficient_string = self.coefficient.to_string() if self.scale == 0: - result += magnitude_string + result += coefficient_string elif self.scale > 0: - if self.scale < len(magnitude_string): + if self.scale < len(coefficient_string): # Example: 123_456 with scale 3 -> 123.456 - result += magnitude_string[: len(magnitude_string) - self.scale] + result += coefficient_string[ + : len(coefficient_string) - self.scale + ] result += "." - result += magnitude_string[len(magnitude_string) - self.scale :] + result += coefficient_string[ + len(coefficient_string) - self.scale : + ] else: # Example: 123_456 with scale 6 -> 0.123_456 # Example: 123_456 with scale 7 -> 0.012_345_6 result += "0." - result += "0" * (self.scale - len(magnitude_string)) - result += magnitude_string + result += "0" * (self.scale - len(coefficient_string)) + result += coefficient_string else: # scale < 0 # Example: 12_345 with scale -3 -> 12_345_000 - result += magnitude_string + result += coefficient_string result += "0" * (-self.scale) return result^ @@ -176,4 +350,4 @@ struct BigDecimal: @always_inline fn is_zero(self) -> Bool: """Returns True if this number represents zero.""" - return self.magnitude.is_zero() + return self.coefficient.is_zero() diff --git a/src/decimojo/bigint/bigint.mojo b/src/decimojo/bigint/bigint.mojo index 24629e43..7e895c0d 100644 --- a/src/decimojo/bigint/bigint.mojo +++ b/src/decimojo/bigint/bigint.mojo @@ -215,14 +215,15 @@ struct BigInt(Absable, IntableRaising, Writable): var sign: Bool var remainder: Int var quotient: Int + var is_min: Bool = False if value < 0: + sign = True # Handle the case of Int.MIN due to asymmetry of Int.MIN and Int.MAX if value == Int.MIN: - return Self( - UInt32(854775807), UInt32(223372036), UInt32(9), sign=True - ) - sign = True - remainder = -value + is_min = True + remainder = Int.MAX + else: + remainder = -value else: sign = False remainder = value @@ -233,6 +234,9 @@ struct BigInt(Absable, IntableRaising, Writable): words.append(UInt32(remainder)) remainder = quotient + if is_min: + words[0] += 1 + return Self(BigUInt(words^), sign) @staticmethod diff --git a/src/decimojo/biguint/biguint.mojo b/src/decimojo/biguint/biguint.mojo index ed460aa2..abce3ee9 100644 --- a/src/decimojo/biguint/biguint.mojo +++ b/src/decimojo/biguint/biguint.mojo @@ -125,7 +125,7 @@ struct BigUInt(Absable, IntableRaising, Writable): """ self = Self.from_int(value) - fn __init__[dtype: DType](out self, value: Scalar[dtype]) raises: + fn __init__(out self, value: Scalar) raises: """Initializes a BigUInt from a Mojo Scalar. See `from_scalar()` for more information. """ @@ -260,7 +260,6 @@ struct BigUInt(Absable, IntableRaising, Writable): remainder = remainder % 1_000_000_000 list_of_words.append(UInt32(remainder)) remainder = quotient - return Self(list_of_words^) else: @@ -268,7 +267,6 @@ struct BigUInt(Absable, IntableRaising, Writable): raise Error( "Error in `from_scalar()`: Cannot convert NaN to BigUInt" ) - # Convert to string with full precision try: return Self.from_string(String(value)) @@ -328,7 +326,6 @@ struct BigUInt(Absable, IntableRaising, Writable): if scale == 0: # This is a true integer - var number_of_digits = len(coef) var end: Int = number_of_digits var start: Int while end >= 9: