How to parse fractional seconds in Go

Time is often represented as Seconds Since the Unix Epoch.

Right now, for example:

1614236182.651

However, different languages handle the resolution different.

  • JavaScript uses Milliseconds: 1614236182.651
  • Golang uses a tuple of (Seconds, Nanoseconds): 1614236182, 651000000

Golang has a great time library with almost everything you’d ever want. Suprisingly, however, it’s strangely missing a function to parse fractional Unix time seconds as a time.Time.

You might expect there to be some sort of time.ParseSeconds which could works like this:

func main() {
	secs, nanos, _ := ParseSeconds("1614236182.651")
	fmt.Println(time.Unix(secs, nanos))
}

But no such function exists and parsing the fractional part of a second into nanoseconds is… awkward.

Anyway, here are a couple of solutions to keep in your back pocket for when you need them.

Parsing Fractional Seconds into Nanoseconds

At a high level, we just want to get secs and nanos so that we can produce a time.Time:

func ParseUnixSeconds(s string) (time.Time, error) {
	secs, nano, err := ParseSeconds(s)
	if nil != err {
		return time.Time{}, err
	}

	return time.Unix(secs, nano), nil
}

There’s a few different ways to do this.

As for myself, I like to use the standard library to the full extend that I can so that I have (probably) performant and (more importantly) easy to read code.

That said I also agree with the philosophy of “a little copying is better than a little dependency” so I provided a few other examples that may (or may not) be more performant depending on whether you’re starting with a string or a float64.

Trick #1: strconv

func ParseSeconds(s string) (int64, int64, error) {
    seconds, err := strconv.ParseFloat(s, 64)
    if nil != err {
		return 0.0, 0.0, error
	}
	secs, nanos := SecondsToInts(seconds)
	return secs, nanos, nil
}

func SecondsToInts(seconds float64) (int64, int64) {
	secs := math.Floor(seconds)
	nanos := math.Round((seconds - secs) * 1_000_000_000)
	return secs, nanos
}

Trick #2: time.ParseDuration

Here’s a very simple version, leveraging the standard library as much as possible.

I’m using time.ParseDuration as a cheap way to convert nanoseconds without padding 0s or adjusting the nanosecond value to the correct 10s place.

func ParseSeconds(secs string) (int64, int64, error) {
	// "789.0123" => []string{"789", "0123"}
	parts := strings.Split(secs, ".")
	if len(parts) > 2 {
		return 0, 0, errors.New("could not parse as seconds")
	} else if len(parts) < 2 {
		// no nanoseconds, just seconds
		// "789" => []string{"789"}
		s, err := strconv.ParseInt(parts[0], 10, 64)
		return s, 0, err
	}

	// convert the second's part
	s, err := strconv.ParseInt(parts[0], 10, 64)
	if nil != err {
		return 0, 0, err
	}

	// get nanoseconds from fractional second
	d, err := time.ParseDuration("0." + parts[1] + "s")
	return s, d.Nanoseconds(), err
}

Trick #3: Strings Parts to Ints

If we don’t want to use the time library then we can either

  • pad with 0s before parsing
  • multiply by the correct magnitude

Here’s the pad and parse approach:

func ParseSeconds(secs string) (int64, int64, error) {
	// "789.0123" => []string{"789", "0123"}
	parts := strings.Split(secs, ".")
	if len(parts) > 2 {
		return 0, 0, errors.New("could not parse as seconds")
	} else if len(parts) < 2 {
		// no nanoseconds, just seconds
		// "789" => []string{"789"}
		s, err := strconv.ParseInt(parts[0], 10, 64)
		return s, 0, err
	}

	// convert the second's part
	s, err := strconv.ParseInt(parts[0], 10, 64)
	if nil != err {
		return 0, 0, err
	}

	// the pad and parse approach
	// (nanoseconds have at most 9 digits)
	nanos := parts[1]
	n := len(nanos)
	if n < 9 {
		nanos += strings.Repeat("0", 9-n)
	} else if n > 9 {
		// truncate nanos to 9 digits
		// 0.0123456789 => 0.012345678
		nanos = nanos[:9]
	}
	nano, err := strconv.ParseInt(nanos, 10, 64)
	if nil != err {
		return 0, 0, err
	}
	return s, nano, nil
}

And here’s the multiplication approach:

func ParseSeconds(secs string) (int64, int64, error) {
	// "789.0123" => []string{"789", "0123"}
	parts := strings.Split(secs, ".")
	if len(parts) > 2 {
		return 0, 0, errors.New("could not parse as seconds")
	} else if len(parts) < 2 {
		// no nanoseconds, just seconds
		// "789" => []string{"789"}
		s, err := strconv.ParseInt(parts[0], 10, 64)
		return s, 0, err
	}

	// convert the second's part
	s, err := strconv.ParseInt(parts[0], 10, 64)
	if nil != err {
		return 0, 0, err
	}

    // the multiply approach
    // (nanoseconds have at most 9 digits)
	nanos := parts[1]
    n := len(nanos)
	if n > 9 {
        // truncate nanos to 9 digits
        // 0.0123456789 => 0.012345678
		nanos = nanos[:9]
        n = 9
	}
    mult := int64(math.Pow10(9-n))

	nano, err := strconv.ParseInt(nanos, 10, 64)
	if nil != err {
		return 0, 0, err
	}

	return s, nano * mult, nil
}

The Reverse: Time to Fractional Seconds

Since going from time to fractional (float) seconds is fairly straightforward I’ll just that real quick:

func ToUnixSeconds(t time.Time) float64 {
    // 1614236182.651912345
    secs := float64(t.Unix())                          // 1614236182
    nanos := float64(t.Nanosecond()) / 1_000_000_000.0 // 0.651912345

    // in my case I want to truncate the precision to milliseconds
    nanos = math.Round((10000 * nanos) / 10000) // 0.6519

    s := secs + nanos // 1614236182.651912345
    return s
}

The curious case of right padding 0s

The way to right pad spaces and left pad spaces and zeros in Go is to use fmt.Sprintf(), such as fmt.Sprintf().

But… you can’t right-pad zeros - which would come in really handy when parsing Nanoseconds.

// Left pad up to 10 spaces
fmt.Printf("'%*s'", 10, "Hello")
// '     Hello'

// Right pad up to 10 spaces
fmt.Sprintf("'Hello%-*s'", 10, "Hello")
// 'Hello     '

// Left pad up to 10 zeros
fmt.Printf("'%0*s'\n", 10, "Hello")
// '00000Hello'

// Right pad up to 10 zeros... psych!
fmt.Printf("'%-0*s'\n", 10, "Hello")
// 'Hello     '