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 '