【问题标题】:Java 8: How to create DateTimeFormatter with milli, micro or nano seconds?Java 8:如何使用毫秒、微秒或纳秒创建 DateTimeFormatter?
【发布时间】:2017-11-06 11:24:02
【问题描述】:

我需要创建格式化程序来解析带有可选毫秒、微米或纳秒分数的时间戳。

例如,出于我的需要,我看到了以下机会:

DateTimeFormatter formatter = new DateTimeFormatterBuilder()
                                   .append(DateTimeFormatter.BASIC_ISO_DATE)
                                   .appendLiteral('-')
                                   .append(DateTimeFormatter.ISO_LOCAL_TIME)
                                   .appendOffset("+HH:mm", "Z")
                                   .toFormatter();

或者也可以使用appendFraction(field, minWidth, maxWidth, decimalPoint)

但是,在这些情况下,可以解析具有任意小数位数(最多 9 位或 maxWidth)的时间戳。如何实现我们只能解析(可选)逗号后的 3、6 或 9 个数字?

应该可以解析以下时间部分:

  • HH:mm:ss.SSS
  • HH:mm:ss.SSSSSS
  • HH:mm:ss.SSSSSSSSS

但无法解析:HH:mm:ss.SSSS

【问题讨论】:

  • 不认为你可以,但你为什么在乎呢?接受例如有什么问题? 4个小数位?根据ISO 8601,它完全有效。

标签: java parsing time java-time


【解决方案1】:

您可以使用可选部分模式(由 [] 分隔),并创建 3 个可选部分:1 个用于 9 位数字,另一个用于 6 位数字,另一个用于 3 位数字。

根据DateTimeFormatterBuilder docs,可以使用S模式(相当于NANO_OF_SECOND field):

Pattern  Count  Equivalent builder methods
-------  -----  --------------------------
S..S     1..n   appendFraction(ChronoField.NANO_OF_SECOND, n, n, false)

在旧 API (SimpleDateFormat) 中,S is the pattern used for milliseconds,但在新 API 中改为纳秒。

所以格式化程序会这样创建:

DateTimeFormatter formatter = new DateTimeFormatterBuilder()
    // here is the same as your code
    .append(DateTimeFormatter.BASIC_ISO_DATE).appendLiteral('-')
    // time (hour/minute/seconds)
    .appendPattern("HH:mm:ss")
    // optional nanos, with 9, 6 or 3 digits
    .appendPattern("[.SSSSSSSSS][.SSSSSS][.SSS]")
    // offset
    .appendOffset("+HH:mm", "Z")
    // create formatter
    .toFormatter();

一些测试:

// 3 digits
System.out.println(OffsetDateTime.parse("20161201-10:30:45.123Z", formatter)); // 2016-12-01T10:30:45.123Z

// 6 digits
System.out.println(OffsetDateTime.parse("20161201-10:30:45.123456Z", formatter)); // 2016-12-01T10:30:45.123456Z

// 9 digits
System.out.println(OffsetDateTime.parse("20161201-10:30:45.123456789Z", formatter)); // 2016-12-01T10:30:45.123456789Z

// 4 digits (throws DateTimeParseException: Text '20161201-10:30:45.1234Z' could not be parsed at index 21)
System.out.println(OffsetDateTime.parse("20161201-10:30:45.1234Z", formatter));

输出是:

2016-12-01T10:30:45.123Z
2016-12-01T10:30:45.123456Z
2016-12-01T10:30:45.123456789Z
线程“主”java.time.format.DateTimeParseException 中的异常:无法在索引 21 处解析文本“20161201-10:30:45.1234Z”


注意事项:

  • DateTimeFormatter 的这个实例不适合格式化,因为它会打印所有可选部分(因此纳秒会打印 3 次):

    // don't use it to format, it prints all optional sections
    // (so nanos are printed 3 times: with 9, 6 and 3 digits)
    OffsetDateTime odt = OffsetDateTime.parse("20161201-10:30:45.123Z", formatter);
    System.out.println(formatter.format(odt));
    // output is 20161201Z-10:30:45.123000000.123000.123Z
    

