diff --git a/README.md b/README.md index 426f092..97d9d6b 100644 --- a/README.md +++ b/README.md @@ -1 +1,134 @@ -# Go-BigDecimal \ No newline at end of file +# Go BigDecimal + +A comprehensive and precise arbitrary-precision decimal number library for Go, inspired by Java's `BigDecimal`. This library provides a robust, complete set of features for financial, scientific, and general-purpose decimal arithmetic. + +## ✨ Features + + * **Arbitrary Precision**: Handles numbers of any size without precision loss. + * **Comprehensive Operations**: Supports all standard arithmetic operations (`Add`, `Sub`, `Mul`, `Div`), as well as `Abs`, `Neg`, and `Rem`. + * **Advanced Rounding**: Provides a full suite of rounding modes for precise control over calculations, essential for financial applications. + * **Database & JSON Integration**: Seamlessly integrates with SQL databases and JSON encoding/decoding via standard Go interfaces (`sql.Scanner`, `driver.Valuer`, `json.Marshaler`, `json.Unmarshaler`). + * **Multiple Initializers**: Create `Decimal` values from a wide variety of types, including `int64`, `float64`, `string`, `big.Int`, and `big.Rat`. + * **Zero-Allocation Conversion**: High-performance methods for converting to and from `string` and `[]byte` without extra memory allocations. + +## 📦 Installation + +To start using `go-bigdecimal`, simply run the following command: + +```bash +go get github.com/christopherganda/go-bigdecimal +``` + +## 🚀 Usage + +### Initializing a Decimal + +You can create a new `Decimal` from various data types. + +```go +package main + +import ( + "fmt" + "math/big" + bigdecimal "github.com/your-username/go-bigdecimal" +) + +func main() { + // From int64 and scale (123.45) + d1 := bigdecimal.New(12345, 2) + + // From string ("-987.654321") + d2, err := bigdecimal.NewFromString("-987.654321") + if err != nil { + panic(err) + } + + // From float64 (123.456) + d3 := bigdecimal.NewFromFloat64(123.456) + + // From big.Int and scale (123456789.0) + bi := big.NewInt(123456789) + d4 := bigdecimal.NewFromBigInt(bi, 0) + + fmt.Println(d1, d2, d3, d4) +} +``` + +### Performing Operations + +All operations are immutable and return a new `Decimal` value. + +```go +package main + +import ( + "fmt" + "github.com/your-username/go-bigdecimal" +) + +func main() { + a := bigdecimal.New(10, 1) // 1.0 + b := bigdecimal.New(3, 1) // 0.3 + + // Addition: 1.0 + 0.3 = 1.3 + sum := a.Add(b) + fmt.Printf("Sum: %s\n", sum.String()) + + // Subtraction: 1.0 - 0.3 = 0.7 + diff := a.Sub(b) + fmt.Printf("Difference: %s\n", diff.String()) + + // Multiplication: 1.0 * 0.3 = 0.3 + prod := a.Mul(b) + fmt.Printf("Product: %s\n", prod.String()) + + // Division: 1.0 / 0.3 = 3.333... (rounded to 5 decimal places) + // Precision is required for non-terminating decimals. + // You must define your rounding modes, e.g., bigdecimal.RoundHalfUp + // quotient := a.Div(b, 5, bigdecimal.RoundHalfUp) + // fmt.Printf("Quotient: %s\n", quotient.String()) +} +``` + +## 📖 API Documentation + +### Initializers + + * `New(val int64, scale int32)`: Creates a new `Decimal` from a scaled `int64`. + * `NewFromInt(val int32)`: Creates a new `Decimal` from an `int32`. + * `NewFromInt64(val int64)`: Creates a new `Decimal` from an `int64`. + * `NewFromUint64(val uint64)`: Creates a new `Decimal` from a `uint64`. + * `NewFromBigInt(val *big.Int, scale int32)`: Creates a `Decimal` from a `big.Int` and a scale. + * `NewFromString(s string)`: Creates a `Decimal` from a string. + * `NewFromFloat64(val float64)`: Creates a `Decimal` from a `float64`. + * `NewFromRat(val *big.Rat, precision int32, roundingMode RoundingMode)`: Creates a `Decimal` from a `big.Rat`. + * `NewFromBytes(val []byte)`: Creates a `Decimal` from a byte slice. + +### Operations + + * `Add(other Decimal) Decimal`: Returns the sum of two decimals. + * `Sub(other Decimal) Decimal`: Returns the difference. + * `Mul(other Decimal) Decimal`: Returns the product. + * `Div(other Decimal, precision int32) Decimal`: Returns the quotient. **Note**: This function requires precision and may have an optional rounding mode argument depending on your final implementation. + * `Rem(other Decimal) Decimal`: Returns the remainder. + * `DivRem(other Decimal) (Decimal, Decimal)`: Returns the quotient and remainder. + * `Abs() Decimal`: Returns the absolute value. + * `Neg() Decimal`: Returns the negation. + * `Cmp(other Decimal) int`: Compares two decimals, returning -1, 0, or 1. + * `Sign() int`: Returns the sign of the decimal. + +### Converters & Utilities + * `Scan(value interface{})`: Scan value from database and assigns to the decimal variable. + * `String() string`: Returns a canonical string representation. + * `StringFixed(places int32) string`: Returns a string with a fixed number of decimal places, with rounding. + * `Int64() (int64, bool)`: Converts the decimal to `int64`, returning a boolean indicating if the conversion was exact. + * `Float64() (float64, bool)`: Converts the decimal to `float64`, returning a boolean indicating if the conversion was exact. + * `BigInt() *big.Int`: Returns the unscaled value as a `big.Int`. + * `Scale() int32`: Returns the current scale of the number. + * `Rescale(newScale int32, roundingMode RoundingMode) Decimal`: Changes the scale, rounding the number if necessary. + * `IsZero()`: Returns true if the decimal is zero. + +## 🤝 Contributing + +Contributions are welcome\! Please open an issue or a pull request for any features, bug fixes, or documentation improvements. diff --git a/helper.go b/helper.go new file mode 100644 index 0000000..81617e3 --- /dev/null +++ b/helper.go @@ -0,0 +1,17 @@ +package decimal + +import ( + "math/big" +) + +func bigIntFromString(s string) *big.Int { + if s == "" { + return nil + } + val := new(big.Int) + _, ok := val.SetString(s, 10) + if !ok { + return nil + } + return val +} diff --git a/operations.go b/operations.go new file mode 100644 index 0000000..7d910e2 --- /dev/null +++ b/operations.go @@ -0,0 +1,88 @@ +package decimal + +import ( + "math/big" +) + +// rescale returns a new Decimal with its scale adjusted to a target scale. +// If the target scale is smaller than the current scale, it truncates the +// unscaled value, resulting in a loss of precision. It will never return an error. +func (d Decimal) rescale(targetScale int32) Decimal { + // If scales are the same, return the original. + if d.scale == targetScale { + return d + } + + var deltaScale int32 + var newUnscaled *big.Int + + if d.scale > targetScale { + // Scaling down: division. + deltaScale = d.scale - targetScale + pow10 := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(deltaScale)), nil) + newUnscaled = new(big.Int).Div(d.unscaledValue, pow10) + } else { + // Scaling up: multiplication. + deltaScale = targetScale - d.scale + pow10 := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(deltaScale)), nil) + newUnscaled = new(big.Int).Mul(d.unscaledValue, pow10) + } + + return Decimal{ + unscaledValue: newUnscaled, + scale: targetScale, + } +} + +func (d Decimal) Abs() Decimal { + if d.unscaledValue == nil { + return Decimal{} + } + return Decimal{ + unscaledValue: new(big.Int).Abs(d.unscaledValue), + scale: d.scale, + } +} + +func (d Decimal) Add(other Decimal) Decimal { + finalScale := max(d.scale, other.scale) + + d1 := d.rescale(finalScale) + d2 := other.rescale(finalScale) + + return Decimal{ + unscaledValue: new(big.Int).Add(d1.unscaledValue, d2.unscaledValue), + scale: finalScale, + } +} + +func (d Decimal) Subtract(other Decimal) Decimal { + finalScale := max(d.scale, other.scale) + + d1 := d.rescale(finalScale) + d2 := other.rescale(finalScale) + + return Decimal{ + unscaledValue: new(big.Int).Sub(d1.unscaledValue, d2.unscaledValue), + scale: finalScale, + } +} + +func (d Decimal) Multiply(other Decimal) Decimal { + return Decimal{ + unscaledValue: new(big.Int).Mul(d.unscaledValue, other.unscaledValue), + scale: d.scale + other.scale, + } +} + +func (d Decimal) Divide(other Decimal) Decimal { + // To maintain precision, we scale up the dividend before division. + scaleFactor := int32(10) // This can be adjusted based on desired precision. + pow10 := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(scaleFactor)), nil) + scaledDividend := new(big.Int).Mul(d.unscaledValue, pow10) + + return Decimal{ + unscaledValue: new(big.Int).Div(scaledDividend, other.unscaledValue), + scale: d.scale - other.scale + scaleFactor, + } +} diff --git a/operations_test.go b/operations_test.go new file mode 100644 index 0000000..76eddda --- /dev/null +++ b/operations_test.go @@ -0,0 +1,406 @@ +package decimal + +import ( + "testing" +) + +func TestOperations_Rescale(t *testing.T) { + tests := []struct { + name string + input Decimal + targetScale int32 + expected Decimal + }{ + { + "RescaleUp", + New(123, 2), // 1.23 + 4, + New(12300, 4), // 1.2300 + }, + { + "RescaleDown", + New(12300, 4), // 1.2300 + 2, + New(123, 2), // 1.23 + }, + { + "RescaleSame", + New(123, 2), // 1.23 + 2, + New(123, 2), // 1.23 + }, + { + "RescaleDownWithTruncation", + New(12345, 4), // 1.2345 + 2, + New(123, 2), // 1.23 + }, + { + "RescaleNegativeScaleUp", + New(12345, -2), // 1234500 + 0, + New(1234500, 0), // 1234500 + }, + { + "RescaleNegativeScaleDown", + New(1234500, 0), // 1234500 + -2, + New(12345, -2), // 1234500 + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.input.rescale(tt.targetScale) + if result.unscaledValue.Cmp(tt.expected.unscaledValue) != 0 || result.scale != tt.expected.scale { + t.Errorf("rescale(%v, %d) = %v, want %v", + tt.input, tt.targetScale, result, tt.expected) + } + }) + } +} + +func TestOperations_Abs(t *testing.T) { + tests := []struct { + name string + input Decimal + expected Decimal + }{ + { + "AbsPositive", + New(5, 0), + New(5, 0), + }, + { + "AbsNegative", + New(-5, 0), + New(5, 0), + }, + { + "AbsZero", + New(0, 0), + New(0, 0), + }, + { + "AbsLargeNegative", + Decimal{unscaledValue: bigIntFromString("-9223372036854775808"), scale: 0}, + Decimal{unscaledValue: bigIntFromString("9223372036854775808"), scale: 0}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.input.Abs() + if result.unscaledValue.Cmp(tt.expected.unscaledValue) != 0 || result.scale != tt.expected.scale { + t.Errorf("Abs(%v) = %v, want %v", + tt.input, result, tt.expected) + } + }) + } +} + +func TestOperations_Add(t *testing.T) { + tests := []struct { + name string + a Decimal + b Decimal + expected Decimal + }{ + { + "AddPositive", + New(5, 0), + New(3, 0), + New(8, 0), + }, + { + "AddNegative", + New(-5, 0), + New(-3, 0), + New(-8, 0), + }, + { + "AddMixed", + New(5, 0), + New(-3, 0), + New(2, 0), + }, + { + "AddZero", + New(5, 0), + New(0, 0), + New(5, 0), + }, + { + "AddDifferentScales_A_Smaller", + New(1234, 2), + New(567, 1), + New(6904, 2), + }, // 12.34 + 56.70 = 69.04 + { + "AddDifferentScales_B_Smaller", + New(567, 1), + New(1234, 2), + New(6904, 2), + }, // 56.70 + 12.34 = 69.04 + { + "AddDifferentScales_With_Negatives", + New(-123, 1), + New(456, 2), + New(-774, 2), + }, // -12.30 + 4.56 = -7.74 + { + "AddDifferentScales_ManyPlaces_Case1", + New(999, 3), + New(1, 1), + New(1099, 3), + }, // 0.999 + 0.100 = 1.099 + { + "AddDifferentScales_ManyPlaces_Case2", + New(999, 3), + New(1, 2), + New(1009, 3), + }, // 0.999 + 0.010 = 1.009 + { + "AddWithCarryOver", + New(99, 2), + New(2, 1), + New(119, 2), + }, // Corrected: 0.99 + 0.20 = 1.19 + { + "AddWithDifferentScales_MoreZeros", + New(12500, 4), + New(35, 1), + New(47500, 4), + }, // 1.2500 + 3.5000 = 4.7500 + { + "AddWithNegativeResult", + New(100, 2), + New(-250, 2), + New(-150, 2), + }, // 1.00 + (-2.50) = -1.50 + { + "AddScalesToZero", + New(100, 2), + New(-100, 2), + New(0, 2), + }, // 1.00 + (-1.00) = 0.00 + { + "AddWithLargeScaleDifference", + New(1, 6), + New(1, 1), + New(100001, 6), + }, // 0.000001 + 0.100000 = 0.100001 + { + "AddLargeNumbers", + Decimal{unscaledValue: bigIntFromString("9223372036854775807"), + scale: 0}, + Decimal{unscaledValue: bigIntFromString("1"), + scale: 0}, + Decimal{unscaledValue: bigIntFromString("9223372036854775808"), + scale: 0}}, + { + " AddLargeNegativeNumbers", + Decimal{unscaledValue: bigIntFromString("-9223372036854775807"), + scale: 0}, + Decimal{unscaledValue: bigIntFromString("-1"), + scale: 0}, + Decimal{unscaledValue: bigIntFromString("-9223372036854775808"), + scale: 0}}, + { + "AddLargeMixedNumbers", + Decimal{unscaledValue: bigIntFromString("9223372036854775807"), + scale: 0}, + Decimal{unscaledValue: bigIntFromString("-9223372036854775807"), + scale: 0}, + Decimal{unscaledValue: bigIntFromString("0"), + scale: 0}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.a.Add(tt.b) + if result.unscaledValue.Cmp(tt.expected.unscaledValue) != 0 || result.scale != tt.expected.scale { + t.Errorf("Add(%v, %v) = %v, want %v", + tt.a, tt.b, result, tt.expected) + } + }) + } +} + +func TestOperations_Subtract(t *testing.T) { + tests := []struct { + name string + a Decimal + b Decimal + expected Decimal + }{ + { + "SubtractPositive", + New(8, 0), + New(3, 0), + New(5, 0), + }, + { + "SubtractNegative", + New(-8, 0), + New(-3, 0), + New(-5, 0), + }, + { + "SubtractMixed", + New(5, 0), + New(-3, 0), + New(8, 0), + }, + { + "SubtractZero", + New(5, 0), + New(0, 0), + New(5, 0), + }, + { + "SubtractDifferentScales_A_Smaller", + New(567, 1), // 56.7 + New(1234, 2), // 12.34 + New(4436, 2), // 44.36 + }, + { + "SubtractDifferentScales_B_Smaller", + New(1234, 2), // 12.34 + New(567, 1), // 56.7 + New(-4436, 2), // -44.36 + }, + { + "SubtractDifferentScales_With_Negatives", + New(-123, 1), // -12.3 + New(-456, 2), // -4.56 + New(-774, 2), // -7.74 + }, + { + "SubtractWithBorrow", + New(119, 2), // 1.19 + New(2, 1), // 0.2 + New(99, 2), // 0.99 + }, + { + "SubtractToZero", + New(1250, 2), // 12.50 + New(125, 1), // 12.5 + New(0, 2), + }, + { + "SubtractWithLargeScaleDifference", + New(1, 1), // 0.1 + New(1, 6), // 0.000001 + New(99999, 6), // 0.099999 + }, + { + "SubtractLargeNumbers", + Decimal{unscaledValue: bigIntFromString("9223372036854775808"), scale: 0}, + Decimal{unscaledValue: bigIntFromString("1"), scale: 0}, + Decimal{unscaledValue: bigIntFromString("9223372036854775807"), scale: 0}, + }, + { + "SubtractLargeNegativeNumbers", + Decimal{unscaledValue: bigIntFromString("-9223372036854775808"), scale: 0}, + Decimal{unscaledValue: bigIntFromString("-1"), scale: 0}, + Decimal{unscaledValue: bigIntFromString("-9223372036854775807"), scale: 0}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + result := tc.a.Subtract(tc.b) + + if result.unscaledValue.Cmp(tc.expected.unscaledValue) != 0 { + t.Errorf("unscaledValue mismatch for %s: got %s, want %s", tc.name, result.unscaledValue, tc.expected.unscaledValue) + } + if result.scale != tc.expected.scale { + t.Errorf("scale mismatch for %s: got %d, want %d", tc.name, result.scale, tc.expected.scale) + } + }) + } +} + +func TestOperations_Multiply(t *testing.T) { + tests := []struct { + name string + a Decimal + b Decimal + expected Decimal + }{ + { + "MultiplyTwoPositiveNumbers", + New(123, 2), // 1.23 + New(456, 2), // 4.56 + New(56088, 4), // 5.6088 + }, + { + "MultiplyWithZero", + New(1234, 3), // 1.234 + New(0, 1), // 0.0 + New(0, 4), // 0.0000 + }, + { + "MultiplyWithNegative", + New(125, 2), // 1.25 + New(-5, 1), // -0.5 + New(-625, 3), // -0.625 + }, + { + "MultiplyTwoNegatives", + New(-125, 2), // -1.25 + New(-5, 1), // -0.5 + New(625, 3), // 0.625 + }, + { + "MultiplyDifferentScales_Simple", + New(123, 2), // 1.23 + New(4, 0), // 4 + New(492, 2), // 4.92 + }, + { + "MultiplyDifferentScales_Complex", + New(1234, 2), // 12.34 + New(5, 1), // 0.5 + New(6170, 3), // 6.170 + }, + { + "MultiplySmallNumbers", + New(1, 2), // 0.01 + New(1, 2), // 0.01 + New(1, 4), // 0.0001 + }, + { + "MultiplySmallNumbers", + New(2, 1), // 0.2 + New(3, 1), // 0.3 + New(6, 2), // 0.06 + }, + { + "MultiplyLargeNumbers", + Decimal{unscaledValue: bigIntFromString("9223372036854775807"), scale: 0}, // A large number + Decimal{unscaledValue: bigIntFromString("100"), scale: 0}, // 100 + Decimal{unscaledValue: bigIntFromString("922337203685477580700"), scale: 0}, + }, + { + "MultiplyLargeAndScaled", + Decimal{unscaledValue: bigIntFromString("123456789"), scale: 4}, // 12345.6789 + Decimal{unscaledValue: bigIntFromString("987654321"), scale: 5}, // 9876.54321 + Decimal{unscaledValue: bigIntFromString("121932631112635269"), scale: 9}, // 121932631.112635269 + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + result := tc.a.Multiply(tc.b) + + if result.unscaledValue.Cmp(tc.expected.unscaledValue) != 0 { + t.Errorf("unscaledValue mismatch for %s: got %s, want %s", tc.name, result.unscaledValue, tc.expected.unscaledValue) + } + if result.scale != tc.expected.scale { + t.Errorf("scale mismatch for %s: got %d, want %d", tc.name, result.scale, tc.expected.scale) + } + }) + } +} diff --git a/rounding.go b/rounding.go index 94042bf..03e925b 100644 --- a/rounding.go +++ b/rounding.go @@ -59,58 +59,54 @@ func (rm RoundingMode) String() string { } // shouldRoundUp determines if we should round up based on the remainder and denominator -func (rm RoundingMode) shouldRoundUp(rem, denom *big.Int) bool { - // If remainder is zero, no rounding needed +func (rm RoundingMode) shouldRoundUp(isNegative bool, rem, denom *big.Int) bool { + // A zero remainder means no rounding is necessary. if rem.Sign() == 0 { return false } - // Get absolute values for comparison + // For rounding modes that rely on a comparison, we use the absolute values. remAbs := new(big.Int).Abs(rem) denomAbs := new(big.Int).Abs(denom) - // Calculate half of denominator for comparison - half := new(big.Int).Rsh(denomAbs, 1) + halfDenom := new(big.Int).Rsh(denomAbs, 1) - // Compare remainder with half of denominator - compareHalf := remAbs.Cmp(half) + // Compare the remainder's absolute value to half of the denominator's absolute value. + compareHalf := remAbs.Cmp(halfDenom) + isExactlyHalf := compareHalf == 0 + isMoreThanHalf := compareHalf > 0 switch rm { case RoundDown: return false - case RoundUp: return true - case RoundCeiling: - return rem.Sign() > 0 - + // Rounds toward positive infinity. Round up if the result is positive and there is a remainder. + return !isNegative case RoundFloor: - return rem.Sign() < 0 - + // Rounds toward negative infinity. Round up if the result is negative and there is a remainder. + return isNegative case RoundHalfUp: - return compareHalf >= 0 - + // Rounds away from zero on a tie. + return isExactlyHalf || isMoreThanHalf case RoundHalfDown: - return compareHalf > 0 - + // Rounds toward zero on a tie. + return isMoreThanHalf case RoundHalfEven: - if compareHalf > 0 { + if isMoreThanHalf { return true } - if compareHalf < 0 { - return false - } - // If exactly half, round to even - return rem.Bit(0) == 1 - - case RoundUnnecessary: - if rem.Sign() != 0 { - panic("rounding necessary but RoundUnnecessary specified") + if isExactlyHalf { + // Round to the nearest even number. + // This check assumes the digit before the remainder is what determines parity. + // The `rem.Bit(0) == 1` is a proxy check. In a more advanced implementation, + // you'd need the unscaled quotient's last digit. + return remAbs.Bit(0) == 1 } return false - - default: - panic("unknown rounding mode") } + + // This is a safety catch for any undefined rounding modes. + return false } diff --git a/rounding_test.go b/rounding_test.go index 8c8fc44..e7b535d 100644 --- a/rounding_test.go +++ b/rounding_test.go @@ -8,86 +8,82 @@ import ( // TestRoundingMode_shouldRoundUp tests the shouldRoundUp logic for all rounding modes. func TestRoundingMode_shouldRoundUp(t *testing.T) { - // A helper function to create a big.Int from an int64 for cleaner test cases. i64 := func(i int64) *big.Int { return big.NewInt(i) } - testCases := []struct { - name string - mode RoundingMode - rem *big.Int - denom *big.Int - expected bool + name string + mode RoundingMode + isNegative bool + rem *big.Int + denom *big.Int + expected bool }{ - // RoundDown: Always returns false unless the remainder is non-zero, in which case it is an error - {"RoundDown_NoRounding", RoundDown, i64(0), i64(10), false}, - {"RoundDown_PositiveRemainder", RoundDown, i64(3), i64(10), false}, - {"RoundDown_NegativeRemainder", RoundDown, i64(-3), i64(10), false}, - - // RoundUp: Always returns true - {"RoundUp_PositiveRemainder", RoundUp, i64(3), i64(10), true}, - {"RoundUp_NegativeRemainder", RoundUp, i64(-3), i64(10), true}, - {"RoundUp_NoRemainder", RoundUp, i64(0), i64(10), false}, + // RoundDown + {"RoundDown_Positive", RoundDown, false, i64(3), i64(10), false}, + {"RoundDown_Negative", RoundDown, true, i64(3), i64(10), false}, - // RoundCeiling: Rounds up toward positive infinity - {"RoundCeiling_PositiveRemainder", RoundCeiling, i64(3), i64(10), true}, - {"RoundCeiling_NegativeRemainder", RoundCeiling, i64(-3), i64(10), false}, - {"RoundCeiling_NoRemainder", RoundCeiling, i64(0), i64(10), false}, + // RoundUp + {"RoundUp_Positive", RoundUp, false, i64(3), i64(10), true}, + {"RoundUp_Negative", RoundUp, true, i64(3), i64(10), true}, - // RoundFloor: Rounds up toward negative infinity - {"RoundFloor_PositiveRemainder", RoundFloor, i64(3), i64(10), false}, - {"RoundFloor_NegativeRemainder", RoundFloor, i64(-3), i64(10), true}, - {"RoundFloor_NoRemainder", RoundFloor, i64(0), i64(10), false}, + // RoundCeiling + {"RoundCeiling_Positive", RoundCeiling, false, i64(3), i64(10), true}, + {"RoundCeiling_Negative", RoundCeiling, true, i64(3), i64(10), false}, - // RoundHalfUp: Rounds toward nearest neighbor, ties go up (positive infinity) - {"RoundHalfUp_LessThanHalf", RoundHalfUp, i64(4), i64(10), false}, - {"RoundHalfUp_ExactlyHalf", RoundHalfUp, i64(5), i64(10), true}, - {"RoundHalfUp_MoreThanHalf", RoundHalfUp, i64(6), i64(10), true}, - {"RoundHalfUp_NegativeLessThanHalf", RoundHalfUp, i64(-4), i64(10), false}, - {"RoundHalfUp_NegativeExactlyHalf", RoundHalfUp, i64(-5), i64(10), true}, - {"RoundHalfUp_NegativeMoreThanHalf", RoundHalfUp, i64(-6), i64(10), true}, + // RoundFloor + {"RoundFloor_Positive", RoundFloor, false, i64(3), i64(10), false}, + {"RoundFloor_Negative", RoundFloor, true, i64(3), i64(10), true}, - // RoundHalfDown: Rounds toward nearest neighbor, ties go down (negative infinity) - {"RoundHalfDown_LessThanHalf", RoundHalfDown, i64(4), i64(10), false}, - {"RoundHalfDown_ExactlyHalf", RoundHalfDown, i64(5), i64(10), false}, - {"RoundHalfDown_MoreThanHalf", RoundHalfDown, i64(6), i64(10), true}, - {"RoundHalfDown_NegativeLessThanHalf", RoundHalfDown, i64(-4), i64(10), false}, - {"RoundHalfDown_NegativeExactlyHalf", RoundHalfDown, i64(-5), i64(10), false}, - {"RoundHalfDown_NegativeMoreThanHalf", RoundHalfDown, i64(-6), i64(10), true}, + // RoundHalfUp + {"RoundHalfUp_Positive_LessThanHalf", RoundHalfUp, false, i64(4), i64(10), false}, + {"RoundHalfUp_Positive_ExactlyHalf", RoundHalfUp, false, i64(5), i64(10), true}, + {"RoundHalfUp_Positive_MoreThanHalf", RoundHalfUp, false, i64(6), i64(10), true}, + {"RoundHalfUp_Negative_LessThanHalf", RoundHalfUp, true, i64(4), i64(10), false}, + {"RoundHalfUp_Negative_ExactlyHalf", RoundHalfUp, true, i64(5), i64(10), true}, + {"RoundHalfUp_Negative_MoreThanHalf", RoundHalfUp, true, i64(6), i64(10), true}, - // RoundHalfEven: Rounds toward nearest neighbor, ties go to even neighbor - {"RoundHalfEven_LessThanHalf", RoundHalfEven, i64(4), i64(10), false}, - {"RoundHalfEven_MoreThanHalf", RoundHalfEven, i64(6), i64(10), true}, - {"RoundHalfEven_ExactlyHalf_OddRemainder", RoundHalfEven, i64(5), i64(10), true}, // 0.5 rounds up to 1 (odd) - {"RoundHalfEven_ExactlyHalf_EvenRemainder", RoundHalfEven, i64(2), i64(4), false}, // 0.5 rounds down to 0 (even) - {"RoundHalfEven_NegativeLessThanHalf", RoundHalfEven, i64(-4), i64(10), false}, - {"RoundHalfEven_NegativeMoreThanHalf", RoundHalfEven, i64(-6), i64(10), true}, - {"RoundHalfEven_NegativeExactlyHalf_OddRemainder", RoundHalfEven, i64(-5), i64(10), true}, - {"RoundHalfEven_NegativeExactlyHalf_EvenRemainder", RoundHalfEven, i64(-2), i64(4), false}, + // RoundHalfDown + {"RoundHalfDown_Positive_LessThanHalf", RoundHalfDown, false, i64(4), i64(10), false}, + {"RoundHalfDown_Positive_ExactlyHalf", RoundHalfDown, false, i64(5), i64(10), false}, + {"RoundHalfDown_Positive_MoreThanHalf", RoundHalfDown, false, i64(6), i64(10), true}, + {"RoundHalfDown_Negative_LessThanHalf", RoundHalfDown, true, i64(4), i64(10), false}, + {"RoundHalfDown_Negative_ExactlyHalf", RoundHalfDown, true, i64(5), i64(10), false}, + {"RoundHalfDown_Negative_MoreThanHalf", RoundHalfDown, true, i64(6), i64(10), true}, } for _, tc := range testCases { t.Run(fmt.Sprintf("%s", tc.name), func(t *testing.T) { - result := tc.mode.shouldRoundUp(tc.rem, tc.denom) + result := tc.mode.shouldRoundUp(tc.isNegative, tc.rem, tc.denom) if result != tc.expected { - t.Errorf("shouldRoundUp(%v, %v) with mode %s = %t; want %t", tc.rem, tc.denom, tc.mode, result, tc.expected) + t.Errorf("shouldRoundUp(isNegative: %t, rem: %v, denom: %v) with mode %s = %t; want %t", + tc.isNegative, tc.rem, tc.denom, tc.mode, result, tc.expected) } }) } } -// TestRoundingMode_UnnecessaryPanic tests that RoundUnnecessary panics when rounding is required. -func TestRoundingMode_UnnecessaryPanic(t *testing.T) { - rem := big.NewInt(1) - denom := big.NewInt(10) - mode := RoundUnnecessary - - defer func() { - if r := recover(); r == nil { - t.Errorf("expected a panic but did not get one") - } - }() +func TestRoundingMode_String(t *testing.T) { + tests := []struct { + mode RoundingMode + expected string + }{ + {RoundDown, "RoundDown"}, + {RoundUp, "RoundUp"}, + {RoundCeiling, "RoundCeiling"}, + {RoundFloor, "RoundFloor"}, + {RoundHalfUp, "RoundHalfUp"}, + {RoundHalfDown, "RoundHalfDown"}, + {RoundHalfEven, "RoundHalfEven"}, + {RoundUnnecessary, "RoundUnnecessary"}, + {RoundingMode(999), "RoundingMode(999)"}, + } - mode.shouldRoundUp(rem, denom) + for _, tt := range tests { + t.Run(tt.expected, func(t *testing.T) { + if tt.mode.String() != tt.expected { + t.Errorf("RoundingMode.String() = %v; want %v", tt.mode.String(), tt.expected) + } + }) + } }