Skip to content

Commit 9cb150c

Browse files
authored
Merge pull request #89 from troyspencer/master
Options framework to allow more control over parsing
2 parents b07ab88 + 539e9f1 commit 9cb150c

2 files changed

Lines changed: 154 additions & 58 deletions

File tree

parseany.go

Lines changed: 119 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -144,8 +144,8 @@ func unknownErr(datestr string) error {
144144
// ParseAny parse an unknown date format, detect the layout.
145145
// Normal parse. Equivalent Timezone rules as time.Parse().
146146
// NOTE: please see readme on mmdd vs ddmm ambiguous dates.
147-
func ParseAny(datestr string) (time.Time, error) {
148-
p, err := parseTime(datestr, nil)
147+
func ParseAny(datestr string, opts ...ParserOption) (time.Time, error) {
148+
p, err := parseTime(datestr, nil, opts...)
149149
if err != nil {
150150
return time.Time{}, err
151151
}
@@ -157,8 +157,8 @@ func ParseAny(datestr string) (time.Time, error) {
157157
// datestring, it uses the given location rules for any zone interpretation.
158158
// That is, MST means one thing when using America/Denver and something else
159159
// in other locations.
160-
func ParseIn(datestr string, loc *time.Location) (time.Time, error) {
161-
p, err := parseTime(datestr, loc)
160+
func ParseIn(datestr string, loc *time.Location, opts ...ParserOption) (time.Time, error) {
161+
p, err := parseTime(datestr, loc, opts...)
162162
if err != nil {
163163
return time.Time{}, err
164164
}
@@ -180,8 +180,8 @@ func ParseIn(datestr string, loc *time.Location) (time.Time, error) {
180180
//
181181
// t, err := dateparse.ParseIn("3/1/2014", denverLoc)
182182
//
183-
func ParseLocal(datestr string) (time.Time, error) {
184-
p, err := parseTime(datestr, time.Local)
183+
func ParseLocal(datestr string, opts ...ParserOption) (time.Time, error) {
184+
p, err := parseTime(datestr, time.Local, opts...)
185185
if err != nil {
186186
return time.Time{}, err
187187
}
@@ -190,8 +190,8 @@ func ParseLocal(datestr string) (time.Time, error) {
190190

191191
// MustParse parse a date, and panic if it can't be parsed. Used for testing.
192192
// Not recommended for most use-cases.
193-
func MustParse(datestr string) time.Time {
194-
p, err := parseTime(datestr, nil)
193+
func MustParse(datestr string, opts ...ParserOption) time.Time {
194+
p, err := parseTime(datestr, nil, opts...)
195195
if err != nil {
196196
panic(err.Error())
197197
}
@@ -208,8 +208,8 @@ func MustParse(datestr string) time.Time {
208208
// layout, err := dateparse.ParseFormat("2013-02-01 00:00:00")
209209
// // layout = "2006-01-02 15:04:05"
210210
//
211-
func ParseFormat(datestr string) (string, error) {
212-
p, err := parseTime(datestr, nil)
211+
func ParseFormat(datestr string, opts ...ParserOption) (string, error) {
212+
p, err := parseTime(datestr, nil, opts...)
213213
if err != nil {
214214
return "", err
215215
}
@@ -222,8 +222,8 @@ func ParseFormat(datestr string) (string, error) {
222222

223223
// ParseStrict parse an unknown date format. IF the date is ambigous
224224
// mm/dd vs dd/mm then return an error. These return errors: 3.3.2014 , 8/8/71 etc
225-
func ParseStrict(datestr string) (time.Time, error) {
226-
p, err := parseTime(datestr, nil)
225+
func ParseStrict(datestr string, opts ...ParserOption) (time.Time, error) {
226+
p, err := parseTime(datestr, nil, opts...)
227227
if err != nil {
228228
return time.Time{}, err
229229
}
@@ -233,9 +233,31 @@ func ParseStrict(datestr string) (time.Time, error) {
233233
return p.parse()
234234
}
235235

236-
func parseTime(datestr string, loc *time.Location) (*parser, error) {
236+
func parseTime(datestr string, loc *time.Location, opts ...ParserOption) (p *parser, err error) {
237+
238+
p = newParser(datestr, loc, opts...)
239+
if p.retryAmbiguousDateWithSwap {
240+
// month out of range signifies that a day/month swap is the correct solution to an ambiguous date
241+
// this is because it means that a day is being interpreted as a month and overflowing the valid value for that
242+
// by retrying in this case, we can fix a common situation with no assumptions
243+
defer func() {
244+
if p.ambiguousMD {
245+
// if it errors out with the following error, swap before we
246+
// get out of this function to reduce scope it needs to be applied on
247+
_, err := p.parse()
248+
if err != nil && strings.Contains(err.Error(), "month out of range") {
249+
// create the option to reverse the preference
250+
preferMonthFirst := PreferMonthFirst(!p.preferMonthFirst)
251+
// turn off the retry to avoid endless recursion
252+
retryAmbiguousDateWithSwap := RetryAmbiguousDateWithSwap(false)
253+
modifiedOpts := append(opts, preferMonthFirst, retryAmbiguousDateWithSwap)
254+
p, err = parseTime(datestr, time.Local, modifiedOpts...)
255+
}
256+
}
257+
258+
}()
259+
}
237260

238-
p := newParser(datestr, loc)
239261
i := 0
240262

241263
// General strategy is to read rune by rune through the date looking for
@@ -293,6 +315,12 @@ iterRunes:
293315
p.setMonth()
294316
p.dayi = i + 1
295317
}
318+
} else {
319+
if p.daylen == 0 {
320+
p.daylen = i
321+
p.setDay()
322+
p.moi = i + 1
323+
}
296324
}
297325
}
298326

@@ -489,6 +517,12 @@ iterRunes:
489517
p.setDay()
490518
p.yeari = i + 1
491519
}
520+
} else {
521+
if p.molen == 0 {
522+
p.molen = i - p.moi
523+
p.setMonth()
524+
p.yeari = i + 1
525+
}
492526
}
493527
}
494528

@@ -712,7 +746,7 @@ iterRunes:
712746
} else if i == 4 {
713747
// gross
714748
datestr = datestr[0:i-1] + datestr[i:]
715-
return parseTime(datestr, loc)
749+
return parseTime(datestr, loc, opts...)
716750
} else {
717751
return nil, unknownErr(datestr)
718752
}
@@ -867,25 +901,25 @@ iterRunes:
867901
case 't', 'T':
868902
if p.nextIs(i, 'h') || p.nextIs(i, 'H') {
869903
if len(datestr) > i+2 {
870-
return parseTime(fmt.Sprintf("%s%s", p.datestr[0:i], p.datestr[i+2:]), loc)
904+
return parseTime(fmt.Sprintf("%s%s", p.datestr[0:i], p.datestr[i+2:]), loc, opts...)
871905
}
872906
}
873907
case 'n', 'N':
874908
if p.nextIs(i, 'd') || p.nextIs(i, 'D') {
875909
if len(datestr) > i+2 {
876-
return parseTime(fmt.Sprintf("%s%s", p.datestr[0:i], p.datestr[i+2:]), loc)
910+
return parseTime(fmt.Sprintf("%s%s", p.datestr[0:i], p.datestr[i+2:]), loc, opts...)
877911
}
878912
}
879913
case 's', 'S':
880914
if p.nextIs(i, 't') || p.nextIs(i, 'T') {
881915
if len(datestr) > i+2 {
882-
return parseTime(fmt.Sprintf("%s%s", p.datestr[0:i], p.datestr[i+2:]), loc)
916+
return parseTime(fmt.Sprintf("%s%s", p.datestr[0:i], p.datestr[i+2:]), loc, opts...)
883917
}
884918
}
885919
case 'r', 'R':
886920
if p.nextIs(i, 'd') || p.nextIs(i, 'D') {
887921
if len(datestr) > i+2 {
888-
return parseTime(fmt.Sprintf("%s%s", p.datestr[0:i], p.datestr[i+2:]), loc)
922+
return parseTime(fmt.Sprintf("%s%s", p.datestr[0:i], p.datestr[i+2:]), loc, opts...)
889923
}
890924
}
891925
}
@@ -1059,7 +1093,7 @@ iterRunes:
10591093
// 2014-05-11 08:20:13,787
10601094
ds := []byte(p.datestr)
10611095
ds[i] = '.'
1062-
return parseTime(string(ds), loc)
1096+
return parseTime(string(ds), loc, opts...)
10631097
case '-', '+':
10641098
// 03:21:51+00:00
10651099
p.stateTime = timeOffset
@@ -1763,48 +1797,75 @@ iterRunes:
17631797
}
17641798

17651799
type parser struct {
1766-
loc *time.Location
1767-
preferMonthFirst bool
1768-
ambiguousMD bool
1769-
stateDate dateState
1770-
stateTime timeState
1771-
format []byte
1772-
datestr string
1773-
fullMonth string
1774-
skip int
1775-
extra int
1776-
part1Len int
1777-
yeari int
1778-
yearlen int
1779-
moi int
1780-
molen int
1781-
dayi int
1782-
daylen int
1783-
houri int
1784-
hourlen int
1785-
mini int
1786-
minlen int
1787-
seci int
1788-
seclen int
1789-
msi int
1790-
mslen int
1791-
offseti int
1792-
offsetlen int
1793-
tzi int
1794-
tzlen int
1795-
t *time.Time
1800+
loc *time.Location
1801+
preferMonthFirst bool
1802+
retryAmbiguousDateWithSwap bool
1803+
ambiguousMD bool
1804+
stateDate dateState
1805+
stateTime timeState
1806+
format []byte
1807+
datestr string
1808+
fullMonth string
1809+
skip int
1810+
extra int
1811+
part1Len int
1812+
yeari int
1813+
yearlen int
1814+
moi int
1815+
molen int
1816+
dayi int
1817+
daylen int
1818+
houri int
1819+
hourlen int
1820+
mini int
1821+
minlen int
1822+
seci int
1823+
seclen int
1824+
msi int
1825+
mslen int
1826+
offseti int
1827+
offsetlen int
1828+
tzi int
1829+
tzlen int
1830+
t *time.Time
1831+
}
1832+
1833+
// ParserOption defines a function signature implemented by options
1834+
// Options defined like this accept the parser and operate on the data within
1835+
type ParserOption func(*parser) error
1836+
1837+
// PreferMonthFirst is an option that allows preferMonthFirst to be changed from its default
1838+
func PreferMonthFirst(preferMonthFirst bool) ParserOption {
1839+
return func(p *parser) error {
1840+
p.preferMonthFirst = preferMonthFirst
1841+
return nil
1842+
}
17961843
}
17971844

1798-
func newParser(dateStr string, loc *time.Location) *parser {
1799-
p := parser{
1800-
stateDate: dateStart,
1801-
stateTime: timeIgnore,
1802-
datestr: dateStr,
1803-
loc: loc,
1804-
preferMonthFirst: true,
1845+
// RetryAmbiguousDateWithSwap is an option that allows retryAmbiguousDateWithSwap to be changed from its default
1846+
func RetryAmbiguousDateWithSwap(retryAmbiguousDateWithSwap bool) ParserOption {
1847+
return func(p *parser) error {
1848+
p.retryAmbiguousDateWithSwap = retryAmbiguousDateWithSwap
1849+
return nil
1850+
}
1851+
}
1852+
1853+
func newParser(dateStr string, loc *time.Location, opts ...ParserOption) *parser {
1854+
p := &parser{
1855+
stateDate: dateStart,
1856+
stateTime: timeIgnore,
1857+
datestr: dateStr,
1858+
loc: loc,
1859+
preferMonthFirst: true,
1860+
retryAmbiguousDateWithSwap: false,
18051861
}
18061862
p.format = []byte(dateStr)
1807-
return &p
1863+
1864+
// allow the options to mutate the parser fields from their defaults
1865+
for _, option := range opts {
1866+
option(p)
1867+
}
1868+
return p
18081869
}
18091870

18101871
func (p *parser) nextIs(i int, b byte) bool {

parseany_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -686,3 +686,38 @@ func TestInLocation(t *testing.T) {
686686
assert.Equal(t, zeroTime, ts.Unix())
687687
assert.NotEqual(t, nil, err)
688688
}
689+
690+
func TestPreferMonthFirst(t *testing.T) {
691+
// default case is true
692+
ts, err := ParseAny("04/02/2014 04:08:09 +0000 UTC")
693+
assert.Equal(t, nil, err)
694+
assert.Equal(t, "2014-04-02 04:08:09 +0000 UTC", fmt.Sprintf("%v", ts.In(time.UTC)))
695+
696+
preferMonthFirstTrue := PreferMonthFirst(true)
697+
ts, err = ParseAny("04/02/2014 04:08:09 +0000 UTC", preferMonthFirstTrue)
698+
assert.Equal(t, nil, err)
699+
assert.Equal(t, "2014-04-02 04:08:09 +0000 UTC", fmt.Sprintf("%v", ts.In(time.UTC)))
700+
701+
// allows the day to be preferred before the month, when completely ambiguous
702+
preferMonthFirstFalse := PreferMonthFirst(false)
703+
ts, err = ParseAny("04/02/2014 04:08:09 +0000 UTC", preferMonthFirstFalse)
704+
assert.Equal(t, nil, err)
705+
assert.Equal(t, "2014-02-04 04:08:09 +0000 UTC", fmt.Sprintf("%v", ts.In(time.UTC)))
706+
}
707+
708+
func TestRetryAmbiguousDateWithSwap(t *testing.T) {
709+
// default is false
710+
_, err := ParseAny("13/02/2014 04:08:09 +0000 UTC")
711+
assert.NotEqual(t, nil, err)
712+
713+
// will fail error if the month preference cannot work due to the value being larger than 12
714+
retryAmbiguousDateWithSwapFalse := RetryAmbiguousDateWithSwap(false)
715+
_, err = ParseAny("13/02/2014 04:08:09 +0000 UTC", retryAmbiguousDateWithSwapFalse)
716+
assert.NotEqual(t, nil, err)
717+
718+
// will retry with the other month preference if this error is detected
719+
retryAmbiguousDateWithSwapTrue := RetryAmbiguousDateWithSwap(true)
720+
ts, err := ParseAny("13/02/2014 04:08:09 +0000 UTC", retryAmbiguousDateWithSwapTrue)
721+
assert.Equal(t, nil, err)
722+
assert.Equal(t, "2014-02-13 04:08:09 +0000 UTC", fmt.Sprintf("%v", ts.In(time.UTC)))
723+
}

0 commit comments

Comments
 (0)