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
}
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
// to local time, and discards the time-of-day components.
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
// date as midnight in the local time zone.
func UnmarshalDate(s string) (time.Time, error) {
for _, f := range dateFmts {
if t, err := time.ParseInLocation(f, s, localLoc); err == nil {
return t, nil
year, month, day, err := parseDateParts(s)
if err != 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.
@ -88,7 +214,7 @@ type TimeOfDay struct {
// 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 int16
Offset int
}
// 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
}
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.
func UnmarshalTimeOfDayTz(s string) (TimeOfDay, error) {
parts := timeRegexp.FindStringSubmatch(s)
if parts == nil {
return TimeOfDay{}, fmt.Errorf("soap time.tz: value %q is not in ISO8601 time format", s)
func UnmarshalTimeOfDayTz(s string) (tod TimeOfDay, err error) {
zoneIndex := strings.IndexAny(s, "Z+-")
var timePart string
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.
parts = parts[1:]
var iParts [3]int64
for i, pStr := range parts[:3] {
iParts[i], _ = strconv.ParseInt(pStr, 10, 64)
hour, minute, second, err := parseTimeParts(timePart)
if err != nil {
return
}
hour, minute, second := iParts[0], iParts[1], iParts[2]
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)
}
// 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{
FromMidnight: time.Duration(hour*3600+minute*60+second) * time.Second,
HasOffset: hasOffset,
Offset: int16(offset),
Offset: offset,
}, 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.
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
}
// 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.
func UnmarshalDatetime(s string) (time.Time, error) {
parts := strings.SplitN(s, "T", 2)
datePart, err := UnmarshalDate(parts[0])
func UnmarshalDateTime(s string) (result time.Time, err error) {
dateStr, timeStr, zoneStr, err := splitCompleteDateTimeZone(s)
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 time.Time{}, err
return
}
datePart = datePart.Add(timePart.FromMidnight)
var hour, minute, second int
if len(timeStr) != 0 {
hour, minute, second, err = parseTimeParts(timeStr)
if err != nil {
return
}
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 {
if tag != "no:dateTime" {
return DatetimeTest{v.Time}
return DateTimeTest{v.Time}
}
return nil
}
@ -104,15 +104,33 @@ func (v TimeOfDayTzTest) Equal(result interface{}) bool {
return v.TimeOfDay == result.(TimeOfDay)
}
type DatetimeTest struct{ time.Time }
type DateTimeTest struct{ time.Time }
func (v DatetimeTest) Marshal() (string, error) {
return MarshalDatetime(time.Time(v.Time))
func (v DateTimeTest) Marshal() (string, error) {
return MarshalDateTime(time.Time(v.Time))
}
func (v DatetimeTest) Unmarshal(s string) (interface{}, error) {
return UnmarshalDatetime(s)
func (v DateTimeTest) Unmarshal(s string) (interface{}, error) {
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))
}
@ -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-0123", value: TimeOfDayTzTest{TimeOfDay{time010203, true, -(3600 + 23*60)}}, noMarshal: true},
// datetime
{str: "2013-10-08T00:00:00", value: DatetimeTest{time.Date(2013, 10, 8, 0, 0, 0, 0, localLoc)}},
{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:50T", value: DatetimeTest{}, wantUnmarshalErr: true, noMarshal: true},
// dateTime
{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: "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: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.