SimpleDateFormat宽恕会导致意外行为

时间:2018-06-07 17:09:57

标签: java date parsing string-formatting simpledateformat

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

  

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

如果我将宽容设置为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"。我想知道宽容的区别在哪里以及为什么我的第一个代码片段的最后一行没有抛出异常?文档说:

  

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

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

3 个答案:

答案 0 :(得分:3)

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

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

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

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

答案 1 :(得分:1)

如果您使用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"));

答案 2 :(得分:1)

  

为什么会这样(不)发生?

在文档中没有很好地解释。

  

通过宽松解析,解析器可以使用启发式来解释   输入不能精确匹配此对象的格式。严格要求   解析时,输入必须与此对象的格式匹配。

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

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

      

    对于解析,如果模式字母的数量大于2,则为年份   无论数字位数如何,都按字面解释。所以使用   模式" MM / dd / yyyy"," 01/11/12"解析到1月11日,12 A.D。

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

      

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

  • 正如我间接所说,在将解析后的值解释为日期和时间时,宽恕会产生影响。因此,宽松的SimpleDateFormat会将29.02.2019解释为01.03.2019,因为2019年2月只有28天。严格的SimpleDateFormat将拒绝这样做,并会抛出异常。默认的宽松行为可能导致非常令人惊讶和彻头彻尾的莫名其妙的结果。举个简单的例子,给出错误顺序的日,月和年:1990.03.12将导致公元8月11日17年(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:Text' 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:Text' 03.12.199o'不能   在索引6处解析

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

  

字母数决定最小字段宽度......

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

链接