diff --git a/CHANGELOG.md b/CHANGELOG.md index 25d0ed32e..f4b57c82c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Optional msgpack.v5 usage (#124) - TZ support for datetime (#163) +- Interval support for datetime (#165) ### Changed diff --git a/datetime/adjust.go b/datetime/adjust.go new file mode 100644 index 000000000..35812f45f --- /dev/null +++ b/datetime/adjust.go @@ -0,0 +1,31 @@ +package datetime + +// An Adjust is used as a parameter for date adjustions, see: +// https://github.com/tarantool/tarantool/wiki/Datetime-Internals#date-adjustions-and-leap-years +type Adjust int + +const ( + NoneAdjust Adjust = 0 // adjust = "none" in Tarantool + ExcessAdjust Adjust = 1 // adjust = "excess" in Tarantool + LastAdjust Adjust = 2 // adjust = "last" in Tarantool +) + +// We need the mappings to make NoneAdjust as a default value instead of +// dtExcess. +const ( + dtExcess = 0 // DT_EXCESS from dt-c/dt_arithmetic.h + dtLimit = 1 // DT_LIMIT + dtSnap = 2 // DT_SNAP +) + +var adjustToDt = map[Adjust]int64{ + NoneAdjust: dtLimit, + ExcessAdjust: dtExcess, + LastAdjust: dtSnap, +} + +var dtToAdjust = map[int64]Adjust{ + dtExcess: ExcessAdjust, + dtLimit: NoneAdjust, + dtSnap: LastAdjust, +} diff --git a/datetime/config.lua b/datetime/config.lua index 8b1ba2316..9b5baf719 100644 --- a/datetime/config.lua +++ b/datetime/config.lua @@ -61,6 +61,16 @@ local function call_datetime_testdata() end rawset(_G, 'call_datetime_testdata', call_datetime_testdata) +local function call_interval_testdata(interval) + return interval +end +rawset(_G, 'call_interval_testdata', call_interval_testdata) + +local function call_datetime_interval(dtleft, dtright) + return dtright - dtleft +end +rawset(_G, 'call_datetime_interval', call_datetime_interval) + -- Set listen only when every other thing is configured. box.cfg{ listen = os.getenv("TEST_TNT_LISTEN"), diff --git a/datetime/datetime.go b/datetime/datetime.go index a73550b23..b9fbc1dbe 100644 --- a/datetime/datetime.go +++ b/datetime/datetime.go @@ -120,6 +120,104 @@ func NewDatetime(t time.Time) (*Datetime, error) { return dt, nil } +func intervalFromDatetime(dtime *Datetime) (ival Interval) { + ival.Year = int64(dtime.time.Year()) + ival.Month = int64(dtime.time.Month()) + ival.Day = int64(dtime.time.Day()) + ival.Hour = int64(dtime.time.Hour()) + ival.Min = int64(dtime.time.Minute()) + ival.Sec = int64(dtime.time.Second()) + ival.Nsec = int64(dtime.time.Nanosecond()) + ival.Adjust = NoneAdjust + + return ival +} + +func daysInMonth(year int64, month int64) int64 { + if month == 12 { + year++ + month = 1 + } else { + month += 1 + } + + // We use the fact that time.Date accepts values outside their usual + // ranges - the values are normalized during the conversion. + // + // So we got a day (year, month - 1, last day of the month) before + // (year, month, 1) because we pass (year, month, 0). + return int64(time.Date(int(year), time.Month(month), 0, 0, 0, 0, 0, time.UTC).Day()) +} + +// C imlementation: +// https://github.com/tarantool/c-dt/blob/cec6acebb54d9e73ea0b99c63898732abd7683a6/dt_arithmetic.c#L74-L98 +func addMonth(ival *Interval, delta int64, adjust Adjust) { + oldYear := ival.Year + oldMonth := ival.Month + + ival.Month += delta + if ival.Month < 1 || ival.Month > 12 { + ival.Year += ival.Month / 12 + ival.Month %= 12 + if ival.Month < 1 { + ival.Year-- + ival.Month += 12 + } + } + if adjust == ExcessAdjust || ival.Day < 28 { + return + } + + dim := daysInMonth(ival.Year, ival.Month) + if ival.Day > dim || (adjust == LastAdjust && ival.Day == daysInMonth(oldYear, oldMonth)) { + ival.Day = dim + } +} + +func (dtime *Datetime) add(ival Interval, positive bool) (*Datetime, error) { + newVal := intervalFromDatetime(dtime) + + var direction int64 + if positive { + direction = 1 + } else { + direction = -1 + } + + addMonth(&newVal, direction*ival.Year*12+direction*ival.Month, ival.Adjust) + newVal.Day += direction * 7 * ival.Week + newVal.Day += direction * ival.Day + newVal.Hour += direction * ival.Hour + newVal.Min += direction * ival.Min + newVal.Sec += direction * ival.Sec + newVal.Nsec += direction * ival.Nsec + + tm := time.Date(int(newVal.Year), time.Month(newVal.Month), + int(newVal.Day), int(newVal.Hour), int(newVal.Min), + int(newVal.Sec), int(newVal.Nsec), dtime.time.Location()) + + return NewDatetime(tm) +} + +// Add creates a new Datetime as addition of the Datetime and Interval. It may +// return an error if a new Datetime is out of supported range. +func (dtime *Datetime) Add(ival Interval) (*Datetime, error) { + return dtime.add(ival, true) +} + +// Sub creates a new Datetime as subtraction of the Datetime and Interval. It +// may return an error if a new Datetime is out of supported range. +func (dtime *Datetime) Sub(ival Interval) (*Datetime, error) { + return dtime.add(ival, false) +} + +// Interval returns an Interval value to a next Datetime value. +func (dtime *Datetime) Interval(next *Datetime) Interval { + curIval := intervalFromDatetime(dtime) + nextIval := intervalFromDatetime(next) + return nextIval.Sub(curIval) +} + // ToTime returns a time.Time that Datetime contains. func (dtime *Datetime) ToTime() time.Time { return dtime.time diff --git a/datetime/datetime_test.go b/datetime/datetime_test.go index 77153b9fc..c128a82d0 100644 --- a/datetime/datetime_test.go +++ b/datetime/datetime_test.go @@ -54,6 +54,354 @@ func skipIfDatetimeUnsupported(t *testing.T) { } } +func TestDatetimeAdd(t *testing.T) { + tm := time.Unix(0, 0).UTC() + dt, err := NewDatetime(tm) + if err != nil { + t.Fatalf("Unexpected error: %s", err.Error()) + } + + newdt, err := dt.Add(Interval{ + Year: 1, + Month: -3, + Week: 3, + Day: 4, + Hour: -5, + Min: 5, + Sec: 6, + Nsec: -3, + }) + if err != nil { + t.Fatalf("Unexpected error: %s", err.Error()) + } + + expected := "1970-10-25 19:05:05.999999997 +0000 UTC" + if newdt.ToTime().String() != expected { + t.Fatalf("Unexpected result: %s, expected: %s", newdt.ToTime().String(), expected) + } +} + +func TestDatetimeAddAdjust(t *testing.T) { + /* + How-to test in Tarantool: + > date = require("datetime") + > date.parse("2012-12-31T00:00:00Z") + {month = -1, adjust = "excess"} + */ + cases := []struct { + year int64 + month int64 + adjust Adjust + date string + want string + }{ + { + year: 0, + month: 1, + adjust: NoneAdjust, + date: "2013-02-28T00:00:00Z", + want: "2013-03-28T00:00:00Z", + }, + { + year: 0, + month: 1, + adjust: LastAdjust, + date: "2013-02-28T00:00:00Z", + want: "2013-03-31T00:00:00Z", + }, + { + year: 0, + month: 1, + adjust: ExcessAdjust, + date: "2013-02-28T00:00:00Z", + want: "2013-03-28T00:00:00Z", + }, + { + year: 0, + month: 1, + adjust: NoneAdjust, + date: "2013-01-31T00:00:00Z", + want: "2013-02-28T00:00:00Z", + }, + { + year: 0, + month: 1, + adjust: LastAdjust, + date: "2013-01-31T00:00:00Z", + want: "2013-02-28T00:00:00Z", + }, + { + year: 0, + month: 1, + adjust: ExcessAdjust, + date: "2013-01-31T00:00:00Z", + want: "2013-03-03T00:00:00Z", + }, + { + year: 2, + month: 2, + adjust: NoneAdjust, + date: "2011-12-31T00:00:00Z", + want: "2014-02-28T00:00:00Z", + }, + { + year: 2, + month: 2, + adjust: LastAdjust, + date: "2011-12-31T00:00:00Z", + want: "2014-02-28T00:00:00Z", + }, + { + year: 2, + month: 2, + adjust: ExcessAdjust, + date: "2011-12-31T00:00:00Z", + want: "2014-03-03T00:00:00Z", + }, + { + year: 0, + month: -1, + adjust: NoneAdjust, + date: "2013-02-28T00:00:00Z", + want: "2013-01-28T00:00:00Z", + }, + { + year: 0, + month: -1, + adjust: LastAdjust, + date: "2013-02-28T00:00:00Z", + want: "2013-01-31T00:00:00Z", + }, + { + year: 0, + month: -1, + adjust: ExcessAdjust, + date: "2013-02-28T00:00:00Z", + want: "2013-01-28T00:00:00Z", + }, + { + year: 0, + month: -1, + adjust: NoneAdjust, + date: "2012-12-31T00:00:00Z", + want: "2012-11-30T00:00:00Z", + }, + { + year: 0, + month: -1, + adjust: LastAdjust, + date: "2012-12-31T00:00:00Z", + want: "2012-11-30T00:00:00Z", + }, + { + year: 0, + month: -1, + adjust: ExcessAdjust, + date: "2012-12-31T00:00:00Z", + want: "2012-12-01T00:00:00Z", + }, + { + year: -2, + month: -2, + adjust: NoneAdjust, + date: "2011-01-31T00:00:00Z", + want: "2008-11-30T00:00:00Z", + }, + { + year: -2, + month: -2, + adjust: LastAdjust, + date: "2011-12-31T00:00:00Z", + want: "2009-10-31T00:00:00Z", + }, + { + year: -2, + month: -2, + adjust: ExcessAdjust, + date: "2011-12-31T00:00:00Z", + want: "2009-10-31T00:00:00Z", + }, + } + + for _, tc := range cases { + tm, err := time.Parse(time.RFC3339, tc.date) + if err != nil { + t.Fatalf("Unexpected error: %s", err.Error()) + } + dt, err := NewDatetime(tm) + if err != nil { + t.Fatalf("Unexpected error: %s", err.Error()) + } + t.Run(fmt.Sprintf("%d_%d_%d_%s", tc.year, tc.month, tc.adjust, tc.date), + func(t *testing.T) { + newdt, err := dt.Add(Interval{ + Year: tc.year, + Month: tc.month, + Adjust: tc.adjust, + }) + if err != nil { + t.Fatalf("Unable to add: %s", err.Error()) + } + if newdt == nil { + t.Fatalf("Unexpected nil value") + } + res := newdt.ToTime().Format(time.RFC3339) + if res != tc.want { + t.Fatalf("Unexpected result %s, expected %s", res, tc.want) + } + }) + } +} + +func TestDatetimeAddSubSymmetric(t *testing.T) { + tm := time.Unix(0, 0).UTC() + dt, err := NewDatetime(tm) + if err != nil { + t.Fatalf("Unexpected error: %s", err.Error()) + } + + newdtadd, err := dt.Add(Interval{ + Year: 1, + Month: -3, + Week: 3, + Day: 4, + Hour: -5, + Min: 5, + Sec: 6, + Nsec: -3, + }) + if err != nil { + t.Fatalf("Unexpected error: %s", err.Error()) + } + + newdtsub, err := dt.Sub(Interval{ + Year: -1, + Month: 3, + Week: -3, + Day: -4, + Hour: 5, + Min: -5, + Sec: -6, + Nsec: 3, + }) + if err != nil { + t.Fatalf("Unexpected error: %s", err.Error()) + } + + expected := "1970-10-25 19:05:05.999999997 +0000 UTC" + addstr := newdtadd.ToTime().String() + substr := newdtsub.ToTime().String() + + if addstr != expected { + t.Fatalf("Unexpected Add result: %s, expected: %s", addstr, expected) + } + if substr != expected { + t.Fatalf("Unexpected Sub result: %s, expected: %s", substr, expected) + } +} + +// We have a separate test for accurate Datetime boundaries. +func TestDatetimeAddOutOfRange(t *testing.T) { + tm := time.Unix(0, 0).UTC() + dt, err := NewDatetime(tm) + if err != nil { + t.Fatalf("Unexpected error: %s", err.Error()) + } + + newdt, err := dt.Add(Interval{Year: 1000000000}) + if err == nil { + t.Fatalf("Unexpected success: %v", newdt) + } + expected := "time 1000001970-01-01 00:00:00 +0000 UTC is out of supported range" + if err.Error() != expected { + t.Fatalf("Unexpected error: %s", err.Error()) + } + if newdt != nil { + t.Fatalf("Unexpected result: %v", newdt) + } +} + +func TestDatetimeInterval(t *testing.T) { + var first = "2015-03-20T17:50:56.000000009Z" + var second = "2013-01-31T17:51:56.000000009Z" + + tmFirst, err := time.Parse(time.RFC3339, first) + if err != nil { + t.Fatalf("Error in time.Parse(): %s", err) + } + tmSecond, err := time.Parse(time.RFC3339, second) + if err != nil { + t.Fatalf("Error in time.Parse(): %s", err) + } + + dtFirst, err := NewDatetime(tmFirst) + if err != nil { + t.Fatalf("Unable to create Datetime from %s: %s", tmFirst, err) + } + dtSecond, err := NewDatetime(tmSecond) + if err != nil { + t.Fatalf("Unable to create Datetime from %s: %s", tmSecond, err) + } + + ivalFirst := dtFirst.Interval(dtSecond) + ivalSecond := dtSecond.Interval(dtFirst) + + expectedFirst := Interval{-2, -2, 0, 11, 0, 1, 0, 0, NoneAdjust} + expectedSecond := Interval{2, 2, 0, -11, 0, -1, 0, 0, NoneAdjust} + + if !reflect.DeepEqual(ivalFirst, expectedFirst) { + t.Errorf("Unexpected interval %v, expected %v", ivalFirst, expectedFirst) + } + if !reflect.DeepEqual(ivalSecond, expectedSecond) { + t.Errorf("Unexpected interval %v, expected %v", ivalFirst, expectedSecond) + } +} + +func TestDatetimeTarantoolInterval(t *testing.T) { + skipIfDatetimeUnsupported(t) + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + dates := []string{ + "2015-03-20T17:50:56.000000009+01:00", + "2015-12-21T17:50:53Z", + "2010-02-24T23:03:56.0000013-04:00", + "1980-03-28T13:18:39.000099Z", + "2025-08-01T00:00:00.000000003+11:00", + "2020-01-01T01:01:01+11:30", + } + datetimes := []*Datetime{} + for _, date := range dates { + tm, err := time.Parse(time.RFC3339, date) + if err != nil { + t.Fatalf("Error in time.Parse(%s): %s", date, err) + } + dt, err := NewDatetime(tm) + if err != nil { + t.Fatalf("Error in NewDatetime(%s): %s", tm, err) + } + datetimes = append(datetimes, dt) + } + + for _, dti := range datetimes { + for _, dtj := range datetimes { + t.Run(fmt.Sprintf("%s_to_%s", dti.ToTime(), dtj.ToTime()), + func(t *testing.T) { + resp, err := conn.Call17("call_datetime_interval", + []interface{}{dti, dtj}) + if err != nil { + t.Fatalf("Unable to call call_datetime_interval: %s", err) + } + ival := dti.Interval(dtj) + ret := resp.Data[0].(Interval) + if !reflect.DeepEqual(ival, ret) { + t.Fatalf("%v != %v", ival, ret) + } + }) + } + } +} + // Expect that first element of tuple is time.Time. Compare extracted actual // and expected datetime values. func assertDatetimeIsEqual(t *testing.T, tuples []interface{}, tm time.Time) { diff --git a/datetime/example_test.go b/datetime/example_test.go index ce8cfb800..ca1603ea8 100644 --- a/datetime/example_test.go +++ b/datetime/example_test.go @@ -100,3 +100,138 @@ func ExampleNewDatetime_noTimezone() { fmt.Printf("Time value: %v\n", dt.ToTime()) } + +// ExampleDatetime_Interval demonstrates how to get an Interval value between +// two Datetime values. +func ExampleDatetime_Interval() { + var first = "2013-01-31T17:51:56.000000009Z" + var second = "2015-03-20T17:50:56.000000009Z" + + tmFirst, err := time.Parse(time.RFC3339, first) + if err != nil { + fmt.Printf("Error in time.Parse() is %v", err) + return + } + tmSecond, err := time.Parse(time.RFC3339, second) + if err != nil { + fmt.Printf("Error in time.Parse() is %v", err) + return + } + + dtFirst, err := NewDatetime(tmFirst) + if err != nil { + fmt.Printf("Unable to create Datetime from %s: %s", tmFirst, err) + return + } + dtSecond, err := NewDatetime(tmSecond) + if err != nil { + fmt.Printf("Unable to create Datetime from %s: %s", tmSecond, err) + return + } + + ival := dtFirst.Interval(dtSecond) + fmt.Printf("%v", ival) + // Output: + // {2 2 0 -11 0 -1 0 0 0} +} + +// ExampleDatetime_Add demonstrates how to add an Interval to a Datetime value. +func ExampleDatetime_Add() { + var datetime = "2013-01-31T17:51:56.000000009Z" + tm, err := time.Parse(time.RFC3339, datetime) + if err != nil { + fmt.Printf("Error in time.Parse() is %s", err) + return + } + dt, err := NewDatetime(tm) + if err != nil { + fmt.Printf("Unable to create Datetime from %s: %s", tm, err) + return + } + + newdt, err := dt.Add(Interval{ + Year: 1, + Month: 1, + Sec: 333, + Adjust: LastAdjust, + }) + if err != nil { + fmt.Printf("Unable to add to Datetime: %s", err) + return + } + + fmt.Printf("New time: %s\n", newdt.ToTime().String()) + // Output: + // New time: 2014-02-28 17:57:29.000000009 +0000 UTC +} + +// ExampleDatetime_Sub demonstrates how to subtract an Interval from a +// Datetime value. +func ExampleDatetime_Sub() { + var datetime = "2013-01-31T17:51:56.000000009Z" + tm, err := time.Parse(time.RFC3339, datetime) + if err != nil { + fmt.Printf("Error in time.Parse() is %s", err) + return + } + dt, err := NewDatetime(tm) + if err != nil { + fmt.Printf("Unable to create Datetime from %s: %s", tm, err) + return + } + + newdt, err := dt.Sub(Interval{ + Year: 1, + Month: 1, + Sec: 333, + Adjust: LastAdjust, + }) + if err != nil { + fmt.Printf("Unable to sub from Datetime: %s", err) + return + } + + fmt.Printf("New time: %s\n", newdt.ToTime().String()) + // Output: + // New time: 2011-12-31 17:46:23.000000009 +0000 UTC +} + +// ExampleInterval_Add demonstrates how to add two intervals. +func ExampleInterval_Add() { + orig := Interval{ + Year: 1, + Month: 2, + Week: 3, + Sec: 10, + Adjust: ExcessAdjust, + } + ival := orig.Add(Interval{ + Year: 10, + Min: 30, + Adjust: LastAdjust, + }) + + fmt.Printf("%v", ival) + // Output: + // {11 2 3 0 0 30 10 0 1} +} + +// ExampleInterval_Sub demonstrates how to subtract two intervals. +func ExampleInterval_Sub() { + orig := Interval{ + Year: 1, + Month: 2, + Week: 3, + Sec: 10, + Adjust: ExcessAdjust, + } + ival := orig.Sub(Interval{ + Year: 10, + Min: 30, + Adjust: LastAdjust, + }) + + fmt.Printf("%v", ival) + // Output: + // {-9 2 3 0 0 -30 10 0 1} +} diff --git a/datetime/interval.go b/datetime/interval.go new file mode 100644 index 000000000..eee6b2d97 --- /dev/null +++ b/datetime/interval.go @@ -0,0 +1,179 @@ +package datetime + +import ( + "fmt" + "reflect" +) + +const interval_extId = 6 + +const ( + fieldYear = 0 + fieldMonth = 1 + fieldWeek = 2 + fieldDay = 3 + fieldHour = 4 + fieldMin = 5 + fieldSec = 6 + fieldNSec = 7 + fieldAdjust = 8 +) + +// Interval type is GoLang implementation of Tarantool intervals. +type Interval struct { + Year int64 + Month int64 + Week int64 + Day int64 + Hour int64 + Min int64 + Sec int64 + Nsec int64 + Adjust Adjust +} + +// We use int64 for every field to avoid changes in the future, see: +// https://github.com/tarantool/tarantool/blob/943ce3caf8401510ced4f074bca7006c3d73f9b3/src/lib/core/datetime.h#L106 + +// Add creates a new Interval as addition of intervals. +func (ival Interval) Add(add Interval) Interval { + ival.Year += add.Year + ival.Month += add.Month + ival.Week += add.Week + ival.Day += add.Day + ival.Hour += add.Hour + ival.Min += add.Min + ival.Sec += add.Sec + ival.Nsec += add.Nsec + + return ival +} + +// Sub creates a new Interval as subtraction of intervals. +func (ival Interval) Sub(sub Interval) Interval { + ival.Year -= sub.Year + ival.Month -= sub.Month + ival.Week -= sub.Week + ival.Day -= sub.Day + ival.Hour -= sub.Hour + ival.Min -= sub.Min + ival.Sec -= sub.Sec + ival.Nsec -= sub.Nsec + + return ival +} + +func encodeIntervalValue(e *encoder, typ uint64, value int64) (err error) { + if value == 0 { + return + } + err = encodeUint(e, typ) + if err == nil { + if value > 0 { + err = encodeUint(e, uint64(value)) + } else if value < 0 { + err = encodeInt(e, int64(value)) + } + } + return +} + +func encodeInterval(e *encoder, v reflect.Value) (err error) { + val := v.Interface().(Interval) + + var fieldNum uint64 + for _, val := range []int64{val.Year, val.Month, val.Week, val.Day, + val.Hour, val.Min, val.Sec, val.Nsec, + adjustToDt[val.Adjust]} { + if val != 0 { + fieldNum++ + } + } + if err = encodeUint(e, fieldNum); err != nil { + return + } + + if err = encodeIntervalValue(e, fieldYear, val.Year); err != nil { + return + } + if err = encodeIntervalValue(e, fieldMonth, val.Month); err != nil { + return + } + if err = encodeIntervalValue(e, fieldWeek, val.Week); err != nil { + return + } + if err = encodeIntervalValue(e, fieldDay, val.Day); err != nil { + return + } + if err = encodeIntervalValue(e, fieldHour, val.Hour); err != nil { + return + } + if err = encodeIntervalValue(e, fieldMin, val.Min); err != nil { + return + } + if err = encodeIntervalValue(e, fieldSec, val.Sec); err != nil { + return + } + if err = encodeIntervalValue(e, fieldNSec, val.Nsec); err != nil { + return + } + if err = encodeIntervalValue(e, fieldAdjust, adjustToDt[val.Adjust]); err != nil { + return + } + return nil +} + +func decodeInterval(d *decoder, v reflect.Value) (err error) { + var fieldNum uint + if fieldNum, err = d.DecodeUint(); err != nil { + return + } + + var val Interval + + hasAdjust := false + for i := 0; i < int(fieldNum); i++ { + var fieldType uint + if fieldType, err = d.DecodeUint(); err != nil { + return + } + var fieldVal int64 + if fieldVal, err = d.DecodeInt64(); err != nil { + return + } + switch fieldType { + case fieldYear: + val.Year = fieldVal + case fieldMonth: + val.Month = fieldVal + case fieldWeek: + val.Week = fieldVal + case fieldDay: + val.Day = fieldVal + case fieldHour: + val.Hour = fieldVal + case fieldMin: + val.Min = fieldVal + case fieldSec: + val.Sec = fieldVal + case fieldNSec: + val.Nsec = fieldVal + case fieldAdjust: + hasAdjust = true + if adjust, ok := dtToAdjust[fieldVal]; ok { + val.Adjust = adjust + } else { + return fmt.Errorf("unsupported Adjust: %d", fieldVal) + } + default: + return fmt.Errorf("unsupported interval field type: %d", fieldType) + } + } + + if !hasAdjust { + val.Adjust = dtToAdjust[0] + } + + v.Set(reflect.ValueOf(val)) + return nil +} diff --git a/datetime/interval_test.go b/datetime/interval_test.go new file mode 100644 index 000000000..2d1ad41f9 --- /dev/null +++ b/datetime/interval_test.go @@ -0,0 +1,132 @@ +package datetime_test + +import ( + "fmt" + "reflect" + "testing" + + . "github.com/tarantool/go-tarantool/datetime" + "github.com/tarantool/go-tarantool/test_helpers" +) + +func TestIntervalAdd(t *testing.T) { + orig := Interval{ + Year: 1, + Month: 2, + Week: 3, + Day: 4, + Hour: -5, + Min: 6, + Sec: -7, + Nsec: 8, + Adjust: LastAdjust, + } + cpyOrig := orig + add := Interval{ + Year: 2, + Month: 3, + Week: -4, + Day: 5, + Hour: -6, + Min: 7, + Sec: -8, + Nsec: 0, + Adjust: ExcessAdjust, + } + expected := Interval{ + Year: orig.Year + add.Year, + Month: orig.Month + add.Month, + Week: orig.Week + add.Week, + Day: orig.Day + add.Day, + Hour: orig.Hour + add.Hour, + Min: orig.Min + add.Min, + Sec: orig.Sec + add.Sec, + Nsec: orig.Nsec + add.Nsec, + Adjust: orig.Adjust, + } + + ival := orig.Add(add) + + if !reflect.DeepEqual(ival, expected) { + t.Fatalf("Unexpected %v, expected %v", ival, expected) + } + if !reflect.DeepEqual(cpyOrig, orig) { + t.Fatalf("Original value changed %v, expected %v", orig, cpyOrig) + } +} + +func TestIntervalSub(t *testing.T) { + orig := Interval{ + Year: 1, + Month: 2, + Week: 3, + Day: 4, + Hour: -5, + Min: 6, + Sec: -7, + Nsec: 8, + Adjust: LastAdjust, + } + cpyOrig := orig + sub := Interval{ + Year: 2, + Month: 3, + Week: -4, + Day: 5, + Hour: -6, + Min: 7, + Sec: -8, + Nsec: 0, + Adjust: ExcessAdjust, + } + expected := Interval{ + Year: orig.Year - sub.Year, + Month: orig.Month - sub.Month, + Week: orig.Week - sub.Week, + Day: orig.Day - sub.Day, + Hour: orig.Hour - sub.Hour, + Min: orig.Min - sub.Min, + Sec: orig.Sec - sub.Sec, + Nsec: orig.Nsec - sub.Nsec, + Adjust: orig.Adjust, + } + + ival := orig.Sub(sub) + + if !reflect.DeepEqual(ival, expected) { + t.Fatalf("Unexpected %v, expected %v", ival, expected) + } + if !reflect.DeepEqual(cpyOrig, orig) { + t.Fatalf("Original value changed %v, expected %v", orig, cpyOrig) + } +} + +func TestIntervalTarantoolEncoding(t *testing.T) { + skipIfDatetimeUnsupported(t) + + conn := test_helpers.ConnectWithValidation(t, server, opts) + defer conn.Close() + + cases := []Interval{ + {}, + {1, 2, 3, 4, -5, 6, -7, 8, LastAdjust}, + {1, 2, 3, 4, -5, 6, -7, 8, ExcessAdjust}, + {1, 2, 3, 4, -5, 6, -7, 8, LastAdjust}, + {0, 2, 3, 4, -5, 0, -7, 8, LastAdjust}, + {0, 0, 3, 0, -5, 6, -7, 8, ExcessAdjust}, + {0, 0, 0, 4, 0, 0, 0, 8, LastAdjust}, + } + for _, tc := range cases { + t.Run(fmt.Sprintf("%v", tc), func(t *testing.T) { + resp, err := conn.Call17("call_interval_testdata", []interface{}{tc}) + if err != nil { + t.Fatalf("Unexpected error: %s", err.Error()) + } + + ret := resp.Data[0].(Interval) + if !reflect.DeepEqual(ret, tc) { + t.Fatalf("Unexpected response: %v, expected %v", ret, tc) + } + }) + } +} diff --git a/datetime/msgpack.go b/datetime/msgpack.go index 4f48f1d3e..b5bc0d7c5 100644 --- a/datetime/msgpack.go +++ b/datetime/msgpack.go @@ -4,9 +4,25 @@ package datetime import ( + "reflect" + "gopkg.in/vmihailenco/msgpack.v2" ) +type encoder = msgpack.Encoder +type decoder = msgpack.Decoder + +func encodeUint(e *encoder, v uint64) error { + return e.EncodeUint(uint(v)) +} + +func encodeInt(e *encoder, v int64) error { + return e.EncodeInt(int(v)) +} + func init() { msgpack.RegisterExt(datetime_extId, &Datetime{}) + + msgpack.Register(reflect.TypeOf((*Interval)(nil)).Elem(), encodeInterval, decodeInterval) + msgpack.RegisterExt(interval_extId, (*Interval)(nil)) } diff --git a/datetime/msgpack_v5.go b/datetime/msgpack_v5.go index a69a81aa3..69285576e 100644 --- a/datetime/msgpack_v5.go +++ b/datetime/msgpack_v5.go @@ -4,9 +4,39 @@ package datetime import ( + "bytes" + "reflect" + "github.com/vmihailenco/msgpack/v5" ) +type encoder = msgpack.Encoder +type decoder = msgpack.Decoder + +func encodeUint(e *encoder, v uint64) error { + return e.EncodeUint(v) +} + +func encodeInt(e *encoder, v int64) error { + return e.EncodeInt(v) +} + func init() { msgpack.RegisterExt(datetime_extId, (*Datetime)(nil)) + + msgpack.RegisterExtEncoder(interval_extId, Interval{}, + func(e *msgpack.Encoder, v reflect.Value) (ret []byte, err error) { + var b bytes.Buffer + + enc := msgpack.NewEncoder(&b) + if err = encodeInterval(enc, v); err == nil { + ret = b.Bytes() + } + + return + }) + msgpack.RegisterExtDecoder(interval_extId, Interval{}, + func(d *msgpack.Decoder, v reflect.Value, extLen int) error { + return decodeInterval(d, v) + }) }