fpdecimal

package module
v0.19.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 21, 2024 License: MIT Imports: 1 Imported by: 2

README

🛫 Fixed-Point Decimals

To use in money, look at github.com/nikolaydubina/fpmoney

Be Precise. Using floats to represent currency is almost criminal. — Robert.C.Martin, "Clean Code" p.301

codecov Go Reference Awesome Go Report Card OpenSSF Scorecard

  • int64 inside
  • does not use float neither in parsing nor printing
  • as fast as int64 in parsing, printing, arithmetics — 3x faser float, 20x faster shopspring/decimal, 30x faster fmt
  • zero-overhead
  • preventing error-prone fixed-point arithmetics
  • Fuzz tests, Benchmarks
  • JSON
  • 200LOC
import fp "github.com/nikolaydubina/fpdecimal"

var BuySP500Price = fp.FromInt(9000)

input := []byte(`{"sp500": 9000.023}`)

type Stocks struct {
    SP500 fp.Decimal `json:"sp500"`
}
var v Stocks
if err := json.Unmarshal(input, &v); err != nil {
    log.Fatal(err)
}

var amountToBuy fp.Decimal
if v.SP500.GreaterThan(BuySP500Price) {
    amountToBuy = amountToBuy.Add(v.SP500.Mul(fp.FromInt(2)))
}

fmt.Println(amountToBuy)
// Output: 18000.046
Implementation

Parsing and Printing is expensive operation and requires a lot of code. However, if you know that your numbers are always small and simple and you do not care or do not permit lots of fractions like -1234.567, then parsing and printing can be greatly simplified. Code is heavily influenced by hot-path from Go core strconv package.

It is wrapped into struct to prevent bugs:

  • block multiplication by fpdecimal type, which leads to increase in decimal fractions and loose of precision
  • block additions of untyped constants, which leads to errors if you forget to scale by factor
Benchmarks

Parse

$ go test -bench=BenchmarkParse -benchtime=5s -benchmem .
goos: darwin
goarch: arm64
pkg: github.com/nikolaydubina/fpdecimal
BenchmarkParse/fromString/small-10                             534307098            11.36 ns/op           0 B/op           0 allocs/op
BenchmarkParse/fromString/large-10                             254741558            23.42 ns/op           0 B/op           0 allocs/op
BenchmarkParse/UnmarshalJSON/small-10                          816873427             7.32 ns/op           0 B/op           0 allocs/op
BenchmarkParse/UnmarshalJSON/large-10                          272173255            22.16 ns/op           0 B/op           0 allocs/op
BenchmarkParse_int_strconv_Atoi/small-10                      1000000000             4.87 ns/op           0 B/op           0 allocs/op
BenchmarkParse_int_strconv_Atoi/large-10                       420536834            14.31 ns/op           0 B/op           0 allocs/op
BenchmarkParse_int_strconv_ParseInt/small/int32-10             561137575            10.67 ns/op           0 B/op           0 allocs/op
BenchmarkParse_int_strconv_ParseInt/small/int64-10             564200026            10.64 ns/op           0 B/op           0 allocs/op
BenchmarkParse_int_strconv_ParseInt/large/int64-10             219626983            27.17 ns/op           0 B/op           0 allocs/op
BenchmarkParse_float_strconv_ParseFloat/small/float32-10       345666214            17.36 ns/op           0 B/op           0 allocs/op
BenchmarkParse_float_strconv_ParseFloat/small/float64-10       339620222            17.68 ns/op           0 B/op           0 allocs/op
BenchmarkParse_float_strconv_ParseFloat/large/float32-10       128824344            46.68 ns/op           0 B/op           0 allocs/op
BenchmarkParse_float_strconv_ParseFloat/large/float64-10       128140617            46.89 ns/op           0 B/op           0 allocs/op
BenchmarkParse_float_fmt_Sscanf/small-10                        21202892           281.6  ns/op          69 B/op           2 allocs/op
BenchmarkParse_float_fmt_Sscanf/large-10                        10074237           599.2  ns/op          88 B/op           3 allocs/op
PASS
ok      github.com/nikolaydubina/fpdecimal    116.249s

Print

