mirror of
https://github.com/kataras/iris.git
synced 2025-01-23 02:31:04 +01:00
minor improvements on x/jsonx.ISO8601 time parser
note that many new features are coming that I'm designing for the past 2-3 months, this is a commit with just one hotfix
This commit is contained in:
parent
ddda4f6998
commit
94cfb5494f
|
@ -32,11 +32,31 @@ const (
|
||||||
// ISO8601Layout holds the time layout for the the javascript iso time.
|
// ISO8601Layout holds the time layout for the the javascript iso time.
|
||||||
// Read more at: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString.
|
// Read more at: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString.
|
||||||
ISO8601Layout = "2006-01-02T15:04:05"
|
ISO8601Layout = "2006-01-02T15:04:05"
|
||||||
// ISO8601ZLayout same as ISO8601Layout but with the timezone suffix.
|
// ISO8601LayoutWithTimezone same as ISO8601Layout but with the timezone suffix.
|
||||||
ISO8601ZLayout = "2006-01-02T15:04:05Z"
|
ISO8601LayoutWithTimezone = "2006-01-02T15:04:05Z"
|
||||||
// ISO8601ZUTCOffsetLayout ISO 8601 format, with full time and zone with UTC offset.
|
|
||||||
|
// To match Go’s standard time layout that pads zeroes for microseconds, you can use the format 2006-01-02T15:04:05.000000Z07:00.
|
||||||
|
// This layout uses 0s instead of 9s for the fractional second part, which ensures that the microseconds are
|
||||||
|
// always represented with six digits, padding with leading zeroes if necessary.
|
||||||
|
// ISO8601ZUTCOffsetLayoutWithMicroseconds = "2006-01-02T15:04:05.000000Z07:00"
|
||||||
|
// ISO8601ZUTCOffsetLayoutWithMicroseconds ISO 8601 format, with full time and zone with UTC offset.
|
||||||
// Example: 2022-08-10T03:21:00.000000+03:00, 2023-02-04T09:48:14+00:00, 2022-08-09T00:00:00.000000.
|
// Example: 2022-08-10T03:21:00.000000+03:00, 2023-02-04T09:48:14+00:00, 2022-08-09T00:00:00.000000.
|
||||||
ISO8601ZUTCOffsetLayout = "2006-01-02T15:04:05.999999Z07:00" // -07:00
|
ISO8601ZUTCOffsetLayoutWithMicroseconds = "2006-01-02T15:04:05.999999Z07:00"
|
||||||
|
// ISO8601ZUTCOffsetLayoutWithoutMicroseconds ISO 8601 format, with full time and zone with UTC offset without microsecond precision.
|
||||||
|
ISO8601ZUTCOffsetLayoutWithoutMicroseconds = "2006-01-02T15:04:05Z07:00"
|
||||||
|
/*
|
||||||
|
The difference between the two time layouts "2006-01-02T15:04:05Z07:00" and "2006-01-02T15:04:05-07:00" is the presence of the Z character:
|
||||||
|
|
||||||
|
"2006-01-02T15:04:05Z07:00": The Z indicates that the time is in UTC (Coordinated Universal Time) if there’s no offset specified.
|
||||||
|
When an offset is present, as in +03:00, it indicates the time is in a timezone that is 3 hours ahead of UTC.
|
||||||
|
The Z is combined with the offset (07:00), which can be positive or negative to represent the timezone difference from UTC.
|
||||||
|
|
||||||
|
"2006-01-02T15:04:05-07:00": This layout does not have the Z character and directly uses the offset (-07:00).
|
||||||
|
It’s more straightforward and indicates that the time is in a timezone that is 7 hours behind UTC.
|
||||||
|
In summary, the Z in the first layout serves as a placeholder for UTC and is used when the time might be in UTC or might have an offset.
|
||||||
|
The second layout is used when you’re directly specifying the offset without any reference to UTC.
|
||||||
|
Both layouts can parse the timestamp "2024-04-08T04:47:10+03:00" correctly, as they include placeholders for the timezone offset.
|
||||||
|
*/
|
||||||
)
|
)
|
||||||
|
|
||||||
// ISO8601 describes a time compatible with javascript time format.
|
// ISO8601 describes a time compatible with javascript time format.
|
||||||
|
@ -55,30 +75,17 @@ func ParseISO8601(s string) (ISO8601, error) {
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
|
|
||||||
if idx := strings.LastIndexFunc(s, startUTCOffsetIndexFunc); idx > 18 /* should have some distance, with and without milliseconds */ {
|
// Check if the string contains a timezone offset after the 'T' character.
|
||||||
length := parseSignedOffset(s[idx:])
|
hasOffset := strings.Contains(s, "Z") || (strings.Index(s, "+") > strings.Index(s, "T")) || (strings.Index(s, "-") > strings.Index(s, "T"))
|
||||||
|
|
||||||
if idx+1 > idx+length || len(s) <= idx+length+1 {
|
switch {
|
||||||
return ISO8601{}, fmt.Errorf("ISO8601: invalid timezone format: %s", s[idx:])
|
case strings.HasSuffix(s, "Z"):
|
||||||
}
|
tt, err = time.Parse(ISO8601LayoutWithTimezone, s)
|
||||||
|
case hasOffset && strings.Contains(s, "."):
|
||||||
offsetText := s[idx+1 : idx+length]
|
tt, err = time.Parse(ISO8601ZUTCOffsetLayoutWithMicroseconds, s)
|
||||||
offset, parseErr := strconv.Atoi(offsetText)
|
case hasOffset:
|
||||||
if parseErr != nil {
|
tt, err = parseWithOffset(s)
|
||||||
return ISO8601{}, fmt.Errorf("ISO8601: %w", parseErr)
|
default:
|
||||||
}
|
|
||||||
|
|
||||||
// E.g. offset of +0300 is returned as 10800 which is - (3 * 60 * 60).
|
|
||||||
secondsEastUTC := offset * 60 * 60
|
|
||||||
|
|
||||||
if loc, ok := fixedEastUTCLocations[secondsEastUTC]; ok { // Specific (fixed) zone.
|
|
||||||
tt, err = time.ParseInLocation(ISO8601ZUTCOffsetLayout, s, loc)
|
|
||||||
} else { // Local or UTC.
|
|
||||||
tt, err = time.Parse(ISO8601ZUTCOffsetLayout, s)
|
|
||||||
}
|
|
||||||
} else if s[len(s)-1] == 'Z' {
|
|
||||||
tt, err = time.Parse(ISO8601ZLayout, s)
|
|
||||||
} else {
|
|
||||||
tt, err = time.Parse(ISO8601Layout, s)
|
tt, err = time.Parse(ISO8601Layout, s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,6 +94,97 @@ func ParseISO8601(s string) (ISO8601, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return ISO8601(tt), nil
|
return ISO8601(tt), nil
|
||||||
|
|
||||||
|
/*
|
||||||
|
if idx := strings.LastIndexFunc(s, startUTCOffsetIndexFunc); idx > 18 { // should have some distance, with and without milliseconds
|
||||||
|
length := parseSignedOffset(s[idx:])
|
||||||
|
|
||||||
|
if idx+1 > idx+length || len(s) <= idx+length+1 {
|
||||||
|
return ISO8601{}, fmt.Errorf("ISO8601: invalid timezone format: %s", s[idx:])
|
||||||
|
}
|
||||||
|
|
||||||
|
offsetText := s[idx+1 : idx+length]
|
||||||
|
offset, parseErr := strconv.Atoi(offsetText)
|
||||||
|
if parseErr != nil {
|
||||||
|
return ISO8601{}, fmt.Errorf("ISO8601: %w", parseErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// E.g. offset of +0300 is returned as 10800 which is - (3 * 60 * 60).
|
||||||
|
secondsEastUTC := offset * 60 * 60
|
||||||
|
|
||||||
|
// fmt.Printf("parsing %s with offset %s, secondsEastUTC: %d, using time layout: %s\n", s, offsetText, secondsEastUTC, ISO8601ZUTCOffsetLayoutWithMicroseconds)
|
||||||
|
if loc, ok := fixedEastUTCLocations[secondsEastUTC]; ok { // Specific (fixed) zone.
|
||||||
|
if strings.Contains(s, ".") {
|
||||||
|
tt, err = time.ParseInLocation(ISO8601ZUTCOffsetLayoutWithMicroseconds, s, loc)
|
||||||
|
} else {
|
||||||
|
tt, err = time.ParseInLocation(ISO8601ZUTCOffsetLayout, s, loc)
|
||||||
|
}
|
||||||
|
} else { // Local or UTC.
|
||||||
|
if strings.Contains(s, ".") {
|
||||||
|
tt, err = time.Parse(ISO8601ZUTCOffsetLayoutWithMicroseconds, s)
|
||||||
|
} else {
|
||||||
|
tt, err = time.Parse(ISO8601ZUTCOffsetLayout, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if s[len(s)-1] == 'Z' {
|
||||||
|
tt, err = time.Parse(ISO8601ZLayout, s)
|
||||||
|
} else {
|
||||||
|
tt, err = time.Parse(ISO8601Layout, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return ISO8601{}, fmt.Errorf("ISO8601: %w", err)
|
||||||
|
}
|
||||||
|
return ISO8601(tt), nil
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseWithOffset(s string) (time.Time, error) {
|
||||||
|
idx := strings.LastIndexFunc(s, startUTCOffsetIndexFunc)
|
||||||
|
if idx == -1 {
|
||||||
|
return time.Time{}, fmt.Errorf("ISO8601: missing timezone offset")
|
||||||
|
}
|
||||||
|
|
||||||
|
offsetText := s[idx:]
|
||||||
|
secondsEastUTC, err := parseOffsetToSeconds(offsetText)
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
loc, ok := fixedEastUTCLocations[secondsEastUTC]
|
||||||
|
if !ok {
|
||||||
|
loc = time.FixedZone("", secondsEastUTC)
|
||||||
|
}
|
||||||
|
|
||||||
|
return time.ParseInLocation(ISO8601ZUTCOffsetLayoutWithoutMicroseconds, s, loc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseOffsetToSeconds(offsetText string) (int, error) {
|
||||||
|
if len(offsetText) < 6 {
|
||||||
|
return 0, fmt.Errorf("ISO8601: invalid timezone offset length: %s", offsetText)
|
||||||
|
}
|
||||||
|
|
||||||
|
sign := offsetText[0]
|
||||||
|
if sign != '-' && sign != '+' {
|
||||||
|
return 0, fmt.Errorf("ISO8601: invalid timezone offset sign: %c", sign)
|
||||||
|
}
|
||||||
|
|
||||||
|
hours, err := strconv.Atoi(offsetText[1:3])
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("ISO8601: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
minutes, err := strconv.Atoi(offsetText[4:6])
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("ISO8601: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
secondsEastUTC := (hours*60 + minutes) * 60
|
||||||
|
if sign == '-' {
|
||||||
|
secondsEastUTC = -secondsEastUTC
|
||||||
|
}
|
||||||
|
|
||||||
|
return secondsEastUTC, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalJSON parses the "b" into ISO8601 time.
|
// UnmarshalJSON parses the "b" into ISO8601 time.
|
||||||
|
|
|
@ -6,6 +6,77 @@ import (
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestParseISO8601(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
want ISO8601
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Timestamp with microseconds",
|
||||||
|
input: "2024-01-02T15:04:05.999999Z",
|
||||||
|
want: ISO8601(time.Date(2024, 01, 02, 15, 04, 05, 999999*1000, time.UTC)),
|
||||||
|
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Timestamp with timezone but no microseconds",
|
||||||
|
input: "2024-01-02T15:04:05+07:00",
|
||||||
|
want: ISO8601(time.Date(2024, 01, 02, 15, 04, 05, 0, time.FixedZone("", 7*3600))),
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Timestamp with timezone of UTC with microseconds",
|
||||||
|
input: "2024-04-08T08:05:04.830140+00:00",
|
||||||
|
// time.Date function interprets the nanosecond parameter. The time.Date function expects the nanosecond parameter to be the entire nanosecond part of the time, not just the microsecond part.
|
||||||
|
// When we pass 830140 as the nanosecond argument, Go interprets this as 830140 nanoseconds,
|
||||||
|
// which is equivalent to 000830140 microseconds (padded with leading zeros to fill the nanosecond precision).
|
||||||
|
// This is why we see 2024-04-08 08:05:04.00083014 +0000 UTC as the output.
|
||||||
|
// To correctly represent 830140 microseconds, we need to convert it to nanoseconds by multiplying by 1000 (or set the value to 830140000).
|
||||||
|
want: ISO8601(time.Date(2024, 04, 8, 8, 05, 04, 830140*1000, time.UTC)),
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Timestamp with timezone but no microseconds (2)",
|
||||||
|
input: "2024-04-08T04:47:10+03:00",
|
||||||
|
want: ISO8601(time.Date(2024, 04, 8, 4, 47, 10, 0, time.FixedZone("", 3*3600))),
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Timestamp with Zulu time",
|
||||||
|
input: "2024-01-02T15:04:05Z",
|
||||||
|
want: ISO8601(time.Date(2024, 01, 02, 15, 04, 05, 0, time.UTC)),
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Basic ISO8601 layout",
|
||||||
|
input: "2024-01-02T15:04:05",
|
||||||
|
want: ISO8601(time.Date(2024, 01, 02, 15, 04, 05, 0, time.UTC)),
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid format",
|
||||||
|
input: "2024-01-02",
|
||||||
|
want: ISO8601{},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := ParseISO8601(tt.input)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("ParseISO8601() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !tt.wantErr && !time.Time(got).Equal(time.Time(tt.want)) {
|
||||||
|
t.Errorf("ParseISO8601() = %v (%s), want %v (%s)", got, got.ToTime().String(), tt.want, tt.want.ToTime().String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestISO8601(t *testing.T) {
|
func TestISO8601(t *testing.T) {
|
||||||
data := `{"start": "2021-08-20T10:05:01", "end": "2021-12-01T17:05:06", "nothing": null, "empty": ""}`
|
data := `{"start": "2021-08-20T10:05:01", "end": "2021-12-01T17:05:06", "nothing": null, "empty": ""}`
|
||||||
v := struct {
|
v := struct {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user