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.
This commit is contained in:
John Beisley 2021-07-10 18:35:48 +01:00 committed by Huin
parent 500ae47278
commit 0f6ea5004c
2 changed files with 376 additions and 315 deletions

View File

@ -1,4 +1,9 @@
// Package types defines types that encode values in SOAP requests and responses. // 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 package types
import ( import (
@ -14,13 +19,6 @@ import (
"unicode/utf8" "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 { type SOAPValue interface {
Marshal() (string, error) Marshal() (string, error)
Unmarshal(s string) error Unmarshal(s string) error
@ -474,87 +472,23 @@ var dateRegexps = []*regexp.Regexp{
regexp.MustCompile(`^(\d{4})(?:(\d{2})(?:(\d{2}))?)?$`), regexp.MustCompile(`^(\d{4})(?:(\d{2})(?:(\d{2}))?)?$`),
} }
var timeRegexps = []*regexp.Regexp{ type prefixRemainder struct {
// hh[:mm[:ss]] prefix string
regexp.MustCompile(`^(\d{2})(?::(\d{2})(?::(\d{2}))?)?$`), remainder string
// hh[mm[ss]]
regexp.MustCompile(`^(\d{2})(?:(\d{2})(?:(\d{2}))?)?$`),
} }
func parseTimeParts(s string) (TimeOfDay, error) { // prefixUntilAny returns a prefix of the leading string prior to any
var parts []string // characters in `chars`, and the remainder. If no character from `chars` is
for _, re := range timeRegexps { // present in `s`, then returns `s` as `prefix`, and empty remainder.
parts = re.FindStringSubmatch(s) //
if parts != nil { // prefixUntilAny("123/abc", "/") => {"123", "/abc"}
break // prefixUntilAny("123", "/") => {"123", ""}
func prefixUntilAny(s string, chars string) prefixRemainder {
i := strings.IndexAny(s, chars)
if i == -1 {
return prefixRemainder{prefix: s, remainder: ""}
} }
} return prefixRemainder{prefix: s[:i], remainder: s[i:]}
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
} }
// TimeOfDay is used in cases where SOAP "time" or "time.tz" is used. // TimeOfDay is used in cases where SOAP "time" or "time.tz" is used.
@ -567,135 +501,131 @@ type TimeOfDay struct {
var _ SOAPValue = &TimeOfDay{} 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. // 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 { if d < 0 || d > 24*time.Hour {
return fmt.Errorf("out of range of SOAP time type: %v", d) 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 d = d % time.Hour
v.Minute = int8(d / time.Minute) tod.Minute = int8(d / time.Minute)
d = d % time.Minute d = d % time.Minute
v.Second = int8(d / time.Second) tod.Second = int8(d / time.Second)
return nil return nil
} }
// Returns duration since midnight. // Returns duration since midnight.
func (v *TimeOfDay) ToDuration() time.Duration { func (tod TimeOfDay) ToDuration() time.Duration {
return time.Duration(v.Hour)*time.Hour + return time.Duration(tod.Hour)*time.Hour +
time.Duration(v.Minute)*time.Minute + time.Duration(tod.Minute)*time.Minute +
time.Duration(v.Second)*time.Second time.Duration(tod.Second)*time.Second
} }
func (v *TimeOfDay) String() string { func (tod TimeOfDay) String() string {
return fmt.Sprintf("%02d:%02d:%02d", v.Hour, v.Minute, v.Second) return fmt.Sprintf("%02d:%02d:%02d", tod.Hour, tod.Minute, tod.Second)
}
func (v *TimeOfDay) Equal(o *TimeOfDay) bool {
return v.Hour == o.Hour && v.Minute == o.Minute && v.Second == o.Second
} }
// IsValid returns true iff v is positive and <= 24 hours. // 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 // It allows equal to 24 hours as a special case as 24:00:00 is an allowed
// value by the SOAP type. // value by the SOAP type.
func (v *TimeOfDay) CheckValid() error { func (tod TimeOfDay) CheckValid() error {
if (v.Hour < 0 || v.Minute < 0 || v.Second < 0) || if (tod.Hour < 0 || tod.Minute < 0 || tod.Second < 0) ||
(v.Hour == 24 && (v.Minute > 0 || v.Second > 0)) || (tod.Hour == 24 && (tod.Minute > 0 || tod.Second > 0)) ||
v.Hour > 24 || v.Minute >= 60 || v.Second >= 60 { tod.Hour > 24 || tod.Minute >= 60 || tod.Second >= 60 {
return fmt.Errorf("soap time: value %v has components(s) out of range", v) return fmt.Errorf("soap time: value %v has components(s) out of range", tod)
} }
return nil return nil
} }
func (v *TimeOfDay) Marshal() (string, error) { // clear removes data from v, setting to default values.
if err := v.CheckValid(); err != nil { 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 "", err
} }
return v.String(), nil return tod.String(), nil
}
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)
} }
func (v *TimeOfDay) Unmarshal(s string) error {
var err error var err error
*v, err = parseTimeParts(s) tod.Hour = int8(parseInt(parts[1], &err))
tod.Minute = int8(parseInt(parts[2], &err))
tod.Second = int8(parseInt(parts[3], &err))
if err != nil { if err != nil {
return err return fmt.Errorf("value %q is not in ISO8601 time format: %v", s, err)
} }
return v.CheckValid() 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 { type TimeOfDayTZ struct {
// Components of the time of day. // Components of the time of day.
TimeOfDay TimeOfDay TimeOfDay TimeOfDay
// Set to true if Offset is specified. If false, then the timezone is // Timezone designator.
// unspecified (and by ISO8601 - implies some "local" time). TZ TZD
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
} }
var _ SOAPValue = &TimeOfDayTZ{} var _ SOAPValue = &TimeOfDayTZ{}
func (v *TimeOfDayTZ) String() string { func (todz 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) return fmt.Sprintf("%v%v", todz.TimeOfDay, todz.TZ)
} }
func (v *TimeOfDayTZ) Equal(o *TimeOfDayTZ) bool { // clear removes data from v, setting to default values.
return v.TimeOfDay.Equal(&o.TimeOfDay) && func (todz *TimeOfDayTZ) clear() {
v.HasOffset == o.HasOffset && v.Offset == o.Offset todz.TimeOfDay.clear()
todz.TZ.clear()
} }
func (v *TimeOfDayTZ) Marshal() (string, error) { func (todz *TimeOfDayTZ) Marshal() (string, error) {
tod, err := v.TimeOfDay.Marshal() return todz.String(), nil
if err != nil {
return "", err
} }
tz := "" func (todz *TimeOfDayTZ) Unmarshal(s string) error {
if v.HasOffset { todz.clear()
if v.Offset == 0 { parts := prefixUntilAny(s, "Z+-")
tz = "Z" if err := todz.TimeOfDay.Unmarshal(parts.prefix); err != nil {
} 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 (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 return err
} }
} return todz.TZ.unmarshal(parts.remainder)
if err := v.TimeOfDay.Unmarshal(timePart); err != nil {
return err
}
return nil
} }
// Date maps to the SOAP "date" type. Marshaling and Unmarshalling does *not* // 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 // ToTime returns a time.Time from the date components, at midnight, and using
// the given location. // 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) 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) return fmt.Sprintf("%04d-%02d-%02d", d.Year, d.Month, d.Day)
} }
// CheckValid returns an error if the date components are out of range. // 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() y, m, day := d.ToTime(time.UTC).Date()
if y != d.Year || m != d.Month || day != d.Day { if y != d.Year || m != d.Month || day != d.Day {
return fmt.Errorf("SOAP date component(s) out of range in %v", d) return fmt.Errorf("SOAP date component(s) out of range in %v", d)
@ -739,11 +669,19 @@ func (d *Date) CheckValid() error {
return nil 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) { func (d *Date) Marshal() (string, error) {
return d.String(), nil return d.String(), nil
} }
func (d *Date) Unmarshal(s string) error { func (d *Date) Unmarshal(s string) error {
d.clear()
var parts []string var parts []string
for _, re := range dateRegexps { for _, re := range dateRegexps {
parts = re.FindStringSubmatch(s) parts = re.FindStringSubmatch(s)
@ -773,115 +711,222 @@ func (d *Date) Unmarshal(s string) error {
return nil return nil
} }
// MarshalDateTime maps time.Time to SOAP "dateTime" type, with the local timezone. // DateTime maps to SOAP "dateTime" type.
type DateTimeLocal time.Time type DateTime struct {
Date Date
var _ SOAPValue = &DateTimeLocal{} TimeOfDay TimeOfDay
func NewDateTimeLocal(v time.Time) *DateTimeLocal {
v2 := DateTimeLocal(v)
return &v2
} }
func (v DateTimeLocal) String() string { var _ SOAPValue = &DateTime{}
return v.ToTime().String()
func DateTimeFromTime(v time.Time) DateTime {
dt := DateTime{}
dt.Date.SetFromTime(v)
dt.TimeOfDay.SetFromTime(v)
return dt
} }
func (v DateTimeLocal) ToTime() time.Time { func (dt DateTime) String() string {
return time.Time(v) return fmt.Sprintf("%vT%v", dt.Date, dt.TimeOfDay)
} }
func (v *DateTimeLocal) Marshal() (string, error) { func (dt DateTime) ToTime(loc *time.Location) time.Time {
return v.ToTime().In(localLoc).Format("2006-01-02T15:04:05"), nil 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 { // clear removes data from dt, setting to default values.
dateStr, timeStr, zoneStr, err := splitCompleteDateTimeZone(s) func (dt *DateTime) clear() {
if err != nil { 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 return err
} }
if len(zoneStr) != 0 { if parts.remainder == "" {
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 return nil
} }
// DateTimeLocal maps time.Time to SOAP "dateTime.tz" type, using the local if parts.remainder[0] != 'T' {
// timezone when one is unspecified. return fmt.Errorf("missing 'T' time separator in dateTime %q", s)
type DateTimeTZLocal time.Time
var _ SOAPValue = &DateTimeTZLocal{}
func NewDateTimeTZLocal(v time.Time) *DateTimeTZLocal {
v2 := DateTimeTZLocal(v)
return &v2
} }
func (v DateTimeTZLocal) String() string { return dt.TimeOfDay.Unmarshal(parts.remainder[1:])
return v.ToTime().String()
} }
func (v DateTimeTZLocal) ToTime() time.Time { // DateTime maps to SOAP type "dateTime.tz".
return time.Time(v) type DateTimeTZ struct {
Date Date
TimeOfDay TimeOfDay
TZ TZD
} }
func (v *DateTimeTZLocal) Marshal() (string, error) { var _ SOAPValue = &DateTimeTZ{}
return time.Time(*v).Format("2006-01-02T15:04:05-07:00"), nil
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
} }
func (v *DateTimeTZLocal) Unmarshal(s string) error {
dateStr, timeStr, zoneStr, err := splitCompleteDateTimeZone(s)
if err != nil { if err != nil {
return err err = fmt.Errorf("value %q is not in ISO8601 timezone format: %v", s, 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 return nil
} }

View File

@ -8,6 +8,8 @@ import (
"time" "time"
) )
var dummyLoc = time.FixedZone("DummyTZ", 6*3600)
func newFixed14_4Parts(intPart int64, fracPart int16) *Fixed14_4 { func newFixed14_4Parts(intPart int64, fracPart int16) *Fixed14_4 {
v, err := Fixed14_4FromParts(intPart, fracPart) v, err := Fixed14_4FromParts(intPart, fracPart)
if err != nil { if err != nil {
@ -38,12 +40,6 @@ type unmarshalCase struct {
} }
func Test(t *testing.T) { 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"} badNumbers := []string{"", " ", "abc"}
typeTestCases := []typeTestCase{ typeTestCases := []typeTestCase{
@ -193,7 +189,7 @@ func Test(t *testing.T) {
{ {
makeValue: func() SOAPValue { return new(TimeOfDay) }, makeValue: func() SOAPValue { return new(TimeOfDay) },
isEqual: func(got, want SOAPValue) bool { isEqual: func(got, want SOAPValue) bool {
return got.(*TimeOfDay).Equal(want.(*TimeOfDay)) return got.(*TimeOfDay).equal(*want.(*TimeOfDay))
}, },
marshalTests: []marshalCase{ marshalTests: []marshalCase{
{&TimeOfDay{}, "00:00:00"}, {&TimeOfDay{}, "00:00:00"},
@ -216,36 +212,29 @@ func Test(t *testing.T) {
"00:00:60", "00:00:60",
// Unexpected timezone component: // Unexpected timezone component:
"01:02:03Z", "01:02:03Z",
"01:02:03+01",
"01:02:03+01:23", "01:02:03+01:23",
"01:02:03+0123", "01:02:03+01:23",
"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) }, makeValue: func() SOAPValue { return new(TimeOfDayTZ) },
isEqual: func(got, want SOAPValue) bool { isEqual: func(got, want SOAPValue) bool {
return got.(*TimeOfDayTZ).Equal(want.(*TimeOfDayTZ)) return got.(*TimeOfDayTZ).equal(*want.(*TimeOfDayTZ))
}, },
marshalTests: []marshalCase{ marshalTests: []marshalCase{
{&TimeOfDayTZ{}, "00:00:00"}, {&TimeOfDayTZ{}, "00:00:00"},
// ISO8601 special case // ISO8601 special case
{&TimeOfDayTZ{TimeOfDay{24, 0, 0}, false, 0}, "24:00:00"}, {&TimeOfDayTZ{TimeOfDay{24, 0, 0}, TZD{}}, "24:00:00"},
{&TimeOfDayTZ{TimeOfDay{1, 2, 3}, true, 0}, "01:02:03Z"}, {&TimeOfDayTZ{TimeOfDay{1, 2, 3}, TZDOffset(0)}, "01:02:03Z"},
{&TimeOfDayTZ{TimeOfDay{1, 2, 3}, true, 3600 + 23*60}, "01:02:03+01:23"}, {&TimeOfDayTZ{TimeOfDay{1, 2, 3}, TZDOffset(3600 + 23*60)}, "01:02:03+01:23"},
{&TimeOfDayTZ{TimeOfDay{1, 2, 3}, true, -(3600 + 23*60)}, "01:02:03-01:23"}, {&TimeOfDayTZ{TimeOfDay{1, 2, 3}, TZDOffset(-(3600 + 23*60))}, "01:02:03-01:23"},
}, },
unmarshalTests: []unmarshalCase{ unmarshalTests: []unmarshalCase{
{"000000", &TimeOfDayTZ{}}, {"010203+01:23", &TimeOfDayTZ{TimeOfDay{1, 2, 3}, TZDOffset(3600 + 23*60)}},
{"01Z", &TimeOfDayTZ{TimeOfDay{1, 0, 0}, true, 0}}, {"010203-01:23", &TimeOfDayTZ{TimeOfDay{1, 2, 3}, TZDOffset(-(3600 + 23*60))}},
{"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)}},
}, },
unmarshalErrs: []string{ unmarshalErrs: []string{
// Misformatted values: // 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 { isEqual: func(got, want SOAPValue) bool {
return got.(*DateTimeLocal).ToTime().Equal(want.(*DateTimeLocal).ToTime()) return got.(*DateTime).equal(*want.(*DateTime))
}, },
marshalTests: []marshalCase{ marshalTests: []marshalCase{
{NewDateTimeLocal(time.Date(2013, 10, 8, 0, 0, 0, 0, localLoc)), "2013-10-08T00:00:00"}, {DateTimeFromTime(time.Date(2013, 10, 8, 0, 0, 0, 0, dummyLoc)).ptr(), "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, 10, 30, 50, 0, dummyLoc)).ptr(), "2013-10-08T10:30:50"},
}, },
unmarshalTests: []unmarshalCase{ 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{ unmarshalErrs: []string{
// Unexpected timezone component. // 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 { isEqual: func(got, want SOAPValue) bool {
return got.(*DateTimeTZLocal).ToTime().Equal(want.(*DateTimeTZLocal).ToTime()) return got.(*DateTimeTZ).equal(*want.(*DateTimeTZ))
}, },
marshalTests: []marshalCase{ marshalTests: []marshalCase{
{NewDateTimeTZLocal(time.Date(2013, 10, 8, 0, 0, 0, 0, localLoc)), "2013-10-08T00:00:00+06:00"}, {DateTimeTZFromTime(time.Date(2013, 10, 8, 0, 0, 0, 0, dummyLoc)).ptr(), "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"}, {DateTimeTZFromTime(time.Date(2013, 10, 8, 10, 30, 50, 0, dummyLoc)).ptr(), "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"}, {DateTimeTZFromTime(time.Date(2013, 10, 8, 0, 0, 0, 0, time.UTC)).ptr(), "2013-10-08T00:00:00Z"},
{NewDateTimeTZLocal(time.Date(2013, 10, 8, 10, 30, 50, 0, time.UTC)), "2013-10-08T10:30:50+00:00"}, {DateTimeTZFromTime(time.Date(2013, 10, 8, 10, 30, 50, 0, time.UTC)).ptr(), "2013-10-08T10:30:50Z"},
{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, 10, 30, 50, 0, time.FixedZone("+01:23", 3600+23*60))).ptr(), "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, 10, 30, 50, 0, time.FixedZone("-01:23", -(3600+23*60)))).ptr(), "2013-10-08T10:30:50-01:23"},
}, },
unmarshalTests: []unmarshalCase{ unmarshalTests: []unmarshalCase{
{"20131008", NewDateTimeTZLocal(time.Date(2013, 10, 8, 0, 0, 0, 0, localLoc))}, {"2013-10-08T10:30:50", &DateTimeTZ{Date{2013, 10, 8}, TimeOfDay{10, 30, 50}, TZD{}}},
{"2013-10-08T10:30:50", NewDateTimeTZLocal(time.Date(2013, 10, 8, 10, 30, 50, 0, localLoc))}, {"2013-10-08T10:30:50+00:00", DateTimeTZFromTime(time.Date(2013, 10, 8, 10, 30, 50, 0, time.UTC)).ptr()},
{"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))))},
}, },
}, },
@ -411,7 +395,7 @@ func Test(t *testing.T) {
t.Run(fmt.Sprintf("unmarshalTest#%d_%q", i, ut.input), func(t *testing.T) { t.Run(fmt.Sprintf("unmarshalTest#%d_%q", i, ut.input), func(t *testing.T) {
got := tt.makeValue() got := tt.makeValue()
if err := got.Unmarshal(ut.input); err != nil { 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) { if !tt.isEqual(got, ut.want) {
t.Errorf("got %v, want %v", 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
}