Floating Point Parsing - ParseFloat and ParseComplex in Go 3/7

When working with Go applications, you’ll frequently need to convert string representations of numbers into their numeric types. The strconv
package provides two essential functions for floating-point conversions: ParseFloat
and ParseComplex
. These functions handle the intricacies of parsing decimal numbers, scientific notation, and even complex numbers from string input.
Understanding ParseFloat
The ParseFloat
function converts a string to a floating-point number with the signature:
func ParseFloat(s string, bitSize int) (float64, error)
The bitSize
parameter specifies the precision: 32 for float32
or 64 for float64
. Regardless of the bitSize, the function always returns a float64
, but when bitSize is 32, the result can be converted to float32
without changing its value.
Let’s examine basic usage:
package main
import (
"fmt"
"strconv"
)
func main() {
// Basic decimal parsing
f1, err := strconv.ParseFloat("3.14159", 64)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Printf("f1: %f (type: %T)\n", f1, f1)
// Scientific notation
f2, err := strconv.ParseFloat("1.23e-4", 64)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Printf("f2: %e\n", f2)
// Negative numbers
f3, err := strconv.ParseFloat("-42.7", 32)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Printf("f3: %f\n", f3)
}
The function accepts various string formats including standard decimal notation, scientific notation with ‘e’ or ‘E’, and positive/negative signs.
BitSize 32 vs 64 Precision
Understanding the difference between 32-bit and 64-bit parsing becomes crucial when dealing with precision-sensitive applications:
package main
import (
"fmt"
"strconv"
)
func main() {
testValue := "1.23456789012345678901234567890"
// Parse as 32-bit precision
f32, _ := strconv.ParseFloat(testValue, 32)
actual32 := float32(f32)
// Parse as 64-bit precision
f64, _ := strconv.ParseFloat(testValue, 64)
fmt.Printf("Original: %s\n", testValue)
fmt.Printf("32-bit: %.20f\n", actual32)
fmt.Printf("64-bit: %.20f\n", f64)
// Demonstrate precision limits
largeNumber := "999999999999999999999.0"
f32Large, _ := strconv.ParseFloat(largeNumber, 32)
f64Large, _ := strconv.ParseFloat(largeNumber, 64)
fmt.Printf("\nLarge number: %s\n", largeNumber)
fmt.Printf("32-bit: %.1f\n", float32(f32Large))
fmt.Printf("64-bit: %.1f\n", f64Large)
}
The 32-bit version provides approximately 7 decimal digits of precision, while 64-bit offers about 15-17 digits. WWhen bitSize=32, ParseFloat rounds the result to float32 precision; if the value is out of range, it returns ±Inf along with an ErrRange error.
Special Values Handling
ParseFloat recognizes several special floating-point values defined by IEEE 754:
package main
import (
"fmt"
"math"
"strconv"
)
func main() {
specialValues := []string{
"NaN",
"nan",
"+Inf",
"-Inf",
"inf",
"infinity",
"+infinity",
"-infinity",
}
for _, val := range specialValues {
f, err := strconv.ParseFloat(val, 64)
if err != nil {
fmt.Printf("Error parsing %s: %v\n", val, err)
continue
}
fmt.Printf("%-12s -> %f", val, f)
// Check what type of special value
if math.IsNaN(f) {
fmt.Print(" (NaN)")
} else if math.IsInf(f, 1) {
fmt.Print(" (Positive Infinity)")
} else if math.IsInf(f, -1) {
fmt.Print(" (Negative Infinity)")
}
fmt.Println()
}
// Working with special values
nanVal, _ := strconv.ParseFloat("NaN", 64)
infVal, _ := strconv.ParseFloat("+Inf", 64)
fmt.Printf("\nNaN == NaN: %t\n", nanVal == nanVal) // Always false
fmt.Printf("Is NaN: %t\n", math.IsNaN(nanVal))
fmt.Printf("Is Infinite: %t\n", math.IsInf(infVal, 0))
}
These special values are case-insensitive and follow standard conventions. Remember that NaN comparisons always return false, even when comparing NaN to itself.
Hexadecimal Float Parsing
ParseFloat supports hexadecimal floating-point notation, which provides exact binary representation of floating-point values. This format uses the prefix 0x
or 0X
followed by hexadecimal significand (integer and/or fractional part) and a required binary exponent introduced by p or P:
package main
import (
"fmt"
"strconv"
)
func main() {
// Basic hexadecimal float
hex1, err := strconv.ParseFloat("0x1.8p3", 64)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Printf("0x1.8p3 = %f\n", hex1) // 1.5 * 2^3 = 12.0
// Hexadecimal with fractional part
hex2, _ := strconv.ParseFloat("0x1.921fb54442d18p+1", 64)
fmt.Printf("Pi approximation: %.15f\n", hex2)
// Negative hexadecimal
hex3, _ := strconv.ParseFloat("-0x1.4p-1", 64)
fmt.Printf("-0x1.4p-1 = %f\n", hex3) // -1.25 * 2^-1 = -0.625
// Without fractional part
hex4, _ := strconv.ParseFloat("0x10p-4", 64)
fmt.Printf("0x10p-4 = %f\n", hex4) // 16 * 2^-4 = 1.0
// Demonstrate precision advantage
decimalImprecise := "0.1"
hexPrecise := "0x1.999999999999ap-4"
dec, _ := strconv.ParseFloat(decimalImprecise, 64)
hex, _ := strconv.ParseFloat(hexPrecise, 64)
fmt.Printf("\nDecimal 0.1: %.17f\n", dec)
fmt.Printf("Hex equivalent: %.17f\n", hex)
fmt.Printf("Difference: %.20f\n", dec-hex)
}
The hexadecimal format follows the pattern: [sign]0xhex_digits[.hex_digits]p[sign]decimal_exponent (the exponent is required; the fraction is optional). The p
indicates the binary exponent (power of 2), distinguishing it from decimal scientific notation that uses e
.
Understanding ParseComplex
Complex numbers consist of real and imaginary parts. Go’s ParseComplex
function handles various string representations:
func ParseComplex(s string, bitSize int) (complex128, error)
The bitSize parameter works similarly to ParseFloat: 64 for complex64
(two float32 values) or 128 for complex128
(two float64 values).
package main
import (
"fmt"
"strconv"
)
func main() {
complexFormats := []string{
"1+2i", // Standard form
"1-2i", // Negative imaginary
"-1+2i", // Negative real
"-1-2i", // Both negative
"3.14+2.71i", // Decimal values
"1.5e2+2.5e-1i", // Scientific notation
"5i", // Pure imaginary
"42", // Pure real
"(1+2i)", // Parentheses (supported)
}
for _, format := range complexFormats {
c, err := strconv.ParseComplex(format, 128)
if err != nil {
fmt.Printf("%-15s -> Error: %v\n", format, err)
} else {
fmt.Printf("%-15s -> %v\n", format, c)
// Extract real and imaginary parts
fmt.Printf("%-15s Real: %.2f, Imaginary: %.2f\n",
"", real(c), imag(c))
}
fmt.Println()
}
}
Complex Number Format Variations
ParseComplex is quite flexible in accepting different formats, but it has specific rules:
package main
import (
"fmt"
"strconv"
)
func main() {
// Valid formats
validFormats := map[string]string{
"0+0i": "Zero complex number",
"1": "Pure real number",
"2i": "Pure imaginary number",
"+3i": "Positive imaginary with explicit sign",
"1.5+2.5i": "Decimal real and imaginary parts",
"1e2+3e-1i": "Scientific notation for both parts",
"inf+infi": "Infinite real and imaginary parts",
"nan+nani": "NaN real and imaginary parts",
}
fmt.Println("Valid Complex Formats:")
for format, description := range validFormats {
c, err := strconv.ParseComplex(format, 128)
if err != nil {
fmt.Printf("%-12s: Error - %v\n", format, err)
} else {
fmt.Printf("%-12s: %v (%s)\n", format, c, description)
}
}
fmt.Println("\nInvalid Formats:")
invalidFormats := []string{
"1 + 2i", // Spaces not allowed
"i", // Missing coefficient
"1+i", // Missing imaginary coefficient
"1+2j", // 'j' not supported (only 'i')
"2*i", // Multiplication symbol not supported
}
for _, format := range invalidFormats {
_, err := strconv.ParseComplex(format, 128)
if err != nil {
fmt.Printf("%-10s: %v\n", format, err)
}
}
}
The parser expects the imaginary unit to be ‘i’ (not ‘j’), requires explicit coefficients for the imaginary part when both real and imaginary components are present, and doesn’t accept spaces in the input string, but does support parentheses around the entire expression.
BitSize Impact on Complex Numbers
The bitSize parameter in ParseComplex determines the precision of both real and imaginary components. Understanding this distinction helps prevent precision loss in complex calculations:
package main
import (
"fmt"
"strconv"
)
func main() {
precisionTest := "1.23456789012345678901+2.98765432109876543210i"
// Parse with 64-bit precision (complex64)
c64, _ := strconv.ParseComplex(precisionTest, 64)
actual64 := complex64(c64)
// Parse with 128-bit precision (complex128)
c128, _ := strconv.ParseComplex(precisionTest, 128)
fmt.Printf("Original: %s\n", precisionTest)
fmt.Printf("64-bit: %v\n", actual64)
fmt.Printf("128-bit: %v\n", c128)
// Show individual component precision
fmt.Printf("\n64-bit components:\n")
fmt.Printf(" Real: %.10f\n", real(actual64))
fmt.Printf(" Imaginary: %.10f\n", imag(actual64))
fmt.Printf("\n128-bit components:\n")
fmt.Printf(" Real: %.20f\n", real(c128))
fmt.Printf(" Imaginary: %.20f\n", imag(c128))
// Demonstrate range limitations
largeComplex := "1e30+1e30i"
large64, err64 := strconv.ParseComplex(largeComplex, 64)
large128, err128 := strconv.ParseComplex(largeComplex, 128)
fmt.Printf("\nLarge number test: %s\n", largeComplex)
if err64 != nil {
fmt.Printf("64-bit error: %v\n", err64)
} else {
fmt.Printf("64-bit: %v\n", complex64(large64))
}
if err128 != nil {
fmt.Printf("128-bit error: %v\n", err128)
} else {
fmt.Printf("128-bit: %v\n", large128)
}
}
Error Handling and Edge Cases
Both ParseFloat and ParseComplex return specific error types that help identify parsing failures:
package main
import (
"fmt"
"strconv"
)
func main() {
// ParseFloat error scenarios
floatErrors := []string{
"", // Empty string
"abc", // Non-numeric
"1.2.3", // Multiple decimal points
"1e", // Incomplete scientific notation
"++1", // Multiple signs
"1.23ee4", // Invalid exponent
" 1.23 ", // Leading/trailing spaces
}
fmt.Println("ParseFloat Error Examples:")
for _, invalid := range floatErrors {
_, err := strconv.ParseFloat(invalid, 64)
if err != nil {
fmt.Printf("%-10q -> %v\n", invalid, err)
// Check for specific error type
if numErr, ok := err.(*strconv.NumError); ok {
fmt.Printf(" NumError - Func: %s, Num: %q, Err: %v\n",
numErr.Func, numErr.Num, numErr.Err)
}
}
}
fmt.Println("\nParseComplex Error Examples:")
complexErrors := []string{
"", // Empty string
"1+", // Incomplete
"1+2", // Missing 'i'
"1++2i", // Invalid syntax
"1+2k", // Wrong imaginary unit
"real+imagi", // Non-numeric components
}
for _, invalid := range complexErrors {
_, err := strconv.ParseComplex(invalid, 128)
if err != nil {
fmt.Printf("%-12q -> %v\n", invalid, err)
}
}
// Demonstrate proper error handling pattern
fmt.Println("\nProper Error Handling:")
userInput := "not-a-number"
if val, err := strconv.ParseFloat(userInput, 64); err != nil {
if numErr, ok := err.(*strconv.NumError); ok {
switch numErr.Err {
case strconv.ErrSyntax:
fmt.Println("Invalid number format")
case strconv.ErrRange:
fmt.Println("Number out of range")
default:
fmt.Printf("Parsing error: %v\n", numErr.Err)
}
}
} else {
fmt.Printf("Parsed successfully: %f\n", val)
}
}
Practical Applications and Performance Considerations
Understanding when to use each parsing function and their performance characteristics helps build efficient applications:
package main
import (
"fmt"
"math/cmplx"
"strconv"
"time"
)
func main() {
// Demonstrate parsing real-world data formats
scientificData := []string{
"6.62607015e-34", // Planck constant
"2.99792458e8", // Speed of light
"1.602176634e-19", // Elementary charge
"9.1093837015e-31", // Electron mass
}
fmt.Println("Scientific Constants:")
for i, data := range scientificData {
val, _ := strconv.ParseFloat(data, 64)
fmt.Printf("Constant %d: %e (%.2e)\n", i+1, val, val)
}
// Complex number applications
impedanceValues := []string{
"50+0i", // Pure resistive
"0+50i", // Pure reactive
"30+40i", // Mixed impedance
"100-75i", // Capacitive
}
fmt.Println("\nElectrical Impedance Values:")
for i, impedance := range impedanceValues {
z, _ := strconv.ParseComplex(impedance, 128)
magnitude := cmplx.Abs(z)
fmt.Printf("Z%d: %v (|Z| = %.2f ohms)\n", i+1, z, magnitude)
}
// Performance comparison
testData := "123.456789"
iterations := 1000000
start := time.Now()
for i := 0; i < iterations; i++ {
strconv.ParseFloat(testData, 32)
}
duration32 := time.Since(start)
start = time.Now()
for i := 0; i < iterations; i++ {
strconv.ParseFloat(testData, 64)
}
duration64 := time.Since(start)
fmt.Printf("\nPerformance Test (%d iterations):\n", iterations)
fmt.Printf("32-bit parsing: %v\n", duration32)
fmt.Printf("64-bit parsing: %v\n", duration64)
fmt.Printf("Difference: %v\n", duration64-duration32)
}
The choice between 32-bit and 64-bit parsing depends on your precision requirements and memory constraints. For most applications, 64-bit provides sufficient precision without significant performance penalties. However, when processing large datasets where memory usage is critical, 32-bit parsing can reduce memory footprint while maintaining adequate precision for many use cases.