/*
* 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.joda;
import org.elasticsearch.ElasticsearchParseException;
import org.elasticsearch.ExceptionsHelper;
import org.elasticsearch.test.ESTestCase;
import org.joda.time.DateTimeZone;
import org.junit.Test;
import java.util.TimeZone;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.hamcrest.Matchers.equalTo;
public class DateMathParserTests extends ESTestCase {
FormatDateTimeFormatter formatter = Joda.forPattern("dateOptionalTime||epoch_millis");
DateMathParser parser = new DateMathParser(formatter);
private static Callable<Long> callable(final long value) {
return new Callable<Long>() {
@Override
public Long call() throws Exception {
return value;
}
};
}
void assertDateMathEquals(String toTest, String expected) {
assertDateMathEquals(toTest, expected, 0, false, null);
}
void assertDateMathEquals(String toTest, String expected, final long now, boolean roundUp, DateTimeZone timeZone) {
long gotMillis = parser.parse(toTest, callable(now), roundUp, timeZone);
assertDateEquals(gotMillis, toTest, expected);
}
void assertDateEquals(long gotMillis, String original, String expected) {
long expectedMillis = parser.parse(expected, callable(0));
if (gotMillis != expectedMillis) {
fail("Date math not equal\n" +
"Original : " + original + "\n" +
"Parsed : " + formatter.printer().print(gotMillis) + "\n" +
"Expected : " + expected + "\n" +
"Expected milliseconds : " + expectedMillis + "\n" +
"Actual milliseconds : " + gotMillis + "\n");
}
}
public void testBasicDates() {
assertDateMathEquals("2014", "2014-01-01T00:00:00.000");
assertDateMathEquals("2014-05", "2014-05-01T00:00:00.000");
assertDateMathEquals("2014-05-30", "2014-05-30T00:00:00.000");
assertDateMathEquals("2014-05-30T20", "2014-05-30T20:00:00.000");
assertDateMathEquals("2014-05-30T20:21", "2014-05-30T20:21:00.000");
assertDateMathEquals("2014-05-30T20:21:35", "2014-05-30T20:21:35.000");
assertDateMathEquals("2014-05-30T20:21:35.123", "2014-05-30T20:21:35.123");
}
public void testRoundingDoesNotAffectExactDate() {
assertDateMathEquals("2014-11-12T22:55:00.000Z", "2014-11-12T22:55:00.000Z", 0, true, null);
assertDateMathEquals("2014-11-12T22:55:00.000Z", "2014-11-12T22:55:00.000Z", 0, false, null);
assertDateMathEquals("2014-11-12T22:55:00.000", "2014-11-12T21:55:00.000Z", 0, true, DateTimeZone.forID("+01:00"));
assertDateMathEquals("2014-11-12T22:55:00.000", "2014-11-12T21:55:00.000Z", 0, false, DateTimeZone.forID("+01:00"));
assertDateMathEquals("2014-11-12T22:55:00.000+01:00", "2014-11-12T21:55:00.000Z", 0, true, null);
assertDateMathEquals("2014-11-12T22:55:00.000+01:00", "2014-11-12T21:55:00.000Z", 0, false, null);
}
public void testTimezone() {
// timezone works within date format
assertDateMathEquals("2014-05-30T20:21+02:00", "2014-05-30T18:21:00.000");
// but also externally
assertDateMathEquals("2014-05-30T20:21", "2014-05-30T18:21:00.000", 0, false, DateTimeZone.forID("+02:00"));
// and timezone in the date has priority
assertDateMathEquals("2014-05-30T20:21+03:00", "2014-05-30T17:21:00.000", 0, false, DateTimeZone.forID("-08:00"));
assertDateMathEquals("2014-05-30T20:21Z", "2014-05-30T20:21:00.000", 0, false, DateTimeZone.forID("-08:00"));
}
public void testBasicMath() {
assertDateMathEquals("2014-11-18||+y", "2015-11-18");
assertDateMathEquals("2014-11-18||-2y", "2012-11-18");
assertDateMathEquals("2014-11-18||+3M", "2015-02-18");
assertDateMathEquals("2014-11-18||-M", "2014-10-18");
assertDateMathEquals("2014-11-18||+1w", "2014-11-25");
assertDateMathEquals("2014-11-18||-3w", "2014-10-28");
assertDateMathEquals("2014-11-18||+22d", "2014-12-10");
assertDateMathEquals("2014-11-18||-423d", "2013-09-21");
assertDateMathEquals("2014-11-18T14||+13h", "2014-11-19T03");
assertDateMathEquals("2014-11-18T14||-1h", "2014-11-18T13");
assertDateMathEquals("2014-11-18T14||+13H", "2014-11-19T03");
assertDateMathEquals("2014-11-18T14||-1H", "2014-11-18T13");
assertDateMathEquals("2014-11-18T14:27||+10240m", "2014-11-25T17:07");
assertDateMathEquals("2014-11-18T14:27||-10m", "2014-11-18T14:17");
assertDateMathEquals("2014-11-18T14:27:32||+60s", "2014-11-18T14:28:32");
assertDateMathEquals("2014-11-18T14:27:32||-3600s", "2014-11-18T13:27:32");
}
public void testLenientEmptyMath() {
assertDateMathEquals("2014-05-30T20:21||", "2014-05-30T20:21:00.000");
}
public void testMultipleAdjustments() {
assertDateMathEquals("2014-11-18||+1M-1M", "2014-11-18");
assertDateMathEquals("2014-11-18||+1M-1m", "2014-12-17T23:59");
assertDateMathEquals("2014-11-18||-1m+1M", "2014-12-17T23:59");
assertDateMathEquals("2014-11-18||+1M/M", "2014-12-01");
assertDateMathEquals("2014-11-18||+1M/M+1h", "2014-12-01T01");
}
public void testNow() {
final long now = parser.parse("2014-11-18T14:27:32", callable(0), false, null);
assertDateMathEquals("now", "2014-11-18T14:27:32", now, false, null);
assertDateMathEquals("now+M", "2014-12-18T14:27:32", now, false, null);
assertDateMathEquals("now-2d", "2014-11-16T14:27:32", now, false, null);
assertDateMathEquals("now/m", "2014-11-18T14:27", now, false, null);
// timezone does not affect now
assertDateMathEquals("now/m", "2014-11-18T14:27", now, false, DateTimeZone.forID("+02:00"));
}
public void testRoundingPreservesEpochAsBaseDate() {
// If a user only specifies times, then the date needs to always be 1970-01-01 regardless of rounding
FormatDateTimeFormatter formatter = Joda.forPattern("HH:mm:ss");
DateMathParser parser = new DateMathParser(formatter);
assertEquals(
this.formatter.parser().parseMillis("1970-01-01T04:52:20.000Z"),
parser.parse("04:52:20", callable(0), false, null));
assertEquals(
this.formatter.parser().parseMillis("1970-01-01T04:52:20.999Z"),
parser.parse("04:52:20", callable(0), true, null));
}
// Implicit rounding happening when parts of the date are not specified
public void testImplicitRounding() {
assertDateMathEquals("2014-11-18", "2014-11-18", 0, false, null);
assertDateMathEquals("2014-11-18", "2014-11-18T23:59:59.999Z", 0, true, null);
assertDateMathEquals("2014-11-18T09:20", "2014-11-18T09:20", 0, false, null);
assertDateMathEquals("2014-11-18T09:20", "2014-11-18T09:20:59.999Z", 0, true, null);
assertDateMathEquals("2014-11-18", "2014-11-17T23:00:00.000Z", 0, false, DateTimeZone.forID("CET"));
assertDateMathEquals("2014-11-18", "2014-11-18T22:59:59.999Z", 0, true, DateTimeZone.forID("CET"));
assertDateMathEquals("2014-11-18T09:20", "2014-11-18T08:20:00.000Z", 0, false, DateTimeZone.forID("CET"));
assertDateMathEquals("2014-11-18T09:20", "2014-11-18T08:20:59.999Z", 0, true, DateTimeZone.forID("CET"));
// implicit rounding with explicit timezone in the date format
FormatDateTimeFormatter formatter = Joda.forPattern("YYYY-MM-ddZ");
DateMathParser parser = new DateMathParser(formatter);
long time = parser.parse("2011-10-09+01:00", callable(0), false, null);
assertEquals(this.parser.parse("2011-10-09T00:00:00.000+01:00", callable(0)), time);
time = parser.parse("2011-10-09+01:00", callable(0), true, null);
assertEquals(this.parser.parse("2011-10-09T23:59:59.999+01:00", callable(0)), time);
}
// Explicit rounding using the || separator
public void testExplicitRounding() {
assertDateMathEquals("2014-11-18||/y", "2014-01-01", 0, false, null);
assertDateMathEquals("2014-11-18||/y", "2014-12-31T23:59:59.999", 0, true, null);
assertDateMathEquals("2014||/y", "2014-01-01", 0, false, null);
assertDateMathEquals("2014-01-01T00:00:00.001||/y", "2014-12-31T23:59:59.999", 0, true, null);
// rounding should also take into account time zone
assertDateMathEquals("2014-11-18||/y", "2013-12-31T23:00:00.000Z", 0, false, DateTimeZone.forID("CET"));
assertDateMathEquals("2014-11-18||/y", "2014-12-31T22:59:59.999Z", 0, true, DateTimeZone.forID("CET"));
assertDateMathEquals("2014-11-18||/M", "2014-11-01", 0, false, null);
assertDateMathEquals("2014-11-18||/M", "2014-11-30T23:59:59.999", 0, true, null);
assertDateMathEquals("2014-11||/M", "2014-11-01", 0, false, null);
assertDateMathEquals("2014-11||/M", "2014-11-30T23:59:59.999", 0, true, null);
assertDateMathEquals("2014-11-18||/M", "2014-10-31T23:00:00.000Z", 0, false, DateTimeZone.forID("CET"));
assertDateMathEquals("2014-11-18||/M", "2014-11-30T22:59:59.999Z", 0, true, DateTimeZone.forID("CET"));
assertDateMathEquals("2014-11-18T14||/w", "2014-11-17", 0, false, null);
assertDateMathEquals("2014-11-18T14||/w", "2014-11-23T23:59:59.999", 0, true, null);
assertDateMathEquals("2014-11-18||/w", "2014-11-17", 0, false, null);
assertDateMathEquals("2014-11-18||/w", "2014-11-23T23:59:59.999", 0, true, null);
assertDateMathEquals("2014-11-18||/w", "2014-11-16T23:00:00.000Z", 0, false, DateTimeZone.forID("+01:00"));
assertDateMathEquals("2014-11-18||/w", "2014-11-17T01:00:00.000Z", 0, false, DateTimeZone.forID("-01:00"));
assertDateMathEquals("2014-11-18||/w", "2014-11-16T23:00:00.000Z", 0, false, DateTimeZone.forID("CET"));
assertDateMathEquals("2014-11-18||/w", "2014-11-23T22:59:59.999Z", 0, true, DateTimeZone.forID("CET"));
assertDateMathEquals("2014-07-22||/w", "2014-07-20T22:00:00.000Z", 0, false, DateTimeZone.forID("CET")); // with DST
assertDateMathEquals("2014-11-18T14||/d", "2014-11-18", 0, false, null);
assertDateMathEquals("2014-11-18T14||/d", "2014-11-18T23:59:59.999", 0, true, null);
assertDateMathEquals("2014-11-18||/d", "2014-11-18", 0, false, null);
assertDateMathEquals("2014-11-18||/d", "2014-11-18T23:59:59.999", 0, true, null);
assertDateMathEquals("2014-11-18T14:27||/h", "2014-11-18T14", 0, false, null);
assertDateMathEquals("2014-11-18T14:27||/h", "2014-11-18T14:59:59.999", 0, true, null);
assertDateMathEquals("2014-11-18T14||/H", "2014-11-18T14", 0, false, null);
assertDateMathEquals("2014-11-18T14||/H", "2014-11-18T14:59:59.999", 0, true, null);
assertDateMathEquals("2014-11-18T14:27||/h", "2014-11-18T14", 0, false, null);
assertDateMathEquals("2014-11-18T14:27||/h", "2014-11-18T14:59:59.999", 0, true, null);
assertDateMathEquals("2014-11-18T14||/H", "2014-11-18T14", 0, false, null);
assertDateMathEquals("2014-11-18T14||/H", "2014-11-18T14:59:59.999", 0, true, null);
assertDateMathEquals("2014-11-18T14:27:32||/m", "2014-11-18T14:27", 0, false, null);
assertDateMathEquals("2014-11-18T14:27:32||/m", "2014-11-18T14:27:59.999", 0, true, null);
assertDateMathEquals("2014-11-18T14:27||/m", "2014-11-18T14:27", 0, false, null);
assertDateMathEquals("2014-11-18T14:27||/m", "2014-11-18T14:27:59.999", 0, true, null);
assertDateMathEquals("2014-11-18T14:27:32.123||/s", "2014-11-18T14:27:32", 0, false, null);
assertDateMathEquals("2014-11-18T14:27:32.123||/s", "2014-11-18T14:27:32.999", 0, true, null);
assertDateMathEquals("2014-11-18T14:27:32||/s", "2014-11-18T14:27:32", 0, false, null);
assertDateMathEquals("2014-11-18T14:27:32||/s", "2014-11-18T14:27:32.999", 0, true, null);
}
public void testTimestamps() {
assertDateMathEquals("1418248078000", "2014-12-10T21:47:58.000");
// datemath still works on timestamps
assertDateMathEquals("1418248078000||/m", "2014-12-10T21:47:00.000");
// also check other time units
DateMathParser parser = new DateMathParser(Joda.forPattern("epoch_second||dateOptionalTime"));
long datetime = parser.parse("1418248078", callable(0));
assertDateEquals(datetime, "1418248078", "2014-12-10T21:47:58.000");
// a timestamp before 10000 is a year
assertDateMathEquals("9999", "9999-01-01T00:00:00.000");
// 10000 is also a year, breaking bwc, used to be a timestamp
assertDateMathEquals("10000", "10000-01-01T00:00:00.000");
// but 10000 with T is still a date format
assertDateMathEquals("10000T", "10000-01-01T00:00:00.000");
}
void assertParseException(String msg, String date, String exc) {
try {
parser.parse(date, callable(0));
fail("Date: " + date + "\n" + msg);
} catch (ElasticsearchParseException e) {
assertThat(ExceptionsHelper.detailedMessage(e).contains(exc), equalTo(true));
}
}
public void testIllegalMathFormat() {
assertParseException("Expected date math unsupported operator exception", "2014-11-18||*5", "operator not supported");
assertParseException("Expected date math incompatible rounding exception", "2014-11-18||/2m", "rounding");
assertParseException("Expected date math illegal unit type exception", "2014-11-18||+2a", "unit [a] not supported");
assertParseException("Expected date math truncation exception", "2014-11-18||+12", "truncated");
assertParseException("Expected date math truncation exception", "2014-11-18||-", "truncated");
}
public void testIllegalDateFormat() {
assertParseException("Expected bad timestamp exception", Long.toString(Long.MAX_VALUE) + "0", "failed to parse date field");
assertParseException("Expected bad date format exception", "123bogus", "with format");
}
public void testOnlyCallsNowIfNecessary() {
final AtomicBoolean called = new AtomicBoolean();
final Callable<Long> now = new Callable<Long>() {
@Override
public Long call() throws Exception {
called.set(true);
return 42L;
}
};
parser.parse("2014-11-18T14:27:32", now, false, null);
assertFalse(called.get());
parser.parse("now/d", now, false, null);
assertTrue(called.get());
}
@Test(expected = ElasticsearchParseException.class)
public void testThatUnixTimestampMayNotHaveTimeZone() {
DateMathParser parser = new DateMathParser(Joda.forPattern("epoch_millis"));
parser.parse("1234567890123", callable(42), false, DateTimeZone.forTimeZone(TimeZone.getTimeZone("CET")));
}
}