Rez Moss

Rez Moss

Digital Reflections: Exploring Tech, Innovation & Ideas

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

Sep 2025

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.

comments powered by Disqus