Optimize date rounding (#128687)

This commit is contained in:
Luigi Dell'Aquila 2025-06-10 10:56:56 +02:00 committed by GitHub
parent 15dd896a61
commit aa87f46681
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 137 additions and 64 deletions

View file

@ -64,7 +64,7 @@ public class AutoDateHistogramAggregationBuilder extends ValuesSourceAggregation
entry(Rounding.DateTimeUnit.MONTH_OF_YEAR, "month"), entry(Rounding.DateTimeUnit.MONTH_OF_YEAR, "month"),
entry(Rounding.DateTimeUnit.DAY_OF_MONTH, "day"), entry(Rounding.DateTimeUnit.DAY_OF_MONTH, "day"),
entry(Rounding.DateTimeUnit.HOUR_OF_DAY, "hour"), entry(Rounding.DateTimeUnit.HOUR_OF_DAY, "hour"),
entry(Rounding.DateTimeUnit.MINUTES_OF_HOUR, "minute"), entry(Rounding.DateTimeUnit.MINUTE_OF_HOUR, "minute"),
entry(Rounding.DateTimeUnit.SECOND_OF_MINUTE, "second") entry(Rounding.DateTimeUnit.SECOND_OF_MINUTE, "second")
); );
@ -84,7 +84,7 @@ public class AutoDateHistogramAggregationBuilder extends ValuesSourceAggregation
RoundingInfo[] roundings = new RoundingInfo[6]; RoundingInfo[] roundings = new RoundingInfo[6];
roundings[0] = new RoundingInfo(Rounding.DateTimeUnit.SECOND_OF_MINUTE, timeZone, 1000L, "s", 1, 5, 10, 30); roundings[0] = new RoundingInfo(Rounding.DateTimeUnit.SECOND_OF_MINUTE, timeZone, 1000L, "s", 1, 5, 10, 30);
roundings[1] = new RoundingInfo(Rounding.DateTimeUnit.MINUTES_OF_HOUR, timeZone, 60 * 1000L, "m", 1, 5, 10, 30); roundings[1] = new RoundingInfo(Rounding.DateTimeUnit.MINUTE_OF_HOUR, timeZone, 60 * 1000L, "m", 1, 5, 10, 30);
roundings[2] = new RoundingInfo(Rounding.DateTimeUnit.HOUR_OF_DAY, timeZone, 60 * 60 * 1000L, "h", 1, 3, 12); roundings[2] = new RoundingInfo(Rounding.DateTimeUnit.HOUR_OF_DAY, timeZone, 60 * 60 * 1000L, "h", 1, 3, 12);
roundings[3] = new RoundingInfo(Rounding.DateTimeUnit.DAY_OF_MONTH, timeZone, 24 * 60 * 60 * 1000L, "d", 1, 7); roundings[3] = new RoundingInfo(Rounding.DateTimeUnit.DAY_OF_MONTH, timeZone, 24 * 60 * 60 * 1000L, "d", 1, 7);
roundings[4] = new RoundingInfo(Rounding.DateTimeUnit.MONTH_OF_YEAR, timeZone, 30 * 24 * 60 * 60 * 1000L, "M", 1, 3); roundings[4] = new RoundingInfo(Rounding.DateTimeUnit.MONTH_OF_YEAR, timeZone, 30 * 24 * 60 * 60 * 1000L, "M", 1, 3);

View file

@ -108,7 +108,7 @@ public class InternalAutoDateHistogramTests extends AggregationMultiBucketAggreg
// an innerInterval that is quite large, such that targetBuckets * roundings[i].getMaximumInnerInterval() // an innerInterval that is quite large, such that targetBuckets * roundings[i].getMaximumInnerInterval()
// will be larger than the estimate. // will be larger than the estimate.
roundings[0] = new RoundingInfo(Rounding.DateTimeUnit.SECOND_OF_MINUTE, timeZone, 1000L, "s", 1000); roundings[0] = new RoundingInfo(Rounding.DateTimeUnit.SECOND_OF_MINUTE, timeZone, 1000L, "s", 1000);
roundings[1] = new RoundingInfo(Rounding.DateTimeUnit.MINUTES_OF_HOUR, timeZone, 60 * 1000L, "m", 1, 5, 10, 30); roundings[1] = new RoundingInfo(Rounding.DateTimeUnit.MINUTE_OF_HOUR, timeZone, 60 * 1000L, "m", 1, 5, 10, 30);
roundings[2] = new RoundingInfo(Rounding.DateTimeUnit.HOUR_OF_DAY, timeZone, 60 * 60 * 1000L, "h", 1, 3, 12); roundings[2] = new RoundingInfo(Rounding.DateTimeUnit.HOUR_OF_DAY, timeZone, 60 * 60 * 1000L, "h", 1, 3, 12);
OffsetDateTime timestamp = Instant.parse("2018-01-01T00:00:01.000Z").atOffset(ZoneOffset.UTC); OffsetDateTime timestamp = Instant.parse("2018-01-01T00:00:01.000Z").atOffset(ZoneOffset.UTC);

View file

@ -61,7 +61,8 @@ public abstract class Rounding implements Writeable {
@Override @Override
long roundFloor(long utcMillis, int multiplier) { long roundFloor(long utcMillis, int multiplier) {
return DateUtils.roundWeekIntervalOfWeekYear(utcMillis, multiplier); assert multiplier == 1;
return DateUtils.roundWeekOfWeekYear(utcMillis);
} }
@Override @Override
@ -74,7 +75,8 @@ public abstract class Rounding implements Writeable {
@Override @Override
long roundFloor(long utcMillis, int multiplier) { long roundFloor(long utcMillis, int multiplier) {
return multiplier == 1 ? DateUtils.roundYear(utcMillis) : DateUtils.roundYearInterval(utcMillis, multiplier); assert multiplier == 1;
return DateUtils.roundYear(utcMillis);
} }
@Override @Override
@ -87,9 +89,8 @@ public abstract class Rounding implements Writeable {
@Override @Override
long roundFloor(long utcMillis, int multiplier) { long roundFloor(long utcMillis, int multiplier) {
return multiplier == 1 assert multiplier == 1;
? DateUtils.roundQuarterOfYear(utcMillis) return DateUtils.roundQuarterOfYear(utcMillis);
: DateUtils.roundIntervalMonthOfYear(utcMillis, multiplier * 3);
} }
@Override @Override
@ -102,7 +103,8 @@ public abstract class Rounding implements Writeable {
@Override @Override
long roundFloor(long utcMillis, int multiplier) { long roundFloor(long utcMillis, int multiplier) {
return multiplier == 1 ? DateUtils.roundMonthOfYear(utcMillis) : DateUtils.roundIntervalMonthOfYear(utcMillis, multiplier); assert multiplier == 1;
return DateUtils.roundMonthOfYear(utcMillis);
} }
@Override @Override
@ -113,7 +115,8 @@ public abstract class Rounding implements Writeable {
DAY_OF_MONTH((byte) 5, "day", ChronoField.DAY_OF_MONTH, true, ChronoField.DAY_OF_MONTH.getBaseUnit().getDuration().toMillis()) { DAY_OF_MONTH((byte) 5, "day", ChronoField.DAY_OF_MONTH, true, ChronoField.DAY_OF_MONTH.getBaseUnit().getDuration().toMillis()) {
@Override @Override
long roundFloor(long utcMillis, int multiplier) { long roundFloor(long utcMillis, int multiplier) {
return DateUtils.roundFloor(utcMillis, this.ratio * multiplier); assert multiplier == 1;
return DateUtils.roundFloor(utcMillis, this.ratio);
} }
@Override @Override
@ -124,7 +127,8 @@ public abstract class Rounding implements Writeable {
HOUR_OF_DAY((byte) 6, "hour", ChronoField.HOUR_OF_DAY, true, ChronoField.HOUR_OF_DAY.getBaseUnit().getDuration().toMillis()) { HOUR_OF_DAY((byte) 6, "hour", ChronoField.HOUR_OF_DAY, true, ChronoField.HOUR_OF_DAY.getBaseUnit().getDuration().toMillis()) {
@Override @Override
long roundFloor(long utcMillis, int multiplier) { long roundFloor(long utcMillis, int multiplier) {
return DateUtils.roundFloor(utcMillis, ratio * multiplier); assert multiplier == 1;
return DateUtils.roundFloor(utcMillis, ratio);
} }
@Override @Override
@ -132,7 +136,7 @@ public abstract class Rounding implements Writeable {
return ratio; return ratio;
} }
}, },
MINUTES_OF_HOUR( MINUTE_OF_HOUR(
(byte) 7, (byte) 7,
"minute", "minute",
ChronoField.MINUTE_OF_HOUR, ChronoField.MINUTE_OF_HOUR,
@ -141,7 +145,8 @@ public abstract class Rounding implements Writeable {
) { ) {
@Override @Override
long roundFloor(long utcMillis, int multiplier) { long roundFloor(long utcMillis, int multiplier) {
return DateUtils.roundFloor(utcMillis, ratio * multiplier); assert multiplier == 1;
return DateUtils.roundFloor(utcMillis, ratio);
} }
@Override @Override
@ -158,13 +163,40 @@ public abstract class Rounding implements Writeable {
) { ) {
@Override @Override
long roundFloor(long utcMillis, int multiplier) { long roundFloor(long utcMillis, int multiplier) {
return DateUtils.roundFloor(utcMillis, ratio * multiplier); assert multiplier == 1;
return DateUtils.roundFloor(utcMillis, ratio);
} }
@Override @Override
long extraLocalOffsetLookup() { long extraLocalOffsetLookup() {
return ratio; return ratio;
} }
},
YEARS_OF_CENTURY((byte) 9, "years", ChronoField.YEAR_OF_ERA, false, 12) {
private final long extraLocalOffsetLookup = TimeUnit.DAYS.toMillis(366);
@Override
long roundFloor(long utcMillis, int multiplier) {
return multiplier == 1 ? DateUtils.roundYear(utcMillis) : DateUtils.roundYearInterval(utcMillis, multiplier);
}
@Override
long extraLocalOffsetLookup() {
return extraLocalOffsetLookup;
}
},
MONTHS_OF_YEAR((byte) 10, "months", ChronoField.MONTH_OF_YEAR, false, 1) {
private final long extraLocalOffsetLookup = TimeUnit.DAYS.toMillis(31);
@Override
long roundFloor(long utcMillis, int multiplier) {
return multiplier == 1 ? DateUtils.roundMonthOfYear(utcMillis) : DateUtils.roundIntervalMonthOfYear(utcMillis, multiplier);
}
@Override
long extraLocalOffsetLookup() {
return extraLocalOffsetLookup;
}
}; };
private final byte id; private final byte id;
@ -222,8 +254,10 @@ public abstract class Rounding implements Writeable {
case 4 -> MONTH_OF_YEAR; case 4 -> MONTH_OF_YEAR;
case 5 -> DAY_OF_MONTH; case 5 -> DAY_OF_MONTH;
case 6 -> HOUR_OF_DAY; case 6 -> HOUR_OF_DAY;
case 7 -> MINUTES_OF_HOUR; case 7 -> MINUTE_OF_HOUR;
case 8 -> SECOND_OF_MINUTE; case 8 -> SECOND_OF_MINUTE;
case 9 -> YEARS_OF_CENTURY;
case 10 -> MONTHS_OF_YEAR;
default -> throw new ElasticsearchException("Unknown date time unit id [" + id + "]"); default -> throw new ElasticsearchException("Unknown date time unit id [" + id + "]");
}; };
} }
@ -480,7 +514,7 @@ public abstract class Rounding implements Writeable {
case SECOND_OF_MINUTE: case SECOND_OF_MINUTE:
return localDateTime.withNano(0); return localDateTime.withNano(0);
case MINUTES_OF_HOUR: case MINUTE_OF_HOUR:
return LocalDateTime.of( return LocalDateTime.of(
localDateTime.getYear(), localDateTime.getYear(),
localDateTime.getMonthValue(), localDateTime.getMonthValue(),
@ -517,6 +551,12 @@ public abstract class Rounding implements Writeable {
case YEAR_OF_CENTURY: case YEAR_OF_CENTURY:
return LocalDateTime.of(LocalDate.of(localDateTime.getYear(), 1, 1), LocalTime.MIDNIGHT); return LocalDateTime.of(LocalDate.of(localDateTime.getYear(), 1, 1), LocalTime.MIDNIGHT);
case YEARS_OF_CENTURY:
return LocalDateTime.of(LocalDate.of(localDateTime.getYear(), 1, 1), LocalTime.MIDNIGHT);
case MONTHS_OF_YEAR:
return LocalDateTime.of(localDateTime.getYear(), localDateTime.getMonthValue(), 1, 0, 0);
default: default:
throw new IllegalArgumentException("NOT YET IMPLEMENTED for unit " + unit); throw new IllegalArgumentException("NOT YET IMPLEMENTED for unit " + unit);
} }
@ -879,6 +919,8 @@ public abstract class Rounding implements Writeable {
case MONTH_OF_YEAR -> localMidnight.plus(1, ChronoUnit.MONTHS); case MONTH_OF_YEAR -> localMidnight.plus(1, ChronoUnit.MONTHS);
case QUARTER_OF_YEAR -> localMidnight.plus(3, ChronoUnit.MONTHS); case QUARTER_OF_YEAR -> localMidnight.plus(3, ChronoUnit.MONTHS);
case YEAR_OF_CENTURY -> localMidnight.plus(1, ChronoUnit.YEARS); case YEAR_OF_CENTURY -> localMidnight.plus(1, ChronoUnit.YEARS);
case YEARS_OF_CENTURY -> localMidnight.plus(1, ChronoUnit.YEARS);
case MONTHS_OF_YEAR -> localMidnight.plus(1, ChronoUnit.MONTHS);
default -> throw new IllegalArgumentException("Unknown round-to-midnight unit: " + unit); default -> throw new IllegalArgumentException("Unknown round-to-midnight unit: " + unit);
}; };
} }

View file

@ -479,24 +479,7 @@ public class DateUtils {
* @return The milliseconds since the epoch rounded down to the beginning of the week based on week year * @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) { public static long roundWeekOfWeekYear(final long utcMillis) {
return roundWeekIntervalOfWeekYear(utcMillis, 1); return roundFloor(utcMillis + 3 * 86400 * 1000L, 604800000L) - 3 * 86400 * 1000L;
}
/**
* 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;
} }
/** /**

View file

@ -67,10 +67,12 @@ public class DateHistogramAggregationBuilder extends ValuesSourceAggregationBuil
entry("1d", Rounding.DateTimeUnit.DAY_OF_MONTH), entry("1d", Rounding.DateTimeUnit.DAY_OF_MONTH),
entry("hour", Rounding.DateTimeUnit.HOUR_OF_DAY), entry("hour", Rounding.DateTimeUnit.HOUR_OF_DAY),
entry("1h", Rounding.DateTimeUnit.HOUR_OF_DAY), entry("1h", Rounding.DateTimeUnit.HOUR_OF_DAY),
entry("minute", Rounding.DateTimeUnit.MINUTES_OF_HOUR), entry("minute", Rounding.DateTimeUnit.MINUTE_OF_HOUR),
entry("1m", Rounding.DateTimeUnit.MINUTES_OF_HOUR), entry("1m", Rounding.DateTimeUnit.MINUTE_OF_HOUR),
entry("second", Rounding.DateTimeUnit.SECOND_OF_MINUTE), entry("second", Rounding.DateTimeUnit.SECOND_OF_MINUTE),
entry("1s", Rounding.DateTimeUnit.SECOND_OF_MINUTE) entry("1s", Rounding.DateTimeUnit.SECOND_OF_MINUTE),
entry("years", Rounding.DateTimeUnit.YEARS_OF_CENTURY),
entry("months", Rounding.DateTimeUnit.MONTHS_OF_YEAR)
); );
public static final ObjectParser<DateHistogramAggregationBuilder, String> PARSER = ObjectParser.fromBuilder( public static final ObjectParser<DateHistogramAggregationBuilder, String> PARSER = ObjectParser.fromBuilder(

View file

@ -69,13 +69,22 @@ public class RoundingTests extends ESTestCase {
assertThat(tzRounding.round(time("2012-01-10T01:01:01")), isDate(time("2012-01-01T00:00:00.000Z"), tz)); assertThat(tzRounding.round(time("2012-01-10T01:01:01")), isDate(time("2012-01-01T00:00:00.000Z"), tz));
assertThat(tzRounding.nextRoundingValue(time("2012-01-09T00:00:00.000Z")), isDate(time("2013-01-01T00:00:00.000Z"), tz)); assertThat(tzRounding.nextRoundingValue(time("2012-01-09T00:00:00.000Z")), isDate(time("2013-01-01T00:00:00.000Z"), tz));
tzRounding = Rounding.builder(Rounding.DateTimeUnit.MINUTES_OF_HOUR).build(); tzRounding = Rounding.builder(Rounding.DateTimeUnit.MINUTE_OF_HOUR).build();
assertThat(tzRounding.round(time("2012-01-10T01:01:01")), isDate(time("2012-01-10T01:01:00.000Z"), tz)); assertThat(tzRounding.round(time("2012-01-10T01:01:01")), isDate(time("2012-01-10T01:01:00.000Z"), tz));
assertThat(tzRounding.nextRoundingValue(time("2012-01-09T00:00:00.000Z")), isDate(time("2012-01-09T00:01:00.000Z"), tz)); assertThat(tzRounding.nextRoundingValue(time("2012-01-09T00:00:00.000Z")), isDate(time("2012-01-09T00:01:00.000Z"), tz));
tzRounding = Rounding.builder(Rounding.DateTimeUnit.SECOND_OF_MINUTE).build(); tzRounding = Rounding.builder(Rounding.DateTimeUnit.SECOND_OF_MINUTE).build();
assertThat(tzRounding.round(time("2012-01-10T01:01:01")), isDate(time("2012-01-10T01:01:01.000Z"), tz)); assertThat(tzRounding.round(time("2012-01-10T01:01:01")), isDate(time("2012-01-10T01:01:01.000Z"), tz));
assertThat(tzRounding.nextRoundingValue(time("2012-01-09T00:00:00.000Z")), isDate(time("2012-01-09T00:00:01.000Z"), tz)); assertThat(tzRounding.nextRoundingValue(time("2012-01-09T00:00:00.000Z")), isDate(time("2012-01-09T00:00:01.000Z"), tz));
tzRounding = Rounding.builder(Rounding.DateTimeUnit.YEARS_OF_CENTURY).build();
assertThat(tzRounding.round(time("2012-01-10T01:01:01")), isDate(time("2012-01-01T00:00:00.000Z"), tz));
assertThat(tzRounding.nextRoundingValue(time("2012-01-09T00:00:00.000Z")), isDate(time("2013-01-01T00:00:00.000Z"), tz));
tzRounding = Rounding.builder(Rounding.DateTimeUnit.MONTHS_OF_YEAR).build();
tz = ZoneOffset.UTC;
assertThat(tzRounding.round(time("2009-02-03T01:01:01")), isDate(time("2009-02-01T00:00:00.000Z"), tz));
assertThat(tzRounding.nextRoundingValue(time("2009-02-01T00:00:00.000Z")), isDate(time("2009-03-01T00:00:00.000Z"), tz));
} }
public void testUTCIntervalRounding() { public void testUTCIntervalRounding() {
@ -135,6 +144,9 @@ public class RoundingTests extends ESTestCase {
tzRounding = Rounding.builder(Rounding.DateTimeUnit.MONTH_OF_YEAR).timeZone(tz).build(); tzRounding = Rounding.builder(Rounding.DateTimeUnit.MONTH_OF_YEAR).timeZone(tz).build();
assertThat(tzRounding.round(time("2012-04-01T04:15:30Z")), equalTo(time("2012-03-01T08:00:00Z"))); assertThat(tzRounding.round(time("2012-04-01T04:15:30Z")), equalTo(time("2012-03-01T08:00:00Z")));
tzRounding = Rounding.builder(Rounding.DateTimeUnit.MONTHS_OF_YEAR).timeZone(tz).build();
assertThat(tzRounding.round(time("2012-04-01T04:15:30Z")), equalTo(time("2012-03-01T08:00:00Z")));
// date in Feb-3rd, but still in Feb-2nd in -02:00 timezone // date in Feb-3rd, but still in Feb-2nd in -02:00 timezone
tz = ZoneId.of("-02:00"); tz = ZoneId.of("-02:00");
tzRounding = Rounding.builder(Rounding.DateTimeUnit.DAY_OF_MONTH).timeZone(tz).build(); tzRounding = Rounding.builder(Rounding.DateTimeUnit.DAY_OF_MONTH).timeZone(tz).build();
@ -645,6 +657,21 @@ public class RoundingTests extends ESTestCase {
// Two timestamps in same year and different timezone offset ("Double buckets" issue - #9491) // Two timestamps in same year and different timezone offset ("Double buckets" issue - #9491)
tzRounding = Rounding.builder(Rounding.DateTimeUnit.YEAR_OF_CENTURY).timeZone(tz).build(); tzRounding = Rounding.builder(Rounding.DateTimeUnit.YEAR_OF_CENTURY).timeZone(tz).build();
assertThat(tzRounding.round(time("2014-11-11T17:00:00", tz)), isDate(tzRounding.round(time("2014-08-11T17:00:00", tz)), tz)); assertThat(tzRounding.round(time("2014-11-11T17:00:00", tz)), isDate(tzRounding.round(time("2014-08-11T17:00:00", tz)), tz));
// Month interval
tzRounding = Rounding.builder(Rounding.DateTimeUnit.MONTHS_OF_YEAR).timeZone(tz).build();
assertThat(tzRounding.round(time("2014-11-11T17:00:00", tz)), isDate(time("2014-11-01T00:00:00", tz), tz));
// DST on
assertThat(tzRounding.round(time("2014-10-10T17:00:00", tz)), isDate(time("2014-10-01T00:00:00", tz), tz));
// Year interval
tzRounding = Rounding.builder(Rounding.DateTimeUnit.YEARS_OF_CENTURY).timeZone(tz).build();
assertThat(tzRounding.round(time("2014-11-11T17:00:00", tz)), isDate(time("2014-01-01T00:00:00", tz), tz));
// Two timestamps in same year and different timezone offset ("Double buckets" issue - #9491)
tzRounding = Rounding.builder(Rounding.DateTimeUnit.YEARS_OF_CENTURY).timeZone(tz).build();
assertThat(tzRounding.round(time("2014-11-11T17:00:00", tz)), isDate(tzRounding.round(time("2014-08-11T17:00:00", tz)), tz));
} }
/** /**
@ -656,7 +683,7 @@ public class RoundingTests extends ESTestCase {
long start = time("2014-10-18T20:50:00.000", tz); long start = time("2014-10-18T20:50:00.000", tz);
long end = time("2014-10-19T01:00:00.000", tz); long end = time("2014-10-19T01:00:00.000", tz);
Rounding tzRounding = new Rounding.TimeUnitRounding(Rounding.DateTimeUnit.MINUTES_OF_HOUR, tz); Rounding tzRounding = new Rounding.TimeUnitRounding(Rounding.DateTimeUnit.MINUTE_OF_HOUR, tz);
Rounding dayTzRounding = new Rounding.TimeIntervalRounding(60000, tz); Rounding dayTzRounding = new Rounding.TimeIntervalRounding(60000, tz);
for (long time = start; time < end; time = time + 60000) { for (long time = start; time < end; time = time + 60000) {
assertThat(tzRounding.nextRoundingValue(time), greaterThan(time)); assertThat(tzRounding.nextRoundingValue(time), greaterThan(time));
@ -1045,10 +1072,7 @@ public class RoundingTests extends ESTestCase {
prepared.roundingSize(time("2015-01-01T00:00:00.000Z"), Rounding.DateTimeUnit.SECOND_OF_MINUTE), prepared.roundingSize(time("2015-01-01T00:00:00.000Z"), Rounding.DateTimeUnit.SECOND_OF_MINUTE),
closeTo(36000.0, 0.000001) closeTo(36000.0, 0.000001)
); );
assertThat( assertThat(prepared.roundingSize(time("2015-01-01T00:00:00.000Z"), Rounding.DateTimeUnit.MINUTE_OF_HOUR), closeTo(600.0, 0.000001));
prepared.roundingSize(time("2015-01-01T00:00:00.000Z"), Rounding.DateTimeUnit.MINUTES_OF_HOUR),
closeTo(600.0, 0.000001)
);
assertThat(prepared.roundingSize(time("2015-01-01T00:00:00.000Z"), Rounding.DateTimeUnit.HOUR_OF_DAY), closeTo(10.0, 0.000001)); assertThat(prepared.roundingSize(time("2015-01-01T00:00:00.000Z"), Rounding.DateTimeUnit.HOUR_OF_DAY), closeTo(10.0, 0.000001));
assertThat( assertThat(
prepared.roundingSize(time("2015-01-01T00:00:00.000Z"), Rounding.DateTimeUnit.DAY_OF_MONTH), prepared.roundingSize(time("2015-01-01T00:00:00.000Z"), Rounding.DateTimeUnit.DAY_OF_MONTH),
@ -1091,6 +1115,30 @@ public class RoundingTests extends ESTestCase {
+ "only week, day, hour, minute and second are supported for this histogram" + "only week, day, hour, minute and second are supported for this histogram"
) )
); );
ex = expectThrows(
IllegalArgumentException.class,
() -> prepared.roundingSize(time("2015-01-01T00:00:00.000Z"), Rounding.DateTimeUnit.MONTHS_OF_YEAR)
);
assertThat(
ex.getMessage(),
equalTo(
"Cannot use month-based rate unit [months] with fixed interval based histogram, "
+ "only week, day, hour, minute and second are supported for this histogram"
)
);
ex = expectThrows(
IllegalArgumentException.class,
() -> prepared.roundingSize(time("2015-01-01T00:00:00.000Z"), Rounding.DateTimeUnit.YEARS_OF_CENTURY)
);
assertThat(
ex.getMessage(),
equalTo(
"Cannot use month-based rate unit [years] with fixed interval based histogram, "
+ "only week, day, hour, minute and second are supported for this histogram"
)
);
} }
public void testMillisecondsBasedUnitCalendarRoundingSize() { public void testMillisecondsBasedUnitCalendarRoundingSize() {
@ -1101,8 +1149,8 @@ public class RoundingTests extends ESTestCase {
closeTo(3600.0, 0.000001) closeTo(3600.0, 0.000001)
); );
assertThat(prepared.roundingSize(Rounding.DateTimeUnit.SECOND_OF_MINUTE), closeTo(3600.0, 0.000001)); assertThat(prepared.roundingSize(Rounding.DateTimeUnit.SECOND_OF_MINUTE), closeTo(3600.0, 0.000001));
assertThat(prepared.roundingSize(time("2015-01-01T00:00:00.000Z"), Rounding.DateTimeUnit.MINUTES_OF_HOUR), closeTo(60.0, 0.000001)); assertThat(prepared.roundingSize(time("2015-01-01T00:00:00.000Z"), Rounding.DateTimeUnit.MINUTE_OF_HOUR), closeTo(60.0, 0.000001));
assertThat(prepared.roundingSize(Rounding.DateTimeUnit.MINUTES_OF_HOUR), closeTo(60.0, 0.000001)); assertThat(prepared.roundingSize(Rounding.DateTimeUnit.MINUTE_OF_HOUR), closeTo(60.0, 0.000001));
assertThat(prepared.roundingSize(time("2015-01-01T00:00:00.000Z"), Rounding.DateTimeUnit.HOUR_OF_DAY), closeTo(1.0, 0.000001)); assertThat(prepared.roundingSize(time("2015-01-01T00:00:00.000Z"), Rounding.DateTimeUnit.HOUR_OF_DAY), closeTo(1.0, 0.000001));
assertThat(prepared.roundingSize(Rounding.DateTimeUnit.HOUR_OF_DAY), closeTo(1.0, 0.000001)); assertThat(prepared.roundingSize(Rounding.DateTimeUnit.HOUR_OF_DAY), closeTo(1.0, 0.000001));
assertThat( assertThat(
@ -1180,14 +1228,17 @@ public class RoundingTests extends ESTestCase {
long firstQuarter = prepared.round(time("2015-01-01T00:00:00.000Z")); long firstQuarter = prepared.round(time("2015-01-01T00:00:00.000Z"));
// Ratio based // Ratio based
assertThat(prepared.roundingSize(firstQuarter, Rounding.DateTimeUnit.MONTH_OF_YEAR), closeTo(3.0, 0.000001)); assertThat(prepared.roundingSize(firstQuarter, Rounding.DateTimeUnit.MONTH_OF_YEAR), closeTo(3.0, 0.000001));
assertThat(prepared.roundingSize(firstQuarter, Rounding.DateTimeUnit.MONTHS_OF_YEAR), closeTo(3.0, 0.000001));
assertThat(prepared.roundingSize(firstQuarter, Rounding.DateTimeUnit.QUARTER_OF_YEAR), closeTo(1.0, 0.000001)); assertThat(prepared.roundingSize(firstQuarter, Rounding.DateTimeUnit.QUARTER_OF_YEAR), closeTo(1.0, 0.000001));
assertThat(prepared.roundingSize(firstQuarter, Rounding.DateTimeUnit.YEAR_OF_CENTURY), closeTo(0.25, 0.000001)); assertThat(prepared.roundingSize(firstQuarter, Rounding.DateTimeUnit.YEAR_OF_CENTURY), closeTo(0.25, 0.000001));
assertThat(prepared.roundingSize(firstQuarter, Rounding.DateTimeUnit.YEARS_OF_CENTURY), closeTo(0.25, 0.000001));
assertThat(prepared.roundingSize(Rounding.DateTimeUnit.MONTH_OF_YEAR), closeTo(3.0, 0.000001)); assertThat(prepared.roundingSize(Rounding.DateTimeUnit.MONTH_OF_YEAR), closeTo(3.0, 0.000001));
assertThat(prepared.roundingSize(Rounding.DateTimeUnit.MONTHS_OF_YEAR), closeTo(3.0, 0.000001));
assertThat(prepared.roundingSize(Rounding.DateTimeUnit.QUARTER_OF_YEAR), closeTo(1.0, 0.000001)); assertThat(prepared.roundingSize(Rounding.DateTimeUnit.QUARTER_OF_YEAR), closeTo(1.0, 0.000001));
assertThat(prepared.roundingSize(Rounding.DateTimeUnit.YEAR_OF_CENTURY), closeTo(0.25, 0.000001)); assertThat(prepared.roundingSize(Rounding.DateTimeUnit.YEAR_OF_CENTURY), closeTo(0.25, 0.000001));
// Real interval based // Real interval based
assertThat(prepared.roundingSize(firstQuarter, Rounding.DateTimeUnit.SECOND_OF_MINUTE), closeTo(7776000.0, 0.000001)); assertThat(prepared.roundingSize(firstQuarter, Rounding.DateTimeUnit.SECOND_OF_MINUTE), closeTo(7776000.0, 0.000001));
assertThat(prepared.roundingSize(firstQuarter, Rounding.DateTimeUnit.MINUTES_OF_HOUR), closeTo(129600.0, 0.000001)); assertThat(prepared.roundingSize(firstQuarter, Rounding.DateTimeUnit.MINUTE_OF_HOUR), closeTo(129600.0, 0.000001));
assertThat(prepared.roundingSize(firstQuarter, Rounding.DateTimeUnit.HOUR_OF_DAY), closeTo(2160.0, 0.000001)); assertThat(prepared.roundingSize(firstQuarter, Rounding.DateTimeUnit.HOUR_OF_DAY), closeTo(2160.0, 0.000001));
assertThat(prepared.roundingSize(firstQuarter, Rounding.DateTimeUnit.DAY_OF_MONTH), closeTo(90.0, 0.000001)); assertThat(prepared.roundingSize(firstQuarter, Rounding.DateTimeUnit.DAY_OF_MONTH), closeTo(90.0, 0.000001));
long thirdQuarter = prepared.round(time("2015-07-01T00:00:00.000Z")); long thirdQuarter = prepared.round(time("2015-07-01T00:00:00.000Z"));

View file

@ -313,15 +313,7 @@ public class DateUtilsTests extends ESTestCase {
assertThat(DateUtils.roundWeekOfWeekYear(1), is(epochMilli)); assertThat(DateUtils.roundWeekOfWeekYear(1), 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(); epochMilli = Year.of(2025).atMonth(1).atDay(20).atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli();
assertThat(DateUtils.roundWeekOfWeekYear(1737378896000L), is(epochMilli)); 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));
} }
} }

View file

@ -61,7 +61,7 @@ public class InternalResetTrackingRateTests extends InternalAggregationTestCase<
} }
public void testReductionMinute() { public void testReductionMinute() {
testReduction(Rounding.DateTimeUnit.MINUTES_OF_HOUR, 0.01 * MILLIS_IN_MINUTE); testReduction(Rounding.DateTimeUnit.MINUTE_OF_HOUR, 0.01 * MILLIS_IN_MINUTE);
} }
public void testReductionHour() { public void testReductionHour() {

View file

@ -135,11 +135,10 @@ public final class DatafeedConfigUtils {
case WEEK_OF_WEEKYEAR -> new TimeValue(7, TimeUnit.DAYS); case WEEK_OF_WEEKYEAR -> new TimeValue(7, TimeUnit.DAYS);
case DAY_OF_MONTH -> new TimeValue(1, TimeUnit.DAYS); case DAY_OF_MONTH -> new TimeValue(1, TimeUnit.DAYS);
case HOUR_OF_DAY -> new TimeValue(1, TimeUnit.HOURS); case HOUR_OF_DAY -> new TimeValue(1, TimeUnit.HOURS);
case MINUTES_OF_HOUR -> new TimeValue(1, TimeUnit.MINUTES); case MINUTE_OF_HOUR -> new TimeValue(1, TimeUnit.MINUTES);
case SECOND_OF_MINUTE -> new TimeValue(1, TimeUnit.SECONDS); case SECOND_OF_MINUTE -> new TimeValue(1, TimeUnit.SECONDS);
case MONTH_OF_YEAR, YEAR_OF_CENTURY, QUARTER_OF_YEAR -> throw ExceptionsHelper.badRequestException( case MONTH_OF_YEAR, YEAR_OF_CENTURY, QUARTER_OF_YEAR, YEARS_OF_CENTURY, MONTHS_OF_YEAR -> throw ExceptionsHelper
invalidDateHistogramCalendarIntervalMessage(calendarInterval) .badRequestException(invalidDateHistogramCalendarIntervalMessage(calendarInterval));
);
}; };
} else { } else {
interval = TimeValue.parseTimeValue(calendarInterval, "date_histogram.calendar_interval"); interval = TimeValue.parseTimeValue(calendarInterval, "date_histogram.calendar_interval");

View file

@ -194,10 +194,14 @@ public class DateTrunc extends EsqlScalarFunction {
// java.time.Period does not have a QUARTERLY 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 // returns a quarterly rounding
rounding = new Rounding.Builder(Rounding.DateTimeUnit.QUARTER_OF_YEAR); rounding = new Rounding.Builder(Rounding.DateTimeUnit.QUARTER_OF_YEAR);
} else if (period.getMonths() == 1) {
rounding = new Rounding.Builder(Rounding.DateTimeUnit.MONTH_OF_YEAR);
} else if (period.getMonths() > 0) { } else if (period.getMonths() > 0) {
rounding = new Rounding.Builder(Rounding.DateTimeUnit.MONTH_OF_YEAR, period.getMonths()); rounding = new Rounding.Builder(Rounding.DateTimeUnit.MONTHS_OF_YEAR, period.getMonths());
} else if (period.getYears() == 1) {
rounding = new Rounding.Builder(Rounding.DateTimeUnit.YEAR_OF_CENTURY);
} else if (period.getYears() > 0) { } else if (period.getYears() > 0) {
rounding = new Rounding.Builder(Rounding.DateTimeUnit.YEAR_OF_CENTURY, period.getYears()); rounding = new Rounding.Builder(Rounding.DateTimeUnit.YEARS_OF_CENTURY, period.getYears());
} else { } else {
throw new IllegalArgumentException("Time interval is not supported"); throw new IllegalArgumentException("Time interval is not supported");
} }

View file

@ -40,10 +40,10 @@ public class DateTruncRoundingTests extends ESTestCase {
assertEquals(10, rounding.roundingSize(Rounding.DateTimeUnit.HOUR_OF_DAY), 0d); assertEquals(10, rounding.roundingSize(Rounding.DateTimeUnit.HOUR_OF_DAY), 0d);
rounding = createRounding(Duration.ofMinutes(1)); rounding = createRounding(Duration.ofMinutes(1));
assertEquals(1, rounding.roundingSize(Rounding.DateTimeUnit.MINUTES_OF_HOUR), 0d); assertEquals(1, rounding.roundingSize(Rounding.DateTimeUnit.MINUTE_OF_HOUR), 0d);
rounding = createRounding(Duration.ofMinutes(100)); rounding = createRounding(Duration.ofMinutes(100));
assertEquals(100, rounding.roundingSize(Rounding.DateTimeUnit.MINUTES_OF_HOUR), 0d); assertEquals(100, rounding.roundingSize(Rounding.DateTimeUnit.MINUTE_OF_HOUR), 0d);
rounding = createRounding(Duration.ofSeconds(1)); rounding = createRounding(Duration.ofSeconds(1));
assertEquals(1, rounding.roundingSize(Rounding.DateTimeUnit.SECOND_OF_MINUTE), 0d); assertEquals(1, rounding.roundingSize(Rounding.DateTimeUnit.SECOND_OF_MINUTE), 0d);
@ -52,7 +52,7 @@ public class DateTruncRoundingTests extends ESTestCase {
assertEquals(120, rounding.roundingSize(Rounding.DateTimeUnit.SECOND_OF_MINUTE), 0d); assertEquals(120, rounding.roundingSize(Rounding.DateTimeUnit.SECOND_OF_MINUTE), 0d);
rounding = createRounding(Duration.ofSeconds(60).plusMinutes(5).plusHours(1)); rounding = createRounding(Duration.ofSeconds(60).plusMinutes(5).plusHours(1));
assertEquals(1 + 5 + 60, rounding.roundingSize(Rounding.DateTimeUnit.MINUTES_OF_HOUR), 0d); assertEquals(1 + 5 + 60, rounding.roundingSize(Rounding.DateTimeUnit.MINUTE_OF_HOUR), 0d);
} }
public void testCreateRoundingPeriod() { public void testCreateRoundingPeriod() {