Skip to content

Commit

Permalink
feat: Added time helper to parse various strings into time.Time. Closes
Browse files Browse the repository at this point in the history
  • Loading branch information
graza-io authored Feb 11, 2025
1 parent d8d7159 commit 8f5c0dc
Show file tree
Hide file tree
Showing 3 changed files with 321 additions and 0 deletions.
5 changes: 5 additions & 0 deletions helpers/string.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,8 @@ func TrimBlankLines(str string) string {
strippedLines := RemoveFromStringSlice(lines, "")
return strings.Join(strippedLines, "\n")
}

// IsOnlyNumeric returns true if the string only contains numeric characters
func IsOnlyNumeric(s string) bool {
return strings.Trim(s, "0123456789") == ""
}
87 changes: 87 additions & 0 deletions helpers/time.go
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
}
229 changes: 229 additions & 0 deletions helpers/time_test.go
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)
}
})
}
}

0 comments on commit 8f5c0dc

Please sign in to comment.