diff --git a/NOTICE.txt b/NOTICE.txt index f1e3198ab4a9a..9dd784d5a09ec 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -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/). diff --git a/benchmarks/src/main/java/org/elasticsearch/benchmark/time/RoundingBenchmark.java b/benchmarks/src/main/java/org/elasticsearch/benchmark/time/RoundingBenchmark.java index 6da6d5290bfee..0928a7565607b 100644 --- a/benchmarks/src/main/java/org/elasticsearch/benchmark/time/RoundingBenchmark.java +++ b/benchmarks/src/main/java/org/elasticsearch/benchmark/time/RoundingBenchmark.java @@ -34,8 +34,14 @@ 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 long timeRoundingDateTimeUnitJava() { 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 long timeRoundingDateTimeUnitDayOfMonthJava() { 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 long timeIntervalRoundingJava() { 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); + } } diff --git a/server/src/main/java/org/elasticsearch/common/Rounding.java b/server/src/main/java/org/elasticsearch/common/Rounding.java index 6d11739133dda..3558b16aac1c8 100644 --- a/server/src/main/java/org/elasticsearch/common/Rounding.java +++ b/server/src/main/java/org/elasticsearch/common/Rounding.java @@ -54,19 +54,51 @@ */ 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 enum DateTimeUnit { 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 @@ static class TimeUnitRounding extends Rounding { 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 @@ private LocalDateTime truncateLocalDateTime(LocalDateTime localDateTime) { 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 @@ private LocalDateTime truncateLocalDateTime(LocalDateTime localDateTime) { } @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); diff --git a/server/src/main/java/org/elasticsearch/common/time/DateUtils.java b/server/src/main/java/org/elasticsearch/common/time/DateUtils.java index 4235344bd8371..48edae06c20e3 100644 --- a/server/src/main/java/org/elasticsearch/common/time/DateUtils.java +++ b/server/src/main/java/org/elasticsearch/common/time/DateUtils.java @@ -34,6 +34,11 @@ 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) { @@ -143,6 +148,79 @@ public static long toMilliSeconds(long nanoSecondsSinceEpoch) { 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; + } + /** * Returns the current UTC date-time with milliseconds precision. * In Java 9+ (as opposed to Java 8) the {@code Clock} implementation uses system's best clock implementation (which could mean diff --git a/server/src/main/java/org/elasticsearch/common/time/DateUtilsRounding.java b/server/src/main/java/org/elasticsearch/common/time/DateUtilsRounding.java new file mode 100644 index 0000000000000..d9c0a9597b8a1 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/common/time/DateUtilsRounding.java @@ -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]; + } + } +} diff --git a/server/src/test/java/org/elasticsearch/common/RoundingTests.java b/server/src/test/java/org/elasticsearch/common/RoundingTests.java index a809131b932e2..bee3f57764f32 100644 --- a/server/src/test/java/org/elasticsearch/common/RoundingTests.java +++ b/server/src/test/java/org/elasticsearch/common/RoundingTests.java @@ -57,6 +57,30 @@ public void testUTCTimeUnitRounding() { 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 @@ private void assertInterval(long rounded, long nextRoundingValue, Rounding round } /** - * 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 diff --git a/server/src/test/java/org/elasticsearch/common/rounding/RoundingDuelTests.java b/server/src/test/java/org/elasticsearch/common/rounding/RoundingDuelTests.java index 3ee4ce0e7d7bf..b791d0e3ce9dd 100644 --- a/server/src/test/java/org/elasticsearch/common/rounding/RoundingDuelTests.java +++ b/server/src/test/java/org/elasticsearch/common/rounding/RoundingDuelTests.java @@ -24,6 +24,7 @@ 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 void testSerialization() throws Exception { 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"); } diff --git a/server/src/test/java/org/elasticsearch/common/rounding/TimeZoneRoundingTests.java b/server/src/test/java/org/elasticsearch/common/rounding/TimeZoneRoundingTests.java index e49f25772a726..029eb3b041d3d 100644 --- a/server/src/test/java/org/elasticsearch/common/rounding/TimeZoneRoundingTests.java +++ b/server/src/test/java/org/elasticsearch/common/rounding/TimeZoneRoundingTests.java @@ -56,6 +56,30 @@ public void testUTCTimeUnitRounding() { 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() { diff --git a/server/src/test/java/org/elasticsearch/common/time/DateUtilsRoundingTests.java b/server/src/test/java/org/elasticsearch/common/time/DateUtilsRoundingTests.java new file mode 100644 index 0000000000000..4ec1c261a2ace --- /dev/null +++ b/server/src/test/java/org/elasticsearch/common/time/DateUtilsRoundingTests.java @@ -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)); + } + } + } + } +} diff --git a/server/src/test/java/org/elasticsearch/common/time/DateUtilsTests.java b/server/src/test/java/org/elasticsearch/common/time/DateUtilsTests.java index e35d8df1b9c06..e9d0a5a5b9a33 100644 --- a/server/src/test/java/org/elasticsearch/common/time/DateUtilsTests.java +++ b/server/src/test/java/org/elasticsearch/common/time/DateUtilsTests.java @@ -23,8 +23,14 @@ 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 @@ private Instant createRandomInstant() { 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)); + } }