Local time loses 52 seconds after marshal/unmarshal - go

I parse a time in Local, marshal it to JSON, un-marshal it and the times no longer match.
timeA, _ := time.ParseInLocation("15:04", "8:00", time.Local)
jBytes, _ := json.Marshal(timeA)
var timeB time.Time
json.Unmarshal(jBytes, &timeB)
fmt.Printf("Time A: %+v, Time B: %+v\n", timeA, timeB)
fmt.Printf("Time A: %+v, Time B: %+v\n", timeA.Local(), timeB.Local())
fmt.Printf("Diff: %s\n", timeA.Sub(timeB))
fmt.Printf("Marshaled: %s", string(jBytes))
Time A: 0000-01-01 08:00:00 -0733 LMT, Time B: 0000-01-01 08:00:00 -0733 -0733
Time A: 0000-01-01 08:00:00 -0733 LMT, Time B: 0000-01-01 07:59:08 -0733 LMT
Diff: 52s
Marshaled: "0000-01-01T08:00:00-07:33"
This is running on linux with Edmonton/Mountain as my local time so I guess it's not recognizing the location and showing offset twice -733 -733. When I call local, the parsed one consistently loses 52 seconds for some reason.
I'd expect the times to match. Is my clock 52 seconds off a remote one it's referencing or something?

Prior to September 1, 1906, your time zone difference was UTC-7:33:52. json.Unmarshal is just using the 7:33 in the marshaled text for the offset, instead of the correct value of 7:33:52, so the time.Time value it calculates is off by 52 seconds. But your time.Local implementation seems to be getting it right (to the extent we can describe backdating time zone differences to year 1 as "right") and subtracting the full 7:33:52 from the time.Time value, resulting in the difference you're seeing.
If you output:
fmt.Printf("Time A: %+v, Time B: %+v\n", timeA.UTC(), timeB.UTC())
with your current code you should see that the UTC time for timeB is getting set to 15:33:00 after unmarshaling, whereas the UTC time for timeA is getting set to 15:33:52. I suspect if you include a year after 1906 in your time string you'll see this 52 seconds difference disappear.
For example:
package main
import (
"encoding/json"
"fmt"
"log"
"time"
)
func main() {
zone, err := time.LoadLocation("America/Edmonton")
if err != nil {
log.Fatalf("%v", err)
}
for _, timestring := range []string{
"01 02 1905 8:00",
"01 02 1907 8:00",
} {
timeA, err := time.ParseInLocation("01 02 2006 15:04", timestring, zone)
if err != nil {
log.Fatalf("%v", err)
}
jBytes, _ := json.Marshal(timeA)
var timeB time.Time
json.Unmarshal(jBytes, &timeB)
fmt.Printf("Time string: %s\n", timestring)
fmt.Printf("Time A: %+v, Time B: %+v\n", timeA, timeB)
fmt.Printf("Time A: %+v, Time B: %+v\n", timeA.UTC(), timeB.UTC())
fmt.Printf("Time A: %+v, Time B: %+v\n", timeA.In(zone), timeB.In(zone))
fmt.Printf("Diff: %s\n", timeA.Sub(timeB))
fmt.Printf("Marshaled: %s\n", string(jBytes))
}
}
outputs:
paul#mac:got$ ./got
Time string: 01 02 1905 8:00
Time A: 1905-01-02 08:00:00 -0733 LMT, Time B: 1905-01-02 08:00:00 -0733 -0733
Time A: 1905-01-02 15:33:52 +0000 UTC, Time B: 1905-01-02 15:33:00 +0000 UTC
Time A: 1905-01-02 08:00:00 -0733 LMT, Time B: 1905-01-02 07:59:08 -0733 LMT
Diff: 52s
Marshaled: "1905-01-02T08:00:00-07:33"
Time string: 01 02 1907 8:00
Time A: 1907-01-02 08:00:00 -0700 MST, Time B: 1907-01-02 08:00:00 -0700 -0700
Time A: 1907-01-02 15:00:00 +0000 UTC, Time B: 1907-01-02 15:00:00 +0000 UTC
Time A: 1907-01-02 08:00:00 -0700 MST, Time B: 1907-01-02 08:00:00 -0700 MST
Diff: 0s
Marshaled: "1907-01-02T08:00:00-07:00"
paul#mac:got$
showing that the 52 second difference is there for 1905, but not for 1907 after the time zone difference changed to a straight UTC-7:00:00.
Short answer: marshaling to and unmarshaling from json by default appears unable to correctly handle seconds in time zone offsets, because no seconds appear in the offset in the marshaled string and this is the only time zone information json.Unmarshal has available to it.
For sure there is no referencing of clocks, remote or otherwise, in any of this code - it's just manipulating values.

