From 0f6ea5004c2668d8a43ca04e4d1eb7d208bd837d Mon Sep 17 00:00:00 2001 From: John Beisley Date: Sat, 10 Jul 2021 18:35:48 +0100 Subject: [PATCH] Complete move to naive date/time types. These types no longer assume the local timezone as a default, instead accepting a time.Location when converting to a time.Time. --- v2/soap/types/types.go | 585 +++++++++++++++++++----------------- v2/soap/types/types_test.go | 106 ++++--- 2 files changed, 376 insertions(+), 315 deletions(-) diff --git a/v2/soap/types/types.go b/v2/soap/types/types.go index fcaf1e4..6983671 100644 --- a/v2/soap/types/types.go +++ b/v2/soap/types/types.go @@ -1,4 +1,9 @@ // Package types defines types that encode values in SOAP requests and responses. +// +// Based on https://openconnectivity.org/upnp-specs/UPnP-arch-DeviceArchitecture-v2.0-20200417.pdf +// pages 58-60. +// +// Date/time formats are based on http://www.w3.org/TR/1998/NOTE-datetime-19980827 package types import ( @@ -14,13 +19,6 @@ import ( "unicode/utf8" ) -var ( - // localLoc acts like time.Local for this package, but is faked out by the - // unit tests to ensure that things stay constant (especially when running - // this test in a place where local time is UTC which might mask bugs). - localLoc = time.Local -) - type SOAPValue interface { Marshal() (string, error) Unmarshal(s string) error @@ -474,87 +472,23 @@ var dateRegexps = []*regexp.Regexp{ regexp.MustCompile(`^(\d{4})(?:(\d{2})(?:(\d{2}))?)?$`), } -var timeRegexps = []*regexp.Regexp{ - // hh[:mm[:ss]] - regexp.MustCompile(`^(\d{2})(?::(\d{2})(?::(\d{2}))?)?$`), - // hh[mm[ss]] - regexp.MustCompile(`^(\d{2})(?:(\d{2})(?:(\d{2}))?)?$`), +type prefixRemainder struct { + prefix string + remainder string } -func parseTimeParts(s string) (TimeOfDay, error) { - var parts []string - for _, re := range timeRegexps { - parts = re.FindStringSubmatch(s) - if parts != nil { - break - } +// prefixUntilAny returns a prefix of the leading string prior to any +// characters in `chars`, and the remainder. If no character from `chars` is +// present in `s`, then returns `s` as `prefix`, and empty remainder. +// +// prefixUntilAny("123/abc", "/") => {"123", "/abc"} +// prefixUntilAny("123", "/") => {"123", ""} +func prefixUntilAny(s string, chars string) prefixRemainder { + i := strings.IndexAny(s, chars) + if i == -1 { + return prefixRemainder{prefix: s, remainder: ""} } - if parts == nil { - return TimeOfDay{}, fmt.Errorf("soap time: value %q is not in ISO8601 time format", s) - } - - var err error - var hour, minute, second int8 - hour = int8(parseInt(parts[1], &err)) - if len(parts[2]) != 0 { - minute = int8(parseInt(parts[2], &err)) - if len(parts[3]) != 0 { - second = int8(parseInt(parts[3], &err)) - } - } - - if err != nil { - return TimeOfDay{}, fmt.Errorf("soap time: %q: %v", s, err) - } - - return TimeOfDay{hour, minute, second}, nil -} - -// (+|-)hh[[:]mm] -var timezoneRegexp = regexp.MustCompile(`^([+-])(\d{2})(?::?(\d{2}))?$`) - -func parseTimezone(s string) (offset int, err error) { - if s == "Z" { - return 0, nil - } - parts := timezoneRegexp.FindStringSubmatch(s) - if parts == nil { - err = fmt.Errorf("soap timezone: value %q is not in ISO8601 timezone format", s) - return - } - - offset = parseInt(parts[2], &err) * 3600 - if len(parts[3]) != 0 { - offset += parseInt(parts[3], &err) * 60 - } - if parts[1] == "-" { - offset = -offset - } - - if err != nil { - err = fmt.Errorf("soap timezone: %q: %v", s, err) - } - - return -} - -var completeDateTimeZoneRegexp = regexp.MustCompile(`^([^T]+)(?:T([^-+Z]+)(.+)?)?$`) - -// splitCompleteDateTimeZone splits date, time and timezone apart from an -// ISO8601 string. It does not ensure that the contents of each part are -// correct, it merely splits on certain delimiters. -// e.g "2010-09-08T12:15:10+0700" => "2010-09-08", "12:15:10", "+0700". -// Timezone can only be present if time is also present. -func splitCompleteDateTimeZone(s string) (dateStr, timeStr, zoneStr string, err error) { - parts := completeDateTimeZoneRegexp.FindStringSubmatch(s) - if parts == nil { - err = fmt.Errorf("soap date/time/zone: value %q is not in ISO8601 datetime format", s) - return - } - dateStr = parts[1] - timeStr = parts[2] - zoneStr = parts[3] - return + return prefixRemainder{prefix: s[:i], remainder: s[i:]} } // TimeOfDay is used in cases where SOAP "time" or "time.tz" is used. @@ -567,135 +501,131 @@ type TimeOfDay struct { var _ SOAPValue = &TimeOfDay{} +func TimeOfDayFromTime(t time.Time) TimeOfDay { + h, m, s := t.Clock() + return TimeOfDay{int8(h), int8(m), int8(s)} +} + +func (tod *TimeOfDay) SetFromTime(t time.Time) { + h, m, s := t.Clock() + tod.Hour = int8(h) + tod.Minute = int8(m) + tod.Second = int8(s) +} + // Sets components based on duration since midnight. -func (v *TimeOfDay) SetFromDuration(d time.Duration) error { +func (tod *TimeOfDay) SetFromDuration(d time.Duration) error { if d < 0 || d > 24*time.Hour { return fmt.Errorf("out of range of SOAP time type: %v", d) } - v.Hour = int8(d / time.Hour) + tod.Hour = int8(d / time.Hour) d = d % time.Hour - v.Minute = int8(d / time.Minute) + tod.Minute = int8(d / time.Minute) d = d % time.Minute - v.Second = int8(d / time.Second) + tod.Second = int8(d / time.Second) return nil } // Returns duration since midnight. -func (v *TimeOfDay) ToDuration() time.Duration { - return time.Duration(v.Hour)*time.Hour + - time.Duration(v.Minute)*time.Minute + - time.Duration(v.Second)*time.Second +func (tod TimeOfDay) ToDuration() time.Duration { + return time.Duration(tod.Hour)*time.Hour + + time.Duration(tod.Minute)*time.Minute + + time.Duration(tod.Second)*time.Second } -func (v *TimeOfDay) String() string { - return fmt.Sprintf("%02d:%02d:%02d", v.Hour, v.Minute, v.Second) -} - -func (v *TimeOfDay) Equal(o *TimeOfDay) bool { - return v.Hour == o.Hour && v.Minute == o.Minute && v.Second == o.Second +func (tod TimeOfDay) String() string { + return fmt.Sprintf("%02d:%02d:%02d", tod.Hour, tod.Minute, tod.Second) } // IsValid returns true iff v is positive and <= 24 hours. // It allows equal to 24 hours as a special case as 24:00:00 is an allowed // value by the SOAP type. -func (v *TimeOfDay) CheckValid() error { - if (v.Hour < 0 || v.Minute < 0 || v.Second < 0) || - (v.Hour == 24 && (v.Minute > 0 || v.Second > 0)) || - v.Hour > 24 || v.Minute >= 60 || v.Second >= 60 { - return fmt.Errorf("soap time: value %v has components(s) out of range", v) +func (tod TimeOfDay) CheckValid() error { + if (tod.Hour < 0 || tod.Minute < 0 || tod.Second < 0) || + (tod.Hour == 24 && (tod.Minute > 0 || tod.Second > 0)) || + tod.Hour > 24 || tod.Minute >= 60 || tod.Second >= 60 { + return fmt.Errorf("soap time: value %v has components(s) out of range", tod) } return nil } -func (v *TimeOfDay) Marshal() (string, error) { - if err := v.CheckValid(); err != nil { +// clear removes data from v, setting to default values. +func (tod *TimeOfDay) clear() { + tod.Hour = 0 + tod.Minute = 0 + tod.Second = 0 +} + +func (tod *TimeOfDay) Marshal() (string, error) { + if err := tod.CheckValid(); err != nil { return "", err } - return v.String(), nil + return tod.String(), nil } -func (v *TimeOfDay) Unmarshal(s string) error { - var err error - *v, err = parseTimeParts(s) - if err != nil { - return err +var timeRegexps = []*regexp.Regexp{ + // hh:mm:ss + regexp.MustCompile(`^(\d{2})(\d{2})(\d{2})$`), + // hhmmss + regexp.MustCompile(`^(\d{2}):(\d{2}):(\d{2})$`), +} + +func (tod *TimeOfDay) Unmarshal(s string) error { + tod.clear() + var parts []string + for _, re := range timeRegexps { + parts = re.FindStringSubmatch(s) + if parts != nil { + break + } + } + if parts == nil { + return fmt.Errorf("value %q is not in ISO8601 time format", s) } - return v.CheckValid() + var err error + tod.Hour = int8(parseInt(parts[1], &err)) + tod.Minute = int8(parseInt(parts[2], &err)) + tod.Second = int8(parseInt(parts[3], &err)) + if err != nil { + return fmt.Errorf("value %q is not in ISO8601 time format: %v", s, err) + } + + return tod.CheckValid() } -// TimeOfDayTZ is used in cases where SOAP "time.tz" is used. +// TimeOfDayTZ maps to the SOAP "time.tz" type. type TimeOfDayTZ struct { // Components of the time of day. TimeOfDay TimeOfDay - // Set to true if Offset is specified. If false, then the timezone is - // unspecified (and by ISO8601 - implies some "local" time). - HasOffset bool - - // Offset is non-zero only if time.tz is used. It is otherwise ignored. If - // non-zero, then it is regarded as a UTC offset in seconds. Note that the - // sub-minutes is ignored by the marshal function. - Offset int + // Timezone designator. + TZ TZD } var _ SOAPValue = &TimeOfDayTZ{} -func (v *TimeOfDayTZ) String() string { - return fmt.Sprintf("%v %t %+03d:%02d:%02d", v.TimeOfDay, v.HasOffset, v.Offset/3600, (v.Offset%3600)/60, v.Offset%60) +func (todz TimeOfDayTZ) String() string { + return fmt.Sprintf("%v%v", todz.TimeOfDay, todz.TZ) } -func (v *TimeOfDayTZ) Equal(o *TimeOfDayTZ) bool { - return v.TimeOfDay.Equal(&o.TimeOfDay) && - v.HasOffset == o.HasOffset && v.Offset == o.Offset +// clear removes data from v, setting to default values. +func (todz *TimeOfDayTZ) clear() { + todz.TimeOfDay.clear() + todz.TZ.clear() } -func (v *TimeOfDayTZ) Marshal() (string, error) { - tod, err := v.TimeOfDay.Marshal() - if err != nil { - return "", err - } - - tz := "" - if v.HasOffset { - if v.Offset == 0 { - tz = "Z" - } else { - offsetMins := v.Offset / 60 - sign := '+' - if offsetMins < 1 { - offsetMins = -offsetMins - sign = '-' - } - tz = fmt.Sprintf("%c%02d:%02d", sign, offsetMins/60, offsetMins%60) - } - } - - return tod + tz, nil +func (todz *TimeOfDayTZ) Marshal() (string, error) { + return todz.String(), nil } -func (v *TimeOfDayTZ) Unmarshal(s string) error { - zoneIndex := strings.IndexAny(s, "Z+-") - var timePart string - if zoneIndex == -1 { - v.HasOffset = false - v.Offset = 0 - timePart = s - } else { - v.HasOffset = true - timePart = s[:zoneIndex] - var err error - v.Offset, err = parseTimezone(s[zoneIndex:]) - if err != nil { - return err - } - } - - if err := v.TimeOfDay.Unmarshal(timePart); err != nil { +func (todz *TimeOfDayTZ) Unmarshal(s string) error { + todz.clear() + parts := prefixUntilAny(s, "Z+-") + if err := todz.TimeOfDay.Unmarshal(parts.prefix); err != nil { return err } - - return nil + return todz.TZ.unmarshal(parts.remainder) } // Date maps to the SOAP "date" type. Marshaling and Unmarshalling does *not* @@ -722,16 +652,16 @@ func (d *Date) SetFromTime(t time.Time) { // ToTime returns a time.Time from the date components, at midnight, and using // the given location. -func (d *Date) ToTime(loc *time.Location) time.Time { +func (d Date) ToTime(loc *time.Location) time.Time { return time.Date(d.Year, d.Month, d.Day, 0, 0, 0, 0, loc) } -func (d *Date) String() string { +func (d Date) String() string { return fmt.Sprintf("%04d-%02d-%02d", d.Year, d.Month, d.Day) } // CheckValid returns an error if the date components are out of range. -func (d *Date) CheckValid() error { +func (d Date) CheckValid() error { y, m, day := d.ToTime(time.UTC).Date() if y != d.Year || m != d.Month || day != d.Day { return fmt.Errorf("SOAP date component(s) out of range in %v", d) @@ -739,11 +669,19 @@ func (d *Date) CheckValid() error { return nil } +// clear removes data from d, setting to default (invalid/zero) values. +func (d *Date) clear() { + d.Year = 0 + d.Month = 0 + d.Day = 0 +} + func (d *Date) Marshal() (string, error) { return d.String(), nil } func (d *Date) Unmarshal(s string) error { + d.clear() var parts []string for _, re := range dateRegexps { parts = re.FindStringSubmatch(s) @@ -773,115 +711,222 @@ func (d *Date) Unmarshal(s string) error { return nil } -// MarshalDateTime maps time.Time to SOAP "dateTime" type, with the local timezone. -type DateTimeLocal time.Time - -var _ SOAPValue = &DateTimeLocal{} - -func NewDateTimeLocal(v time.Time) *DateTimeLocal { - v2 := DateTimeLocal(v) - return &v2 +// DateTime maps to SOAP "dateTime" type. +type DateTime struct { + Date Date + TimeOfDay TimeOfDay } -func (v DateTimeLocal) String() string { - return v.ToTime().String() +var _ SOAPValue = &DateTime{} + +func DateTimeFromTime(v time.Time) DateTime { + dt := DateTime{} + dt.Date.SetFromTime(v) + dt.TimeOfDay.SetFromTime(v) + return dt } -func (v DateTimeLocal) ToTime() time.Time { - return time.Time(v) +func (dt DateTime) String() string { + return fmt.Sprintf("%vT%v", dt.Date, dt.TimeOfDay) } -func (v *DateTimeLocal) Marshal() (string, error) { - return v.ToTime().In(localLoc).Format("2006-01-02T15:04:05"), nil +func (dt DateTime) ToTime(loc *time.Location) time.Time { + return time.Date(dt.Date.Year, dt.Date.Month, dt.Date.Day, + int(dt.TimeOfDay.Hour), int(dt.TimeOfDay.Minute), int(dt.TimeOfDay.Second), 0, + loc) } -func (v *DateTimeLocal) Unmarshal(s string) error { - dateStr, timeStr, zoneStr, err := splitCompleteDateTimeZone(s) +// clear removes data from dt, setting to default values. +func (dt *DateTime) clear() { + dt.Date.clear() + dt.TimeOfDay.clear() +} + +func (dt *DateTime) Marshal() (string, error) { + return dt.String(), nil +} + +func (dt *DateTime) Unmarshal(s string) error { + dt.clear() + parts := prefixUntilAny(s, "T") + if err := dt.Date.Unmarshal(parts.prefix); err != nil { + return err + } + + if parts.remainder == "" { + return nil + } + + if parts.remainder[0] != 'T' { + return fmt.Errorf("missing 'T' time separator in dateTime %q", s) + } + + return dt.TimeOfDay.Unmarshal(parts.remainder[1:]) +} + +// DateTime maps to SOAP type "dateTime.tz". +type DateTimeTZ struct { + Date Date + TimeOfDay TimeOfDay + TZ TZD +} + +var _ SOAPValue = &DateTimeTZ{} + +func DateTimeTZFromTime(t time.Time) DateTimeTZ { + return DateTimeTZ{ + Date: DateFromTime(t), + TimeOfDay: TimeOfDayFromTime(t), + TZ: TZDFromTime(t), + } +} + +func (dtz DateTimeTZ) String() string { + return dtz.Date.String() + "T" + dtz.TimeOfDay.String() + dtz.TZ.String() +} + +// Time converts `dtz` to time.Time, using defaultLoc as the default location if +// `dtz` contains no offset information. +func (dtz DateTimeTZ) Time(defaultLoc *time.Location) time.Time { + return time.Date( + dtz.Date.Year, + dtz.Date.Month, + dtz.Date.Day, + int(dtz.TimeOfDay.Hour), + int(dtz.TimeOfDay.Minute), + int(dtz.TimeOfDay.Second), + 0, + dtz.TZ.Location(defaultLoc), + ) +} + +// clear removes data from dtz, setting to default values. +func (dtz *DateTimeTZ) clear() { + dtz.Date.clear() + dtz.TimeOfDay.clear() + dtz.TZ.clear() +} + +func (dtz *DateTimeTZ) Marshal() (string, error) { + return dtz.String(), nil +} + +func (dtz *DateTimeTZ) Unmarshal(s string) error { + dtz.clear() + dateParts := prefixUntilAny(s, "T") + if err := dtz.Date.Unmarshal(dateParts.prefix); err != nil { + return err + } + + if dateParts.remainder == "" { + return nil + } + + // Trim the leading "T" between date and time. + remainder := dateParts.remainder[1:] + timeParts := prefixUntilAny(remainder, "Z+-") + if err := dtz.TimeOfDay.Unmarshal(timeParts.prefix); err != nil { + return err + } + + if timeParts.remainder == "" { + return nil + } + + return dtz.TZ.unmarshal(timeParts.remainder) +} + +// TZD is a timezone designator. Not a full SOAP time in itself, but used as +// part of timezone-aware date/time types that are. +type TZD struct { + // Offset is the timezone offset in seconds. Note that the SOAP encoding + // only encodes precisions up to minutes, this is in seconds for + // interoperability with time.Time. + Offset int + // HasTZ specifies if a timezone offset is specified or not. + HasTZ bool +} + +func TZDFromTime(t time.Time) TZD { + _, offset := t.Zone() + return TZD{ + Offset: offset, + HasTZ: true, + } +} + +func TZDOffset(secondsOffset int) TZD { + return TZD{ + Offset: secondsOffset, + HasTZ: true, + } +} + +// Location returns an appropriate *time.Location (time.UTC or time.FixedZone), +// or defaultLoc if `v` contains no offset information. +func (tzd TZD) Location(defaultLoc *time.Location) *time.Location { + if !tzd.HasTZ { + return defaultLoc + } + if tzd.Offset == 0 { + return time.UTC + } + return time.FixedZone(tzd.String(), tzd.Offset) +} + +func (tzd TZD) String() string { + if !tzd.HasTZ { + return "" + } + + if tzd.Offset == 0 { + return "Z" + } + + offsetMins := tzd.Offset / 60 + sign := '+' + if offsetMins < 1 { + offsetMins = -offsetMins + sign = '-' + } + h, m := offsetMins/60, offsetMins%60 + return fmt.Sprintf("%c%02d:%02d", sign, h, m) +} + +// clear removes offset information from `v`. +func (tzd *TZD) clear() { + tzd.Offset = 0 + tzd.HasTZ = false +} + +// (+|-)(hh):(mm) +var timezoneRegexp = regexp.MustCompile(`^([+-])(\d{2}):(\d{2})$`) + +func (tzd *TZD) unmarshal(s string) error { + tzd.clear() + if s == "" { + return nil + } + tzd.HasTZ = true + if s == "Z" { + return nil + } + parts := timezoneRegexp.FindStringSubmatch(s) + if parts == nil { + return fmt.Errorf("value %q is not in ISO8601 timezone format", s) + } + + var err error + tzd.Offset = parseInt(parts[2], &err) * 3600 + tzd.Offset += parseInt(parts[3], &err) * 60 + if parts[1] == "-" { + tzd.Offset = -tzd.Offset + } + if err != nil { - return err + err = fmt.Errorf("value %q is not in ISO8601 timezone format: %v", s, err) } - if len(zoneStr) != 0 { - return fmt.Errorf("soap datetime: unexpected timezone in %q", s) - } - - var date Date - if err := date.Unmarshal(dateStr); err != nil { - return err - } - - var tod TimeOfDay - if len(timeStr) != 0 { - tod, err = parseTimeParts(timeStr) - if err != nil { - return err - } - } - - *v = DateTimeLocal(time.Date(date.Year, time.Month(date.Month), date.Day, - int(tod.Hour), int(tod.Minute), int(tod.Second), 0, - localLoc)) - return nil -} - -// DateTimeLocal maps time.Time to SOAP "dateTime.tz" type, using the local -// timezone when one is unspecified. -type DateTimeTZLocal time.Time - -var _ SOAPValue = &DateTimeTZLocal{} - -func NewDateTimeTZLocal(v time.Time) *DateTimeTZLocal { - v2 := DateTimeTZLocal(v) - return &v2 -} - -func (v DateTimeTZLocal) String() string { - return v.ToTime().String() -} - -func (v DateTimeTZLocal) ToTime() time.Time { - return time.Time(v) -} - -func (v *DateTimeTZLocal) Marshal() (string, error) { - return time.Time(*v).Format("2006-01-02T15:04:05-07:00"), nil -} - -func (v *DateTimeTZLocal) Unmarshal(s string) error { - dateStr, timeStr, zoneStr, err := splitCompleteDateTimeZone(s) - if err != nil { - return err - } - - var date Date - if err := date.Unmarshal(dateStr); err != nil { - return err - } - - var tod TimeOfDay - var location *time.Location = localLoc - if len(timeStr) != 0 { - tod, err = parseTimeParts(timeStr) - if err != nil { - return err - } - if len(zoneStr) != 0 { - var offset int - offset, err = parseTimezone(zoneStr) - if err != nil { - return err - } - if offset == 0 { - location = time.UTC - } else { - location = time.FixedZone("", offset) - } - } - } - - *v = DateTimeTZLocal(time.Date(date.Year, time.Month(date.Month), date.Day, - int(tod.Hour), int(tod.Minute), int(tod.Second), 0, - location)) return nil } diff --git a/v2/soap/types/types_test.go b/v2/soap/types/types_test.go index 1255fda..d5246ae 100644 --- a/v2/soap/types/types_test.go +++ b/v2/soap/types/types_test.go @@ -8,6 +8,8 @@ import ( "time" ) +var dummyLoc = time.FixedZone("DummyTZ", 6*3600) + func newFixed14_4Parts(intPart int64, fracPart int16) *Fixed14_4 { v, err := Fixed14_4FromParts(intPart, fracPart) if err != nil { @@ -38,12 +40,6 @@ type unmarshalCase struct { } func Test(t *testing.T) { - // Fake out the local time for the implementation. - localLoc = time.FixedZone("Fake/Local", 6*3600) - defer func() { - localLoc = time.Local - }() - badNumbers := []string{"", " ", "abc"} typeTestCases := []typeTestCase{ @@ -193,7 +189,7 @@ func Test(t *testing.T) { { makeValue: func() SOAPValue { return new(TimeOfDay) }, isEqual: func(got, want SOAPValue) bool { - return got.(*TimeOfDay).Equal(want.(*TimeOfDay)) + return got.(*TimeOfDay).equal(*want.(*TimeOfDay)) }, marshalTests: []marshalCase{ {&TimeOfDay{}, "00:00:00"}, @@ -216,36 +212,29 @@ func Test(t *testing.T) { "00:00:60", // Unexpected timezone component: "01:02:03Z", - "01:02:03+01", "01:02:03+01:23", - "01:02:03+0123", - "01:02:03-01", + "01:02:03+01:23", + "01:02:03-01:23", "01:02:03-01:23", - "01:02:03-0123", }, }, { makeValue: func() SOAPValue { return new(TimeOfDayTZ) }, isEqual: func(got, want SOAPValue) bool { - return got.(*TimeOfDayTZ).Equal(want.(*TimeOfDayTZ)) + return got.(*TimeOfDayTZ).equal(*want.(*TimeOfDayTZ)) }, marshalTests: []marshalCase{ {&TimeOfDayTZ{}, "00:00:00"}, // ISO8601 special case - {&TimeOfDayTZ{TimeOfDay{24, 0, 0}, false, 0}, "24:00:00"}, - {&TimeOfDayTZ{TimeOfDay{1, 2, 3}, true, 0}, "01:02:03Z"}, - {&TimeOfDayTZ{TimeOfDay{1, 2, 3}, true, 3600 + 23*60}, "01:02:03+01:23"}, - {&TimeOfDayTZ{TimeOfDay{1, 2, 3}, true, -(3600 + 23*60)}, "01:02:03-01:23"}, + {&TimeOfDayTZ{TimeOfDay{24, 0, 0}, TZD{}}, "24:00:00"}, + {&TimeOfDayTZ{TimeOfDay{1, 2, 3}, TZDOffset(0)}, "01:02:03Z"}, + {&TimeOfDayTZ{TimeOfDay{1, 2, 3}, TZDOffset(3600 + 23*60)}, "01:02:03+01:23"}, + {&TimeOfDayTZ{TimeOfDay{1, 2, 3}, TZDOffset(-(3600 + 23*60))}, "01:02:03-01:23"}, }, unmarshalTests: []unmarshalCase{ - {"000000", &TimeOfDayTZ{}}, - {"01Z", &TimeOfDayTZ{TimeOfDay{1, 0, 0}, true, 0}}, - {"01+01", &TimeOfDayTZ{TimeOfDay{1, 0, 0}, true, 3600}}, - {"01:02:03+01", &TimeOfDayTZ{TimeOfDay{1, 2, 3}, true, 3600}}, - {"01:02:03+0123", &TimeOfDayTZ{TimeOfDay{1, 2, 3}, true, 3600 + 23*60}}, - {"01:02:03-01", &TimeOfDayTZ{TimeOfDay{1, 2, 3}, true, -3600}}, - {"01:02:03-0123", &TimeOfDayTZ{TimeOfDay{1, 2, 3}, true, -(3600 + 23*60)}}, + {"010203+01:23", &TimeOfDayTZ{TimeOfDay{1, 2, 3}, TZDOffset(3600 + 23*60)}}, + {"010203-01:23", &TimeOfDayTZ{TimeOfDay{1, 2, 3}, TZDOffset(-(3600 + 23*60))}}, }, unmarshalErrs: []string{ // Misformatted values: @@ -275,44 +264,39 @@ func Test(t *testing.T) { }, { - makeValue: func() SOAPValue { return new(DateTimeLocal) }, + makeValue: func() SOAPValue { return new(DateTime) }, isEqual: func(got, want SOAPValue) bool { - return got.(*DateTimeLocal).ToTime().Equal(want.(*DateTimeLocal).ToTime()) + return got.(*DateTime).equal(*want.(*DateTime)) }, marshalTests: []marshalCase{ - {NewDateTimeLocal(time.Date(2013, 10, 8, 0, 0, 0, 0, localLoc)), "2013-10-08T00:00:00"}, - {NewDateTimeLocal(time.Date(2013, 10, 8, 10, 30, 50, 0, localLoc)), "2013-10-08T10:30:50"}, + {DateTimeFromTime(time.Date(2013, 10, 8, 0, 0, 0, 0, dummyLoc)).ptr(), "2013-10-08T00:00:00"}, + {DateTimeFromTime(time.Date(2013, 10, 8, 10, 30, 50, 0, dummyLoc)).ptr(), "2013-10-08T10:30:50"}, }, unmarshalTests: []unmarshalCase{ - {"20131008", NewDateTimeLocal(time.Date(2013, 10, 8, 0, 0, 0, 0, localLoc))}, + {"20131008", DateTimeFromTime(time.Date(2013, 10, 8, 0, 0, 0, 0, dummyLoc)).ptr()}, }, unmarshalErrs: []string{ // Unexpected timezone component. - "2013-10-08T10:30:50+01", + "2013-10-08T10:30:50+01:00", }, }, { - makeValue: func() SOAPValue { return new(DateTimeTZLocal) }, + makeValue: func() SOAPValue { return new(DateTimeTZ) }, isEqual: func(got, want SOAPValue) bool { - return got.(*DateTimeTZLocal).ToTime().Equal(want.(*DateTimeTZLocal).ToTime()) + return got.(*DateTimeTZ).equal(*want.(*DateTimeTZ)) }, marshalTests: []marshalCase{ - {NewDateTimeTZLocal(time.Date(2013, 10, 8, 0, 0, 0, 0, localLoc)), "2013-10-08T00:00:00+06:00"}, - {NewDateTimeTZLocal(time.Date(2013, 10, 8, 10, 30, 50, 0, localLoc)), "2013-10-08T10:30:50+06:00"}, - {NewDateTimeTZLocal(time.Date(2013, 10, 8, 0, 0, 0, 0, time.UTC)), "2013-10-08T00:00:00+00:00"}, - {NewDateTimeTZLocal(time.Date(2013, 10, 8, 10, 30, 50, 0, time.UTC)), "2013-10-08T10:30:50+00:00"}, - {NewDateTimeTZLocal(time.Date(2013, 10, 8, 10, 30, 50, 0, time.FixedZone("+01:23", 3600+23*60))), "2013-10-08T10:30:50+01:23"}, - {NewDateTimeTZLocal(time.Date(2013, 10, 8, 10, 30, 50, 0, time.FixedZone("-01:23", -(3600+23*60)))), "2013-10-08T10:30:50-01:23"}, + {DateTimeTZFromTime(time.Date(2013, 10, 8, 0, 0, 0, 0, dummyLoc)).ptr(), "2013-10-08T00:00:00+06:00"}, + {DateTimeTZFromTime(time.Date(2013, 10, 8, 10, 30, 50, 0, dummyLoc)).ptr(), "2013-10-08T10:30:50+06:00"}, + {DateTimeTZFromTime(time.Date(2013, 10, 8, 0, 0, 0, 0, time.UTC)).ptr(), "2013-10-08T00:00:00Z"}, + {DateTimeTZFromTime(time.Date(2013, 10, 8, 10, 30, 50, 0, time.UTC)).ptr(), "2013-10-08T10:30:50Z"}, + {DateTimeTZFromTime(time.Date(2013, 10, 8, 10, 30, 50, 0, time.FixedZone("+01:23", 3600+23*60))).ptr(), "2013-10-08T10:30:50+01:23"}, + {DateTimeTZFromTime(time.Date(2013, 10, 8, 10, 30, 50, 0, time.FixedZone("-01:23", -(3600+23*60)))).ptr(), "2013-10-08T10:30:50-01:23"}, }, unmarshalTests: []unmarshalCase{ - {"20131008", NewDateTimeTZLocal(time.Date(2013, 10, 8, 0, 0, 0, 0, localLoc))}, - {"2013-10-08T10:30:50", NewDateTimeTZLocal(time.Date(2013, 10, 8, 10, 30, 50, 0, localLoc))}, - {"2013-10-08T10:30:50Z", NewDateTimeTZLocal(time.Date(2013, 10, 8, 10, 30, 50, 0, time.UTC))}, - {"2013-10-08T10:30:50+01", NewDateTimeTZLocal(time.Date(2013, 10, 8, 10, 30, 50, 0, time.FixedZone("+01:00", 3600)))}, - {"2013-10-08T10:30:50+0123", NewDateTimeTZLocal(time.Date(2013, 10, 8, 10, 30, 50, 0, time.FixedZone("+01:23", 3600+23*60)))}, - {"2013-10-08T10:30:50-01", NewDateTimeTZLocal(time.Date(2013, 10, 8, 10, 30, 50, 0, time.FixedZone("-01:00", -3600)))}, - {"2013-10-08T10:30:50-0123", NewDateTimeTZLocal(time.Date(2013, 10, 8, 10, 30, 50, 0, time.FixedZone("-01:23", -(3600+23*60))))}, + {"2013-10-08T10:30:50", &DateTimeTZ{Date{2013, 10, 8}, TimeOfDay{10, 30, 50}, TZD{}}}, + {"2013-10-08T10:30:50+00:00", DateTimeTZFromTime(time.Date(2013, 10, 8, 10, 30, 50, 0, time.UTC)).ptr()}, }, }, @@ -411,7 +395,7 @@ func Test(t *testing.T) { t.Run(fmt.Sprintf("unmarshalTest#%d_%q", i, ut.input), func(t *testing.T) { got := tt.makeValue() if err := got.Unmarshal(ut.input); err != nil { - t.Errorf("got error, want success") + t.Errorf("got unexpected error: %v", err) } if !tt.isEqual(got, ut.want) { t.Errorf("got %v, want %v", got, ut.want) @@ -518,3 +502,35 @@ func TestFixed14_4(t *testing.T) { } }) } + +// methods only used in testing: + +func (v TimeOfDay) equal(o TimeOfDay) bool { + return v.Hour == o.Hour && v.Minute == o.Minute && v.Second == o.Second +} + +func (v TimeOfDayTZ) equal(o TimeOfDayTZ) bool { + return v.TimeOfDay.equal(o.TimeOfDay) && v.TZ.equal(o.TZ) +} + +func (d Date) equal(o Date) bool { + return d.Year == o.Year && d.Month == o.Month && d.Day == o.Day +} + +func (dtz DateTime) ptr() *DateTime { return &dtz } + +func (dt DateTime) equal(o DateTime) bool { + return dt.Date.equal(o.Date) && dt.TimeOfDay.equal(o.TimeOfDay) +} + +func (dtz DateTimeTZ) ptr() *DateTimeTZ { return &dtz } + +func (dtz DateTimeTZ) equal(o DateTimeTZ) bool { + return dtz.Date.equal(o.Date) && + dtz.TimeOfDay.equal(o.TimeOfDay) && + dtz.TZ.equal(o.TZ) +} + +func (tzd TZD) equal(o TZD) bool { + return tzd.Offset == o.Offset && tzd.HasTZ == o.HasTZ +}