Using the std lib, this solution provides three methods. The first two methods (TruncFloat
and RoundFloat
) meet the criteria:
- Supports very large floats
- Supports very small floats
- Supports invalid floats
- Truncates when you want it to
- Rounds when you want it to
The third method TruncFloatFast
provides more speed, but is limited and provides an error for bad rounds. For most of my applications I prefer robustness over speed, but when speed matters I want to know about bad rounds. I did a quick perf measure on my dev machine.
TruncFLoat: 155 millis per million calls
RoundFloat: 150 millis per million calls
RoundFloatFast: 3 millis per million calls
// prec controls the number of digits (excluding the exponent)
// prec of -1 uses the smallest number of digits
func TruncFloat(f float64, prec int) (float64, error) {
floatBits := 64
if math.IsNaN(f) || math.IsInf(f, 1) || math.IsInf(f, -1) {
return 0, fmt.Errorf("bad float val %f", f)
}
fTruncStr := strconv.FormatFloat(f, 'f', prec+1, floatBits)
fTruncStr = fTruncStr[:len(fTruncStr)-1]
fTrunc, err := strconv.ParseFloat(fTruncStr, floatBits)
if err != nil {
return 0, err
}
return fTrunc, nil
}
// prec controls the number of digits (excluding the exponent)
// prec of -1 uses the smallest number of digits
func RoundFloat(f float64, prec int) (float64, error) {
floatBits := 64
if math.IsNaN(f) || math.IsInf(f, 1) || math.IsInf(f, -1) {
return 0, fmt.Errorf("bad float val %f", f)
}
fRoundedStr := strconv.FormatFloat(f, 'f', prec, floatBits)
fRounded, err := strconv.ParseFloat(fRoundedStr, floatBits)
if err != nil {
return 0, err
}
return fRounded, nil
}
func RoundFloatFast(f float64, prec int) (float64, error) {
mul := math.Pow10(prec)
if mul == 0 {
return 0, nil
}
product := f * mul
var roundingErr error
if product > float64(math.MaxInt64) {
roundingErr = fmt.Errorf("unsafe round: float64=%+v, places=%d", f, prec)
}
return math.Round(product) / mul, roundingErr
}
Test Examples
f, _ := TruncFloat(0.000000000000000000000000000000019, 32)
expected := 0.00000000000000000000000000000001
if f != expected {
t.Errorf("Expected=%+v Actual=%+v", expected, f)
}
f, _ = TruncFloat(2.289, 2)
expected = 2.28
if f != expected {
t.Errorf("Expected=%+v Actual=%+v", expected, f)
}
f, _ = TruncFloat(111222333444555666777888999000111222333.123456789, 8)
expected = 111222333444555666777888999000111222333.12345678
if f != expected {
t.Errorf("Expected=%+v Actual=%+v", expected, f)
}
f, _ = TruncFloat(1, 0)
expected = 1.
if f != expected {
t.Errorf("Expected=%+v Actual=%+v", expected, f)
}
f, _ = TruncFloat(1, 2)
expected = 1.
if f != expected {
t.Errorf("Expected=%+v Actual=%+v", expected, f)
}
f, _ = RoundFloat(0.000000000000000000000000000000011, 32)
expected = 0.00000000000000000000000000000001
if f != expected {
t.Errorf("Expected=%+v Actual=%+v", expected, f)
}
f, _ = RoundFloat(92234, 14)
expected = 92234.
if f != expected {
t.Errorf("Expected=%+v Actual=%+v", expected, f)
}
f, _ = RoundFloat(2.289, 2)
expected = 2.29
if f != expected {
t.Errorf("Expected=%+v Actual=%+v", expected, f)
}
f, _ = RoundFloat(111222333444555666777888999000111222333.123456789, 8)
expected = 111222333444555666777888999000111222333.12345679
if f != expected {
t.Errorf("Expected=%+v Actual=%+v", expected, f)
}
a := time.Now().UnixMilli()
for i := 0.; i < 1_000_000; i+= 1.23456789 {
TruncFloat(i, 2)
}
t.Logf("TruncFloat millis=%d", time.Now().UnixMilli()-a)
a = time.Now().UnixMilli()
for i := 0.; i < 1_000_000; i+= 1.23456789 {
RoundFloat(i, 2)
}
t.Logf("RoundFloat millis=%d", time.Now().UnixMilli()-a)
a = time.Now().UnixMilli()
for i := 0.; i < 1_000_000; i+= 1.23456789 {
RoundFloatFast(i, 2)
}
t.Logf("RoundFloatFast millis=%d", time.Now().UnixMilli()-a)