因此,如果您想以其他格式显示日期,请考虑创建另一个 DateTimeFormatter

  • 在您的代码中,您使用了DateTimeFormatter.ISO_LOCAL_TIME。根据javadoc,在此格式化程序中,秒数是可选的。如果您想拥有相同的行为,只需将时间模式更改为:

    // time (hour/minute) with optional seconds
    .appendPattern("HH:mm[:ss]")
    
  • [] 模式是创建可选部分的好方法,但您也可以使用 optionalStart()appendFraction() 创建它们:

    DateTimeFormatter formatter = new DateTimeFormatterBuilder()
        // here is the same as your code
        .append(DateTimeFormatter.BASIC_ISO_DATE).appendLiteral('-')
        // time (hour/minute/seconds)
        .appendPattern("HH:mm:ss")
        // optional nanos with 9 digits (including decimal point)
        .optionalStart()
        .appendFraction(ChronoField.NANO_OF_SECOND, 9, 9, true)
        .optionalEnd()
        // optional nanos with 6 digits (including decimal point)
        .optionalStart()
        .appendFraction(ChronoField.NANO_OF_SECOND, 6, 6, true)
        .optionalEnd()
        // optional nanos with 3 digits (including decimal point)
        .optionalStart()
        .appendFraction(ChronoField.NANO_OF_SECOND, 3, 3, true)
        .optionalEnd()
        // offset
        .appendOffset("+HH:mm", "Z")
        // create formatter
        .toFormatter();
    

此格式化程序的工作方式与使用[] 模式创建的第一个格式化程序完全相同。


副作用

正如@SeanVanGorder 注意到的in his comment,此格式化程序具有接受纳秒字段的多种模式的副作用,但它仅在值相同时才有效:

// side effect
// multiple nanos values (accepts multiple values if they're all the same)
System.out.println(OffsetDateTime.parse("20161201-10:30:45.123000.123Z", formatter)); // 2016-12-01T10:30:45.123Z
System.out.println(OffsetDateTime.parse("20161201-10:30:45.123000000.123Z", formatter)); // 2016-12-01T10:30:45.123Z
System.out.println(OffsetDateTime.parse("20161201-10:30:45.123000000.123000.123Z", formatter)); // 2016-12-01T10:30:45.123Z

以上所有行都输出2016-12-01T10:30:45.123Z,但请注意它们接受所有可选值(如.123000000.123)。由于值相同,解析完成没有错误。

但是,如果值不同,则会引发异常:

// multiple nanos values (throws exception if values are different)
System.out.println(OffsetDateTime.parse("20161201-10:30:45.123000.124Z", formatter)); // exception

如果不需要这种行为,唯一的选择是创建许多不同的格式化程序(每种情况一个)并执行for 循环,直到获得有效的解析值(非常类似于this answer)。

首先,我创建了一个方法,接收DateTimeFormatterTemporalQuery 的列表,以将解析后的字符串转换为您想要的任何对象:

// method to parse, it receives a list of DateTimeFormatter and a TemporalQuery to convert the parsed string
public <T> T parse(String input, TemporalQuery<T> query, DateTimeFormatter... formatters) {
    for (DateTimeFormatter fmt : formatters) {
        try {
            // try to parse
            return fmt.parse(input, query);
        } catch (Exception e) {}
    }

    // none worked, throw exception
    throw new DateTimeParseException("Text '" + input + "' could not be parsed", input, 0);
}

现在您只需要创建格式化程序并在parse 方法中使用它们:

// alternative: have 3 different formatters
DateTimeFormatter f1 = new DateTimeFormatterBuilder()
    // here is the same as your code
    .append(DateTimeFormatter.BASIC_ISO_DATE).appendLiteral('-')
    // time (hour/minute/seconds/3 digit nanos)
    .appendPattern("HH:mm:ss.SSS")
    // offset
    .appendOffset("+HH:mm", "Z")
    // create formatter
    .toFormatter();
