Skip to content

Commit

Permalink
datetime: add interval support
Browse files Browse the repository at this point in the history
The patch adds interval [1] support for datetime. Except
encoding/decoding interval values from MessagePack, it adds a several
functions for addition and substraction Interval and Datetime types
in GoLang. Reproducing, thus, arithmetic operations from the Lua
implementation [2].

1. https://github.com/tarantool/tarantool/wiki/Datetime-Internals#interval-arithmetic
2. https://github.com/tarantool/tarantool/wiki/Datetime-Internals#arithmetic-operations

Closes #165
  • Loading branch information
oleg-jukovec committed Aug 11, 2022
1 parent 913bf30 commit ab1fb93
Show file tree
Hide file tree
Showing 10 changed files with 980 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
31 changes: 31 additions & 0 deletions datetime/adjust.go
Original file line number Diff line number Diff line change
@@ -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,
}
10 changes: 10 additions & 0 deletions datetime/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
98 changes: 98 additions & 0 deletions datetime/datetime.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit ab1fb93

Please sign in to comment.