Skip to content

Commit b9fe72e

Browse files
Add ParseInLocation (#28)
* 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>
1 parent 125e13f commit b9fe72e

File tree

3 files changed

+116
-93
lines changed

3 files changed

+116
-93
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,5 @@ _testmain.go
2222
*.exe
2323
*.test
2424
*.prof
25+
26+
.idea

iso8601.go

Lines changed: 17 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const (
2727
// ParseISOZone parses the 5 character zone information in an ISO8601 date string.
2828
// This function expects input that matches:
2929
//
30+
// Z, z (UTC)
3031
// -0100
3132
// +0100
3233
// +01:00
@@ -35,11 +36,13 @@ const (
3536
// +01:45
3637
// +0145
3738
func ParseISOZone(inp []byte) (*time.Location, error) {
38-
if len(inp) < 3 || len(inp) > 6 {
39+
if len(inp) != 1 && (len(inp) < 3 || len(inp) > 6) {
3940
return nil, ErrZoneCharacters
4041
}
4142
var neg bool
4243
switch inp[0] {
44+
case 'Z', 'z':
45+
return time.UTC, nil
4346
case '+':
4447
case '-':
4548
neg = true
@@ -87,6 +90,12 @@ func ParseISOZone(inp []byte) (*time.Location, error) {
8790
// Parse parses an ISO8601 compliant date-time byte slice into a time.Time object.
8891
// If any component of an input date-time is not within the expected range then an *iso8601.RangeError is returned.
8992
func Parse(inp []byte) (time.Time, error) {
93+
return ParseInLocation(inp, time.UTC)
94+
}
95+
96+
// ParseInLocation parses an ISO8601 compliant date-time byte slice into a time.Time object.
97+
// If the input does not have timezone information, it will use the given location.
98+
func ParseInLocation(inp []byte, loc *time.Location) (time.Time, error) {
9099
var (
91100
Y uint
92101
M uint
@@ -98,9 +107,6 @@ func Parse(inp []byte) (time.Time, error) {
98107
nfraction = 1 //counts amount of precision for the second fraction
99108
)
100109

101-
// Always assume UTC by default
102-
var loc = time.UTC
103-
104110
var c uint
105111
var p = year
106112

@@ -131,7 +137,7 @@ parse:
131137
continue
132138
}
133139
fallthrough
134-
case '+':
140+
case '+', 'Z':
135141
if i == 0 {
136142
// The ISO8601 technically allows signed year components.
137143
// 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:
185191
s = c
186192
c = 0
187193
p++
188-
case 'Z':
189-
switch p {
190-
case hour:
191-
h = c
192-
case minute:
193-
m = c
194-
case second:
195-
s = c
196-
case millisecond:
197-
fraction = int(c)
198-
default:
199-
return time.Time{}, newUnexpectedCharacterError(inp[i])
200-
}
201-
c = 0
202-
if len(inp) != i+1 {
203-
return time.Time{}, ErrRemainingData
204-
}
205194
default:
206195
return time.Time{}, newUnexpectedCharacterError(inp[i])
207196
}
@@ -290,3 +279,9 @@ parse:
290279
func ParseString(inp string) (time.Time, error) {
291280
return Parse([]byte(inp))
292281
}
282+
283+
// ParseStringInLocation parses an ISO8601 compliant date-time string into a time.Time object.
284+
// If the input does not have timezone information, it will use the given location.
285+
func ParseStringInLocation(inp string, loc *time.Location) (time.Time, error) {
286+
return ParseInLocation([]byte(inp), loc)
287+
}

iso8601_test.go

Lines changed: 97 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package iso8601
22

33
import (
44
"testing"
5+
"time"
56
)
67

78
type TestCase struct {
@@ -57,6 +58,41 @@ func (tc TestCase) CheckError(err error, t *testing.T) bool {
5758
return false
5859
}
5960

61+
func (tc TestCase) Check(d time.Time, t *testing.T) {
62+
if y := d.Year(); y != tc.Year {
63+
t.Errorf("Year = %d; want %d", y, tc.Year)
64+
}
65+
if m := int(d.Month()); m != tc.Month {
66+
t.Errorf("Month = %d; want %d", m, tc.Month)
67+
}
68+
if d := d.Day(); d != tc.Day {
69+
t.Errorf("Day = %d; want %d", d, tc.Day)
70+
}
71+
if h := d.Hour(); h != tc.Hour {
72+
t.Errorf("Hour = %d; want %d", h, tc.Hour)
73+
}
74+
if m := d.Minute(); m != tc.Minute {
75+
t.Errorf("Minute = %d; want %d", m, tc.Minute)
76+
}
77+
if s := d.Second(); s != tc.Second {
78+
t.Errorf("Second = %d; want %d", s, tc.Second)
79+
}
80+
81+
if ms := d.Nanosecond() / 1000000; ms != tc.MilliSecond {
82+
t.Errorf(
83+
"Millisecond = %d; want %d (%d nanoseconds)",
84+
ms,
85+
tc.MilliSecond,
86+
d.Nanosecond(),
87+
)
88+
}
89+
90+
_, z := d.Zone()
91+
if offset := float64(z) / 3600; offset != tc.Zone {
92+
t.Errorf("Zone = %.2f (%d); want %.2f", offset, z, tc.Zone)
93+
}
94+
}
95+
6096
var cases = []TestCase{
6197
{
6298
Using: "2017-04-24T09:41:34.502+0100",
@@ -340,83 +376,32 @@ var cases = []TestCase{
340376

341377
func TestParse(t *testing.T) {
342378
for _, c := range cases {
343-
t.Run(c.Using, func(t *testing.T) {
344-
d, err := Parse([]byte(c.Using))
345-
if c.CheckError(err, t) {
346-
return
347-
}
348-
t.Log(d)
349-
350-
if y := d.Year(); y != c.Year {
351-
t.Errorf("Year = %d; want %d", y, c.Year)
352-
}
353-
if m := int(d.Month()); m != c.Month {
354-
t.Errorf("Month = %d; want %d", m, c.Month)
355-
}
356-
if d := d.Day(); d != c.Day {
357-
t.Errorf("Day = %d; want %d", d, c.Day)
358-
}
359-
if h := d.Hour(); h != c.Hour {
360-
t.Errorf("Hour = %d; want %d", h, c.Hour)
361-
}
362-
if m := d.Minute(); m != c.Minute {
363-
t.Errorf("Minute = %d; want %d", m, c.Minute)
364-
}
365-
if s := d.Second(); s != c.Second {
366-
t.Errorf("Second = %d; want %d", s, c.Second)
367-
}
368-
369-
if ms := d.Nanosecond() / 1000000; ms != c.MilliSecond {
370-
t.Errorf("Millisecond = %d; want %d (%d nanoseconds)", ms, c.MilliSecond, d.Nanosecond())
371-
}
372-
373-
_, z := d.Zone()
374-
if offset := float64(z) / 3600; offset != c.Zone {
375-
t.Errorf("Zone = %.2f (%d); want %.2f", offset, z, c.Zone)
376-
}
377-
})
379+
t.Run(
380+
c.Using, func(t *testing.T) {
381+
d, err := Parse([]byte(c.Using))
382+
if c.CheckError(err, t) {
383+
return
384+
}
385+
t.Log(d)
386+
c.Check(d, t)
387+
},
388+
)
378389

379390
}
380391
}
381392

382393
func TestParseString(t *testing.T) {
383394
for _, c := range cases {
384-
t.Run(c.Using, func(t *testing.T) {
385-
d, err := ParseString(c.Using)
386-
if c.CheckError(err, t) {
387-
return
388-
}
389-
t.Log(d)
390-
391-
if y := d.Year(); y != c.Year {
392-
t.Errorf("Year = %d; want %d", y, c.Year)
393-
}
394-
if m := int(d.Month()); m != c.Month {
395-
t.Errorf("Month = %d; want %d", m, c.Month)
396-
}
397-
if d := d.Day(); d != c.Day {
398-
t.Errorf("Day = %d; want %d", d, c.Day)
399-
}
400-
if h := d.Hour(); h != c.Hour {
401-
t.Errorf("Hour = %d; want %d", h, c.Hour)
402-
}
403-
if m := d.Minute(); m != c.Minute {
404-
t.Errorf("Minute = %d; want %d", m, c.Minute)
405-
}
406-
if s := d.Second(); s != c.Second {
407-
t.Errorf("Second = %d; want %d", s, c.Second)
408-
}
409-
410-
if ms := d.Nanosecond() / 1000000; ms != c.MilliSecond {
411-
t.Errorf("Millisecond = %d; want %d (%d nanoseconds)", ms, c.MilliSecond, d.Nanosecond())
412-
}
413-
414-
_, z := d.Zone()
415-
if offset := float64(z) / 3600; offset != c.Zone {
416-
t.Errorf("Zone = %.2f (%d); want %.2f", offset, z, c.Zone)
417-
}
418-
})
419-
395+
t.Run(
396+
c.Using, func(t *testing.T) {
397+
d, err := ParseString(c.Using)
398+
if c.CheckError(err, t) {
399+
return
400+
}
401+
t.Log(d)
402+
c.Check(d, t)
403+
},
404+
)
420405
}
421406
}
422407

@@ -429,3 +414,44 @@ func BenchmarkParse(b *testing.B) {
429414
}
430415
}
431416
}
417+
418+
func TestParseStringInLocation(t *testing.T) {
419+
cases := []TestCase{
420+
{
421+
Using: "2017-04-24T09:41:34.502+05:45",
422+
Year: 2017, Month: 4, Day: 24,
423+
Hour: 9, Minute: 41, Second: 34,
424+
MilliSecond: 502,
425+
Zone: 5.75,
426+
},
427+
{
428+
Using: "2017-04-24T09:41:34.502",
429+
Year: 2017, Month: 4, Day: 24,
430+
Hour: 9, Minute: 41, Second: 34,
431+
MilliSecond: 502,
432+
Zone: 5,
433+
},
434+
{
435+
Using: "2017-04-24T09:41:34.502Z",
436+
Year: 2017, Month: 4, Day: 24,
437+
Hour: 9, Minute: 41, Second: 34,
438+
MilliSecond: 502,
439+
Zone: 0,
440+
},
441+
}
442+
443+
loc := time.FixedZone("UTC+5", 5*60*60)
444+
445+
for _, c := range cases {
446+
t.Run(
447+
c.Using, func(t *testing.T) {
448+
d, err := ParseStringInLocation(c.Using, loc)
449+
if c.CheckError(err, t) {
450+
return
451+
}
452+
t.Log(d)
453+
c.Check(d, t)
454+
},
455+
)
456+
}
457+
}

0 commit comments

Comments
 (0)