You are defaulting to pseudo-date 0000-01-01 when local time was likely based on the position of the Sun at midday.
Simply parse the time-of-day. For example,
package main
import (
"encoding/json"
"fmt"
"time"
)
func main() {
timeA, err := time.Parse("15:04", "8:00")
fmt.Println(timeA, err)
jBytes, _ := json.Marshal(timeA)
var timeB time.Time
json.Unmarshal(jBytes, &timeB)
fmt.Printf("Time A: %+v, Time B: %+v\n", timeA, timeB)
fmt.Printf("Diff: %s\n", timeA.Sub(timeB))
fmt.Printf("Marshaled: %s\n", string(jBytes))
}
Output:
0000-01-01 08:00:00 +0000 UTC <nil>
Time A: 0000-01-01 08:00:00 +0000 UTC, Time B: 0000-01-01 08:00:00 +0000 UTC
Diff: 0s
Marshaled: "0000-01-01T08:00:00Z"

Related

Dealing with Timezones and Unix Timestamp in Golang

Given the following code:
https://go.dev/play/p/moLVHXIc4ba
It shows the next result:
now: 2009-11-10 23:00:00 +0000 UTC m=+0.000000001 | inLoc: 2009-11-10 18:00:00 -0500 -05unix | now: 1257894000 | inLoc: 1257894000
I don't get why when I call .Unix() to the date evaluated in Lima location, I get the same timestamp that original date in GMT.
I expect to get the timestamp corresponding to the original timestamp in GMT with 5 hours less as Lima has UTC -5.
I expect to get the timestamp corresponding to the original timestamp in GMT [UTC] with 5 hours less as Lima has UTC -5.
how can I get the timestamp with 5 hours less to pass to frontend?
You appear to be asking for this inLocTimestamp function. Five hours is 18,000 (5 * 60 * 60) seconds.
package main
import (
"fmt"
"time"
)
func inLocTimestamp(inLoc time.Time) int64 {
_, offset := inLoc.Zone()
ts := inLoc.Add(time.Duration(offset) * time.Second)
return ts.Unix()
}
func inLocTimestampMilli(inLoc time.Time) int64 {
_, offset := inLoc.Zone()
ts := inLoc.Add(time.Duration(offset) * time.Second)
return ts.UnixMilli()
}
func main() {
loc, _ := time.LoadLocation("America/Lima")
now := time.Now().Round(0)
inLoc := now.In(loc)
fmt.Printf("time | now: %v | inLoc: %v\n", now, inLoc)
fmt.Printf("unix | now: %v | inLoc: %v\n", now.Unix(), inLoc.Unix())
fmt.Printf("timestamp | now: %v | inLoc: %v\n", now.Unix(), inLocTimestamp(inLoc))
fmt.Printf("difference | inLoc: timestamp - unix = %v\n", inLocTimestamp(inLoc)-inLoc.Unix())
}
https://go.dev/play/p/IyzA4qZLLuS
time | now: 2009-11-10 23:00:00 +0000 UTC | inLoc: 2009-11-10 18:00:00 -0500 -05
unix | now: 1257894000 | inLoc: 1257894000
timestamp | now: 1257894000 | inLoc: 1257876000
difference | inLoc: timestamp - unix = -18000

How to construct time.Time with timezone offset [duplicate]

This question already has answers here:
How do you convert a time offset to a location/timezone in Go
(2 answers)
Closed 3 years ago.
This is an example date from an Apache log:
[07/Mar/2004:16:47:46 -0800]
I have successfully parsed this into year(int), month(time.Month), day(int), hour(int), minute(int), second(int), and timezone(string).
How can I construct time.Time such that it includes the -0800 time zone offset?
This is what I have so far:
var nativeDate time.Time
nativeDate = time.Date(year, time.Month(month), day, hour, minute, second, 0, ????)
What should I use in place of ????? time.Local or time.UTC is not appropriate here.
You may use time.FixedZone() to construct a time.Location with a fixed offset.
Example:
loc := time.FixedZone("myzone", -8*3600)
nativeDate := time.Date(2019, 2, 6, 0, 0, 0, 0, loc)
fmt.Println(nativeDate)
Output (try it on the Go Playground):
2019-02-06 00:00:00 -0800 myzone
If you have the zone offset as a string, you may use time.Parse() to parse it. Use a layout string that only contains the reference zone offset:
t, err := time.Parse("-0700", "-0800")
fmt.Println(t, err)
This outputs (try it on the Go Playground):
0000-01-01 00:00:00 -0800 -0800 <nil>
As you can see, the result time.Time has a zone offset of -0800 hours.
So our original example can also be written as:
t, err := time.Parse("-0700", "-0800")
if err != nil {
panic(err)
}
nativeDate := time.Date(2019, 2, 6, 0, 0, 0, 0, t.Location())
fmt.Println(nativeDate)
Output (try it on the Go Playground):
2019-02-06 00:00:00 -0800 -0800

