diff --git a/extra/exports b/extra/exports index 25f9506147f308ac33e0abb051d2295edc77be13..b84e8553486bb769a66c369350ee28c54317fa2b 100644 --- a/extra/exports +++ b/extra/exports @@ -423,6 +423,7 @@ title_set_interpretor_name title_set_script_name title_set_status title_update +tnt_datetime_increment_by tnt_datetime_now tnt_datetime_parse_full tnt_datetime_strftime diff --git a/src/lib/core/datetime.c b/src/lib/core/datetime.c index d237a439def2a036bea1c177cdfa535d5b0cc612..cb2378ab26dc3989c6719169041bea9270271583 100644 --- a/src/lib/core/datetime.c +++ b/src/lib/core/datetime.c @@ -20,8 +20,8 @@ #include "fiber.h" /** floored modulo and divide */ -#define MOD(a, b) unlikely(a < 0) ? (b + (a % b)) : (a % b) -#define DIV(a, b) unlikely(a < 0) ? ((a - b + 1) / b) : (a / b) +#define MOD(a, b) (unlikely((a) < 0) ? ((b) + ((a) % (b))) : ((a) % (b))) +#define DIV(a, b) (unlikely((a) < 0) ? (((a) - (b) + 1) / (b)) : ((a) / (b))) /** * Given the seconds from Epoch (1970-01-01) we calculate date @@ -569,3 +569,119 @@ interval_to_string(const struct interval *ival, char *buf, ssize_t len) } return sz; } + +/** + * Normalize seconds and nanoseconds: + * - make sure that nanoseconds part is positive + * - make sure it's not exceeding maximum allowed value + */ +static void +normalize_nsec(int64_t *psecs, int *pnsec) +{ + assert(psecs != NULL); + assert(pnsec != NULL); + int64_t secs = *psecs; + int nsec = *pnsec; + + if (nsec < 0 || nsec >= NANOS_PER_SEC) { + secs += nsec / NANOS_PER_SEC; + nsec %= NANOS_PER_SEC; + } + *psecs = secs; + *pnsec = nsec; +} + +static inline int64_t +utc_secs(int64_t epoch, int tzoffset) +{ + return epoch - tzoffset * 60; +} + +/** minimum supported date - -5879610-06-22 */ +#define MIN_DATE_YEAR -5879610 +#define MIN_DATE_MONTH 6 +#define MIN_DATE_DAY 22 + +/** maximum supported date - 5879611-07-11 */ +#define MAX_DATE_YEAR 5879611 +#define MAX_DATE_MONTH 7 +#define MAX_DATE_DAY 11 +/** + * In the Julian calendar, the average year length is + * 365 1/4 days = 365.25 days. This gives an error of + * about 1 day in 128 years. + */ +#define AVERAGE_DAYS_YEAR 365.25 +#define AVERAGE_DAYS_MONTH (AVERAGE_DAYS_YEAR / 12) +#define AVERAGE_WEEK_YEAR (AVERAGE_DAYS_YEAR / 7) + +static inline int +verify_range(int64_t v, int64_t from, int64_t to) +{ + return (v < from) ? -1 : (v > to ? +1 : 0); +} + +static inline int +verify_dt(int64_t dt) +{ + return verify_range(dt, INT_MIN, INT_MAX); +} + +int +datetime_increment_by(struct datetime *self, int direction, + const struct interval *ival) +{ + int64_t secs = local_secs(self); + int64_t dt = local_dt(secs); + int nsec = self->nsec; + int offset = self->tzindex; + + bool is_ym_updated = false; + int years = ival->year; + int months = ival->month; + int seconds = ival->sec; + int nanoseconds = ival->nsec; + dt_adjust_t adjust = ival->adjust; + int rc = 0; + + if (years != 0) { + rc = verify_range(years, MIN_DATE_YEAR, MAX_DATE_YEAR); + if (rc != 0) + return rc; + rc = verify_dt(dt + direction * years * AVERAGE_DAYS_YEAR); + if (rc != 0) + return rc; + /* tnt_dt_add_years() not handle properly DT_SNAP or DT_LIMIT + * mode so use tnt_dt_add_months() as a work-around + */ + dt = dt_add_months(dt, direction * years * 12, adjust); + is_ym_updated = true; + } + if (months != 0) { + rc = verify_dt(dt + direction * months * AVERAGE_DAYS_MONTH); + if (rc != 0) + return rc; + + dt = dt_add_months(dt, direction * months, adjust); + is_ym_updated = true; + } + + if (is_ym_updated) { + secs = dt * SECS_PER_DAY - SECS_EPOCH_1970_OFFSET + + secs % SECS_PER_DAY; + } + + if (seconds != 0) + secs += direction * seconds; + if (nanoseconds != 0) + nsec += direction * nanoseconds; + + normalize_nsec(&secs, &nsec); + rc = verify_dt((secs + SECS_EPOCH_1970_OFFSET) / SECS_PER_DAY); + if (rc != 0) + return rc; + + self->epoch = utc_secs(secs, offset); + self->nsec = nsec; + return 0; +} diff --git a/src/lib/core/datetime.h b/src/lib/core/datetime.h index 5b45129f20ef087451a619e2d95ea3c0eb9b86e9..970d603a5eded15739b605dcbe08b942d2769eea 100644 --- a/src/lib/core/datetime.h +++ b/src/lib/core/datetime.h @@ -192,6 +192,18 @@ datetime_strptime(struct datetime *date, const char *buf, const char *fmt); size_t interval_to_string(const struct interval *ival, char *buf, ssize_t len); +/** + * Add/subtract datetime value by passed interval using direction as a hint + * @param[in,out] self left source operand and target for binary operation + * @param[in] direction addition (0) or subtraction (-1) + * @param[in] ival interval objects (right operand) + * @retval 0 if there is no overflow, negative value if there is underflow + * or positive if there is overflow. + */ +int +datetime_increment_by(struct datetime *self, int direction, + const struct interval *ival); + /** Return the year from a date. */ int64_t datetime_year(const struct datetime *date); diff --git a/src/lua/datetime.lua b/src/lua/datetime.lua index 8e22d513c164d9170c4a14ce801cdb3e13eb6ecb..5ca8156ed78f4ae45abb9e7d98363f56287bb6c7 100644 --- a/src/lua/datetime.lua +++ b/src/lua/datetime.lua @@ -74,8 +74,9 @@ size_t tnt_datetime_strptime(struct datetime *date, const char *buf, void tnt_datetime_now(struct datetime *now); /* Tarantool interval support functions */ -size_t -tnt_interval_to_string(const struct interval *, char *, ssize_t); +size_t tnt_interval_to_string(const struct interval *, char *, ssize_t); +int tnt_datetime_increment_by(struct datetime *self, int direction, + const struct interval *ival); ]] @@ -87,7 +88,6 @@ local math_floor = math.floor local DAYS_EPOCH_OFFSET = 719163 local SECS_PER_DAY = 86400 local SECS_EPOCH_OFFSET = DAYS_EPOCH_OFFSET * SECS_PER_DAY -local NANOS_PER_SEC = 1e9 local TOSTRING_BUFSIZE = 48 local IVAL_TOSTRING_BUFSIZE = 96 local STRFTIME_BUFSIZE = 128 @@ -104,10 +104,8 @@ local MAX_DATE_DAY = 11 -- 365 1/4 days = 365.25 days. This gives an error of -- about 1 day in 128 years. local AVERAGE_DAYS_YEAR = 365.25 -local AVERAGE_DAYS_MONTH = AVERAGE_DAYS_YEAR / 12 local AVERAGE_WEEK_YEAR = AVERAGE_DAYS_YEAR / 7 local INT_MAX = 2147483647 -local INT_MIN = -2147483648 -- -5879610-06-22 local MIN_DATE_TEXT = ('%d-%02d-%02d'):format(MIN_DATE_YEAR, MIN_DATE_MONTH, MIN_DATE_DAY) @@ -246,24 +244,6 @@ local function check_range(v, from, to, txt, extra) end end -local function check_dt(dt_v, direction, v, unit) - if dt_v >= INT_MIN and dt_v <= INT_MAX then - return - end - local operation = direction >= 0 and 'addition' or 'subtraction' - local title = unit ~= nil and ('%s of %d %s'):format(operation, v, unit) or - operation - if dt_v < INT_MIN then - error(('%s makes date less than minimum allowed %s'): - format(title, MIN_DATE_TEXT), 3) - elseif dt_v > INT_MAX then - error(('%s makes date greater than maximum allowed %s'): - format(title, MAX_DATE_TEXT), 3) - else - assert(false) - end -end - local function dt_from_ymd_checked(y, M, d) local pdt = date_dt_stash_take() local is_valid = builtin.tnt_dt_from_ymd_checked(y, M, d, pdt) @@ -297,14 +277,6 @@ local function bool2int(b) return b and 1 or 0 end -local function normalize_nsec(secs, nsec) - if nsec < 0 or nsec >= NANOS_PER_SEC then - secs = secs + math_floor(nsec / NANOS_PER_SEC) - nsec = nsec % NANOS_PER_SEC - end - return secs, nsec -end - local adjust_xlat = { none = builtin.DT_LIMIT, last = builtin.DT_SNAP, @@ -634,48 +606,18 @@ local function interval_tostring(self) end local function datetime_increment_by(self, direction, ival) - -- operations with intervals should be done using local human dates - -- not UTC dates, thus normalize to local timezone before operations. - local dt = local_dt(self) - local secs, nsec = local_secs(self), self.nsec - local offset = self.tzoffset - - local is_ym_updated = false - local years, months = ival.year, ival.month - local seconds, nanoseconds = ival.sec, ival.nsec - local adjust = ival.adjust or DEF_DT_ADJUST - - if years ~= 0 then - check_range(years, MIN_DATE_YEAR, MAX_DATE_YEAR, 'years') - local approx_dt = dt + direction * years * AVERAGE_DAYS_YEAR - check_dt(approx_dt, direction, years, 'years') - -- tnt_dt_add_years() not handle properly DT_SNAP or DT_LIMIT mode - -- so use tnt_dt_add_months() as a work-around - dt = builtin.tnt_dt_add_months(dt, direction * years * 12, adjust) - is_ym_updated = true - end - if months ~= 0 then - local new_dt = dt + direction * months * AVERAGE_DAYS_MONTH - check_dt(new_dt, direction, months, 'months') - dt = builtin.tnt_dt_add_months(dt, direction * months, adjust) - is_ym_updated = true - end - if is_ym_updated then - secs = (dt - DAYS_EPOCH_OFFSET) * SECS_PER_DAY + secs % SECS_PER_DAY - end - - if seconds ~= 0 then - secs = secs + direction * seconds + local rc = builtin.tnt_datetime_increment_by(self, direction, ival) + if rc == 0 then + return self end - - if nanoseconds ~= 0 then - nsec = nsec + direction * nanoseconds + local operation = direction >= 0 and 'addition' or 'subtraction' + if rc < 0 then + error(('%s makes date less than minimum allowed %s'): + format(operation, MIN_DATE_TEXT), 3) + else -- rc > 0 + error(('%s makes date greater than maximum allowed %s'): + format(operation, MAX_DATE_TEXT), 3) end - - secs, self.nsec = normalize_nsec(secs, nsec) - check_dt((secs + SECS_EPOCH_OFFSET) / SECS_PER_DAY, direction) - self.epoch = utc_secs(secs, offset) - return self end local function date_first(lhs, rhs) diff --git a/src/lua/tnt_datetime.c b/src/lua/tnt_datetime.c index db6848ea077d0b282131e032bfb58884cc941f74..8d953f300e24d84f346d80a23c3495488f8d891e 100644 --- a/src/lua/tnt_datetime.c +++ b/src/lua/tnt_datetime.c @@ -50,3 +50,10 @@ tnt_interval_to_string(const struct interval *ival, char *buf, ssize_t len) { return interval_to_string(ival, buf, len); } + +int +tnt_datetime_increment_by(struct datetime *self, int direction, + const struct interval *ival) +{ + return datetime_increment_by(self, direction, ival); +}