【问题标题】:SimpleDateFormat leniency leads to unexpected behaviorSimpleDateFormat 宽大处理导致意外行为
【发布时间】:2018-06-07 17:09:57
【问题描述】:

我发现SimpleDateFormat::parse(String source) 的行为(不幸的是)默认设置为宽松:setLenient(true)

默认情况下,解析是宽松的:如果输入不是该对象的格式方法使用的格式,但仍然可以解析为日期,则解析成功。

如果我将 leniency 设置为 false,文档说通过严格解析,输入必须匹配此对象的格式。我在没有宽松模式的情况下使用了与SimpleDateFormat 配对,并且错误地,我在日期中有错字(字母o 而不是数字0)。 (这是简短的工作代码:)

// PASSED (year 199)
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("dd.mm.yyyy");
System.out.println(simpleDateFormat.parse("03.12.199o"));
simpleDateFormat.setLenient(false);
System.out.println(simpleDateFormat.parse("03.12.199o"));        //WTF?

令我惊讶的是,这已经过去了,没有ParseException 被抛出。我会更进一步:

// PASSED (year 1990)
String string = "just a String to mess with SimpleDateFormat";

SimpleDateFormat simpleDateFormat = new SimpleDateFormat("dd.mm.yyyy");
System.out.println(simpleDateFormat.parse("03.12.1990" + string));
simpleDateFormat.setLenient(false);
System.out.println(simpleDateFormat.parse("03.12.1990" + string));

我们继续吧:

// FAILED on the 2nd line
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("dd.mm.yyyy");
System.out.println(simpleDateFormat.parse("o3.12.1990"));
simpleDateFormat.setLenient(false);
System.out.println(simpleDateFormat.parse("o3.12.1990"));

最后抛出异常:Unparseable date: "o3.12.1990"。我想知道宽大处理的区别在哪里,为什么我的第一个代码 sn-p 的最后一行没有引发异常?文档说:

通过严格解析,输入必须匹配此对象的格式。

我的输入显然与格式不匹配严格 - 我希望这种解析非常严格。为什么会(不)发生这种情况?

【问题讨论】:

  • 您当然可以改用LocalDate date = LocalDate.parse("03.12.199o", DateTimeFormatter.ofPattern("dd.MM.yyyy"));,这将对整个字符串表现出您似乎想要的严格行为。请注意,在 SimpleDateFormat 和 DateTimeFormatter 中,日期格式中的mm 是分钟,而不是月;你的意思可能是MM

标签: java date parsing string-formatting simpledateformat


【解决方案1】:

为什么会(不)发生这种情况?

文档中没有很好地解释。

通过宽松的解析,解析器可以使用启发式来解释 与此对象的格式不完全匹配的输入。以严格 解析,输入必须匹配这个对象的格式。

不过,文档确实有点帮助,因为提到 DateFormat 使用的是宽松的 Calendar 对象。 Calendar 对象不用于解析本身,而是用于将解析的值解释为日期和时间(我引用 DateFormat 文档,因为 SimpleDateFormatDateFormat 的子类)。

  • SimpleDateFormat,无论是否宽松,都将接受 3 位数的年份,例如 199,即使您在格式模式字符串中指定了 yyyy。文档说大约是一年:

    对于解析,如果模式字母的个数大于2,则年份 是按字面意思解释的,与位数无关。所以使用 模式 "MM/dd/yyyy", "01/11/12" 解析为公元 12 年 1 月 11 日

  • DateFormat,无论是否宽松,都接受并忽略解析后的文本,例如第一个示例中的小写字母o。它反对文本之前或内部的意外文本,例如在上一个示例中,您将字母 o 放在前面。 DateFormat.parse 的文档说:

    该方法可能不会使用给定字符串的整个文本。

  • 正如我间接所说,在将解析值解释为日期和时间时,宽大处理会有所不同。因此,宽松的SimpleDateFormat 会将 29.02.2019 解释为 01.03.2019,因为 2019 年 2 月只有 28 天。严格的 SimpleDateFormat 将拒绝这样做并抛出异常。默认的宽松行为会导致非常令人惊讶和完全莫名其妙的结果。举个简单的例子,以错误的顺序给出日、月和年:1990.03.12 将导致公元 17 年 8 月 11 日(2001 年前)。

解决办法

