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() ...@@ -3668,12 +3668,14 @@ CURRENT_TIMESTAMP()
"Functions (Time and Date)","DATEADD"," "Functions (Time and Date)","DATEADD","
{ DATEADD| TIMESTAMPADD } (unitString, addIntLong, timestamp) { 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. Use negative values to subtract units.
addIntLong may be a long value when manipulating milliseconds, addIntLong may be a long value when manipulating milliseconds,
otherwise it's range is restricted to int. otherwise it's range is restricted to int.
The same units as in the EXTRACT function are supported. 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') DATEADD('MONTH', 1, DATE '2001-01-31')
" "
...@@ -3723,8 +3725,9 @@ DAY_OF_YEAR(CREATED) ...@@ -3723,8 +3725,9 @@ DAY_OF_YEAR(CREATED)
" "
"Functions (Time and Date)","EXTRACT"," "Functions (Time and Date)","EXTRACT","
EXTRACT ( { YEAR | YY | MONTH | MM | WEEK | ISO_WEEK | DAY | DD | DAY_OF_YEAR EXTRACT ( { YEAR | YY | MONTH | MM | QUARTER | WEEK | ISO_WEEK
| DOY | HOUR | HH | MINUTE | MI | SECOND | SS | MILLISECOND | MS } | DAY | DD | DAY_OF_YEAR | DOY
| HOUR | HH | MINUTE | MI | SECOND | SS | MILLISECOND | MS }
FROM timestamp ) FROM timestamp )
"," ","
Returns a specific value from a timestamps. Returns a specific value from a timestamps.
......
...@@ -14,7 +14,6 @@ import java.nio.charset.StandardCharsets; ...@@ -14,7 +14,6 @@ import java.nio.charset.StandardCharsets;
import java.sql.Connection; import java.sql.Connection;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.sql.Timestamp;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Calendar; import java.util.Calendar;
...@@ -171,6 +170,7 @@ public class Function extends Expression implements FunctionCall { ...@@ -171,6 +170,7 @@ public class Function extends Expression implements FunctionCall {
DATE_PART.put("MONTH", MONTH); DATE_PART.put("MONTH", MONTH);
DATE_PART.put("MM", MONTH); DATE_PART.put("MM", MONTH);
DATE_PART.put("M", MONTH); DATE_PART.put("M", MONTH);
DATE_PART.put("QUARTER", QUARTER);
DATE_PART.put("SQL_TSI_WEEK", WEEK); DATE_PART.put("SQL_TSI_WEEK", WEEK);
DATE_PART.put("WW", WEEK); DATE_PART.put("WW", WEEK);
DATE_PART.put("WK", WEEK); DATE_PART.put("WK", WEEK);
...@@ -348,7 +348,7 @@ public class Function extends Expression implements FunctionCall { ...@@ -348,7 +348,7 @@ public class Function extends Expression implements FunctionCall {
addFunction("DATEADD", DATE_ADD, addFunction("DATEADD", DATE_ADD,
3, Value.TIMESTAMP); 3, Value.TIMESTAMP);
addFunction("TIMESTAMPADD", DATE_ADD, addFunction("TIMESTAMPADD", DATE_ADD,
3, Value.LONG); 3, Value.TIMESTAMP);
addFunction("DATEDIFF", DATE_DIFF, addFunction("DATEDIFF", DATE_DIFF,
3, Value.LONG); 3, Value.LONG);
addFunction("TIMESTAMPDIFF", DATE_DIFF, addFunction("TIMESTAMPDIFF", DATE_DIFF,
...@@ -1486,8 +1486,7 @@ public class Function extends Expression implements FunctionCall { ...@@ -1486,8 +1486,7 @@ public class Function extends Expression implements FunctionCall {
database.getMode().treatEmptyStringsAsNull); database.getMode().treatEmptyStringsAsNull);
break; break;
case DATE_ADD: case DATE_ADD:
result = ValueTimestamp.get(dateadd( result = dateadd(v0.getString(), v1.getLong(), v2);
v0.getString(), v1.getLong(), v2.getTimestamp()));
break; break;
case DATE_DIFF: case DATE_DIFF:
result = ValueLong.get(datediff(v0.getString(), v1, v2)); result = ValueLong.get(datediff(v0.getString(), v1, v2));
...@@ -1806,53 +1805,85 @@ public class Function extends Expression implements FunctionCall { ...@@ -1806,53 +1805,85 @@ public class Function extends Expression implements FunctionCall {
return p.intValue(); 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); 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) { switch (field) {
case QUARTER:
count *= 3;
//$FALL-THROUGH$
case YEAR: case YEAR:
field = Calendar.YEAR; case MONTH: {
break; if (!withDate) {
case MONTH: throw DbException.getInvalidValueException("DATEADD time part", part);
field = Calendar.MONTH; }
break; 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: case DAY_OF_MONTH:
field = Calendar.DAY_OF_MONTH;
break;
case DAY_OF_YEAR: case DAY_OF_YEAR:
field = Calendar.DAY_OF_YEAR; if (!withDate) {
break; throw DbException.getInvalidValueException("DATEADD time part", part);
case WEEK: }
field = Calendar.WEEK_OF_YEAR; dateValue = DateTimeUtils.dateValueFromAbsoluteDay(
break; DateTimeUtils.absoluteDayFromDateValue(dateValue) + count);
return DateTimeUtils.dateTimeToValue(v, dateValue, timeNanos, forceTimestamp);
case HOUR: case HOUR:
field = Calendar.HOUR_OF_DAY; count *= 3_600_000_000_000L;
break; break;
case MINUTE: case MINUTE:
field = Calendar.MINUTE; count *= 60_000_000_000L;
break; break;
case SECOND: case SECOND:
field = Calendar.SECOND; count *= 1_000_000_000;
break;
case MILLISECOND:
count *= 1_000_000;
break; break;
case MILLISECOND: {
Timestamp ts = new Timestamp(d.getTime() + count);
ts.setNanos(ts.getNanos() + (d.getNanos() % 1000000));
return ts;
}
default: default:
throw DbException.getUnsupportedException("DATEADD " + part); throw DbException.getUnsupportedException("DATEADD " + part);
} }
// We allow long for manipulating the millisecond component, if (!withTime) {
// for the rest we only allow int. // Treat date as timestamp at the start of this date
if (count > Integer.MAX_VALUE) { forceTimestamp = true;
throw DbException.getInvalidValueException("DATEADD count", count);
} }
Calendar calendar = DateTimeUtils.createGregorianCalendar(); timeNanos += count;
int nanos = d.getNanos() % 1000000; if (timeNanos > DateTimeUtils.NANOS_PER_DAY || timeNanos < 0) {
calendar.setTime(d); long d;
calendar.add(field, (int) count); if (timeNanos > DateTimeUtils.NANOS_PER_DAY) {
Timestamp ts = new Timestamp(calendar.getTimeInMillis()); d = timeNanos / DateTimeUtils.NANOS_PER_DAY;
ts.setNanos(ts.getNanos() + nanos); } else {
return ts; 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 { ...@@ -1908,6 +1939,10 @@ public class Function extends Expression implements FunctionCall {
case MONTH: case MONTH:
return (DateTimeUtils.yearFromDateValue(dateValue2) - DateTimeUtils.yearFromDateValue(dateValue1)) * 12 return (DateTimeUtils.yearFromDateValue(dateValue2) - DateTimeUtils.yearFromDateValue(dateValue1)) * 12
+ DateTimeUtils.monthFromDateValue(dateValue2) - DateTimeUtils.monthFromDateValue(dateValue1); + 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: case YEAR:
return DateTimeUtils.yearFromDateValue(dateValue2) - DateTimeUtils.yearFromDateValue(dateValue1); return DateTimeUtils.yearFromDateValue(dateValue2) - DateTimeUtils.yearFromDateValue(dateValue1);
default: default:
......
...@@ -24,7 +24,6 @@ import org.h2.value.ValueTime; ...@@ -24,7 +24,6 @@ import org.h2.value.ValueTime;
import org.h2.value.ValueTimestamp; import org.h2.value.ValueTimestamp;
import org.h2.value.ValueTimestampTimeZone; import org.h2.value.ValueTimestampTimeZone;
/** /**
* This utility class contains time conversion functions. * This utility class contains time conversion functions.
* <p> * <p>
...@@ -590,6 +589,40 @@ public class DateTimeUtils { ...@@ -590,6 +589,40 @@ public class DateTimeUtils {
return new long[] {dateValue, timeNanos}; 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. * Get the year (positive or negative) from a calendar.
* *
...@@ -830,6 +863,26 @@ public class DateTimeUtils { ...@@ -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. * Verify if the specified date is valid.
* *
...@@ -842,24 +895,11 @@ public class DateTimeUtils { ...@@ -842,24 +895,11 @@ public class DateTimeUtils {
if (month < 1 || month > 12 || day < 1) { if (month < 1 || month > 12 || day < 1) {
return false; return false;
} }
if (year > 1582) { if (year == 1582 && month == 10) {
// 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) {
// special case: days 1582-10-05 .. 1582-10-14 don't exist // special case: days 1582-10-05 .. 1582-10-14 don't exist
return day <= 31 && (day < 5 || day > 14); return day < 5 || (day > 14 && day <= 31);
}
if (month != 2 && day <= NORMAL_DAYS_PER_MONTH[month]) {
return true;
} }
return day <= ((year & 3) != 0 ? 28 : 29); return day <= getDaysInMonth(year, month);
} }
/** /**
...@@ -985,6 +1025,38 @@ public class DateTimeUtils { ...@@ -985,6 +1025,38 @@ public class DateTimeUtils {
return (year << SHIFT_YEAR) | (month << SHIFT_MONTH) | day; 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 * Convert a UTC datetime in millis to an encoded date in the default
* timezone. * timezone.
......
...@@ -81,7 +81,7 @@ select d + t, t + d - t x from test; ...@@ -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; 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) > 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 > rows: 1
select 1 + d + t + 1 from test; select 1 + d + t + 1 from test;
...@@ -104,3 +104,36 @@ call dateadd('MS', 1, TIMESTAMP '2001-02-03 04:05:06.789001'); ...@@ -104,3 +104,36 @@ call dateadd('MS', 1, TIMESTAMP '2001-02-03 04:05:06.789001');
> -------------------------------------- > --------------------------------------
> 2001-02-03 04:05:06.790001 > 2001-02-03 04:05:06.790001
> rows: 1 > 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 ...@@ -194,3 +194,34 @@ SELECT DATEDIFF('WEEK', DATE '1969-12-28', DATE '1969-12-29'), DATEDIFF('ISO_WEE
> - - > - -
> 0 1 > 0 1
> rows: 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; ...@@ -7586,9 +7586,9 @@ SELECT * FROM TEST;
SELECT XD+1, XD-1, XD-XD FROM TEST; SELECT XD+1, XD-1, XD-XD FROM TEST;
> DATEADD('DAY', 1, XD) DATEADD('DAY', -1, XD) DATEDIFF('DAY', XD, XD) > 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 > 0001-02-04 0001-02-02 0
> 0004-05-07 00:00:00.0 0004-05-05 00:00:00.0 0 > 0004-05-07 0004-05-05 0
> 2000-01-01 00:00:00.0 1999-12-30 00:00:00.0 0 > 2000-01-01 1999-12-30 0
> null null null > null null null
> rows: 4 > rows: 4
......
...@@ -11,6 +11,8 @@ import java.util.GregorianCalendar; ...@@ -11,6 +11,8 @@ import java.util.GregorianCalendar;
import org.h2.test.TestBase; import org.h2.test.TestBase;
import org.h2.util.DateTimeUtils; import org.h2.util.DateTimeUtils;
import static org.h2.util.DateTimeUtils.dateValue;
/** /**
* Unit tests for the DateTimeUtils class * Unit tests for the DateTimeUtils class
*/ */
...@@ -30,6 +32,7 @@ public class TestDateTimeUtils extends TestBase { ...@@ -30,6 +32,7 @@ public class TestDateTimeUtils extends TestBase {
testParseTimeNanosDB2Format(); testParseTimeNanosDB2Format();
testDayOfWeek(); testDayOfWeek();
testWeekOfYear(); testWeekOfYear();
testDateValueFromDenormalizedDate();
} }
private void testParseTimeNanosDB2Format() { private void testParseTimeNanosDB2Format() {
...@@ -53,7 +56,7 @@ public class TestDateTimeUtils extends TestBase { ...@@ -53,7 +56,7 @@ public class TestDateTimeUtils extends TestBase {
if (gc.get(Calendar.ERA) == GregorianCalendar.BC) { if (gc.get(Calendar.ERA) == GregorianCalendar.BC) {
year = 1 - year; 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)); gc.get(Calendar.DAY_OF_MONTH));
long dateValue = DateTimeUtils.dateValueFromAbsoluteDay(i); long dateValue = DateTimeUtils.dateValueFromAbsoluteDay(i);
assertEquals(expectedDateValue, dateValue); assertEquals(expectedDateValue, dateValue);
...@@ -92,4 +95,15 @@ public class TestDateTimeUtils extends TestBase { ...@@ -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 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论