Skip to content

Commit

Permalink
Date Parse bug fixed; added AutoParseUS functions
Browse files Browse the repository at this point in the history
  • Loading branch information
rickb777 committed Dec 13, 2023
1 parent 8b74c0b commit 69be1a1
Show file tree
Hide file tree
Showing 2 changed files with 147 additions and 22 deletions.
93 changes: 75 additions & 18 deletions parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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")
Expand All @@ -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 {
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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, '-')
Expand All @@ -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)
}

Expand Down Expand Up @@ -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 {
Expand Down
76 changes: 72 additions & 4 deletions parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
time "time"
)

func TestAutoParse(t *testing.T) {
func TestAutoParse_both(t *testing.T) {
cases := []struct {
value string
year int
Expand All @@ -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},
Expand Down Expand Up @@ -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) {
Expand All @@ -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",
Expand All @@ -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)
}
}
}

Expand Down Expand Up @@ -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`},
Expand Down Expand Up @@ -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 {
Expand Down

0 comments on commit 69be1a1

Please sign in to comment.