Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
b125437
Add `multiplier` parameter to `roundFloor` method in `DateTimeUnit`
kanoshiou Jan 16, 2025
a8eb484
Update changelog
kanoshiou Jan 16, 2025
060a5bb
Add tests for `DateUtils`
kanoshiou Jan 20, 2025
6b94a67
Add tests for `DateTrunc`
kanoshiou Jan 21, 2025
f0b6ffa
Merge branch 'main' into arbitrary-intervals-of-month-year-buckets
kanoshiou Jan 21, 2025
9ad78d4
Merge branch 'main' into arbitrary-intervals-of-month-year-buckets
bpintea Feb 18, 2025
8ac8e01
Apply suggestions from code review
kanoshiou Feb 19, 2025
32dfa96
Add `override` annotation
kanoshiou Feb 19, 2025
ac37cfa
Update testcases
kanoshiou Feb 19, 2025
5c25ce3
Merge branch 'main' into arbitrary-intervals-of-month-year-buckets
kanoshiou Feb 19, 2025
74d31da
Checkstyle
kanoshiou Feb 19, 2025
650eed7
Add more tests
kanoshiou Feb 19, 2025
ddf2923
Merge branch 'main' into arbitrary-intervals-of-month-year-buckets
kanoshiou Feb 19, 2025
585d4c0
reformat code
kanoshiou Feb 19, 2025
6b1368b
Merge branch 'main' into arbitrary-intervals-of-month-year-buckets
kanoshiou Feb 20, 2025
3214599
Resolve failing tests
kanoshiou Feb 20, 2025
8f290e0
fix bwc
kanoshiou Feb 20, 2025
1e4de28
Merge branch 'main' into arbitrary-intervals-of-month-year-buckets
kanoshiou Feb 20, 2025
eb77d06
Merge branch 'main' into arbitrary-intervals-of-month-year-buckets
kanoshiou Feb 24, 2025
e94b139
Update docs
kanoshiou Feb 24, 2025
db0afb3
Merge branch 'main' into arbitrary-intervals-of-month-year-buckets
kanoshiou Feb 26, 2025
708c9af
Update changelog
kanoshiou Mar 9, 2025
6bd9dfe
Merge branch 'refs/heads/main' into arbitrary-intervals-of-month-year…
kanoshiou Mar 9, 2025
d1d3ff6
Merge branch 'refs/heads/main' into arbitrary-intervals-of-month-year…
kanoshiou Mar 17, 2025
2dd6c62
Update docs
kanoshiou Mar 17, 2025
abf55f5
Merge branch 'main' into arbitrary-intervals-of-month-year-buckets
bpintea Mar 19, 2025
34e791f
Support for negative years
kanoshiou Mar 20, 2025
03a4a2d
Merge branch 'main' into arbitrary-intervals-of-month-year-buckets
kanoshiou Mar 20, 2025
05dbc3b
Update `DateTruncTests` + docs
kanoshiou Mar 21, 2025
ced2fdf
Merge branch 'main' into arbitrary-intervals-of-month-year-buckets
kanoshiou Mar 21, 2025
3dccdd6
Merge branch 'main' into arbitrary-intervals-of-month-year-buckets
bpintea Mar 26, 2025
aaa8799
Update tests
kanoshiou Mar 27, 2025
b3f1026
Merge branch 'main' into arbitrary-intervals-of-month-year-buckets
bpintea Mar 27, 2025
4874777
Minor performance improvement
kanoshiou Mar 27, 2025
dd3ec23
Comments
kanoshiou Mar 27, 2025
f9d170f
Merge branch 'main' into arbitrary-intervals-of-month-year-buckets
bpintea Mar 31, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/changelog/120302.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
pr: 120302
summary: "ESQL: Enhanced `DATE_TRUNC` with arbitrary intervals"
area: ES|QL
type: enhancement
issues:
- 120094

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

81 changes: 56 additions & 25 deletions server/src/main/java/org/elasticsearch/common/Rounding.java
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,9 @@ public enum DateTimeUnit {
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
Expand All @@ -71,50 +72,62 @@ long extraLocalOffsetLookup() {
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;
}
},
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;
}
},
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;
}
Expand All @@ -126,10 +139,12 @@ long extraLocalOffsetLookup() {
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;
}
Expand All @@ -141,10 +156,12 @@ long extraLocalOffsetLookup() {
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;
}
Expand All @@ -171,10 +188,11 @@ long extraLocalOffsetLookup() {
* 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
Expand Down Expand Up @@ -329,17 +347,24 @@ public static class Builder {

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();
}
Expand All @@ -365,7 +390,7 @@ public Builder offset(long offset) {
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);
}
Expand Down Expand Up @@ -422,11 +447,17 @@ static class TimeUnitRounding extends Rounding {
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 {
Expand Down Expand Up @@ -660,7 +691,7 @@ private class FixedToMidnightRounding extends TimeUnitPreparedRounding {

@Override
public long round(long utcMillis) {
return offset.localToUtcInThisOffset(unit.roundFloor(offset.utcToLocalTime(utcMillis)));
return offset.localToUtcInThisOffset(unit.roundFloor(offset.utcToLocalTime(utcMillis), multiplier));
}

@Override
Expand All @@ -686,7 +717,7 @@ private class FixedNotToMidnightRounding extends TimeUnitPreparedRounding {

@Override
public long round(long utcMillis) {
return offset.localToUtcInThisOffset(unit.roundFloor(offset.utcToLocalTime(utcMillis)));
return offset.localToUtcInThisOffset(unit.roundFloor(offset.utcToLocalTime(utcMillis), multiplier));
}

@Override
Expand All @@ -710,7 +741,7 @@ private class ToMidnightRounding extends TimeUnitPreparedRounding implements Loc
@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
Expand Down Expand Up @@ -764,14 +795,14 @@ private class NotToMidnightRounding extends AbstractNotToMidnightRounding implem
@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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -350,8 +350,8 @@ public static int compareNanosToMillis(long nanos, long millis) {
}

/**
* 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
*
Expand Down Expand Up @@ -391,6 +391,49 @@ public static long roundMonthOfYear(final long utcMillis) {
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
Expand All @@ -401,13 +444,59 @@ public static long roundYear(final long utcMillis) {
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;
}

/**
Expand Down
Loading
Loading