在Go中将货币从float转换为integer的最佳方法是什么?

在Go中将货币从float转换为integer的最佳方法是什么?

问题描述:

What is the best way to convert a currency from float to integer in Go?

------------- Added Explanation of question ----------

To expand my question a little, the following is an example of what I see as the “problem” solved by adding a rounding value of 0.004 (for 2-decimal currencies).

As far as I know, external values stored as eg. decimal, double, currency in an RDBMS need to be “imported” to Go as a float. In order to perform calculations, they then need to be converted to integer, or at least that is one method.

In the example below, fVal1 emulates an amount imported from a database. To perform calculations on it, I want to convert it to an integer. The easiest way to do that appears to me to be to add a rounding figure as illustrated.

Example code:

var fVal1 float64 = 19.08
var f100 float64 = 100.0
var fRound float64 = 0.004
var fResult float64 = fVal1 * f100
fmt.Printf("%f * %f as float = %f
", fVal1, f100, fResult)
var iResult1 int64 = int64(fResult)
fmt.Printf("as Integer unrounded = %d
", iResult1)
var iResult2 int64 = int64(fResult + fRound)
fmt.Printf("as Integer rounded = %d
", iResult2)

Console output:

19.080000 * 100.000000 as float = 1908.000000
as Integer unrounded = 1907
as Integer rounded = 1908

----------- end of addition to question -----------

I’ve implemented a small package to handle multiple currencies in Go, and essentially it revolves around the following bit of code (below).

When I discovered that there were rounding errors in converting between floats and integers, the first rounding factor that I tried was 0.004 (for 2 decimals), and it appeared to work OK.

Basically, the following bit of code for currency conversion from float to int revolves around these two alternative lines of code :

  var iCcyAmt1 int64 = int64(fCcyBase * fScale)
  var iCcyAmt2 int64 = int64((fCcyBase + fRound) * fScale)

The rounding being used in “fRound” in the second alternative is “0.004”.

Running this test program (10 million iterations) results in a consistent rounding error of about 588,000 cents where the “0.004” is not added, and zero difference using the rounding figure.

Is this a safe way to handle this situation (I don’t want to use strings), or is there a better way?

Test Program:

package main

import (
    "fmt"
    "math/rand"
    "time"
)

func main() {
    var (
        fRound      float64 = 0.004
        fScale      float64 = 100.0
        iCcyTotBase int64
        iCcyTot1    int64
        iCcyTot2    int64
    )

    const I_ITERS int = 10000000

    rand.Seed(time.Now().UTC().UnixNano())

    fmt.Printf("
Testing Float to Integer (%d iterations)"+
        " .........", I_ITERS)

    for i := 0; i < I_ITERS; i++ {
        var iCcyBase int64 = int64(999 + rand.Intn(9999999))
        var fCcyBase float64 = float64(iCcyBase) / fScale
        var iCcyAmt1 int64 = int64(fCcyBase * fScale)
        var iCcyAmt2 int64 = int64((fCcyBase + fRound) * fScale)
        iCcyTotBase += iCcyBase
        iCcyTot1 += iCcyAmt1
        iCcyTot2 += iCcyAmt2
    }

    var iCcyDiff1 int64 = iCcyTot1 - iCcyTotBase
    var iCcyDiff2 int64 = iCcyTot2 - iCcyTotBase
    fmt.Printf("
Diff without rounding = %d
", iCcyDiff1)
    fmt.Printf("Diff with rounding = %d
", iCcyDiff2)
}

You have to round (as you've done). And -as you've alluded to- the nature of floating points make this difficult, and you have to deal with some bad decimal results due to rounding errors. AFAIK, golang doesn't have a rounding function, so you'll need to implement it. I've seen this gist tweeted/mentioned a few times... and it seems to have been born in this thread.

func RoundViaFloat(x float64, prec int) float64 {
    var rounder float64
    pow := math.Pow(10, float64(prec))
    intermed := x * pow
    _, frac := math.Modf(intermed)
    intermed += .5
    x = .5
    if frac < 0.0 {
        x=-.5
        intermed -=1
    }
    if frac >= x {
        rounder = math.Ceil(intermed)
    } else {
        rounder = math.Floor(intermed)
    }

    return rounder / pow
}

A couple of notes/resources (for others that show up later):

  • When converting a floating point via int64() the fractional part will be discarded.

  • When printing a floating point use the correct golang format. e.g. fmt.Printf("%.20f * %.20f as float = %.20f ", fVal1, f100, fResult)

  • Use an Integer for money. Avoid all of this by using pennies (and integer math in order to avoid the rounding) and divide by 100. i.e. use a large enough fixed size integer or math/big.Int and avoid the floating point arithmetic.

Note: You can, alternatively, round using strconv (the OP wanted to avoid this):

func RoundFloat(x float64, prec int) float64 {
    frep := strconv.FormatFloat(x, 'g', prec, 64)
    f, _ := strconv.ParseFloat(frep, 64)
    return f
}

The problem here is, that it's not possible to store an exact representation of the values you have given in a float. The Printf() you are doing is misleading, it is rounding the value for you. Try this:

package main

import "fmt"

func main() {
    a := 19.08
    b := 100.0
    c := a * b
    fmt.Printf("%.20f * %.20f as float = %.20f
", a, b, c)
}

Which gives:

19.07999999999999829470 * 100.00000000000000000000 as float = 1907.99999999999977262632

Now int64() is probably just cutting off the non-integer part of the number.

To learn more about floating point numbers see floating-point-gui.de for example.

=> Never ever try to store exact values in floating point numbers

I've developed a decimal class based on int64 for handling money that is handling floats, string parsing, JSON, etc.

It stores amount as 64 bit integer number of cents. Can be easily created from float or converted back to float.

Handy for storing in DB as well.

https://github.com/strongo/decimal

package example

import "github.com/strongo/decimal"

func Example() {
    var amount decimal.Decimal64p2; print(amount)  // 0

    amount = decimal.NewDecimal64p2(0, 43); print(amount)  // 0.43
    amount = decimal.NewDecimal64p2(1, 43); print(amount)  // 1.43
    amount = decimal.NewDecimal64p2FromFloat64(23.100001); print(amount)  // 23.10
    amount, _ = decimal.ParseDecimal64p2("2.34"); print(amount)  // 2.34
    amount, _ = decimal.ParseDecimal64p2("-3.42"); print(amount)  // -3.42
}

Works well for my debts tracker app https://debtstracker.io/