diff --git a/atproto/syntax/datetime.go b/atproto/syntax/datetime.go index 7910d6e31..22c23e2d9 100644 --- a/atproto/syntax/datetime.go +++ b/atproto/syntax/datetime.go @@ -50,6 +50,32 @@ func ParseDatetimeTime(raw string) (time.Time, error) { return d.Time(), nil } +// Similar to ParseDatetime, but more flexible about some parsing. +// +// Note that this may mutate the internal string, so a round-trip will fail. This is intended for working with legacy/broken records, not to be used in an ongoing way. +func ParseDatetimeLenient(raw string) (Datetime, error) { + // fast path: it is a valid overall datetime + valid, err := ParseDatetime(raw) + if nil == err { + return valid, nil + } + + if strings.HasSuffix(raw, "-00:00") { + return ParseDatetime(strings.Replace(raw, "-00:00", "+00:00", 1)) + } + + // try adding timezone if it is missing + var hasTimezoneRegex = regexp.MustCompile(`^.*(([+-]\d\d:?\d\d)|[a-zA-Z])$`) + if !hasTimezoneRegex.MatchString(raw) { + withTZ, err := ParseDatetime(raw + "Z") + if nil == err { + return withTZ, nil + } + } + + return "", fmt.Errorf("Datetime could not be parsed, even leniently: %v", err) +} + // Parses the Datetime string in to a golang [time.Time]. // // This method assumes that [ParseDatetime] was used to create the Datetime, which already verified parsing, and thus that [time.Parse] will always succeed. In the event of an error, zero/nil will be returned. diff --git a/atproto/syntax/datetime_test.go b/atproto/syntax/datetime_test.go index c12e20e84..e9dda3677 100644 --- a/atproto/syntax/datetime_test.go +++ b/atproto/syntax/datetime_test.go @@ -74,6 +74,35 @@ func TestInteropDatetimeTimeInvalid(t *testing.T) { assert.NoError(scanner.Err()) } +func TestParseDatetimeLenient(t *testing.T) { + assert := assert.New(t) + + valid := []string{ + "1985-04-12T23:20:50.123Z", + "1985-04-12T23:20:50.123", + "2023-08-27T19:07:00.186173", + "1985-04-12T23:20:50.123-00:00", + "1985-04-12T23:20:50.123+00:00", + } + for _, s := range valid { + _, err := ParseDatetimeLenient(s) + assert.NoError(err) + if err != nil { + fmt.Println(s) + } + } + + invalid := []string{ + "1985-04-", + "", + "blah", + } + for _, s := range invalid { + _, err := ParseDatetimeLenient(s) + assert.Error(err) + } +} + func TestDatetimeNow(t *testing.T) { assert := assert.New(t)