diff --git a/bench_test.go b/bench_test.go index 0c6739a..dfec292 100644 --- a/bench_test.go +++ b/bench_test.go @@ -37,7 +37,7 @@ func BenchmarkParseAny(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { for _, dateStr := range testDates { - ParseAny(dateStr) + ParseAny(dateStr, true) } } } diff --git a/parseany.go b/parseany.go index 5e66aa6..3f0318a 100644 --- a/parseany.go +++ b/parseany.go @@ -120,12 +120,24 @@ func unknownErr(datestr string) error { // ParseAny parse an unknown date format, detect the layout. // Normal parse. Equivalent Timezone rules as time.Parse(). // NOTE: please see readme on mmdd vs ddmm ambiguous dates. -func ParseAny(datestr string) (time.Time, error) { - p, err := parseTime(datestr, nil) +func ParseAny(datestr string, preferMonthFirst bool) (time.Time, error) { + p, err := parseTime(datestr, nil, preferMonthFirst) if err != nil { return time.Time{}, err } - return p.parse() + + t, err := p.parse() + + if err != nil && strings.Contains(err.Error(), "month out of range") { + p, err = parseTime(datestr, nil, !preferMonthFirst) + if err != nil { + return time.Time{}, err + } + + t, err = p.parse() + } + + return t, err } // ParseIn with Location, equivalent to time.ParseInLocation() timezone/offset @@ -133,12 +145,24 @@ func ParseAny(datestr string) (time.Time, error) { // datestring, it uses the given location rules for any zone interpretation. // That is, MST means one thing when using America/Denver and something else // in other locations. -func ParseIn(datestr string, loc *time.Location) (time.Time, error) { - p, err := parseTime(datestr, loc) +func ParseIn(datestr string, loc *time.Location, preferMonthFirst bool) (time.Time, error) { + p, err := parseTime(datestr, loc, preferMonthFirst) if err != nil { return time.Time{}, err } - return p.parse() + + t, err := p.parse() + + if err != nil && strings.Contains(err.Error(), "month out of range") { + p, err = parseTime(datestr, loc, !preferMonthFirst) + if err != nil { + return time.Time{}, err + } + + t, err = p.parse() + } + + return t, err } // ParseLocal Given an unknown date format, detect the layout, @@ -156,20 +180,31 @@ func ParseIn(datestr string, loc *time.Location) (time.Time, error) { // // t, err := dateparse.ParseIn("3/1/2014", denverLoc) // -func ParseLocal(datestr string) (time.Time, error) { - p, err := parseTime(datestr, time.Local) +func ParseLocal(datestr string, preferMonthFirst bool) (time.Time, error) { + p, err := parseTime(datestr, time.Local, preferMonthFirst) if err != nil { return time.Time{}, err } - return p.parse() + + t, err := p.parse() + + if err != nil && strings.Contains(err.Error(), "month out of range") { + p, err = parseTime(datestr, time.Local, !preferMonthFirst) + if err != nil { + return time.Time{}, err + } + + t, err = p.parse() + } + + return t, err } // MustParse parse a date, and panic if it can't be parsed. Used for testing. // Not recommended for most use-cases. -func MustParse(datestr string) time.Time { - p, err := parseTime(datestr, nil) +func MustParse(datestr string, preferMonthFirst bool) time.Time { + p, err := parseTime(datestr, nil, preferMonthFirst) if err != nil { - panic(err.Error()) } t, err := p.parse() if err != nil { @@ -184,22 +219,34 @@ func MustParse(datestr string) time.Time { // layout, err := dateparse.ParseFormat("2013-02-01 00:00:00") // // layout = "2006-01-02 15:04:05" // -func ParseFormat(datestr string) (string, error) { - p, err := parseTime(datestr, nil) +func ParseFormat(datestr string, preferMonthFirst bool) (string, error) { + p, err := parseTime(datestr, nil, preferMonthFirst) if err != nil { return "", err } _, err = p.parse() if err != nil { - return "", err + if strings.Contains(err.Error(), "month out of range") { + p, err := parseTime(datestr, nil, !preferMonthFirst) + if err != nil { + return "", err + } + _, err = p.parse() + if err != nil { + return "", err + } + return string(p.format), nil + } else { + return "", err + } } return string(p.format), nil } // ParseStrict parse an unknown date format. IF the date is ambigous // mm/dd vs dd/mm then return an error. These return errors: 3.3.2014 , 8/8/71 etc -func ParseStrict(datestr string) (time.Time, error) { - p, err := parseTime(datestr, nil) +func ParseStrict(datestr string, preferMonthFirst bool) (time.Time, error) { + p, err := parseTime(datestr, nil, preferMonthFirst) if err != nil { return time.Time{}, err } @@ -209,9 +256,9 @@ func ParseStrict(datestr string) (time.Time, error) { return p.parse() } -func parseTime(datestr string, loc *time.Location) (*parser, error) { +func parseTime(datestr string, loc *time.Location, preferMonthFirst bool) (*parser, error) { - p := newParser(datestr, loc) + p := newParser(datestr, loc, preferMonthFirst) i := 0 // General strategy is to read rune by rune through the date looking for @@ -269,6 +316,12 @@ iterRunes: p.setMonth() p.dayi = i + 1 } + } else { + if p.daylen == 0 { + p.daylen = i + p.setDay() + p.moi = i + 1 + } } } @@ -446,6 +499,12 @@ iterRunes: p.setDay() p.yeari = i + 1 } + } else { + if p.molen == 0 { + p.molen = i - p.moi + p.setMonth() + p.yeari = i + 1 + } } } @@ -618,7 +677,7 @@ iterRunes: } else if i == 4 { // gross datestr = datestr[0:i-1] + datestr[i:] - return parseTime(datestr, loc) + return parseTime(datestr, loc, preferMonthFirst) } else { return nil, unknownErr(datestr) } @@ -783,25 +842,25 @@ iterRunes: case 't', 'T': if p.nextIs(i, 'h') || p.nextIs(i, 'H') { if len(datestr) > i+2 { - return parseTime(fmt.Sprintf("%s%s", p.datestr[0:i], p.datestr[i+2:]), loc) + return parseTime(fmt.Sprintf("%s%s", p.datestr[0:i], p.datestr[i+2:]), loc, preferMonthFirst) } } case 'n', 'N': if p.nextIs(i, 'd') || p.nextIs(i, 'D') { if len(datestr) > i+2 { - return parseTime(fmt.Sprintf("%s%s", p.datestr[0:i], p.datestr[i+2:]), loc) + return parseTime(fmt.Sprintf("%s%s", p.datestr[0:i], p.datestr[i+2:]), loc, preferMonthFirst) } } case 's', 'S': if p.nextIs(i, 't') || p.nextIs(i, 'T') { if len(datestr) > i+2 { - return parseTime(fmt.Sprintf("%s%s", p.datestr[0:i], p.datestr[i+2:]), loc) + return parseTime(fmt.Sprintf("%s%s", p.datestr[0:i], p.datestr[i+2:]), loc, preferMonthFirst) } } case 'r', 'R': if p.nextIs(i, 'd') || p.nextIs(i, 'D') { if len(datestr) > i+2 { - return parseTime(fmt.Sprintf("%s%s", p.datestr[0:i], p.datestr[i+2:]), loc) + return parseTime(fmt.Sprintf("%s%s", p.datestr[0:i], p.datestr[i+2:]), loc, preferMonthFirst) } } } @@ -975,7 +1034,7 @@ iterRunes: // 2014-05-11 08:20:13,787 ds := []byte(p.datestr) ds[i] = '.' - return parseTime(string(ds), loc) + return parseTime(string(ds), loc, preferMonthFirst) case '-', '+': // 03:21:51+00:00 p.stateTime = timeOffset @@ -1699,13 +1758,13 @@ type parser struct { t *time.Time } -func newParser(dateStr string, loc *time.Location) *parser { +func newParser(dateStr string, loc *time.Location, preferMonthFirst bool) *parser { p := parser{ stateDate: dateStart, stateTime: timeIgnore, datestr: dateStr, loc: loc, - preferMonthFirst: true, + preferMonthFirst: preferMonthFirst, } p.format = []byte(dateStr) return &p diff --git a/parseany_test.go b/parseany_test.go index 2d92f43..c660795 100644 --- a/parseany_test.go +++ b/parseany_test.go @@ -11,7 +11,7 @@ import ( func TestOne(t *testing.T) { time.Local = time.UTC var ts time.Time - ts = MustParse("2018.09.30") + ts = MustParse("2018.09.30", true) assert.Equal(t, "2018-09-30 00:00:00 +0000 UTC", fmt.Sprintf("%v", ts.In(time.UTC))) } @@ -386,13 +386,13 @@ func TestParse(t *testing.T) { time.Local = time.UTC zeroTime := time.Time{}.Unix() - ts, err := ParseAny("INVALID") + ts, err := ParseAny("INVALID", true) assert.Equal(t, zeroTime, ts.Unix()) assert.NotEqual(t, nil, err) assert.Equal(t, true, testDidPanic("NOT GONNA HAPPEN")) // https://github.com/golang/go/issues/5294 - _, err = ParseAny(time.RFC3339) + _, err = ParseAny(time.RFC3339, true) assert.NotEqual(t, nil, err) for _, th := range testInputs { @@ -401,7 +401,7 @@ func TestParse(t *testing.T) { if err != nil { t.Fatalf("Expected to load location %q but got %v", th.loc, err) } - ts, err = ParseIn(th.in, loc) + ts, err = ParseIn(th.in, loc, true) if err != nil { t.Fatalf("expected to parse %q but got %v", th.in, err) } @@ -411,7 +411,7 @@ func TestParse(t *testing.T) { panic("whoops") } } else { - ts = MustParse(th.in) + ts = MustParse(th.in, true) got := fmt.Sprintf("%v", ts.In(time.UTC)) assert.Equal(t, th.out, got, "Expected %q but got %q from %q", th.out, got, th.in) if th.out != got { @@ -424,13 +424,13 @@ func TestParse(t *testing.T) { assert.Equal(t, true, testDidPanic(`{"ts":"now"}`)) - _, err = ParseAny("138421636711122233311111") // too many digits + _, err = ParseAny("138421636711122233311111", true) // too many digits assert.NotEqual(t, nil, err) - _, err = ParseAny("-1314") + _, err = ParseAny("-1314", true) assert.NotEqual(t, nil, err) - _, err = ParseAny("2014-13-13 08:20:13,787") // month 13 doesn't exist so error + _, err = ParseAny("2014-13-13 08:20:13,787", true) // month 13 doesn't exist so error assert.NotEqual(t, nil, err) } @@ -440,7 +440,7 @@ func testDidPanic(datestr string) (paniced bool) { paniced = true } }() - MustParse(datestr) + MustParse(datestr, true) return false } @@ -449,7 +449,7 @@ func TestPStruct(t *testing.T) { denverLoc, err := time.LoadLocation("America/Denver") assert.Equal(t, nil, err) - p := newParser("08.21.71", denverLoc) + p := newParser("08.21.71", denverLoc, true) p.setMonth() assert.Equal(t, 0, p.moi) @@ -479,7 +479,7 @@ var testParseErrors = []dateTest{ func TestParseErrors(t *testing.T) { for _, th := range testParseErrors { - v, err := ParseAny(th.in) + v, err := ParseAny(th.in, true) assert.NotEqual(t, nil, err, "%v for %v", v, th.in) } } @@ -514,7 +514,7 @@ var testParseFormat = []dateTest{ func TestParseLayout(t *testing.T) { for _, th := range testParseFormat { - l, err := ParseFormat(th.in) + l, err := ParseFormat(th.in, true) if th.err { assert.NotEqual(t, nil, err) } else { @@ -544,14 +544,14 @@ var testParseStrict = []dateTest{ func TestParseStrict(t *testing.T) { for _, th := range testParseStrict { - _, err := ParseStrict(th.in) + _, err := ParseStrict(th.in, true) assert.NotEqual(t, nil, err) } - _, err := ParseStrict(`{"hello"}`) + _, err := ParseStrict(`{"hello"}`, true) assert.NotEqual(t, nil, err) - _, err = ParseStrict("2009-08-12T22:15Z") + _, err = ParseStrict("2009-08-12T22:15Z", true) assert.Equal(t, nil, err) } @@ -570,7 +570,7 @@ func TestInLocation(t *testing.T) { time.Local = time.UTC // Just normal parse to test out zone/offset - ts := MustParse("2013-02-01 00:00:00") + ts := MustParse("2013-02-01 00:00:00", true) zone, offset := ts.Zone() assert.Equal(t, 0, offset, "Should have found offset = 0 %v", offset) assert.Equal(t, "UTC", zone, "Should have found zone = UTC %v", zone) @@ -579,26 +579,26 @@ func TestInLocation(t *testing.T) { // Now lets set to denver (MST/MDT) and re-parse the same time string // and since no timezone info in string, we expect same result time.Local = denverLoc - ts = MustParse("2013-02-01 00:00:00") + ts = MustParse("2013-02-01 00:00:00", true) zone, offset = ts.Zone() assert.Equal(t, 0, offset, "Should have found offset = 0 %v", offset) assert.Equal(t, "UTC", zone, "Should have found zone = UTC %v", zone) assert.Equal(t, "2013-02-01 00:00:00 +0000 UTC", fmt.Sprintf("%v", ts.In(time.UTC))) - ts = MustParse("Tue, 5 Jul 2017 16:28:13 -0700 (MST)") + ts = MustParse("Tue, 5 Jul 2017 16:28:13 -0700 (MST)", true) assert.Equal(t, "2017-07-05 23:28:13 +0000 UTC", fmt.Sprintf("%v", ts.In(time.UTC))) // Now we are going to use ParseIn() and see that it gives different answer // with different zone, offset time.Local = nil - ts, err = ParseIn("2013-02-01 00:00:00", denverLoc) + ts, err = ParseIn("2013-02-01 00:00:00", denverLoc, true) assert.Equal(t, nil, err) zone, offset = ts.Zone() assert.Equal(t, -25200, offset, "Should have found offset = -25200 %v %v", offset, denverLoc) assert.Equal(t, "MST", zone, "Should have found zone = MST %v", zone) assert.Equal(t, "2013-02-01 07:00:00 +0000 UTC", fmt.Sprintf("%v", ts.In(time.UTC))) - ts, err = ParseIn("18 January 2018", denverLoc) + ts, err = ParseIn("18 January 2018", denverLoc, true) assert.Equal(t, nil, err) zone, offset = ts.Zone() assert.Equal(t, -25200, offset, "Should have found offset = 0 %v", offset) @@ -608,7 +608,7 @@ func TestInLocation(t *testing.T) { // Now we are going to use ParseLocal() and see that it gives same // answer as ParseIn when we have time.Local set to a location time.Local = denverLoc - ts, err = ParseLocal("2013-02-01 00:00:00") + ts, err = ParseLocal("2013-02-01 00:00:00", true) assert.Equal(t, nil, err) zone, offset = ts.Zone() assert.Equal(t, -25200, offset, "Should have found offset = -25200 %v %v", offset, denverLoc) @@ -617,7 +617,7 @@ func TestInLocation(t *testing.T) { // Lets advance past daylight savings time start // use parseIn and see offset/zone has changed to Daylight Savings Equivalents - ts, err = ParseIn("2013-04-01 00:00:00", denverLoc) + ts, err = ParseIn("2013-04-01 00:00:00", denverLoc, true) assert.Equal(t, nil, err) zone, offset = ts.Zone() assert.Equal(t, -21600, offset, "Should have found offset = -21600 %v %v", offset, denverLoc) @@ -628,7 +628,7 @@ func TestInLocation(t *testing.T) { time.Local = time.UTC // UnixDate = "Mon Jan _2 15:04:05 MST 2006" - ts = MustParse("Mon Jan 2 15:04:05 MST 2006") + ts = MustParse("Mon Jan 2 15:04:05 MST 2006", true) _, offset = ts.Zone() assert.Equal(t, 0, offset, "Should have found offset = 0 %v", offset) @@ -636,7 +636,7 @@ func TestInLocation(t *testing.T) { // Now lets set to denver(mst/mdt) time.Local = denverLoc - ts = MustParse("Mon Jan 2 15:04:05 MST 2006") + ts = MustParse("Mon Jan 2 15:04:05 MST 2006", true) // this time is different from one above parsed with time.Local set to UTC _, offset = ts.Zone() @@ -647,25 +647,25 @@ func TestInLocation(t *testing.T) { time.Local = time.UTC // RFC850 = "Monday, 02-Jan-06 15:04:05 MST" - ts = MustParse("Monday, 02-Jan-06 15:04:05 MST") + ts = MustParse("Monday, 02-Jan-06 15:04:05 MST", true) _, offset = ts.Zone() assert.Equal(t, 0, offset, "Should have found offset = 0 %v", offset) assert.Equal(t, "2006-01-02 15:04:05 +0000 UTC", fmt.Sprintf("%v", ts.In(time.UTC))) // Now lets set to denver time.Local = denverLoc - ts = MustParse("Monday, 02-Jan-06 15:04:05 MST") + ts = MustParse("Monday, 02-Jan-06 15:04:05 MST", true) _, offset = ts.Zone() assert.NotEqual(t, 0, offset, "Should have found offset %v", offset) assert.Equal(t, "2006-01-02 22:04:05 +0000 UTC", fmt.Sprintf("%v", ts.In(time.UTC))) // Now some errors zeroTime := time.Time{}.Unix() - ts, err = ParseIn("INVALID", denverLoc) + ts, err = ParseIn("INVALID", denverLoc, true) assert.Equal(t, zeroTime, ts.Unix()) assert.NotEqual(t, nil, err) - ts, err = ParseLocal("INVALID") + ts, err = ParseLocal("INVALID", true) assert.Equal(t, zeroTime, ts.Unix()) assert.NotEqual(t, nil, err) }