pax_global_header00006660000000000000000000000064143563237410014522gustar00rootroot0000000000000052 comment=dd7beba0ec7c549223285a0a02ed6c27b8733074 iso8601-1.3.0/000077500000000000000000000000001435632374100126345ustar00rootroot00000000000000iso8601-1.3.0/.github/000077500000000000000000000000001435632374100141745ustar00rootroot00000000000000iso8601-1.3.0/.github/workflows/000077500000000000000000000000001435632374100162315ustar00rootroot00000000000000iso8601-1.3.0/.github/workflows/verify.yml000066400000000000000000000015721435632374100202650ustar00rootroot00000000000000name: Verify on: [push] jobs: go-vet-lint-test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Go uses: actions/setup-go@v3 with: go-version: stable - name: Verify dependencies run: go mod verify - name: Run go vet run: go vet ./... - name: Install staticcheck run: go install honnef.co/go/tools/cmd/staticcheck@latest - name: Run staticcheck run: staticcheck ./... - name: Install golint run: go install golang.org/x/lint/golint@latest - name: Run golint run: golint ./... - name: Run tests run: go test -race -vet=off -json ./... > TestResults.json - name: Upload Go test results uses: actions/upload-artifact@v3 with: name: Go-test-results path: TestResults.jsoniso8601-1.3.0/.gitignore000066400000000000000000000004121435632374100146210ustar00rootroot00000000000000# Compiled Object files, Static and Dynamic libs (Shared Objects) *.o *.a *.so # Folders _obj _test # Architecture specific extensions/prefixes *.[568vq] [568vq].out *.cgo1.go *.cgo2.c _cgo_defun.c _cgo_gotypes.go _cgo_export.* _testmain.go *.exe *.test *.prof iso8601-1.3.0/LICENSE000066400000000000000000000020651435632374100136440ustar00rootroot00000000000000MIT License Copyright (c) 2017-2020 Jason Kingsbury Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. iso8601-1.3.0/README.md000066400000000000000000000033621435632374100141170ustar00rootroot00000000000000A fast ISO8601 date parser for Go [![GoDoc](https://godoc.org/github.com/relvacode/iso8601?status.svg)](https://godoc.org/github.com/relvacode/iso8601) ![Build Status](https://github.com/relvacode/iso8601/actions/workflows/verify.yml/badge.svg) [![Go Report Card](https://goreportcard.com/badge/github.com/relvacode/iso8601)](https://goreportcard.com/report/github.com/relvacode/iso8601) ``` go get github.com/relvacode/iso8601 ``` The built-in RFC3333 time layout in Go is too restrictive to support any ISO8601 date-time. This library parses any ISO8601 date into a native Go time object without regular expressions. ## Usage ```go package main import "github.com/relvacode/iso8601" // iso8601.Time can be used as a drop-in replacement for time.Time with JSON responses type ExternalAPIResponse struct { Timestamp *iso8601.Time } func main() { // iso8601.ParseString can also be called directly t, err := iso8601.ParseString("2020-01-02T16:20:00") } ``` ## Benchmark ``` BenchmarkParse-16 13364954 77.7 ns/op 0 B/op 0 allocs/op ``` ## Release History - `1.3.0` Allow a leading `+` sign in the year component [#11](https://github.com/relvacode/iso8601/issues/11) - `1.2.0` Time range validity checking equivalent to the standard library. Note that previous versions would not validate that a given date string was in the expected range. Additionally, this version no longer accepts `0000-00-00T00:00:00` as a valid input which can be the zero time representation in other languages nor does it support leap seconds (such that the seconds field is `60`) as is the case in the [standard library](https://github.com/golang/go/issues/15247) - `1.1.0` Check for `-0` time zone - `1.0.0` Initial release iso8601-1.3.0/error.go000066400000000000000000000033021435632374100143120ustar00rootroot00000000000000package iso8601 import ( "errors" "fmt" ) var ( // ErrZoneCharacters indicates an incorrect amount of characters was passed to ParseISOZone. ErrZoneCharacters = errors.New("iso8601: Expected between 3 and 6 characters for zone information") // ErrInvalidZone indicates an invalid timezone per the standard that doesn't violate any specific // character parsing rules. ErrInvalidZone = errors.New("iso8601: Specified zone is invalid") // ErrRemainingData indicates that there is extra data after a `Z` character. ErrRemainingData = errors.New("iso8601: Unexepected remaining data after `Z`") // ErrNotString indicates that a non string type was passed to the UnmarshalJSON method of `Time`. ErrNotString = errors.New("iso8601: Invalid json type (expected string)") // ErrPrecision indicates that there was too much precision (characters) given to parse // for the fraction of a second of the input time. ErrPrecision = errors.New("iso8601: Too many characters in fraction of second precision") ) func newUnexpectedCharacterError(c byte) error { return &UnexpectedCharacterError{Character: c} } // UnexpectedCharacterError indicates the parser scanned a character that was not expected at that time. type UnexpectedCharacterError struct { Character byte } func (e *UnexpectedCharacterError) Error() string { return fmt.Sprintf("iso8601: Unexpected character `%c`", e.Character) } // RangeError indicates that a value is not in an expected range. type RangeError struct { Value string Element string Min int Max int Given int } func (e *RangeError) Error() string { return fmt.Sprintf("iso8601: Cannot parse %q: %s %d is not in range %d-%d", e.Value, e.Element, e.Given, e.Min, e.Max) } iso8601-1.3.0/go.mod000066400000000000000000000000551435632374100137420ustar00rootroot00000000000000module github.com/relvacode/iso8601 go 1.13 iso8601-1.3.0/iso8601.go000066400000000000000000000132171435632374100143000ustar00rootroot00000000000000// Package iso8601 is a utility for parsing ISO8601 datetime strings into native Go times. // The standard library's RFC3339 reference layout can be too strict for working with 3rd party APIs, // especially ones written in other languages. // // Use the provided `Time` structure instead of the default `time.Time` to provide ISO8601 support for JSON responses. package iso8601 import ( "time" ) const ( year uint = iota month day hour minute second millisecond ) const ( // charStart is the binary position of the character `0` charStart uint = '0' ) // ParseISOZone parses the 5 character zone information in an ISO8061 date string. // This function expects input that matches: // // -0100 // +0100 // +01:00 // -01:00 // +01 // +01:45 // +0145 func ParseISOZone(inp []byte) (*time.Location, error) { if len(inp) < 3 || len(inp) > 6 { return nil, ErrZoneCharacters } var neg bool switch inp[0] { case '+': case '-': neg = true default: return nil, newUnexpectedCharacterError(inp[0]) } var offset int var z uint var multiplier = uint(3600) // start with initial multiplier of hours for i := 1; i < len(inp); i++ { if i == 3 { // next multiplier offset = int(z * multiplier) multiplier = 60 // multiplier for minutes z = 0 } else { // next digit z = z * 10 } switch inp[i] { case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': z += uint(inp[i]) - charStart case ':': if i != 3 { return nil, newUnexpectedCharacterError(inp[i]) } default: return nil, newUnexpectedCharacterError(inp[i]) } } offset += int(z * multiplier) if neg { offset = -offset } if neg && offset == 0 { return nil, ErrInvalidZone } return time.FixedZone("", offset), nil } // 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) { var ( Y uint M uint d uint h uint m uint s uint fraction int 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 var i int parse: for ; i < len(inp); i++ { switch inp[i] { case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': c = c * 10 c += uint(inp[i]) - charStart if p == millisecond { nfraction++ } case '-': if p < hour { switch p { case year: Y = c case month: M = c default: return time.Time{}, newUnexpectedCharacterError(inp[i]) } p++ c = 0 continue } fallthrough case '+': 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. // It must be the very first character of the input (#11). continue } 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 var err error loc, err = ParseISOZone(inp[i:]) if err != nil { return time.Time{}, err } break parse case 'T': if p != day { return time.Time{}, newUnexpectedCharacterError(inp[i]) } d = c c = 0 p++ case ':': switch p { case hour: h = c case minute: m = c case second: m = c default: return time.Time{}, newUnexpectedCharacterError(inp[i]) } c = 0 p++ case '.': if p != second { return time.Time{}, newUnexpectedCharacterError(inp[i]) } 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]) } } // Capture remaining data // Sometimes a date can end without a non-integer character if c > 0 { switch p { case day: d = c case hour: h = c case minute: m = c case second: s = c case millisecond: fraction = int(c) } } // Get the seconds fraction as nanoseconds if fraction < 0 || 1e9 <= fraction { return time.Time{}, ErrPrecision } scale := 10 - nfraction for i := 0; i < scale; i++ { fraction *= 10 } switch { case M < 1 || M > 12: // Month 1-12 return time.Time{}, &RangeError{ Value: string(inp), Element: "month", Given: int(M), Min: 1, Max: 12, } case d < 1 || int(d) > daysIn(time.Month(M), int(Y)): // Day 1-daysIn(month, year) return time.Time{}, &RangeError{ Value: string(inp), Element: "day", Given: int(d), Min: 1, Max: daysIn(time.Month(M), int(Y)), } case h > 23: // Hour 0-23 return time.Time{}, &RangeError{ Value: string(inp), Element: "hour", Given: int(h), Min: 0, Max: 23, } case m > 59: // Minute 0-59 return time.Time{}, &RangeError{ Value: string(inp), Element: "minute", Given: int(m), Min: 0, Max: 59, } case s > 59: // Second 0-59 return time.Time{}, &RangeError{ Value: string(inp), Element: "second", Given: int(s), Min: 0, Max: 59, } } return time.Date(int(Y), time.Month(M), int(d), int(h), int(m), int(s), fraction, loc), nil } // ParseString parses an ISO8601 compliant date-time string into a time.Time object. func ParseString(inp string) (time.Time, error) { return Parse([]byte(inp)) } iso8601-1.3.0/iso8601_test.go000066400000000000000000000212251435632374100153350ustar00rootroot00000000000000package iso8601 import ( "testing" ) type TestCase struct { Using string Year int Month int Day int Hour int Minute int Second int MilliSecond int Zone float64 ShouldFailParse bool ShouldInvalidRange bool RangeElementWhenInvalid string } func (tc TestCase) CheckError(err error, t *testing.T) bool { if err != nil { if tc.ShouldInvalidRange { re, ok := err.(*RangeError) if !ok { t.Fatalf("Found error %s of type %T but was expecting a RangeError", err, err) } if tc.RangeElementWhenInvalid != "" && re.Element != tc.RangeElementWhenInvalid { t.Fatalf("Expected a range error on %q but encountered %q: %s", tc.RangeElementWhenInvalid, re.Element, err) } return true } if tc.ShouldFailParse { return true } t.Fatal(err) return false } if err == nil && (tc.ShouldFailParse || tc.ShouldInvalidRange) { reason := "fail to parse" if tc.ShouldInvalidRange { reason = "catch an invalid date range" } t.Fatalf("Expected test case %s", reason) return true } return false } var cases = []TestCase{ { Using: "2017-04-24T09:41:34.502+0100", Year: 2017, Month: 4, Day: 24, Hour: 9, Minute: 41, Second: 34, MilliSecond: 502, Zone: 1, }, { Using: "2017-04-24T09:41+0100", Year: 2017, Month: 4, Day: 24, Hour: 9, Minute: 41, Zone: 1, }, { Using: "2017-04-24T09+0100", Year: 2017, Month: 4, Day: 24, Hour: 9, Zone: 1, }, { Using: "2017-04-24T", Year: 2017, Month: 4, Day: 24, }, { Using: "2017-04-24", Year: 2017, Month: 4, Day: 24, }, { Using: "2017-04-24T09:41:34+0100", Year: 2017, Month: 4, Day: 24, Hour: 9, Minute: 41, Second: 34, Zone: 1, }, { Using: "2017-04-24T09:41:34.502-0100", Year: 2017, Month: 4, Day: 24, Hour: 9, Minute: 41, Second: 34, MilliSecond: 502, Zone: -1, }, { Using: "2017-04-24T09:41:34.502-01:00", Year: 2017, Month: 4, Day: 24, Hour: 9, Minute: 41, Second: 34, MilliSecond: 502, Zone: -1, }, { Using: "2017-04-24T09:41-01:00", Year: 2017, Month: 4, Day: 24, Hour: 9, Minute: 41, Zone: -1, }, { Using: "2017-04-24T09-01:00", Year: 2017, Month: 4, Day: 24, Hour: 9, Zone: -1, }, { Using: "2017-04-24T09:41:34-0100", Year: 2017, Month: 4, Day: 24, Hour: 9, Minute: 41, Second: 34, Zone: -1, }, { Using: "2017-04-24T09:41:34.502Z", Year: 2017, Month: 4, Day: 24, Hour: 9, Minute: 41, Second: 34, MilliSecond: 502, Zone: 0, }, { Using: "2017-04-24T09:41:34Z", Year: 2017, Month: 4, Day: 24, Hour: 9, Minute: 41, Second: 34, Zone: 0, }, { Using: "2017-04-24T09:41Z", Year: 2017, Month: 4, Day: 24, Hour: 9, Minute: 41, Zone: 0, }, { Using: "2017-04-24T09Z", Year: 2017, Month: 4, Day: 24, Hour: 9, Zone: 0, }, { Using: "2017-04-24T09:41:34.089", Year: 2017, Month: 4, Day: 24, Hour: 9, Minute: 41, Second: 34, MilliSecond: 89, Zone: 0, }, { Using: "2017-04-24T09:41", Year: 2017, Month: 4, Day: 24, Hour: 9, Minute: 41, Zone: 0, }, { Using: "2017-04-24T09", Year: 2017, Month: 4, Day: 24, Hour: 9, Zone: 0, }, { Using: "2017-04-24T09:41:34.009", Year: 2017, Month: 4, Day: 24, Hour: 9, Minute: 41, Second: 34, MilliSecond: 9, Zone: 0, }, { Using: "2017-04-24T09:41:34.893", Year: 2017, Month: 4, Day: 24, Hour: 9, Minute: 41, Second: 34, MilliSecond: 893, Zone: 0, }, { Using: "2017-04-24T09:41:34.89312523Z", Year: 2017, Month: 4, Day: 24, Hour: 9, Minute: 41, Second: 34, MilliSecond: 893, Zone: 0, }, { Using: "2017-04-24T09:41:34.502-0530", Year: 2017, Month: 4, Day: 24, Hour: 9, Minute: 41, Second: 34, MilliSecond: 502, Zone: -5.5, }, { Using: "2017-04-24T09:41:34.502+0530", Year: 2017, Month: 4, Day: 24, Hour: 9, Minute: 41, Second: 34, MilliSecond: 502, Zone: 5.5, }, { Using: "2017-04-24T09:41:34.502+05:30", Year: 2017, Month: 4, Day: 24, Hour: 9, Minute: 41, Second: 34, MilliSecond: 502, Zone: 5.5, }, { 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+00", Year: 2017, Month: 4, Day: 24, Hour: 9, Minute: 41, Second: 34, MilliSecond: 502, Zone: 0, }, { Using: "2017-04-24T09:41:34.502+0000", Year: 2017, Month: 4, Day: 24, Hour: 9, Minute: 41, Second: 34, MilliSecond: 502, Zone: 0, }, { Using: "2017-04-24T09:41:34.502+00:00", Year: 2017, Month: 4, Day: 24, Hour: 9, Minute: 41, Second: 34, MilliSecond: 502, Zone: 0, }, { Using: "+2017-04-24T09:41:34.502+00:00", Year: 2017, Month: 4, Day: 24, Hour: 9, Minute: 41, Second: 34, MilliSecond: 502, Zone: 0, }, // Invalid Parse Test Cases { Using: "2017-04-24T09:41:34.502-00", ShouldFailParse: true, }, { Using: "2017-04-24T09:41:34.502-0000", ShouldFailParse: true, }, { Using: "2017-04-24T09:41:34.502-00:00", ShouldFailParse: true, }, { Using: "-2017-04-24T09:41:34.502-00:00", ShouldFailParse: true, }, { Using: "2017-+04-24T09:41:34.502-00:00", ShouldFailParse: true, }, // Invalid Range Test Cases { Using: "2017-00-01T00:00:00.000+00:00", ShouldInvalidRange: true, RangeElementWhenInvalid: "month", }, { Using: "2017-13-01T00:00:00.000+00:00", ShouldInvalidRange: true, RangeElementWhenInvalid: "month", }, { Using: "2017-01-00T00:00:00.000+00:00", ShouldInvalidRange: true, RangeElementWhenInvalid: "day", }, { Using: "2017-01-32T00:00:00.000+00:00", ShouldInvalidRange: true, RangeElementWhenInvalid: "day", }, { Using: "2019-02-29T00:00:00.000+00:00", ShouldInvalidRange: true, RangeElementWhenInvalid: "day", }, { Using: "2020-02-30T00:00:00.000+00:00", // Leap year ShouldInvalidRange: true, RangeElementWhenInvalid: "day", }, { Using: "2017-01-01T24:00:00.000+00:00", ShouldInvalidRange: true, RangeElementWhenInvalid: "hour", }, { Using: "2017-01-01T00:60:00.000+00:00", ShouldInvalidRange: true, RangeElementWhenInvalid: "minute", }, { Using: "2017-01-01T00:00:60.000+00:00", ShouldInvalidRange: true, RangeElementWhenInvalid: "second", }, } 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) } }) } } 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) } }) } } func BenchmarkParse(b *testing.B) { x := []byte("2017-04-24T09:41:34.502Z") for i := 0; i < b.N; i++ { _, err := Parse(x) if err != nil { b.Fatal(err) } } } iso8601-1.3.0/json.go000066400000000000000000000014401435632374100141330ustar00rootroot00000000000000package iso8601 import ( "encoding/json" "time" ) // null returns true if the given byte slice is a JSON null. // This is about 3x faster than `bytes.Compare`. func null(b []byte) bool { if len(b) != 4 { return false } if b[0] != 'n' && b[1] != 'u' && b[2] != 'l' && b[3] != 'l' { return false } return true } var _ json.Unmarshaler = &Time{} // Time is a helper object for parsing ISO8061 dates as a JSON string. type Time struct { time.Time } // UnmarshalJSON decodes a JSON string or null into a iso8601 time func (t *Time) UnmarshalJSON(b []byte) error { // Do not process null types if null(b) { return nil } if len(b) > 0 && b[0] == '"' && b[len(b)-1] == '"' { b = b[1 : len(b)-1] } else { return ErrNotString } var err error t.Time, err = Parse(b) return err } iso8601-1.3.0/json_test.go000066400000000000000000000066271435632374100152060ustar00rootroot00000000000000package iso8601 import ( "bytes" "encoding/json" "testing" "time" ) type TestAPIResponse struct { Ptr *Time Nptr Time } type TestStdLibAPIResponse struct { Ptr *time.Time Nptr time.Time } var ShortTest = TestCase{ Using: "2001-11-13", Year: 2001, Month: 11, Day: 13, } var StructTestData = []byte(` { "Ptr": "2017-04-26T11:13:04+01:00", "Nptr": "2017-04-26T11:13:04+01:00" } `) var NullTestData = []byte(` { "Ptr": null, "Nptr": null } `) var ZeroedTestData = []byte(` { "Ptr": "0001-01-01", "Nptr": "0001-01-01" } `) var StructTest = TestCase{ Year: 2017, Month: 04, Day: 26, Hour: 11, Minute: 13, Second: 04, Zone: 1, } func TestTime_UnmarshalJSON(t *testing.T) { t.Run("short", func(t *testing.T) { var b = []byte(`"2001-11-13"`) tn := new(Time) if err := tn.UnmarshalJSON(b); err != nil { t.Fatal(err) } if y := tn.Year(); y != ShortTest.Year { t.Errorf("Year = %d; want %d", y, ShortTest.Year) } if m := int(tn.Month()); m != ShortTest.Month { t.Errorf("Month = %d; want %d", m, ShortTest.Month) } if d := tn.Day(); d != ShortTest.Day { t.Errorf("Day = %d; want %d", d, ShortTest.Day) } err := tn.UnmarshalJSON([]byte(`2001-11-13`)) if err != ErrNotString { t.Fatal(err) } if err == nil { t.Fatal("Expected an error from unmarshal") } }) t.Run("struct", func(t *testing.T) { resp := new(TestAPIResponse) if err := json.Unmarshal(StructTestData, resp); err != nil { t.Fatal(err) } stdlibResp := new(TestStdLibAPIResponse) if err := json.Unmarshal(StructTestData, stdlibResp); err != nil { t.Fatal(err) } t.Run("stblib parity", func(t *testing.T) { if !resp.Ptr.Equal(*stdlibResp.Ptr) || !resp.Nptr.Equal(stdlibResp.Nptr) { t.Fatalf("Parsed time values are not equal to standard library implementation") } }) t.Run("ptr", func(t *testing.T) { if y := resp.Ptr.Year(); y != StructTest.Year { t.Errorf("Ptr: Year = %d; want %d", y, StructTest.Year) } if d := resp.Ptr.Day(); d != StructTest.Day { t.Errorf("Ptr: Day = %d; want %d", d, StructTest.Day) } if s := resp.Ptr.Second(); s != StructTest.Second { t.Errorf("Ptr: Second = %d; want %d", s, StructTest.Second) } }) t.Run("noptr", func(t *testing.T) { if y := resp.Nptr.Year(); y != StructTest.Year { t.Errorf("NoPtr: Year = %d; want %d", y, StructTest.Year) } if d := resp.Nptr.Day(); d != StructTest.Day { t.Errorf("NoPtr: Day = %d; want %d", d, StructTest.Day) } if s := resp.Nptr.Second(); s != StructTest.Second { t.Errorf("NoPtr: Second = %d; want %d", s, StructTest.Second) } }) }) t.Run("null", func(t *testing.T) { resp := new(TestAPIResponse) if err := json.Unmarshal(NullTestData, resp); err != nil { t.Fatal(err) } }) t.Run("time zeroed", func(t *testing.T) { resp := new(TestAPIResponse) if err := json.Unmarshal(ZeroedTestData, resp); err != nil { t.Fatal(err) } }) t.Run("reparse", func(t *testing.T) { s := time.Now().UTC() data := []byte(s.Format(time.RFC3339Nano)) n, err := Parse(data) if err != nil { t.Fatal(err) } if !s.Equal(n) { t.Fatalf("Parsing a JSON date mismatch; wanted %s; got %s", s, n) } }) } func BenchmarkCheckNull(b *testing.B) { var n = []byte("null") b.Run("compare", func(b *testing.B) { for i := 0; i < b.N; i++ { bytes.Compare(n, n) } }) b.Run("exact", func(b *testing.B) { for i := 0; i < b.N; i++ { null(n) } }) } iso8601-1.3.0/time.go000066400000000000000000000010711435632374100141200ustar00rootroot00000000000000package iso8601 import "time" // The following is copied from the Go standard library to implement date range validation logic // equivalent to the behaviour of Go's time.Parse. func isLeap(year int) bool { return year%4 == 0 && (year%100 != 0 || year%400 == 0) } // daysInMonth is the number of days for non-leap years in each calendar month starting at 1 var daysInMonth = [...]int{0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31} func daysIn(m time.Month, year int) int { if m == time.February && isLeap(year) { return 29 } return daysInMonth[int(m)] }