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);
+}