// Copyright 2025 International Digital Economy Academy
//
// 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.
///|
/// Arbitrary precision decimal type for exact decimal arithmetic.
///
/// This type is designed for financial calculations and other applications
/// that require exact decimal representation without floating-point errors.
///
/// The decimal is represented as a coefficient (BigInt) and a scale (Int):
/// - coefficient: The significant digits as a BigInt
/// - scale: The number of decimal places (0 <= scale <= max_scale)
///
/// For example: 123.45 is represented as coefficient=12345, scale=2
///
/// Invariants:
/// - 0 <= scale <= max_scale (default 28)
/// - coefficient and scale are normalized (no trailing zeros in coefficient)
/// - Zero is represented as coefficient=0, scale=0
///
/// Example:
/// ```moonbit check
/// test {
/// let price = match @decimal.Decimal::from_string("19.99") {
/// Some(p) => p
/// None => fail("Invalid price")
/// }
/// let tax = match @decimal.Decimal::from_string("0.08") {
/// Some(t) => t
/// None => fail("Invalid tax rate")
/// }
/// let total = price + tax
/// inspect(total.to_string(), content="20.07")
/// }
/// ```
///
pub struct Decimal {
coefficient : @bigint.BigInt
scale : Int
} derive(Debug)
///|
/// Maximum number of decimal places (scale) allowed.
pub let max_scale = 28
///|
/// Creates a decimal from a coefficient and scale.
///
/// Parameters:
/// * `coefficient` : The significant digits as a BigInt
/// * `scale` : The number of decimal places
///
/// Returns `Some(decimal)` if the scale is valid, `None` otherwise.
///
/// Example:
/// ```moonbit check
/// test {
/// let d = match @decimal.Decimal::new(12345N, 2) {
/// Some(d) => d
/// None => fail("Invalid decimal")
/// } // 123.45
/// inspect(d.to_string(), content="123.45")
/// }
/// ```
///
pub fn Decimal::new(coefficient : @bigint.BigInt, scale : Int) -> Decimal? {
if scale < 0 || scale > max_scale {
None
} else {
Some(normalize({ coefficient, scale }))
}
}
///|
/// Creates a decimal from a coefficient and scale without validation.
/// Used internally for performance-critical operations.
fn new_unchecked(coefficient : @bigint.BigInt, scale : Int) -> Decimal {
{ coefficient, scale }
}
///|
/// Normalizes a decimal by removing trailing zeros from the coefficient
/// and adjusting the scale accordingly.
fn normalize(d : Decimal) -> Decimal {
if d.coefficient.is_zero() {
{ coefficient: 0N, scale: 0 }
} else {
let mut coefficient = d.coefficient
let mut scale = d.scale
// Remove trailing zeros
while !coefficient.is_zero() && coefficient % 10N == 0N && scale > 0 {
coefficient = coefficient / 10N
scale = scale - 1
}
{ coefficient, scale }
}
}
///|
/// Creates a decimal from a string representation.
///
/// Parameters:
/// * `s` : String representation of the decimal (e.g., "123.45", "-0.001")
///
/// Returns `Some(decimal)` if parsing succeeds, `None` otherwise.
///
/// Example:
/// ```moonbit check
/// test {
/// let d = match @decimal.Decimal::from_string("123.45") {
/// Some(d) => d
/// None => fail("Invalid decimal string")
/// }
/// inspect(d.to_string(), content="123.45")
/// }
/// ```
///
pub fn Decimal::from_string(s : String) -> Decimal? {
parse_decimal_string(s)
}
///|
/// Parses a decimal string into a Decimal value.
fn parse_decimal_string(s : String) -> Decimal? {
let s = s.trim(char_set=" ")
if s.is_empty() {
return None
}
let (sign, s) = if s.has_prefix("-") {
(-1, s.view(start_offset=1))
} else if s.has_prefix("+") {
(1, s.view(start_offset=1))
} else {
(1, s.view())
}
if s.is_empty() {
return None
}
// Find decimal point
let dot_index = s.find(".")
let (integer_part, fractional_part) = match dot_index {
Some(i) => (s.view(end_offset=i), s.view(start_offset=i + 1))
None => (s, "".view())
}
// Validate parts - at least one part must be non-empty
if integer_part.is_empty() && fractional_part.is_empty() {
return None
}
// Validate that parts contain only digits
if !integer_part.is_empty() && !is_digit_string(integer_part) {
return None
}
if !fractional_part.is_empty() && !is_digit_string(fractional_part) {
return None
}
// Parse integer part
let coefficient = if integer_part.is_empty() {
0N
} else {
@bigint.BigInt::from_string(integer_part.to_owned())
}
// Parse fractional part
let scale = fractional_part.length()
if scale > max_scale {
return None
}
let fractional_coefficient = if fractional_part.is_empty() {
0N
} else {
@bigint.BigInt::from_string(fractional_part.to_owned())
}
// Combine parts
let total_coefficient = coefficient *
@bigint.BigInt::from_int(10).pow(@bigint.BigInt::from_int(scale)) +
fractional_coefficient
let final_coefficient = if sign < 0 {
-total_coefficient
} else {
total_coefficient
}
Decimal::new(final_coefficient, scale)
}
///|
/// Helper function to check if a string contains only digits.
fn is_digit_string(s : StringView) -> Bool {
for c in s {
if c < '0' || c > '9' {
return false
}
}
true
}
///|
/// Creates a decimal from an integer.
///
/// Parameters:
/// * `n` : The integer value
///
/// Returns a decimal with scale 0.
///
/// Example:
/// ```moonbit check
/// test {
/// let d = @decimal.Decimal::from_int(42)
/// inspect(d.to_string(), content="42")
/// }
/// ```
///
pub fn Decimal::from_int(n : Int) -> Decimal {
{ coefficient: @bigint.BigInt::from_int(n), scale: 0 }
}
///|
/// Creates a decimal from a BigInt.
///
/// Parameters:
/// * `n` : The BigInt value
///
/// Returns a decimal with scale 0.
///
/// Example:
/// ```moonbit check
/// test {
/// let d = @decimal.Decimal::from_bigint(12345678901234567890N)
/// inspect(d.to_string(), content="12345678901234567890")
/// }
/// ```
///
pub fn Decimal::from_bigint(n : @bigint.BigInt) -> Decimal {
{ coefficient: n, scale: 0 }
}
///|
/// Creates a decimal from a double with specified precision.
///
/// Parameters:
/// * `d` : The double value
/// * `precision` : Number of decimal places
///
/// Returns `Some(decimal)` if conversion succeeds, `None` otherwise.
///
/// Example:
/// ```moonbit check
/// test {
/// let d = match @decimal.Decimal::from_double(3.14159, 5) {
/// Some(d) => d
/// None => fail("Invalid double conversion")
/// }
/// inspect(d.to_string(), content="3.14159")
/// }
/// ```
///
pub fn Decimal::from_double(d : Double, precision : Int) -> Decimal? {
if d.is_nan() {
return None
}
if precision < 0 || precision > max_scale {
return None
}
let multiplier = @math.pow(10.0, precision.to_double())
let scaled = d * multiplier
let rounded = scaled.round()
// Check for overflow
if rounded.abs() > 9223372036854775807.0 { // Int64::max_value
return None
}
let coefficient = @bigint.BigInt::from_int64(rounded.to_int64())
Decimal::new(coefficient, precision)
}
///|
/// Converts the decimal to a string representation.
///
/// Returns a string in the format "integer.fractional" or "integer".
///
/// Example:
/// ```moonbit check
/// test {
/// let d = match @decimal.Decimal::from_string("123.45") {
/// Some(d) => d
/// None => fail("Invalid decimal string")
/// }
/// inspect(d.to_string(), content="123.45")
/// }
/// ```
///
pub fn Decimal::to_string(self : Decimal) -> String {
if self.is_zero() {
"0"
} else {
let coeff_str = self.coefficient.to_string()
let is_negative = coeff_str.has_prefix("-")
let abs_coeff = if is_negative {
coeff_str.view(start_offset=1)
} else {
coeff_str.view()
}
if self.scale == 0 {
coeff_str
} else if abs_coeff.length() <= self.scale {
// Need leading zeros
let leading_zeros = "0".repeat(self.scale - abs_coeff.length() + 1)
let with_zeros = leading_zeros + abs_coeff.to_owned()
let integer_part = with_zeros.view(
end_offset=with_zeros.length() - self.scale,
)
let fractional_part = with_zeros.view(
start_offset=with_zeros.length() - self.scale,
)
let result = integer_part.to_owned() + "." + fractional_part.to_owned()
if is_negative {
"-" + result
} else {
result
}
} else {
// Insert decimal point
let integer_part = abs_coeff.view(
end_offset=abs_coeff.length() - self.scale,
)
let fractional_part = abs_coeff.view(
start_offset=abs_coeff.length() - self.scale,
)
let result = integer_part.to_owned() + "." + fractional_part.to_owned()
if is_negative {
"-" + result
} else {
result
}
}
}
}
///|
/// Converts the decimal to a double (with potential precision loss).
///
/// Warning: This conversion may lose precision for large numbers or high precision decimals.
///
/// Example:
/// ```moonbit check
/// test {
/// let d = match @decimal.Decimal::from_string("3.14159") {
/// Some(d) => d
/// None => fail("Invalid decimal string")
/// }
/// inspect(d.to_double(), content="3.14159")
/// }
/// ```
///
pub fn Decimal::to_double(self : Decimal) -> Double {
if self.is_zero() {
0.0
} else {
let coeff_double = @string.parse_double(self.coefficient.to_string().view()) catch {
_ => 0.0 // Fallback to 0.0 if parsing fails
}
let divisor = @math.pow(10.0, self.scale.to_double())
coeff_double / divisor
}
}
///|
/// Converts the decimal to an integer by truncating towards zero.
///
/// Returns `Some(int)` if the decimal fits in an Int, `None` otherwise.
///
/// Example:
/// ```moonbit check
/// test {
/// let d = match @decimal.Decimal::from_string("123.99") {
/// Some(d) => d
/// None => fail("Invalid decimal string")
/// }
/// debug_inspect(d.to_int(), content="Some(123)")
/// }
/// ```
///
pub fn Decimal::to_int(self : Decimal) -> Int? {
if self.scale == 0 {
Some(self.coefficient.to_int())
} else {
let divisor = @bigint.BigInt::from_int(10).pow(
@bigint.BigInt::from_int(self.scale),
)
let quotient = self.coefficient / divisor
Some(quotient.to_int())
}
}
///|
/// Converts the decimal to a BigInt by truncating towards zero.
///
/// Example:
/// ```moonbit check
/// test {
/// let d = match @decimal.Decimal::from_string("123.99") {
/// Some(d) => d
/// None => fail("Invalid decimal string")
/// }
/// inspect(d.to_bigint(), content="123")
/// }
/// ```
///
pub fn Decimal::to_bigint(self : Decimal) -> @bigint.BigInt {
if self.scale == 0 {
self.coefficient
} else {
let divisor = @bigint.BigInt::from_int(10).pow(
@bigint.BigInt::from_int(self.scale),
)
self.coefficient / divisor
}
}
///|
/// Returns the zero decimal value.
pub fn zero() -> Decimal {
{ coefficient: 0N, scale: 0 }
}
///|
/// Returns the one decimal value.
pub fn one() -> Decimal {
{ coefficient: 1N, scale: 0 }
}
///|
/// Returns the negative one decimal value.
pub fn neg_one() -> Decimal {
{ coefficient: -1N, scale: 0 }
}
///|
/// Checks if the decimal is zero.
pub fn Decimal::is_zero(self : Decimal) -> Bool {
self.coefficient.is_zero()
}
///|
/// Checks if the decimal is positive.
pub fn Decimal::is_positive(self : Decimal) -> Bool {
self.coefficient > 0N
}
///|
/// Checks if the decimal is negative.
pub fn Decimal::is_negative(self : Decimal) -> Bool {
self.coefficient < 0N
}
///|
/// Returns the sign of the decimal: -1, 0, or 1.
pub fn Decimal::signum(self : Decimal) -> Int {
if self.coefficient.is_zero() {
0
} else if self.coefficient > 0N {
1
} else {
-1
}
}
///|
/// Returns the absolute value of the decimal.
pub fn Decimal::abs(self : Decimal) -> Decimal {
if self.coefficient < 0N {
{ coefficient: -self.coefficient, scale: self.scale }
} else {
self
}
}
///|
/// Returns the coefficient (significant digits) of the decimal.
pub fn Decimal::coefficient(self : Decimal) -> @bigint.BigInt {
self.coefficient
}
///|
/// Returns the scale (number of decimal places) of the decimal.
pub fn Decimal::scale(self : Decimal) -> Int {
self.scale
}
///|
/// Rounds the decimal to the specified number of decimal places.
///
/// Parameters:
/// * `self` : The decimal to round
/// * `places` : Number of decimal places (0 <= places <= max_scale)
///
/// Returns `Some(decimal)` if places is valid, `None` otherwise.
///
/// Example:
/// ```moonbit check
/// test {
/// let d = match @decimal.Decimal::from_string("3.14159") {
/// Some(d) => d
/// None => fail("Invalid decimal string")
/// }
/// let rounded = match d.round(2) {
/// Some(r) => r
/// None => fail("Invalid rounding")
/// }
/// inspect(rounded.to_string(), content="3.14")
/// }
/// ```
///
pub fn Decimal::round(self : Decimal, places : Int) -> Decimal? {
if places < 0 || places > max_scale {
return None
}
if places >= self.scale {
// No rounding needed
return Some(self)
}
let places_to_remove = self.scale - places
let divisor = @bigint.BigInt::from_int(10).pow(
@bigint.BigInt::from_int(places_to_remove),
)
let remainder = self.coefficient % divisor
let half_divisor = divisor / 2N
let rounded_coeff = if remainder >= half_divisor {
self.coefficient / divisor + 1N
} else {
self.coefficient / divisor
}
Decimal::new(rounded_coeff, places)
}
///|
/// Truncates the decimal to the specified number of decimal places.
///
/// Parameters:
/// * `self` : The decimal to truncate
/// * `places` : Number of decimal places (0 <= places <= max_scale)
///
/// Returns `Some(decimal)` if places is valid, `None` otherwise.
///
/// Example:
/// ```moonbit check
/// test {
/// let d = match @decimal.Decimal::from_string("3.14159") {
/// Some(d) => d
/// None => fail("Invalid decimal string")
/// }
/// let truncated = match d.truncate(2) {
/// Some(t) => t
/// None => fail("Invalid truncation")
/// }
/// inspect(truncated.to_string(), content="3.14")
/// }
/// ```
///
pub fn Decimal::truncate(self : Decimal, places : Int) -> Decimal? {
if places < 0 || places > max_scale {
return None
}
if places >= self.scale {
// No truncation needed
return Some(self)
}
let places_to_remove = self.scale - places
let divisor = @bigint.BigInt::from_int(10).pow(
@bigint.BigInt::from_int(places_to_remove),
)
let truncated_coeff = self.coefficient / divisor
Decimal::new(truncated_coeff, places)
}
///|
/// Scales the decimal to have the specified number of decimal places.
///
/// Parameters:
/// * `self` : The decimal to scale
/// * `new_scale` : New number of decimal places (0 <= new_scale <= max_scale)
///
/// Returns `Some(decimal)` if new_scale is valid, `None` otherwise.
///
/// Example:
/// ```moonbit check
/// test {
/// let d = match @decimal.Decimal::from_string("123.45") {
/// Some(d) => d
/// None => fail("Invalid decimal string")
/// }
/// let scaled = match d.scale_to(4) {
/// Some(s) => s
/// None => fail("Invalid scaling")
/// }
/// inspect(scaled.to_string(), content="123.4500")
/// }
/// ```
///
pub fn Decimal::scale_to(self : Decimal, new_scale : Int) -> Decimal? {
if new_scale < 0 || new_scale > max_scale {
return None
}
if new_scale == self.scale {
return Some(self)
} else if new_scale > self.scale {
// Add zeros
let scale_diff = new_scale - self.scale
let multiplier = @bigint.BigInt::from_int(10).pow(
@bigint.BigInt::from_int(scale_diff),
)
let new_coeff = self.coefficient * multiplier
Some({ coefficient: new_coeff, scale: new_scale })
} else {
// Remove digits (truncate)
let scale_diff = self.scale - new_scale
let divisor = @bigint.BigInt::from_int(10).pow(
@bigint.BigInt::from_int(scale_diff),
)
let new_coeff = self.coefficient / divisor
Some({ coefficient: new_coeff, scale: new_scale })
}
}
///|
/// Returns the default value (zero) for Decimal.
pub impl Default for Decimal with fn default() {
zero()
}
///|
/// Tests equality between two decimals.
pub impl Eq for Decimal with fn equal(self : Decimal, other : Decimal) -> Bool {
// Normalize both decimals to the same scale for comparison
let max_scale = Int::max(self.scale, other.scale)
match (self.scale_to(max_scale), other.scale_to(max_scale)) {
(Some(self_scaled), Some(other_scaled)) =>
self_scaled.coefficient == other_scaled.coefficient
_ => false
}
}
///|
/// Compares two decimals and returns their relative order.
pub impl Compare for Decimal with fn compare(self : Decimal, other : Decimal) -> Int {
// Normalize both decimals to the same scale for comparison
let max_scale = Int::max(self.scale, other.scale)
match (self.scale_to(max_scale), other.scale_to(max_scale)) {
(Some(self_scaled), Some(other_scaled)) =>
self_scaled.coefficient.compare(other_scaled.coefficient)
_ => 0
}
}
///|
/// Adds two decimals.
pub impl Add for Decimal with fn add(self : Decimal, other : Decimal) -> Decimal {
// Normalize both decimals to the same scale
let max_scale = Int::max(self.scale, other.scale)
match (self.scale_to(max_scale), other.scale_to(max_scale)) {
(Some(self_scaled), Some(other_scaled)) =>
new_unchecked(
self_scaled.coefficient + other_scaled.coefficient,
max_scale,
)
_ => self
}
}
///|
/// Subtracts one decimal from another.
pub impl Sub for Decimal with fn sub(self : Decimal, other : Decimal) -> Decimal {
// Normalize both decimals to the same scale
let max_scale = Int::max(self.scale, other.scale)
match (self.scale_to(max_scale), other.scale_to(max_scale)) {
(Some(self_scaled), Some(other_scaled)) =>
new_unchecked(
self_scaled.coefficient - other_scaled.coefficient,
max_scale,
)
_ => self
}
}
///|
/// Multiplies two decimals.
pub impl Mul for Decimal with fn mul(self : Decimal, other : Decimal) -> Decimal {
let new_coeff = self.coefficient * other.coefficient
let new_scale = self.scale + other.scale
// Check if scale exceeds maximum
if new_scale > max_scale {
// Truncate to max_scale
let scale_diff = new_scale - max_scale
let divisor = @bigint.BigInt::from_int(10).pow(
@bigint.BigInt::from_int(scale_diff),
)
let truncated_coeff = new_coeff / divisor
new_unchecked(truncated_coeff, max_scale)
} else {
new_unchecked(new_coeff, new_scale)
}
}
///|
/// Implements division with maximum precision.
pub impl Div for Decimal with fn div(self : Decimal, other : Decimal) -> Decimal {
if other.is_zero() {
abort("Division by zero")
}
// Scale up the dividend to get maximum precision
let scale_diff = max_scale - self.scale + other.scale
let multiplier = @bigint.BigInt::from_int(10).pow(
@bigint.BigInt::from_int(Int::max(0, scale_diff)),
)
let scaled_dividend = self.coefficient * multiplier
let quotient = scaled_dividend / other.coefficient
new_unchecked(quotient, max_scale)
}
///|
/// Returns the negation of the decimal.
pub impl Neg for Decimal with fn neg(self : Decimal) -> Decimal {
{ coefficient: -self.coefficient, scale: self.scale }
}
///|
/// Converts the decimal to its string representation.
pub impl Show for Decimal with fn output(self, logger) {
logger.write_string(self.to_string())
}
///|
/// Converts a decimal to its JSON representation as a string.
pub impl ToJson for Decimal with fn to_json(self : Decimal) -> Json {
Json::string(self.to_string())
}
///|
/// Creates a decimal from its JSON representation.
pub impl @json.FromJson for Decimal with fn from_json(json, path) {
guard json is String(s) else {
raise JsonDecodeError(
(path, "Decimal::from_json: expected string representation"),
)
}
let result = Decimal::from_string(s)
match result {
Some(d) => d
None =>
raise JsonDecodeError(
(path, "Decimal::from_json: invalid decimal string"),
)
}
}
///|
/// Implements hash for decimal values.
pub impl Hash for Decimal with fn hash_combine(self, hasher) {
hasher.combine(self.coefficient)
hasher.combine(self.scale)
}
///|
/// Generates arbitrary decimal values for testing.
pub impl @quickcheck.Arbitrary for Decimal with fn arbitrary(size, rs) {
let coeff = @quickcheck.Arbitrary::arbitrary(size, rs)
let scale = rs.next_int() % (max_scale + 1)
new_unchecked(coeff, scale)
}