Elastic Search 和 Y10k(超过 4 位数的年份)

jar*_*bjo 10 java datetime-format elasticsearch

我在 Elastic Search 查询中发现了这个问题,但由于ES 日期格式文档链接到java.time.format.DateTimeFormatter类的API 文档,因此该问题并不是真正特定于 ES 的。

简短摘要:我们遇到了超过 9999 年的日期问题,更准确地说,是超过 4 位数字的年份。

存储在 ES 中的文档有一个日期字段,它在索引描述符中定义为格式“date”,使用 DateTimeFormatter 的模式语言对应于“yyyy-MM-dd”。我们正在获取用户输入,使用 org.apache.commons.validator.DateValidator.isValid 也使用模式“yyyy-MM-dd”验证输入,如果有效,我们会使用用户输入创建一个 ES 查询。如果用户输入诸如 20202-12-03 之类的内容,这将失败并执行 execption。搜索词可能不是故意的,但预期的行为是找不到任何东西,而不是软件咳出异常。

问题是 org.apache.commons.validator.DateValidator 在内部使用旧的 SimpleDateFormat 类来验证输入是否符合模式,并且 SimpleDateFormat 解释的“yyyy”的含义类似于:使用至少 4 位数字,但如果需要,允许更多的数字。因此,使用模式“yyyy-MM-dd”创建 SimpleDateFormat 将解析像“20202-07-14”这样的输入,并类似地格式化年超过 9999 的 Date 对象。

新的 DateTimeFormatter 类更加严格,意味着“yyyy”正好是四位数字。它将无法解析像“20202-07-14”这样的输入字符串,也无法格式化年份超过 9999 的 Temporal 对象。值得注意的是,DateTimeFormatter 本身能够处理可变长度字段。例如,常量 DateTimeFormatter.ISO_LOCAL_DATE 不等同于“yyyy-MM-dd”,但符合 ISO8601,允许超过四位数的年份,但将使用至少四位数。此常量是使用 DateTimeFormatterBuilder 以编程方式创建的,而不是使用模式字符串。

ES 不能配置为使用 DateTimeFormatter 中定义的常量,如 ISO_LOCAL_DATE,但只能使用模式字符串。ES 还知道一个预定义模式列表,文档中偶尔也会引用 ISO 标准,但他们似乎误会并忽略了有效的 ISO 日期字符串可以包含五位数年份。

我可以使用多个允许的日期模式列表配置 ES,例如“yyyy-MM-dd||yyyyy-MM-dd”。这将允许在年份中使用四位数和五位数,但对于六位数年份则失败。我可以通过添加另一个允许的模式来支持六位数的年份:“yyyy-MM-dd||yyyyy-MM-dd||yyyyyy-MM-dd”,但是它在七位数年份失败等等。

我是在监督某些事情,还是真的不可能将 ES(或使用模式字符串的 DateTimeFormatter 实例)配置为具有 ISO 标准所使用的至少四位数(但可能更多)的年份字段?

Ole*_*.V. 7

编辑

ISO 8601

既然你的要求是符合ISO 8601,那我们先来看看ISO 8601是怎么说的(引自底部链接):

为了表示 0000 之前或 9999 之后的年份,该标准还允许扩展年份表示,但只能通过发送方和接收方之间的事先协议。扩展的年份表示 [±YYYYY] 必须具有商定的超出四位数最小值的额外年份位数,并且必须以 + 或 ? 用符号代替更常见的 AD/BC(或 CE/BCE)符号;…

所以20202-12-03不是 ISO 8601 中的有效日期。如果您明确通知您的用户您接受(例如)最多 6 位数字的年份,则+20202-12-03-20202-12-03是有效的,并且仅带有+-符号。

接受4位数以上

格式模式uuuu-MM-dd根据 ISO 8601 格式化和解析日期,以及超过四位数字的年份。例如:

    DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("uuuu-MM-dd");
    LocalDate date = LocalDate.parse("+20202-12-03", dateFormatter);
    System.out.println("Parsed: " + date);
    System.out.println("Formatted back: " + date.format(dateFormatter));
Run Code Online (Sandbox Code Playgroud)

输出:

Parsed: +20202-12-03
Formatted back: +20202-12-03
Run Code Online (Sandbox Code Playgroud)

对于带前缀的减号而不是加号,它的工作原理非常相似。

接受4位以上无符号

    yyyy-MM-dd||yyyyy-MM-dd||yyyyyy-MM-dd||yyyyyyy-MM-dd||yyyyyyyy-MM-dd||yyyyyyyyy-MM-dd
Run Code Online (Sandbox Code Playgroud)

正如我所说,这与 ISO 8601 不一致。我也同意你的看法,它不好。显然它会失败 10 位或更多位,但无论如何都会失败:java.time 处理区间 -999 999 999 到 +999 999 999 中的yyyyyyyyyy-MM-dd年份。所以尝试(10 位年份)会让你进入严重的麻烦,除非在用户输入带有前导零的年份的极端情况下。

对不起,这已经够好了。DateTimeFormatter格式模式不支持您所要求的所有内容。没有(单一)模式可以为您提供 0000 到 9999 范围内的四位数年份以及之后的年份。

DateTimeFormatter关于格式化和解析年份的文档说:

Year:字母数决定了使用填充的最小字段宽度。如果字母数为两个,则使用减少的两位数形式。对于打印,这会输出最右边的两位数字。对于解析,这将使用基值 2000 进行解析,从而生成 2000 到 2099 范围内的年份。如果字母数少于四个(但不是两个),则符号仅按 输出负年份 SignStyle.NORMAL。否则,如果超过焊盘宽度,则按照 输出符号SignStyle.EXCEEDS_PAD

所以,无论哪个算你去为模式字母,您将无法解析年以上数字无符号,年用更少的数字将与前导零这个数字的许多被格式化。

原答案

您可能可以摆脱模式u-MM-dd。示范:

    String formatPattern = "u-MM-dd";
    
    DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern(formatPattern);
    
    LocalDate normalDate = LocalDate.parse("2020-07-14", dateFormatter);
    String formattedAgain = normalDate.format(dateFormatter);
    System.out.format("LocalDate: %s. String: %s.%n", normalDate, formattedAgain);
    
    LocalDate largeDate = LocalDate.parse("20202-07-14", dateFormatter);
    String largeFormattedAgain = largeDate.format(dateFormatter);
    System.out.format("LocalDate: %s. String: %s.%n", largeDate, largeFormattedAgain);
Run Code Online (Sandbox Code Playgroud)

输出:

LocalDate: 2020-07-14. String: 2020-07-14.
LocalDate: +20202-07-14. String: 20202-07-14.
Run Code Online (Sandbox Code Playgroud)

反intuituvely但实际上很一种格式字母并不意味着1位,而是尽可能多的数字,因为它需要。所以上面的另一面是 1000 年之前的年份将被格式化为少于 4 位数字。正如您所说,这与 ISO 8601 不一致。

有关模式字母yu年份之间的区别,请参阅底部的链接。

您可能还会考虑接受一个M和/或一个,但同样,这将导致小于 10 的数字仅格式化为 1 位,例如,这可能不是您想要的,并且再次不同意 ISO。d2020-007-0142020-7-14

链接