diff --git a/.gitignore b/.gitignore index daf913b..11b90db 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,5 @@ _testmain.go *.exe *.test *.prof + +.idea diff --git a/iso8601.go b/iso8601.go index f4f4e8c..266d871 100644 --- a/iso8601.go +++ b/iso8601.go @@ -27,6 +27,7 @@ const ( // ParseISOZone parses the 5 character zone information in an ISO8601 date string. // This function expects input that matches: // +// Z, z (UTC) // -0100 // +0100 // +01:00 @@ -35,11 +36,13 @@ const ( // +01:45 // +0145 func ParseISOZone(inp []byte) (*time.Location, error) { - if len(inp) < 3 || len(inp) > 6 { + if len(inp) != 1 && (len(inp) < 3 || len(inp) > 6) { return nil, ErrZoneCharacters } var neg bool switch inp[0] { + case 'Z', 'z': + return time.UTC, nil case '+': case '-': neg = true @@ -87,6 +90,12 @@ func ParseISOZone(inp []byte) (*time.Location, error) { // Parse parses an ISO8601 compliant date-time byte slice into a time.Time object. // If any component of an input date-time is not within the expected range then an *iso8601.RangeError is returned. func Parse(inp []byte) (time.Time, error) { + return ParseInLocation(inp, time.UTC) +} + +// ParseInLocation parses an ISO8601 compliant date-time byte slice into a time.Time object. +// If the input does not have timezone information, it will use the given location. +func ParseInLocation(inp []byte, loc *time.Location) (time.Time, error) { var ( Y uint M uint @@ -98,9 +107,6 @@ func Parse(inp []byte) (time.Time, error) { nfraction = 1 //counts amount of precision for the second fraction ) - // Always assume UTC by default - var loc = time.UTC - var c uint var p = year @@ -131,7 +137,7 @@ parse: continue } fallthrough - case '+': + case '+', 'Z': if i == 0 { // The ISO8601 technically allows signed year components. // Go does not allow negative years, but let's allow a positive sign to be more compatible with the spec. @@ -185,23 +191,6 @@ parse: s = c c = 0 p++ - case 'Z': - switch p { - case hour: - h = c - case minute: - m = c - case second: - s = c - case millisecond: - fraction = int(c) - default: - return time.Time{}, newUnexpectedCharacterError(inp[i]) - } - c = 0 - if len(inp) != i+1 { - return time.Time{}, ErrRemainingData - } default: return time.Time{}, newUnexpectedCharacterError(inp[i]) } @@ -290,3 +279,9 @@ parse: func ParseString(inp string) (time.Time, error) { return Parse([]byte(inp)) } + +// ParseStringInLocation parses an ISO8601 compliant date-time string into a time.Time object. +// If the input does not have timezone information, it will use the given location. +func ParseStringInLocation(inp string, loc *time.Location) (time.Time, error) { + return ParseInLocation([]byte(inp), loc) +} diff --git a/iso8601_test.go b/iso8601_test.go index 1336014..3a6cf77 100644 --- a/iso8601_test.go +++ b/iso8601_test.go @@ -2,6 +2,7 @@ package iso8601 import ( "testing" + "time" ) type TestCase struct { @@ -57,6 +58,41 @@ func (tc TestCase) CheckError(err error, t *testing.T) bool { return false } +func (tc TestCase) Check(d time.Time, t *testing.T) { + if y := d.Year(); y != tc.Year { + t.Errorf("Year = %d; want %d", y, tc.Year) + } + if m := int(d.Month()); m != tc.Month { + t.Errorf("Month = %d; want %d", m, tc.Month) + } + if d := d.Day(); d != tc.Day { + t.Errorf("Day = %d; want %d", d, tc.Day) + } + if h := d.Hour(); h != tc.Hour { + t.Errorf("Hour = %d; want %d", h, tc.Hour) + } + if m := d.Minute(); m != tc.Minute { + t.Errorf("Minute = %d; want %d", m, tc.Minute) + } + if s := d.Second(); s != tc.Second { + t.Errorf("Second = %d; want %d", s, tc.Second) + } + + if ms := d.Nanosecond() / 1000000; ms != tc.MilliSecond { + t.Errorf( + "Millisecond = %d; want %d (%d nanoseconds)", + ms, + tc.MilliSecond, + d.Nanosecond(), + ) + } + + _, z := d.Zone() + if offset := float64(z) / 3600; offset != tc.Zone { + t.Errorf("Zone = %.2f (%d); want %.2f", offset, z, tc.Zone) + } +} + var cases = []TestCase{ { Using: "2017-04-24T09:41:34.502+0100", @@ -340,83 +376,32 @@ var cases = []TestCase{ func TestParse(t *testing.T) { for _, c := range cases { - t.Run(c.Using, func(t *testing.T) { - d, err := Parse([]byte(c.Using)) - if c.CheckError(err, t) { - return - } - t.Log(d) - - if y := d.Year(); y != c.Year { - t.Errorf("Year = %d; want %d", y, c.Year) - } - if m := int(d.Month()); m != c.Month { - t.Errorf("Month = %d; want %d", m, c.Month) - } - if d := d.Day(); d != c.Day { - t.Errorf("Day = %d; want %d", d, c.Day) - } - if h := d.Hour(); h != c.Hour { - t.Errorf("Hour = %d; want %d", h, c.Hour) - } - if m := d.Minute(); m != c.Minute { - t.Errorf("Minute = %d; want %d", m, c.Minute) - } - if s := d.Second(); s != c.Second { - t.Errorf("Second = %d; want %d", s, c.Second) - } - - if ms := d.Nanosecond() / 1000000; ms != c.MilliSecond { - t.Errorf("Millisecond = %d; want %d (%d nanoseconds)", ms, c.MilliSecond, d.Nanosecond()) - } - - _, z := d.Zone() - if offset := float64(z) / 3600; offset != c.Zone { - t.Errorf("Zone = %.2f (%d); want %.2f", offset, z, c.Zone) - } - }) + t.Run( + c.Using, func(t *testing.T) { + d, err := Parse([]byte(c.Using)) + if c.CheckError(err, t) { + return + } + t.Log(d) + c.Check(d, t) + }, + ) } } func TestParseString(t *testing.T) { for _, c := range cases { - t.Run(c.Using, func(t *testing.T) { - d, err := ParseString(c.Using) - if c.CheckError(err, t) { - return - } - t.Log(d) - - if y := d.Year(); y != c.Year { - t.Errorf("Year = %d; want %d", y, c.Year) - } - if m := int(d.Month()); m != c.Month { - t.Errorf("Month = %d; want %d", m, c.Month) - } - if d := d.Day(); d != c.Day { - t.Errorf("Day = %d; want %d", d, c.Day) - } - if h := d.Hour(); h != c.Hour { - t.Errorf("Hour = %d; want %d", h, c.Hour) - } - if m := d.Minute(); m != c.Minute { - t.Errorf("Minute = %d; want %d", m, c.Minute) - } - if s := d.Second(); s != c.Second { - t.Errorf("Second = %d; want %d", s, c.Second) - } - - if ms := d.Nanosecond() / 1000000; ms != c.MilliSecond { - t.Errorf("Millisecond = %d; want %d (%d nanoseconds)", ms, c.MilliSecond, d.Nanosecond()) - } - - _, z := d.Zone() - if offset := float64(z) / 3600; offset != c.Zone { - t.Errorf("Zone = %.2f (%d); want %.2f", offset, z, c.Zone) - } - }) - + t.Run( + c.Using, func(t *testing.T) { + d, err := ParseString(c.Using) + if c.CheckError(err, t) { + return + } + t.Log(d) + c.Check(d, t) + }, + ) } } @@ -429,3 +414,44 @@ func BenchmarkParse(b *testing.B) { } } } + +func TestParseStringInLocation(t *testing.T) { + cases := []TestCase{ + { + Using: "2017-04-24T09:41:34.502+05:45", + Year: 2017, Month: 4, Day: 24, + Hour: 9, Minute: 41, Second: 34, + MilliSecond: 502, + Zone: 5.75, + }, + { + Using: "2017-04-24T09:41:34.502", + Year: 2017, Month: 4, Day: 24, + Hour: 9, Minute: 41, Second: 34, + MilliSecond: 502, + Zone: 5, + }, + { + Using: "2017-04-24T09:41:34.502Z", + Year: 2017, Month: 4, Day: 24, + Hour: 9, Minute: 41, Second: 34, + MilliSecond: 502, + Zone: 0, + }, + } + + loc := time.FixedZone("UTC+5", 5*60*60) + + for _, c := range cases { + t.Run( + c.Using, func(t *testing.T) { + d, err := ParseStringInLocation(c.Using, loc) + if c.CheckError(err, t) { + return + } + t.Log(d) + c.Check(d, t) + }, + ) + } +}