4

我正在尝试解析可以具有树不同格式的日期字符串。即使字符串不应该匹配第二个模式,它也会以某种方式匹配,因此返回错误的日期。

那是我的代码:

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class Start {

    public static void main(String[] args) {
        SimpleDateFormat sdf = new SimpleDateFormat("dd.MM.yyyy");
        try{
            System.out.println(sdf.format(parseDate("2013-01-31")));
        } catch(ParseException ex){
            System.out.println("Unable to parse");
        }
    }

    public static Date parseDate(String dateString) throws ParseException{
        SimpleDateFormat sdf = new SimpleDateFormat("dd.MM.yyyy");
        SimpleDateFormat sdf2 = new SimpleDateFormat("dd-MM-yyyy");
        SimpleDateFormat sdf3 = new SimpleDateFormat("yyyy-MM-dd");

        Date parsedDate;
        try {
            parsedDate = sdf.parse(dateString);
        } catch (ParseException ex) {
            try{
                parsedDate = sdf2.parse(dateString);
            } catch (ParseException ex2){
                parsedDate = sdf3.parse(dateString);    
            }
        }
        return parsedDate;
    }
}

有了输入2013-01-31,我得到了输出05.07.0036

如果我尝试解析31-01-201331.01.2013得到31.01.2013预期的结果。

我认识到如果我设置这样的模式,程序会给我完全相同的输出:

SimpleDateFormat sdf = new SimpleDateFormat("d.M.y");
SimpleDateFormat sdf2 = new SimpleDateFormat("d-M-y");
SimpleDateFormat sdf3 = new SimpleDateFormat("y-M-d");

为什么它会忽略我的模式中的字符数?

4

5 回答 5

12

SimpleDateFormat 存在严重问题。默认的 lenient 设置会产生垃圾答案,我想不出 lenient 有什么好处的情况。宽松设置不是对人类输入的日期变化产生合理解释的可靠方法。这不应该是默认设置。

如果可以,请改用 DateTimeFormatter,请参阅 Ole VV 的答案。这种较新的方法更优越,并产生线程安全和不可变的实例。如果您在线程之间共享 SimpleDateFormat 实例,它们可以产生垃圾结果而不会出现错误或异常。可悲的是,我建议的实现继承了这种不良行为。

禁用 lenient 只是解决方案的一部分。您仍然会得到在测试中难以捕捉到的垃圾结果。有关示例,请参见下面代码中的注释。

这是 SimpleDateFormat 的扩展,它强制进行严格的模式匹配。这应该是该类的默认行为。

import java.text.DateFormatSymbols;
import java.text.ParseException;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;

/**
 * Extension of SimpleDateFormat that implements strict matching.
 * parse(text) will only return a Date if text exactly matches the
 * pattern. 
 * 
 * This is needed because SimpleDateFormat does not enforce strict 
 * matching. First there is the lenient setting, which is true
 * by default. This allows text that does not match the pattern and
 * garbage to be interpreted as valid date/time information. For example,
 * parsing "2010-09-01" using the format "yyyyMMdd" yields the date 
 * 2009/12/09! Is this bizarre interpretation the ninth day of the  
 * zeroth month of 2010? If you are dealing with inputs that are not 
 * strictly formatted, you WILL get bad results. You can override lenient  
 * with setLenient(false), but this strangeness should not be the default. 
 *
 * Second, setLenient(false) still does not strictly interpret the pattern. 
 * For example "2010/01/5" will match "yyyy/MM/dd". And data disagreement like 
 * "1999/2011" for the pattern "yyyy/yyyy" is tolerated (yielding 2011). 
 *
 * Third, setLenient(false) still allows garbage after the pattern match. 
 * For example: "20100901" and "20100901andGarbage" will both match "yyyyMMdd". 
 * 
 * This class restricts this undesirable behavior, and makes parse() and 
 * format() functional inverses, which is what you would expect. Thus
 * text.equals(format(parse(text))) when parse returns a non-null result.
 * 
 * @author zobell
 *
 */
public class StrictSimpleDateFormat extends SimpleDateFormat {

    protected boolean strict = true;

    public StrictSimpleDateFormat() {
        super();
        setStrict(true);
    }

    public StrictSimpleDateFormat(String pattern) {
        super(pattern);
        setStrict(true);
    }

    public StrictSimpleDateFormat(String pattern, DateFormatSymbols formatSymbols) {
        super(pattern, formatSymbols);
        setStrict(true);
    }

    public StrictSimpleDateFormat(String pattern, Locale locale) {
        super(pattern, locale);
        setStrict(true);
    }

    /**
     * Set the strict setting. If strict == true (the default)
     * then parsing requires an exact match to the pattern. Setting
     * strict = false will tolerate text after the pattern match. 
     * @param strict
     */
    public void setStrict(boolean strict) {
        this.strict = strict;
        // strict with lenient does not make sense. Really lenient does
        // not make sense in any case.
        if (strict)
            setLenient(false); 
    }

    public boolean getStrict() {
        return strict;
    }

    /**
     * Parse text to a Date. Exact match of the pattern is required.
     * Parse and format are now inverse functions, so this is
     * required to be true for valid text date information:
     * text.equals(format(parse(text))
     * @param text
     * @param pos
     * @return
     */
    @Override
    public Date parse(String text, ParsePosition pos) {
        Date d = super.parse(text, pos);
        if (strict && d != null) {
           String format = this.format(d);
           if (pos.getIndex() + format.length() != text.length() ||
                 !text.endsWith(format)) {
              d = null; // Not exact match
           }
        }
        return d;
    }
}
于 2013-10-21T19:26:10.703 回答
4

java.time

java.time 是现代 Java 日期和时间 API,其行为方式符合您的预期。因此,只需简单翻译您的代码即可:

private static final DateTimeFormatter formatter1 = DateTimeFormatter.ofPattern("dd.MM.yyyy");
private static final DateTimeFormatter formatter2 = DateTimeFormatter.ofPattern("dd-MM-yyyy");
private static final DateTimeFormatter formatter3 = DateTimeFormatter.ofPattern("yyyy-MM-dd");

public static LocalDate parseDate(String dateString) {
    LocalDate parsedDate;
    try {
        parsedDate = LocalDate.parse(dateString, formatter1);
    } catch (DateTimeParseException dtpe1) {
        try {
            parsedDate = LocalDate.parse(dateString, formatter2);
        } catch (DateTimeParseException dtpe2) {
            parsedDate = LocalDate.parse(dateString, formatter3);
        }
    }
    return parsedDate;
}

(我将格式化程序放在您的方法之外,因此不会为每次调用重新创建它们。如果您愿意,可以将它们放在里面。)

让我们试一试:

    LocalDate date = parseDate("2013-01-31");
    System.out.println(date);

输出是:

2013-01-31

对于数字DateTimeFormatter.ofPattern,将模式字母的数量作为最小字段宽度。它还假设月份中的日期不超过两位数。因此,在尝试格式时dd-MM-yyyy,它成功解析20为一个月中的一天,然后抛出 a DateTimeParseException,因为 . 之后没有连字符(破折号)20。然后该方法继续尝试下一个格式化程序。

你的代码出了什么问题

SimpleDateFormat您尝试使用的类是出了名的麻烦,幸运的是早已过时。您遇到的只是它的众多问题之一。重复文档中关于它如何处理来自 Teetoo 答案的数字的重要句子:

对于解析,模式字母的数量将被忽略,除非需要分隔两个相邻的字段。

如此new SimpleDateFormat("dd-MM-yyyy")愉快地解析2013为月份的日期,01月份和31年份。接下来我们应该预料到它会抛出异常,因为 1 月 31 日没有 2013 天。但是SimpleDateFormat默认设置不会这样做。它只是在接下来的几个月和几年中不断计算天数,并在五年半后的第 36 年 7 月 5 日结束,这是您观察到的结果。

关联

Oracle 教程:日期时间解释如何使用 java.time。

于 2019-05-26T17:39:51.617 回答
3

一种解决方法可能是使用正则表达式测试 yyyy-MM-dd 格式:

public static Date parseDate(String dateString) throws ParseException {
    SimpleDateFormat sdf = new SimpleDateFormat("dd.MM.yyyy");
    SimpleDateFormat sdf2 = new SimpleDateFormat("dd-MM-yyyy");
    SimpleDateFormat sdf3 = new SimpleDateFormat("yyyy-MM-dd");

    Date parsedDate;
    try {
        if (dateString.matches("\\d{4}-\\d{2}-\\d{2}")) {
            parsedDate = sdf3.parse(dateString);
        } else {
            throw new ParseException("", 0);
        }
    } catch (ParseException ex) {
        try {
            parsedDate = sdf2.parse(dateString);
        } catch (ParseException ex2) {
            parsedDate = sdf.parse(dateString);
        }
    }
    return parsedDate;
}
于 2013-04-15T12:31:27.373 回答
2

它记录在SimpleDateFormatjavadoc 中:

对于格式化,模式字母的数量是最小位数,较短的数字在此数量上补零。对于解析,模式字母的数量将被忽略,除非需要分隔两个相邻的字段。

于 2013-04-15T12:05:26.793 回答
0

谢谢@Teetoo。这帮助我找到了解决问题的方法:

如果我希望解析函数与模式完全匹配,我必须将SimpleDateFormat.setLenientSimpleDateFormat 的“lenient”()设置为false

SimpleDateFormat sdf = new SimpleDateFormat("d.M.y");
sdf.setLenient(false);
SimpleDateFormat sdf2 = new SimpleDateFormat("d-M-y");
sdf2.setLenient(false);
SimpleDateFormat sdf3 = new SimpleDateFormat("y-M-d");
sdf3.setLenient(false);

如果我只为每个段使用一个模式字母,这仍然会解析日期,但它会认识到 2013 年不可能是这一天,因此它与第二个模式不匹配。结合长度检查,我得到了我想要的。

于 2013-04-15T12:23:41.847 回答