udecimal

repository·master·Indexed 41 days ago

https://github.com/quagmt/udecimal

A high-performance, high-precision, zero-allocation fixed-point decimal library for Go, optimized for financial applications. It supports up to 19 decimal places and utilizes a custom 128-bit unsigned integer (u128) implementation to minimize memory allocations, falling back to math/big.Int only when necessary. The library provides comprehensive arithmetic operations, explicit rounding methods (Banker's, Half Away From Zero, etc.), and supports JSON, Binary, Text, and SQL serialization. Requires Go 1.23 or higher.

Tokens
6.9K
Snippets
28
Records
39
Agent score
52%

What's inside udecimal

  1. How udecimal achieves high performance and zero allocation

    master

    The Decimal type is a fixed-point representation consisting of a sign (neg), a coefficient (coef), and a precision (prec).

    To minimize memory allocation, the coefficient coef uses a custom bint type:

    • For coefficients that fit within a 128-bit unsigned integer (u128), arithmetic is performed using u128 operations, which are extremely fast and require zero memory allocation.
    • If a result exceeds the u128 capacity, the library falls back to the big.Int API. While this involves memory allocation and is slower, the 128-bit range is large enough that these cases are rare in most financial applications.

    Precision Limits:

    • Supports up to 19 decimal places.
    • With precision 0, it can store values up to approximately $\pm 340$ undecillion.
    • With precision 19, it can store values up to approximately $\pm 34$ quintillion.
  2. Understand udecimal benchmark fallback behavior

    master
    In udecimal benchmarks, some operations include a fallback suffix (e.g., BenchmarkDivFallback). This indicates that the operation could not be performed using the optimized u128 implementation and instead fell back to the big.Int API. Fallback operations typically result in higher latency (ns/op) and more memory allocations (B/op and allocs/op) compared to standard operations.
  3. Apply rounding methods to Decimals

    master

    udecimal does not perform implicit rounding. If an arithmetic operation results in more digits than the maximum precision, the extra digits are truncated. To round a value, you must explicitly call one of the following methods:

    • RoundBank(prec int): Banker's rounding (round half to even).
    • RoundAwayFromZero(prec int): Round away from zero.
    • RoundHAZ(prec int): Half away from zero.
    • RoundHTZ(prec int): Half toward zero.
    • Trunc(prec int): Truncate to the specified precision.
    • Floor(): Round down to the nearest integer.
    • Ceil(): Round up to the nearest integer.

    Example

    a, _ := udecimal.NewFromFloat64(1.345)
    fmt.Println(a.RoundBank(2))         // 1.34
    fmt.Println(a.RoundAwayFromZero(2)) // 1.35
    fmt.Println(a.RoundHAZ(2))          // 1.35
    fmt.Println(a.RoundHTZ(2))          // 1.34
    package main
    
    import (
    	"fmt"
    	"github.com/quagmt/udecimal"
    )
    
    func main() {
    	// Create a new decimal number
    	a, _ := udecimal.NewFromFloat64(1.345) // a = 1.345
    
    	// Rounding
    	fmt.Println(a.RoundBank(2))         // banker's rounding: 1.345 -> 1.34
    	fmt.Println(a.RoundAwayFromZero(2)) // round away from zero: 1.345 -> 1.35
    	fmt.Println(a.RoundHAZ(2))          // half away from zero: 1.345 -> 1.35
    	fmt.Println(a.RoundHTZ(2))         // half towards zero: 1.345 -> 1.34
    }
  4. Create and manipulate Decimal numbers

    master

    Use the udecimal package to create decimal numbers from various types and perform arithmetic operations. All arithmetic operations return a new Decimal value, making the library concurrent-safe.

    Creation

    • NewFromInt64(value int64, prec uint8): Creates a decimal from an integer with a specified precision.
    • NewFromFloat64(f float64): Creates a decimal from a float64.
    • Parse(s string): Parses a decimal from a string.

    Arithmetic

    • Add(other Decimal)
    • Sub(other Decimal)
    • Mul(other Decimal)
    • Div(other Decimal)

    Display

    • String(): Returns the decimal as a string.
    • StringFixed(prec int): Returns the decimal as a string with a fixed number of decimal places.
    • InexactFloat64(): Returns the decimal as a float64.
    package main
    
    import (
    	"fmt"
    	"github.com/quagmt/udecimal"
    )
    
    func main() {
    	// Create a new decimal number
    	a, _ := udecimal.NewFromInt64(123456, 3)              // a = 123.456
    	b, _ := udecimal.NewFromInt64(-123456, 4)             // b = -12.3456
    	c, _ := udecimal.NewFromFloat64(1.2345)               // c = 1.2345
    	d, _ := udecimal.Parse("4123547.1234567890123456789") // d = 4123547.1234567890123456789
    
    	// Basic arithmetic operations
    	fmt.Println(a.Add(b)) // 123.456 - 12.3456 = 111.1104
    	fmt.Println(a.Sub(b)) // 123.456 + 12.3456 = 135.8016
    	fmt.Println(a.Mul(b)) // 123.456 * -12.3456 = -1524.1383936
    	fmt.Println(a.Div(b)) // 123.456 / -12.3456 = -10
    	fmt.Println(a.Div(d)) // 123.456 / 4123547.1234567890123456789 = 0.0000299392722585176
    
    	// Display
    	fmt.Println(a.String())         // 123.456
    	fmt.Println(a.StringFixed(10))  // 123.4560000000
    	fmt.Println(a.InexactFloat64()) // 123.456
    }
  5. Understand ParseMode types

    master

    The ParseMode type defines the behavior of the parser when encountering decimal precision that exceeds the library's default limits.

    • ParseModeError: The default mode. It ensures strict precision by returning an error if the input string has more decimal digits exceeding defaultPrec.
    • ParseModeTrunc: A permissive mode that truncates digits beyond defaultPrec to allow parsing to continue, potentially with precision loss.
  6. Use bint for high-performance decimal arithmetic

    master

    The bint type represents a whole decimal number (the coefficient) without a decimal point. It is optimized for performance by using a 128-bit unsigned integer (u128) for small numbers to allow zero-allocation operations. If a value exceeds the 128-bit range, it automatically falls back to using math/big.Int.

    Note: bint values are always stored as positive numbers; the sign is handled by the higher-level decimal type.

  7. Configure the default parse mode

    master

    You can control how the library handles string numbers that exceed the default precision (defaultPrec) by setting the global default parse mode using SetDefaultParseMode.

    Available modes:

    • ParseModeError: Returns an error if the string number has more than defaultPrec decimal digits.
    • ParseModeTrunc: Truncates the excess decimal digits instead of returning an error. This is useful when dealing with external data sources that might have higher precision than your application requires.
    // To set the mode to truncate instead of erroring on high precision:
    udecimal.SetDefaultParseMode(udecimal.ParseModeTrunc)
  8. Configure default precision

    master

    You can set a global default precision for the package using SetDefaultPrecision(prec uint8). This affects how many digits after the decimal point are used when precision is not explicitly provided in certain operations (like Mul or Div).

    • Constraints: Precision must be between 1 and 19.
    • Usage: This should be called only once at the beginning of your application.
    • Behavior: Panics if prec > 19 or prec == 0.
    // Set default precision to 4 digits for the whole app
    udecimal.SetDefaultPrecision(4)
  9. Review udecimal performance benchmarks

    master

    The udecimal library provides benchmarks for various categories including arithmetic operations, string parsing, and JSON/Binary marshaling. Performance results vary based on hardware, but they serve as a reference for the library's efficiency.

    Key benchmark categories include:

    • Arithmetic operations: Add, Sub, Mul, Div, Pow.
    • Parsing string: Parse and String conversions.
    • Marshal/Unmarshal: MarshalJSON, UnmarshalJSON, MarshalBinary, and UnmarshalBinary.
    # Arithmetic operations
    BenchmarkAdd/1234567890123456789.1234567890123456879.Add(1111.1789)-32                     	100000000	 10.91 ns/op	   0 B/op	 0 allocs/op
    BenchmarkSub/123.456.Sub(0.123)-32                                                       	100000000	 10.75 ns/op	   0 B/op	 0 allocs/op
    BenchmarkMul/1234.1234567890123456879.Mul(1111.1789)-32                                  	100000000	 10.43 ns/op	   0 B/op	 0 allocs/op
    BenchmarkDiv/1234567890123456789.1234567890123456879.Div(1111.1789)-32                    	100000000	 10.46 ns/op	   0 B/op	 0 allocs/op
    BenchmarkDivFallback/12345679012345679890123456789.1234567890123456789.Div(999999)-32     	  4187094	 302.1 ns/op	 264 B/op	 7 allocs/op
    
    # Parsing string
    BenchmarkParse/1234567890123456789.1234567890123456879-32                                  	 32111433	 38.21 ns/op	   0 B/op	 0 allocs/op
    BenchmarkString/1234567890123456789.1234567890123456879-32                                 	 14577884	 76.50 ns/op	  48 B/op	 1 allocs/op
    
    # Marshal/Unmarshal
    BenchmarkMarshalJSON/1234567890123456789.1234567890123456879-32                            	 13965998	 77.22 ns/op	  48 B/op	 1 allocs/op
    BenchmarkUnmarshalBinary/1234567890123456789.1234567890123456879-32                       	554818034	 2.034 ns/op	   0 B/op	 0 allocs/op
  10. Compare bint values with Cmp and GT

    master

    You can compare two bint values using Cmp or GT.

    • Cmp(v bint) int: Returns 0 if the values are equal, 1 if u > v, and -1 if u < v.
    • GT(v bint) bool: Returns true if u > v.
    // Example comparison
    if val1.GT(val2) {
        // val1 is greater than val2
    }
    
    result := val1.Cmp(val2)