diff --git a/parse.go b/parse.go index 6b192607..81904d87 100644 --- a/parse.go +++ b/parse.go @@ -23,9 +23,21 @@ func MustAutoParse(value string) Date { return d } +// MustAutoParseUS is as per AutoParseUS except that it panics if the string cannot be parsed. +// This is intended for setup code; don't use it for user inputs. +func MustAutoParseUS(value string) Date { + d, err := AutoParseUS(value) + if err != nil { + panic(err) + } + return d +} + // AutoParse is like ParseISO, except that it automatically adapts to a variety of date formats -// provided that they can be detected unambiguously. Specifically, this includes the "European" -// and "British" date formats but not the common US format. Surrounding whitespace is ignored. +// provided that they can be detected unambiguously. Specifically, this includes the widely-used +// "European" and "British" date formats but not the common US format. Surrounding whitespace is +// ignored. +// // The supported formats are: // // * all formats supported by ParseISO @@ -34,8 +46,34 @@ func MustAutoParse(value string) Date { // // * dd/mm/yyyy | dd.mm.yyyy (or any similar pattern) // +// * d/m/yyyy | d.m.yyyy (or any similar pattern) +// // * surrounding whitespace is ignored func AutoParse(value string) (Date, error) { + return autoParse(value, func(yyyy, f1, f2 string) string { return fmt.Sprintf("%s-%s-%s", yyyy, f1, f2) }) +} + +// AutoParseUS is like ParseISO, except that it automatically adapts to a variety of date formats +// provided that they can be detected unambiguously. Specifically, this includes the widely-used +// "European" and "US" date formats but not the common "British" format. Surrounding whitespace is +// ignored. +// +// The supported formats are: +// +// * all formats supported by ParseISO +// +// * yyyy/mm/dd | yyyy.mm.dd (or any similar pattern) +// +// * mm/dd/yyyy | mm.dd.yyyy (or any similar pattern) +// +// * m/d/yyyy | m.d.yyyy (or any similar pattern) +// +// * surrounding whitespace is ignored +func AutoParseUS(value string) (Date, error) { + return autoParse(value, func(yyyy, f1, f2 string) string { return fmt.Sprintf("%s-%s-%s", yyyy, f2, f1) }) +} + +func autoParse(value string, compose func(yyyy, f1, f2 string) string) (Date, error) { abs := strings.TrimSpace(value) if len(abs) == 0 { return 0, errors.New("Date.AutoParse: cannot parse a blank string") @@ -47,7 +85,7 @@ func AutoParse(value string) (Date, error) { abs = abs[1:] } - if len(abs) >= 10 { + if len(abs) >= 8 { i1 := -1 i2 := -1 for i, r := range abs { @@ -59,18 +97,26 @@ func AutoParse(value string) (Date, error) { } } } + if i1 >= 4 && i2 > i1 && abs[i1] == abs[i2] { // just normalise the punctuation a := []byte(abs) a[i1] = '-' a[i2] = '-' abs = string(a) - } else if i1 >= 2 && i2 > i1 && abs[i1] == abs[i2] { + + } else if i1 >= 1 && i2 > i1 && abs[i1] == abs[i2] { // harder case - need to swap the field order - dd := abs[0:i1] - mm := abs[i1+1 : i2] + f1 := abs[0:i1] // day or month + f2 := abs[i1+1 : i2] // month or day + if len(f1) == 1 { + f1 = "0" + f1 + } + if len(f2) == 1 { + f2 = "0" + f2 + } yyyy := abs[i2+1:] - abs = fmt.Sprintf("%s-%s-%s", yyyy, mm, dd) + abs = compose(yyyy, f2, f1) } } return parseISO(value, sign+abs) @@ -92,12 +138,15 @@ func MustParseISO(value string) Date { // - the common formats ±YYYY-MM-DD and ±YYYYMMDD (e.g. 2006-01-02 and 20060102) // - the ordinal date representation ±YYYY-OOO (e.g. 2006-217) // -// ParseISO will accept dates with more year digits than the four-digit minimum. A -// leading plus '+' sign is allowed and ignored. +// For common formats, ParseISO will accept dates with more year digits than the four-digit +// minimum. A leading plus '+' sign is allowed and ignored. Basic format (without '-' +// separators) is allowed. +// +// For ordinal dates, the extended format (including '-') is supported, but the basic format +// (without '-') is not supported because it could not be distinguished from the YYYYMMDD format. // -// Function date.Parse can be used to parse date strings in other formats, but it -// is currently not able to parse ISO 8601 formatted strings that use the -// expanded year format. +// See also date.Parse, which can be used to parse date strings in other formats; however, it +// only accepts years represented with exactly four digits. // // Background: https://en.wikipedia.org/wiki/ISO_8601#Dates // https://www.iso.org/obp/ui#iso:std:iso:8601:-1:ed-1:v1:en:term:3.1.3.1 @@ -108,12 +157,15 @@ func ParseISO(value string) (Date, error) { func parseISO(input, value string) (Date, error) { abs := value sign := 1 - switch value[0] { - case '+': - abs = value[1:] - case '-': - abs = value[1:] - sign = -1 + + if len(value) > 0 { + switch value[0] { + case '+': + abs = value[1:] + case '-': + abs = value[1:] + sign = -1 + } } dash1 := strings.IndexByte(abs, '-') @@ -124,6 +176,10 @@ func parseISO(input, value string) (Date, error) { ln := len(abs) fm := ln - 4 fd := ln - 2 + if fm < 0 || fd < 0 { + return 0, fmt.Errorf("Date.ParseISO: cannot parse %q: too short", input) + } + return parseYYYYMMDD(input, abs[:fm], abs[fm:fd], abs[fd:], sign) } @@ -217,6 +273,7 @@ func MustParse(layout, value string) Date { // // This function cannot currently parse ISO 8601 strings that use the expanded // year format; you should use date.ParseISO to parse those strings correctly. +// That is, it only accepts years represented with exactly four digits. func Parse(layout, value string) (Date, error) { t, err := time.Parse(layout, value) if err != nil { diff --git a/parse_test.go b/parse_test.go index d4866c0e..f3ac553a 100644 --- a/parse_test.go +++ b/parse_test.go @@ -10,7 +10,7 @@ import ( time "time" ) -func TestAutoParse(t *testing.T) { +func TestAutoParse_both(t *testing.T) { cases := []struct { value string year int @@ -20,7 +20,6 @@ func TestAutoParse(t *testing.T) { {value: "01-01-1970", year: 1970, month: time.January, day: 1}, {value: "+1970-01-01", year: 1970, month: time.January, day: 1}, {value: "+01970-01-02", year: 1970, month: time.January, day: 2}, - {value: " 31/12/1969 ", year: 1969, month: time.December, day: 31}, {value: "1969/12/31", year: 1969, month: time.December, day: 31}, {value: "1969.12.31", year: 1969, month: time.December, day: 31}, {value: "1969-12-31", year: 1969, month: time.December, day: 31}, @@ -48,6 +47,15 @@ func TestAutoParse(t *testing.T) { {value: "+12340506", year: 1234, month: time.May, day: 6}, {value: "-00191012", year: -19, month: time.October, day: 12}, {value: " -00191012 ", year: -19, month: time.October, day: 12}, + // yyyy-ooo ordinal cases + {value: "2004-001", year: 2004, month: time.January, day: 1}, + {value: "2004-060", year: 2004, month: time.February, day: 29}, + {value: "2004-366", year: 2004, month: time.December, day: 31}, + {value: "2003-365", year: 2003, month: time.December, day: 31}, + // basic format is only supported for yyyymmdd (yyyyooo ordinal is not supported) + {value: "12340506", year: 1234, month: time.May, day: 6}, + {value: "+12340506", year: 1234, month: time.May, day: 6}, + {value: "-00191012", year: -19, month: time.October, day: 12}, } for i, c := range cases { t.Run(fmt.Sprintf("%d %s", i, c.value), func(t *testing.T) { @@ -56,9 +64,59 @@ func TestAutoParse(t *testing.T) { if year != c.year || month != c.month || day != c.day { t.Errorf("ParseISO(%v) == %v, want (%v, %v, %v)", c.value, d, c.year, c.month, c.day) } + + d = MustAutoParseUS(c.value) + year, month, day = d.Date() + if year != c.year || month != c.month || day != c.day { + t.Errorf("ParseISO(%v) == %v, want (%v, %v, %v)", c.value, d, c.year, c.month, c.day) + } }) } +} +func TestAutoParse(t *testing.T) { + cases := []struct { + value string + year int + month time.Month + day int + }{ + {value: " 31/12/1969 ", year: 1969, month: time.December, day: 31}, + {value: " 5/6/1905 ", year: 1905, month: time.June, day: 5}, + } + for i, c := range cases { + t.Run(fmt.Sprintf("%d %s", i, c.value), func(t *testing.T) { + d := MustAutoParse(c.value) + year, month, day := d.Date() + if year != c.year || month != c.month || day != c.day { + t.Errorf("ParseISO(%v) == %v, want (%v, %v, %v)", c.value, d, c.year, c.month, c.day) + } + }) + } +} + +func TestAutoParseUS(t *testing.T) { + cases := []struct { + value string + year int + month time.Month + day int + }{ + {value: " 12/31/1969 ", year: 1969, month: time.December, day: 31}, + {value: " 6/5/1905 ", year: 1905, month: time.June, day: 5}, + } + for i, c := range cases { + t.Run(fmt.Sprintf("%d %s", i, c.value), func(t *testing.T) { + d := MustAutoParseUS(c.value) + year, month, day := d.Date() + if year != c.year || month != c.month || day != c.day { + t.Errorf("ParseISO(%v) == %v, want (%v, %v, %v)", c.value, d, c.year, c.month, c.day) + } + }) + } +} + +func TestAutoParse_errors(t *testing.T) { badCases := []string{ "1234-05", "1234-5-6", @@ -84,6 +142,11 @@ func TestAutoParse(t *testing.T) { if err == nil { t.Errorf("ParseISO(%v) == %v", c, d) } + + d, err = AutoParseUS(c) + if err == nil { + t.Errorf("ParseISO(%v) == %v", c, d) + } } } @@ -144,6 +207,10 @@ func TestParseISO_errors(t *testing.T) { value string want string }{ + {value: ``, want: `Date.ParseISO: cannot parse "": ` + "too short"}, + {value: `-`, want: `Date.ParseISO: cannot parse "-": ` + "too short"}, + {value: `z`, want: `Date.ParseISO: cannot parse "z": ` + "too short"}, + {value: `z--`, want: `Date.ParseISO: cannot parse "z--": ` + "year has wrong length\nmonth has wrong length\nday has wrong length"}, {value: `not-a-date`, want: `Date.ParseISO: cannot parse "not-a-date": ` + "year has wrong length\nmonth has wrong length\nday has wrong length"}, {value: `foot-of-og`, want: `Date.ParseISO: cannot parse "foot-of-og": ` + "invalid year\ninvalid month\ninvalid day"}, {value: `215-08-15`, want: `Date.ParseISO: cannot parse "215-08-15": year has wrong length`}, @@ -237,9 +304,10 @@ func TestParse(t *testing.T) { func TestParse_errors(t *testing.T) { // Test inability to parse ISO 8601 expanded year format badCases := []string{ - "+1234-05-06", + "+1234-05-06", // plus sign is not allowed "+12345-06-07", - "-1234-05-06", + "12345-06-07", // five digits are not allowed + "-1234-05-06", // negative sign is not allowed "-12345-06-07", } for i, c := range badCases {