For loop while time.Now() is reached

Is it possible in Golang to increment a date in a for loop by a given date variable till it reached the current date/ time.Now()
// Start date
t, _ := time.Parse(time.RFC3339, "2018-07-19T12:25:10.8584224+02:00")
// Current date
ct := time.Now()
for d := t; d.Day() == ct.Day(); d = d.AddDate(0, 0, 1) {
// Print all days between start date and current date
fmt.Println(d)
}
I expect that variable d prints out all dates (with time etc.) till it reached the current date
according to godoc: https://golang.org/pkg/time/#Time.Day
func (t Time) Day() int
Day returns the day of the month specified by t.
So comparing d.Day() and ct.Day() is not the right approaches. What if today is "2019-01-01",and you start time is "2018-12-23"?
The right way to compare two time.Time is https://golang.org/pkg/time/#Time.After
func (t Time) After(u Time) bool
func (t Time) Before(u Time) bool
After reports whether the time instant t is after u.
Before reports whether the time instant t is before u.
So #Alex Pliutau's solution is more in common use. But need more careful with today.
package main
import (
"fmt"
"time"
)
func main() {
t, _ := time.Parse(time.RFC3339, "2009-11-02T12:25:10.8584224+02:00")
// truncate to 0:0:0
t = t.Truncate(24 * time.Hour)
fmt.Println("start time is:", t)
// Current date truncate to 0:0:0
ct := time.Now().Truncate(24 * time.Hour)
fmt.Println("now is:", ct)
fmt.Println("---------------")
// for t.Before(ct) { //if you don't want to print the date of today
for !t.After(ct) {
// Print all days between start date and current date
fmt.Println(t.Format("2006-01-02 15:04:05"))
t = t.AddDate(0, 0, 1)
}
}
Output:
start time is: 2009-11-02 02:00:00 +0200 +0200
now is: 2009-11-10 00:00:00 +0000 UTC
---------------
2009-11-02 02:00:00
2009-11-03 02:00:00
2009-11-04 02:00:00
2009-11-05 02:00:00
2009-11-06 02:00:00
2009-11-07 02:00:00
2009-11-08 02:00:00
2009-11-09 02:00:00
2009-11-10 02:00:00
https://play.golang.org/p/iMr7M5W9K4N
get the loop condition right and..
package main
import (
"fmt"
"time"
)
func main() {
fmt.Println("Hello, playground")
t, _ := time.Parse(time.RFC3339, "2018-07-19T12:25:10.8584224+02:00")
// Current date
ct := time.Now()
for d := t; d.Day() >= ct.Day(); d = d.AddDate(0, 0, 1) {
// Print all days between start date and current date
fmt.Println(d)
}
}
Hello, playground
2018-07-19 12:25:10.8584224 +0200 +0200
2018-07-20 12:25:10.8584224 +0200 +0200
2018-07-21 12:25:10.8584224 +0200 +0200
2018-07-22 12:25:10.8584224 +0200 +0200
2018-07-23 12:25:10.8584224 +0200 +0200
2018-07-24 12:25:10.8584224 +0200 +0200
2018-07-25 12:25:10.8584224 +0200 +0200
2018-07-26 12:25:10.8584224 +0200 +0200
2018-07-27 12:25:10.8584224 +0200 +0200
2018-07-28 12:25:10.8584224 +0200 +0200
2018-07-29 12:25:10.8584224 +0200 +0200
2018-07-30 12:25:10.8584224 +0200 +0200
2018-07-31 12:25:10.8584224 +0200 +0200
https://play.golang.org/p/yRBTUZKfseG
Based on your comments, you need to actually tell it to Format the date to something of value:
package main
import (
"fmt"
"log"
"time"
)
func main() {
start, err := time.Parse("2006-1-2", "2018-1-1")
if err != nil {
log.Fatal(err)
}
for d := start; d.Month() == start.Month(); d = d.AddDate(0, 0, 1) {
fmt.Println(d.Format("2006-1-2"))
}
}
Here's a simpler version of your code (I used a custom time format, cause I didn't wanna edit the RFC syntax, but ultimately it's the same thing) = I'm also iterating Month for brevity.
package main
import (
"fmt"
"time"
)
func main() {
t, _ := time.Parse(time.RFC3339, "2018-07-19T12:25:10.8584224+02:00")
ct := time.Now()
for t.Before(ct) {
fmt.Println(t)
t.AddDate(0, 0, 1)
}
}

