【问题标题】:SimpleDateFormat.parse() ignores the number of characters in patternSimpleDateFormat.parse() 忽略模式中的字符数
【发布时间】:2013-04-15 11:53:40
【问题描述】:

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

这是我的代码:

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");

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

【问题讨论】:

    标签: java parsing date simpledateformat


    【解决方案1】:

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

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

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

    这是 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;
        }
    }
    

    【讨论】:

    • 很好的实现。但我不会用setStrict 对抗setLenient,因为这会造成冗余。最好坚持使用setLenient 超级方法并让构造函数调用setLenient(false),并且解析方法应该调用!isLenient() 而不是使用strict。有了冗余,就有可能出现不一致。想象一下:setStrict(true); setLenient(true); 现在是严格还是宽松?
    • setStrict(true) 覆盖并隐藏宽松设置。设置 setLenient(false) 不会改变外部行为,但它可以允许内部 parse() 在奇怪的日期成功,然后我的 parse() 将拒绝。 setStrict(false) 将您返回到标准的 SimpleDateFormat 行为,无论您选择什么 lenient 变体。我的目标是真正改变 SimpleDateFormat 的候选者,它允许用户恢复旧的不良行为。我希望 strict=true 是默认值,因为它符合人们的理解。
    【解决方案2】:

    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 解析为月份中的一天,然后抛出DateTimeParseException,因为20 之后没有连字符(破折号)。然后方法继续尝试下一个格式化程序。

    你的代码出了什么问题

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

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

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

    链接

    Oracle tutorial: Date Time 解释如何使用 java.time。

    【讨论】:

    • DateTimeFormatter 如果您拥有 Java 8 或更高版本,并且不受依赖于 SimpleDateFormat 的遗留代码的束缚,那么它是一个很好的方法。较新的类产生不可变的实例并且是线程安全的。在线程之间共享 SimpleDateFormat 对象会产生垃圾结果而不会出现错误或异常。一个严重缺陷的实现!
    【解决方案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;
    }
    

    【讨论】:

      【解决方案4】:

      它记录在SimpleDateFormat javadoc:

      对于格式化,模式字母的数量是最小位数,较短的数字会用零填充到这个数量。 在解析时,模式字母的数量会被忽略,除非需要分隔两个相邻的字段。

      【讨论】:

      • 谢谢提图。我在 oracle.com 类文档中没有看到这一点。这是一个我们在测试过程中没有发现的隐藏得很好的陷阱。真是一个糟糕的设计。另一个不好的特性是它们默认是“宽松的”,所以你需要设置Lenient(false)。否则他们会将各种垃圾解释为有效的日期/时间信息。
      【解决方案5】:

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

      如果我希望解析函数与模式完全匹配,我必须将 SimpleDateFormat 的“宽松”(SimpleDateFormat.setLenient)设置为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 年不可能是这一天,因此它与第二个模式不匹配。 结合长度检查,我得到了我想要的。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-05-24
        • 1970-01-01
        相关资源
        最近更新 更多