1
0
mirror of https://github.com/kataras/iris.git synced 2026-01-07 20:17:05 +00:00

x/jsonx: ISO8601: add support for unconventional offset timestamps

This commit is contained in:
Gerasimos (Makis) Maropoulos
2024-05-23 15:23:29 +03:00
parent 4117f3b88f
commit e89806766a
5 changed files with 288 additions and 103 deletions

View File

@@ -57,6 +57,10 @@ const (
The second layout is used when youre 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.
*/
// ISO8601UnconventionalOffsetLayout is the layout for the unconventional offset.
// Custom offset layout, e.g., 2024-05-21T18:06:07.000000-04:01:19.
ISO8601UnconventionalOffsetLayout = "2006-01-02T15:04:05.000000"
)
// ISO8601 describes a time compatible with javascript time format.
@@ -73,6 +77,7 @@ var _ Exampler = (*ISO8601)(nil)
// - 2024-01-02T15:04:05Z
// - 2024-04-08T08:05:04.830140
// - 2024-01-02T15:04:05
// - 2024-05-21T18:06:07.000000-04:01:19
func ParseISO8601(s string) (ISO8601, error) {
if s == "" || s == "null" {
return ISO8601{}, nil
@@ -108,6 +113,21 @@ func ParseISO8601(s string) (ISO8601, error) {
if idx := strings.LastIndexFunc(s, startUTCOffsetIndexFunc); idx > 18 { // should have some distance, with and without milliseconds
length := parseSignedOffset(s[idx:])
// Check if the offset is unconventional, e.g., -04:01:19
if offset := s[idx:]; isUnconventionalOffset(offset) {
mainPart := s[:idx]
tt, err = time.Parse("2006-01-02T15:04:05.000000", mainPart)
if err != nil {
return ISO8601{}, fmt.Errorf("ISO8601: %w", err)
}
adjustedTime, parseErr := adjustForUnconventionalOffset(tt, offset)
if parseErr != nil {
return ISO8601{}, fmt.Errorf("ISO8601: %w", parseErr)
}
return ISO8601(adjustedTime), nil
}
if idx+1 > idx+length || len(s) <= idx+length+1 {
return ISO8601{}, fmt.Errorf("ISO8601: invalid timezone format: %s", s[idx:])
}
@@ -195,6 +215,53 @@ func parseOffsetToSeconds(offsetText string) (int, error) {
return secondsEastUTC, nil
}
func isUnconventionalOffset(offset string) bool {
parts := strings.Split(offset, ":")
return len(parts) == 3
}
func adjustForUnconventionalOffset(t time.Time, offset string) (time.Time, error) {
sign := 1
if offset[0] == '-' {
sign = -1
}
offset = offset[1:]
offsetParts := strings.Split(offset, ":")
if len(offsetParts) != 3 {
return time.Time{}, fmt.Errorf("invalid offset format: %s", offset)
}
hours, err := strconv.Atoi(offsetParts[0])
if err != nil {
return time.Time{}, fmt.Errorf("error parsing offset hours: %s: %w", offset, err)
}
if hours > 24 {
return time.Time{}, fmt.Errorf("invalid offset hours: %d: %s", hours, offset)
}
minutes, err := strconv.Atoi(offsetParts[1])
if err != nil {
return time.Time{}, fmt.Errorf("error parsing offset minutes: %s: %w", offset, err)
}
if minutes > 60 {
return time.Time{}, fmt.Errorf("invalid offset minutes: %d: %s", minutes, offset)
}
seconds, err := strconv.Atoi(offsetParts[2])
if err != nil {
return time.Time{}, fmt.Errorf("error parsing offset seconds: %s: %w", offset, err)
}
if seconds > 60 {
return time.Time{}, fmt.Errorf("invalid offset seconds: %d: %s", seconds, offset)
}
totalOffset := time.Duration(sign) * (time.Duration(hours)*time.Hour + time.Duration(minutes)*time.Minute + time.Duration(seconds)*time.Second)
return t.Add(-totalOffset), nil
}
// UnmarshalJSON parses the "b" into ISO8601 time.
func (t *ISO8601) UnmarshalJSON(b []byte) error {
if len(b) == 0 {
@@ -234,9 +301,8 @@ func (t ISO8601) ListExamples() any {
}
// ToTime returns the unwrapped *t to time.Time.
func (t *ISO8601) ToTime() time.Time {
tt := time.Time(*t)
return tt
func (t ISO8601) ToTime() time.Time {
return time.Time(t)
}
// IsZero reports whether "t" is zero time.
@@ -275,8 +341,23 @@ func (t ISO8601) String() string {
return tt.Format(ISO8601Layout)
}
// ToSimpleDate converts the current ISO8601 "t" to SimpleDate in specific location.
func (t ISO8601) ToSimpleDate(in *time.Location) SimpleDate {
// To24Hour returns the 24-hour representation of the time.
func (t ISO8601) To24Hour() string {
tt := t.ToTime()
if tt.IsZero() {
return ""
}
return tt.Format("15:04")
}
// ToSimpleDate converts the current ISO8601 "t" to SimpleDate.
func (t ISO8601) ToSimpleDate() SimpleDate {
return SimpleDateFromTime(t.ToTime())
}
// ToSimpleDateIn converts the current ISO8601 "t" to SimpleDate in specific location.
func (t ISO8601) ToSimpleDateIn(in *time.Location) SimpleDate {
if in == nil {
in = time.UTC
}
@@ -284,6 +365,11 @@ func (t ISO8601) ToSimpleDate(in *time.Location) SimpleDate {
return SimpleDateFromTime(t.ToTime().In(in))
}
// ToDayTime converts the current ISO8601 "t" to DayTime.
func (t ISO8601) ToDayTime() DayTime {
return DayTime(t.ToTime())
}
// Value returns the database value of time.Time.
func (t ISO8601) Value() (driver.Value, error) {
return time.Time(t), nil

View File

@@ -198,3 +198,86 @@ func TestISO8601WithZoneUTCOffsetWithoutMilliseconds(t *testing.T) {
t.Fatalf("expected 'end' to be: %v but got: %v", expected, got)
}
}
func TestParseISO8601_StandardLayouts(t *testing.T) {
tests := []struct {
input string
expected time.Time
hasError bool
}{
{
input: "2024-05-21T18:06:07Z",
expected: time.Date(2024, 5, 21, 18, 6, 7, 0, time.UTC),
hasError: false,
},
{
input: "2024-05-21T18:06:07-04:00",
expected: time.Date(2024, 5, 21, 22, 6, 7, 0, time.UTC),
hasError: false,
},
{
input: "2024-05-21T18:06:07",
expected: time.Date(2024, 5, 21, 18, 6, 7, 0, time.UTC), // no time local.
hasError: false,
},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
parsedTime, err := ParseISO8601(tt.input)
if (err != nil) != tt.hasError {
t.Errorf("ParseISO8601() error = %v, wantErr %v", err, tt.hasError)
return
}
if !tt.hasError && !parsedTime.ToTime().Equal(tt.expected) {
t.Errorf("ParseISO8601() = %v, want %v", parsedTime, tt.expected)
}
})
}
}
func TestParseISO8601_UnconventionalOffset(t *testing.T) {
tests := []struct {
input string
expected time.Time
hasError bool
}{
{
input: "2024-05-21T18:06:07.000000-04:01:19",
expected: time.Date(2024, 5, 21, 22, 7, 26, 0, time.UTC),
hasError: false,
},
{
input: "2024-12-31T23:59:59.000000-00:00:59",
expected: time.Date(2025, 1, 1, 0, 0, 58, 0, time.UTC),
hasError: false,
},
{
input: "2024-05-21T18:06:07.000000+03:30:15",
expected: time.Date(2024, 5, 21, 14, 35, 52, 0, time.UTC),
hasError: false,
},
{
input: "2024-05-21T18:06:07.000000-24:00:00",
expected: time.Date(2024, 5, 22, 18, 6, 7, 0, time.UTC),
hasError: false,
},
{
input: "2024-05-21T18:06:07.000000-04:61:19", // Invalid minute part in offset
expected: time.Time{},
hasError: true,
},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
parsedTime, err := ParseISO8601(tt.input)
if (err != nil) != tt.hasError {
t.Errorf("ParseISO8601() error = %v, wantErr %v", err, tt.hasError)
return
}
if !tt.hasError && !parsedTime.ToTime().Equal(tt.expected) {
t.Errorf("ParseISO8601() = %v, want %v", parsedTime, tt.expected)
}
})
}
}