ESQL: Enhanced `DATE_TRUNC` with arbitrary intervals (#120302)
Originally, `DATE_TRUNC` only supported 1-month and 3-month intervals for months, and 1-year interval for years, while arbitrary intervals were supported for weeks and days. This PR adds support for `DATE_TRUNC` with arbitrary month and year intervals. Closes #120094
This commit is contained in:
parent
44a74f9fec
commit
30b2a1f729
|
@ -0,0 +1,6 @@
|
|||
pr: 120302
|
||||
summary: "ESQL: Enhanced `DATE_TRUNC` with arbitrary intervals"
|
||||
area: ES|QL
|
||||
type: enhancement
|
||||
issues:
|
||||
- 120094
|
|
@ -2,5 +2,6 @@
|
|||
|
||||
**Description**
|
||||
|
||||
Rounds down a date to the closest interval.
|
||||
Rounds down a date to the closest interval since epoch, which starts
|
||||
at `0001-01-01T00:00:00Z`.
|
||||
|
||||
|
|
|
@ -100,7 +100,8 @@ FROM employees
|
|||
|
||||
::::{note}
|
||||
When providing the bucket size as the second parameter, it must be a time
|
||||
duration or date period.
|
||||
duration or date period. Also the reference is epoch, which starts
|
||||
at `0001-01-01T00:00:00Z`.
|
||||
::::
|
||||
|
||||
`BUCKET` can also operate on numeric fields. For example, to create a salary histogram:
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"comment" : "This is generated by ESQL’s AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.",
|
||||
"type" : "scalar",
|
||||
"name" : "date_trunc",
|
||||
"description" : "Rounds down a date to the closest interval.",
|
||||
"description": "Rounds down a date to the closest interval since epoch, which starts at `0001-01-01T00:00:00Z`.",
|
||||
"signatures" : [
|
||||
{
|
||||
"params" : [
|
||||
|
|
|
@ -3,7 +3,9 @@ This is generated by ESQL’s AbstractFunctionTestCase. Do no edit it. See ../RE
|
|||
-->
|
||||
|
||||
### DATE_TRUNC
|
||||
Rounds down a date to the closest interval.
|
||||
|
||||
Rounds down a date to the closest interval since epoch, which starts
|
||||
at `0001-01-01T00:00:00Z`.
|
||||
|
||||
```esql
|
||||
FROM employees
|
||||
|
|
|
@ -59,8 +59,9 @@ public abstract class Rounding implements Writeable {
|
|||
WEEK_OF_WEEKYEAR((byte) 1, "week", IsoFields.WEEK_OF_WEEK_BASED_YEAR, true, TimeUnit.DAYS.toMillis(7)) {
|
||||
private final long extraLocalOffsetLookup = TimeUnit.DAYS.toMillis(7);
|
||||
|
||||
long roundFloor(long utcMillis) {
|
||||
return DateUtils.roundWeekOfWeekYear(utcMillis);
|
||||
@Override
|
||||
long roundFloor(long utcMillis, int multiplier) {
|
||||
return DateUtils.roundWeekIntervalOfWeekYear(utcMillis, multiplier);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -71,10 +72,12 @@ public abstract class Rounding implements Writeable {
|
|||
YEAR_OF_CENTURY((byte) 2, "year", ChronoField.YEAR_OF_ERA, false, 12) {
|
||||
private final long extraLocalOffsetLookup = TimeUnit.DAYS.toMillis(366);
|
||||
|
||||
long roundFloor(long utcMillis) {
|
||||
return DateUtils.roundYear(utcMillis);
|
||||
@Override
|
||||
long roundFloor(long utcMillis, int multiplier) {
|
||||
return multiplier == 1 ? DateUtils.roundYear(utcMillis) : DateUtils.roundYearInterval(utcMillis, multiplier);
|
||||
}
|
||||
|
||||
@Override
|
||||
long extraLocalOffsetLookup() {
|
||||
return extraLocalOffsetLookup;
|
||||
}
|
||||
|
@ -82,10 +85,14 @@ public abstract class Rounding implements Writeable {
|
|||
QUARTER_OF_YEAR((byte) 3, "quarter", IsoFields.QUARTER_OF_YEAR, false, 3) {
|
||||
private final long extraLocalOffsetLookup = TimeUnit.DAYS.toMillis(92);
|
||||
|
||||
long roundFloor(long utcMillis) {
|
||||
return DateUtils.roundQuarterOfYear(utcMillis);
|
||||
@Override
|
||||
long roundFloor(long utcMillis, int multiplier) {
|
||||
return multiplier == 1
|
||||
? DateUtils.roundQuarterOfYear(utcMillis)
|
||||
: DateUtils.roundIntervalMonthOfYear(utcMillis, multiplier * 3);
|
||||
}
|
||||
|
||||
@Override
|
||||
long extraLocalOffsetLookup() {
|
||||
return extraLocalOffsetLookup;
|
||||
}
|
||||
|
@ -93,28 +100,34 @@ public abstract class Rounding implements Writeable {
|
|||
MONTH_OF_YEAR((byte) 4, "month", ChronoField.MONTH_OF_YEAR, false, 1) {
|
||||
private final long extraLocalOffsetLookup = TimeUnit.DAYS.toMillis(31);
|
||||
|
||||
long roundFloor(long utcMillis) {
|
||||
return DateUtils.roundMonthOfYear(utcMillis);
|
||||
@Override
|
||||
long roundFloor(long utcMillis, int multiplier) {
|
||||
return multiplier == 1 ? DateUtils.roundMonthOfYear(utcMillis) : DateUtils.roundIntervalMonthOfYear(utcMillis, multiplier);
|
||||
}
|
||||
|
||||
@Override
|
||||
long extraLocalOffsetLookup() {
|
||||
return extraLocalOffsetLookup;
|
||||
}
|
||||
},
|
||||
DAY_OF_MONTH((byte) 5, "day", ChronoField.DAY_OF_MONTH, true, ChronoField.DAY_OF_MONTH.getBaseUnit().getDuration().toMillis()) {
|
||||
long roundFloor(long utcMillis) {
|
||||
return DateUtils.roundFloor(utcMillis, this.ratio);
|
||||
@Override
|
||||
long roundFloor(long utcMillis, int multiplier) {
|
||||
return DateUtils.roundFloor(utcMillis, this.ratio * multiplier);
|
||||
}
|
||||
|
||||
@Override
|
||||
long extraLocalOffsetLookup() {
|
||||
return ratio;
|
||||
}
|
||||
},
|
||||
HOUR_OF_DAY((byte) 6, "hour", ChronoField.HOUR_OF_DAY, true, ChronoField.HOUR_OF_DAY.getBaseUnit().getDuration().toMillis()) {
|
||||
long roundFloor(long utcMillis) {
|
||||
return DateUtils.roundFloor(utcMillis, ratio);
|
||||
@Override
|
||||
long roundFloor(long utcMillis, int multiplier) {
|
||||
return DateUtils.roundFloor(utcMillis, ratio * multiplier);
|
||||
}
|
||||
|
||||
@Override
|
||||
long extraLocalOffsetLookup() {
|
||||
return ratio;
|
||||
}
|
||||
|
@ -126,10 +139,12 @@ public abstract class Rounding implements Writeable {
|
|||
true,
|
||||
ChronoField.MINUTE_OF_HOUR.getBaseUnit().getDuration().toMillis()
|
||||
) {
|
||||
long roundFloor(long utcMillis) {
|
||||
return DateUtils.roundFloor(utcMillis, ratio);
|
||||
@Override
|
||||
long roundFloor(long utcMillis, int multiplier) {
|
||||
return DateUtils.roundFloor(utcMillis, ratio * multiplier);
|
||||
}
|
||||
|
||||
@Override
|
||||
long extraLocalOffsetLookup() {
|
||||
return ratio;
|
||||
}
|
||||
|
@ -141,10 +156,12 @@ public abstract class Rounding implements Writeable {
|
|||
true,
|
||||
ChronoField.SECOND_OF_MINUTE.getBaseUnit().getDuration().toMillis()
|
||||
) {
|
||||
long roundFloor(long utcMillis) {
|
||||
return DateUtils.roundFloor(utcMillis, ratio);
|
||||
@Override
|
||||
long roundFloor(long utcMillis, int multiplier) {
|
||||
return DateUtils.roundFloor(utcMillis, ratio * multiplier);
|
||||
}
|
||||
|
||||
@Override
|
||||
long extraLocalOffsetLookup() {
|
||||
return ratio;
|
||||
}
|
||||
|
@ -171,10 +188,11 @@ public abstract class Rounding implements Writeable {
|
|||
* This rounds down the supplied milliseconds since the epoch down to the next unit. In order to retain performance this method
|
||||
* should be as fast as possible and not try to convert dates to java-time objects if possible
|
||||
*
|
||||
* @param utcMillis the milliseconds since the epoch
|
||||
* @return the rounded down milliseconds since the epoch
|
||||
* @param utcMillis the milliseconds since the epoch
|
||||
* @param multiplier the factor by which the unit is multiplied
|
||||
* @return the rounded down milliseconds since the epoch
|
||||
*/
|
||||
abstract long roundFloor(long utcMillis);
|
||||
abstract long roundFloor(long utcMillis, int multiplier);
|
||||
|
||||
/**
|
||||
* When looking up {@link LocalTimeOffset} go this many milliseconds
|
||||
|
@ -329,17 +347,24 @@ public abstract class Rounding implements Writeable {
|
|||
|
||||
private final DateTimeUnit unit;
|
||||
private final long interval;
|
||||
private final int multiplier;
|
||||
|
||||
private ZoneId timeZone = ZoneOffset.UTC;
|
||||
private long offset = 0;
|
||||
|
||||
public Builder(DateTimeUnit unit) {
|
||||
this(unit, 1);
|
||||
}
|
||||
|
||||
public Builder(DateTimeUnit unit, int multiplier) {
|
||||
this.unit = unit;
|
||||
this.multiplier = multiplier;
|
||||
this.interval = -1;
|
||||
}
|
||||
|
||||
public Builder(TimeValue interval) {
|
||||
this.unit = null;
|
||||
this.multiplier = -1;
|
||||
if (interval.millis() < 1) throw new IllegalArgumentException("Zero or negative time interval not supported");
|
||||
this.interval = interval.millis();
|
||||
}
|
||||
|
@ -365,7 +390,7 @@ public abstract class Rounding implements Writeable {
|
|||
public Rounding build() {
|
||||
Rounding rounding;
|
||||
if (unit != null) {
|
||||
rounding = new TimeUnitRounding(unit, timeZone);
|
||||
rounding = new TimeUnitRounding(unit, multiplier, timeZone);
|
||||
} else {
|
||||
rounding = new TimeIntervalRounding(interval, timeZone);
|
||||
}
|
||||
|
@ -422,11 +447,17 @@ public abstract class Rounding implements Writeable {
|
|||
private final DateTimeUnit unit;
|
||||
private final ZoneId timeZone;
|
||||
private final boolean unitRoundsToMidnight;
|
||||
private final int multiplier;
|
||||
|
||||
TimeUnitRounding(DateTimeUnit unit, ZoneId timeZone) {
|
||||
this(unit, 1, timeZone);
|
||||
}
|
||||
|
||||
TimeUnitRounding(DateTimeUnit unit, int multiplier, ZoneId timeZone) {
|
||||
this.unit = unit;
|
||||
this.timeZone = timeZone;
|
||||
this.unitRoundsToMidnight = this.unit.field.getBaseUnit().getDuration().toMillis() > 3600000L;
|
||||
this.multiplier = multiplier;
|
||||
}
|
||||
|
||||
TimeUnitRounding(StreamInput in) throws IOException {
|
||||
|
@ -660,7 +691,7 @@ public abstract class Rounding implements Writeable {
|
|||
|
||||
@Override
|
||||
public long round(long utcMillis) {
|
||||
return offset.localToUtcInThisOffset(unit.roundFloor(offset.utcToLocalTime(utcMillis)));
|
||||
return offset.localToUtcInThisOffset(unit.roundFloor(offset.utcToLocalTime(utcMillis), multiplier));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -686,7 +717,7 @@ public abstract class Rounding implements Writeable {
|
|||
|
||||
@Override
|
||||
public long round(long utcMillis) {
|
||||
return offset.localToUtcInThisOffset(unit.roundFloor(offset.utcToLocalTime(utcMillis)));
|
||||
return offset.localToUtcInThisOffset(unit.roundFloor(offset.utcToLocalTime(utcMillis), multiplier));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -710,7 +741,7 @@ public abstract class Rounding implements Writeable {
|
|||
@Override
|
||||
public long round(long utcMillis) {
|
||||
LocalTimeOffset offset = lookup.lookup(utcMillis);
|
||||
return offset.localToUtc(unit.roundFloor(offset.utcToLocalTime(utcMillis)), this);
|
||||
return offset.localToUtc(unit.roundFloor(offset.utcToLocalTime(utcMillis), multiplier), this);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -764,14 +795,14 @@ public abstract class Rounding implements Writeable {
|
|||
@Override
|
||||
public long round(long utcMillis) {
|
||||
LocalTimeOffset offset = lookup.lookup(utcMillis);
|
||||
long roundedLocalMillis = unit.roundFloor(offset.utcToLocalTime(utcMillis));
|
||||
long roundedLocalMillis = unit.roundFloor(offset.utcToLocalTime(utcMillis), multiplier);
|
||||
return offset.localToUtc(roundedLocalMillis, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long inGap(long localMillis, Gap gap) {
|
||||
// Round from just before the start of the gap
|
||||
return gap.previous().localToUtc(unit.roundFloor(gap.firstMissingLocalTime() - 1), this);
|
||||
return gap.previous().localToUtc(unit.roundFloor(gap.firstMissingLocalTime() - 1, multiplier), this);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -350,8 +350,8 @@ public class DateUtils {
|
|||
}
|
||||
|
||||
/**
|
||||
* Rounds the given utc milliseconds sicne the epoch down to the next unit millis
|
||||
*
|
||||
* Rounds the given utc milliseconds since the epoch down to the next unit millis
|
||||
* <p>
|
||||
* Note: This does not check for correctness of the result, as this only works with units smaller or equal than a day
|
||||
* In order to ensure the performance of this methods, there are no guards or checks in it
|
||||
*
|
||||
|
@ -391,6 +391,49 @@ public class DateUtils {
|
|||
return DateUtils.of(year, month);
|
||||
}
|
||||
|
||||
/**
|
||||
* Round down to the beginning of the nearest multiple of the specified month interval based on the year
|
||||
* @param utcMillis the milliseconds since the epoch
|
||||
* @param monthInterval the interval in months to round down to
|
||||
*
|
||||
* @return The milliseconds since the epoch rounded down to the beginning of the nearest multiple of the
|
||||
* specified month interval based on the year
|
||||
*/
|
||||
public static long roundIntervalMonthOfYear(final long utcMillis, final int monthInterval) {
|
||||
if (monthInterval <= 0) {
|
||||
throw new IllegalArgumentException("month interval must be strictly positive, got [" + monthInterval + "]");
|
||||
}
|
||||
int year = getYear(utcMillis);
|
||||
int month = getMonthOfYear(utcMillis, year);
|
||||
|
||||
// Convert date to total months since epoch reference point (year 1 BCE boundary which is year 0)
|
||||
// 1. (year-1): Adjusts for 1-based year counting
|
||||
// 2. * 12: Converts years to months
|
||||
// 3. (month-1): Converts 1-based month to 0-based index
|
||||
int totalMonths = (year - 1) * 12 + (month - 1);
|
||||
|
||||
// Calculate interval index using floor division to handle negative values correctly
|
||||
// This ensures proper alignment for BCE dates (negative totalMonths)
|
||||
int quotient = Math.floorDiv(totalMonths, monthInterval);
|
||||
|
||||
// Calculate the starting month of the interval period
|
||||
int firstMonthOfInterval = quotient * monthInterval;
|
||||
|
||||
// Convert back to month-of-year (1-12):
|
||||
// 1. Calculate modulo 12 to get 0-11 month index
|
||||
// 2. Add 12 before final modulo to handle negative values
|
||||
// 3. Convert to 1-based month numbering
|
||||
int monthInYear = (firstMonthOfInterval % 12 + 12) % 12 + 1;
|
||||
|
||||
// Calculate corresponding year:
|
||||
// 1. Subtract month offset (monthInYear - 1) to get total months at year boundary
|
||||
// 2. Convert months to years
|
||||
// 3. Add 1 to adjust back to 1-based year counting
|
||||
int yearResult = (firstMonthOfInterval - (monthInYear - 1)) / 12 + 1;
|
||||
|
||||
return DateUtils.of(yearResult, monthInYear);
|
||||
}
|
||||
|
||||
/**
|
||||
* Round down to the beginning of the year of the specified time
|
||||
* @param utcMillis the milliseconds since the epoch
|
||||
|
@ -401,13 +444,59 @@ public class DateUtils {
|
|||
return utcMillisAtStartOfYear(year);
|
||||
}
|
||||
|
||||
/**
|
||||
* Round down to the beginning of the nearest multiple of the specified year interval
|
||||
* @param utcMillis the milliseconds since the epoch
|
||||
* @param yearInterval the interval in years to round down to
|
||||
*
|
||||
* @return The milliseconds since the epoch rounded down to the beginning of the nearest multiple of the specified year interval
|
||||
*/
|
||||
public static long roundYearInterval(final long utcMillis, final int yearInterval) {
|
||||
if (yearInterval <= 0) {
|
||||
throw new IllegalArgumentException("year interval must be strictly positive, got [" + yearInterval + "]");
|
||||
}
|
||||
int year = getYear(utcMillis);
|
||||
|
||||
// Convert date to total years since epoch reference point (year 1 BCE boundary which is year 0)
|
||||
int totalYears = year - 1;
|
||||
|
||||
// Calculate interval index using floor division to handle negative values correctly
|
||||
// This ensures proper alignment for BCE dates (negative totalYears)
|
||||
int quotient = Math.floorDiv(totalYears, yearInterval);
|
||||
|
||||
// Calculate the starting total years of the current interval
|
||||
int startTotalYears = quotient * yearInterval;
|
||||
|
||||
// Convert back to actual calendar year by adding 1 (reverse the base year adjustment)
|
||||
int startYear = startTotalYears + 1;
|
||||
|
||||
return utcMillisAtStartOfYear(startYear);
|
||||
}
|
||||
|
||||
/**
|
||||
* Round down to the beginning of the week based on week year of the specified time
|
||||
* @param utcMillis the milliseconds since the epoch
|
||||
* @return The milliseconds since the epoch rounded down to the beginning of the week based on week year
|
||||
*/
|
||||
public static long roundWeekOfWeekYear(final long utcMillis) {
|
||||
return roundFloor(utcMillis + 3 * 86400 * 1000L, 604800000) - 3 * 86400 * 1000L;
|
||||
return roundWeekIntervalOfWeekYear(utcMillis, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Round down to the beginning of the nearest multiple of the specified week interval based on week year
|
||||
* <p>
|
||||
* Consider Sun Dec 29 1969 00:00:00.000 as the start of the first week.
|
||||
* @param utcMillis the milliseconds since the epoch
|
||||
* @param weekInterval the interval in weeks to round down to
|
||||
*
|
||||
* @return The milliseconds since the epoch rounded down to the beginning of the nearest multiple of the
|
||||
* specified week interval based on week year
|
||||
*/
|
||||
public static long roundWeekIntervalOfWeekYear(final long utcMillis, final int weekInterval) {
|
||||
if (weekInterval <= 0) {
|
||||
throw new IllegalArgumentException("week interval must be strictly positive, got [" + weekInterval + "]");
|
||||
}
|
||||
return roundFloor(utcMillis + 3 * 86400 * 1000L, 604800000L * weekInterval) - 3 * 86400 * 1000L;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -267,6 +267,17 @@ public class DateUtilsTests extends ESTestCase {
|
|||
assertThat(DateUtils.roundMonthOfYear(1), is(0L));
|
||||
long dec1969 = LocalDate.of(1969, 12, 1).atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli();
|
||||
assertThat(DateUtils.roundMonthOfYear(-1), is(dec1969));
|
||||
|
||||
IllegalArgumentException exc = expectThrows(IllegalArgumentException.class, () -> DateUtils.roundIntervalMonthOfYear(0, -1));
|
||||
assertThat(exc.getMessage(), is("month interval must be strictly positive, got [-1]"));
|
||||
long epochMilli = LocalDate.of(1969, 10, 1).atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli();
|
||||
assertThat(DateUtils.roundIntervalMonthOfYear(1, 5), is(epochMilli));
|
||||
epochMilli = LocalDate.of(1969, 6, 1).atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli();
|
||||
assertThat(DateUtils.roundIntervalMonthOfYear(-1, 13), is(epochMilli));
|
||||
epochMilli = LocalDate.of(2024, 8, 1).atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli();
|
||||
assertThat(DateUtils.roundIntervalMonthOfYear(1737378896000L, 7), is(epochMilli));
|
||||
epochMilli = LocalDate.of(-2026, 4, 1).atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli();
|
||||
assertThat(DateUtils.roundIntervalMonthOfYear(-126068400000000L, 11), is(epochMilli));
|
||||
}
|
||||
|
||||
public void testRoundYear() {
|
||||
|
@ -276,9 +287,41 @@ public class DateUtilsTests extends ESTestCase {
|
|||
assertThat(DateUtils.roundYear(-1), is(startOf1969));
|
||||
long endOf1970 = ZonedDateTime.of(1970, 12, 31, 23, 59, 59, 999_999_999, ZoneOffset.UTC).toInstant().toEpochMilli();
|
||||
assertThat(DateUtils.roundYear(endOf1970), is(0L));
|
||||
// test with some leapyear
|
||||
// test with some leap year
|
||||
long endOf1996 = ZonedDateTime.of(1996, 12, 31, 23, 59, 59, 999_999_999, ZoneOffset.UTC).toInstant().toEpochMilli();
|
||||
long startOf1996 = Year.of(1996).atDay(1).atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli();
|
||||
assertThat(DateUtils.roundYear(endOf1996), is(startOf1996));
|
||||
|
||||
IllegalArgumentException exc = expectThrows(IllegalArgumentException.class, () -> DateUtils.roundYearInterval(0, -1));
|
||||
assertThat(exc.getMessage(), is("year interval must be strictly positive, got [-1]"));
|
||||
assertThat(DateUtils.roundYearInterval(0, 2), is(startOf1969));
|
||||
long startOf1968 = Year.of(1968).atDay(1).atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli();
|
||||
assertThat(DateUtils.roundYearInterval(0, 7), is(startOf1968));
|
||||
long startOf1966 = Year.of(1966).atDay(1).atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli();
|
||||
assertThat(DateUtils.roundYearInterval(1, 5), is(startOf1966));
|
||||
long startOf1961 = Year.of(1961).atDay(1).atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli();
|
||||
assertThat(DateUtils.roundYearInterval(-1, 10), is(startOf1961));
|
||||
long startOf1992 = Year.of(1992).atDay(1).atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli();
|
||||
assertThat(DateUtils.roundYearInterval(endOf1996, 11), is(startOf1992));
|
||||
long epochMilli = Year.of(-2034).atDay(1).atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli();
|
||||
assertThat(DateUtils.roundYearInterval(-126068400000000L, 11), is(epochMilli));
|
||||
}
|
||||
|
||||
public void testRoundWeek() {
|
||||
long epochMilli = Year.of(1969).atMonth(12).atDay(29).atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli();
|
||||
assertThat(DateUtils.roundWeekOfWeekYear(0), is(epochMilli));
|
||||
assertThat(DateUtils.roundWeekOfWeekYear(1), is(epochMilli));
|
||||
assertThat(DateUtils.roundWeekOfWeekYear(-1), is(epochMilli));
|
||||
|
||||
IllegalArgumentException exc = expectThrows(IllegalArgumentException.class, () -> DateUtils.roundWeekIntervalOfWeekYear(0, -1));
|
||||
assertThat(exc.getMessage(), is("week interval must be strictly positive, got [-1]"));
|
||||
assertThat(DateUtils.roundWeekIntervalOfWeekYear(0, 3), is(epochMilli));
|
||||
assertThat(DateUtils.roundWeekIntervalOfWeekYear(1, 3), is(epochMilli));
|
||||
assertThat(DateUtils.roundWeekIntervalOfWeekYear(-1, 2), is(epochMilli));
|
||||
|
||||
epochMilli = Year.of(2025).atMonth(1).atDay(20).atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli();
|
||||
assertThat(DateUtils.roundWeekOfWeekYear(1737378896000L), is(epochMilli));
|
||||
epochMilli = Year.of(2025).atMonth(1).atDay(13).atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli();
|
||||
assertThat(DateUtils.roundWeekIntervalOfWeekYear(1737378896000L, 4), is(epochMilli));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -815,3 +815,38 @@ c:long |b:date
|
|||
0 |null
|
||||
1 |1965-01-01T00:00:00.000Z
|
||||
;
|
||||
|
||||
bucketByYearInArbitraryIntervals
|
||||
required_capability: date_trunc_with_arbitrary_intervals
|
||||
|
||||
FROM employees
|
||||
| STATS c = COUNT(*) BY b = BUCKET(birth_date, 4 year)
|
||||
| SORT c DESC, b
|
||||
| LIMIT 5
|
||||
;
|
||||
|
||||
c:long | b:date
|
||||
28 | 1953-01-01T00:00:00.000Z
|
||||
28 | 1957-01-01T00:00:00.000Z
|
||||
25 | 1961-01-01T00:00:00.000Z
|
||||
10 | null
|
||||
8 | 1949-01-01T00:00:00.000Z
|
||||
;
|
||||
|
||||
|
||||
bucketByMonthInArbitraryIntervals
|
||||
required_capability: date_trunc_with_arbitrary_intervals
|
||||
|
||||
FROM employees
|
||||
| STATS c = COUNT(*) BY b = BUCKET(hire_date, 20 month)
|
||||
| SORT c DESC, b
|
||||
| LIMIT 5
|
||||
;
|
||||
|
||||
c:long | b:date
|
||||
23 | 1986-01-01T00:00:00.000Z
|
||||
22 | 1989-05-01T00:00:00.000Z
|
||||
15 | 1987-09-01T00:00:00.000Z
|
||||
11 | 1984-05-01T00:00:00.000Z
|
||||
11 | 1991-01-01T00:00:00.000Z
|
||||
;
|
||||
|
|
|
@ -1522,3 +1522,41 @@ FROM employees
|
|||
result:boolean
|
||||
null
|
||||
;
|
||||
|
||||
evalDateTruncYearInArbitraryIntervals
|
||||
required_capability: date_trunc_with_arbitrary_intervals
|
||||
|
||||
ROW x = ["1963-01-01", "1973-04-11", "1978-04-12", "0000-01-01", "-0006-01-01", "-0007-01-01"]::DATETIME
|
||||
| MV_EXPAND x
|
||||
| EVAL y = DATE_TRUNC(7 years, x)
|
||||
;
|
||||
|
||||
x:date | y:date
|
||||
1963-01-01T00:00:00.000Z | 1961-01-01T00:00:00.000Z
|
||||
1973-04-11T00:00:00.000Z | 1968-01-01T00:00:00.000Z
|
||||
1978-04-12T00:00:00.000Z | 1975-01-01T00:00:00.000Z
|
||||
0000-01-01T00:00:00.000Z | -0006-01-01T00:00:00.000Z
|
||||
-0006-01-01T00:00:00.000Z | -0006-01-01T00:00:00.000Z
|
||||
-0007-01-01T00:00:00.000Z | -0013-01-01T00:00:00.000Z
|
||||
;
|
||||
|
||||
evalDateTruncMonthInArbitraryIntervals
|
||||
required_capability: date_trunc_with_arbitrary_intervals
|
||||
|
||||
ROW x = ["1969-11-12", "1970-05-01", "1970-12-31", "1972-01-12", "0001-01-01", "0000-12-01", "-0001-12-01"]::DATETIME
|
||||
| MV_EXPAND x
|
||||
| EVAL y = DATE_TRUNC(7 months, x)
|
||||
;
|
||||
|
||||
x:date | y:date
|
||||
1969-11-12T00:00:00.000Z | 1969-10-01T00:00:00.000Z
|
||||
1970-05-01T00:00:00.000Z | 1970-05-01T00:00:00.000Z
|
||||
1970-12-31T00:00:00.000Z | 1970-12-01T00:00:00.000Z
|
||||
1972-01-12T00:00:00.000Z | 1971-07-01T00:00:00.000Z
|
||||
0001-01-01T00:00:00.000Z | 0001-01-01T00:00:00.000Z
|
||||
0000-12-01T00:00:00.000Z | 0000-06-01T00:00:00.000Z
|
||||
-0001-12-01T00:00:00.000Z | -0001-11-01T00:00:00.000Z
|
||||
|
||||
;
|
||||
|
||||
|
||||
|
|
|
@ -583,6 +583,11 @@ public class EsqlCapabilities {
|
|||
*/
|
||||
BUCKET_INCLUSIVE_UPPER_BOUND,
|
||||
|
||||
/**
|
||||
* Enhanced DATE_TRUNC with arbitrary month and year intervals. (#120302)
|
||||
*/
|
||||
DATE_TRUNC_WITH_ARBITRARY_INTERVALS,
|
||||
|
||||
/**
|
||||
* Changed error messages for fields with conflicting types in different indices.
|
||||
*/
|
||||
|
|
|
@ -140,7 +140,7 @@ public class Bucket extends GroupingFunction implements PostOptimizationVerifica
|
|||
argument, leaving the range out:""", file = "bucket", tag = "docsBucketWeeklyHistogramWithSpan", explanation = """
|
||||
::::{note}
|
||||
When providing the bucket size as the second parameter, it must be a time
|
||||
duration or date period.
|
||||
duration or date period. Also the reference is epoch, which starts at `0001-01-01T00:00:00Z`.
|
||||
::::"""),
|
||||
@Example(
|
||||
description = "`BUCKET` can also operate on numeric fields. For example, to create a salary histogram:",
|
||||
|
|
|
@ -63,7 +63,7 @@ public class DateTrunc extends EsqlScalarFunction {
|
|||
|
||||
@FunctionInfo(
|
||||
returnType = { "date", "date_nanos" },
|
||||
description = "Rounds down a date to the closest interval.",
|
||||
description = "Rounds down a date to the closest interval since epoch, which starts at `0001-01-01T00:00:00Z`.",
|
||||
examples = {
|
||||
@Example(file = "date", tag = "docsDateTrunc"),
|
||||
@Example(
|
||||
|
@ -190,14 +190,14 @@ public class DateTrunc extends EsqlScalarFunction {
|
|||
rounding = new Rounding.Builder(Rounding.DateTimeUnit.WEEK_OF_WEEKYEAR);
|
||||
} else if (period.getDays() > 1) {
|
||||
rounding = new Rounding.Builder(new TimeValue(period.getDays(), TimeUnit.DAYS));
|
||||
} else if (period.getMonths() == 1) {
|
||||
rounding = new Rounding.Builder(Rounding.DateTimeUnit.MONTH_OF_YEAR);
|
||||
} else if (period.getMonths() == 3) {
|
||||
// java.time.Period does not have a QUATERLY period, so a period of 3 months
|
||||
// java.time.Period does not have a QUARTERLY period, so a period of 3 months
|
||||
// returns a quarterly rounding
|
||||
rounding = new Rounding.Builder(Rounding.DateTimeUnit.QUARTER_OF_YEAR);
|
||||
} else if (period.getYears() == 1) {
|
||||
rounding = new Rounding.Builder(Rounding.DateTimeUnit.YEAR_OF_CENTURY);
|
||||
} else if (period.getMonths() > 0) {
|
||||
rounding = new Rounding.Builder(Rounding.DateTimeUnit.MONTH_OF_YEAR, period.getMonths());
|
||||
} else if (period.getYears() > 0) {
|
||||
rounding = new Rounding.Builder(Rounding.DateTimeUnit.YEAR_OF_CENTURY, period.getYears());
|
||||
} else {
|
||||
throw new IllegalArgumentException("Time interval is not supported");
|
||||
}
|
||||
|
|
|
@ -82,11 +82,14 @@ public class DateTruncRoundingTests extends ESTestCase {
|
|||
rounding = createRounding(Period.ofMonths(3));
|
||||
assertEquals(1, rounding.roundingSize(Rounding.DateTimeUnit.QUARTER_OF_YEAR), 0d);
|
||||
|
||||
rounding = createRounding(Period.ofMonths(5));
|
||||
assertEquals(1, rounding.roundingSize(Rounding.DateTimeUnit.MONTH_OF_YEAR), 0d);
|
||||
|
||||
rounding = createRounding(Period.ofYears(1));
|
||||
assertEquals(1, rounding.roundingSize(Rounding.DateTimeUnit.YEAR_OF_CENTURY), 0d);
|
||||
|
||||
e = expectThrows(IllegalArgumentException.class, () -> createRounding(Period.ofYears(3)));
|
||||
assertThat(e.getMessage(), containsString("Time interval is not supported"));
|
||||
rounding = createRounding(Period.ofYears(3));
|
||||
assertEquals(1, rounding.roundingSize(Rounding.DateTimeUnit.YEAR_OF_CENTURY), 0d);
|
||||
}
|
||||
|
||||
public void testCreateRoundingNullInterval() {
|
||||
|
|
|
@ -56,6 +56,10 @@ public class DateTruncTests extends AbstractScalarFunctionTestCase {
|
|||
suppliers.addAll(ofDuration(Duration.ofSeconds(30), ts, "2023-02-17T10:25:30.00Z"));
|
||||
suppliers.add(randomSecond());
|
||||
|
||||
// arbitrary period of months and years
|
||||
suppliers.addAll(ofDatePeriod(Period.ofMonths(7), ts, "2022-11-01T00:00:00.00Z"));
|
||||
suppliers.addAll(ofDatePeriod(Period.ofYears(5), ts, "2021-01-01T00:00:00.00Z"));
|
||||
|
||||
return parameterSuppliersFromTypedDataWithDefaultChecksNoErrors(true, suppliers);
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue