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 002eac2274
commit 58b047e43b
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.
//
// 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
}

View File

@ -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
}