exposes monthfirst

This commit is contained in:
Kamal Galrani 2019-07-09 19:25:50 +05:30
parent 0fb0a474d1
commit 23d378dd18
3 changed files with 115 additions and 56 deletions

View File

@ -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)
}
}
}

View File

@ -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

View File

@ -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)
}