Error parsing time string with timezone as "GMT+0000" - go

I am trying to parse "Tue Jun 11 2019 13:26:45 GMT+0000" with "Mon Jan 02 2006 15:04:05 MST-0700" as a layout string using time.Parse but I get this error parsing time "Tue Jun 11 2019 13:26:45 GMT+0000" as "Mon Jan 02 2006 15:04:05 MST-0700": cannot parse "" as "-0700".
I've been using above layout string for other offsets and it works fine. But, I think "+0000" is not being considered a valid offset or something? Any suggestions would be helpful.
EDIT: Using "Mon Jan 02 2006 15:04:05 GMT-0700" as the layout works and I get the output as 2019-06-11 13:26:45 +0000 +0000.

EDIT 2: According to reply in github issues, it turns out that in layout string "MST-0700" is actually two time zone value "MST" and numeric time zone value "-0700" which takes precedence over the former. And "GMT+08" or "GMT+00" is considered as a time zone value as a whole and matches against "MST". So it would be "GMT+08+0800" for "MST-0700".
I rarely touches time zone related problems, but I personally think this behaviour is confusing.
Original answer:
This is a bug confusing behaviour of Go's time library. While I am not certain of the desired behaviour of GMT time format (since I rarely see something like GMT+0800 or so in a time format string), the code logic that make GMT+0800 valid but GMT+0000 does not makes sense.
I have submitted an issue on the github: https://github.com/golang/go/issues/40472
In short, Go's time library parses format by consuming the layout string and the value string together. And currently when parsing a "GMT" time zone, it consumes more of the value string, if the following value string (say, "+0800" or "+0000") is signed and in range from -23 to +23. So not only does "GMT+0000" fails, but "GMT+0030" and "GMT+08" (when matching to `"MST-07") fails too.
Here is the detail: When parsing time zone, it checks the layout string and found "MST", so it tries to parse the time zone value in the value string, which at that time only has "GMT+0X00" (where X is '0' or '8') - others have been consumed (correctly). Source
case stdTZ:
// Does it look like a time zone?
if len(value) >= 3 && value[0:3] == "UTC" {
z = UTC
value = value[3:]
break
}
n, ok := parseTimeZone(value)
if !ok {
err = errBad
break
}
zoneName, value = value[:n], value[n:]
It's all good here. The parseTimeZone function makes a special case for GMT format for reason that is not obvious to me, but here is the code:
// Special case 2: GMT may have an hour offset; treat it specially.
if value[:3] == "GMT" {
length = parseGMT(value)
return length, true
}
And then parseGMT code is like this:
// parseGMT parses a GMT time zone. The input string is known to start "GMT".
// The function checks whether that is followed by a sign and a number in the
// range -23 through +23 excluding zero.
func parseGMT(value string) int {
value = value[3:]
if len(value) == 0 {
return 3
}
return 3 + parseSignedOffset(value)
}
From the comment, we can already sense problem: "+0800" is not a number in range from -23 to +23 excluding leading zero (while "+0000") is. And obviously, this function is trying to indicate (from the return value) to consume more than 3 bytes, that is more than the "GMT". We can confirm it in code of parseSignedOffset.
// parseSignedOffset parses a signed timezone offset (e.g. "+03" or "-04").
// The function checks for a signed number in the range -23 through +23 excluding zero.
// Returns length of the found offset string or 0 otherwise
func parseSignedOffset(value string) int {
sign := value[0]
if sign != '-' && sign != '+' {
return 0
}
x, rem, err := leadingInt(value[1:])
// fail if nothing consumed by leadingInt
if err != nil || value[1:] == rem {
return 0
}
if sign == '-' {
x = -x
}
if x < -23 || 23 < x {
return 0
}
return len(value) - len(rem)
}
Here, leadingInt is an uninteresting function that converts the prefix of a string to an int64, and the remaining part of the string.
So parseSignedOffset did what the documentation advertises: it parse the value string and see if it is in the range form -23 to +23, but at face value:
It considers +0800 as 800, larger than +23, so it returns 0. As a result, the parseGMT (and parseTimeZone) returns 3, so the Parse function only consumes 3 bytes this time, and leave "+0800" to match with "-0700", so it is parsed correctly.
It considers +0000 as 0, a valid value in the range, so it returns 5, meaning "+0000" is part of the time zone, and thus parseGMT (and parseTimeZone) returns 8, making the Parse function consumes the whole string, leaving "" to match with "-0700", and hence the error.
EDIT:
Using GMT in format string and get the "right" value is because that the "GMT" in format string is not considered as time zone, but a part of the format (just like spaces in the string), and "GMT" time zone is the same as the default time zone ("UTC").
You can time.Parse("Mon Jan 02 2006 15:04:05 XYZ-0700", "Tue Jun 11 2019 13:26:45 XYZ+0800") without getting an error.

Related

How to get the same output of departed Date.parse() in groovy?

