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

huangapple go评论77阅读模式
英文:

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

问题

在Go语言中,将货币从浮点数转换为整数的最佳方法是使用math包中的Round函数。以下是示例代码:

package main

import (
	"fmt"
	"math"
)

func main() {
	var fVal1 float64 = 19.08
	var f100 float64 = 100.0
	var fResult float64 = fVal1 * f100
	fmt.Printf("%f * %f as float = %f\n", fVal1, f100, fResult)
	var iResult1 int64 = int64(math.Round(fResult))
	fmt.Printf("as Integer unrounded = %d\n", iResult1)
	var iResult2 int64 = int64(math.Round(fResult))
	fmt.Printf("as Integer rounded = %d\n", iResult2)
}

这段代码使用了math包中的Round函数来进行四舍五入。你可以将浮点数乘以100,然后使用Round函数将结果转换为整数。这样就可以得到正确的货币转换结果。

希望对你有所帮助!

英文:

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\n", fVal1, f100, fResult)
var iResult1 int64 = int64(fResult)
fmt.Printf("as Integer unrounded = %d\n", iResult1)
var iResult2 int64 = int64(fResult + fRound)
fmt.Printf("as Integer rounded = %d\n", 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("\nTesting 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("\nDiff without rounding = %d\n", iCcyDiff1)
    fmt.Printf("Diff with rounding = %d\n", iCcyDiff2)
}

答案1

得分: 3

你必须进行四舍五入(就像你已经做的那样)。正如你所提到的,浮点数的性质使得这个过程变得困难,并且由于舍入误差,你必须处理一些不准确的小数结果。据我所知,golang没有一个四舍五入的函数,所以你需要自己实现。我曾经看到过这个代码片段在推特上被提到过几次...它似乎是在这个讨论串中诞生的。

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
}

一些建议/资源(供以后的其他人参考):

  • 当通过int64()将浮点数转换为整数时,小数部分将被丢弃。
  • 当打印浮点数时,请使用正确的golang格式。例如:fmt.Printf("%.20f * %.20f as float = %.20f\n", fVal1, f100, fResult)
  • 在处理货币时使用整数。通过使用分(以及整数运算来避免舍入),并且除以100来避免浮点数运算的问题。也就是说,使用足够大的固定大小整数或math/big.Int,并避免浮点数运算。

*注意:*你也可以使用strconv进行四舍五入(原帖中想要避免这种方法):

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

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 &lt; 0.0 {
		x=-.5
		intermed -=1
	}
	if frac &gt;= 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(&quot;%.20f * %.20f as float = %.20f\n&quot;, 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, &#39;g&#39;, prec, 64)
	f, _ := strconv.ParseFloat(frep, 64)
	return f
}

答案2

得分: 1

这里的问题是,无法在浮点数中存储给定值的精确表示。你所做的Printf()是误导性的,它会为你四舍五入值。试试这个:

package main

import "fmt"

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

运行结果为:

19.07999999999999829470 * 100.00000000000000000000 as float = 1907.99999999999977262632

现在int64()可能只是截断了数字的非整数部分。

要了解更多关于浮点数的知识,可以参考floating-point-gui.de

=> 永远不要试图在浮点数中存储精确值

英文:

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 &quot;fmt&quot;

func main() {
	a := 19.08
	b := 100.0
	c := a * b
	fmt.Printf(&quot;%.20f * %.20f as float = %.20f\n&quot;, 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

答案3

得分: 0

我开发了一个基于int64的十进制类,用于处理货币,可以处理浮点数、字符串解析、JSON等。

它将金额存储为64位整数,表示为分。可以轻松地从浮点数创建,或者转换回浮点数。

也很方便存储在数据库中。

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
}

对于我的债务追踪应用程序https://debtstracker.io/效果很好。

英文:

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 &quot;github.com/strongo/decimal&quot;

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(&quot;2.34&quot;); print(amount)  // 2.34
	amount, _ = decimal.ParseDecimal64p2(&quot;-3.42&quot;); print(amount)  // -3.42
}

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

huangapple
  • 本文由 发表于 2013年10月8日 10:37:44
  • 转载请务必保留本文链接:https://go.coder-hub.com/19238175.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定