2017-06-05 4400 views
1

我需要创建格式化程序来解析时间戳,其中可选的毫秒,微秒或纳秒分数。Java 8:如何使用毫秒,微秒或纳秒创建DateTimeFormatter?

例如,对于我的要求,我看到以下机会:

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

+3

不要以为你可以,但你为什么在意?接受例如什么是错误的4个小数位?根据[ISO 8601](https://en.wikipedia.org/wiki/ISO_8601)完全有效。 – Andreas

回答

2

DateTimeFormatter只支持宽度范围,所以这对单个实例是不可能的。你可以使用.appendFraction(NANO_OF_SECOND, #, #, true)其中#是3,6或9那就试试他们按顺序做3个独立的格式化,忽略任何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.*)?")

3

您可以使用可选部分模式(由[]定界),并创建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.123 Z
2016-12-01T10:30:45.123456Z
2016-12-01T10:30:45.123456789Z
线程“main”中的异常java.time.format.DateTimeParseException:Text'20161201-10:30:45。

  • DateTimeFormatter此实例格式化,因为它打印所有可选节(:1234Z”无法在指数21


注意事项解析所以纳秒将被打印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)。

首先,我创建了一个方法接收的DateTimeFormatter的列表和一个TemporalQuery来解析字符串转换成任何你想要的对象:

// 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::fromTemporalQuery,但你可以把它改成你需要的任何查询。

+0

我在想这会允许多个小数(如'.123456.123'),但经过测试,它看起来像DateTimeFormatter不接受同一字段的多个不同的值。但它确实接受'.123000.123',但我怀疑这是一个问题。 –

+0

@SeanVanGorder事实上,这是对同一领域有很多可选部分的副作用。但正如你所说的,它只有在值相同时(如在你的例子中,或者.123000000.123000.123或者.123000000.123)才接受。如果输入与'.123000.456'类似,则会引发异常。我认为这可以通过验证输入来避免 - 或者您不需要,如果确保输入字符串正确生成(OP没有详细说明输入是如何生成的,但我假设它们来自日期/时间对象)。无论如何,我同意这是一个小问题。谢谢! – 2017-06-06 16:41:41