Skip to content

Commit

Permalink
Add ParseInLocation (#28)
Browse files Browse the repository at this point in the history
* Add ParseInLocation

* Ensure ParseInLocation overrides input location hint with explicit Zulu time; ParseISOZone supports Zulu (Z) time.

---------

Co-authored-by: Jason Kingsbury <jason@relva.co.uk>
  • Loading branch information
squarespirit and relvacode authored Nov 22, 2024
1 parent 125e13f commit b9fe72e
Show file tree
Hide file tree
Showing 3 changed files with 116 additions and 93 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,5 @@ _testmain.go
*.exe
*.test
*.prof

.idea
39 changes: 17 additions & 22 deletions iso8601.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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])
}
Expand Down Expand Up @@ -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)
}
168 changes: 97 additions & 71 deletions iso8601_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package iso8601

import (
"testing"
"time"
)

type TestCase struct {
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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)
},
)
}
}

Expand All @@ -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)
},
)
}
}

0 comments on commit b9fe72e

Please sign in to comment.