{"id":13412841,"url":"https://github.com/nikolaydubina/fpdecimal","last_synced_at":"2025-04-14T16:11:56.099Z","repository":{"id":39628006,"uuid":"496980334","full_name":"nikolaydubina/fpdecimal","owner":"nikolaydubina","description":"🛫 Fixed-Point Decimals","archived":false,"fork":false,"pushed_at":"2024-11-29T12:23:47.000Z","size":110,"stargazers_count":31,"open_issues_count":1,"forks_count":1,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-03-11T16:15:22.966Z","etag":null,"topics":["decoding","encoding","fixed-point-arithmetic","go","json","money","numerics","performance","serialization"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/nikolaydubina.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":"CITATION.cff","codeowners":".github/CODEOWNERS","security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-05-27T12:01:28.000Z","updated_at":"2024-12-27T22:53:54.000Z","dependencies_parsed_at":"2023-01-31T11:31:15.769Z","dependency_job_id":"a0e29e7f-c554-4bcf-87db-abc87ca68379","html_url":"https://github.com/nikolaydubina/fpdecimal","commit_stats":null,"previous_names":["nikolaydubina/fpfloat"],"tags_count":23,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nikolaydubina%2Ffpdecimal","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nikolaydubina%2Ffpdecimal/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nikolaydubina%2Ffpdecimal/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nikolaydubina%2Ffpdecimal/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/nikolaydubina","download_url":"https://codeload.github.com/nikolaydubina/fpdecimal/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":245972665,"owners_count":20702721,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["decoding","encoding","fixed-point-arithmetic","go","json","money","numerics","performance","serialization"],"created_at":"2024-07-30T20:01:29.930Z","updated_at":"2025-03-30T03:31:37.623Z","avatar_url":"https://github.com/nikolaydubina.png","language":"Go","funding_links":[],"categories":["Financial","金融"],"sub_categories":["Search and Analytic Databases","检索及分析资料库","Advanced Console UIs"],"readme":"# Fixed-Point Decimals\n\n\u003e To use in money, look at [github.com/nikolaydubina/fpmoney](https://github.com/nikolaydubina/fpmoney)\n\n\u003e _Be Precise. Using floats to represent currency is almost criminal. — Robert.C.Martin, \"Clean Code\" p.301_\n\n[![codecov](https://codecov.io/gh/nikolaydubina/fpdecimal/branch/main/graph/badge.svg?token=0pf0P5qloX)](https://codecov.io/gh/nikolaydubina/fpdecimal)\n[![Go Reference](https://pkg.go.dev/badge/github.com/nikolaydubina/fpdecimal.svg)](https://pkg.go.dev/github.com/nikolaydubina/fpdecimal)\n[![Awesome](https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg)](https://github.com/avelino/awesome-go#financial)\n[![Go Report Card](https://goreportcard.com/badge/github.com/nikolaydubina/fpdecimal)](https://goreportcard.com/report/github.com/nikolaydubina/fpdecimal)\n[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/nikolaydubina/fpdecimal/badge)](https://securityscorecards.dev/viewer/?uri=github.com/nikolaydubina/fpdecimal)\n\n* `int64` inside\n* does not use `float` neither in parsing nor printing\n* as fast as `int64` in parsing, printing, arithmetics — 3x faser `float`, 20x faster [shopspring/decimal](https://github.com/shopspring/decimal), 30x faster `fmt`\n* zero-overhead\n* preventing error-prone fixed-point arithmetics\n* Fuzz tests, Benchmarks\n* JSON\n* 200LOC\n\n```go\nimport fp \"github.com/nikolaydubina/fpdecimal\"\n\nvar BuySP500Price = fp.FromInt(9000)\n\ninput := []byte(`{\"sp500\": 9000.023}`)\n\ntype Stocks struct {\n    SP500 fp.Decimal `json:\"sp500\"`\n}\nvar v Stocks\nif err := json.Unmarshal(input, \u0026v); err != nil {\n    log.Fatal(err)\n}\n\nvar amountToBuy fp.Decimal\nif v.SP500.GreaterThan(BuySP500Price) {\n    amountToBuy = amountToBuy.Add(v.SP500.Mul(fp.FromInt(2)))\n}\n\nfmt.Println(amountToBuy)\n// Output: 18000.046\n```\n\n### Implementation\n\nParsing and Printing is expensive operation and requires a lot of code.\nHowever, 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.\nCode is heavily influenced by hot-path from Go core `strconv` package.\n\nIt is wrapped into struct to prevent bugs:\n- block multiplication by `fpdecimal` type, which leads to increase in decimal fractions and loose of precision\n- block additions of untyped constants, which leads to errors if you forget to scale by factor\n\n### Benchmarks\n\nParse\n```\n$ go test -bench=BenchmarkParse -benchtime=5s -benchmem .\ngoos: darwin\ngoarch: arm64\npkg: github.com/nikolaydubina/fpdecimal\nBenchmarkParse/fromString/small-10                             534307098            11.36 ns/op           0 B/op           0 allocs/op\nBenchmarkParse/fromString/large-10                             254741558            23.42 ns/op           0 B/op           0 allocs/op\nBenchmarkParse/UnmarshalJSON/small-10                          816873427             7.32 ns/op           0 B/op           0 allocs/op\nBenchmarkParse/UnmarshalJSON/large-10                          272173255            22.16 ns/op           0 B/op           0 allocs/op\nBenchmarkParse_int_strconv_Atoi/small-10                      1000000000             4.87 ns/op           0 B/op           0 allocs/op\nBenchmarkParse_int_strconv_Atoi/large-10                       420536834            14.31 ns/op           0 B/op           0 allocs/op\nBenchmarkParse_int_strconv_ParseInt/small/int32-10             561137575            10.67 ns/op           0 B/op           0 allocs/op\nBenchmarkParse_int_strconv_ParseInt/small/int64-10             564200026            10.64 ns/op           0 B/op           0 allocs/op\nBenchmarkParse_int_strconv_ParseInt/large/int64-10             219626983            27.17 ns/op           0 B/op           0 allocs/op\nBenchmarkParse_float_strconv_ParseFloat/small/float32-10       345666214            17.36 ns/op           0 B/op           0 allocs/op\nBenchmarkParse_float_strconv_ParseFloat/small/float64-10       339620222            17.68 ns/op           0 B/op           0 allocs/op\nBenchmarkParse_float_strconv_ParseFloat/large/float32-10       128824344            46.68 ns/op           0 B/op           0 allocs/op\nBenchmarkParse_float_strconv_ParseFloat/large/float64-10       128140617            46.89 ns/op           0 B/op           0 allocs/op\nBenchmarkParse_float_fmt_Sscanf/small-10                        21202892           281.6  ns/op          69 B/op           2 allocs/op\nBenchmarkParse_float_fmt_Sscanf/large-10                        10074237           599.2  ns/op          88 B/op           3 allocs/op\nPASS\nok      github.com/nikolaydubina/fpdecimal    116.249s\n```\n\nPrint\n```\n$ go test -bench=BenchmarkPrint -benchtime=5s -benchmem .\ngoos: darwin\ngoarch: arm64\npkg: github.com/nikolaydubina/fpdecimal\nBenchmarkPrint/small-10                                      191982066            31.24 ns/op           8 B/op           1 allocs/op\nBenchmarkPrint/large-10                                      150874335            39.89 ns/op          24 B/op           1 allocs/op\nBenchmarkPrint_int_strconv_Itoa/small-10                     446302868            13.39 ns/op           3 B/op           0 allocs/op\nBenchmarkPrint_int_strconv_Itoa/large-10                     237484774            25.20 ns/op          18 B/op           1 allocs/op\nBenchmarkPrint_int_strconv_FormatInt/small-10                444861666            13.70 ns/op           3 B/op           0 allocs/op\nBenchmarkPrint_float_strconv_FormatFloat/small/float32-10     55003357           104.2  ns/op          31 B/op           2 allocs/op\nBenchmarkPrint_float_strconv_FormatFloat/small/float64-10     43565430           137.4  ns/op          31 B/op           2 allocs/op\nBenchmarkPrint_float_strconv_FormatFloat/large/float32-10     64069650            92.07 ns/op          48 B/op           2 allocs/op\nBenchmarkPrint_float_strconv_FormatFloat/large/float64-10     68441746            87.36 ns/op          48 B/op           2 allocs/op\nBenchmarkPrint_float_fmt_Sprintf/small-10                     46503666           127.7  ns/op          16 B/op           2 allocs/op\nBenchmarkPrint_float_fmt_Sprintf/large-10                     51764224           115.8  ns/op          28 B/op           2 allocs/op\nPASS\nok      github.com/nikolaydubina/fpdecimal    79.192s\n```\n\nArithmetics\n```\n$ go test -bench=BenchmarkArithmetic -benchtime=5s -benchmem .\ngoos: darwin\ngoarch: arm64\npkg: github.com/nikolaydubina/fpdecimal\nBenchmarkArithmetic/add-10                   1000000000             0.316 ns/op           0 B/op           0 allocs/op\nBenchmarkArithmetic/div-10                   1000000000             0.950 ns/op           0 B/op           0 allocs/op\nBenchmarkArithmetic/divmod-10                1000000000             1.890 ns/op           0 B/op           0 allocs/op\nBenchmarkArithmetic_int64/add-10             1000000000             0.314 ns/op           0 B/op           0 allocs/op\nBenchmarkArithmetic_int64/div-10             1000000000             0.316 ns/op           0 B/op           0 allocs/op\nBenchmarkArithmetic_int64/divmod-10          1000000000             1.261 ns/op           0 B/op           0 allocs/op\nBenchmarkArithmetic_int64/mod-10             1000000000             0.628 ns/op           0 B/op           0 allocs/op\nPASS\nok      github.com/nikolaydubina/fpdecimal    6.721s\n```\n\n## References\n\n- [Fixed-Point Arithmetic Wiki](https://en.wikipedia.org/wiki/Fixed-point_arithmetic)\n- [shopspring/decimal](https://github.com/shopspring/decimal)\n\n## Appendix A: Comparison to other libraries\n\n- https://github.com/shopspring/decimal solves arbitrary precision, fpdecimal solves only simple small decimals\n- https://github.com/Rhymond/go-money solves typed number (currency), decodes through `interface{}` and float64, no precision in decoding, expects encoding to be in cents\n\n## Appendix B: Benchmarking [shopspring/decimal](https://github.com/shopspring/decimal)\n\n`2022-05-28`\n```\n$ go test -bench=. -benchtime=5s -benchmem ./...\ngoos: darwin\ngoarch: arm64\npkg: github.com/shopspring/decimal\nBenchmarkNewFromFloatWithExponent-10                        59701516          97.7 ns/op         106 B/op           4 allocs/op\nBenchmarkNewFromFloat-10                                    14771503         410.3 ns/op          67 B/op           2 allocs/op\nBenchmarkNewFromStringFloat-10                              16246342         375.2 ns/op         175 B/op           5 allocs/op\nBenchmark_FloorFast-10                                    1000000000           2.1 ns/op           0 B/op           0 allocs/op\nBenchmark_FloorRegular-10                                   53857244         106.3 ns/op         112 B/op           6 allocs/op\nBenchmark_DivideOriginal-10                                        7   715322768   ns/op   737406446 B/op    30652495 allocs/op\nBenchmark_DivideNew-10                                            22   262893689   ns/op   308046721 B/op    12054905 allocs/op\nBenchmarkDecimal_RoundCash_Five-10                           9311530         636.5 ns/op         616 B/op          28 allocs/op\nBenchmark_Cmp-10                                                  44   133191579   ns/op          24 B/op           1 allocs/op\nBenchmark_decimal_Decimal_Add_different_precision-10        31561636         176.6 ns/op         280 B/op           9 allocs/op\nBenchmark_decimal_Decimal_Sub_different_precision-10        36892767         164.4 ns/op         240 B/op           9 allocs/op\nBenchmark_decimal_Decimal_Add_same_precision-10            134831919          44.9 ns/op          80 B/op           2 allocs/op\nBenchmark_decimal_Decimal_Sub_same_precision-10            134902627          43.1 ns/op          80 B/op           2 allocs/op\nBenchmarkDecimal_IsInteger-10                               92543083          66.1 ns/op           8 B/op           1 allocs/op\nBenchmarkDecimal_NewFromString-10                             827455        7382   ns/op        3525 B/op         216 allocs/op\nBenchmarkDecimal_NewFromString_large_number-10                212538       28836   ns/op       16820 B/op         360 allocs/op\nBenchmarkDecimal_ExpHullAbraham-10                             10000      572091   ns/op      486628 B/op         568 allocs/op\nBenchmarkDecimal_ExpTaylor-10                                  26343      222915   ns/op      431226 B/op        3172 allocs/op\nPASS\nok      github.com/shopspring/decimal    123.541sa\n```\n\n## Appendix C: Why this is good fit for money?\n\nThere are only ~200 currencies in the world.\nAll currencies have at most 3 decimal digits, thus it is sufficient to handle 3 decimal fractions.\nNext, 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.\n\n## Appendix D: Is it safe to use arithmetic operators in Go?\n\nSort of... \n\nIn one of iterations, I did Type Alias, but it required some effort to use it carefully.\n\nOperations with defined types (variables) will fail.\n```go\nvar a int64\nvar b fpdecimal.FromInt(1000)\n\n// does not compile\na + b\n```\n\nHowever, untyped constants will be resolved to underlying type `int64` and will be allowed.  \n```go\nconst a 10000\nvar b fpdecimal.FromInt(1000)\n\n// compiles\na + b\n\n// also compiles\nb - 42\n\n// this one too\nb *= 23\n```\n\nIs this a problem? \n* 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.\n* For addition substraction - yes, it can be. You have to be careful and remind yourself that constants would be reduced 1000x.\n\nBoth of this can be addressed at compile time by providing linter.\nThis can be also addressed by wrapping into a struct and defining methods.\nFormed is hard to achieve in Go, due to lack of operator overload and lots of work required to write AST parser.\nLater has been implemented in this pacakge, and, as benchmarks show, without any extra memory or calls overhead as compared to `int64`.\n\n## Appendix E: Print into destination\n\nTo avoid mallocs, it is advantageous to print formatted value to pre-allocated destination.\nSimilarly, to `strconv.AppendInt`, we provide `AppendFixedPointDecimal`.\nThis is utilized in `github.com/nikolaydubina/fpmoney` package.\n\n```\nBenchmarkFixedPointDecimalToString/small-10     28522474         35.43 ns/op       24 B/op        1 allocs/op\nBenchmarkFixedPointDecimalToString/large-10     36883687         32.32 ns/op       24 B/op        1 allocs/op\nBenchmarkAppendFixedPointDecimal/small-10       38105520         30.51 ns/op      117 B/op        0 allocs/op\nBenchmarkAppendFixedPointDecimal/large-10       55147478         29.52 ns/op      119 B/op        0 allocs/op\n```\n\n## Appendix F: DivMod notation\n\nIn early versions, `Div` and `Mul` operated on `int` and `Div` returned remainder.\nAs recommended by @vanodevium and more in line with other common libraries, notation is changed.\nBellow is survey as of 2023-05-18.\n\nGo, https://pkg.go.dev/math/big\n```go\nfunc (z *Int) Div(x, y *Int) *Int\nfunc (z *Int) DivMod(x, y, m *Int) (*Int, *Int)\nfunc (z *Int) Mod(x, y *Int) *Int\n```\n\nGo, github.com/shopspring/decimal\n```go\nfunc (d Decimal) Div(d2 Decimal) Decimal\n// X no DivMod\nfunc (d Decimal) Mod(d2 Decimal) Decimal\nfunc (d Decimal) DivRound(d2 Decimal, precision int32) Decimal\n```\n\nPython, https://docs.python.org/3/library/decimal.html\n```python\ndivide(x, y) number\ndivide_int(x, y) number // truncates\ndivmod(x, y) number\nremainder(x, y) number\n```\n\nPytorch, https://pytorch.org/docs/stable/generated/torch.div.html\n```python\ntorch.div(input, other, *, rounding_mode=None, out=None) → [Tensor] // discards remainder\ntorch.remainder(input, other, *, out=None) → [Tensor] // remainder\n```\n\nnumpy, https://numpy.org/doc/stable/reference/generated/numpy.divmod.html\n```python\nnp.divmod(x, y) (number, number) // is equivalent to (x // y, x % y\nnp.mod(x, y) number\nnp.remainder(x, y) number\nnp.divide(x, y) number\nnp.true_divide(x, y) number // same as divide\nnp.floor_divide(x, y) number // rounding down\n```\n\n## Appendix G: generics switch for decimal counting\n\nGo 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.\nThis does not work well since Go does not support switch case (casting generic) back to integer well.\n\n## Appendix H: `string` vs `[]byte` in interface\n\nThe typical usage of parsing number is through some JSON or other mechanism. Those APIs are dealing with `[]byte`.\nNow, conversion from `[]byte` to `string` requires to copy data, since `string` is immutable.\nTo improve performance, we are using `[]byte` in signatures.\n\nUsing `string`\n```\nBenchmarkParse/fromString/small-10                 831217767             7.07 ns/op           0 B/op           0 allocs/op\nBenchmarkParse/fromString/large-10                 275009497            21.79 ns/op           0 B/op           0 allocs/op\nBenchmarkParse/UnmarshalJSON/small-10              553035127            10.98 ns/op           0 B/op           0 allocs/op\nBenchmarkParse/UnmarshalJSON/large-10              248815030            24.14 ns/op           0 B/op           0 allocs/op\n```\n\nUsing `[]byte`\n```\nBenchmarkParse/fromString/small-10                 523937236            11.32 ns/op           0 B/op           0 allocs/op\nBenchmarkParse/fromString/large-10                 257542226            23.23 ns/op           0 B/op           0 allocs/op\nBenchmarkParse/UnmarshalJSON/small-10              809793006             7.31 ns/op           0 B/op           0 allocs/op\nBenchmarkParse/UnmarshalJSON/large-10              272087984            22.04 ns/op           0 B/op           0 allocs/op\n```\n\n## Appendix F: dynamic pkg level fraction digits\n\nThis is very bug prone. In fact, this was observd in production, [issue](https://github.com/nikolaydubina/fpdecimal/issues/26).\n\nConsider:\n\n1. package A. init map with .FromInt\n2. package B imports A and sets in init() num fraction digits\n3. package B sees values in package A initialized with different fraction digits\n4. 💥\n\nTherefore, we are inlining fraction digits into most common fractions.\n\n- 3 is enough to represent all currencies\n- 3 is enough for SI units conversion ladder\n- 6 is enough for 10cm x 10cm of (lat,long)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnikolaydubina%2Ffpdecimal","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnikolaydubina%2Ffpdecimal","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnikolaydubina%2Ffpdecimal/lists"}