Use timestamp from Mysql in humanize.Time() package

I have a mariaDB database with a timestamp field. I want the values from that field being parsed to time.Time() values. This is possible by adding the ?parseTime=true to the connection string. After fetching a row, I want to use the value (which is time.Time) with humanize.Time(). Unfortunately values within the past 60 minutes are converted by humanize.Time() as 1 hour from now. When I put directly a time.Time() value into humanize.Time(), it gives me x seconds ago.
So I don't know what I'm doing wrong here. I think I need to convert 2017-04-23 14:00:16 +0000 UTC to 2017-04-23 14:00:16.370758048 +0200 CEST, but how?
package main
import (
"fmt"
"log"
"time"
humanize "github.com/dustin/go-humanize"
)
// value from database: 2017-04-23 14:00:16 +0000 UTC
// typical time.Now() value: 2017-04-23 14:00:16.370758048 +0200 CEST
func main() {
layout := "2006-01-02 15:04:05 -0700 MST"
beforeParsing := "2017-04-23 14:00:16 +0000 UTC"
t, err := time.Parse(layout, beforeParsing)
if err != nil {
log.Fatal(err)
}
fmt.Println(t)
fmt.Println(humanize.Time(t))
}

PST to UTC parsing of time in Golang

I am trying to convert the time from PST to UTC timezone but seeing some unexpected result, while IST to UTC is working fine:
package main
import (
"fmt"
"time"
)
func main() {
const longForm = "2006-01-02 15:04:05 MST"
t, err := time.Parse(longForm, "2016-01-17 20:04:05 IST")
fmt.Println(t, err)
fmt.Printf("IST to UTC: %v\n\n", t.UTC())
s, err1 := time.Parse(longForm, "2016-01-17 23:04:05 PST")
fmt.Println(s, err1)
fmt.Printf("PST to UTC: %v\n\n", s.UTC())
}
Output is :
2016-01-17 20:04:05 +0530 IST <nil>
IST to UTC: 2016-01-17 14:34:05 +0000 UTC
2016-01-17 23:04:05 +0000 PST <nil>
PST to UTC: 2016-01-17 23:04:05 +0000 UTC
When parsing is done for IST, it shows +0530, while for PST shows +0000 and in UTC it print same value of HH:MM:SS (23:04:05) as in PST. Am i missing anything here?
The documentation for time.Parse() says:
If the zone abbreviation is unknown, Parse records the time as being in a fabricated location with the given zone abbreviation and a zero offset. This choice means that such a time can be parsed and reformatted with the same layout losslessly, but the exact instant used in the representation will differ by the actual zone offset. To avoid such problems, prefer time layouts that use a numeric zone offset, or use ParseInLocation.
Here is how to use ParseInLocation:
IST, err := time.LoadLocation("Asia/Kolkata")
if err != nil {
fmt.Println(err)
return
}
PST, err := time.LoadLocation("America/Los_Angeles")
if err != nil {
fmt.Println(err)
return
}
const longForm = "2006-01-02 15:04:05 MST"
t, err := time.ParseInLocation(longForm, "2016-01-17 20:04:05 IST", IST)
fmt.Println(t, err)
fmt.Printf("IST to UTC: %v\n\n", t.UTC())
s, err1 := time.ParseInLocation(longForm, "2016-01-17 23:04:05 PST", PST)
fmt.Println(s, err1)
fmt.Printf("PST to UTC: %v\n\n", s.UTC())
Output:
2016-01-17 20:04:05 +0530 IST <nil>
IST to UTC: 2016-01-17 14:34:05 +0000 UTC
2016-01-17 23:04:05 -0800 PST <nil>
PST to UTC: 2016-01-18 07:04:05 +0000 UTC
Full code on the Go Playground
The documentation for time.Parse() says:
If the zone abbreviation is unknown, Parse records the time as being in a fabricated location with the given zone abbreviation and a zero offset. This choice means that such a time can be parsed and reformatted with the same layout losslessly, but the exact instant used in the representation will differ by the actual zone offset. To avoid such problems, prefer time layouts that use a numeric zone offset, or use ParseInLocation.
So, the system doesn't know what "PST" is. For me, the system also doesn't know what IST is. You can check for supported locations like so:
package main
import (
"fmt"
"time"
)
func main() {
for _, name := range []string{"MST", "UTC", "IST", "PST", "EST", "PT"} {
loc, err := time.LoadLocation(name)
if err != nil {
fmt.Println("No location", name)
} else {
fmt.Println("Location", name, "is", loc)
}
}
}
Output on my system:
Location MST is MST
Location UTC is UTC
No location IST
No location PST
Location EST is EST
No location PT

Resources