My service receives price as string, like "12.34". I have to save it in cents in db, so it must be 1234. Is the following solution safe? Safe means that result is always the same and correct. "12.34" must be 1234 but not 1235 or 1233.
s := "12.34"
f, _ := strconv.ParseFloat(s, 64)
i := int(100 * f)
(I don't handle error here, because question is not about error handling)
Go uses IEEE-754 binary floating-point numbers. Floating-point numbers are imprecise. Don't use them for financial transactions. Use integers.
For example,
package main
import (
"fmt"
"strconv"
"strings"
)
func parseCents(s string) (int64, error) {
n := strings.SplitN(s, ".", 3)
if len(n) != 2 || len(n[1]) != 2 {
err := fmt.Errorf("format error: %s", s)
return 0, err
}
d, err := strconv.ParseInt(n[0], 10, 56)
if err != nil {
return 0, err
}
c, err := strconv.ParseUint(n[1], 10, 8)
if err != nil {
return 0, err
}
if d < 0 {
c = -c
}
return d*100 + int64(c), nil
}
func main() {
s := "12.34"
fmt.Println(parseCents(s))
s = "-12.34"
fmt.Println(parseCents(s))
s = "12.3"
fmt.Println(parseCents(s))
}
Playground: https://play.golang.org/p/AwXg4_jp8lo
Output:
1234 <nil>
-1234 <nil>
0 format error: 12.3
If you can guarantee that the amount always has two digits for cents of course the solution is easy, just remove ., and parse as an integer, no need for a float.
No, it's not safe, consider $12, 12.4 12.00, and also rounding errors as you're passing through a float first (floats are imprecise by design). I think better to transform the string to cents first (make sure it has . make sure it has two digits after ., then remove anything but digits), then the conversion doesn't have to go through a float.
You should write some table tests for this with unexpected input if this is from user input or another service that you don't control. Something like this:
https://play.golang.org/p/AiikcIMwgZP
var tests = map[string]int64{
"10": 1000,
"12.34": 1234,
"12.3": 1230,
"€12.3": 1230,
"$12.99": 1299,
"123": 12300,
"12344444444.04": 1234444444404,
"$12.991": 1299,
"0.29": 29,
}
func parseCents(s string) (int64, error) {
// Check it has . if not add 00
if !strings.Contains(s, ".") {
s += ".00"
}
// Check it has two digits after . if not add 0
if strings.Index(s, ".") > len(s)-3 {
s += "0"
}
// Remove dot to get cents
s = strings.Replace(s, ".", "", 1)
// Remove any stray formatting users might add
s = strings.Trim(s, "£€$ ")
// Parse int
return strconv.ParseInt(s, 10, 64)
}
For more info on using floats for currency, see answers like this (this is a problem in all languages):