$ go test -bench=BenchmarkPrint -benchtime=5s -benchmem .
goos: darwin
goarch: arm64
pkg: github.com/nikolaydubina/fpdecimal
BenchmarkPrint/small-10                                      191982066            31.24 ns/op           8 B/op           1 allocs/op
BenchmarkPrint/large-10                                      150874335            39.89 ns/op          24 B/op           1 allocs/op
BenchmarkPrint_int_strconv_Itoa/small-10                     446302868            13.39 ns/op           3 B/op           0 allocs/op
BenchmarkPrint_int_strconv_Itoa/large-10                     237484774            25.20 ns/op          18 B/op           1 allocs/op
BenchmarkPrint_int_strconv_FormatInt/small-10                444861666            13.70 ns/op           3 B/op           0 allocs/op
BenchmarkPrint_float_strconv_FormatFloat/small/float32-10     55003357           104.2  ns/op          31 B/op           2 allocs/op
BenchmarkPrint_float_strconv_FormatFloat/small/float64-10     43565430           137.4  ns/op          31 B/op           2 allocs/op
BenchmarkPrint_float_strconv_FormatFloat/large/float32-10     64069650            92.07 ns/op          48 B/op           2 allocs/op
BenchmarkPrint_float_strconv_FormatFloat/large/float64-10     68441746            87.36 ns/op          48 B/op           2 allocs/op
BenchmarkPrint_float_fmt_Sprintf/small-10                     46503666           127.7  ns/op          16 B/op           2 allocs/op
BenchmarkPrint_float_fmt_Sprintf/large-10                     51764224           115.8  ns/op          28 B/op           2 allocs/op
PASS
ok      github.com/nikolaydubina/fpdecimal    79.192s

Arithmetics

$ go test -bench=BenchmarkArithmetic -benchtime=5s -benchmem .
goos: darwin
goarch: arm64
pkg: github.com/nikolaydubina/fpdecimal
BenchmarkArithmetic/add-10                   1000000000             0.316 ns/op           0 B/op           0 allocs/op
BenchmarkArithmetic/div-10                   1000000000             0.950 ns/op           0 B/op           0 allocs/op
BenchmarkArithmetic/divmod-10                1000000000             1.890 ns/op           0 B/op           0 allocs/op
BenchmarkArithmetic_int64/add-10             1000000000             0.314 ns/op           0 B/op           0 allocs/op
BenchmarkArithmetic_int64/div-10             1000000000             0.316 ns/op           0 B/op           0 allocs/op
BenchmarkArithmetic_int64/divmod-10          1000000000             1.261 ns/op           0 B/op           0 allocs/op
BenchmarkArithmetic_int64/mod-10             1000000000             0.628 ns/op           0 B/op           0 allocs/op
PASS
ok      github.com/nikolaydubina/fpdecimal    6.721s

References

Appendix A: Comparison to other libraries

Appendix B: Benchmarking shopspring/decimal

2022-05-28

$ go test -bench=. -benchtime=5s -benchmem ./...
goos: darwin
goarch: arm64
pkg: github.com/shopspring/decimal
BenchmarkNewFromFloatWithExponent-10                        59701516          97.7 ns/op         106 B/op           4 allocs/op
BenchmarkNewFromFloat-10                                    14771503         410.3 ns/op          67 B/op           2 allocs/op
BenchmarkNewFromStringFloat-10                              16246342         375.2 ns/op         175 B/op           5 allocs/op
Benchmark_FloorFast-10                                    1000000000           2.1 ns/op           0 B/op           0 allocs/op
Benchmark_FloorRegular-10                                   53857244         106.3 ns/op         112 B/op           6 allocs/op
Benchmark_DivideOriginal-10                                        7   715322768   ns/op   737406446 B/op    30652495 allocs/op
Benchmark_DivideNew-10                                            22   262893689   ns/op   308046721 B/op    12054905 allocs/op
BenchmarkDecimal_RoundCash_Five-10                           9311530         636.5 ns/op         616 B/op          28 allocs/op
Benchmark_Cmp-10                                                  44   133191579   ns/op          24 B/op           1 allocs/op
Benchmark_decimal_Decimal_Add_different_precision-10        31561636         176.6 ns/op         280 B/op           9 allocs/op
Benchmark_decimal_Decimal_Sub_different_precision-10        36892767         164.4 ns/op         240 B/op           9 allocs/op
Benchmark_decimal_Decimal_Add_same_precision-10            134831919          44.9 ns/op          80 B/op           2 allocs/op
Benchmark_decimal_Decimal_Sub_same_precision-10            134902627          43.1 ns/op          80 B/op           2 allocs/op
BenchmarkDecimal_IsInteger-10                               92543083          66.1 ns/op           8 B/op           1 allocs/op
BenchmarkDecimal_NewFromString-10                             827455        7382   ns/op        3525 B/op         216 allocs/op
BenchmarkDecimal_NewFromString_large_number-10                212538       28836   ns/op       16820 B/op         360 allocs/op
BenchmarkDecimal_ExpHullAbraham-10                             10000      572091   ns/op      486628 B/op         568 allocs/op
BenchmarkDecimal_ExpTaylor-10                                  26343      222915   ns/op      431226 B/op        3172 allocs/op
PASS
ok      github.com/shopspring/decimal    123.541sa