I have an application that runs the old version of the spring application. The application has the function to create date objects using Date.parse as follows
Date getCstTimeZoneDateNow() {
String dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
def zonedDateString = new Date().format(dateFormat, TimeZone.getTimeZone('CST'))
Date date = Date.parse(dateFormat, zonedDateString)
return date // Tue Oct 18 20:36:12 EDT 2022 (in Date)
}
However, the code above is deprecated. I need to produce the same result.
I read other posts and it seems like Calender or SimpleDateFormatter is preferred.
And I thought SimpleDateFormatter has more capabilities.
This post helped me understand more about what is going on in the following code
SimpleDateFormat parse loses timezone
Date getCstTimeZoneDateNow() {
Date now = new Date()
String pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
SimpleDateFormat sdf = new SimpleDateFormat()
sdf.setTimeZone(TimeZone.getTimeZone('CST'))
// cstDateTime prints times in cst
String cstDateTime = sdf.format(now) // 2022-10-18T20:36:12.088Z (in String)
// JVM current time
Date date = sdf.parse(cstDateTime) // Tue Oct 18 21:36:12 EDT 2022 (in Date)
return date
}
Here my goal is to return the date object that is in the format of Tue Oct 18 20:36:12 EDT 2022
The format is good. However, like the post says, when I do sdf.parse(), it prints in JVM time.
This means, the format is good but the time zone is off.
How can I get the exact same result as before?
It does not have to use SimpleDateFormatter. Could be anything.
Thank you so much for reading and for your time.
Perhaps the important thing is, that the Date is always neutral to the timezone. Given example shows what is to be expected to work from the Java specs:
def format = new SimpleDateFormat()
format.setTimeZone(TimeZone.getTimeZone("CST"))
println new Date()
def date = format.parse(format.format(new Date()))
printf "parsed to %s%n", date
printf "formatted to %s (%s)%n", format.format(date), format.getTimeZone().getDisplayName()
In the output, notice when using the Format and when the toString(), a different time is shown accordingly, which is perfectly fine, since first we format and then parse again in the same format, thus the same time-zone. Later, we use the Date.toString() to output the date, this time using the system default time-zone which is always used when Date.toString() is called. In the output, the time-zone shift is reflected:
Thu Oct 20 09:22:58 EDT 2022
parsed to Thu Oct 20 09:22:00 EDT 2022
formatted to 10/20/22 8:22 AM (Central Standard Time)

golang time.Format() gives different results for the same unix timestamp

time.Time initialized with time.Unix() and time.Parse() with exactly the same unix timestamp gives different results being printed with time.Format("2006-01-02")
The problem is not reproducible in playground, but I get it if I compile it myself.
My default time zone is Los Angeles, probably in different timezone result would be different.
go version
go version go1.12.1 darwin/amd64
go build
./test
test.go:
package main
import (
"fmt"
"time"
)
func main() {
control1 := time.Unix(1546300800, 0)
test, _ := time.Parse("2006-01-02", "2019-01-01")
fmt.Println("control:", control1.Unix(), control1.Format("2006-01-02"))
fmt.Println("test:", test.Unix(), test.Format("2006-01-02"))
}
./test
control: 1546300800 2018-12-31
test: 1546300800 2019-01-01
So unix ts is the same (1546300800), but dates are different. Why?
The printed dates are different because they have different timezones.
time.Unix() returns the local Time, while time.Parse():
Elements omitted from the value are assumed to be zero or, when zero is impossible, one, so parsing "3:04pm" returns the time corresponding to Jan 1, year 0, 15:04:00 UTC (note that because the year is 0, this time is before the zero Time).
time.Parse() returns a time.Time having UTC zone by default (if zone info is not part of the input and layout).
This also explains why you can't see it on the Go Playground: the local time there is UTC.
Printing the zone info in my local computer (CET timezone):
fmt.Println("control:", control1.Unix(), control1.Format("2006-01-02 -0700"))
fmt.Println("test :", test.Unix(), test.Format("2006-01-02 -0700"))
fmt.Println(control1.Zone())
fmt.Println(test.Zone())
Outputs:
control: 1546300800 2019-01-01 +0100
test : 1546300800 2019-01-01 +0000
CET 3600
UTC 0
If you switch both times to the same zone (e.g. UTC or local), the printed dates will be the same:
control1 = control1.UTC()
test = test.UTC()
After this, the output:
control: 1546300800 2019-01-01 +0000
test : 1546300800 2019-01-01 +0000
UTC 0
UTC 0

How to format current time into YYYY-MM-DDTHH:MM:SSZ

