Add marshalling for dateTime.tz type.

This commit is contained in:
John Beisley 2013-10-27 19:08:31 +00:00
parent de724897db
commit ecf830777a
2 changed files with 259 additions and 63 deletions

View File

@ -57,6 +57,133 @@ func UnmarshalChar(s string) (rune, error) {
return r, nil return r, nil
} }
func parseInt(s string, err *error) int {
v, parseErr := strconv.ParseInt(s, 10, 64)
if parseErr != nil {
*err = parseErr
}
return int(v)
}
var dateRegexps = []*regexp.Regexp{
// yyyy[-mm[-dd]]
regexp.MustCompile(`^(\d{4})(?:-(\d{2})(?:-(\d{2}))?)?$`),
// yyyy[mm[dd]]
regexp.MustCompile(`^(\d{4})(?:(\d{2})(?:(\d{2}))?)?$`),
}
func parseDateParts(s string) (year, month, day int, err error) {
var parts []string
for _, re := range dateRegexps {
parts = re.FindStringSubmatch(s)
if parts != nil {
break
}
}
if parts == nil {
err = fmt.Errorf("soap date: value %q is not in a recognized ISO8601 date format", s)
return
}
year = parseInt(parts[1], &err)
month = 1
day = 1
if len(parts[2]) != 0 {
month = parseInt(parts[2], &err)
if len(parts[3]) != 0 {
day = parseInt(parts[3], &err)
}
}
if err != nil {
err = fmt.Errorf("soap date: %q: %v", s, err)
}
return
}
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}))?)?$`),
}
func parseTimeParts(s string) (hour, minute, second int, err error) {
var parts []string
for _, re := range timeRegexps {
parts = re.FindStringSubmatch(s)
if parts != nil {
break
}
}
if parts == nil {
err = fmt.Errorf("soap time: value %q is not in ISO8601 time format", s)
return
}
hour = parseInt(parts[1], &err)
if len(parts[2]) != 0 {
minute = parseInt(parts[2], &err)
if len(parts[3]) != 0 {
second = parseInt(parts[3], &err)
}
}
if err != nil {
err = fmt.Errorf("soap time: %q: %v", s, err)
}
return
}
// (+|-)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
}
// MarshalDate marshals time.Time to SOAP "date" type. Note that this converts // MarshalDate marshals time.Time to SOAP "date" type. Note that this converts
// to local time, and discards the time-of-day components. // to local time, and discards the time-of-day components.
func MarshalDate(v time.Time) (string, error) { func MarshalDate(v time.Time) (string, error) {
@ -68,12 +195,11 @@ var dateFmts = []string{"2006-01-02", "20060102"}
// UnmarshalDate unmarshals time.Time from SOAP "date" type. This outputs the // UnmarshalDate unmarshals time.Time from SOAP "date" type. This outputs the
// date as midnight in the local time zone. // date as midnight in the local time zone.
func UnmarshalDate(s string) (time.Time, error) { func UnmarshalDate(s string) (time.Time, error) {
for _, f := range dateFmts { year, month, day, err := parseDateParts(s)
if t, err := time.ParseInLocation(f, s, localLoc); err == nil { if err != nil {
return t, nil return time.Time{}, err
}
} }
return time.Time{}, fmt.Errorf("soap date: value %q is not in a recognized date format", s) return time.Date(year, time.Month(month), day, 0, 0, 0, 0, localLoc), nil
} }
// 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.
@ -88,7 +214,7 @@ type TimeOfDay struct {
// Offset is non-zero only if time.tz is used. It is otherwise ignored. If // 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 // non-zero, then it is regarded as a UTC offset in seconds. Note that the
// sub-minutes is ignored by the marshal function. // sub-minutes is ignored by the marshal function.
Offset int16 Offset int
} }
// MarshalTimeOfDay marshals TimeOfDay to the "time" type. // MarshalTimeOfDay marshals TimeOfDay to the "time" type.
@ -139,24 +265,27 @@ func MarshalTimeOfDayTz(v TimeOfDay) (string, error) {
return fmt.Sprintf("%02d:%02d:%02d%s", hour, minute, second, tz), nil return fmt.Sprintf("%02d:%02d:%02d%s", hour, minute, second, tz), nil
} }
var timeRegexp = regexp.MustCompile(
`^(\d\d)(?::?(\d\d)(?::?(\d\d))?)?` + // hh[:mm[:ss]]
`(?:(Z)|([+-])(\d\d)(?::?(\d\d))?)?$`) // Z | ±hh[:mm]
// UnmarshalTimeOfDayTz unmarshals TimeOfDay from the "time.tz" type. // UnmarshalTimeOfDayTz unmarshals TimeOfDay from the "time.tz" type.
func UnmarshalTimeOfDayTz(s string) (TimeOfDay, error) { func UnmarshalTimeOfDayTz(s string) (tod TimeOfDay, err error) {
parts := timeRegexp.FindStringSubmatch(s) zoneIndex := strings.IndexAny(s, "Z+-")
if parts == nil { var timePart string
return TimeOfDay{}, fmt.Errorf("soap time.tz: value %q is not in ISO8601 time format", s) var hasOffset bool
var offset int
if zoneIndex == -1 {
hasOffset = false
timePart = s
} else {
hasOffset = true
timePart = s[:zoneIndex]
if offset, err = parseTimezone(s[zoneIndex:]); err != nil {
return
}
} }
// HH:MM:SS parsing. hour, minute, second, err := parseTimeParts(timePart)
parts = parts[1:] if err != nil {
var iParts [3]int64 return
for i, pStr := range parts[:3] {
iParts[i], _ = strconv.ParseInt(pStr, 10, 64)
} }
hour, minute, second := iParts[0], iParts[1], iParts[2]
fromMidnight := time.Duration(hour*3600+minute*60+second) * time.Second fromMidnight := time.Duration(hour*3600+minute*60+second) * time.Second
@ -166,51 +295,85 @@ func UnmarshalTimeOfDayTz(s string) (TimeOfDay, error) {
return TimeOfDay{}, fmt.Errorf("soap time.tz: value %q has value(s) out of range", s) return TimeOfDay{}, fmt.Errorf("soap time.tz: value %q has value(s) out of range", s)
} }
// Timezone offset parsing.
hasOffset := false
var offset int64
if parts[3] == "Z" {
hasOffset = true
offset = 0
} else if parts[4] != "" {
hasOffset = true
hours, _ := strconv.ParseInt(parts[5], 10, 64)
var mins int64
if parts[6] != "" {
mins, _ = strconv.ParseInt(parts[6], 10, 64)
}
offset = hours*3600 + mins*60
if parts[4] == "-" {
offset = -offset
}
}
return TimeOfDay{ return TimeOfDay{
FromMidnight: time.Duration(hour*3600+minute*60+second) * time.Second, FromMidnight: time.Duration(hour*3600+minute*60+second) * time.Second,
HasOffset: hasOffset, HasOffset: hasOffset,
Offset: int16(offset), Offset: offset,
}, nil }, nil
} }
// MarshalDatetime marshals time.Time to SOAP "date" type. Note that this // MarshalDateTime marshals time.Time to SOAP "dateTime" type. Note that this
// converts to local time. // converts to local time.
func MarshalDatetime(v time.Time) (string, error) { func MarshalDateTime(v time.Time) (string, error) {
return v.In(localLoc).Format("2006-01-02T15:04:05"), nil return v.In(localLoc).Format("2006-01-02T15:04:05"), nil
} }
// UnmarshalDatetime unmarshals time.Time from the SOAP "dateTime" type. This // UnmarshalDateTime unmarshals time.Time from the SOAP "dateTime" type. This
// returns a value in the local timezone. // returns a value in the local timezone.
func UnmarshalDatetime(s string) (time.Time, error) { func UnmarshalDateTime(s string) (result time.Time, err error) {
parts := strings.SplitN(s, "T", 2) dateStr, timeStr, zoneStr, err := splitCompleteDateTimeZone(s)
datePart, err := UnmarshalDate(parts[0])
if err != nil { if err != nil {
return time.Time{}, err return
} }
if len(parts) == 2 {
timePart, err := UnmarshalTimeOfDay(parts[1]) if len(zoneStr) != 0 {
err = fmt.Errorf("soap datetime: unexpected timezone in %q", s)
return
}
year, month, day, err := parseDateParts(dateStr)
if err != nil {
return
}
var hour, minute, second int
if len(timeStr) != 0 {
hour, minute, second, err = parseTimeParts(timeStr)
if err != nil { if err != nil {
return time.Time{}, err return
} }
datePart = datePart.Add(timePart.FromMidnight)
} }
return datePart, nil
result = time.Date(year, time.Month(month), day, hour, minute, second, 0, localLoc)
return
}
// MarshalDateTimeTz marshals time.Time to SOAP "dateTime.tz" type.
func MarshalDateTimeTz(v time.Time) (string, error) {
return v.Format("2006-01-02T15:04:05-07:00"), nil
}
// UnmarshalDateTimeTz unmarshals time.Time from the SOAP "dateTime.tz" type.
// This returns a value in the local timezone when the timezone is unspecified.
func UnmarshalDateTimeTz(s string) (result time.Time, err error) {
dateStr, timeStr, zoneStr, err := splitCompleteDateTimeZone(s)
if err != nil {
return
}
year, month, day, err := parseDateParts(dateStr)
if err != nil {
return
}
var hour, minute, second int
var location *time.Location = localLoc
if len(timeStr) != 0 {
hour, minute, second, err = parseTimeParts(timeStr)
if err != nil {
return
}
if len(zoneStr) != 0 {
var offset int
offset, err = parseTimezone(zoneStr)
if offset == 0 {
location = time.UTC
} else {
location = time.FixedZone("", offset)
}
}
}
result = time.Date(year, time.Month(month), day, hour, minute, second, 0, location)
return
} }

View File

@ -65,7 +65,7 @@ func (v DateTest) Equal(result interface{}) bool {
} }
func (v DateTest) Dupe(tag string) convTest { func (v DateTest) Dupe(tag string) convTest {
if tag != "no:dateTime" { if tag != "no:dateTime" {
return DatetimeTest{v.Time} return DateTimeTest{v.Time}
} }
return nil return nil
} }
@ -104,15 +104,33 @@ func (v TimeOfDayTzTest) Equal(result interface{}) bool {
return v.TimeOfDay == result.(TimeOfDay) return v.TimeOfDay == result.(TimeOfDay)
} }
type DatetimeTest struct{ time.Time } type DateTimeTest struct{ time.Time }
func (v DatetimeTest) Marshal() (string, error) { func (v DateTimeTest) Marshal() (string, error) {
return MarshalDatetime(time.Time(v.Time)) return MarshalDateTime(time.Time(v.Time))
} }
func (v DatetimeTest) Unmarshal(s string) (interface{}, error) { func (v DateTimeTest) Unmarshal(s string) (interface{}, error) {
return UnmarshalDatetime(s) return UnmarshalDateTime(s)
} }
func (v DatetimeTest) Equal(result interface{}) bool { func (v DateTimeTest) Equal(result interface{}) bool {
return v.Time.Equal(result.(time.Time))
}
func (v DateTimeTest) Dupe(tag string) convTest {
if tag != "no:dateTime.tz" {
return DateTimeTzTest{v.Time}
}
return nil
}
type DateTimeTzTest struct{ time.Time }
func (v DateTimeTzTest) Marshal() (string, error) {
return MarshalDateTimeTz(time.Time(v.Time))
}
func (v DateTimeTzTest) Unmarshal(s string) (interface{}, error) {
return UnmarshalDateTimeTz(s)
}
func (v DateTimeTzTest) Equal(result interface{}) bool {
return v.Time.Equal(result.(time.Time)) return v.Time.Equal(result.(time.Time))
} }
@ -195,11 +213,26 @@ func Test(t *testing.T) {
{str: "01:02:03-01:23", value: TimeOfDayTzTest{TimeOfDay{time010203, true, -(3600 + 23*60)}}}, {str: "01:02:03-01:23", value: TimeOfDayTzTest{TimeOfDay{time010203, true, -(3600 + 23*60)}}},
{str: "01:02:03-0123", value: TimeOfDayTzTest{TimeOfDay{time010203, true, -(3600 + 23*60)}}, noMarshal: true}, {str: "01:02:03-0123", value: TimeOfDayTzTest{TimeOfDay{time010203, true, -(3600 + 23*60)}}, noMarshal: true},
// datetime // dateTime
{str: "2013-10-08T00:00:00", value: DatetimeTest{time.Date(2013, 10, 8, 0, 0, 0, 0, localLoc)}}, {str: "2013-10-08T00:00:00", value: DateTimeTest{time.Date(2013, 10, 8, 0, 0, 0, 0, localLoc)}, tag: "no:dateTime.tz"},
{str: "20131008", value: DatetimeTest{time.Date(2013, 10, 8, 0, 0, 0, 0, localLoc)}, noMarshal: true}, {str: "20131008", value: DateTimeTest{time.Date(2013, 10, 8, 0, 0, 0, 0, localLoc)}, noMarshal: true},
{str: "2013-10-08T10:30:50", value: DatetimeTest{time.Date(2013, 10, 8, 10, 30, 50, 0, localLoc)}}, {str: "2013-10-08T10:30:50", value: DateTimeTest{time.Date(2013, 10, 8, 10, 30, 50, 0, localLoc)}, tag: "no:dateTime.tz"},
{str: "2013-10-08T10:30:50T", value: DatetimeTest{}, wantUnmarshalErr: true, noMarshal: true}, {str: "2013-10-08T10:30:50T", value: DateTimeTest{}, wantUnmarshalErr: true, noMarshal: true},
{str: "2013-10-08T10:30:50+01", value: DateTimeTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:dateTime.tz"},
{str: "2013-10-08T10:30:50+01:23", value: DateTimeTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:dateTime.tz"},
{str: "2013-10-08T10:30:50+0123", value: DateTimeTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:dateTime.tz"},
{str: "2013-10-08T10:30:50-01", value: DateTimeTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:dateTime.tz"},
{str: "2013-10-08T10:30:50-01:23", value: DateTimeTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:dateTime.tz"},
{str: "2013-10-08T10:30:50-0123", value: DateTimeTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:dateTime.tz"},
// dateTime.tz
{str: "2013-10-08T10:30:50", value: DateTimeTzTest{time.Date(2013, 10, 8, 10, 30, 50, 0, localLoc)}, noMarshal: true},
{str: "2013-10-08T10:30:50+01", value: DateTimeTzTest{time.Date(2013, 10, 8, 10, 30, 50, 0, time.FixedZone("+01:00", 3600))}, noMarshal: true},
{str: "2013-10-08T10:30:50+01:23", value: DateTimeTzTest{time.Date(2013, 10, 8, 10, 30, 50, 0, time.FixedZone("+01:23", 3600+23*60))}},
{str: "2013-10-08T10:30:50+0123", value: DateTimeTzTest{time.Date(2013, 10, 8, 10, 30, 50, 0, time.FixedZone("+01:23", 3600+23*60))}, noMarshal: true},
{str: "2013-10-08T10:30:50-01", value: DateTimeTzTest{time.Date(2013, 10, 8, 10, 30, 50, 0, time.FixedZone("-01:00", -3600))}, noMarshal: true},
{str: "2013-10-08T10:30:50-01:23", value: DateTimeTzTest{time.Date(2013, 10, 8, 10, 30, 50, 0, time.FixedZone("-01:23", -(3600+23*60)))}},
{str: "2013-10-08T10:30:50-0123", value: DateTimeTzTest{time.Date(2013, 10, 8, 10, 30, 50, 0, time.FixedZone("-01:23", -(3600+23*60)))}, noMarshal: true},
} }
// Generate extra test cases from convTests that implement duper. // Generate extra test cases from convTests that implement duper.