Appendix C: Why this is good fit for money?

There are only ~200 currencies in the world. All currencies have at most 3 decimal digits, thus it is sufficient to handle 3 decimal fractions. Next, currencies without decimal digits are typically 1000x larger than dollar, but even then maximum number that fits into int64 (without 3 decimal fractions) is 9 223 372 036 854 775.807 which is ~9 quadrillion. This should be enough for most operations with money.

Appendix D: Is it safe to use arithmetic operators in Go?

Sort of...

In one of iterations, I did Type Alias, but it required some effort to use it carefully.

Operations with defined types (variables) will fail.

var a int64
var b fpdecimal.FromInt(1000)

// does not compile
a + b

However, untyped constants will be resolved to underlying type int64 and will be allowed.

const a 10000
var b fpdecimal.FromInt(1000)

// compiles
a + b

// also compiles
b - 42

// this one too
b *= 23

Is this a problem?

  • For multiplication and division - yes, it can be. You have to be careful not to multiply two fpdecimal numbers, since scaling factor will quadruple. Multiplying by constants is ok tho.
  • For addition substraction - yes, it can be. You have to be careful and remind yourself that constants would be reduced 1000x.

Both of this can be addressed at compile time by providing linter. This can be also addressed by wrapping into a struct and defining methods. Formed is hard to achieve in Go, due to lack of operator overload and lots of work required to write AST parser. Later has been implemented in this pacakge, and, as benchmarks show, without any extra memory or calls overhead as compared to int64.

Appendix E: Print into destination

To avoid mallocs, it is advantageous to print formatted value to pre-allocated destination. Similarly, to strconv.AppendInt, we provide AppendFixedPointDecimal. This is utilized in github.com/nikolaydubina/fpmoney package.

BenchmarkFixedPointDecimalToString/small-10     28522474         35.43 ns/op       24 B/op        1 allocs/op
BenchmarkFixedPointDecimalToString/large-10     36883687         32.32 ns/op       24 B/op        1 allocs/op
BenchmarkAppendFixedPointDecimal/small-10       38105520         30.51 ns/op      117 B/op        0 allocs/op
BenchmarkAppendFixedPointDecimal/large-10       55147478         29.52 ns/op      119 B/op        0 allocs/op

Appendix F: DivMod notation

In early versions, Div and Mul operated on int and Div returned remainder. As recommended by @vanodevium and more in line with other common libraries, notation is changed. Bellow is survey as of 2023-05-18.

Go, https://pkg.go.dev/math/big

func (z *Int) Div(x, y *Int) *Int
func (z *Int) DivMod(x, y, m *Int) (*Int, *Int)
func (z *Int) Mod(x, y *Int) *Int

Go, github.com/shopspring/decimal

func (d Decimal) Div(d2 Decimal) Decimal
// X no DivMod
func (d Decimal) Mod(d2 Decimal) Decimal
func (d Decimal) DivRound(d2 Decimal, precision int32) Decimal

Python, https://docs.python.org/3/library/decimal.html

divide(x, y) number
divide_int(x, y) number // truncates
divmod(x, y) number
remainder(x, y) number

Pytorch, https://pytorch.org/docs/stable/generated/torch.div.html

torch.div(input, other, *, rounding_mode=None, out=None) → [Tensor] // discards remainder
torch.remainder(input, other, *, out=None) → [Tensor] // remainder

numpy, https://numpy.org/doc/stable/reference/generated/numpy.divmod.html

np.divmod(x, y) (number, number) // is equivalent to (x // y, x % y
np.mod(x, y) number
np.remainder(x, y) number
np.divide(x, y) number
np.true_divide(x, y) number // same as divide
np.floor_divide(x, y) number // rounding down

Appendix G: generics switch for decimal counting

Go does not support numerics in templates. However, defining multiple types each associated with specific number of decimals and passing them to functions and defining constraint as union of these types — is an attractive option. This does not work well since Go does not support switch case (casting generic) back to integer well.

Appendix H: string vs []byte in interface

The typical usage of parsing number is through some JSON or other mechanism. Those APIs are dealing with []byte. Now, conversion from []byte to string requires to copy data, since string is immutable. To improve performance, we are using []byte in signatures.

Using string

