【问题标题】:Determine if a String is a valid date before parsing在解析之前确定字符串是否为有效日期
【发布时间】:2019-11-08 03:00:33
【问题描述】:

我有这种情况,我正在阅读大约 130K 记录,其中包含存储为字符串字段的日期。有些记录包含空格(null),有些包含这样的字符串:'dd-MMM-yy',有些包含这个'dd/MM/yyyy'。

我写了一个这样的方法:

public Date parsedate(String date){

   if(date !== null){
      try{
        1. create a SimpleDateFormat object using 'dd-MMM-yy' as the pattern
        2. parse the date
        3. return the parsed date
      }catch(ParseException e){
          try{
              1. create a SimpleDateFormat object using 'dd/MM/yyy' as the pattern
              2. parse the date
              3. return parsed date
           }catch(ParseException e){
              return null
           }
      }
   }else{
      return null
   }

} 

所以您可能已经发现了问题所在。 我正在使用 try .. catch 作为我的逻辑的一部分。更好的是我可以事先确定字符串实际上包含某种格式的可解析日期,然后尝试解析它。

那么,是否有一些 API 或库可以帮助解决这个问题?我不介意编写几个不同的 Parse 类来处理不同的格式,然后创建一个工厂来选择正确的6,但是,我如何确定哪一个?

谢谢。

【问题讨论】:

  • 如果您决定保留您的解决方案,请仅创建 2 个 SimpleDateFormat 实例并将它们作为常量存储在您的类中,而不是创建 130K 次。
  • 如果您将它们存储为常量,请绝对确保它们不会同时从多个线程中使用!我早些时候遇到了问题,并贡献了一个 FindBugs 检测器,它可以找到静态的 DateFormats 和 Calendars。它们被记录为非线程安全的,但很容易错过。见dschneller.blogspot.com/2007/04/…dschneller.blogspot.com/2007/04/…dschneller.blogspot.com/2007/05/…
  • @van:不要那样做。 SimpleDateFormat 不是线程安全的,所以如果你在多个线程中使用这个类,事情就会在你的脸上爆炸。
  • 我认为我会听取大家的建议并继续使用 try ..catch,因为这确实是一个一次性应用程序,所以我不会在生产环境中运行它。但我会让函数式 Java 成为长期解决方案。我觉得很干净。谢谢。
  • 由于线程安全问题,经过漫长的调试过程,我建议使用JODA。它是完全线程安全的,因为所有格式化程序和日期时间都是不可变的。

标签: java date parsing


【解决方案1】:

有关如何使用 Option 类型消除 try/catch 块的概述,请参阅 Lazy Error Handling in Java

Functional Java 是你的朋友。

本质上,您要做的是将日期解析包装在一个不抛出任何东西的函数中,但在其返回类型中指示解析是否成功。例如:

import fj.F; import fj.F2;
import fj.data.Option;
import java.text.SimpleDateFormat;
import java.text.ParseException;
import static fj.Function.curry;
import static fj.Option.some;
import static fj.Option.none;
...

F<String, F<String, Option<Date>>> parseDate =
  curry(new F2<String, String, Option<Date>>() {
    public Option<Date> f(String pattern, String s) {
      try {
        return some(new SimpleDateFormat(pattern).parse(s));
      }
      catch (ParseException e) {
        return none();
      }
    }
  });

好的,现在您有了一个可重用的日期解析器,它不会抛出任何东西,但会通过返回 Option.None 类型的值来指示失败。以下是您的使用方法:

import fj.data.List;
import static fj.data.Stream.stream;
import static fj.data.Option.isSome_;
....
public Option<Date> parseWithPatterns(String s, Stream<String> patterns) { 
  return stream(s).apply(patterns.map(parseDate)).find(isSome_()); 
}

这将为您提供使用第一个匹配的模式解析的日期,或 Option.None 类型的值,这是类型安全的,而 null 不是。

如果您想知道Stream 是什么...it's a lazy list。这可确保您在第一个成功模式之后忽略模式。不需要做太多的工作。

像这样调用你的函数:

