Restore date aggregation performance in UTC case (#38221) (#38700)

The benchmarks showed a sharp decrease in aggregation performance for
the UTC case.

This commit uses the same calculation as joda time, which requires no
conversion into any java time object, also, the check for an fixedoffset
has been put into the ctor to reduce the need for runtime calculations.
The same goes for the amount of the used unit in milliseconds.

Closes #37826
This commit is contained in:
Alexander Reelsen 2019-02-11 16:30:48 +03:00 committed by GitHub
parent bd4ca4c702
commit e7868e92bd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 610 additions and 31 deletions

View File

@ -3,3 +3,6 @@ Copyright 2009-2018 Elasticsearch
This product includes software developed by The Apache Software
Foundation (http://www.apache.org/).
This product includes software developed by
Joda.org (http://www.joda.org/).

View File

@ -34,8 +34,14 @@ import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Warmup;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.util.concurrent.TimeUnit;
import static org.elasticsearch.common.Rounding.DateTimeUnit.DAY_OF_MONTH;
import static org.elasticsearch.common.Rounding.DateTimeUnit.MONTH_OF_YEAR;
import static org.elasticsearch.common.Rounding.DateTimeUnit.QUARTER_OF_YEAR;
import static org.elasticsearch.common.Rounding.DateTimeUnit.YEAR_OF_CENTURY;
@Fork(3)
@Warmup(iterations = 10)
@Measurement(iterations = 10)
@ -48,23 +54,13 @@ public class RoundingBenchmark {
private final ZoneId zoneId = ZoneId.of("Europe/Amsterdam");
private final DateTimeZone timeZone = DateUtils.zoneIdToDateTimeZone(zoneId);
private long timestamp = 1548879021354L;
private final org.elasticsearch.common.rounding.Rounding jodaRounding =
org.elasticsearch.common.rounding.Rounding.builder(DateTimeUnit.HOUR_OF_DAY).timeZone(timeZone).build();
org.elasticsearch.common.rounding.Rounding.builder(DateTimeUnit.HOUR_OF_DAY).timeZone(timeZone).build();
private final Rounding javaRounding = Rounding.builder(Rounding.DateTimeUnit.HOUR_OF_DAY)
.timeZone(zoneId).build();
private final org.elasticsearch.common.rounding.Rounding jodaDayOfMonthRounding =
org.elasticsearch.common.rounding.Rounding.builder(DateTimeUnit.DAY_OF_MONTH).timeZone(timeZone).build();
private final Rounding javaDayOfMonthRounding = Rounding.builder(TimeValue.timeValueMinutes(60))
.timeZone(zoneId).build();
private final org.elasticsearch.common.rounding.Rounding timeIntervalRoundingJoda =
org.elasticsearch.common.rounding.Rounding.builder(DateTimeUnit.DAY_OF_MONTH).timeZone(timeZone).build();
private final Rounding timeIntervalRoundingJava = Rounding.builder(TimeValue.timeValueMinutes(60))
.timeZone(zoneId).build();
private final long timestamp = 1548879021354L;
@Benchmark
public long timeRoundingDateTimeUnitJoda() {
return jodaRounding.round(timestamp);
@ -75,6 +71,11 @@ public class RoundingBenchmark {
return javaRounding.round(timestamp);
}
private final org.elasticsearch.common.rounding.Rounding jodaDayOfMonthRounding =
org.elasticsearch.common.rounding.Rounding.builder(DateTimeUnit.DAY_OF_MONTH).timeZone(timeZone).build();
private final Rounding javaDayOfMonthRounding = Rounding.builder(DAY_OF_MONTH)
.timeZone(zoneId).build();
@Benchmark
public long timeRoundingDateTimeUnitDayOfMonthJoda() {
return jodaDayOfMonthRounding.round(timestamp);
@ -85,6 +86,11 @@ public class RoundingBenchmark {
return javaDayOfMonthRounding.round(timestamp);
}
private final org.elasticsearch.common.rounding.Rounding timeIntervalRoundingJoda =
org.elasticsearch.common.rounding.Rounding.builder(TimeValue.timeValueMinutes(60)).timeZone(timeZone).build();
private final Rounding timeIntervalRoundingJava = Rounding.builder(TimeValue.timeValueMinutes(60))
.timeZone(zoneId).build();
@Benchmark
public long timeIntervalRoundingJava() {
return timeIntervalRoundingJava.round(timestamp);
@ -94,4 +100,65 @@ public class RoundingBenchmark {
public long timeIntervalRoundingJoda() {
return timeIntervalRoundingJoda.round(timestamp);
}
private final org.elasticsearch.common.rounding.Rounding timeUnitRoundingUtcDayOfMonthJoda =
org.elasticsearch.common.rounding.Rounding.builder(DateTimeUnit.DAY_OF_MONTH).timeZone(DateTimeZone.UTC).build();
private final Rounding timeUnitRoundingUtcDayOfMonthJava = Rounding.builder(DAY_OF_MONTH)
.timeZone(ZoneOffset.UTC).build();
@Benchmark
public long timeUnitRoundingUtcDayOfMonthJava() {
return timeUnitRoundingUtcDayOfMonthJava.round(timestamp);
}
@Benchmark
public long timeUnitRoundingUtcDayOfMonthJoda() {
return timeUnitRoundingUtcDayOfMonthJoda.round(timestamp);
}
private final org.elasticsearch.common.rounding.Rounding timeUnitRoundingUtcQuarterOfYearJoda =
org.elasticsearch.common.rounding.Rounding.builder(DateTimeUnit.QUARTER).timeZone(DateTimeZone.UTC).build();
private final Rounding timeUnitRoundingUtcQuarterOfYearJava = Rounding.builder(QUARTER_OF_YEAR)
.timeZone(ZoneOffset.UTC).build();
@Benchmark
public long timeUnitRoundingUtcQuarterOfYearJava() {
return timeUnitRoundingUtcQuarterOfYearJava.round(timestamp);
}
@Benchmark
public long timeUnitRoundingUtcQuarterOfYearJoda() {
return timeUnitRoundingUtcQuarterOfYearJoda.round(timestamp);
}
private final org.elasticsearch.common.rounding.Rounding timeUnitRoundingUtcMonthOfYearJoda =
org.elasticsearch.common.rounding.Rounding.builder(DateTimeUnit.MONTH_OF_YEAR).timeZone(DateTimeZone.UTC).build();
private final Rounding timeUnitRoundingUtcMonthOfYearJava = Rounding.builder(MONTH_OF_YEAR)
.timeZone(ZoneOffset.UTC).build();
@Benchmark
public long timeUnitRoundingUtcMonthOfYearJava() {
return timeUnitRoundingUtcMonthOfYearJava.round(timestamp);
}
@Benchmark
public long timeUnitRoundingUtcMonthOfYearJoda() {
return timeUnitRoundingUtcMonthOfYearJoda.round(timestamp);
}
private final org.elasticsearch.common.rounding.Rounding timeUnitRoundingUtcYearOfCenturyJoda =
org.elasticsearch.common.rounding.Rounding.builder(DateTimeUnit.YEAR_OF_CENTURY).timeZone(DateTimeZone.UTC).build();
private final Rounding timeUnitRoundingUtcYearOfCenturyJava = Rounding.builder(YEAR_OF_CENTURY)
.timeZone(ZoneOffset.UTC).build();
@Benchmark
public long timeUnitRoundingUtcYearOfCenturyJava() {
return timeUnitRoundingUtcYearOfCenturyJava.round(timestamp);
}
@Benchmark
public long timeUnitRoundingUtcYearOfCenturyJoda() {
return timeUnitRoundingUtcYearOfCenturyJoda.round(timestamp);
}
}

View File

@ -54,19 +54,51 @@ import java.util.Objects;
*/
public abstract class Rounding implements Writeable {
public static String format(long epochMillis) {
return Instant.ofEpochMilli(epochMillis) + "/" + epochMillis;
}
public enum DateTimeUnit {
WEEK_OF_WEEKYEAR( (byte) 1, IsoFields.WEEK_OF_WEEK_BASED_YEAR),
YEAR_OF_CENTURY( (byte) 2, ChronoField.YEAR_OF_ERA),
QUARTER_OF_YEAR( (byte) 3, IsoFields.QUARTER_OF_YEAR),
MONTH_OF_YEAR( (byte) 4, ChronoField.MONTH_OF_YEAR),
DAY_OF_MONTH( (byte) 5, ChronoField.DAY_OF_MONTH),
HOUR_OF_DAY( (byte) 6, ChronoField.HOUR_OF_DAY),
MINUTES_OF_HOUR( (byte) 7, ChronoField.MINUTE_OF_HOUR),
SECOND_OF_MINUTE( (byte) 8, ChronoField.SECOND_OF_MINUTE);
WEEK_OF_WEEKYEAR((byte) 1, IsoFields.WEEK_OF_WEEK_BASED_YEAR) {
long roundFloor(long utcMillis) {
return DateUtils.roundWeekOfWeekYear(utcMillis);
}
},
YEAR_OF_CENTURY((byte) 2, ChronoField.YEAR_OF_ERA) {
long roundFloor(long utcMillis) {
return DateUtils.roundYear(utcMillis);
}
},
QUARTER_OF_YEAR((byte) 3, IsoFields.QUARTER_OF_YEAR) {
long roundFloor(long utcMillis) {
return DateUtils.roundQuarterOfYear(utcMillis);
}
},
MONTH_OF_YEAR((byte) 4, ChronoField.MONTH_OF_YEAR) {
long roundFloor(long utcMillis) {
return DateUtils.roundMonthOfYear(utcMillis);
}
},
DAY_OF_MONTH((byte) 5, ChronoField.DAY_OF_MONTH) {
final long unitMillis = ChronoField.DAY_OF_MONTH.getBaseUnit().getDuration().toMillis();
long roundFloor(long utcMillis) {
return DateUtils.roundFloor(utcMillis, unitMillis);
}
},
HOUR_OF_DAY((byte) 6, ChronoField.HOUR_OF_DAY) {
final long unitMillis = ChronoField.HOUR_OF_DAY.getBaseUnit().getDuration().toMillis();
long roundFloor(long utcMillis) {
return DateUtils.roundFloor(utcMillis, unitMillis);
}
},
MINUTES_OF_HOUR((byte) 7, ChronoField.MINUTE_OF_HOUR) {
final long unitMillis = ChronoField.MINUTE_OF_HOUR.getBaseUnit().getDuration().toMillis();
long roundFloor(long utcMillis) {
return DateUtils.roundFloor(utcMillis, unitMillis);
}
},
SECOND_OF_MINUTE((byte) 8, ChronoField.SECOND_OF_MINUTE) {
final long unitMillis = ChronoField.SECOND_OF_MINUTE.getBaseUnit().getDuration().toMillis();
long roundFloor(long utcMillis) {
return DateUtils.roundFloor(utcMillis, unitMillis);
}
};
private final byte id;
private final TemporalField field;
@ -76,6 +108,15 @@ public abstract class Rounding implements Writeable {
this.field = field;
}
/**
* 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
*/
abstract long roundFloor(long utcMillis);
public byte getId() {
return id;
}
@ -182,12 +223,13 @@ public abstract class Rounding implements Writeable {
private final DateTimeUnit unit;
private final ZoneId timeZone;
private final boolean unitRoundsToMidnight;
private final boolean isUtcTimeZone;
TimeUnitRounding(DateTimeUnit unit, ZoneId timeZone) {
this.unit = unit;
this.timeZone = timeZone;
this.unitRoundsToMidnight = this.unit.field.getBaseUnit().getDuration().toMillis() > 3600000L;
this.isUtcTimeZone = timeZone.normalized().equals(ZoneOffset.UTC);
}
TimeUnitRounding(StreamInput in) throws IOException {
@ -223,9 +265,7 @@ public abstract class Rounding implements Writeable {
return LocalDateTime.of(localDateTime.getYear(), localDateTime.getMonthValue(), 1, 0, 0);
case QUARTER_OF_YEAR:
int quarter = (int) IsoFields.QUARTER_OF_YEAR.getFrom(localDateTime);
int month = ((quarter - 1) * 3) + 1;
return LocalDateTime.of(localDateTime.getYear(), month, 1, 0, 0);
return LocalDateTime.of(localDateTime.getYear(), localDateTime.getMonth().firstMonthOfQuarter(), 1, 0, 0);
case YEAR_OF_CENTURY:
return LocalDateTime.of(LocalDate.of(localDateTime.getYear(), 1, 1), LocalTime.MIDNIGHT);
@ -236,7 +276,14 @@ public abstract class Rounding implements Writeable {
}
@Override
public long round(final long utcMillis) {
public long round(long utcMillis) {
// this works as long as the offset doesn't change. It is worth getting this case out of the way first, as
// the calculations for fixing things near to offset changes are a little expensive and are unnecessary in the common case
// of working in UTC.
if (isUtcTimeZone) {
return unit.roundFloor(utcMillis);
}
Instant instant = Instant.ofEpochMilli(utcMillis);
if (unitRoundsToMidnight) {
final LocalDateTime localDateTime = LocalDateTime.ofInstant(instant, timeZone);

View File

@ -31,6 +31,11 @@ import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import static org.elasticsearch.common.time.DateUtilsRounding.getMonthOfYear;
import static org.elasticsearch.common.time.DateUtilsRounding.getTotalMillisByYearMonth;
import static org.elasticsearch.common.time.DateUtilsRounding.getYear;
import static org.elasticsearch.common.time.DateUtilsRounding.utcMillisAtStartOfYear;
public class DateUtils {
public static DateTimeZone zoneIdToDateTimeZone(ZoneId zoneId) {
if (zoneId == null) {
@ -139,4 +144,77 @@ public class DateUtils {
return nanoSecondsSinceEpoch / 1_000_000;
}
/**
* Rounds the given utc milliseconds sicne the epoch down to the next unit millis
*
* 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 performane of this methods, there are no guards or checks in it
*
* @param utcMillis the milliseconds since the epoch
* @param unitMillis the unit to round to
* @return the rounded milliseconds since the epoch
*/
public static long roundFloor(long utcMillis, final long unitMillis) {
if (utcMillis >= 0) {
return utcMillis - utcMillis % unitMillis;
} else {
utcMillis += 1;
return utcMillis - utcMillis % unitMillis - unitMillis;
}
}
/**
* Round down to the beginning of the quarter of the year of the specified time
* @param utcMillis the milliseconds since the epoch
* @return The milliseconds since the epoch rounded down to the quarter of the year
*/
public static long roundQuarterOfYear(final long utcMillis) {
int year = getYear(utcMillis);
int month = getMonthOfYear(utcMillis, year);
int firstMonthOfQuarter = (((month-1) / 3) * 3) + 1;
return DateUtils.of(year, firstMonthOfQuarter);
}
/**
* Round down to the beginning of the month of the year of the specified time
* @param utcMillis the milliseconds since the epoch
* @return The milliseconds since the epoch rounded down to the month of the year
*/
public static long roundMonthOfYear(final long utcMillis) {
int year = getYear(utcMillis);
int month = getMonthOfYear(utcMillis, year);
return DateUtils.of(year, month);
}
/**
* Round down to the beginning of the 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 year
*/
public static long roundYear(final long utcMillis) {
int year = getYear(utcMillis);
return utcMillisAtStartOfYear(year);
}
/**
* 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 the first day of the month
* @param year the year to return
* @param month the month to return, ranging from 1-12
* @return the milliseconds since the epoch of the first day of the month in the year
*/
private static long of(final int year, final int month) {
long millis = utcMillisAtStartOfYear(year);
millis += getTotalMillisByYearMonth(year, month);
return millis;
}
}

View File

@ -0,0 +1,182 @@
/*
* Copyright 2001-2014 Stephen Colebourne
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.elasticsearch.common.time;
/**
* This class has been copied from different locations within the joda time package, as
* these methods fast when used for rounding, as they do not require conversion to java
* time objects
*
* This code has been copied from jodatime 2.10.1
* The source can be found at https://github.com/JodaOrg/joda-time/tree/v2.10.1
*
* See following methods have been copied (along with required helper variables)
*
* - org.joda.time.chrono.GregorianChronology.calculateFirstDayOfYearMillis(int year)
* - org.joda.time.chrono.BasicChronology.getYear(int year)
* - org.joda.time.chrono.BasicGJChronology.getMonthOfYear(long utcMillis, int year)
* - org.joda.time.chrono.BasicGJChronology.getTotalMillisByYearMonth(int year, int month)
*/
class DateUtilsRounding {
private static final int DAYS_0000_TO_1970 = 719527;
private static final int MILLIS_PER_DAY = 86_400_000;
private static final long MILLIS_PER_YEAR = 31556952000L;
// see org.joda.time.chrono.BasicGJChronology
private static final long[] MIN_TOTAL_MILLIS_BY_MONTH_ARRAY;
private static final long[] MAX_TOTAL_MILLIS_BY_MONTH_ARRAY;
private static final int[] MIN_DAYS_PER_MONTH_ARRAY = {
31,28,31,30,31,30,31,31,30,31,30,31
};
private static final int[] MAX_DAYS_PER_MONTH_ARRAY = {
31,29,31,30,31,30,31,31,30,31,30,31
};
static {
MIN_TOTAL_MILLIS_BY_MONTH_ARRAY = new long[12];
MAX_TOTAL_MILLIS_BY_MONTH_ARRAY = new long[12];
long minSum = 0;
long maxSum = 0;
for (int i = 0; i < 11; i++) {
long millis = MIN_DAYS_PER_MONTH_ARRAY[i]
* (long) MILLIS_PER_DAY;
minSum += millis;
MIN_TOTAL_MILLIS_BY_MONTH_ARRAY[i + 1] = minSum;
millis = MAX_DAYS_PER_MONTH_ARRAY[i]
* (long) MILLIS_PER_DAY;
maxSum += millis;
MAX_TOTAL_MILLIS_BY_MONTH_ARRAY[i + 1] = maxSum;
}
}
/**
* calculates the first day of a year in milliseconds since the epoch (assuming UTC)
*
* @param year the year
* @return the milliseconds since the epoch of the first of january at midnight of the specified year
*/
// see org.joda.time.chrono.GregorianChronology.calculateFirstDayOfYearMillis
static long utcMillisAtStartOfYear(final int year) {
// Initial value is just temporary.
int leapYears = year / 100;
if (year < 0) {
// Add 3 before shifting right since /4 and >>2 behave differently
// on negative numbers. When the expression is written as
// (year / 4) - (year / 100) + (year / 400),
// it works for both positive and negative values, except this optimization
// eliminates two divisions.
leapYears = ((year + 3) >> 2) - leapYears + ((leapYears + 3) >> 2) - 1;
} else {
leapYears = (year >> 2) - leapYears + (leapYears >> 2);
if (isLeapYear(year)) {
leapYears--;
}
}
return (year * 365L + (leapYears - DAYS_0000_TO_1970)) * MILLIS_PER_DAY; // millis per day
}
private static boolean isLeapYear(final int year) {
return ((year & 3) == 0) && ((year % 100) != 0 || (year % 400) == 0);
}
private static final long AVERAGE_MILLIS_PER_YEAR_DIVIDED_BY_TWO = MILLIS_PER_YEAR / 2;
private static final long APPROX_MILLIS_AT_EPOCH_DIVIDED_BY_TWO = (1970L * MILLIS_PER_YEAR) / 2;
// see org.joda.time.chrono.BasicChronology
static int getYear(final long utcMillis) {
// Get an initial estimate of the year, and the millis value that
// represents the start of that year. Then verify estimate and fix if
// necessary.
// Initial estimate uses values divided by two to avoid overflow.
long unitMillis = AVERAGE_MILLIS_PER_YEAR_DIVIDED_BY_TWO;
long i2 = (utcMillis >> 1) + APPROX_MILLIS_AT_EPOCH_DIVIDED_BY_TWO;
if (i2 < 0) {
i2 = i2 - unitMillis + 1;
}
int year = (int) (i2 / unitMillis);
long yearStart = utcMillisAtStartOfYear(year);
long diff = utcMillis - yearStart;
if (diff < 0) {
year--;
} else if (diff >= MILLIS_PER_DAY * 365L) {
// One year may need to be added to fix estimate.
long oneYear;
if (isLeapYear(year)) {
oneYear = MILLIS_PER_DAY * 366L;
} else {
oneYear = MILLIS_PER_DAY * 365L;
}
yearStart += oneYear;
if (yearStart <= utcMillis) {
// Didn't go too far, so actually add one year.
year++;
}
}
return year;
}
// see org.joda.time.chrono.BasicGJChronology
static int getMonthOfYear(final long utcMillis, final int year) {
// Perform a binary search to get the month. To make it go even faster,
// compare using ints instead of longs. The number of milliseconds per
// year exceeds the limit of a 32-bit int's capacity, so divide by
// 1024. No precision is lost (except time of day) since the number of
// milliseconds per day contains 1024 as a factor. After the division,
// the instant isn't measured in milliseconds, but in units of
// (128/125)seconds.
int i = (int)((utcMillis - utcMillisAtStartOfYear(year)) >> 10);
// There are 86400000 milliseconds per day, but divided by 1024 is
// 84375. There are 84375 (128/125)seconds per day.
return
(isLeapYear(year))
? ((i < 182 * 84375)
? ((i < 91 * 84375)
? ((i < 31 * 84375) ? 1 : (i < 60 * 84375) ? 2 : 3)
: ((i < 121 * 84375) ? 4 : (i < 152 * 84375) ? 5 : 6))
: ((i < 274 * 84375)
? ((i < 213 * 84375) ? 7 : (i < 244 * 84375) ? 8 : 9)
: ((i < 305 * 84375) ? 10 : (i < 335 * 84375) ? 11 : 12)))
: ((i < 181 * 84375)
? ((i < 90 * 84375)
? ((i < 31 * 84375) ? 1 : (i < 59 * 84375) ? 2 : 3)
: ((i < 120 * 84375) ? 4 : (i < 151 * 84375) ? 5 : 6))
: ((i < 273 * 84375)
? ((i < 212 * 84375) ? 7 : (i < 243 * 84375) ? 8 : 9)
: ((i < 304 * 84375) ? 10 : (i < 334 * 84375) ? 11 : 12)));
}
// see org.joda.time.chrono.BasicGJChronology
static long getTotalMillisByYearMonth(final int year, final int month) {
if (isLeapYear(year)) {
return MAX_TOTAL_MILLIS_BY_MONTH_ARRAY[month - 1];
} else {
return MIN_TOTAL_MILLIS_BY_MONTH_ARRAY[month - 1];
}
}
}

View File

@ -57,6 +57,30 @@ public class RoundingTests extends ESTestCase {
tzRounding = Rounding.builder(Rounding.DateTimeUnit.WEEK_OF_WEEKYEAR).build();
assertThat(tzRounding.round(time("2012-01-10T01:01:01")), isDate(time("2012-01-09T00:00:00.000Z"), tz));
assertThat(tzRounding.nextRoundingValue(time("2012-01-09T00:00:00.000Z")), isDate(time("2012-01-16T00:00:00.000Z"), tz));
tzRounding = Rounding.builder(Rounding.DateTimeUnit.QUARTER_OF_YEAR).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("2012-04-01T00:00:00.000Z"), tz));
tzRounding = Rounding.builder(Rounding.DateTimeUnit.HOUR_OF_DAY).build();
assertThat(tzRounding.round(time("2012-01-10T01:01:01")), isDate(time("2012-01-10T01:00:00.000Z"), tz));
assertThat(tzRounding.nextRoundingValue(time("2012-01-09T00:00:00.000Z")), isDate(time("2012-01-09T01:00:00.000Z"), tz));
tzRounding = Rounding.builder(Rounding.DateTimeUnit.DAY_OF_MONTH).build();
assertThat(tzRounding.round(time("2012-01-10T01:01:01")), isDate(time("2012-01-10T00:00:00.000Z"), tz));
assertThat(tzRounding.nextRoundingValue(time("2012-01-09T00:00:00.000Z")), isDate(time("2012-01-10T00:00:00.000Z"), tz));
tzRounding = Rounding.builder(Rounding.DateTimeUnit.YEAR_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.MINUTES_OF_HOUR).build();
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));
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.nextRoundingValue(time("2012-01-09T00:00:00.000Z")), isDate(time("2012-01-09T00:00:01.000Z"), tz));
}
public void testUTCIntervalRounding() {
@ -667,7 +691,7 @@ public class RoundingTests extends ESTestCase {
}
/**
* perform a number on assertions and checks on {@link org.elasticsearch.common.rounding.Rounding.TimeUnitRounding} intervals
* perform a number on assertions and checks on {@link org.elasticsearch.common.Rounding.TimeUnitRounding} intervals
* @param rounded the expected low end of the rounding interval
* @param unrounded a date in the interval to be checked for rounding
* @param nextRoundingValue the expected upper end of the rounding interval

View File

@ -24,6 +24,7 @@ import org.elasticsearch.common.io.stream.BytesStreamOutput;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.test.VersionUtils;
import org.joda.time.DateTimeZone;
import java.time.ZoneOffset;
@ -56,6 +57,26 @@ public class RoundingDuelTests extends ESTestCase {
assertThat(roundingJoda.nextRoundingValue(randomInt), is(roundingJavaTime.nextRoundingValue(randomInt)));
}
public void testDuellingImplementations() {
org.elasticsearch.common.Rounding.DateTimeUnit randomDateTimeUnit =
randomFrom(org.elasticsearch.common.Rounding.DateTimeUnit.values());
org.elasticsearch.common.Rounding rounding;
Rounding roundingJoda;
if (randomBoolean()) {
rounding = org.elasticsearch.common.Rounding.builder(randomDateTimeUnit).timeZone(ZoneOffset.UTC).build();
DateTimeUnit dateTimeUnit = DateTimeUnit.resolve(randomDateTimeUnit.getId());
roundingJoda = Rounding.builder(dateTimeUnit).timeZone(DateTimeZone.UTC).build();
} else {
TimeValue interval = timeValue();
rounding = org.elasticsearch.common.Rounding.builder(interval).timeZone(ZoneOffset.UTC).build();
roundingJoda = Rounding.builder(interval).timeZone(DateTimeZone.UTC).build();
}
long roundValue = randomLong();
assertThat(roundingJoda.round(roundValue), is(rounding.round(roundValue)));
}
static TimeValue timeValue() {
return TimeValue.parseTimeValue(randomIntBetween(1, 1000) + randomFrom(ALLOWED_TIME_SUFFIXES), "settingName");
}

View File

@ -56,6 +56,30 @@ public class TimeZoneRoundingTests extends ESTestCase {
tzRounding = Rounding.builder(DateTimeUnit.WEEK_OF_WEEKYEAR).build();
assertThat(tzRounding.round(time("2012-01-10T01:01:01")), isDate(time("2012-01-09T00:00:00.000Z"), tz));
assertThat(tzRounding.nextRoundingValue(time("2012-01-09T00:00:00.000Z")), isDate(time("2012-01-16T00:00:00.000Z"), tz));
tzRounding = Rounding.builder(DateTimeUnit.QUARTER).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("2012-04-01T00:00:00.000Z"), tz));
tzRounding = Rounding.builder(DateTimeUnit.HOUR_OF_DAY).build();
assertThat(tzRounding.round(time("2012-01-10T01:01:01")), isDate(time("2012-01-10T01:00:00.000Z"), tz));
assertThat(tzRounding.nextRoundingValue(time("2012-01-09T00:00:00.000Z")), isDate(time("2012-01-09T01:00:00.000Z"), tz));
tzRounding = Rounding.builder(DateTimeUnit.DAY_OF_MONTH).build();
assertThat(tzRounding.round(time("2012-01-10T01:01:01")), isDate(time("2012-01-10T00:00:00.000Z"), tz));
assertThat(tzRounding.nextRoundingValue(time("2012-01-09T00:00:00.000Z")), isDate(time("2012-01-10T00:00:00.000Z"), tz));
tzRounding = Rounding.builder(DateTimeUnit.YEAR_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(DateTimeUnit.MINUTES_OF_HOUR).build();
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));
tzRounding = Rounding.builder(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.nextRoundingValue(time("2012-01-09T00:00:00.000Z")), isDate(time("2012-01-09T00:00:01.000Z"), tz));
}
public void testUTCIntervalRounding() {

View File

@ -0,0 +1,49 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch.common.time;
import org.elasticsearch.test.ESTestCase;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import static org.hamcrest.Matchers.equalTo;
public class DateUtilsRoundingTests extends ESTestCase {
public void testDateUtilsRounding() {
for (int year = -1000; year < 3000; year++) {
final long startOfYear = DateUtilsRounding.utcMillisAtStartOfYear(year);
assertThat(startOfYear, equalTo(ZonedDateTime.of(year, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC).toInstant().toEpochMilli()));
assertThat(DateUtilsRounding.getYear(startOfYear), equalTo(year));
assertThat(DateUtilsRounding.getYear(startOfYear - 1), equalTo(year - 1));
assertThat(DateUtilsRounding.getMonthOfYear(startOfYear, year), equalTo(1));
assertThat(DateUtilsRounding.getMonthOfYear(startOfYear - 1, year - 1), equalTo(12));
for (int month = 1; month <= 12; month++) {
final long startOfMonth = ZonedDateTime.of(year, month, 1, 0, 0, 0, 0, ZoneOffset.UTC).toInstant().toEpochMilli();
assertThat(DateUtilsRounding.getMonthOfYear(startOfMonth, year), equalTo(month));
if (month > 1) {
assertThat(DateUtilsRounding.getYear(startOfMonth - 1), equalTo(year));
assertThat(DateUtilsRounding.getMonthOfYear(startOfMonth - 1, year), equalTo(month - 1));
}
}
}
}
}

View File

@ -23,8 +23,14 @@ import org.elasticsearch.test.ESTestCase;
import org.joda.time.DateTimeZone;
import java.time.Instant;
import java.time.LocalDate;
import java.time.Month;
import java.time.Year;
import java.time.YearMonth;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoField;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
@ -114,4 +120,82 @@ public class DateUtilsTests extends ESTestCase {
long nanos = randomLongBetween(0, 999_999_999L);
return Instant.ofEpochSecond(seconds, nanos);
}
public void testRoundFloor() {
assertThat(DateUtils.roundFloor(0, randomLongBetween(0, Long.MAX_VALUE)), is(0L));
ChronoField randomChronoField =
randomFrom(ChronoField.DAY_OF_MONTH, ChronoField.HOUR_OF_DAY, ChronoField.MINUTE_OF_HOUR, ChronoField.SECOND_OF_MINUTE);
long unitMillis = randomChronoField.getBaseUnit().getDuration().toMillis();
int year = randomIntBetween(-3000, 3000);
int month = randomIntBetween(1, 12);
int day = randomIntBetween(1, YearMonth.of(year, month).lengthOfMonth());
int hour = randomIntBetween(1, 23);
int minute = randomIntBetween(1, 59);
int second = randomIntBetween(1, 59);
int nanos = randomIntBetween(1, 999_999_999);
ZonedDateTime randomDate = ZonedDateTime.of(year, month, day, hour, minute, second, nanos, ZoneOffset.UTC);
ZonedDateTime result = randomDate;
switch (randomChronoField) {
case SECOND_OF_MINUTE:
result = result.withNano(0);
break;
case MINUTE_OF_HOUR:
result = result.withNano(0).withSecond(0);
break;
case HOUR_OF_DAY:
result = result.withNano(0).withSecond(0).withMinute(0);
break;
case DAY_OF_MONTH:
result = result.withNano(0).withSecond(0).withMinute(0).withHour(0);
break;
}
long rounded = DateUtils.roundFloor(randomDate.toInstant().toEpochMilli(), unitMillis);
assertThat(rounded, is(result.toInstant().toEpochMilli()));
}
public void testRoundQuarterOfYear() {
assertThat(DateUtils.roundQuarterOfYear(0), is(0L));
long lastQuarter1969 = ZonedDateTime.of(1969, 10, 1, 0, 0, 0, 0, ZoneOffset.UTC)
.toInstant().toEpochMilli();
assertThat(DateUtils.roundQuarterOfYear(-1), is(lastQuarter1969));
int year = randomIntBetween(1970, 2040);
int month = randomIntBetween(1, 12);
int day = randomIntBetween(1, YearMonth.of(year, month).lengthOfMonth());
ZonedDateTime randomZonedDateTime = ZonedDateTime.of(year, month, day,
randomIntBetween(0, 23), randomIntBetween(0, 59), randomIntBetween(0, 59), 999_999_999, ZoneOffset.UTC);
long quarterInMillis = Year.of(randomZonedDateTime.getYear()).atMonth(Month.of(month).firstMonthOfQuarter()).atDay(1)
.atStartOfDay(ZoneOffset.UTC).toInstant().toEpochMilli();
long result = DateUtils.roundQuarterOfYear(randomZonedDateTime.toInstant().toEpochMilli());
assertThat(result, is(quarterInMillis));
}
public void testRoundMonthOfYear() {
assertThat(DateUtils.roundMonthOfYear(0), is(0L));
assertThat(DateUtils.roundMonthOfYear(1), is(0L));
long dec1969 = LocalDate.of(1969, 12, 1).atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli();
assertThat(DateUtils.roundMonthOfYear(-1), is(dec1969));
}
public void testRoundYear() {
assertThat(DateUtils.roundYear(0), is(0L));
assertThat(DateUtils.roundYear(1), is(0L));
long startOf1969 = ZonedDateTime.of(1969, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)
.toInstant().toEpochMilli();
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
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));
}
}