BenchmarkParse/fromString/small-10                 831217767             7.07 ns/op           0 B/op           0 allocs/op
BenchmarkParse/fromString/large-10                 275009497            21.79 ns/op           0 B/op           0 allocs/op
BenchmarkParse/UnmarshalJSON/small-10              553035127            10.98 ns/op           0 B/op           0 allocs/op
BenchmarkParse/UnmarshalJSON/large-10              248815030            24.14 ns/op           0 B/op           0 allocs/op

Using []byte

BenchmarkParse/fromString/small-10                 523937236            11.32 ns/op           0 B/op           0 allocs/op
BenchmarkParse/fromString/large-10                 257542226            23.23 ns/op           0 B/op           0 allocs/op
BenchmarkParse/UnmarshalJSON/small-10              809793006             7.31 ns/op           0 B/op           0 allocs/op
BenchmarkParse/UnmarshalJSON/large-10              272087984            22.04 ns/op           0 B/op           0 allocs/op

Documentation

Index

Examples

Constants

This section is empty.

Variables

View Source
var FractionDigits uint8 = 3

FractionDigits that operations will use. Warning, after change, existing variables are not updated. Likely you want to use this once per runtime and in `func init()`.

View Source
var Zero = Decimal{}

Functions

func AppendFixedPointDecimal added in v0.11.0

func AppendFixedPointDecimal(b []byte, v int64, p uint8) []byte

AppendFixedPointDecimal appends formatted fixed point decimal to destination buffer. Returns appended slice. This is efficient for avoiding memory copy.

func FixedPointDecimalToString

func FixedPointDecimalToString(v int64, p uint8) string

FixedPointDecimalToString formats fixed-point decimal to string

func ParseFixedPointDecimal

func ParseFixedPointDecimal(s []byte, p uint8) (int64, error)

ParseFixedPointDecimal parses fixed-point decimal of p fractions into int64.

Types

type Decimal added in v0.14.0

type Decimal struct {
	// contains filtered or unexported fields
}

Decimal is a decimal with fixed number of fraction digits. By default, uses 3 fractional digits. For example, values with 3 fractional digits will fit in ~9 quadrillion. Fractions lower than that are discarded in operations. Max: +9223372036854775.807 Min: -9223372036854775.808

Example
package main

import (
	"encoding/json"
	"fmt"
	"log"

	fp "github.com/nikolaydubina/fpdecimal"
)

func main() {
	var BuySP500Price = fp.FromInt(9000)

	input := []byte(`{"sp500": 9000.023}`)

	type Stocks struct {
		SP500 fp.Decimal `json:"sp500"`
	}
	var v Stocks
	if err := json.Unmarshal(input, &v); err != nil {
		log.Fatal(err)
	}

	var amountToBuy fp.Decimal
	if v.SP500.GreaterThan(BuySP500Price) {
		amountToBuy = amountToBuy.Add(v.SP500.Mul(fp.FromInt(2)))
	}

	fmt.Println(amountToBuy)
}
Output:

18000.046
Example (Skip_trailing_zeros)
package main

import (
	"fmt"

	fp "github.com/nikolaydubina/fpdecimal"
)

func main() {
	v, _ := fp.FromString("102.0020")
	fmt.Println(v)
}
Output:

102.002
Example (Skip_whole_fraction)
package main

import (
	"fmt"

	fp "github.com/nikolaydubina/fpdecimal"
)

func main() {
	v, _ := fp.FromString("1013.0000")
	fmt.Println(v)
}
Output:

1013

func FromFloat added in v0.14.0

func FromFloat[T float32 | float64](v T) Decimal

func FromInt added in v0.14.0

func FromInt[T integer](v T) Decimal
Example (Int)
package main

import (
	"fmt"

	fp "github.com/nikolaydubina/fpdecimal"
)

func main() {
	var x int = -100
	v := fp.FromInt(x)
	fmt.Print(v)
}
Output:

-100
Example (Int8)
package main

import (
	"fmt"

	fp "github.com/nikolaydubina/fpdecimal"
)

func main() {
	var x int8 = -100
	v := fp.FromInt(x)
	fmt.Print(v)
}
Output:

-100
Example (Uint)
package main

import (
	"fmt"

	fp "github.com/nikolaydubina/fpdecimal"
)

func main() {
	var x uint = 100
	v := fp.FromInt(x)
	fmt.Print(v)
}
Output:

100
Example (Uint8)
package main

import (
	"fmt"

	fp "github.com/nikolaydubina/fpdecimal"
)

func main() {
	var x uint8 = 100
	v := fp.FromInt(x)
	fmt.Print(v)
}
Output:

100

func FromIntScaled added in v0.14.0

func FromIntScaled[T integer](v T) Decimal

FromIntScaled expects value already scaled to minor units