VGR 已经在评论中提到 LocalDate 来自 java.time,现代 Java 日期和时间 API。根据我的经验,java.time 比旧的日期和时间类更好用,所以让我们试一试。首先尝试一个正确的日期字符串:

    DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("dd.mm.yyyy");
    System.out.println(LocalDate.parse("03.12.1990", dateFormatter));

我们得到:

java.time.format.DateTimeParseException: 文本 '03.12.1990' 不能 被解析:无法从 TemporalAccessor 获取 LocalDate: {Year=1990, DayOfMonth=3, MinuteOfHour=12}, ISO 类型 java.time.format.Parsed

这是因为我使用了您的格式模式字符串dd.mm.yyyy,其中小写mm 表示分钟。当我们仔细阅读错误消息时,它确实指出DateTimeFormatter 将 12 解释为一小时的分钟,这不是我们想要的。虽然SimpleDateFormat 默许了这一点(即使是严格的),java.time 更有助于指出我们的错误。该消息仅间接表示的是它缺少月份值。我们需要使用大写的MM 来表示月份。同时,我正在尝试使用拼写错误的日期字符串:

    DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy");
    System.out.println(LocalDate.parse("03.12.199o", dateFormatter));

我们得到:

java.time.format.DateTimeParseException: 文本 '03.12.199o' 不能 在索引 6 处解析

索引 6 是 199 的位置。它反对,因为我们指定了 4 位数字并且只提供 3 位。文档说:

字母的数量决定了最小字段宽度……

它还会反对日期之后的未解析文本。简而言之,在我看来,它为您提供了您所期望的一切。

链接

【讨论】:

  • 感谢您详尽而清晰的回答。我很感激你的努力。 :))
【解决方案2】:

宽大不是关于整个输入是否匹配,而是格式是否匹配。您的输入仍然可以是3.12.1990somecrap,它会起作用。

实际的解析是在parse(String, ParsePosition) 中完成的,您也可以使用它。基本上parse(String) 将传递一个ParsePosition,该ParsePosition 设置为从索引0 开始,当解析完成时,检查该位置的当前索引。

如果它仍然是 0,则输入的开头与格式不匹配,即使在宽松模式下也不匹配。

但是,03.12.199 对解析器来说是一个有效的日期,因此它在索引 8 处停止 - 这不是 0,因此解析成功。如果要检查是否所有内容都已解析,则必须传递自己的 ParsePosition 并检查索引是否与输入的长度匹配。

【讨论】:

  • 你的意思是严格解析一点都不严格吗?
  • @Nikolas,这取决于你认为什么是严格的。基本上,通过严格解析,您的输入必须以与模式完全匹配的日期时间开始。一个宽松的解析器会处理诸如溢出之类的事情(30.02.2018 -> 2.3.201813:59 -> 13:59 即使您的格式声明该小时需要采用上午 1-12 点/下午格式)等。跨度>
  • “严格”这个词的意思是没有比这更严格的了。我希望字符串必须与模式完全匹配,没有额外的字符余量。现在我明白DateFormat 的宽大处理了。感谢您提供有用的答案。 :))
  • @Nikolas 您可以使用 Apache Commons Lang 的 DateUtils#parseDateStrictly() 之类的库,也可以使用自己的库(基本上只检查 ParsePosition
  • 我不喜欢带有一堆静态方法的 util-classes,也不需要为一个特定的用例添加依赖项。但是,很高兴知道存在类似的东西 - 再次感谢您。 :))
【解决方案3】:

如果您使用setLenient(false),它仍然会解析日期,直到满足所需的模式。但是,它会检查输出日期是否为有效日期。在您的情况下,03.12.199 是一个有效日期,因此它不会引发异常。让我们举个例子来了解setLenient(false)setLenient(true)/default 的不同之处。

SimpleDateFormat simpleDateFormat = new SimpleDateFormat("dd.MM.yyyy"); 
System.out.println(simpleDateFormat.parse("31.02.2018"));

上面会给我输出:Sat Mar 03 00:00:00 IST 2018

但以下代码将 ParseException 抛出为 31.02.2018 不是有效/可能的日期:

SimpleDateFormat simpleDateFormat = new SimpleDateFormat("dd.MM.yyyy");
simpleDateFormat.setLenient(false);
System.out.println(simpleDateFormat.parse("31.02.2018"));

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2022-07-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-07-20
    • 1970-01-01
    • 2013-08-03
    • 1970-01-01
    相关资源
    最近更新 更多