DateTimeFormatter f2 = new DateTimeFormatterBuilder()
    // here is the same as your code
    .append(DateTimeFormatter.BASIC_ISO_DATE).appendLiteral('-')
    // time (hour/minute/seconds/6 digit nanos)
    .appendPattern("HH:mm:ss.SSSSSS")
    // offset
    .appendOffset("+HH:mm", "Z")
    // create formatter
    .toFormatter();
DateTimeFormatter f3 = new DateTimeFormatterBuilder()
    // here is the same as your code
    .append(DateTimeFormatter.BASIC_ISO_DATE).appendLiteral('-')
    // time (hour/minute/seconds/9 digit nanos)
    .appendPattern("HH:mm:ss.SSSSSSSSS")
    // offset
    .appendOffset("+HH:mm", "Z")
    // create formatter
    .toFormatter();

// all formatters
DateTimeFormatter[] formatters = new DateTimeFormatter[] { f1, f2, f3 };

// 3 digits
System.out.println(parse("20161201-10:30:45.123Z", OffsetDateTime::from, formatters)); // 2016-12-01T10:30:45.123Z
// 6 digits
System.out.println(parse("20161201-10:30:45.123456Z", OffsetDateTime::from, formatters)); // 2016-12-01T10:30:45.123456Z
// 9 digits
System.out.println(parse("20161201-10:30:45.123456789Z", OffsetDateTime::from, formatters)); // 2016-12-01T10:30:45.123456789Z

// 4 digits (throws exception)
System.out.println(parse("20161201-10:30:45.1234Z", OffsetDateTime::from, formatters));
// java.time.format.DateTimeParseException: Text '20161201-10:30:45.1234Z' could not be parsed

// multiple values (throws exception)
System.out.println(parse("20161201-10:30:45.123000.123Z", OffsetDateTime::from, formatters));
// java.time.format.DateTimeParseException: Text '20161201-10:30:45.123000.123Z' could not be parsed

请注意,我将方法引用 OffsetDateTime::from 用作 TemporalQuery,但您可以将其更改为您需要的任何查询。

【讨论】:

  • 我在想这将允许多个小数(如.123456.123),但经过测试,看起来 DateTimeFormatter 不接受同一字段的多个不同值。不过,它确实接受.123000.123,但我怀疑这是个问题。
  • @SeanVanGorder 实际上,这是为同一字段提供大量可选部分的副作用。但是正如您所说,它仅在值相同时才接受(例如在您的示例中,或.123000000.123000.123.123000000.123)。如果输入类似于.123000.456,则会引发异常。我认为这可以通过验证输入来避免 - 或者你不需要,如果你确保输入字符串正确生成(OP没有详细说明输入是如何生成的,但我假设它们来自日期/时间对象)。无论如何,我同意这是一个小问题。谢谢!
  • 对于数字位数有多种选择,请确保将它们放在模式字符串中按长度递减的顺序。否则,解析似乎会失败(但我不确定为什么)。
【解决方案2】:

DateTimeFormatter 只支持宽度范围,所以这对于单个实例是不可能的。您可以使用 .appendFraction(NANO_OF_SECOND, #, #, true) 制作 3 个单独的格式化程序,其中 # 是 3、6 或 9。然后按顺序尝试它们,忽略任何 DateTimeParseException 直到最后一个:

private static TemporalAccessor parse(String text) {
    try {
        return formatter3.parse(text);
    } catch (DateTimeParseException e) {
        // ignore
    }
    try {
        return formatter6.parse(text);
    } catch (DateTimeParseException e) {
        // ignore
    }
    return formatter9.parse(text); // let this one throw
}

另一种选择是首先使用正则表达式检查输入,例如text.matches("[^.]+(.+\\.(\\d{3}|\\d{6}|\\d{9})\\b.*)?")

【讨论】:

    猜你喜欢
    • 2019-09-27
    • 2018-07-26
    • 2011-05-17
    • 2018-11-08
    • 2019-08-01
    • 1970-01-01
    • 2015-06-25
    • 2015-11-14
    • 2015-07-20
    相关资源
    最近更新 更多