func FromString added in v0.14.0

func FromString(s string) (Decimal, error)

func Max added in v0.19.0

func Max(vs ...Decimal) Decimal
Example
package main

import (
	"fmt"

	fp "github.com/nikolaydubina/fpdecimal"
)

func main() {
	max := fp.Max(fp.FromInt(100), fp.FromFloat(0.999), fp.FromFloat(100.001))
	fmt.Print(max)
}
Output:

100.001
Example (Empty)
package main

import (
	"fmt"

	fp "github.com/nikolaydubina/fpdecimal"
)

func main() {
	defer func() { fmt.Print(recover()) }()
	fp.Max()
}
Output:

max of empty set is undefined

func Min added in v0.19.0

func Min(vs ...Decimal) Decimal
Example
package main

import (
	"fmt"

	fp "github.com/nikolaydubina/fpdecimal"
)

func main() {
	min := fp.Min(fp.FromInt(100), fp.FromFloat(0.999), fp.FromFloat(100.001))
	fmt.Print(min)
}
Output:

0.999
Example (Empty)
package main

import (
	"fmt"

	fp "github.com/nikolaydubina/fpdecimal"
)

func main() {
	defer func() { fmt.Print(recover()) }()
	fp.Min()
}
Output:

min of empty set is undefined

func (Decimal) Add added in v0.14.0

func (a Decimal) Add(b Decimal) Decimal

func (Decimal) Compare added in v0.14.0

func (a Decimal) Compare(b Decimal) int

func (Decimal) Div added in v0.14.0

func (a Decimal) Div(b Decimal) Decimal
Example
package main

import (
	"fmt"

	fp "github.com/nikolaydubina/fpdecimal"
)

func main() {
	x, _ := fp.FromString("1.000")
	p := x.Div(fp.FromInt(3))
	fmt.Print(p)
}
Output:

0.333
Example (Whole)
package main

import (
	"fmt"

	fp "github.com/nikolaydubina/fpdecimal"
)

func main() {
	x, _ := fp.FromString("1.000")
	p := x.Div(fp.FromInt(5))
	fmt.Print(p)
}
Output:

0.2

func (Decimal) DivMod added in v0.16.0

func (a Decimal) DivMod(b Decimal) (part, remainder Decimal)
Example
package main

import (
	"fmt"

	fp "github.com/nikolaydubina/fpdecimal"
)

func main() {
	x, _ := fp.FromString("1.000")
	p, m := x.DivMod(fp.FromInt(3))
	fmt.Print(p, m)
}
Output:

0.333 0.001
Example (Whole)
package main

import (
	"fmt"

	fp "github.com/nikolaydubina/fpdecimal"
)

func main() {
	x, _ := fp.FromString("1.000")
	p, m := x.DivMod(fp.FromInt(5))
	fmt.Print(p, m)
}
Output:

0.2 0

func (Decimal) Equal added in v0.14.0

func (a Decimal) Equal(b Decimal) bool

func (Decimal) Float32 added in v0.14.0

func (a Decimal) Float32() float32

func (Decimal) Float64 added in v0.14.0

func (a Decimal) Float64() float64

func (Decimal) GreaterThan added in v0.14.0

func (a Decimal) GreaterThan(b Decimal) bool

func (Decimal) GreaterThanOrEqual added in v0.14.0

func (a Decimal) GreaterThanOrEqual(b Decimal) bool

func (Decimal) LessThan added in v0.14.0

func (a Decimal) LessThan(b Decimal) bool

func (Decimal) LessThanOrEqual added in v0.14.0

func (a Decimal) LessThanOrEqual(b Decimal) bool

func (Decimal) MarshalJSON added in v0.14.0

func (v Decimal) MarshalJSON() ([]byte, error)

func (Decimal) Mod added in v0.19.0

func (a Decimal) Mod(b Decimal) Decimal
Example
package main

import (
	"fmt"

	fp "github.com/nikolaydubina/fpdecimal"
)

func main() {
	x, _ := fp.FromString("1.000")
	m := x.Mod(fp.FromInt(3))
	fmt.Print(m)
}
Output:

0.001

func (Decimal) Mul added in v0.14.0

func (a Decimal) Mul(b Decimal) Decimal

func (Decimal) Scaled added in v0.15.0

func (a Decimal) Scaled() int64

func (Decimal) String added in v0.14.0

func (a Decimal) String() string

func (Decimal) Sub added in v0.14.0

func (a Decimal) Sub(b Decimal) Decimal

func (*Decimal) UnmarshalJSON added in v0.14.0

func (v *Decimal) UnmarshalJSON(b []byte) (err error)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL