-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Added time helper to parse various strings into time.Time. Closes
- Loading branch information
Showing
3 changed files
with
321 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
package helpers | ||
|
||
import ( | ||
"fmt" | ||
"strconv" | ||
"strings" | ||
"time" | ||
) | ||
|
||
// ParseTime parses a string into a *time.Time object attempting to parse it as a variety of formats | ||
func ParseTime(input string) (*time.Time, error) { | ||
var timeFormats = []string{ | ||
time.RFC3339Nano, // "2006-01-02T15:04:05.999999999Z07:00" | ||
time.RFC3339, // "2006-01-02T15:04:05Z07:00" | ||
time.RFC1123Z, // "Mon, 02 Jan 2006 15:04:05 -0700" | ||
time.RFC1123, // "Mon, 02 Jan 2006 15:04:05 MST" | ||
time.RFC850, // "Monday, 02-Jan-06 15:04:05 MST" | ||
time.RFC822Z, // "02 Jan 06 15:04 -0700" | ||
time.RFC822, // "02 Jan 06 15:04 MST" | ||
time.UnixDate, // "Mon Jan _2 15:04:05 MST 2006" | ||
time.RubyDate, // "Mon Jan 02 15:04:05 -0700 2006" | ||
time.ANSIC, // "Mon Jan _2 15:04:05 2006" | ||
"02/Jan/2006:15:04:05 -0700", // Nginx/Apache standard log format | ||
"02/Jan/2006:15:04:05 MST", // Nginx/Apache alternative (MST timezone) | ||
time.DateTime, // "2006-01-02 15:04:05" (ISO-like format without timezone) | ||
"2006-01-02 15:04:05 -0700", // ISO-like format with offset | ||
"2006-01-02 15:04:05 MST", // ISO-like format with named timezone | ||
"2006/01/02 15:04:05", // ISO-like format with different separator | ||
"2006/01/02 15:04:05 -0700", // ISO-like format with different separator and offset | ||
"2006/01/02 15:04:05 MST", // ISO-like format with different separator and named timezone | ||
time.DateOnly, // "2006-01-02" (Date-only format) | ||
} | ||
|
||
var t time.Time | ||
var err error | ||
|
||
// short-circuit for unix seconds/milliseconds/nanoseconds | ||
if IsOnlyNumeric(input) { | ||
var unixTime int64 | ||
unixTime, err = strconv.ParseInt(input, 10, 64) | ||
if err == nil { | ||
switch len(input) { | ||
case 9, 10: | ||
t = time.Unix(unixTime, 0) // Unix seconds | ||
return &t, nil | ||
case 12, 13: | ||
t = time.UnixMilli(unixTime) // Unix milliseconds | ||
return &t, nil | ||
case 18, 19: | ||
t = time.Unix(0, unixTime) // Unix nanoseconds | ||
return &t, nil | ||
} | ||
} | ||
} | ||
|
||
// parse time with each format until we match | ||
for _, timeFormat := range timeFormats { | ||
t, err = time.Parse(timeFormat, input) | ||
if err == nil { | ||
|
||
if strings.Contains(timeFormat, "MST") { | ||
t = adjustToNamedTimezone(t, input) | ||
} | ||
return &t, nil | ||
} | ||
} | ||
|
||
return nil, fmt.Errorf("unrecognized timestamp format: %s", input) | ||
} | ||
|
||
func adjustToNamedTimezone(t time.Time, originalTs string) time.Time { | ||
// Extract timezone string from input | ||
parts := strings.Fields(originalTs) | ||
for _, part := range parts { | ||
|
||
// Try Go's location database as a fallback | ||
loc, err := time.LoadLocation(part) | ||
if err == nil { | ||
return time.Date( | ||
t.Year(), t.Month(), t.Day(), | ||
t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), | ||
loc, | ||
) | ||
} | ||
} | ||
return t | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,229 @@ | ||
package helpers | ||
|
||
import ( | ||
"testing" | ||
"time" | ||
) | ||
|
||
type parseTimeTest struct { | ||
name string | ||
input string | ||
expected time.Time | ||
wantErr bool | ||
} | ||
|
||
var testCasesParseTime = []parseTimeTest{ | ||
{ | ||
name: "RFC3339Nano", | ||
input: "2024-02-10T15:04:05.123456789Z", | ||
expected: time.Date(2024, 2, 10, 15, 4, 5, 123456789, time.UTC), | ||
wantErr: false, | ||
}, | ||
{ | ||
name: "RFC3339", | ||
input: "2024-02-10T15:04:05Z", | ||
expected: time.Date(2024, 2, 10, 15, 4, 5, 0, time.UTC), | ||
wantErr: false, | ||
}, | ||
{ | ||
name: "RFC1123Z", | ||
input: "Sat, 10 Feb 2024 15:04:05 GMT", | ||
expected: time.Date(2024, 2, 10, 15, 4, 5, 0, time.UTC), | ||
wantErr: false, | ||
}, | ||
{ | ||
name: "RFC1123NamedTimezone", | ||
input: "Sat, 10 Feb 2024 15:04:05 UTC", | ||
expected: time.Date(2024, 2, 10, 15, 4, 5, 0, time.UTC), | ||
wantErr: false, | ||
}, | ||
{ | ||
name: "RFC850", | ||
input: "Saturday, 10-Feb-24 15:04:05 UTC", | ||
expected: time.Date(2024, 2, 10, 15, 4, 5, 0, time.UTC), | ||
wantErr: false, | ||
}, | ||
{ | ||
name: "RFC822", | ||
input: "10 Feb 24 15:04 UTC", | ||
expected: time.Date(2024, 2, 10, 15, 4, 0, 0, time.UTC), | ||
wantErr: false, | ||
}, | ||
{ | ||
name: "UnixDate", | ||
input: "Tue Feb 10 15:04:05 UTC 2024", | ||
expected: time.Date(2024, 2, 10, 15, 4, 5, 0, time.UTC), | ||
wantErr: false, | ||
}, | ||
{ | ||
name: "RubyDate", | ||
input: "Sat Feb 10 15:04:05 UTC 2024", | ||
expected: time.Date(2024, 2, 10, 15, 4, 5, 0, time.UTC), | ||
wantErr: false, | ||
}, | ||
{ | ||
name: "NginxApacheStandard", | ||
input: "10/Feb/2024:15:04:05 -0500", | ||
expected: time.Date(2024, 2, 10, 15, 4, 5, 0, time.FixedZone("-0500", -5*60*60)), | ||
wantErr: false, | ||
}, | ||
{ | ||
name: "UnixMilliseconds", | ||
input: "1707583205123", | ||
expected: time.UnixMilli(1707583205123), | ||
wantErr: false, | ||
}, | ||
{ | ||
name: "NginxApacheNamedTimezone", | ||
input: "10/Feb/2024:15:04:05 EST", | ||
expected: time.Date(2024, 2, 10, 15, 4, 5, 0, time.FixedZone("EST", -5*60*60)), | ||
wantErr: false, | ||
}, | ||
{ | ||
name: "ISOFormatWithOffset", | ||
input: "2024-02-10 15:04:05 -0700", | ||
expected: time.Date(2024, 2, 10, 15, 4, 5, 0, time.FixedZone("-0700", -7*60*60)), | ||
wantErr: false, | ||
}, | ||
{ | ||
name: "ISOFormatWithNamedTimezone", | ||
input: "2024-02-10 15:04:05 UTC", | ||
expected: time.Date(2024, 2, 10, 15, 4, 5, 0, time.UTC), | ||
wantErr: false, | ||
}, | ||
{ | ||
name: "DateTimeNoTimezone", | ||
input: "2024-02-10 15:04:05", | ||
expected: time.Date(2024, 2, 10, 15, 4, 5, 0, time.UTC), | ||
wantErr: false, | ||
}, | ||
{ | ||
name: "DateOnly", | ||
input: "2024-02-10", | ||
expected: time.Date(2024, 2, 10, 0, 0, 0, 0, time.UTC), | ||
wantErr: false, | ||
}, | ||
{ | ||
name: "IncorrectOrder", | ||
input: "10-02-2024 15:04:05", | ||
expected: time.Time{}, | ||
wantErr: true, | ||
}, | ||
{ | ||
name: "AlternateSeparator", | ||
input: "2024/02/10 15:04:05", | ||
expected: time.Date(2024, 02, 10, 15, 4, 5, 0, time.UTC), | ||
wantErr: false, | ||
}, | ||
{ | ||
name: "ShortenedYear", | ||
input: "24-02-10 15:04:05", | ||
expected: time.Time{}, | ||
wantErr: true, | ||
}, | ||
{ | ||
name: "OddOrdering", | ||
input: "Saturday 10 Feb 2024 15:04:05", | ||
expected: time.Time{}, | ||
wantErr: true, | ||
}, | ||
{ | ||
name: "TimeFirst", | ||
input: "23:59:59 2024-02-10", | ||
expected: time.Time{}, | ||
wantErr: true, | ||
}, | ||
{ | ||
name: "RFC1123ZWithOffset", | ||
input: "Sun, 11 Feb 2024 03:15:30 +0100", | ||
expected: time.Date(2024, 2, 11, 3, 15, 30, 0, time.FixedZone("+0100", 1*60*60)), | ||
wantErr: false, | ||
}, | ||
{ | ||
name: "RandomString", | ||
input: "invalid timestamp", | ||
expected: time.Time{}, | ||
wantErr: true, | ||
}, | ||
{ | ||
name: "WrittenOutDate", | ||
input: "10th of February, 2024", | ||
expected: time.Time{}, | ||
wantErr: true, | ||
}, | ||
{ | ||
name: "NonParsableWord", | ||
input: "Yesterday", | ||
expected: time.Time{}, | ||
wantErr: true, | ||
}, | ||
{ | ||
name: "UnixNanoseconds", | ||
input: "1707583205123456789", | ||
expected: time.Unix(0, 1707583205123456789), | ||
wantErr: false, | ||
}, | ||
{ | ||
name: "ImpossibleTime", | ||
input: "2024-02-10T99:99:99Z", | ||
expected: time.Time{}, | ||
wantErr: true, | ||
}, | ||
{ | ||
name: "UnixSecondsPre2001", | ||
input: "534611045", | ||
expected: time.Unix(534611045, 0), | ||
wantErr: false, | ||
}, | ||
{ | ||
name: "UnixMillisecondsPre2001", | ||
input: "534611045123", | ||
expected: time.UnixMilli(534611045123), | ||
wantErr: false, | ||
}, | ||
{ | ||
name: "UnixNanosecondsPre2001", | ||
input: "534611045123456789", | ||
expected: time.Unix(0, 534611045123456789), | ||
wantErr: false, | ||
}, | ||
{ | ||
name: "UnixSeconds", | ||
input: "1234567890", | ||
expected: time.Unix(1234567890, 0), | ||
wantErr: false, | ||
}, | ||
{ | ||
name: "RFC3339WithOffset", | ||
input: "2024-02-10T15:04:05+02:00", | ||
expected: time.Date(2024, 2, 10, 15, 4, 5, 0, time.FixedZone("+0200", 2*60*60)), | ||
wantErr: false, | ||
}, | ||
{ | ||
name: "RFC1123ZWithOffset", | ||
input: "Sat, 10 Feb 2024 15:04:05 +0100", | ||
expected: time.Date(2024, 2, 10, 15, 4, 5, 0, time.FixedZone("+0100", 1*60*60)), | ||
wantErr: false, | ||
}, | ||
{ | ||
name: "NginxApacheStandardRepeat", | ||
input: "10/Feb/2024:15:04:05 -0500", | ||
expected: time.Date(2024, 2, 10, 15, 4, 5, 0, time.FixedZone("-0500", -5*60*60)), | ||
wantErr: false, | ||
}, | ||
} | ||
|
||
func TestParseTime(t *testing.T) { | ||
for _, tt := range testCasesParseTime { | ||
t.Run(tt.name, func(t *testing.T) { | ||
result, err := ParseTime(tt.input) | ||
if (err != nil) != tt.wantErr { | ||
t.Errorf("ParseTime() error = %v, wantErr %v", err, tt.wantErr) | ||
return | ||
} | ||
if !tt.wantErr && !result.Equal(tt.expected) { | ||
t.Errorf("expected %v, got %v", tt.expected, result) | ||
} | ||
}) | ||
} | ||
} |