for (Date d: parseWithPatterns(someString, stream("dd/MM/yyyy", "dd-MM-yyyy")) {
  // Do something with the date here.
}

或者……

Option<Date> d = parseWithPatterns(someString,
                                   stream("dd/MM/yyyy", "dd-MM-yyyy"));
if (d.isNone()) {
  // Handle the case where neither pattern matches.
} 
else {
  // Do something with d.some()
}

【讨论】:

  • 这是一个非常有趣的答案。我一定要查一下Functional Java。谢谢。
【解决方案2】:

不要对自己在逻辑中使用 try-catch 太苛刻:这是 Java 强迫您这样做的情况之一,因此您无能为力。

但在这种情况下,您可以改用DateFormat.parse(String, ParsePosition)

【讨论】:

【解决方案3】:

您可以利用正则表达式来确定字符串的格式,以及它是否匹配任何有效格式。像这样的东西(未测试):

(糟糕,在查看您使用的语言之前,我是用 C# 编写的。)

Regex test = new Regex(@"^(?:(?<formatA>\d{2}-[a-zA-Z]{3}-\d{2})|(?<formatB>\d{2}/\d{2}/\d{3}))$", RegexOption.Compiled);
Match match = test.Match(yourString);
if (match.Success)
{
    if (!string.IsNullOrEmpty(match.Groups["formatA"]))
    {
        // Use format A.
    }
    else if (!string.IsNullOrEmpty(match.Groups["formatB"]))
    {
        // Use format B.
    }
    ...
}

【讨论】:

    【解决方案4】:

    如果您的格式是准确的(1999 年 6 月 7 日将是 07-Jun-99 或 07/06/1999:您确定您有前导零),那么您可以只检查尝试解析之前的字符串

    请注意第一个版本中的短月份名称,因为 Jun 在其他语言中可能不是 June。

    但是,如果您的数据来自一个数据库,那么我只需将所有日期转换为通用格式(这是一次性的,但您可以控制数据及其格式)。

    【讨论】:

      【解决方案5】:

      在这种有限的情况下,最好(也是最快的方法)肯定是解析出一天,然后根据下一个字符“/”或“-”尝试解析出其余部分。如果在任何时候有意外数据,则返回NULL。

      【讨论】:

        【解决方案6】:

        假设您提供的模式是唯一可能的选择,我会查看传入的字符串以查看应用哪种格式。

        public Date parseDate(final String date) {
          if (date == null) {
            return null;
          }
        
          SimpleDateFormat format = (date.charAt(2) == '/') ? new SimpleDateFormat("dd/MMM/yyyy")
                                                           : new SimpleDateFormat("dd-MMM-yy");
          try {
            return format.parse(date);
          } catch (ParseException e) {
            // Log a complaint and include date in the complaint
          }
          return null;
        }
        

        正如其他人提到的,如果您可以保证您将永远以多线程方式访问DateFormats,您可以将类级别或静态实例。

        【讨论】:

          【解决方案7】:

          如果您只有两种已知格式,看起来就像三个选项:

          • 首先检查-/ 的存在,然后开始解析该格式。
          • 检查长度,因为“dd-MMM-yy”和“dd/MM/yyyy”不同
          • 使用预编译的正则表达式

          后者似乎没有必要。

          【讨论】:

          • 在 Java 中检查长度应该比在其中搜索字符更快。
          • @van:不需要过早优化。
          【解决方案8】:

          使用正则表达式来解析你的字符串。确保您保留两个正则表达式的预编译(不是在每个方法调用上都创建新的,而是将它们存储为常量),并比较它是否真的比您使用的 try-catch 更快。

          如果两个版本都失败而不是抛出异常,我仍然觉得你的方法返回 null 很奇怪。

          【讨论】:

            【解决方案9】:

            您可以使用 split 来确定使用哪种格式

            String[] parts = date.split("-");
            df = (parts.length==3 ? format1 : format2);
            

            假设它们都是一种或另一种格式,如果需要,您可以改进检查

            【讨论】:

              【解决方案10】:

              每次迭代创建 SimpleDateFormat(或两个)的替代方法是为这些格式延迟填充 ThreadLocal 容器。这将解决线程安全问题和对象创建性能问题。

              【讨论】:

                【解决方案11】:

                我为我的项目编写的一个简单的实用程序类。希望这对某人有所帮助。

                用法示例:

                DateUtils.multiParse("1-12-12");
                DateUtils.multiParse("2-24-2012");
                DateUtils.multiParse("3/5/2012");
                DateUtils.multiParse("2/16/12");
                
                
                
                
                public class DateUtils {
                
                    private static List<SimpleDateFormat> dateFormats = new ArrayList<SimpleDateFormat>();
                
                
                
                    private Utils() {
                        dateFormats.add(new SimpleDateFormat("MM/dd/yy")); // must precede yyyy
                        dateFormats.add(new SimpleDateFormat("MM/dd/yyyy"));
                        dateFormats.add(new SimpleDateFormat("MM-dd-yy"));
                        dateFormats.add(new SimpleDateFormat("MM-dd-yyyy"));            
                
                    }
                        private static Date tryToParse(String input, SimpleDateFormat format) {
                        Date date  = null;
                        try {
                            date = format.parse(input);
                        } catch (ParseException e) {
                
                        }
                
                        return date;
                    }
                
                        public static Date multiParse(String input)  {
                        Date date = null;
                        for (SimpleDateFormat format : dateFormats) {
                            date = tryToParse(input, format);
                            if (date != null) break;
                        }
                        return date;
                    }
                }
                

                【讨论】:

                  【解决方案12】:

                  一方面,我认为您使用 try/catch 没有任何问题,这是我会使用的选项。另一方面,还有其他选择:

                  1. 在决定如何解析之前先从字符串中试一试。
                  2. 使用格式模式字符串的可选部分。

                  在我的演示中,我使用了现代 Java 日期和时间 API java.time,因为问题中使用的 Date 类总是设计得很糟糕,而且现在已经过时了。对于没有时间的日期,我们需要java.time.LocalDate

                  尝试捕捉

                  在 java.time 中使用 try-catch 如下所示:

                      DateTimeFormatter ddmmmuuFormatter = DateTimeFormatter.ofPattern("dd-MMM-uu", Locale.ENGLISH);
                      DateTimeFormatter ddmmuuuuFormatter = DateTimeFormatter.ofPattern("dd/MM/uuuu");
                  
                      String dateString = "07-Jun-09";
                  
                      LocalDate result;
                      try {
                          result = LocalDate.parse(dateString, ddmmmuuFormatter);
                      } catch (DateTimeParseException dtpe) {
                          result = LocalDate.parse(dateString, ddmmuuuuFormatter);
                      }
                      System.out.println("Date: " + result);
                  

                  输出是:

                  日期:2009-06-07

                  假设我们将字符串定义为:

                      String dateString = "07/06/2009";
                  

                  那么输出还是一样的。

                  尝尝

                  如果您希望避免使用 try-catch 结构,可以很容易地通过简单的检查来确定您的字符串符合哪种格式。例如:

                      if (dateString.contains("-")) {
                          result = LocalDate.parse(dateString, ddmmmuuFormatter);
                      } else {
                          result = LocalDate.parse(dateString, ddmmuuuuFormatter);
                      }
                  

                  结果和之前一样。

                  在格式模式字符串中使用可选部分

                  这是我最不喜欢的选项,但它很短,并且呈现出某种程度的完整性。

                      DateTimeFormatter dateFormatter
                              = DateTimeFormatter.ofPattern("[dd-MMM-uu][dd/MM/uuuu]", Locale.ENGLISH);
                      LocalDate result = LocalDate.parse(dateString, dateFormatter);
                  

                  方括号表示格式的可选部分。所以Java首先尝试使用dd-MMM-uu进行解析。无论成功与否,它都会尝试使用dd/MM/uuuu 解析字符串的其余部分。鉴于您的两种格式,其中一种尝试将成功,并且您已经解析了日期。结果还是和上面一样。

                  链接

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

                  【讨论】:

                    猜你喜欢
                    • 2013-10-16
                    • 2014-12-01
                    • 2018-06-15
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    相关资源
                    最近更新 更多