udecimal

build Go Report Card codecov GoDoc Awesome Go

High performance, high precision, zero allocation fixed-point decimal number for financial applications.

Installation

go get github.com/quagmt/udecimal

Features

NOTE: This library does not perform implicit rounding. If the result of an operation exceeds the maximum precision, extra digits are truncated. All rounding methods must be explicitly invoked. (see Rounding Methods for more details)

Documentation

Usage

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

	// Rounding
	fmt.Println(c.RoundBank(3)) // banker's rounding: 1.2345 -> 1.234
	fmt.Println(c.RoundHAZ(3))  // half away from zero: 1.2345 -> 1.235
	fmt.Println(c.RoundHTZ(3))  // half towards zero: 1.2345 -> 1.234
	fmt.Println(c.Trunc(2))     // truncate: 1.2345 -> 1.23
	fmt.Println(c.Floor())      // floor: 1.2345 -> 1
	fmt.Println(c.Ceil())       // ceil: 1.2345 -> 2

	// Display
	fmt.Println(a.String())         // 123.456
	fmt.Println(a.StringFixed(10))  // 123.4560000000
	fmt.Println(a.InexactFloat64()) // 123.456
}

Why another decimal library?

There are already a couple of decimal libraries available in Go, such as shopspring/decimal, cockroachdb/apd, govalues/decimal, etc. However, each of these libraries has its own limitations, for example:

This library is designed to address these limitations, providing both high performance and zero allocation while maintaining an acceptable range of precision, which is suitable for most financial applications.

Rounding Methods

Rounding numbers can often be challenging and confusing due to the variety of methods available. Each method serves specific purposes, and it’s common for developers to make mistakes or incorrect assumptions about how rounding should be performed. For example, the result of round(1.45) could be either 1.4 or 1.5, depending on the rounding method used.

This issue is particularly critical in financial applications, where even minor rounding errors can accumulate and lead to significant financial losses. To mitigate such errors, this library intentionally avoids implicit rounding. If the result of an operation exceeds the maximum precision specified by developers beforehand, extra digits are truncated. Developers need to explicitly choose the rounding method they want to use. The supported rounding methods are:

Examples:

package main

import (
	"fmt"

	"github.com/quagmt/udecimal"
)

func main() {
	// Create a new decimal number
	a, _ := udecimal.NewFromFloat64(1.45) // a = 1.45

	// Rounding
	fmt.Println(a.RoundBank(1)) // banker's rounding: 1.45 -> 1.4
	fmt.Println(a.RoundHAZ(1))  // half away from zero: 1.45 -> 1.5
	fmt.Println(a.RoundHTZ(1))  // half towards zero: 1.45 -> 1.4
}

How it works

As mentioned above, this library is not always memory allocation free. However, those cases where we need to allocate memory are incredily rare. To understand why, let’s take a look at how the Decimal type is implemented.

The Decimal type represents a fixed-point decimal number. It consists of three components: sign, coefficient, and prec. The number is represented as:

// decimal value = (neg == true ? -1 : 1) * coef * 10^(-prec)
type Decimal struct {
	coef bint
	neg bool
	prec uint8 // 0 <= prec <= 19
}

// Example:
// 123.456 = 123456 * 10^-3
// -> neg = false, coef = 123456, prec = 3

// -123.456 = -123456 / 10^-3
// -> neg = true, coef = 123456, prec = 3

You can notice that coef data type is bint, which is a custom data type:

type bint struct {
	// For coefficients exceeding u128
	bigInt *big.Int

	// For coefficients less than 2^128
	u128 u128
}

The bint type can store coefficients up to 2^128 - 1 using u128. Arithmetic operations with u128 are fast and require no memory allocation. If result of an arithmetic operation exceeds u128 capacity, the whole operation will be performed using big.Int API. Such operations are slower and do involve memory allocation. However, those cases are rare in financial applications due to the extensive range provided by a 128-bit unsigned integer, for example:

Therefore, in most cases you can expect high performance and no memory allocation when using this library.

Credits

This library is inspired by govalues/decimal and lukechampine/uint128

License

MIT