6

I want to truncate 1.234567 into a 3-fraction digit floating point number, but the result is not what I want.

E.g: 1.234567 => 1.234

package main

import (
    "strconv"
    "fmt"
)

func main() {
    f := 1.234567
    fmt.Println(strconv.FormatFloat(f, 'f', 3, 64)) //1.235
    fmt.Printf("%.3f", f) //1.235
}

Can anyone tell me how to do this in Go?

icza
  • 389,944
  • 63
  • 907
  • 827
ikool
  • 153
  • 1
  • 9
  • 2
    What you want is to round (or truncate) to 3 fraction digits, not bits. For general rounding, see possible duplicate: [Golang Round to Nearest 0.05](https://stackoverflow.com/questions/39544571/golang-round-to-nearest-0-05/39544897#39544897) – icza Jan 25 '19 at 09:53
  • I don't want round, just truncate to 3 fraction digits. 1.234567 => 1.234 not 1.235 – ikool Jan 25 '19 at 11:47

2 Answers2

11

The naive way (not always correct)

For truncation, we could take advantage of math.Trunc() which throws away the fraction digits. This is not exactly what we want, we want to keep some fraction digits. So in order to achieve what we want, we may first multiply the input by a power of 10 to shift the wanted fraction digits to the "integer" part, and after truncation (calling math.Trunc() which will throw away the remaining fraction digits), we can divide by the same power of 10 we multiplied in the beginning:

f2 := math.Trunc(f*1000) / 1000

Wrapping this into a function:

func truncateNaive(f float64, unit float64) float64 {
    return math.Trunc(f/unit) * unit
}

Testing it:

f := 1.234567
f2 := truncateNaive(f, 0.001)
fmt.Printf("%.10f\n", f2)

Output:

1.2340000000

So far so good, but note that we perform arithmetic operations inside truncateNaive() which may result in unwanted roundings, which could alter the output of the function.

For example, if the input is 0.299999999999999988897769753748434595763683319091796875 (it's representable by a float64 value exactly, see proof), the output should be 0.2999000000, but it will be something else:

f = 0.299999999999999988897769753748434595763683319091796875
f2 = truncateNaive(f, 0.001)
fmt.Printf("%.10f\n", f2)

Output:

0.3000000000

Try these on the Go Playground.

This wrong output is probably not acceptable in most cases (except if you look at it from a way that the input is very close to 0.3–difference is less than 10-16–to which the output is 0.3...).

Using big.Float

To properly truncate all valid float64 values, the intermediate operations must be precise. To achieve that, using a single float64 is insufficient. There are ways to split the input into 2 float64 values and perform operations on them (so precision is not lost) which would be more efficient, or we could use a more convenient way, big.Float which can be of arbitrary precision.

Here's the "transcript" of the above truncateNaive() function using big.Float:

func truncate(f float64, unit float64) float64 {
    bf := big.NewFloat(0).SetPrec(1000).SetFloat64(f)
    bu := big.NewFloat(0).SetPrec(1000).SetFloat64(unit)

    bf.Quo(bf, bu)

    // Truncate:
    i := big.NewInt(0)
    bf.Int(i)
    bf.SetInt(i)

    f, _ = bf.Mul(bf, bu).Float64()
    return f
}

Testing it:

f := 1.234567
f2 := truncate(f, 0.001)
fmt.Printf("%.10f\n", f2)

f = 0.299999999999999988897769753748434595763683319091796875
f2 = truncate(f, 0.001)
fmt.Printf("%.10f\n", f2)

Output is now valid (try it on the Go Playground):

1.2340000000
0.2990000000
icza
  • 389,944
  • 63
  • 907
  • 827
  • `f = 1.234 f2 := truncate(f, 0.001) fmt.Printf("%.10f\n", f2) //1.2330000000 ` – ikool Feb 02 '19 at 09:42
  • @ikool That's because `1.234` cannot be represented exactly with `float64`. When you assign `1.234` to a `float64`, that's gonna be `1.233999999999999985789145284798`, see it on the [Go Playground](https://play.golang.org/p/npqET4ys3b3). So essentially you truncate `1.233999999999999985789145284798`, for which the correct output is `1.233`. – icza Feb 02 '19 at 12:53
  • `truncate(0.123456789, 0.000000001)` [gives](https://go.dev/play/p/Rmhj2wJ2J7w) `0.12345678800000001` – eprst Aug 04 '22 at 20:23
  • 1
    @eprst And that's because the value you pass is **not** `0.123456789` because it can't be represented using `float64`, the value you pass is `0.1234567889999999973361`, and applying the truncation to _this_ is `0.123456788` (which again can't be represented using `float64`, the closest is `0.12345678800000001`). See here: https://go.dev/play/p/DMt7Hd8R2Ax – icza Aug 05 '22 at 05:39
  • @icza Is there a reason why we shouldn't use `big.MaxPrec` as precision ? – user8555937 Aug 14 '22 at 09:19
0

You need to truncate decimals manually, either on string level or with math.Floor like https://play.golang.org/p/UP2gFx2iFru.

Pascal de Kloe
  • 523
  • 4
  • 12
  • This gives wrong result if the input is `0.299999999999999988897769753748434595763683319091796875`, see my answer. – icza Jan 28 '19 at 11:30
  • That number doesn't fit float64. ;-) But you're right, there is a minor chance of roundig errors when you hit a lot of 9s. – Pascal de Kloe Jan 28 '19 at 15:57
  • That number is representable by a `float64` exactly, see https://play.golang.org/p/I-H2RQJ6Lj4 – icza Jan 28 '19 at 19:20
  • 1
    The `float64` is limited to 15 decimals. Your interesting example renders as this exact sequence of decimals due to the algorithms below. Try changing a digit @icza as in: https://play.golang.org/p/pXHwWCyTH-o. ;-) – Pascal de Kloe Jan 29 '19 at 09:12
  • The `float64` type is a binary representation ([IEEE 754](https://en.wikipedia.org/wiki/Double-precision_floating-point_format)), it has finite bits for representing the number, but a finite binary number may have more decimal digits. Just like the finite `0.3` decimal number has an infinite binary representation. (The linked wikipedia page has such example, check it.) – icza Jan 29 '19 at 09:22