Never tried Go before and currently doing a small project. One of the task is to get current system time and represent it in YYYY-MM-DDT00:00:00Z format. I believe that Z means that time is represented in UTC format but when i looked into db, all timestamps are like this i.e., 2011-11-22T15:22:10Z.
So how can i format like this in Go?
Update
I was able to format it using following code
t := time.Now()
fmt.Println(t.Format("2006-01-02T15:04:05Z"))
Now the question remains, what Z signifies here. Should i get UTC Time?
Another question, it looks like that the value i am using to format impacts the output i.e., when i used 2019-01-02T15:04:05Z the output became 2029-02-02T20:45:11Z, why?
Go provides very flexible way to parse the time by example. For this, you have to write the "reference" time in the format of your choice. The reference time is Mon Jan 2 15:04:05 MST 2006. In my case, I used this reference time to parse the Now():
fmt.Println(time.Now().UTC().Format(time.RFC3339))
There are also other reference types if you want to see:
RFC822 = "02 Jan 06 15:04 MST"
RFC822Z = "02 Jan 06 15:04 -0700" // RFC822 with numeric zone
RFC850 = "Monday, 02-Jan-06 15:04:05 MST"
RFC1123 = "Mon, 02 Jan 2006 15:04:05 MST"
RFC1123Z = "Mon, 02 Jan 2006 15:04:05 -0700" // RFC1123 with numeric zone
RFC3339 = "2006-01-02T15:04:05Z07:00"
RFC3339Nano = "2006-01-02T15:04:05.999999999Z07:00"
Or you can use you desired reference.
"If a time is in Coordinated Universal Time (UTC), a "Z" is added directly after the time without a separating space. "Z" is the zone designator for the zero UTC offset. "09:30 UTC" is therefore represented as "09:30Z" or "0930Z". Likewise, "14:45:15 UTC" is written as "14:45:15Z" or "144515Z".[16]"
From https://en.wikipedia.org/wiki/Time_zone#UTC
// Some valid layouts are invalid time values for time.Parse, due to formats
// such as _ for space padding and Z for zone information.
and
// Replacing the sign in the format with a Z triggers
// the ISO 8601 behavior of printing Z instead of an
// offset for the UTC zone. Thus:
// Z0700 Z or ±hhmm
// Z07:00 Z or ±hh:mm
// Z07 Z or ±hh
From the source for package time/format.go

Checking for time

I have a form field which may have a date and may have a time in it. I need to confirm that both a date and time are present.
<input type="text" name="transdate" ... />
I can use isDate(form.transdate) to check if there is a date, but it does not check if there is a time. I wish there was a isTime() function.
Addendum
The date time fields can be made to have
These fields are concatinated via
date_cat = "#form.trans_date# #form.trans_date_h#:#form.trans_date_m# #form.trans_date_t#";
When I run this code:
cat: #date_cat# isValid(date): #isValid('date', date_cat)# isValid(time): #isValid('time', date_cat)#
I get
cat: 12/05/2018 :24 PM isValid(date): YES isValid(time): YES
Some people hate regular expressions. I love them. Why not just check the concatenated string?
dtRegEx = "^(0[1-9]|1[0-2])/(0[1-9]|[1-2][0-9]|3[0-1])/[1-9][0-9]{3} (0[0-9]|1[0-2]):[0-5][0-9] (am|pm)$";
if (reFind(dtRegEx, date_cat) and isDate(date_cat)) {
// valid datetime
} else {
// invalid datetime
}
RegEx Breakdown
^
string has to start with the whole pattern
(0[1-9]|1[0-2])
month in range from 01 to 09 or 10 to 12
/
date delimiter
(0[1-9]|[1-2][0-9]|3[0-1])
day in range from 01 to 09, 10 to 29 or 30 to 31
/
date delimiter
[1-9][0-9]{3}
year in range from 1000 to 9999
space
space, literally
(0[0-9]|1[0-2])
hour in range from 00 to 09 or 10 to 12
:
time delimiter
[0-5][0-9]
seconds in range from 00 to 59
space
space, again
(am|pm)
the meridiem stuff you guys from US and UK like so much :P
$
string has to end with the whole pattern
Note that the above pattern could still have you end up with invalid day ranges like 02/31/2018, that's why you should still check with isDate().
Here is how I addressed it, I validated the fields before the concatenation
if (form.trans_date_h == "" || form.trans_date_m == "" || form.trans_date_t == "") {
// error handling here
Then did the concatenation
date_cat = "#form.trans_date# #form.trans_date_h#:#form.trans_date_m# #form.trans_date_t#";

How to set the Zone of a Go time value when knowing the UTC time and time offset?

I have an UTC time and a time offset in seconds, and need to return the corresponding Go time value.
It is trivial to instantiate the UTC time value by using the time.Unix() function. But to set the Zone, I need to determine the time.Location.
How can I find the time.Location when knowing the UTC time and time offset ?
Without an actual entry to lookup in the time zone database, you can't know the true location for the time. If you want to work with just an offset, you can create a fixed location using time.FixedZone
edt := time.FixedZone("EDT", -60*60*4)
t, _ := time.ParseInLocation("02 Jan 06 15:04", "15 Sep 17 14:55", edt)
fmt.Println(t)
// 2017-09-15 14:55:00 -0400 EDT
You can opt to specify a non-existent zone name, or none at all, as long as the output format you use doesn't require one.
minus4 := time.FixedZone("", -60*60*4)
t, _ = time.ParseInLocation("02 Jan 06 15:04", "15 Sep 17 14:55", minus4)
fmt.Println(t.Format(time.RFC3339))
// 2017-09-15T14:55:00-04:00

Resources