From b612cab96a025d9d28e3c9f6fde7bac1719e5be1 Mon Sep 17 00:00:00 2001 From: Alexander Reelsen Date: Tue, 7 Jul 2015 09:30:45 +0200 Subject: [PATCH] Dates: More strict parsing of ISO dates If you are using the default date or the named identifiers of dates, the current implementation was allowed to read a year with only one digit. In order to make this more strict, this fixes a year to be at least 4 digits. Same applies for month, day, hour, minute, seconds. Also the new default is `strictDateOptionalTime` for indices created with Elasticsearch 2.0 or newer. In addition a couple of not exposed date formats have been exposed, as they have been mentioned in the documentation. Closes #6158 --- .../org/elasticsearch/common/joda/Joda.java | 111 +- .../index/mapper/core/DateFieldMapper.java | 26 +- .../mapper/internal/TimestampFieldMapper.java | 35 +- .../index/mapper/object/RootObjectMapper.java | 2 +- .../elasticsearch/snapshots/SnapshotInfo.java | 2 +- .../time/format/StrictISODateTimeFormat.java | 2028 +++++++++++++++++ .../deps/joda/SimpleJodaTests.java | 365 +++ .../mapper/date/SimpleDateMappingTests.java | 94 +- .../timestamp/TimestampMappingTests.java | 16 +- .../bucket/DateHistogramTests.java | 6 +- .../DecayFunctionScoreTests.java | 7 +- docs/reference/mapping/date-format.asciidoc | 70 +- .../mapping/fields/timestamp-field.asciidoc | 2 +- .../mapping/types/core-types.asciidoc | 2 +- .../mapping/types/root-object-type.asciidoc | 2 +- docs/reference/migration/migrate_2_0.asciidoc | 9 +- 16 files changed, 2709 insertions(+), 68 deletions(-) create mode 100644 core/src/main/java/org/joda/time/format/StrictISODateTimeFormat.java diff --git a/core/src/main/java/org/elasticsearch/common/joda/Joda.java b/core/src/main/java/org/elasticsearch/common/joda/Joda.java index a457b85d80364..5f1ffb4207b50 100644 --- a/core/src/main/java/org/elasticsearch/common/joda/Joda.java +++ b/core/src/main/java/org/elasticsearch/common/joda/Joda.java @@ -118,6 +118,8 @@ public static FormatDateTimeFormatter forPattern(String input, Locale locale) { formatter = ISODateTimeFormat.ordinalDateTimeNoMillis(); } else if ("time".equals(input)) { formatter = ISODateTimeFormat.time(); + } else if ("timeNoMillis".equals(input) || "time_no_millis".equals(input)) { + formatter = ISODateTimeFormat.timeNoMillis(); } else if ("tTime".equals(input) || "t_time".equals(input)) { formatter = ISODateTimeFormat.tTime(); } else if ("tTimeNoMillis".equals(input) || "t_time_no_millis".equals(input)) { @@ -126,10 +128,14 @@ public static FormatDateTimeFormatter forPattern(String input, Locale locale) { formatter = ISODateTimeFormat.weekDate(); } else if ("weekDateTime".equals(input) || "week_date_time".equals(input)) { formatter = ISODateTimeFormat.weekDateTime(); + } else if ("weekDateTimeNoMillis".equals(input) || "week_date_time_no_millis".equals(input)) { + formatter = ISODateTimeFormat.weekDateTimeNoMillis(); } else if ("weekyear".equals(input) || "week_year".equals(input)) { formatter = ISODateTimeFormat.weekyear(); - } else if ("weekyearWeek".equals(input)) { + } else if ("weekyearWeek".equals(input) || "weekyear_week".equals(input)) { formatter = ISODateTimeFormat.weekyearWeek(); + } else if ("weekyearWeekDay".equals(input) || "weekyear_week_day".equals(input)) { + formatter = ISODateTimeFormat.weekyearWeekDay(); } else if ("year".equals(input)) { formatter = ISODateTimeFormat.year(); } else if ("yearMonth".equals(input) || "year_month".equals(input)) { @@ -140,6 +146,77 @@ public static FormatDateTimeFormatter forPattern(String input, Locale locale) { formatter = new DateTimeFormatterBuilder().append(new EpochTimePrinter(false), new EpochTimeParser(false)).toFormatter(); } else if ("epoch_millis".equals(input)) { formatter = new DateTimeFormatterBuilder().append(new EpochTimePrinter(true), new EpochTimeParser(true)).toFormatter(); + // strict date formats here, must be at least 4 digits for year and two for months and two for day + } else if ("strictBasicWeekDate".equals(input) || "strict_basic_week_date".equals(input)) { + formatter = StrictISODateTimeFormat.basicWeekDate(); + } else if ("strictBasicWeekDateTime".equals(input) || "strict_basic_week_date_time".equals(input)) { + formatter = StrictISODateTimeFormat.basicWeekDateTime(); + } else if ("strictBasicWeekDateTimeNoMillis".equals(input) || "strict_basic_week_date_time_no_millis".equals(input)) { + formatter = StrictISODateTimeFormat.basicWeekDateTimeNoMillis(); + } else if ("strictDate".equals(input) || "strict_date".equals(input)) { + formatter = StrictISODateTimeFormat.date(); + } else if ("strictDateHour".equals(input) || "strict_date_hour".equals(input)) { + formatter = StrictISODateTimeFormat.dateHour(); + } else if ("strictDateHourMinute".equals(input) || "strict_date_hour_minute".equals(input)) { + formatter = StrictISODateTimeFormat.dateHourMinute(); + } else if ("strictDateHourMinuteSecond".equals(input) || "strict_date_hour_minute_second".equals(input)) { + formatter = StrictISODateTimeFormat.dateHourMinuteSecond(); + } else if ("strictDateHourMinuteSecondFraction".equals(input) || "strict_date_hour_minute_second_fraction".equals(input)) { + formatter = StrictISODateTimeFormat.dateHourMinuteSecondFraction(); + } else if ("strictDateHourMinuteSecondMillis".equals(input) || "strict_date_hour_minute_second_millis".equals(input)) { + formatter = StrictISODateTimeFormat.dateHourMinuteSecondMillis(); + } else if ("strictDateOptionalTime".equals(input) || "strict_date_optional_time".equals(input)) { + // in this case, we have a separate parser and printer since the dataOptionalTimeParser can't print + // this sucks we should use the root local by default and not be dependent on the node + return new FormatDateTimeFormatter(input, + StrictISODateTimeFormat.dateOptionalTimeParser().withZone(DateTimeZone.UTC), + StrictISODateTimeFormat.dateTime().withZone(DateTimeZone.UTC), locale); + } else if ("strictDateTime".equals(input) || "strict_date_time".equals(input)) { + formatter = StrictISODateTimeFormat.dateTime(); + } else if ("strictDateTimeNoMillis".equals(input) || "strict_date_time_no_millis".equals(input)) { + formatter = StrictISODateTimeFormat.dateTimeNoMillis(); + } else if ("strictHour".equals(input) || "strict_hour".equals(input)) { + formatter = StrictISODateTimeFormat.hour(); + } else if ("strictHourMinute".equals(input) || "strict_hour_minute".equals(input)) { + formatter = StrictISODateTimeFormat.hourMinute(); + } else if ("strictHourMinuteSecond".equals(input) || "strict_hour_minute_second".equals(input)) { + formatter = StrictISODateTimeFormat.hourMinuteSecond(); + } else if ("strictHourMinuteSecondFraction".equals(input) || "strict_hour_minute_second_fraction".equals(input)) { + formatter = StrictISODateTimeFormat.hourMinuteSecondFraction(); + } else if ("strictHourMinuteSecondMillis".equals(input) || "strict_hour_minute_second_millis".equals(input)) { + formatter = StrictISODateTimeFormat.hourMinuteSecondMillis(); + } else if ("strictOrdinalDate".equals(input) || "strict_ordinal_date".equals(input)) { + formatter = StrictISODateTimeFormat.ordinalDate(); + } else if ("strictOrdinalDateTime".equals(input) || "strict_ordinal_date_time".equals(input)) { + formatter = StrictISODateTimeFormat.ordinalDateTime(); + } else if ("strictOrdinalDateTimeNoMillis".equals(input) || "strict_ordinal_date_time_no_millis".equals(input)) { + formatter = StrictISODateTimeFormat.ordinalDateTimeNoMillis(); + } else if ("strictTime".equals(input) || "strict_time".equals(input)) { + formatter = StrictISODateTimeFormat.time(); + } else if ("strictTimeNoMillis".equals(input) || "strict_time_no_millis".equals(input)) { + formatter = StrictISODateTimeFormat.timeNoMillis(); + } else if ("strictTTime".equals(input) || "strict_t_time".equals(input)) { + formatter = StrictISODateTimeFormat.tTime(); + } else if ("strictTTimeNoMillis".equals(input) || "strict_t_time_no_millis".equals(input)) { + formatter = StrictISODateTimeFormat.tTimeNoMillis(); + } else if ("strictWeekDate".equals(input) || "strict_week_date".equals(input)) { + formatter = StrictISODateTimeFormat.weekDate(); + } else if ("strictWeekDateTime".equals(input) || "strict_week_date_time".equals(input)) { + formatter = StrictISODateTimeFormat.weekDateTime(); + } else if ("strictWeekDateTimeNoMillis".equals(input) || "strict_week_date_time_no_millis".equals(input)) { + formatter = StrictISODateTimeFormat.weekDateTimeNoMillis(); + } else if ("strictWeekyear".equals(input) || "strict_weekyear".equals(input)) { + formatter = StrictISODateTimeFormat.weekyear(); + } else if ("strictWeekyearWeek".equals(input) || "strict_weekyear_week".equals(input)) { + formatter = StrictISODateTimeFormat.weekyearWeek(); + } else if ("strictWeekyearWeekDay".equals(input) || "strict_weekyear_week_day".equals(input)) { + formatter = StrictISODateTimeFormat.weekyearWeekDay(); + } else if ("strictYear".equals(input) || "strict_year".equals(input)) { + formatter = StrictISODateTimeFormat.year(); + } else if ("strictYearMonth".equals(input) || "strict_year_month".equals(input)) { + formatter = StrictISODateTimeFormat.yearMonth(); + } else if ("strictYearMonthDay".equals(input) || "strict_year_month_day".equals(input)) { + formatter = StrictISODateTimeFormat.yearMonthDay(); } else if (Strings.hasLength(input) && input.contains("||")) { String[] formats = Strings.delimitedListToStringArray(input, "||"); DateTimeParser[] parsers = new DateTimeParser[formats.length]; @@ -171,6 +248,38 @@ public static FormatDateTimeFormatter forPattern(String input, Locale locale) { return new FormatDateTimeFormatter(input, formatter.withZone(DateTimeZone.UTC), locale); } + public static FormatDateTimeFormatter getStrictStandardDateFormatter() { + // 2014/10/10 + DateTimeFormatter shortFormatter = new DateTimeFormatterBuilder() + .appendFixedDecimal(DateTimeFieldType.year(), 4) + .appendLiteral('/') + .appendFixedDecimal(DateTimeFieldType.monthOfYear(), 2) + .appendLiteral('/') + .appendFixedDecimal(DateTimeFieldType.dayOfMonth(), 2) + .toFormatter() + .withZoneUTC(); + + // 2014/10/10 12:12:12 + DateTimeFormatter longFormatter = new DateTimeFormatterBuilder() + .appendFixedDecimal(DateTimeFieldType.year(), 4) + .appendLiteral('/') + .appendFixedDecimal(DateTimeFieldType.monthOfYear(), 2) + .appendLiteral('/') + .appendFixedDecimal(DateTimeFieldType.dayOfMonth(), 2) + .appendLiteral(' ') + .appendFixedSignedDecimal(DateTimeFieldType.hourOfDay(), 2) + .appendLiteral(':') + .appendFixedSignedDecimal(DateTimeFieldType.minuteOfHour(), 2) + .appendLiteral(':') + .appendFixedSignedDecimal(DateTimeFieldType.secondOfMinute(), 2) + .toFormatter() + .withZoneUTC(); + + DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder().append(longFormatter.withZone(DateTimeZone.UTC).getPrinter(), new DateTimeParser[] {longFormatter.getParser(), shortFormatter.getParser()}); + + return new FormatDateTimeFormatter("yyyy/MM/dd HH:mm:ss||yyyy/MM/dd", builder.toFormatter().withZone(DateTimeZone.UTC), Locale.ROOT); + } + public static final DurationFieldType Quarters = new DurationFieldType("quarters") { private static final long serialVersionUID = -8167713675442491871L; diff --git a/core/src/main/java/org/elasticsearch/index/mapper/core/DateFieldMapper.java b/core/src/main/java/org/elasticsearch/index/mapper/core/DateFieldMapper.java index e1f152ade3209..c2b6970cc53ea 100644 --- a/core/src/main/java/org/elasticsearch/index/mapper/core/DateFieldMapper.java +++ b/core/src/main/java/org/elasticsearch/index/mapper/core/DateFieldMapper.java @@ -69,7 +69,8 @@ public class DateFieldMapper extends NumberFieldMapper { public static final String CONTENT_TYPE = "date"; public static class Defaults extends NumberFieldMapper.Defaults { - public static final FormatDateTimeFormatter DATE_TIME_FORMATTER = Joda.forPattern("dateOptionalTime||epoch_millis", Locale.ROOT); + public static final FormatDateTimeFormatter DATE_TIME_FORMATTER = Joda.forPattern("strictDateOptionalTime||epoch_millis", Locale.ROOT); + public static final FormatDateTimeFormatter DATE_TIME_FORMATTER_BEFORE_2_0 = Joda.forPattern("dateOptionalTime", Locale.ROOT); public static final TimeUnit TIME_UNIT = TimeUnit.MILLISECONDS; public static final DateFieldType FIELD_TYPE = new DateFieldType(); @@ -123,15 +124,13 @@ public DateFieldMapper build(BuilderContext context) { } protected void setupFieldType(BuilderContext context) { - FormatDateTimeFormatter dateTimeFormatter = fieldType().dateTimeFormatter; - // TODO MOVE ME OUTSIDE OF THIS SPACE? - if (Version.indexCreated(context.indexSettings()).before(Version.V_2_0_0)) { - boolean includesEpochFormatter = dateTimeFormatter.format().contains("epoch_"); - if (!includesEpochFormatter) { - String format = fieldType().timeUnit().equals(TimeUnit.SECONDS) ? "epoch_second" : "epoch_millis"; - fieldType().setDateTimeFormatter(Joda.forPattern(format + "||" + dateTimeFormatter.format())); - } + if (Version.indexCreated(context.indexSettings()).before(Version.V_2_0_0) && + !fieldType().dateTimeFormatter().format().contains("epoch_")) { + String format = fieldType().timeUnit().equals(TimeUnit.SECONDS) ? "epoch_second" : "epoch_millis"; + fieldType().setDateTimeFormatter(Joda.forPattern(format + "||" + fieldType().dateTimeFormatter().format())); } + + FormatDateTimeFormatter dateTimeFormatter = fieldType().dateTimeFormatter; if (!locale.equals(dateTimeFormatter.locale())) { fieldType().setDateTimeFormatter(new FormatDateTimeFormatter(dateTimeFormatter.format(), dateTimeFormatter.parser(), dateTimeFormatter.printer(), locale)); } @@ -159,6 +158,7 @@ public static class TypeParser implements Mapper.TypeParser { public Mapper.Builder parse(String name, Map node, ParserContext parserContext) throws MapperParsingException { DateFieldMapper.Builder builder = dateField(name); parseNumberField(builder, name, node, parserContext); + boolean configuredFormat = false; for (Iterator> iterator = node.entrySet().iterator(); iterator.hasNext();) { Map.Entry entry = iterator.next(); String propName = Strings.toUnderscoreCase(entry.getKey()); @@ -171,6 +171,7 @@ public static class TypeParser implements Mapper.TypeParser { iterator.remove(); } else if (propName.equals("format")) { builder.dateTimeFormatter(parseDateTimeFormatter(propNode)); + configuredFormat = true; iterator.remove(); } else if (propName.equals("numeric_resolution")) { builder.timeUnit(TimeUnit.valueOf(propNode.toString().toUpperCase(Locale.ROOT))); @@ -180,6 +181,13 @@ public static class TypeParser implements Mapper.TypeParser { iterator.remove(); } } + if (!configuredFormat) { + if (parserContext.indexVersionCreated().onOrAfter(Version.V_2_0_0)) { + builder.dateTimeFormatter(Defaults.DATE_TIME_FORMATTER); + } else { + builder.dateTimeFormatter(Defaults.DATE_TIME_FORMATTER_BEFORE_2_0); + } + } return builder; } } diff --git a/core/src/main/java/org/elasticsearch/index/mapper/internal/TimestampFieldMapper.java b/core/src/main/java/org/elasticsearch/index/mapper/internal/TimestampFieldMapper.java index 507b2d1da8963..6029ce3828853 100644 --- a/core/src/main/java/org/elasticsearch/index/mapper/internal/TimestampFieldMapper.java +++ b/core/src/main/java/org/elasticsearch/index/mapper/internal/TimestampFieldMapper.java @@ -24,14 +24,12 @@ import org.apache.lucene.index.IndexOptions; import org.elasticsearch.Version; import org.elasticsearch.action.TimestampParsingException; -import org.elasticsearch.common.Explicit; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.Strings; import org.elasticsearch.common.joda.FormatDateTimeFormatter; import org.elasticsearch.common.joda.Joda; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.XContentBuilder; -import org.elasticsearch.index.analysis.NamedAnalyzer; import org.elasticsearch.index.analysis.NumericDateAnalyzer; import org.elasticsearch.index.fielddata.FieldDataType; import org.elasticsearch.index.mapper.MappedFieldType; @@ -41,10 +39,8 @@ import org.elasticsearch.index.mapper.MergeResult; import org.elasticsearch.index.mapper.ParseContext; import org.elasticsearch.index.mapper.MetadataFieldMapper; -import org.elasticsearch.index.mapper.core.AbstractFieldMapper; import org.elasticsearch.index.mapper.core.DateFieldMapper; import org.elasticsearch.index.mapper.core.LongFieldMapper; -import org.elasticsearch.index.mapper.core.NumberFieldMapper; import java.io.IOException; import java.util.Iterator; @@ -59,15 +55,16 @@ public class TimestampFieldMapper extends MetadataFieldMapper { public static final String NAME = "_timestamp"; public static final String CONTENT_TYPE = "_timestamp"; - public static final String DEFAULT_DATE_TIME_FORMAT = "epoch_millis||dateOptionalTime"; + public static final String DEFAULT_DATE_TIME_FORMAT = "epoch_millis||strictDateOptionalTime"; public static class Defaults extends DateFieldMapper.Defaults { public static final String NAME = "_timestamp"; // TODO: this should be removed - public static final MappedFieldType PRE_20_FIELD_TYPE; - public static final FormatDateTimeFormatter DATE_TIME_FORMATTER = Joda.forPattern(DEFAULT_DATE_TIME_FORMAT); + public static final TimestampFieldType PRE_20_FIELD_TYPE; public static final TimestampFieldType FIELD_TYPE = new TimestampFieldType(); + public static final FormatDateTimeFormatter DATE_TIME_FORMATTER = Joda.forPattern(DEFAULT_DATE_TIME_FORMAT); + public static final FormatDateTimeFormatter DATE_TIME_FORMATTER_BEFORE_2_0 = Joda.forPattern("epoch_millis||dateOptionalTime"); static { FIELD_TYPE.setStored(true); @@ -82,6 +79,9 @@ public static class Defaults extends DateFieldMapper.Defaults { PRE_20_FIELD_TYPE = FIELD_TYPE.clone(); PRE_20_FIELD_TYPE.setStored(false); PRE_20_FIELD_TYPE.setHasDocValues(false); + PRE_20_FIELD_TYPE.setDateTimeFormatter(DATE_TIME_FORMATTER_BEFORE_2_0); + PRE_20_FIELD_TYPE.setIndexAnalyzer(NumericDateAnalyzer.buildNamedAnalyzer(DATE_TIME_FORMATTER_BEFORE_2_0, Defaults.PRECISION_STEP_64_BIT)); + PRE_20_FIELD_TYPE.setSearchAnalyzer(NumericDateAnalyzer.buildNamedAnalyzer(DATE_TIME_FORMATTER_BEFORE_2_0, Integer.MAX_VALUE)); PRE_20_FIELD_TYPE.freeze(); } @@ -146,8 +146,23 @@ public TimestampFieldMapper build(BuilderContext context) { if (explicitStore == false && context.indexCreatedVersion().before(Version.V_2_0_0)) { fieldType.setStored(false); } + + if (fieldType().dateTimeFormatter().equals(Defaults.DATE_TIME_FORMATTER)) { + fieldType().setDateTimeFormatter(getDateTimeFormatter(context.indexSettings())); + } + setupFieldType(context); - return new TimestampFieldMapper(fieldType, defaultFieldType, enabledState, path, defaultTimestamp, ignoreMissing, context.indexSettings()); + return new TimestampFieldMapper(fieldType, defaultFieldType, enabledState, path, defaultTimestamp, + ignoreMissing, context.indexSettings()); + } + } + + private static FormatDateTimeFormatter getDateTimeFormatter(Settings indexSettings) { + Version indexCreated = Version.indexCreated(indexSettings); + if (indexCreated.onOrAfter(Version.V_2_0_0)) { + return Defaults.DATE_TIME_FORMATTER; + } else { + return Defaults.DATE_TIME_FORMATTER_BEFORE_2_0; } } @@ -341,7 +356,9 @@ && fieldType().dateTimeFormatter().format().equals(Defaults.DATE_TIME_FORMATTER. if (indexCreatedBefore2x && (includeDefaults || path != Defaults.PATH)) { builder.field("path", path); } - if (includeDefaults || !fieldType().dateTimeFormatter().format().equals(Defaults.DATE_TIME_FORMATTER.format())) { + // different format handling depending on index version + String defaultDateFormat = indexCreatedBefore2x ? Defaults.DATE_TIME_FORMATTER_BEFORE_2_0.format() : Defaults.DATE_TIME_FORMATTER.format(); + if (includeDefaults || !fieldType().dateTimeFormatter().format().equals(defaultDateFormat)) { builder.field("format", fieldType().dateTimeFormatter().format()); } if (includeDefaults || !Defaults.DEFAULT_TIMESTAMP.equals(defaultTimestamp)) { diff --git a/core/src/main/java/org/elasticsearch/index/mapper/object/RootObjectMapper.java b/core/src/main/java/org/elasticsearch/index/mapper/object/RootObjectMapper.java index bb411028a762e..2ba2ada1570fa 100644 --- a/core/src/main/java/org/elasticsearch/index/mapper/object/RootObjectMapper.java +++ b/core/src/main/java/org/elasticsearch/index/mapper/object/RootObjectMapper.java @@ -49,7 +49,7 @@ public static class Defaults { public static final FormatDateTimeFormatter[] DYNAMIC_DATE_TIME_FORMATTERS = new FormatDateTimeFormatter[]{ DateFieldMapper.Defaults.DATE_TIME_FORMATTER, - Joda.forPattern("yyyy/MM/dd HH:mm:ss||yyyy/MM/dd") + Joda.getStrictStandardDateFormatter() }; public static final boolean DATE_DETECTION = true; public static final boolean NUMERIC_DETECTION = false; diff --git a/core/src/main/java/org/elasticsearch/snapshots/SnapshotInfo.java b/core/src/main/java/org/elasticsearch/snapshots/SnapshotInfo.java index f07acb0acb5f8..e862862cd3753 100644 --- a/core/src/main/java/org/elasticsearch/snapshots/SnapshotInfo.java +++ b/core/src/main/java/org/elasticsearch/snapshots/SnapshotInfo.java @@ -38,7 +38,7 @@ */ public class SnapshotInfo implements ToXContent, Streamable { - private static final FormatDateTimeFormatter DATE_TIME_FORMATTER = Joda.forPattern("dateOptionalTime"); + private static final FormatDateTimeFormatter DATE_TIME_FORMATTER = Joda.forPattern("strictDateOptionalTime"); private String name; diff --git a/core/src/main/java/org/joda/time/format/StrictISODateTimeFormat.java b/core/src/main/java/org/joda/time/format/StrictISODateTimeFormat.java new file mode 100644 index 0000000000000..9768b9a8559b1 --- /dev/null +++ b/core/src/main/java/org/joda/time/format/StrictISODateTimeFormat.java @@ -0,0 +1,2028 @@ +package org.joda.time.format; + +/* + * Copyright 2001-2009 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. + */ + +import org.joda.time.DateTimeFieldType; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +/* + * Elasticsearch Note: This class has been copied almost identically from joda, where the + * class is named ISODatetimeFormat + * + * However there has been done one huge modification in several methods, which forces the date + * year to be at least n digits, so that a year like "5" is invalid and must be "0005" + * + * All methods have been marked with an "// ES change" commentary + * + * In case you compare this with the original ISODateTimeFormat, make sure you use a diff + * call, that ignores whitespaces/tabs/indendetations like 'diff -b' + */ +/** + * Factory that creates instances of DateTimeFormatter based on the ISO8601 standard. + *

+ * Date-time formatting is performed by the {@link DateTimeFormatter} class. + * Three classes provide factory methods to create formatters, and this is one. + * The others are {@link DateTimeFormat} and {@link DateTimeFormatterBuilder}. + *

+ * ISO8601 is the international standard for data interchange. It defines a + * framework, rather than an absolute standard. As a result this provider has a + * number of methods that represent common uses of the framework. The most common + * formats are {@link #date() date}, {@link #time() time}, and {@link #dateTime() dateTime}. + *

+ * For example, to format a date time in ISO format: + *

+ * DateTime dt = new DateTime();
+ * DateTimeFormatter fmt = ISODateTimeFormat.dateTime();
+ * String str = fmt.print(dt);
+ * 
+ *

+ * Note that these formatters mostly follow the ISO8601 standard for printing. + * For parsing, the formatters are more lenient and allow formats that are not + * in strict compliance with the standard. + *

+ * It is important to understand that these formatters are not linked to + * the ISOChronology. These formatters may be used with any + * chronology, however there may be certain side effects with more unusual + * chronologies. For example, the ISO formatters rely on dayOfWeek being + * single digit, dayOfMonth being two digit and dayOfYear being three digit. + * A chronology with a ten day week would thus cause issues. However, in + * general, it is safe to use these formatters with other chronologies. + *

+ * ISODateTimeFormat is thread-safe and immutable, and the formatters it + * returns are as well. + * + * @author Brian S O'Neill + * @since 1.0 + * @see DateTimeFormat + * @see DateTimeFormatterBuilder + */ +public class StrictISODateTimeFormat { + + /** + * Constructor. + * + * @since 1.1 (previously private) + */ + protected StrictISODateTimeFormat() { + super(); + } + + //----------------------------------------------------------------------- + /** + * Returns a formatter that outputs only those fields specified. + *

+ * This method examines the fields provided and returns an ISO-style + * formatter that best fits. This can be useful for outputting + * less-common ISO styles, such as YearMonth (YYYY-MM) or MonthDay (--MM-DD). + *

+ * The list provided may have overlapping fields, such as dayOfWeek and + * dayOfMonth. In this case, the style is chosen based on the following + * list, thus in the example, the calendar style is chosen as dayOfMonth + * is higher in priority than dayOfWeek: + *

    + *
  • monthOfYear - calendar date style + *
  • dayOfYear - ordinal date style + *
  • weekOfWeekYear - week date style + *
  • dayOfMonth - calendar date style + *
  • dayOfWeek - week date style + *
  • year + *
  • weekyear + *
+ * The supported formats are: + *
+     * Extended      Basic       Fields
+     * 2005-03-25    20050325    year/monthOfYear/dayOfMonth
+     * 2005-03       2005-03     year/monthOfYear
+     * 2005--25      2005--25    year/dayOfMonth *
+     * 2005          2005        year
+     * --03-25       --0325      monthOfYear/dayOfMonth
+     * --03          --03        monthOfYear
+     * ---03         ---03       dayOfMonth
+     * 2005-084      2005084     year/dayOfYear
+     * -084          -084        dayOfYear
+     * 2005-W12-5    2005W125    weekyear/weekOfWeekyear/dayOfWeek
+     * 2005-W-5      2005W-5     weekyear/dayOfWeek *
+     * 2005-W12      2005W12     weekyear/weekOfWeekyear
+     * -W12-5        -W125       weekOfWeekyear/dayOfWeek
+     * -W12          -W12        weekOfWeekyear
+     * -W-5          -W-5        dayOfWeek
+     * 10:20:30.040  102030.040  hour/minute/second/milli
+     * 10:20:30      102030      hour/minute/second
+     * 10:20         1020        hour/minute
+     * 10            10          hour
+     * -20:30.040    -2030.040   minute/second/milli
+     * -20:30        -2030       minute/second
+     * -20           -20         minute
+     * --30.040      --30.040    second/milli
+     * --30          --30        second
+     * ---.040       ---.040     milli *
+     * 10-30.040     10-30.040   hour/second/milli *
+     * 10:20-.040    1020-.040   hour/minute/milli *
+     * 10-30         10-30       hour/second *
+     * 10--.040      10--.040    hour/milli *
+     * -20-.040      -20-.040    minute/milli *
+     *   plus datetime formats like {date}T{time}
+     * 
+ * * indiates that this is not an official ISO format and can be excluded + * by passing in strictISO as true. + *

+ * This method can side effect the input collection of fields. + * If the input collection is modifiable, then each field that was added to + * the formatter will be removed from the collection, including any duplicates. + * If the input collection is unmodifiable then no side effect occurs. + *

+ * This side effect processing is useful if you need to know whether all + * the fields were converted into the formatter or not. To achieve this, + * pass in a modifiable list, and check that it is empty on exit. + * + * @param fields the fields to get a formatter for, not null, + * updated by the method call unless unmodifiable, + * removing those fields built in the formatter + * @param extended true to use the extended format (with separators) + * @param strictISO true to stick exactly to ISO8601, false to include additional formats + * @return a suitable formatter + * @throws IllegalArgumentException if there is no format for the fields + * @since 1.1 + */ + public static DateTimeFormatter forFields( + Collection fields, + boolean extended, + boolean strictISO) { + + if (fields == null || fields.size() == 0) { + throw new IllegalArgumentException("The fields must not be null or empty"); + } + Set workingFields = new HashSet(fields); + int inputSize = workingFields.size(); + boolean reducedPrec = false; + DateTimeFormatterBuilder bld = new DateTimeFormatterBuilder(); + // date + if (workingFields.contains(DateTimeFieldType.monthOfYear())) { + reducedPrec = dateByMonth(bld, workingFields, extended, strictISO); + } else if (workingFields.contains(DateTimeFieldType.dayOfYear())) { + reducedPrec = dateByOrdinal(bld, workingFields, extended, strictISO); + } else if (workingFields.contains(DateTimeFieldType.weekOfWeekyear())) { + reducedPrec = dateByWeek(bld, workingFields, extended, strictISO); + } else if (workingFields.contains(DateTimeFieldType.dayOfMonth())) { + reducedPrec = dateByMonth(bld, workingFields, extended, strictISO); + } else if (workingFields.contains(DateTimeFieldType.dayOfWeek())) { + reducedPrec = dateByWeek(bld, workingFields, extended, strictISO); + } else if (workingFields.remove(DateTimeFieldType.year())) { + bld.append(Constants.ye); + reducedPrec = true; + } else if (workingFields.remove(DateTimeFieldType.weekyear())) { + bld.append(Constants.we); + reducedPrec = true; + } + boolean datePresent = (workingFields.size() < inputSize); + + // time + time(bld, workingFields, extended, strictISO, reducedPrec, datePresent); + + // result + if (bld.canBuildFormatter() == false) { + throw new IllegalArgumentException("No valid format for fields: " + fields); + } + + // side effect the input collection to indicate the processed fields + // handling unmodifiable collections with no side effect + try { + fields.retainAll(workingFields); + } catch (UnsupportedOperationException ex) { + // ignore, so we can handle unmodifiable collections + } + return bld.toFormatter(); + } + + //----------------------------------------------------------------------- + /** + * Creates a date using the calendar date format. + * Specification reference: 5.2.1. + * + * @param bld the builder + * @param fields the fields + * @param extended true to use extended format + * @param strictISO true to only allow ISO formats + * @return true if reduced precision + * @since 1.1 + */ + private static boolean dateByMonth( + DateTimeFormatterBuilder bld, + Collection fields, + boolean extended, + boolean strictISO) { + + boolean reducedPrec = false; + if (fields.remove(DateTimeFieldType.year())) { + bld.append(Constants.ye); + if (fields.remove(DateTimeFieldType.monthOfYear())) { + if (fields.remove(DateTimeFieldType.dayOfMonth())) { + // YYYY-MM-DD/YYYYMMDD + appendSeparator(bld, extended); + bld.appendMonthOfYear(2); + appendSeparator(bld, extended); + bld.appendDayOfMonth(2); + } else { + // YYYY-MM/YYYY-MM + bld.appendLiteral('-'); + bld.appendMonthOfYear(2); + reducedPrec = true; + } + } else { + if (fields.remove(DateTimeFieldType.dayOfMonth())) { + // YYYY--DD/YYYY--DD (non-iso) + checkNotStrictISO(fields, strictISO); + bld.appendLiteral('-'); + bld.appendLiteral('-'); + bld.appendDayOfMonth(2); + } else { + // YYYY/YYYY + reducedPrec = true; + } + } + + } else if (fields.remove(DateTimeFieldType.monthOfYear())) { + bld.appendLiteral('-'); + bld.appendLiteral('-'); + bld.appendMonthOfYear(2); + if (fields.remove(DateTimeFieldType.dayOfMonth())) { + // --MM-DD/--MMDD + appendSeparator(bld, extended); + bld.appendDayOfMonth(2); + } else { + // --MM/--MM + reducedPrec = true; + } + } else if (fields.remove(DateTimeFieldType.dayOfMonth())) { + // ---DD/---DD + bld.appendLiteral('-'); + bld.appendLiteral('-'); + bld.appendLiteral('-'); + bld.appendDayOfMonth(2); + } + return reducedPrec; + } + + //----------------------------------------------------------------------- + /** + * Creates a date using the ordinal date format. + * Specification reference: 5.2.2. + * + * @param bld the builder + * @param fields the fields + * @param extended true to use extended format + * @param strictISO true to only allow ISO formats + * @since 1.1 + */ + private static boolean dateByOrdinal( + DateTimeFormatterBuilder bld, + Collection fields, + boolean extended, + boolean strictISO) { + + boolean reducedPrec = false; + if (fields.remove(DateTimeFieldType.year())) { + bld.append(Constants.ye); + if (fields.remove(DateTimeFieldType.dayOfYear())) { + // YYYY-DDD/YYYYDDD + appendSeparator(bld, extended); + bld.appendDayOfYear(3); + } else { + // YYYY/YYYY + reducedPrec = true; + } + + } else if (fields.remove(DateTimeFieldType.dayOfYear())) { + // -DDD/-DDD + bld.appendLiteral('-'); + bld.appendDayOfYear(3); + } + return reducedPrec; + } + + //----------------------------------------------------------------------- + /** + * Creates a date using the calendar date format. + * Specification reference: 5.2.3. + * + * @param bld the builder + * @param fields the fields + * @param extended true to use extended format + * @param strictISO true to only allow ISO formats + * @since 1.1 + */ + private static boolean dateByWeek( + DateTimeFormatterBuilder bld, + Collection fields, + boolean extended, + boolean strictISO) { + + boolean reducedPrec = false; + if (fields.remove(DateTimeFieldType.weekyear())) { + bld.append(Constants.we); + if (fields.remove(DateTimeFieldType.weekOfWeekyear())) { + appendSeparator(bld, extended); + bld.appendLiteral('W'); + bld.appendWeekOfWeekyear(2); + if (fields.remove(DateTimeFieldType.dayOfWeek())) { + // YYYY-WWW-D/YYYYWWWD + appendSeparator(bld, extended); + bld.appendDayOfWeek(1); + } else { + // YYYY-WWW/YYYY-WWW + reducedPrec = true; + } + } else { + if (fields.remove(DateTimeFieldType.dayOfWeek())) { + // YYYY-W-D/YYYYW-D (non-iso) + checkNotStrictISO(fields, strictISO); + appendSeparator(bld, extended); + bld.appendLiteral('W'); + bld.appendLiteral('-'); + bld.appendDayOfWeek(1); + } else { + // YYYY/YYYY + reducedPrec = true; + } + } + + } else if (fields.remove(DateTimeFieldType.weekOfWeekyear())) { + bld.appendLiteral('-'); + bld.appendLiteral('W'); + bld.appendWeekOfWeekyear(2); + if (fields.remove(DateTimeFieldType.dayOfWeek())) { + // -WWW-D/-WWWD + appendSeparator(bld, extended); + bld.appendDayOfWeek(1); + } else { + // -WWW/-WWW + reducedPrec = true; + } + } else if (fields.remove(DateTimeFieldType.dayOfWeek())) { + // -W-D/-W-D + bld.appendLiteral('-'); + bld.appendLiteral('W'); + bld.appendLiteral('-'); + bld.appendDayOfWeek(1); + } + return reducedPrec; + } + + //----------------------------------------------------------------------- + /** + * Adds the time fields to the builder. + * Specification reference: 5.3.1. + * + * @param bld the builder + * @param fields the fields + * @param extended whether to use the extended format + * @param strictISO whether to be strict + * @param reducedPrec whether the date was reduced precision + * @param datePresent whether there was a date + * @since 1.1 + */ + private static void time( + DateTimeFormatterBuilder bld, + Collection fields, + boolean extended, + boolean strictISO, + boolean reducedPrec, + boolean datePresent) { + + boolean hour = fields.remove(DateTimeFieldType.hourOfDay()); + boolean minute = fields.remove(DateTimeFieldType.minuteOfHour()); + boolean second = fields.remove(DateTimeFieldType.secondOfMinute()); + boolean milli = fields.remove(DateTimeFieldType.millisOfSecond()); + if (!hour && !minute && !second && !milli) { + return; + } + if (hour || minute || second || milli) { + if (strictISO && reducedPrec) { + throw new IllegalArgumentException("No valid ISO8601 format for fields because Date was reduced precision: " + fields); + } + if (datePresent) { + bld.appendLiteral('T'); + } + } + if (hour && minute && second || (hour && !second && !milli)) { + // OK - HMSm/HMS/HM/H - valid in combination with date + } else { + if (strictISO && datePresent) { + throw new IllegalArgumentException("No valid ISO8601 format for fields because Time was truncated: " + fields); + } + if (!hour && (minute && second || (minute && !milli) || second)) { + // OK - MSm/MS/M/Sm/S - valid ISO formats + } else { + if (strictISO) { + throw new IllegalArgumentException("No valid ISO8601 format for fields: " + fields); + } + } + } + if (hour) { + bld.appendHourOfDay(2); + } else if (minute || second || milli) { + bld.appendLiteral('-'); + } + if (extended && hour && minute) { + bld.appendLiteral(':'); + } + if (minute) { + bld.appendMinuteOfHour(2); + } else if (second || milli) { + bld.appendLiteral('-'); + } + if (extended && minute && second) { + bld.appendLiteral(':'); + } + if (second) { + bld.appendSecondOfMinute(2); + } else if (milli) { + bld.appendLiteral('-'); + } + if (milli) { + bld.appendLiteral('.'); + bld.appendMillisOfSecond(3); + } + } + + //----------------------------------------------------------------------- + /** + * Checks that the iso only flag is not set, throwing an exception if it is. + * + * @param fields the fields + * @param strictISO true if only ISO formats allowed + * @since 1.1 + */ + private static void checkNotStrictISO(Collection fields, boolean strictISO) { + if (strictISO) { + throw new IllegalArgumentException("No valid ISO8601 format for fields: " + fields); + } + } + + /** + * Appends the separator if necessary. + * + * @param bld the builder + * @param extended whether to append the separator + * @since 1.1 + */ + private static void appendSeparator(DateTimeFormatterBuilder bld, boolean extended) { + if (extended) { + bld.appendLiteral('-'); + } + } + + //----------------------------------------------------------------------- + /** + * Returns a generic ISO date parser for parsing dates with a possible zone. + *

+ * The returned formatter can only be used for parsing, printing is unsupported. + *

+ * It accepts formats described by the following syntax: + *

+     * date              = date-element ['T' offset]
+     * date-element      = std-date-element | ord-date-element | week-date-element
+     * std-date-element  = yyyy ['-' MM ['-' dd]]
+     * ord-date-element  = yyyy ['-' DDD]
+     * week-date-element = xxxx '-W' ww ['-' e]
+     * offset            = 'Z' | (('+' | '-') HH [':' mm [':' ss [('.' | ',') SSS]]])
+     * 
+ */ + public static DateTimeFormatter dateParser() { + return Constants.dp; + } + + /** + * Returns a generic ISO date parser for parsing local dates. + *

+ * The returned formatter can only be used for parsing, printing is unsupported. + *

+ * This parser is initialised with the local (UTC) time zone. + *

+ * It accepts formats described by the following syntax: + *

+     * date-element      = std-date-element | ord-date-element | week-date-element
+     * std-date-element  = yyyy ['-' MM ['-' dd]]
+     * ord-date-element  = yyyy ['-' DDD]
+     * week-date-element = xxxx '-W' ww ['-' e]
+     * 
+ * @since 1.3 + */ + public static DateTimeFormatter localDateParser() { + return Constants.ldp; + } + + /** + * Returns a generic ISO date parser for parsing dates. + *

+ * The returned formatter can only be used for parsing, printing is unsupported. + *

+ * It accepts formats described by the following syntax: + *

+     * date-element      = std-date-element | ord-date-element | week-date-element
+     * std-date-element  = yyyy ['-' MM ['-' dd]]
+     * ord-date-element  = yyyy ['-' DDD]
+     * week-date-element = xxxx '-W' ww ['-' e]
+     * 
+ */ + public static DateTimeFormatter dateElementParser() { + return Constants.dpe; + } + + /** + * Returns a generic ISO time parser for parsing times with a possible zone. + *

+ * The returned formatter can only be used for parsing, printing is unsupported. + *

+ * The parser is strict by default, thus time string {@code 24:00} cannot be parsed. + *

+ * It accepts formats described by the following syntax: + *

+     * time           = ['T'] time-element [offset]
+     * time-element   = HH [minute-element] | [fraction]
+     * minute-element = ':' mm [second-element] | [fraction]
+     * second-element = ':' ss [fraction]
+     * fraction       = ('.' | ',') digit+
+     * offset         = 'Z' | (('+' | '-') HH [':' mm [':' ss [('.' | ',') SSS]]])
+     * 
+ */ + public static DateTimeFormatter timeParser() { + return Constants.tp; + } + + /** + * Returns a generic ISO time parser for parsing local times. + *

+ * The returned formatter can only be used for parsing, printing is unsupported. + *

+ * This parser is initialised with the local (UTC) time zone. + * The parser is strict by default, thus time string {@code 24:00} cannot be parsed. + *

+ * It accepts formats described by the following syntax: + *

+     * time           = ['T'] time-element
+     * time-element   = HH [minute-element] | [fraction]
+     * minute-element = ':' mm [second-element] | [fraction]
+     * second-element = ':' ss [fraction]
+     * fraction       = ('.' | ',') digit+
+     * 
+ * @since 1.3 + */ + public static DateTimeFormatter localTimeParser() { + return Constants.ltp; + } + + /** + * Returns a generic ISO time parser. + *

+ * The returned formatter can only be used for parsing, printing is unsupported. + *

+ * The parser is strict by default, thus time string {@code 24:00} cannot be parsed. + *

+ * It accepts formats described by the following syntax: + *

+     * time-element   = HH [minute-element] | [fraction]
+     * minute-element = ':' mm [second-element] | [fraction]
+     * second-element = ':' ss [fraction]
+     * fraction       = ('.' | ',') digit+
+     * 
+ */ + public static DateTimeFormatter timeElementParser() { + return Constants.tpe; + } + + /** + * Returns a generic ISO datetime parser which parses either a date or a time or both. + *

+ * The returned formatter can only be used for parsing, printing is unsupported. + *

+ * The parser is strict by default, thus time string {@code 24:00} cannot be parsed. + *

+ * It accepts formats described by the following syntax: + *

+     * datetime          = time | date-opt-time
+     * time              = 'T' time-element [offset]
+     * date-opt-time     = date-element ['T' [time-element] [offset]]
+     * date-element      = std-date-element | ord-date-element | week-date-element
+     * std-date-element  = yyyy ['-' MM ['-' dd]]
+     * ord-date-element  = yyyy ['-' DDD]
+     * week-date-element = xxxx '-W' ww ['-' e]
+     * time-element      = HH [minute-element] | [fraction]
+     * minute-element    = ':' mm [second-element] | [fraction]
+     * second-element    = ':' ss [fraction]
+     * fraction          = ('.' | ',') digit+
+     * offset            = 'Z' | (('+' | '-') HH [':' mm [':' ss [('.' | ',') SSS]]])
+     * 
+ */ + public static DateTimeFormatter dateTimeParser() { + return Constants.dtp; + } + + /** + * Returns a generic ISO datetime parser where the date is mandatory and the time is optional. + *

+ * The returned formatter can only be used for parsing, printing is unsupported. + *

+ * This parser can parse zoned datetimes. + * The parser is strict by default, thus time string {@code 24:00} cannot be parsed. + *

+ * It accepts formats described by the following syntax: + *

+     * date-opt-time     = date-element ['T' [time-element] [offset]]
+     * date-element      = std-date-element | ord-date-element | week-date-element
+     * std-date-element  = yyyy ['-' MM ['-' dd]]
+     * ord-date-element  = yyyy ['-' DDD]
+     * week-date-element = xxxx '-W' ww ['-' e]
+     * time-element      = HH [minute-element] | [fraction]
+     * minute-element    = ':' mm [second-element] | [fraction]
+     * second-element    = ':' ss [fraction]
+     * fraction          = ('.' | ',') digit+
+     * 
+ * @since 1.3 + */ + public static DateTimeFormatter dateOptionalTimeParser() { + return Constants.dotp; + } + + /** + * Returns a generic ISO datetime parser where the date is mandatory and the time is optional. + *

+ * The returned formatter can only be used for parsing, printing is unsupported. + *

+ * This parser only parses local datetimes. + * This parser is initialised with the local (UTC) time zone. + * The parser is strict by default, thus time string {@code 24:00} cannot be parsed. + *

+ * It accepts formats described by the following syntax: + *

+     * datetime          = date-element ['T' time-element]
+     * date-element      = std-date-element | ord-date-element | week-date-element
+     * std-date-element  = yyyy ['-' MM ['-' dd]]
+     * ord-date-element  = yyyy ['-' DDD]
+     * week-date-element = xxxx '-W' ww ['-' e]
+     * time-element      = HH [minute-element] | [fraction]
+     * minute-element    = ':' mm [second-element] | [fraction]
+     * second-element    = ':' ss [fraction]
+     * fraction          = ('.' | ',') digit+
+     * 
+ * @since 1.3 + */ + public static DateTimeFormatter localDateOptionalTimeParser() { + return Constants.ldotp; + } + + //----------------------------------------------------------------------- + /** + * Returns a formatter for a full date as four digit year, two digit month + * of year, and two digit day of month (yyyy-MM-dd). + *

+ * The returned formatter prints and parses only this format. + * See {@link #dateParser()} for a more flexible parser that accepts different formats. + * + * @return a formatter for yyyy-MM-dd + */ + public static DateTimeFormatter date() { + return yearMonthDay(); + } + + /** + * Returns a formatter for a two digit hour of day, two digit minute of + * hour, two digit second of minute, three digit fraction of second, and + * time zone offset (HH:mm:ss.SSSZZ). + *

+ * The time zone offset is 'Z' for zero, and of the form '\u00b1HH:mm' for non-zero. + * The parser is strict by default, thus time string {@code 24:00} cannot be parsed. + *

+ * The returned formatter prints and parses only this format, which includes milliseconds. + * See {@link #timeParser()} for a more flexible parser that accepts different formats. + * + * @return a formatter for HH:mm:ss.SSSZZ + */ + public static DateTimeFormatter time() { + return Constants.t; + } + + /** + * Returns a formatter for a two digit hour of day, two digit minute of + * hour, two digit second of minute, and time zone offset (HH:mm:ssZZ). + *

+ * The time zone offset is 'Z' for zero, and of the form '\u00b1HH:mm' for non-zero. + * The parser is strict by default, thus time string {@code 24:00} cannot be parsed. + *

+ * The returned formatter prints and parses only this format, which excludes milliseconds. + * See {@link #timeParser()} for a more flexible parser that accepts different formats. + * + * @return a formatter for HH:mm:ssZZ + */ + public static DateTimeFormatter timeNoMillis() { + return Constants.tx; + } + + /** + * Returns a formatter for a two digit hour of day, two digit minute of + * hour, two digit second of minute, three digit fraction of second, and + * time zone offset prefixed by 'T' ('T'HH:mm:ss.SSSZZ). + *

+ * The time zone offset is 'Z' for zero, and of the form '\u00b1HH:mm' for non-zero. + * The parser is strict by default, thus time string {@code 24:00} cannot be parsed. + *

+ * The returned formatter prints and parses only this format, which includes milliseconds. + * See {@link #timeParser()} for a more flexible parser that accepts different formats. + * + * @return a formatter for 'T'HH:mm:ss.SSSZZ + */ + public static DateTimeFormatter tTime() { + return Constants.tt; + } + + /** + * Returns a formatter for a two digit hour of day, two digit minute of + * hour, two digit second of minute, and time zone offset prefixed + * by 'T' ('T'HH:mm:ssZZ). + *

+ * The time zone offset is 'Z' for zero, and of the form '\u00b1HH:mm' for non-zero. + * The parser is strict by default, thus time string {@code 24:00} cannot be parsed. + *

+ * The returned formatter prints and parses only this format, which excludes milliseconds. + * See {@link #timeParser()} for a more flexible parser that accepts different formats. + * + * @return a formatter for 'T'HH:mm:ssZZ + */ + public static DateTimeFormatter tTimeNoMillis() { + return Constants.ttx; + } + + /** + * Returns a formatter that combines a full date and time, separated by a 'T' + * (yyyy-MM-dd'T'HH:mm:ss.SSSZZ). + *

+ * The time zone offset is 'Z' for zero, and of the form '\u00b1HH:mm' for non-zero. + * The parser is strict by default, thus time string {@code 24:00} cannot be parsed. + *

+ * The returned formatter prints and parses only this format, which includes milliseconds. + * See {@link #dateTimeParser()} for a more flexible parser that accepts different formats. + * + * @return a formatter for yyyy-MM-dd'T'HH:mm:ss.SSSZZ + */ + public static DateTimeFormatter dateTime() { + return Constants.dt; + } + + /** + * Returns a formatter that combines a full date and time without millis, + * separated by a 'T' (yyyy-MM-dd'T'HH:mm:ssZZ). + *

+ * The time zone offset is 'Z' for zero, and of the form '\u00b1HH:mm' for non-zero. + * The parser is strict by default, thus time string {@code 24:00} cannot be parsed. + *

+ * The returned formatter prints and parses only this format, which excludes milliseconds. + * See {@link #dateTimeParser()} for a more flexible parser that accepts different formats. + * + * @return a formatter for yyyy-MM-dd'T'HH:mm:ssZZ + */ + public static DateTimeFormatter dateTimeNoMillis() { + return Constants.dtx; + } + + /** + * Returns a formatter for a full ordinal date, using a four + * digit year and three digit dayOfYear (yyyy-DDD). + *

+ * The returned formatter prints and parses only this format. + * See {@link #dateParser()} for a more flexible parser that accepts different formats. + * + * @return a formatter for yyyy-DDD + * @since 1.1 + */ + public static DateTimeFormatter ordinalDate() { + return Constants.od; + } + + /** + * Returns a formatter for a full ordinal date and time, using a four + * digit year and three digit dayOfYear (yyyy-DDD'T'HH:mm:ss.SSSZZ). + *

+ * The time zone offset is 'Z' for zero, and of the form '\u00b1HH:mm' for non-zero. + * The parser is strict by default, thus time string {@code 24:00} cannot be parsed. + *

+ * The returned formatter prints and parses only this format, which includes milliseconds. + * See {@link #dateTimeParser()} for a more flexible parser that accepts different formats. + * + * @return a formatter for yyyy-DDD'T'HH:mm:ss.SSSZZ + * @since 1.1 + */ + public static DateTimeFormatter ordinalDateTime() { + return Constants.odt; + } + + /** + * Returns a formatter for a full ordinal date and time without millis, + * using a four digit year and three digit dayOfYear (yyyy-DDD'T'HH:mm:ssZZ). + *

+ * The time zone offset is 'Z' for zero, and of the form '\u00b1HH:mm' for non-zero. + * The parser is strict by default, thus time string {@code 24:00} cannot be parsed. + *

+ * The returned formatter prints and parses only this format, which excludes milliseconds. + * See {@link #dateTimeParser()} for a more flexible parser that accepts different formats. + * + * @return a formatter for yyyy-DDD'T'HH:mm:ssZZ + * @since 1.1 + */ + public static DateTimeFormatter ordinalDateTimeNoMillis() { + return Constants.odtx; + } + + /** + * Returns a formatter for a full date as four digit weekyear, two digit + * week of weekyear, and one digit day of week (xxxx-'W'ww-e). + *

+ * The returned formatter prints and parses only this format. + * See {@link #dateParser()} for a more flexible parser that accepts different formats. + * + * @return a formatter for xxxx-'W'ww-e + */ + public static DateTimeFormatter weekDate() { + return Constants.wwd; + } + + /** + * Returns a formatter that combines a full weekyear date and time, + * separated by a 'T' (xxxx-'W'ww-e'T'HH:mm:ss.SSSZZ). + *

+ * The time zone offset is 'Z' for zero, and of the form '\u00b1HH:mm' for non-zero. + * The parser is strict by default, thus time string {@code 24:00} cannot be parsed. + *

+ * The returned formatter prints and parses only this format, which includes milliseconds. + * See {@link #dateTimeParser()} for a more flexible parser that accepts different formats. + * + * @return a formatter for xxxx-'W'ww-e'T'HH:mm:ss.SSSZZ + */ + public static DateTimeFormatter weekDateTime() { + return Constants.wdt; + } + + /** + * Returns a formatter that combines a full weekyear date and time without millis, + * separated by a 'T' (xxxx-'W'ww-e'T'HH:mm:ssZZ). + *

+ * The time zone offset is 'Z' for zero, and of the form '\u00b1HH:mm' for non-zero. + * The parser is strict by default, thus time string {@code 24:00} cannot be parsed. + *

+ * The returned formatter prints and parses only this format, which excludes milliseconds. + * See {@link #dateTimeParser()} for a more flexible parser that accepts different formats. + * + * @return a formatter for xxxx-'W'ww-e'T'HH:mm:ssZZ + */ + public static DateTimeFormatter weekDateTimeNoMillis() { + return Constants.wdtx; + } + + //----------------------------------------------------------------------- + /** + * Returns a basic formatter for a full date as four digit year, two digit + * month of year, and two digit day of month (yyyyMMdd). + *

+ * The returned formatter prints and parses only this format. + * + * @return a formatter for yyyyMMdd + */ + public static DateTimeFormatter basicDate() { + return Constants.bd; + } + + /** + * Returns a basic formatter for a two digit hour of day, two digit minute + * of hour, two digit second of minute, three digit millis, and time zone + * offset (HHmmss.SSSZ). + *

+ * The time zone offset is 'Z' for zero, and of the form '\u00b1HHmm' for non-zero. + * The parser is strict by default, thus time string {@code 24:00} cannot be parsed. + *

+ * The returned formatter prints and parses only this format, which includes milliseconds. + * + * @return a formatter for HHmmss.SSSZ + */ + public static DateTimeFormatter basicTime() { + return Constants.bt; + } + + /** + * Returns a basic formatter for a two digit hour of day, two digit minute + * of hour, two digit second of minute, and time zone offset (HHmmssZ). + *

+ * The time zone offset is 'Z' for zero, and of the form '\u00b1HHmm' for non-zero. + * The parser is strict by default, thus time string {@code 24:00} cannot be parsed. + *

+ * The returned formatter prints and parses only this format, which excludes milliseconds. + * + * @return a formatter for HHmmssZ + */ + public static DateTimeFormatter basicTimeNoMillis() { + return Constants.btx; + } + + /** + * Returns a basic formatter for a two digit hour of day, two digit minute + * of hour, two digit second of minute, three digit millis, and time zone + * offset prefixed by 'T' ('T'HHmmss.SSSZ). + *

+ * The time zone offset is 'Z' for zero, and of the form '\u00b1HHmm' for non-zero. + * The parser is strict by default, thus time string {@code 24:00} cannot be parsed. + *

+ * The returned formatter prints and parses only this format, which includes milliseconds. + * + * @return a formatter for 'T'HHmmss.SSSZ + */ + public static DateTimeFormatter basicTTime() { + return Constants.btt; + } + + /** + * Returns a basic formatter for a two digit hour of day, two digit minute + * of hour, two digit second of minute, and time zone offset prefixed by 'T' + * ('T'HHmmssZ). + *

+ * The time zone offset is 'Z' for zero, and of the form '\u00b1HHmm' for non-zero. + * The parser is strict by default, thus time string {@code 24:00} cannot be parsed. + *

+ * The returned formatter prints and parses only this format, which excludes milliseconds. + * + * @return a formatter for 'T'HHmmssZ + */ + public static DateTimeFormatter basicTTimeNoMillis() { + return Constants.bttx; + } + + /** + * Returns a basic formatter that combines a basic date and time, separated + * by a 'T' (yyyyMMdd'T'HHmmss.SSSZ). + *

+ * The time zone offset is 'Z' for zero, and of the form '\u00b1HHmm' for non-zero. + * The parser is strict by default, thus time string {@code 24:00} cannot be parsed. + *

+ * The returned formatter prints and parses only this format, which includes milliseconds. + * + * @return a formatter for yyyyMMdd'T'HHmmss.SSSZ + */ + public static DateTimeFormatter basicDateTime() { + return Constants.bdt; + } + + /** + * Returns a basic formatter that combines a basic date and time without millis, + * separated by a 'T' (yyyyMMdd'T'HHmmssZ). + *

+ * The time zone offset is 'Z' for zero, and of the form '\u00b1HHmm' for non-zero. + * The parser is strict by default, thus time string {@code 24:00} cannot be parsed. + *

+ * The returned formatter prints and parses only this format, which excludes milliseconds. + * + * @return a formatter for yyyyMMdd'T'HHmmssZ + */ + public static DateTimeFormatter basicDateTimeNoMillis() { + return Constants.bdtx; + } + + /** + * Returns a formatter for a full ordinal date, using a four + * digit year and three digit dayOfYear (yyyyDDD). + *

+ * The returned formatter prints and parses only this format. + * + * @return a formatter for yyyyDDD + * @since 1.1 + */ + public static DateTimeFormatter basicOrdinalDate() { + return Constants.bod; + } + + /** + * Returns a formatter for a full ordinal date and time, using a four + * digit year and three digit dayOfYear (yyyyDDD'T'HHmmss.SSSZ). + *

+ * The time zone offset is 'Z' for zero, and of the form '\u00b1HHmm' for non-zero. + * The parser is strict by default, thus time string {@code 24:00} cannot be parsed. + *

+ * The returned formatter prints and parses only this format, which includes milliseconds. + * + * @return a formatter for yyyyDDD'T'HHmmss.SSSZ + * @since 1.1 + */ + public static DateTimeFormatter basicOrdinalDateTime() { + return Constants.bodt; + } + + /** + * Returns a formatter for a full ordinal date and time without millis, + * using a four digit year and three digit dayOfYear (yyyyDDD'T'HHmmssZ). + *

+ * The time zone offset is 'Z' for zero, and of the form '\u00b1HHmm' for non-zero. + * The parser is strict by default, thus time string {@code 24:00} cannot be parsed. + *

+ * The returned formatter prints and parses only this format, which excludes milliseconds. + * + * @return a formatter for yyyyDDD'T'HHmmssZ + * @since 1.1 + */ + public static DateTimeFormatter basicOrdinalDateTimeNoMillis() { + return Constants.bodtx; + } + + /** + * Returns a basic formatter for a full date as four digit weekyear, two + * digit week of weekyear, and one digit day of week (xxxx'W'wwe). + *

+ * The returned formatter prints and parses only this format. + * + * @return a formatter for xxxx'W'wwe + */ + public static DateTimeFormatter basicWeekDate() { + return Constants.bwd; + } + + /** + * Returns a basic formatter that combines a basic weekyear date and time, + * separated by a 'T' (xxxx'W'wwe'T'HHmmss.SSSZ). + *

+ * The time zone offset is 'Z' for zero, and of the form '\u00b1HHmm' for non-zero. + * The parser is strict by default, thus time string {@code 24:00} cannot be parsed. + *

+ * The returned formatter prints and parses only this format, which includes milliseconds. + * + * @return a formatter for xxxx'W'wwe'T'HHmmss.SSSZ + */ + public static DateTimeFormatter basicWeekDateTime() { + return Constants.bwdt; + } + + /** + * Returns a basic formatter that combines a basic weekyear date and time + * without millis, separated by a 'T' (xxxx'W'wwe'T'HHmmssZ). + *

+ * The time zone offset is 'Z' for zero, and of the form '\u00b1HHmm' for non-zero. + * The parser is strict by default, thus time string {@code 24:00} cannot be parsed. + *

+ * The returned formatter prints and parses only this format, which excludes milliseconds. + * + * @return a formatter for xxxx'W'wwe'T'HHmmssZ + */ + public static DateTimeFormatter basicWeekDateTimeNoMillis() { + return Constants.bwdtx; + } + + //----------------------------------------------------------------------- + /** + * Returns a formatter for a four digit year. (yyyy) + * + * @return a formatter for yyyy + */ + public static DateTimeFormatter year() { + return Constants.ye; + } + + /** + * Returns a formatter for a four digit year and two digit month of + * year. (yyyy-MM) + * + * @return a formatter for yyyy-MM + */ + public static DateTimeFormatter yearMonth() { + return Constants.ym; + } + + /** + * Returns a formatter for a four digit year, two digit month of year, and + * two digit day of month. (yyyy-MM-dd) + * + * @return a formatter for yyyy-MM-dd + */ + public static DateTimeFormatter yearMonthDay() { + return Constants.ymd; + } + + /** + * Returns a formatter for a four digit weekyear. (xxxx) + * + * @return a formatter for xxxx + */ + public static DateTimeFormatter weekyear() { + return Constants.we; + } + + /** + * Returns a formatter for a four digit weekyear and two digit week of + * weekyear. (xxxx-'W'ww) + * + * @return a formatter for xxxx-'W'ww + */ + public static DateTimeFormatter weekyearWeek() { + return Constants.ww; + } + + /** + * Returns a formatter for a four digit weekyear, two digit week of + * weekyear, and one digit day of week. (xxxx-'W'ww-e) + * + * @return a formatter for xxxx-'W'ww-e + */ + public static DateTimeFormatter weekyearWeekDay() { + return Constants.wwd; + } + + /** + * Returns a formatter for a two digit hour of day. (HH) + * + * @return a formatter for HH + */ + public static DateTimeFormatter hour() { + return Constants.hde; + } + + /** + * Returns a formatter for a two digit hour of day and two digit minute of + * hour. (HH:mm) + * + * @return a formatter for HH:mm + */ + public static DateTimeFormatter hourMinute() { + return Constants.hm; + } + + /** + * Returns a formatter for a two digit hour of day, two digit minute of + * hour, and two digit second of minute. (HH:mm:ss) + * + * @return a formatter for HH:mm:ss + */ + public static DateTimeFormatter hourMinuteSecond() { + return Constants.hms; + } + + /** + * Returns a formatter for a two digit hour of day, two digit minute of + * hour, two digit second of minute, and three digit fraction of + * second (HH:mm:ss.SSS). Parsing will parse up to 3 fractional second + * digits. + * + * @return a formatter for HH:mm:ss.SSS + */ + public static DateTimeFormatter hourMinuteSecondMillis() { + return Constants.hmsl; + } + + /** + * Returns a formatter for a two digit hour of day, two digit minute of + * hour, two digit second of minute, and three digit fraction of + * second (HH:mm:ss.SSS). Parsing will parse up to 9 fractional second + * digits, throwing away all except the first three. + * + * @return a formatter for HH:mm:ss.SSS + */ + public static DateTimeFormatter hourMinuteSecondFraction() { + return Constants.hmsf; + } + + /** + * Returns a formatter that combines a full date and two digit hour of + * day. (yyyy-MM-dd'T'HH) + * + * @return a formatter for yyyy-MM-dd'T'HH + */ + public static DateTimeFormatter dateHour() { + return Constants.dh; + } + + /** + * Returns a formatter that combines a full date, two digit hour of day, + * and two digit minute of hour. (yyyy-MM-dd'T'HH:mm) + * + * @return a formatter for yyyy-MM-dd'T'HH:mm + */ + public static DateTimeFormatter dateHourMinute() { + return Constants.dhm; + } + + /** + * Returns a formatter that combines a full date, two digit hour of day, + * two digit minute of hour, and two digit second of + * minute. (yyyy-MM-dd'T'HH:mm:ss) + * + * @return a formatter for yyyy-MM-dd'T'HH:mm:ss + */ + public static DateTimeFormatter dateHourMinuteSecond() { + return Constants.dhms; + } + + /** + * Returns a formatter that combines a full date, two digit hour of day, + * two digit minute of hour, two digit second of minute, and three digit + * fraction of second (yyyy-MM-dd'T'HH:mm:ss.SSS). Parsing will parse up + * to 3 fractional second digits. + * + * @return a formatter for yyyy-MM-dd'T'HH:mm:ss.SSS + */ + public static DateTimeFormatter dateHourMinuteSecondMillis() { + return Constants.dhmsl; + } + + /** + * Returns a formatter that combines a full date, two digit hour of day, + * two digit minute of hour, two digit second of minute, and three digit + * fraction of second (yyyy-MM-dd'T'HH:mm:ss.SSS). Parsing will parse up + * to 9 fractional second digits, throwing away all except the first three. + * + * @return a formatter for yyyy-MM-dd'T'HH:mm:ss.SSS + */ + public static DateTimeFormatter dateHourMinuteSecondFraction() { + return Constants.dhmsf; + } + + //----------------------------------------------------------------------- + static final class Constants { + private static final DateTimeFormatter + ye = yearElement(), // year element (yyyy) + mye = monthElement(), // monthOfYear element (-MM) + dme = dayOfMonthElement(), // dayOfMonth element (-dd) + we = weekyearElement(), // weekyear element (xxxx) + wwe = weekElement(), // weekOfWeekyear element (-ww) + dwe = dayOfWeekElement(), // dayOfWeek element (-ee) + dye = dayOfYearElement(), // dayOfYear element (-DDD) + hde = hourElement(), // hourOfDay element (HH) + mhe = minuteElement(), // minuteOfHour element (:mm) + sme = secondElement(), // secondOfMinute element (:ss) + fse = fractionElement(), // fractionOfSecond element (.SSSSSSSSS) + ze = offsetElement(), // zone offset element + lte = literalTElement(), // literal 'T' element + + //y, // year (same as year element) + ym = yearMonth(), // year month + ymd = yearMonthDay(), // year month day + + //w, // weekyear (same as weekyear element) + ww = weekyearWeek(), // weekyear week + wwd = weekyearWeekDay(), // weekyear week day + + //h, // hour (same as hour element) + hm = hourMinute(), // hour minute + hms = hourMinuteSecond(), // hour minute second + hmsl = hourMinuteSecondMillis(), // hour minute second millis + hmsf = hourMinuteSecondFraction(), // hour minute second fraction + + dh = dateHour(), // date hour + dhm = dateHourMinute(), // date hour minute + dhms = dateHourMinuteSecond(), // date hour minute second + dhmsl = dateHourMinuteSecondMillis(), // date hour minute second millis + dhmsf = dateHourMinuteSecondFraction(), // date hour minute second fraction + + //d, // date (same as ymd) + t = time(), // time + tx = timeNoMillis(), // time no millis + tt = tTime(), // Ttime + ttx = tTimeNoMillis(), // Ttime no millis + dt = dateTime(), // date time + dtx = dateTimeNoMillis(), // date time no millis + + //wd, // week date (same as wwd) + wdt = weekDateTime(), // week date time + wdtx = weekDateTimeNoMillis(), // week date time no millis + + od = ordinalDate(), // ordinal date (same as yd) + odt = ordinalDateTime(), // ordinal date time + odtx = ordinalDateTimeNoMillis(), // ordinal date time no millis + + bd = basicDate(), // basic date + bt = basicTime(), // basic time + btx = basicTimeNoMillis(), // basic time no millis + btt = basicTTime(), // basic Ttime + bttx = basicTTimeNoMillis(), // basic Ttime no millis + bdt = basicDateTime(), // basic date time + bdtx = basicDateTimeNoMillis(), // basic date time no millis + + bod = basicOrdinalDate(), // basic ordinal date + bodt = basicOrdinalDateTime(), // basic ordinal date time + bodtx = basicOrdinalDateTimeNoMillis(), // basic ordinal date time no millis + + bwd = basicWeekDate(), // basic week date + bwdt = basicWeekDateTime(), // basic week date time + bwdtx = basicWeekDateTimeNoMillis(), // basic week date time no millis + + dpe = dateElementParser(), // date parser element + tpe = timeElementParser(), // time parser element + dp = dateParser(), // date parser + ldp = localDateParser(), // local date parser + tp = timeParser(), // time parser + ltp = localTimeParser(), // local time parser + dtp = dateTimeParser(), // date time parser + dotp = dateOptionalTimeParser(), // date optional time parser + ldotp = localDateOptionalTimeParser(); // local date optional time parser + + //----------------------------------------------------------------------- + private static DateTimeFormatter dateParser() { + if (dp == null) { + DateTimeParser tOffset = new DateTimeFormatterBuilder() + .appendLiteral('T') + .append(offsetElement()).toParser(); + return new DateTimeFormatterBuilder() + .append(dateElementParser()) + .appendOptional(tOffset) + .toFormatter(); + } + return dp; + } + + private static DateTimeFormatter localDateParser() { + if (ldp == null) { + return dateElementParser().withZoneUTC(); + } + return ldp; + } + + private static DateTimeFormatter dateElementParser() { + if (dpe == null) { + return new DateTimeFormatterBuilder() + .append(null, new DateTimeParser[] { + new DateTimeFormatterBuilder() + .append(yearElement()) + .appendOptional + (new DateTimeFormatterBuilder() + .append(monthElement()) + .appendOptional(dayOfMonthElement().getParser()) + .toParser()) + .toParser(), + new DateTimeFormatterBuilder() + .append(weekyearElement()) + .append(weekElement()) + .appendOptional(dayOfWeekElement().getParser()) + .toParser(), + new DateTimeFormatterBuilder() + .append(yearElement()) + .append(dayOfYearElement()) + .toParser() + }) + .toFormatter(); + } + return dpe; + } + + private static DateTimeFormatter timeParser() { + if (tp == null) { + return new DateTimeFormatterBuilder() + .appendOptional(literalTElement().getParser()) + .append(timeElementParser()) + .appendOptional(offsetElement().getParser()) + .toFormatter(); + } + return tp; + } + + private static DateTimeFormatter localTimeParser() { + if (ltp == null) { + return new DateTimeFormatterBuilder() + .appendOptional(literalTElement().getParser()) + .append(timeElementParser()) + .toFormatter().withZoneUTC(); + } + return ltp; + } + + private static DateTimeFormatter timeElementParser() { + if (tpe == null) { + // Decimal point can be either '.' or ',' + DateTimeParser decimalPoint = new DateTimeFormatterBuilder() + .append(null, new DateTimeParser[] { + new DateTimeFormatterBuilder() + .appendLiteral('.') + .toParser(), + new DateTimeFormatterBuilder() + .appendLiteral(',') + .toParser() + }) + .toParser(); + + return new DateTimeFormatterBuilder() + // time-element + .append(hourElement()) + .append + (null, new DateTimeParser[] { + new DateTimeFormatterBuilder() + // minute-element + .append(minuteElement()) + .append + (null, new DateTimeParser[] { + new DateTimeFormatterBuilder() + // second-element + .append(secondElement()) + // second fraction + .appendOptional(new DateTimeFormatterBuilder() + .append(decimalPoint) + .appendFractionOfSecond(1, 9) + .toParser()) + .toParser(), + // minute fraction + new DateTimeFormatterBuilder() + .append(decimalPoint) + .appendFractionOfMinute(1, 9) + .toParser(), + null + }) + .toParser(), + // hour fraction + new DateTimeFormatterBuilder() + .append(decimalPoint) + .appendFractionOfHour(1, 9) + .toParser(), + null + }) + .toFormatter(); + } + return tpe; + } + + private static DateTimeFormatter dateTimeParser() { + if (dtp == null) { + // This is different from the general time parser in that the 'T' + // is required. + DateTimeParser time = new DateTimeFormatterBuilder() + .appendLiteral('T') + .append(timeElementParser()) + .appendOptional(offsetElement().getParser()) + .toParser(); + return new DateTimeFormatterBuilder() + .append(null, new DateTimeParser[] {time, dateOptionalTimeParser().getParser()}) + .toFormatter(); + } + return dtp; + } + + private static DateTimeFormatter dateOptionalTimeParser() { + if (dotp == null) { + DateTimeParser timeOrOffset = new DateTimeFormatterBuilder() + .appendLiteral('T') + .appendOptional(timeElementParser().getParser()) + .appendOptional(offsetElement().getParser()) + .toParser(); + return new DateTimeFormatterBuilder() + .append(dateElementParser()) + .appendOptional(timeOrOffset) + .toFormatter(); + } + return dotp; + } + + private static DateTimeFormatter localDateOptionalTimeParser() { + if (ldotp == null) { + DateTimeParser time = new DateTimeFormatterBuilder() + .appendLiteral('T') + .append(timeElementParser()) + .toParser(); + return new DateTimeFormatterBuilder() + .append(dateElementParser()) + .appendOptional(time) + .toFormatter().withZoneUTC(); + } + return ldotp; + } + + //----------------------------------------------------------------------- + private static DateTimeFormatter time() { + if (t == null) { + return new DateTimeFormatterBuilder() + .append(hourMinuteSecondFraction()) + .append(offsetElement()) + .toFormatter(); + } + return t; + } + + private static DateTimeFormatter timeNoMillis() { + if (tx == null) { + return new DateTimeFormatterBuilder() + .append(hourMinuteSecond()) + .append(offsetElement()) + .toFormatter(); + } + return tx; + } + + private static DateTimeFormatter tTime() { + if (tt == null) { + return new DateTimeFormatterBuilder() + .append(literalTElement()) + .append(time()) + .toFormatter(); + } + return tt; + } + + private static DateTimeFormatter tTimeNoMillis() { + if (ttx == null) { + return new DateTimeFormatterBuilder() + .append(literalTElement()) + .append(timeNoMillis()) + .toFormatter(); + } + return ttx; + } + + private static DateTimeFormatter dateTime() { + if (dt == null) { + return new DateTimeFormatterBuilder() + .append(date()) + .append(tTime()) + .toFormatter(); + } + return dt; + } + + private static DateTimeFormatter dateTimeNoMillis() { + if (dtx == null) { + return new DateTimeFormatterBuilder() + .append(date()) + .append(tTimeNoMillis()) + .toFormatter(); + } + return dtx; + } + + private static DateTimeFormatter ordinalDate() { + if (od == null) { + return new DateTimeFormatterBuilder() + .append(yearElement()) + .append(dayOfYearElement()) + .toFormatter(); + } + return od; + } + + private static DateTimeFormatter ordinalDateTime() { + if (odt == null) { + return new DateTimeFormatterBuilder() + .append(ordinalDate()) + .append(tTime()) + .toFormatter(); + } + return odt; + } + + private static DateTimeFormatter ordinalDateTimeNoMillis() { + if (odtx == null) { + return new DateTimeFormatterBuilder() + .append(ordinalDate()) + .append(tTimeNoMillis()) + .toFormatter(); + } + return odtx; + } + + private static DateTimeFormatter weekDateTime() { + if (wdt == null) { + return new DateTimeFormatterBuilder() + .append(weekDate()) + .append(tTime()) + .toFormatter(); + } + return wdt; + } + + private static DateTimeFormatter weekDateTimeNoMillis() { + if (wdtx == null) { + return new DateTimeFormatterBuilder() + .append(weekDate()) + .append(tTimeNoMillis()) + .toFormatter(); + } + return wdtx; + } + + //----------------------------------------------------------------------- + private static DateTimeFormatter basicDate() { + if (bd == null) { + return new DateTimeFormatterBuilder() + .appendYear(4, 4) + .appendFixedDecimal(DateTimeFieldType.monthOfYear(), 2) + .appendFixedDecimal(DateTimeFieldType.dayOfMonth(), 2) + .toFormatter(); + } + return bd; + } + + private static DateTimeFormatter basicTime() { + if (bt == null) { + return new DateTimeFormatterBuilder() + .appendFixedDecimal(DateTimeFieldType.hourOfDay(), 2) + .appendFixedDecimal(DateTimeFieldType.minuteOfHour(), 2) + .appendFixedDecimal(DateTimeFieldType.secondOfMinute(), 2) + .appendLiteral('.') + .appendFractionOfSecond(3, 9) + .appendTimeZoneOffset("Z", false, 2, 2) + .toFormatter(); + } + return bt; + } + + private static DateTimeFormatter basicTimeNoMillis() { + if (btx == null) { + return new DateTimeFormatterBuilder() + .appendFixedDecimal(DateTimeFieldType.hourOfDay(), 2) + .appendFixedDecimal(DateTimeFieldType.minuteOfHour(), 2) + .appendFixedDecimal(DateTimeFieldType.secondOfMinute(), 2) + .appendTimeZoneOffset("Z", false, 2, 2) + .toFormatter(); + } + return btx; + } + + private static DateTimeFormatter basicTTime() { + if (btt == null) { + return new DateTimeFormatterBuilder() + .append(literalTElement()) + .append(basicTime()) + .toFormatter(); + } + return btt; + } + + private static DateTimeFormatter basicTTimeNoMillis() { + if (bttx == null) { + return new DateTimeFormatterBuilder() + .append(literalTElement()) + .append(basicTimeNoMillis()) + .toFormatter(); + } + return bttx; + } + + private static DateTimeFormatter basicDateTime() { + if (bdt == null) { + return new DateTimeFormatterBuilder() + .append(basicDate()) + .append(basicTTime()) + .toFormatter(); + } + return bdt; + } + + private static DateTimeFormatter basicDateTimeNoMillis() { + if (bdtx == null) { + return new DateTimeFormatterBuilder() + .append(basicDate()) + .append(basicTTimeNoMillis()) + .toFormatter(); + } + return bdtx; + } + + private static DateTimeFormatter basicOrdinalDate() { + if (bod == null) { + return new DateTimeFormatterBuilder() + .appendYear(4, 4) + .appendFixedDecimal(DateTimeFieldType.dayOfYear(), 3) + .toFormatter(); + } + return bod; + } + + private static DateTimeFormatter basicOrdinalDateTime() { + if (bodt == null) { + return new DateTimeFormatterBuilder() + .append(basicOrdinalDate()) + .append(basicTTime()) + .toFormatter(); + } + return bodt; + } + + private static DateTimeFormatter basicOrdinalDateTimeNoMillis() { + if (bodtx == null) { + return new DateTimeFormatterBuilder() + .append(basicOrdinalDate()) + .append(basicTTimeNoMillis()) + .toFormatter(); + } + return bodtx; + } + + private static DateTimeFormatter basicWeekDate() { + if (bwd == null) { + return new DateTimeFormatterBuilder() + .appendFixedSignedDecimal(DateTimeFieldType.weekyear(), 4) // ES change, was .appendWeekyear(4, 4) + .appendLiteral('W') + .appendFixedDecimal(DateTimeFieldType.weekOfWeekyear(), 2) + .appendFixedDecimal(DateTimeFieldType.dayOfWeek(), 1) + .toFormatter(); + } + return bwd; + } + + private static DateTimeFormatter basicWeekDateTime() { + if (bwdt == null) { + return new DateTimeFormatterBuilder() + .append(basicWeekDate()) + .append(basicTTime()) + .toFormatter(); + } + return bwdt; + } + + private static DateTimeFormatter basicWeekDateTimeNoMillis() { + if (bwdtx == null) { + return new DateTimeFormatterBuilder() + .append(basicWeekDate()) + .append(basicTTimeNoMillis()) + .toFormatter(); + } + return bwdtx; + } + + //----------------------------------------------------------------------- + private static DateTimeFormatter yearMonth() { + if (ym == null) { + return new DateTimeFormatterBuilder() + .append(yearElement()) + .append(monthElement()) + .toFormatter(); + } + return ym; + } + + private static DateTimeFormatter yearMonthDay() { + if (ymd == null) { + return new DateTimeFormatterBuilder() + .append(yearElement()) + .append(monthElement()) + .append(dayOfMonthElement()) + .toFormatter(); + } + return ymd; + } + + private static DateTimeFormatter weekyearWeek() { + if (ww == null) { + return new DateTimeFormatterBuilder() + .append(weekyearElement()) + .append(weekElement()) + .toFormatter(); + } + return ww; + } + + private static DateTimeFormatter weekyearWeekDay() { + if (wwd == null) { + return new DateTimeFormatterBuilder() + .append(weekyearElement()) + .append(weekElement()) + .append(dayOfWeekElement()) + .toFormatter(); + } + return wwd; + } + + private static DateTimeFormatter hourMinute() { + if (hm == null) { + return new DateTimeFormatterBuilder() + .append(hourElement()) + .append(minuteElement()) + .toFormatter(); + } + return hm; + } + + private static DateTimeFormatter hourMinuteSecond() { + if (hms == null) { + return new DateTimeFormatterBuilder() + .append(hourElement()) + .append(minuteElement()) + .append(secondElement()) + .toFormatter(); + } + return hms; + } + + private static DateTimeFormatter hourMinuteSecondMillis() { + if (hmsl == null) { + return new DateTimeFormatterBuilder() + .append(hourElement()) + .append(minuteElement()) + .append(secondElement()) + .appendLiteral('.') + .appendFractionOfSecond(3, 3) + .toFormatter(); + } + return hmsl; + } + + private static DateTimeFormatter hourMinuteSecondFraction() { + if (hmsf == null) { + return new DateTimeFormatterBuilder() + .append(hourElement()) + .append(minuteElement()) + .append(secondElement()) + .append(fractionElement()) + .toFormatter(); + } + return hmsf; + } + + private static DateTimeFormatter dateHour() { + if (dh == null) { + return new DateTimeFormatterBuilder() + .append(date()) + .append(literalTElement()) + .append(hour()) + .toFormatter(); + } + return dh; + } + + private static DateTimeFormatter dateHourMinute() { + if (dhm == null) { + return new DateTimeFormatterBuilder() + .append(date()) + .append(literalTElement()) + .append(hourMinute()) + .toFormatter(); + } + return dhm; + } + + private static DateTimeFormatter dateHourMinuteSecond() { + if (dhms == null) { + return new DateTimeFormatterBuilder() + .append(date()) + .append(literalTElement()) + .append(hourMinuteSecond()) + .toFormatter(); + } + return dhms; + } + + private static DateTimeFormatter dateHourMinuteSecondMillis() { + if (dhmsl == null) { + return new DateTimeFormatterBuilder() + .append(date()) + .append(literalTElement()) + .append(hourMinuteSecondMillis()) + .toFormatter(); + } + return dhmsl; + } + + private static DateTimeFormatter dateHourMinuteSecondFraction() { + if (dhmsf == null) { + return new DateTimeFormatterBuilder() + .append(date()) + .append(literalTElement()) + .append(hourMinuteSecondFraction()) + .toFormatter(); + } + return dhmsf; + } + + //----------------------------------------------------------------------- + private static DateTimeFormatter yearElement() { + if (ye == null) { + return new DateTimeFormatterBuilder() + .appendFixedSignedDecimal(DateTimeFieldType.year(), 4) // ES change, was .appendYear(4, 9) + .toFormatter(); + } + return ye; + } + + private static DateTimeFormatter monthElement() { + if (mye == null) { + return new DateTimeFormatterBuilder() + .appendLiteral('-') + .appendFixedSignedDecimal(DateTimeFieldType.monthOfYear(), 2) // ES change, was .appendMonthOfYear(2) + .toFormatter(); + } + return mye; + } + + private static DateTimeFormatter dayOfMonthElement() { + if (dme == null) { + return new DateTimeFormatterBuilder() + .appendLiteral('-') + .appendFixedSignedDecimal(DateTimeFieldType.dayOfMonth(), 2) // ES change, was .appendDayOfMonth(2) + .toFormatter(); + } + return dme; + } + + private static DateTimeFormatter weekyearElement() { + if (we == null) { + return new DateTimeFormatterBuilder() + .appendFixedSignedDecimal(DateTimeFieldType.weekyear(), 4) // ES change, was .appendWeekyear(4, 9) + .toFormatter(); + } + return we; + } + + private static DateTimeFormatter weekElement() { + if (wwe == null) { + return new DateTimeFormatterBuilder() + .appendLiteral("-W") + .appendFixedSignedDecimal(DateTimeFieldType.weekOfWeekyear(), 2) // ES change, was .appendWeekOfWeekyear(2) + .toFormatter(); + } + return wwe; + } + + private static DateTimeFormatter dayOfWeekElement() { + if (dwe == null) { + return new DateTimeFormatterBuilder() + .appendLiteral('-') + .appendDayOfWeek(1) + .toFormatter(); + } + return dwe; + } + + private static DateTimeFormatter dayOfYearElement() { + if (dye == null) { + return new DateTimeFormatterBuilder() + .appendLiteral('-') + .appendFixedSignedDecimal(DateTimeFieldType.dayOfYear(), 3) // ES change, was .appendDayOfYear(3) + .toFormatter(); + } + return dye; + } + + private static DateTimeFormatter literalTElement() { + if (lte == null) { + return new DateTimeFormatterBuilder() + .appendLiteral('T') + .toFormatter(); + } + return lte; + } + + private static DateTimeFormatter hourElement() { + if (hde == null) { + return new DateTimeFormatterBuilder() + .appendFixedSignedDecimal(DateTimeFieldType.hourOfDay(), 2) // ES change, was .appendHourOfDay(2) + .toFormatter(); + } + return hde; + } + + private static DateTimeFormatter minuteElement() { + if (mhe == null) { + return new DateTimeFormatterBuilder() + .appendLiteral(':') + .appendFixedSignedDecimal(DateTimeFieldType.minuteOfHour(), 2) // ES change, was .appendMinuteOfHour(2) + .toFormatter(); + } + return mhe; + } + + private static DateTimeFormatter secondElement() { + if (sme == null) { + return new DateTimeFormatterBuilder() + .appendLiteral(':') + .appendFixedSignedDecimal(DateTimeFieldType.secondOfMinute(), 2) // ES change, was .appendSecondOfMinute(2) + .toFormatter(); + } + return sme; + } + + private static DateTimeFormatter fractionElement() { + if (fse == null) { + return new DateTimeFormatterBuilder() + .appendLiteral('.') + // Support parsing up to nanosecond precision even though + // those extra digits will be dropped. + .appendFractionOfSecond(3, 9) + .toFormatter(); + } + return fse; + } + + private static DateTimeFormatter offsetElement() { + if (ze == null) { + return new DateTimeFormatterBuilder() + .appendTimeZoneOffset("Z", true, 2, 4) + .toFormatter(); + } + return ze; + } + + } + +} diff --git a/core/src/test/java/org/elasticsearch/deps/joda/SimpleJodaTests.java b/core/src/test/java/org/elasticsearch/deps/joda/SimpleJodaTests.java index 748573db00775..21c16d81b4aa2 100644 --- a/core/src/test/java/org/elasticsearch/deps/joda/SimpleJodaTests.java +++ b/core/src/test/java/org/elasticsearch/deps/joda/SimpleJodaTests.java @@ -22,8 +22,11 @@ import org.elasticsearch.common.joda.FormatDateTimeFormatter; import org.elasticsearch.common.joda.Joda; import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.index.mapper.core.DateFieldMapper; +import org.elasticsearch.index.mapper.object.RootObjectMapper; import org.elasticsearch.test.ElasticsearchTestCase; import org.joda.time.DateTime; +import org.joda.time.DateTimeFieldType; import org.joda.time.DateTimeZone; import org.joda.time.LocalDateTime; import org.joda.time.MutableDateTime; @@ -361,6 +364,368 @@ public void testThatEpochParserIsIdempotent() { assertThat(secondsDateTime.getMillis(), is(1234567890000l)); } + public void testThatDefaultFormatterChecksForCorrectYearLength() throws Exception { + // if no strict version is tested, this means the date format is already strict by itself + // yyyyMMdd + assertValidDateFormatParsing("basicDate", "20140303"); + assertDateFormatParsingThrowingException("basicDate", "2010303"); + + // yyyyMMdd’T'HHmmss.SSSZ + assertValidDateFormatParsing("basicDateTime", "20140303T124343.123Z"); + assertValidDateFormatParsing("basicDateTime", "00050303T124343.123Z"); + assertDateFormatParsingThrowingException("basicDateTime", "50303T124343.123Z"); + + // yyyyMMdd’T'HHmmssZ + assertValidDateFormatParsing("basicDateTimeNoMillis", "20140303T124343Z"); + assertValidDateFormatParsing("basicDateTimeNoMillis", "00050303T124343Z"); + assertDateFormatParsingThrowingException("basicDateTimeNoMillis", "50303T124343Z"); + + // yyyyDDD + assertValidDateFormatParsing("basicOrdinalDate", "0005165"); + assertDateFormatParsingThrowingException("basicOrdinalDate", "5165"); + + // yyyyDDD’T'HHmmss.SSSZ + assertValidDateFormatParsing("basicOrdinalDateTime", "0005165T124343.123Z"); + assertValidDateFormatParsing("basicOrdinalDateTime", "0005165T124343.123Z"); + assertDateFormatParsingThrowingException("basicOrdinalDateTime", "5165T124343.123Z"); + + // yyyyDDD’T'HHmmssZ + assertValidDateFormatParsing("basicOrdinalDateTimeNoMillis", "0005165T124343Z"); + assertValidDateFormatParsing("basicOrdinalDateTimeNoMillis", "0005165T124343Z"); + assertDateFormatParsingThrowingException("basicOrdinalDateTimeNoMillis", "5165T124343Z"); + + // HHmmss.SSSZ + assertValidDateFormatParsing("basicTime", "090909.123Z"); + assertDateFormatParsingThrowingException("basicTime", "90909.123Z"); + + // HHmmssZ + assertValidDateFormatParsing("basicTimeNoMillis", "090909Z"); + assertDateFormatParsingThrowingException("basicTimeNoMillis", "90909Z"); + + // 'T’HHmmss.SSSZ + assertValidDateFormatParsing("basicTTime", "T090909.123Z"); + assertDateFormatParsingThrowingException("basicTTime", "T90909.123Z"); + + // T’HHmmssZ + assertValidDateFormatParsing("basicTTimeNoMillis", "T090909Z"); + assertDateFormatParsingThrowingException("basicTTimeNoMillis", "T90909Z"); + + // xxxx’W'wwe + assertValidDateFormatParsing("basicWeekDate", "0005W414"); + assertValidDateFormatParsing("basicWeekDate", "5W414", "0005W414"); + assertDateFormatParsingThrowingException("basicWeekDate", "5W14"); + + assertValidDateFormatParsing("strictBasicWeekDate", "0005W414"); + assertDateFormatParsingThrowingException("strictBasicWeekDate", "0005W47"); + assertDateFormatParsingThrowingException("strictBasicWeekDate", "5W414"); + assertDateFormatParsingThrowingException("strictBasicWeekDate", "5W14"); + + // xxxx’W'wwe’T'HHmmss.SSSZ + assertValidDateFormatParsing("basicWeekDateTime", "0005W414T124343.123Z"); + assertValidDateFormatParsing("basicWeekDateTime", "5W414T124343.123Z", "0005W414T124343.123Z"); + assertDateFormatParsingThrowingException("basicWeekDateTime", "5W14T124343.123Z"); + + assertValidDateFormatParsing("strictBasicWeekDateTime", "0005W414T124343.123Z"); + assertDateFormatParsingThrowingException("strictBasicWeekDateTime", "0005W47T124343.123Z"); + assertDateFormatParsingThrowingException("strictBasicWeekDateTime", "5W414T124343.123Z"); + assertDateFormatParsingThrowingException("strictBasicWeekDateTime", "5W14T124343.123Z"); + + // xxxx’W'wwe’T'HHmmssZ + assertValidDateFormatParsing("basicWeekDateTimeNoMillis", "0005W414T124343Z"); + assertValidDateFormatParsing("basicWeekDateTimeNoMillis", "5W414T124343Z", "0005W414T124343Z"); + assertDateFormatParsingThrowingException("basicWeekDateTimeNoMillis", "5W14T124343Z"); + + assertValidDateFormatParsing("strictBasicWeekDateTimeNoMillis", "0005W414T124343Z"); + assertDateFormatParsingThrowingException("strictBasicWeekDateTimeNoMillis", "0005W47T124343Z"); + assertDateFormatParsingThrowingException("strictBasicWeekDateTimeNoMillis", "5W414T124343Z"); + assertDateFormatParsingThrowingException("strictBasicWeekDateTimeNoMillis", "5W14T124343Z"); + + // yyyy-MM-dd + assertValidDateFormatParsing("date", "0005-06-03"); + assertValidDateFormatParsing("date", "5-6-3", "0005-06-03"); + + assertValidDateFormatParsing("strictDate", "0005-06-03"); + assertDateFormatParsingThrowingException("strictDate", "5-6-3"); + assertDateFormatParsingThrowingException("strictDate", "0005-06-3"); + assertDateFormatParsingThrowingException("strictDate", "0005-6-03"); + assertDateFormatParsingThrowingException("strictDate", "5-06-03"); + + // yyyy-MM-dd'T'HH + assertValidDateFormatParsing("dateHour", "0005-06-03T12"); + assertValidDateFormatParsing("dateHour", "5-6-3T1", "0005-06-03T01"); + + assertValidDateFormatParsing("strictDateHour", "0005-06-03T12"); + assertDateFormatParsingThrowingException("strictDateHour", "5-6-3T1"); + + // yyyy-MM-dd'T'HH:mm + assertValidDateFormatParsing("dateHourMinute", "0005-06-03T12:12"); + assertValidDateFormatParsing("dateHourMinute", "5-6-3T12:1", "0005-06-03T12:01"); + + assertValidDateFormatParsing("strictDateHourMinute", "0005-06-03T12:12"); + assertDateFormatParsingThrowingException("strictDateHourMinute", "5-6-3T12:1"); + + // yyyy-MM-dd'T'HH:mm:ss + assertValidDateFormatParsing("dateHourMinuteSecond", "0005-06-03T12:12:12"); + assertValidDateFormatParsing("dateHourMinuteSecond", "5-6-3T12:12:1", "0005-06-03T12:12:01"); + + assertValidDateFormatParsing("strictDateHourMinuteSecond", "0005-06-03T12:12:12"); + assertDateFormatParsingThrowingException("strictDateHourMinuteSecond", "5-6-3T12:12:1"); + + // yyyy-MM-dd’T'HH:mm:ss.SSS + assertValidDateFormatParsing("dateHourMinuteSecondFraction", "0005-06-03T12:12:12.123"); + assertValidDateFormatParsing("dateHourMinuteSecondFraction", "5-6-3T12:12:1.123", "0005-06-03T12:12:01.123"); + assertValidDateFormatParsing("dateHourMinuteSecondFraction", "5-6-3T12:12:1.1", "0005-06-03T12:12:01.100"); + + assertValidDateFormatParsing("strictDateHourMinuteSecondFraction", "0005-06-03T12:12:12.123"); + assertDateFormatParsingThrowingException("strictDateHourMinuteSecondFraction", "5-6-3T12:12:12.1"); + assertDateFormatParsingThrowingException("strictDateHourMinuteSecondFraction", "5-6-3T12:12:12.12"); + + assertValidDateFormatParsing("dateHourMinuteSecondMillis", "0005-06-03T12:12:12.123"); + assertValidDateFormatParsing("dateHourMinuteSecondMillis", "5-6-3T12:12:1.123", "0005-06-03T12:12:01.123"); + assertValidDateFormatParsing("dateHourMinuteSecondMillis", "5-6-3T12:12:1.1", "0005-06-03T12:12:01.100"); + + assertValidDateFormatParsing("strictDateHourMinuteSecondMillis", "0005-06-03T12:12:12.123"); + assertDateFormatParsingThrowingException("strictDateHourMinuteSecondMillis", "5-6-3T12:12:12.1"); + assertDateFormatParsingThrowingException("strictDateHourMinuteSecondMillis", "5-6-3T12:12:12.12"); + + // yyyy-MM-dd'T'HH:mm:ss.SSSZ + assertValidDateFormatParsing("dateOptionalTime", "2014-03-03", "2014-03-03T00:00:00.000Z"); + assertValidDateFormatParsing("dateOptionalTime", "1257-3-03", "1257-03-03T00:00:00.000Z"); + assertValidDateFormatParsing("dateOptionalTime", "0005-03-3", "0005-03-03T00:00:00.000Z"); + assertValidDateFormatParsing("dateOptionalTime", "5-03-03", "0005-03-03T00:00:00.000Z"); + assertValidDateFormatParsing("dateOptionalTime", "5-03-03T1:1:1.1", "0005-03-03T01:01:01.100Z"); + assertValidDateFormatParsing("strictDateOptionalTime", "2014-03-03", "2014-03-03T00:00:00.000Z"); + assertDateFormatParsingThrowingException("strictDateOptionalTime", "5-03-03"); + assertDateFormatParsingThrowingException("strictDateOptionalTime", "0005-3-03"); + assertDateFormatParsingThrowingException("strictDateOptionalTime", "0005-03-3"); + assertDateFormatParsingThrowingException("strictDateOptionalTime", "5-03-03T1:1:1.1"); + assertDateFormatParsingThrowingException("strictDateOptionalTime", "5-03-03T01:01:01.1"); + assertDateFormatParsingThrowingException("strictDateOptionalTime", "5-03-03T01:01:1.100"); + assertDateFormatParsingThrowingException("strictDateOptionalTime", "5-03-03T01:1:01.100"); + assertDateFormatParsingThrowingException("strictDateOptionalTime", "5-03-03T1:01:01.100"); + + // yyyy-MM-dd’T'HH:mm:ss.SSSZZ + assertValidDateFormatParsing("dateTime", "5-03-03T1:1:1.1Z", "0005-03-03T01:01:01.100Z"); + assertValidDateFormatParsing("strictDateTime", "2014-03-03T11:11:11.100Z", "2014-03-03T11:11:11.100Z"); + assertDateFormatParsingThrowingException("strictDateTime", "0005-03-03T1:1:1.1Z"); + assertDateFormatParsingThrowingException("strictDateTime", "0005-03-03T01:01:1.100Z"); + assertDateFormatParsingThrowingException("strictDateTime", "0005-03-03T01:1:01.100Z"); + assertDateFormatParsingThrowingException("strictDateTime", "0005-03-03T1:01:01.100Z"); + + // yyyy-MM-dd’T'HH:mm:ssZZ + assertValidDateFormatParsing("dateTimeNoMillis", "5-03-03T1:1:1Z", "0005-03-03T01:01:01Z"); + assertValidDateFormatParsing("strictDateTimeNoMillis", "2014-03-03T11:11:11Z", "2014-03-03T11:11:11Z"); + assertDateFormatParsingThrowingException("strictDateTimeNoMillis", "0005-03-03T1:1:1Z"); + assertDateFormatParsingThrowingException("strictDateTimeNoMillis", "0005-03-03T01:01:1Z"); + assertDateFormatParsingThrowingException("strictDateTimeNoMillis", "0005-03-03T01:1:01Z"); + assertDateFormatParsingThrowingException("strictDateTimeNoMillis", "0005-03-03T1:01:01Z"); + + // HH + assertValidDateFormatParsing("hour", "12"); + assertValidDateFormatParsing("hour", "1", "01"); + assertValidDateFormatParsing("strictHour", "12"); + assertValidDateFormatParsing("strictHour", "01"); + assertDateFormatParsingThrowingException("strictHour", "1"); + + // HH:mm + assertValidDateFormatParsing("hourMinute", "12:12"); + assertValidDateFormatParsing("hourMinute", "12:1", "12:01"); + assertValidDateFormatParsing("strictHourMinute", "12:12"); + assertValidDateFormatParsing("strictHourMinute", "12:01"); + assertDateFormatParsingThrowingException("strictHourMinute", "12:1"); + + // HH:mm:ss + assertValidDateFormatParsing("hourMinuteSecond", "12:12:12"); + assertValidDateFormatParsing("hourMinuteSecond", "12:12:1", "12:12:01"); + assertValidDateFormatParsing("strictHourMinuteSecond", "12:12:12"); + assertValidDateFormatParsing("strictHourMinuteSecond", "12:12:01"); + assertDateFormatParsingThrowingException("strictHourMinuteSecond", "12:12:1"); + + // HH:mm:ss.SSS + assertValidDateFormatParsing("hourMinuteSecondFraction", "12:12:12.123"); + assertValidDateFormatParsing("hourMinuteSecondFraction", "12:12:12.1", "12:12:12.100"); + assertValidDateFormatParsing("strictHourMinuteSecondFraction", "12:12:12.123"); + assertValidDateFormatParsing("strictHourMinuteSecondFraction", "12:12:12.1", "12:12:12.100"); + + assertValidDateFormatParsing("hourMinuteSecondMillis", "12:12:12.123"); + assertValidDateFormatParsing("hourMinuteSecondMillis", "12:12:12.1", "12:12:12.100"); + assertValidDateFormatParsing("strictHourMinuteSecondMillis", "12:12:12.123"); + assertValidDateFormatParsing("strictHourMinuteSecondMillis", "12:12:12.1", "12:12:12.100"); + + // yyyy-DDD + assertValidDateFormatParsing("ordinalDate", "5-3", "0005-003"); + assertValidDateFormatParsing("strictOrdinalDate", "0005-003"); + assertDateFormatParsingThrowingException("strictOrdinalDate", "5-3"); + assertDateFormatParsingThrowingException("strictOrdinalDate", "0005-3"); + assertDateFormatParsingThrowingException("strictOrdinalDate", "5-003"); + + // yyyy-DDD’T'HH:mm:ss.SSSZZ + assertValidDateFormatParsing("ordinalDateTime", "5-3T12:12:12.100Z", "0005-003T12:12:12.100Z"); + assertValidDateFormatParsing("strictOrdinalDateTime", "0005-003T12:12:12.100Z"); + assertDateFormatParsingThrowingException("strictOrdinalDateTime", "5-3T1:12:12.123Z"); + assertDateFormatParsingThrowingException("strictOrdinalDateTime", "5-3T12:1:12.123Z"); + assertDateFormatParsingThrowingException("strictOrdinalDateTime", "5-3T12:12:1.123Z"); + + // yyyy-DDD’T'HH:mm:ssZZ + assertValidDateFormatParsing("ordinalDateTimeNoMillis", "5-3T12:12:12Z", "0005-003T12:12:12Z"); + assertValidDateFormatParsing("strictOrdinalDateTimeNoMillis", "0005-003T12:12:12Z"); + assertDateFormatParsingThrowingException("strictOrdinalDateTimeNoMillis", "5-3T1:12:12Z"); + assertDateFormatParsingThrowingException("strictOrdinalDateTimeNoMillis", "5-3T12:1:12Z"); + assertDateFormatParsingThrowingException("strictOrdinalDateTimeNoMillis", "5-3T12:12:1Z"); + + + // HH:mm:ss.SSSZZ + assertValidDateFormatParsing("time", "12:12:12.100Z"); + assertValidDateFormatParsing("time", "01:01:01.1Z", "01:01:01.100Z"); + assertValidDateFormatParsing("time", "1:1:1.1Z", "01:01:01.100Z"); + assertValidDateFormatParsing("strictTime", "12:12:12.100Z"); + assertDateFormatParsingThrowingException("strictTime", "12:12:1.100Z"); + assertDateFormatParsingThrowingException("strictTime", "12:1:12.100Z"); + assertDateFormatParsingThrowingException("strictTime", "1:12:12.100Z"); + + // HH:mm:ssZZ + assertValidDateFormatParsing("timeNoMillis", "12:12:12Z"); + assertValidDateFormatParsing("timeNoMillis", "01:01:01Z", "01:01:01Z"); + assertValidDateFormatParsing("timeNoMillis", "1:1:1Z", "01:01:01Z"); + assertValidDateFormatParsing("strictTimeNoMillis", "12:12:12Z"); + assertDateFormatParsingThrowingException("strictTimeNoMillis", "12:12:1Z"); + assertDateFormatParsingThrowingException("strictTimeNoMillis", "12:1:12Z"); + assertDateFormatParsingThrowingException("strictTimeNoMillis", "1:12:12Z"); + + // 'T’HH:mm:ss.SSSZZ + assertValidDateFormatParsing("tTime", "T12:12:12.100Z"); + assertValidDateFormatParsing("tTime", "T01:01:01.1Z", "T01:01:01.100Z"); + assertValidDateFormatParsing("tTime", "T1:1:1.1Z", "T01:01:01.100Z"); + assertValidDateFormatParsing("strictTTime", "T12:12:12.100Z"); + assertDateFormatParsingThrowingException("strictTTime", "T12:12:1.100Z"); + assertDateFormatParsingThrowingException("strictTTime", "T12:1:12.100Z"); + assertDateFormatParsingThrowingException("strictTTime", "T1:12:12.100Z"); + + // 'T’HH:mm:ssZZ + assertValidDateFormatParsing("tTimeNoMillis", "T12:12:12Z"); + assertValidDateFormatParsing("tTimeNoMillis", "T01:01:01Z", "T01:01:01Z"); + assertValidDateFormatParsing("tTimeNoMillis", "T1:1:1Z", "T01:01:01Z"); + assertValidDateFormatParsing("strictTTimeNoMillis", "T12:12:12Z"); + assertDateFormatParsingThrowingException("strictTTimeNoMillis", "T12:12:1Z"); + assertDateFormatParsingThrowingException("strictTTimeNoMillis", "T12:1:12Z"); + assertDateFormatParsingThrowingException("strictTTimeNoMillis", "T1:12:12Z"); + + // xxxx-'W’ww-e + assertValidDateFormatParsing("weekDate", "0005-W4-1", "0005-W04-1"); + assertValidDateFormatParsing("strictWeekDate", "0005-W04-1"); + assertDateFormatParsingThrowingException("strictWeekDate", "0005-W4-1"); + + // xxxx-'W’ww-e’T'HH:mm:ss.SSSZZ + assertValidDateFormatParsing("weekDateTime", "0005-W41-4T12:43:43.123Z"); + assertValidDateFormatParsing("weekDateTime", "5-W41-4T12:43:43.123Z", "0005-W41-4T12:43:43.123Z"); + assertValidDateFormatParsing("strictWeekDateTime", "0005-W41-4T12:43:43.123Z"); + assertValidDateFormatParsing("strictWeekDateTime", "0005-W06-4T12:43:43.123Z"); + assertDateFormatParsingThrowingException("strictWeekDateTime", "0005-W4-7T12:43:43.123Z"); + assertDateFormatParsingThrowingException("strictWeekDateTime", "5-W41-4T12:43:43.123Z"); + assertDateFormatParsingThrowingException("strictWeekDateTime", "5-W1-4T12:43:43.123Z"); + + // xxxx-'W’ww-e’T'HH:mm:ssZZ + assertValidDateFormatParsing("weekDateTimeNoMillis", "0005-W41-4T12:43:43Z"); + assertValidDateFormatParsing("weekDateTimeNoMillis", "5-W41-4T12:43:43Z", "0005-W41-4T12:43:43Z"); + assertValidDateFormatParsing("strictWeekDateTimeNoMillis", "0005-W41-4T12:43:43Z"); + assertValidDateFormatParsing("strictWeekDateTimeNoMillis", "0005-W06-4T12:43:43Z"); + assertDateFormatParsingThrowingException("strictWeekDateTimeNoMillis", "0005-W4-7T12:43:43Z"); + assertDateFormatParsingThrowingException("strictWeekDateTimeNoMillis", "5-W41-4T12:43:43Z"); + assertDateFormatParsingThrowingException("strictWeekDateTimeNoMillis", "5-W1-4T12:43:43Z"); + + // yyyy + assertValidDateFormatParsing("weekyear", "2014"); + assertValidDateFormatParsing("weekyear", "5", "0005"); + assertValidDateFormatParsing("weekyear", "0005"); + assertValidDateFormatParsing("strictWeekyear", "2014"); + assertValidDateFormatParsing("strictWeekyear", "0005"); + assertDateFormatParsingThrowingException("strictWeekyear", "5"); + + // yyyy-'W'ee + assertValidDateFormatParsing("weekyearWeek", "2014-W41"); + assertValidDateFormatParsing("weekyearWeek", "2014-W1", "2014-W01"); + assertValidDateFormatParsing("strictWeekyearWeek", "2014-W41"); + assertDateFormatParsingThrowingException("strictWeekyearWeek", "2014-W1"); + + // weekyearWeekDay + assertValidDateFormatParsing("weekyearWeekDay", "2014-W41-1"); + assertValidDateFormatParsing("weekyearWeekDay", "2014-W1-1", "2014-W01-1"); + assertValidDateFormatParsing("strictWeekyearWeekDay", "2014-W41-1"); + assertDateFormatParsingThrowingException("strictWeekyearWeekDay", "2014-W1-1"); + + // yyyy + assertValidDateFormatParsing("year", "2014"); + assertValidDateFormatParsing("year", "5", "0005"); + assertValidDateFormatParsing("strictYear", "2014"); + assertDateFormatParsingThrowingException("strictYear", "5"); + + // yyyy-mm + assertValidDateFormatParsing("yearMonth", "2014-12"); + assertValidDateFormatParsing("yearMonth", "2014-5", "2014-05"); + assertValidDateFormatParsing("strictYearMonth", "2014-12"); + assertDateFormatParsingThrowingException("strictYearMonth", "2014-5"); + + // yyyy-mm-dd + assertValidDateFormatParsing("yearMonthDay", "2014-12-12"); + assertValidDateFormatParsing("yearMonthDay", "2014-05-5", "2014-05-05"); + assertValidDateFormatParsing("strictYearMonthDay", "2014-12-12"); + assertDateFormatParsingThrowingException("strictYearMonthDay", "2014-05-5"); + } + + @Test + public void testThatRootObjectParsingIsStrict() throws Exception { + String[] datesThatWork = new String[] { "2014/10/10", "2014/10/10 12:12:12", "2014-05-05", "2014-05-05T12:12:12.123Z" }; + String[] datesThatShouldNotWork = new String[]{ "5-05-05", "2014-5-05", "2014-05-5", + "2014-05-05T1:12:12.123Z", "2014-05-05T12:1:12.123Z", "2014-05-05T12:12:1.123Z", + "4/10/10", "2014/1/10", "2014/10/1", + "2014/10/10 1:12:12", "2014/10/10 12:1:12", "2014/10/10 12:12:1" + }; + + // good case + for (String date : datesThatWork) { + boolean dateParsingSuccessful = false; + for (FormatDateTimeFormatter dateTimeFormatter : RootObjectMapper.Defaults.DYNAMIC_DATE_TIME_FORMATTERS) { + try { + dateTimeFormatter.parser().parseMillis(date); + dateParsingSuccessful = true; + break; + } catch (Exception e) {} + } + if (!dateParsingSuccessful) { + fail("Parsing for date " + date + " in root object mapper failed, but shouldnt"); + } + } + + // bad case + for (String date : datesThatShouldNotWork) { + for (FormatDateTimeFormatter dateTimeFormatter : RootObjectMapper.Defaults.DYNAMIC_DATE_TIME_FORMATTERS) { + try { + dateTimeFormatter.parser().parseMillis(date); + fail(String.format(Locale.ROOT, "Expected exception when parsing date %s in root mapper", date)); + } catch (Exception e) {} + } + } + } + + private void assertValidDateFormatParsing(String pattern, String dateToParse) { + assertValidDateFormatParsing(pattern, dateToParse, dateToParse); + } + + private void assertValidDateFormatParsing(String pattern, String dateToParse, String expectedDate) { + FormatDateTimeFormatter formatter = Joda.forPattern(pattern); + assertThat(formatter.printer().print(formatter.parser().parseMillis(dateToParse)), is(expectedDate)); + } + + private void assertDateFormatParsingThrowingException(String pattern, String invalidDate) { + try { + FormatDateTimeFormatter formatter = Joda.forPattern(pattern); + DateTimeFormatter parser = formatter.parser(); + parser.parseMillis(invalidDate); + fail(String.format(Locale.ROOT, "Expected parsing exception for pattern [%s] with date [%s], but did not happen", pattern, invalidDate)); + } catch (IllegalArgumentException e) { + } + } + private long utcTimeInMillis(String time) { return ISODateTimeFormat.dateOptionalTimeParser().withZone(DateTimeZone.UTC).parseMillis(time); } diff --git a/core/src/test/java/org/elasticsearch/index/mapper/date/SimpleDateMappingTests.java b/core/src/test/java/org/elasticsearch/index/mapper/date/SimpleDateMappingTests.java index 829d3458f2954..3d9398e426a3b 100644 --- a/core/src/test/java/org/elasticsearch/index/mapper/date/SimpleDateMappingTests.java +++ b/core/src/test/java/org/elasticsearch/index/mapper/date/SimpleDateMappingTests.java @@ -26,6 +26,8 @@ import org.apache.lucene.search.NumericRangeQuery; import org.apache.lucene.util.Constants; import org.elasticsearch.Version; +import org.elasticsearch.action.admin.indices.mapping.get.GetMappingsResponse; +import org.elasticsearch.action.admin.indices.mapping.put.PutMappingResponse; import org.elasticsearch.action.index.IndexResponse; import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.common.settings.Settings; @@ -45,6 +47,7 @@ import org.elasticsearch.search.internal.SearchContext; import org.elasticsearch.test.ElasticsearchSingleNodeTest; import org.elasticsearch.test.TestSearchContext; +import org.elasticsearch.test.VersionUtils; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; import org.junit.Before; @@ -55,7 +58,6 @@ import static com.carrotsearch.randomizedtesting.RandomizedTest.systemPropertyAsBoolean; import static org.elasticsearch.common.settings.Settings.settingsBuilder; import static org.elasticsearch.index.mapper.string.SimpleStringMappingTests.docValuesType; -import static org.elasticsearch.test.VersionUtils.randomVersionBetween; import static org.hamcrest.Matchers.*; public class SimpleDateMappingTests extends ElasticsearchSingleNodeTest { @@ -482,4 +484,94 @@ public void testThatEpochCanBeIgnoredWithCustomFormat() throws Exception { indexResponse = client().prepareIndex("test", "test").setSource(document).get(); assertThat(indexResponse.isCreated(), is(true)); } + + public void testThatOlderIndicesAllowNonStrictDates() throws Exception { + String mapping = XContentFactory.jsonBuilder().startObject().startObject("type") + .startObject("properties").startObject("date_field").field("type", "date").endObject().endObject() + .endObject().endObject().string(); + + Version randomVersion = VersionUtils.randomVersionBetween(getRandom(), Version.V_0_90_0, Version.V_1_6_1); + IndexService index = createIndex("test", settingsBuilder().put(IndexMetaData.SETTING_VERSION_CREATED, randomVersion).build()); + client().admin().indices().preparePutMapping("test").setType("type").setSource(mapping).get(); + assertDateFormat("epoch_millis||dateOptionalTime"); + DocumentMapper defaultMapper = index.mapperService().documentMapper("type"); + + defaultMapper.parse("type", "1", XContentFactory.jsonBuilder() + .startObject() + .field("date_field", "1-1-1T00:00:44.000Z") + .endObject() + .bytes()); + + // also test normal date + defaultMapper.parse("type", "1", XContentFactory.jsonBuilder() + .startObject() + .field("date_field", "2015-06-06T00:00:44.000Z") + .endObject() + .bytes()); + } + + public void testThatNewIndicesOnlyAllowStrictDates() throws Exception { + String mapping = XContentFactory.jsonBuilder().startObject().startObject("type") + .startObject("properties").startObject("date_field").field("type", "date").endObject().endObject() + .endObject().endObject().string(); + + IndexService index = createIndex("test"); + client().admin().indices().preparePutMapping("test").setType("type").setSource(mapping).get(); + assertDateFormat(DateFieldMapper.Defaults.DATE_TIME_FORMATTER.format()); + DocumentMapper defaultMapper = index.mapperService().documentMapper("type"); + + // also test normal date + defaultMapper.parse("type", "1", XContentFactory.jsonBuilder() + .startObject() + .field("date_field", "2015-06-06T00:00:44.000Z") + .endObject() + .bytes()); + + try { + defaultMapper.parse("type", "1", XContentFactory.jsonBuilder() + .startObject() + .field("date_field", "1-1-1T00:00:44.000Z") + .endObject() + .bytes()); + fail("non strict date indexing should have been failed"); + } catch (MapperParsingException e) { + assertThat(e.getCause(), instanceOf(IllegalArgumentException.class)); + } + } + + public void testThatUpgradingAnOlderIndexToStrictDateWorks() throws Exception { + String mapping = XContentFactory.jsonBuilder().startObject().startObject("type") + .startObject("properties").startObject("date_field").field("type", "date").field("format", "dateOptionalTime").endObject().endObject() + .endObject().endObject().string(); + + Version randomVersion = VersionUtils.randomVersionBetween(getRandom(), Version.V_0_90_0, Version.V_1_6_1); + createIndex("test", settingsBuilder().put(IndexMetaData.SETTING_VERSION_CREATED, randomVersion).build()); + client().admin().indices().preparePutMapping("test").setType("type").setSource(mapping).get(); + assertDateFormat("epoch_millis||dateOptionalTime"); + + // index doc + client().prepareIndex("test", "type", "1").setSource(XContentFactory.jsonBuilder() + .startObject() + .field("date_field", "2015-06-06T00:00:44.000Z") + .endObject()).get(); + + // update mapping + String newMapping = XContentFactory.jsonBuilder().startObject().startObject("type") + .startObject("properties").startObject("date_field") + .field("type", "date") + .field("format", "strictDateOptionalTime||epoch_millis") + .endObject().endObject().endObject().endObject().string(); + PutMappingResponse putMappingResponse = client().admin().indices().preparePutMapping("test").setType("type").setSource(newMapping).get(); + assertThat(putMappingResponse.isAcknowledged(), is(true)); + + assertDateFormat("strictDateOptionalTime||epoch_millis"); + } + + private void assertDateFormat(String expectedFormat) throws IOException { + GetMappingsResponse response = client().admin().indices().prepareGetMappings("test").setTypes("type").get(); + Map mappingMap = response.getMappings().get("test").get("type").getSourceAsMap(); + Map properties = (Map) mappingMap.get("properties"); + Map dateField = (Map) properties.get("date_field"); + assertThat((String) dateField.get("format"), is(expectedFormat)); + } } diff --git a/core/src/test/java/org/elasticsearch/index/mapper/timestamp/TimestampMappingTests.java b/core/src/test/java/org/elasticsearch/index/mapper/timestamp/TimestampMappingTests.java index 784811b26f7a3..e1341cecf026f 100644 --- a/core/src/test/java/org/elasticsearch/index/mapper/timestamp/TimestampMappingTests.java +++ b/core/src/test/java/org/elasticsearch/index/mapper/timestamp/TimestampMappingTests.java @@ -37,7 +37,6 @@ import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.json.JsonXContent; -import org.elasticsearch.index.Index; import org.elasticsearch.index.mapper.*; import org.elasticsearch.index.mapper.internal.TimestampFieldMapper; import org.elasticsearch.test.ElasticsearchSingleNodeTest; @@ -56,14 +55,7 @@ import static org.elasticsearch.test.VersionUtils.randomVersion; import static org.elasticsearch.test.VersionUtils.randomVersionBetween; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.hasKey; -import static org.hamcrest.Matchers.instanceOf; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.isIn; -import static org.hamcrest.Matchers.lessThanOrEqualTo; -import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.*; /** */ @@ -113,8 +105,10 @@ public void testDefaultValues() throws Exception { assertThat(docMapper.timestampFieldMapper().fieldType().stored(), equalTo(version.onOrAfter(Version.V_2_0_0))); assertThat(docMapper.timestampFieldMapper().fieldType().indexOptions(), equalTo(TimestampFieldMapper.Defaults.FIELD_TYPE.indexOptions())); assertThat(docMapper.timestampFieldMapper().path(), equalTo(TimestampFieldMapper.Defaults.PATH)); - assertThat(docMapper.timestampFieldMapper().fieldType().dateTimeFormatter().format(), equalTo(TimestampFieldMapper.DEFAULT_DATE_TIME_FORMAT)); assertThat(docMapper.timestampFieldMapper().fieldType().hasDocValues(), equalTo(version.onOrAfter(Version.V_2_0_0))); + String expectedFormat = version.onOrAfter(Version.V_2_0_0) ? TimestampFieldMapper.DEFAULT_DATE_TIME_FORMAT : + TimestampFieldMapper.Defaults.DATE_TIME_FORMATTER_BEFORE_2_0.format(); + assertThat(docMapper.timestampFieldMapper().fieldType().dateTimeFormatter().format(), equalTo(expectedFormat)); assertAcked(client().admin().indices().prepareDelete("test").execute().get()); } } @@ -755,7 +749,7 @@ public void testBackcompatPath() throws Exception { IndexRequest request = new IndexRequest("test", "type", "1").source(doc); request.process(metaData, mappingMetaData, true, "test"); - assertEquals(request.timestamp(), "1"); + assertThat(request.timestamp(), is("1")); } public void testIncludeInObjectBackcompat() throws Exception { diff --git a/core/src/test/java/org/elasticsearch/search/aggregations/bucket/DateHistogramTests.java b/core/src/test/java/org/elasticsearch/search/aggregations/bucket/DateHistogramTests.java index eedddbd94a02e..496b5eb640ac8 100644 --- a/core/src/test/java/org/elasticsearch/search/aggregations/bucket/DateHistogramTests.java +++ b/core/src/test/java/org/elasticsearch/search/aggregations/bucket/DateHistogramTests.java @@ -1281,9 +1281,9 @@ public void testDSTBoundaryIssue9491() throws InterruptedException, ExecutionExc public void testIssue8209() throws InterruptedException, ExecutionException { assertAcked(client().admin().indices().prepareCreate("test8209").addMapping("type", "d", "type=date").get()); indexRandom(true, - client().prepareIndex("test8209", "type").setSource("d", "2014-01-01T0:00:00Z"), - client().prepareIndex("test8209", "type").setSource("d", "2014-04-01T0:00:00Z"), - client().prepareIndex("test8209", "type").setSource("d", "2014-04-30T0:00:00Z")); + client().prepareIndex("test8209", "type").setSource("d", "2014-01-01T00:00:00Z"), + client().prepareIndex("test8209", "type").setSource("d", "2014-04-01T00:00:00Z"), + client().prepareIndex("test8209", "type").setSource("d", "2014-04-30T00:00:00Z")); ensureSearchable("test8209"); SearchResponse response = client().prepareSearch("test8209") .addAggregation(dateHistogram("histo").field("d").interval(DateHistogramInterval.MONTH).timeZone("CET") diff --git a/core/src/test/java/org/elasticsearch/search/functionscore/DecayFunctionScoreTests.java b/core/src/test/java/org/elasticsearch/search/functionscore/DecayFunctionScoreTests.java index 51770fb925f22..4d4c9c71f0c8c 100644 --- a/core/src/test/java/org/elasticsearch/search/functionscore/DecayFunctionScoreTests.java +++ b/core/src/test/java/org/elasticsearch/search/functionscore/DecayFunctionScoreTests.java @@ -42,6 +42,7 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.ExecutionException; +import java.util.Locale; import static org.elasticsearch.client.Requests.indexRequest; import static org.elasticsearch.client.Requests.searchRequest; @@ -530,17 +531,17 @@ public void testDateWithoutOrigin() throws Exception { ensureYellow(); DateTime docDate = dt.minusDays(1); - String docDateString = docDate.getYear() + "-" + docDate.getMonthOfYear() + "-" + docDate.getDayOfMonth(); + String docDateString = docDate.getYear() + "-" + String.format(Locale.ROOT, "%02d", docDate.getMonthOfYear()) + "-" + String.format(Locale.ROOT, "%02d", docDate.getDayOfMonth()); client().index( indexRequest("test").type("type1").id("1") .source(jsonBuilder().startObject().field("test", "value").field("num1", docDateString).endObject())).actionGet(); docDate = dt.minusDays(2); - docDateString = docDate.getYear() + "-" + docDate.getMonthOfYear() + "-" + docDate.getDayOfMonth(); + docDateString = docDate.getYear() + "-" + String.format(Locale.ROOT, "%02d", docDate.getMonthOfYear()) + "-" + String.format(Locale.ROOT, "%02d", docDate.getDayOfMonth()); client().index( indexRequest("test").type("type1").id("2") .source(jsonBuilder().startObject().field("test", "value").field("num1", docDateString).endObject())).actionGet(); docDate = dt.minusDays(3); - docDateString = docDate.getYear() + "-" + docDate.getMonthOfYear() + "-" + docDate.getDayOfMonth(); + docDateString = docDate.getYear() + "-" + String.format(Locale.ROOT, "%02d", docDate.getMonthOfYear()) + "-" + String.format(Locale.ROOT, "%02d", docDate.getDayOfMonth()); client().index( indexRequest("test").type("type1").id("3") .source(jsonBuilder().startObject().field("test", "value").field("num1", docDateString).endObject())).actionGet(); diff --git a/docs/reference/mapping/date-format.asciidoc b/docs/reference/mapping/date-format.asciidoc index 0972340c555db..7f12616cc24c1 100644 --- a/docs/reference/mapping/date-format.asciidoc +++ b/docs/reference/mapping/date-format.asciidoc @@ -49,6 +49,13 @@ first millisecond of the rounding scope. The semantics work as follows: [[built-in]] === Built In Formats +Most of the below dates have a `strict` companion dates, which means, that +year, month and day parts of the week must have prepending zeros in order +to be valid. This means, that a date like `5/11/1` would not be valid, but +you would need to specify the full date, which would be `2005/11/01` in this +example. So instead of `date_optional_time` you would need to specify +`strict_date_optional_time`. + The following tables lists all the defaults ISO formats supported: [cols="<,<",options="header",] @@ -92,112 +99,125 @@ offset prefixed by 'T' ('T'HHmmssZ). |`basic_week_date`|A basic formatter for a full date as four digit weekyear, two digit week of weekyear, and one digit day of week -(xxxx'W'wwe). +(xxxx'W'wwe). `strict_basic_week_date` is supported. |`basic_week_date_time`|A basic formatter that combines a basic weekyear date and time, separated by a 'T' (xxxx'W'wwe'T'HHmmss.SSSZ). +`strict_basic_week_date_time` is supported. |`basic_week_date_time_no_millis`|A basic formatter that combines a basic weekyear date and time without millis, separated by a 'T' -(xxxx'W'wwe'T'HHmmssZ). +(xxxx'W'wwe'T'HHmmssZ). `strict_week_date_time` is supported. |`date`|A formatter for a full date as four digit year, two digit month -of year, and two digit day of month (yyyy-MM-dd). - +of year, and two digit day of month (yyyy-MM-dd). `strict_date` is supported. +_ |`date_hour`|A formatter that combines a full date and two digit hour of -day. +day. strict_date_hour` is supported. + |`date_hour_minute`|A formatter that combines a full date, two digit hour -of day, and two digit minute of hour. +of day, and two digit minute of hour. strict_date_hour_minute` is supported. |`date_hour_minute_second`|A formatter that combines a full date, two digit hour of day, two digit minute of hour, and two digit second of -minute. +minute. `strict_date_hour_minute_second` is supported. |`date_hour_minute_second_fraction`|A formatter that combines a full date, two digit hour of day, two digit minute of hour, two digit second of minute, and three digit fraction of second -(yyyy-MM-dd'T'HH:mm:ss.SSS). +(yyyy-MM-dd'T'HH:mm:ss.SSS). `strict_date_hour_minute_second_fraction` is supported. |`date_hour_minute_second_millis`|A formatter that combines a full date, two digit hour of day, two digit minute of hour, two digit second of minute, and three digit fraction of second (yyyy-MM-dd'T'HH:mm:ss.SSS). +`strict_date_hour_minute_second_millis` is supported. |`date_optional_time`|a generic ISO datetime parser where the date is -mandatory and the time is optional. +mandatory and the time is optional. `strict_date_optional_time` is supported. |`date_time`|A formatter that combines a full date and time, separated by -a 'T' (yyyy-MM-dd'T'HH:mm:ss.SSSZZ). +a 'T' (yyyy-MM-dd'T'HH:mm:ss.SSSZZ). `strict_date_time` is supported. |`date_time_no_millis`|A formatter that combines a full date and time without millis, separated by a 'T' (yyyy-MM-dd'T'HH:mm:ssZZ). +`strict_date_time_no_millis` is supported. -|`hour`|A formatter for a two digit hour of day. +|`hour`|A formatter for a two digit hour of day. `strict_hour` is supported. |`hour_minute`|A formatter for a two digit hour of day and two digit -minute of hour. +minute of hour. `strict_hour_minute` is supported. |`hour_minute_second`|A formatter for a two digit hour of day, two digit minute of hour, and two digit second of minute. +`strict_hour_minute_second` is supported. |`hour_minute_second_fraction`|A formatter for a two digit hour of day, two digit minute of hour, two digit second of minute, and three digit fraction of second (HH:mm:ss.SSS). +`strict_hour_minute_second_fraction` is supported. |`hour_minute_second_millis`|A formatter for a two digit hour of day, two digit minute of hour, two digit second of minute, and three digit fraction of second (HH:mm:ss.SSS). +`strict_hour_minute_second_millis` is supported. |`ordinal_date`|A formatter for a full ordinal date, using a four digit -year and three digit dayOfYear (yyyy-DDD). +year and three digit dayOfYear (yyyy-DDD). `strict_ordinal_date` is supported. |`ordinal_date_time`|A formatter for a full ordinal date and time, using a four digit year and three digit dayOfYear (yyyy-DDD'T'HH:mm:ss.SSSZZ). +`strict_ordinal_date_time` is supported. |`ordinal_date_time_no_millis`|A formatter for a full ordinal date and time without millis, using a four digit year and three digit dayOfYear (yyyy-DDD'T'HH:mm:ssZZ). +`strict_ordinal_date_time_no_millis` is supported. |`time`|A formatter for a two digit hour of day, two digit minute of hour, two digit second of minute, three digit fraction of second, and -time zone offset (HH:mm:ss.SSSZZ). +time zone offset (HH:mm:ss.SSSZZ). `strict_time` is supported. |`time_no_millis`|A formatter for a two digit hour of day, two digit minute of hour, two digit second of minute, and time zone offset -(HH:mm:ssZZ). +(HH:mm:ssZZ). `strict_time_no_millis` is supported. |`t_time`|A formatter for a two digit hour of day, two digit minute of hour, two digit second of minute, three digit fraction of second, and time zone offset prefixed by 'T' ('T'HH:mm:ss.SSSZZ). +`strict_t_time` is supported. |`t_time_no_millis`|A formatter for a two digit hour of day, two digit minute of hour, two digit second of minute, and time zone offset -prefixed by 'T' ('T'HH:mm:ssZZ). +prefixed by 'T' ('T'HH:mm:ssZZ). `strict_t_time_no_millis` is supported. |`week_date`|A formatter for a full date as four digit weekyear, two digit week of weekyear, and one digit day of week (xxxx-'W'ww-e). +`strict_week_date` is supported. |`week_date_time`|A formatter that combines a full weekyear date and time, separated by a 'T' (xxxx-'W'ww-e'T'HH:mm:ss.SSSZZ). +`strict_week_date_time` is supported. -|`weekDateTimeNoMillis`|A formatter that combines a full weekyear date +|`week_date_time_no_millis`|A formatter that combines a full weekyear date and time without millis, separated by a 'T' (xxxx-'W'ww-e'T'HH:mm:ssZZ). +`strict_week_date_time` is supported. -|`week_year`|A formatter for a four digit weekyear. +|`weekyear`|A formatter for a four digit weekyear. `strict_week_year` is supported. -|`weekyearWeek`|A formatter for a four digit weekyear and two digit week -of weekyear. +|`weekyear_week`|A formatter for a four digit weekyear and two digit week +of weekyear. `strict_weekyear_week` is supported. -|`weekyearWeekDay`|A formatter for a four digit weekyear, two digit week -of weekyear, and one digit day of week. +|`weekyear_week_day`|A formatter for a four digit weekyear, two digit week +of weekyear, and one digit day of week. `strict_weekyear_week_day` is supported. -|`year`|A formatter for a four digit year. +|`year`|A formatter for a four digit year. `strict_year` is supported. |`year_month`|A formatter for a four digit year and two digit month of -year. +year. `strict_year_month` is supported. |`year_month_day`|A formatter for a four digit year, two digit month of -year, and two digit day of month. +year, and two digit day of month. `strict_year_month_day` is supported. |`epoch_second`|A formatter for the number of seconds since the epoch. Note, that this timestamp allows a max length of 10 chars, so dates diff --git a/docs/reference/mapping/fields/timestamp-field.asciidoc b/docs/reference/mapping/fields/timestamp-field.asciidoc index b510cef9c2ec6..27988b45b50dd 100644 --- a/docs/reference/mapping/fields/timestamp-field.asciidoc +++ b/docs/reference/mapping/fields/timestamp-field.asciidoc @@ -40,7 +40,7 @@ format>> used to parse the provided timestamp value. For example: } -------------------------------------------------- -Note, the default format is `epoch_millis||dateOptionalTime`. The timestamp value will +Note, the default format is `epoch_millis||strictDateOptionalTime`. The timestamp value will first be parsed as a number and if it fails the format will be tried. [float] diff --git a/docs/reference/mapping/types/core-types.asciidoc b/docs/reference/mapping/types/core-types.asciidoc index 1ca05b793c255..945a5c4e7080c 100644 --- a/docs/reference/mapping/types/core-types.asciidoc +++ b/docs/reference/mapping/types/core-types.asciidoc @@ -349,7 +349,7 @@ date type: Defaults to the property/field name. |`format` |The <>. Defaults to `epoch_millis||dateOptionalTime`. +format>>. Defaults to `epoch_millis||strictDateOptionalTime`. |`store` |Set to `true` to store actual field in the index, `false` to not store it. Defaults to `false` (note, the JSON document itself is stored, diff --git a/docs/reference/mapping/types/root-object-type.asciidoc b/docs/reference/mapping/types/root-object-type.asciidoc index a97f7f576358c..090f88bc8464d 100644 --- a/docs/reference/mapping/types/root-object-type.asciidoc +++ b/docs/reference/mapping/types/root-object-type.asciidoc @@ -42,7 +42,7 @@ and will use the matching format as its format attribute. The date format itself is explained <>. -The default formats are: `dateOptionalTime` (ISO), +The default formats are: `strictDateOptionalTime` (ISO) and `yyyy/MM/dd HH:mm:ss Z||yyyy/MM/dd Z` and `epoch_millis`. *Note:* `dynamic_date_formats` are used *only* for dynamically added diff --git a/docs/reference/migration/migrate_2_0.asciidoc b/docs/reference/migration/migrate_2_0.asciidoc index c6cb6be1178c4..551872bfa9208 100644 --- a/docs/reference/migration/migrate_2_0.asciidoc +++ b/docs/reference/migration/migrate_2_0.asciidoc @@ -302,6 +302,13 @@ Meta fields can no longer be specified within a document. They should be specifi via the API. For example, instead of adding a field `_parent` within a document, use the `parent` url parameter when indexing that document. +==== Default date format now is `strictDateOptionalDate` + +Instead of `dateOptionalTime` the new default date format now is `strictDateOptionalTime`, +which is more strict in parsing dates. This means, that dates now need to have a four digit year, +a two-digit month, day, hour, minute and second. This means, you may need to preprend a part of the date +with a zero to make it conform or switch back to the old `dateOptionalTime` format. + ==== Date format does not support unix timestamps by default In earlier versions of elasticsearch, every timestamp was always tried to be parsed as @@ -723,4 +730,4 @@ to prevent clashes with the watcher plugin === Percolator stats -Changed the `percolate.getTime` stat (total time spent on percolating) to `percolate.time` state. \ No newline at end of file +Changed the `percolate.getTime` stat (total time spent on percolating) to `percolate.time` state.