Unverified 提交 e0619d3a authored 作者: Noel Grandin's avatar Noel Grandin 提交者: GitHub

Merge pull request #855 from katzyn/datetime

Reimplement DATEADD without a Calendar and fix some incompatibilities
......@@ -3668,12 +3668,14 @@ CURRENT_TIMESTAMP()
"Functions (Time and Date)","DATEADD","
{ DATEADD| TIMESTAMPADD } (unitString, addIntLong, timestamp)
","
Adds units to a timestamp. The string indicates the unit.
Adds units to a date-time value. The string indicates the unit.
Use negative values to subtract units.
addIntLong may be a long value when manipulating milliseconds,
otherwise it's range is restricted to int.
The same units as in the EXTRACT function are supported.
DATEADD method returns a timestamp. TIMESTAMPADD method returns a long.
This method returns a value with the same type as specified value if unit is compatible with this value.
If specified unit is a HOUR, MINUTE, SECOND, MILLISECOND, etc and value is a DATE value DATEADD returns combined TIMESTAMP.
Units DAY, MONTH, YEAR, WEEK, etc are not allowed for TIME values.
","
DATEADD('MONTH', 1, DATE '2001-01-31')
"
......@@ -3723,8 +3725,9 @@ DAY_OF_YEAR(CREATED)
"
"Functions (Time and Date)","EXTRACT","
EXTRACT ( { YEAR | YY | MONTH | MM | WEEK | ISO_WEEK | DAY | DD | DAY_OF_YEAR
| DOY | HOUR | HH | MINUTE | MI | SECOND | SS | MILLISECOND | MS }
EXTRACT ( { YEAR | YY | MONTH | MM | QUARTER | WEEK | ISO_WEEK
| DAY | DD | DAY_OF_YEAR | DOY
| HOUR | HH | MINUTE | MI | SECOND | SS | MILLISECOND | MS }
FROM timestamp )
","
Returns a specific value from a timestamps.
......
......@@ -14,7 +14,6 @@ import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
......@@ -171,6 +170,7 @@ public class Function extends Expression implements FunctionCall {
DATE_PART.put("MONTH", MONTH);
DATE_PART.put("MM", MONTH);
DATE_PART.put("M", MONTH);
DATE_PART.put("QUARTER", QUARTER);
DATE_PART.put("SQL_TSI_WEEK", WEEK);
DATE_PART.put("WW", WEEK);
DATE_PART.put("WK", WEEK);
......@@ -348,7 +348,7 @@ public class Function extends Expression implements FunctionCall {
addFunction("DATEADD", DATE_ADD,
3, Value.TIMESTAMP);
addFunction("TIMESTAMPADD", DATE_ADD,
3, Value.LONG);
3, Value.TIMESTAMP);
addFunction("DATEDIFF", DATE_DIFF,
3, Value.LONG);
addFunction("TIMESTAMPDIFF", DATE_DIFF,
......@@ -1486,8 +1486,7 @@ public class Function extends Expression implements FunctionCall {
database.getMode().treatEmptyStringsAsNull);
break;
case DATE_ADD:
result = ValueTimestamp.get(dateadd(
v0.getString(), v1.getLong(), v2.getTimestamp()));
result = dateadd(v0.getString(), v1.getLong(), v2);
break;
case DATE_DIFF:
result = ValueLong.get(datediff(v0.getString(), v1, v2));
......@@ -1806,53 +1805,85 @@ public class Function extends Expression implements FunctionCall {
return p.intValue();
}
private static Timestamp dateadd(String part, long count, Timestamp d) {
private static Value dateadd(String part, long count, Value v) {
int field = getDatePart(part);
//v = v.convertTo(Value.TIMESTAMP);
if (field != MILLISECOND &&
(count > Integer.MAX_VALUE || count < Integer.MIN_VALUE)) {
throw DbException.getInvalidValueException("DATEADD count", count);
}
boolean withDate = !(v instanceof ValueTime);
boolean withTime = !(v instanceof ValueDate);
boolean forceTimestamp = false;
long[] a = DateTimeUtils.dateAndTimeFromValue(v);
long dateValue = a[0];
long timeNanos = a[1];
switch (field) {
case QUARTER:
count *= 3;
//$FALL-THROUGH$
case YEAR:
field = Calendar.YEAR;
break;
case MONTH:
field = Calendar.MONTH;
break;
case MONTH: {
if (!withDate) {
throw DbException.getInvalidValueException("DATEADD time part", part);
}
long year = DateTimeUtils.yearFromDateValue(dateValue);
long month = DateTimeUtils.monthFromDateValue(dateValue);
int day = DateTimeUtils.dayFromDateValue(dateValue);
if (field == YEAR) {
year += count;
} else {
month += count;
}
dateValue = DateTimeUtils.dateValueFromDenormalizedDate(year, month, day);
return DateTimeUtils.dateTimeToValue(v, dateValue, timeNanos, forceTimestamp);
}
case WEEK:
case ISO_WEEK:
count *= 7;
//$FALL-THROUGH$
case DAY_OF_WEEK:
case DAY_OF_MONTH:
field = Calendar.DAY_OF_MONTH;
break;
case DAY_OF_YEAR:
field = Calendar.DAY_OF_YEAR;
break;
case WEEK:
field = Calendar.WEEK_OF_YEAR;
break;
if (!withDate) {
throw DbException.getInvalidValueException("DATEADD time part", part);
}
dateValue = DateTimeUtils.dateValueFromAbsoluteDay(
DateTimeUtils.absoluteDayFromDateValue(dateValue) + count);
return DateTimeUtils.dateTimeToValue(v, dateValue, timeNanos, forceTimestamp);
case HOUR:
field = Calendar.HOUR_OF_DAY;
count *= 3_600_000_000_000L;
break;
case MINUTE:
field = Calendar.MINUTE;
count *= 60_000_000_000L;
break;
case SECOND:
field = Calendar.SECOND;
count *= 1_000_000_000;
break;
case MILLISECOND:
count *= 1_000_000;
break;
case MILLISECOND: {
Timestamp ts = new Timestamp(d.getTime() + count);
ts.setNanos(ts.getNanos() + (d.getNanos() % 1000000));
return ts;
}
default:
throw DbException.getUnsupportedException("DATEADD " + part);
}
// We allow long for manipulating the millisecond component,
// for the rest we only allow int.
if (count > Integer.MAX_VALUE) {
throw DbException.getInvalidValueException("DATEADD count", count);
if (!withTime) {
// Treat date as timestamp at the start of this date
forceTimestamp = true;
}
Calendar calendar = DateTimeUtils.createGregorianCalendar();
int nanos = d.getNanos() % 1000000;
calendar.setTime(d);
calendar.add(field, (int) count);
Timestamp ts = new Timestamp(calendar.getTimeInMillis());
ts.setNanos(ts.getNanos() + nanos);
return ts;
timeNanos += count;
if (timeNanos > DateTimeUtils.NANOS_PER_DAY || timeNanos < 0) {
long d;
if (timeNanos > DateTimeUtils.NANOS_PER_DAY) {
d = timeNanos / DateTimeUtils.NANOS_PER_DAY;
} else {
d = (timeNanos - DateTimeUtils.NANOS_PER_DAY + 1) / DateTimeUtils.NANOS_PER_DAY;
}
timeNanos -= d * DateTimeUtils.NANOS_PER_DAY;
return DateTimeUtils.dateTimeToValue(v,
DateTimeUtils.dateValueFromAbsoluteDay(DateTimeUtils.absoluteDayFromDateValue(dateValue) + d),
timeNanos, forceTimestamp);
}
return DateTimeUtils.dateTimeToValue(v, dateValue, timeNanos, forceTimestamp);
}
/**
......@@ -1908,6 +1939,10 @@ public class Function extends Expression implements FunctionCall {
case MONTH:
return (DateTimeUtils.yearFromDateValue(dateValue2) - DateTimeUtils.yearFromDateValue(dateValue1)) * 12
+ DateTimeUtils.monthFromDateValue(dateValue2) - DateTimeUtils.monthFromDateValue(dateValue1);
case QUARTER:
return (DateTimeUtils.yearFromDateValue(dateValue2) - DateTimeUtils.yearFromDateValue(dateValue1)) * 4
+ (DateTimeUtils.monthFromDateValue(dateValue2) - 1) / 3
- (DateTimeUtils.monthFromDateValue(dateValue1) - 1) / 3;
case YEAR:
return DateTimeUtils.yearFromDateValue(dateValue2) - DateTimeUtils.yearFromDateValue(dateValue1);
default:
......
......@@ -24,7 +24,6 @@ import org.h2.value.ValueTime;
import org.h2.value.ValueTimestamp;
import org.h2.value.ValueTimestampTimeZone;
/**
* This utility class contains time conversion functions.
* <p>
......@@ -590,6 +589,40 @@ public class DateTimeUtils {
return new long[] {dateValue, timeNanos};
}
/**
* Creates a new date-time value with the same type as original value. If
* original value is a ValueTimestampTimeZone, returned value will have the same
* time zone offset as original value.
*
* @param original
* original value
* @param dateValue
* date value for the returned value
* @param timeNanos
* nanos of day for the returned value
* @param forceTimestamp
* if {@code true} return ValueTimestamp if original argument is
* ValueDate or ValueTime
* @return new value with specified date value and nanos of day
*/
public static Value dateTimeToValue(Value original, long dateValue, long timeNanos, boolean forceTimestamp) {
if (!(original instanceof ValueTimestamp)) {
if (!forceTimestamp) {
if (original instanceof ValueDate) {
return ValueDate.fromDateValue(dateValue);
}
if (original instanceof ValueTime) {
return ValueTime.fromNanos(timeNanos);
}
}
if (original instanceof ValueTimestampTimeZone) {
return ValueTimestampTimeZone.fromDateValueAndNanos(dateValue, timeNanos,
((ValueTimestampTimeZone) original).getTimeZoneOffsetMins());
}
}
return ValueTimestamp.fromDateValueAndNanos(dateValue, timeNanos);
}
/**
* Get the year (positive or negative) from a calendar.
*
......@@ -830,6 +863,26 @@ public class DateTimeUtils {
}
}
/**
* Returns number of days in month.
*
* @param year the year
* @param month the month
* @return number of days in the specified month
*/
public static int getDaysInMonth(int year, int month) {
if (month != 2) {
return NORMAL_DAYS_PER_MONTH[month];
}
// All leap years divisible by 4
return (year & 3) == 0
// All such years before 1582 are Julian and leap
&& (year < 1582
// Otherwise check Gregorian conditions
|| year % 100 != 0 || year % 400 == 0)
? 29 : 28;
}
/**
* Verify if the specified date is valid.
*
......@@ -842,24 +895,11 @@ public class DateTimeUtils {
if (month < 1 || month > 12 || day < 1) {
return false;
}
if (year > 1582) {
// Gregorian calendar
if (month != 2) {
return day <= NORMAL_DAYS_PER_MONTH[month];
}
// February
if ((year & 3) != 0) {
return day <= 28;
}
return day <= ((year % 100 != 0) || (year % 400 == 0) ? 29 : 28);
} else if (year == 1582 && month == 10) {
if (year == 1582 && month == 10) {
// special case: days 1582-10-05 .. 1582-10-14 don't exist
return day <= 31 && (day < 5 || day > 14);
}
if (month != 2 && day <= NORMAL_DAYS_PER_MONTH[month]) {
return true;
return day < 5 || (day > 14 && day <= 31);
}
return day <= ((year & 3) != 0 ? 28 : 29);
return day <= getDaysInMonth(year, month);
}
/**
......@@ -985,6 +1025,38 @@ public class DateTimeUtils {
return (year << SHIFT_YEAR) | (month << SHIFT_MONTH) | day;
}
/**
* Get the date value from a given denormalized date with possible out of range
* values of month and/or day. Used after addition or subtraction month or years
* to (from) it to get a valid date.
*
* @param year
* the year
* @param month
* the month, if out of range month and year will be normalized
* @param day
* the day of the month, if out of range it will be saturated
* @return the date value
*/
public static long dateValueFromDenormalizedDate(long year, long month, int day) {
long mm1 = month - 1;
long yd = mm1 / 12;
if (mm1 < 0 && yd * 12 != mm1) {
yd--;
}
int y = (int) (year + yd);
int m = (int) (month - yd * 12);
if (day < 1) {
day = 1;
} else {
int max = getDaysInMonth(y, m);
if (day > max) {
day = max;
}
}
return dateValue(y, m, day);
}
/**
* Convert a UTC datetime in millis to an encoded date in the default
* timezone.
......
......@@ -81,7 +81,7 @@ select d + t, t + d - t x from test;
select 1 + d + 1, d - 1, 2 + ts + 2, ts - 2 from test;
> DATEADD('DAY', 1, DATEADD('DAY', 1, D)) DATEADD('DAY', -1, D) DATEADD('DAY', 2, DATEADD('DAY', 2, TS)) DATEADD('DAY', -2, TS)
> --------------------------------------- --------------------- ---------------------------------------- ----------------------
> 2001-01-03 00:00:00.0 2000-12-31 00:00:00.0 2010-01-05 00:00:00.0 2009-12-30 00:00:00.0
> 2001-01-03 2000-12-31 2010-01-05 00:00:00.0 2009-12-30 00:00:00.0
> rows: 1
select 1 + d + t + 1 from test;
......@@ -104,3 +104,36 @@ call dateadd('MS', 1, TIMESTAMP '2001-02-03 04:05:06.789001');
> --------------------------------------
> 2001-02-03 04:05:06.790001
> rows: 1
SELECT DATEADD('HOUR', 1, DATE '2010-01-20');
> TIMESTAMP '2010-01-20 01:00:00.0'
> ---------------------------------
> 2010-01-20 01:00:00.0
> rows: 1
SELECT DATEADD('MINUTE', 30, TIME '12:30:55');
> TIME '13:00:55'
> ---------------
> 13:00:55
> rows: 1
SELECT DATEADD('DAY', 1, TIME '12:30:55');
> exception
SELECT DATEADD('QUARTER', 1, DATE '2010-11-16');
> DATE '2011-02-16'
> -----------------
> 2011-02-16
> rows: 1
SELECT DATEADD('DAY', 10, TIMESTAMP WITH TIME ZONE '2000-01-05 15:00:30.123456789-10');
> TIMESTAMP WITH TIME ZONE '2000-01-15 15:00:30.123456789-10'
> -----------------------------------------------------------
> 2000-01-15 15:00:30.123456789-10
> rows: 1
SELECT TIMESTAMPADD('DAY', 10, TIMESTAMP '2000-01-05 15:00:30.123456789');
> TIMESTAMP '2000-01-15 15:00:30.123456789'
> -----------------------------------------
> 2000-01-15 15:00:30.123456789
> rows: 1
......@@ -194,3 +194,34 @@ SELECT DATEDIFF('WEEK', DATE '1969-12-28', DATE '1969-12-29'), DATEDIFF('ISO_WEE
> - -
> 0 1
> rows: 1
SELECT DATEDIFF('QUARTER', DATE '2009-12-30', DATE '2009-12-31');
> 0
> -
> 0
> rows: 1
SELECT DATEDIFF('QUARTER', DATE '2010-01-01', DATE '2009-12-31');
> -1
> --
> -1
> rows: 1
SELECT DATEDIFF('QUARTER', DATE '2010-01-01', DATE '2010-01-02');
> 0
> -
> 0
> rows: 1
SELECT DATEDIFF('QUARTER', DATE '2010-01-01', DATE '2010-03-31');
> 0
> -
> 0
> rows: 1
SELECT DATEDIFF('QUARTER', DATE '-1000-01-01', DATE '2000-01-01');
> 12000
> -----
> 12000
> rows: 1
......@@ -7586,9 +7586,9 @@ SELECT * FROM TEST;
SELECT XD+1, XD-1, XD-XD FROM TEST;
> DATEADD('DAY', 1, XD) DATEADD('DAY', -1, XD) DATEDIFF('DAY', XD, XD)
> --------------------- ---------------------- -----------------------
> 0001-02-04 00:00:00.0 0001-02-02 00:00:00.0 0
> 0004-05-07 00:00:00.0 0004-05-05 00:00:00.0 0
> 2000-01-01 00:00:00.0 1999-12-30 00:00:00.0 0
> 0001-02-04 0001-02-02 0
> 0004-05-07 0004-05-05 0
> 2000-01-01 1999-12-30 0
> null null null
> rows: 4
......
......@@ -11,6 +11,8 @@ import java.util.GregorianCalendar;
import org.h2.test.TestBase;
import org.h2.util.DateTimeUtils;
import static org.h2.util.DateTimeUtils.dateValue;
/**
* Unit tests for the DateTimeUtils class
*/
......@@ -30,6 +32,7 @@ public class TestDateTimeUtils extends TestBase {
testParseTimeNanosDB2Format();
testDayOfWeek();
testWeekOfYear();
testDateValueFromDenormalizedDate();
}
private void testParseTimeNanosDB2Format() {
......@@ -53,7 +56,7 @@ public class TestDateTimeUtils extends TestBase {
if (gc.get(Calendar.ERA) == GregorianCalendar.BC) {
year = 1 - year;
}
long expectedDateValue = DateTimeUtils.dateValue(year, gc.get(Calendar.MONTH) + 1,
long expectedDateValue = dateValue(year, gc.get(Calendar.MONTH) + 1,
gc.get(Calendar.DAY_OF_MONTH));
long dateValue = DateTimeUtils.dateValueFromAbsoluteDay(i);
assertEquals(expectedDateValue, dateValue);
......@@ -92,4 +95,15 @@ public class TestDateTimeUtils extends TestBase {
}
}
/**
* Test for {@link DateTimeUtils#dateValueFromDenormalizedDate(long, long, int)}.
*/
private void testDateValueFromDenormalizedDate() {
assertEquals(dateValue(2017, 1, 1), DateTimeUtils.dateValueFromDenormalizedDate(2018, -11, 0));
assertEquals(dateValue(2001, 2, 28), DateTimeUtils.dateValueFromDenormalizedDate(2000, 14, 29));
assertEquals(dateValue(1999, 8, 1), DateTimeUtils.dateValueFromDenormalizedDate(2000, -4, -100));
assertEquals(dateValue(2100, 12, 31), DateTimeUtils.dateValueFromDenormalizedDate(2100, 12, 2000));
assertEquals(dateValue(-100, 2, 29), DateTimeUtils.dateValueFromDenormalizedDate(-100, 2, 30));
}
}
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论