前言

Java 中日期、时间相关的类相当的多,并且分不同的版本提供了不同的实现,包括 DateCalendarLocalDateTimeZoneDateTimeOffsetDateTime 等等。针对这些时间类型又通过 SimpleDateFormatDateTimeFormatter 实现不同的日期与字符串之间的格式化和解析。

为了应对各种各样的日期解析,我们通常会封装类似于 DateUtils 的工具类,专门用来处理日期字符串的解析,同时为了兼容不同格式的日期字符串,又需要预先枚举出可能用到的日期格式。这种传统的 DateUtils 通常会面临性能与兼容性的两难问题。

而本文要介绍的,是一个截然不同的日期解析工具 dateparser ,它可以智能地解析几百上千种任意格式的日期字符串,更为难得的是它的性能同样非常出色。

DateUtils 的两难问题

一个比较典型的日期解析函数类似这样(这是 commons-lang3 在其 DateUtils 中提供的函数):

    public static Date parseDate(final String str, final String... parsePatterns) {
return parseDate(str, null, parsePatterns);
}

这种日期解析函数的内部逻辑,往往是根据一批 DATE_FORMAT 轮番尝试,通过异常重试的方式试出来唯一匹配的格式。这种简单粗暴的方式,事实上存在着一个两难问题。

首先,我们很难穷举出全部可能出现的日期格式,年月日的分隔符、排列次序、时分秒、是否有毫秒、时区处理、PM 与 AM 的支持等等,罗列出来的话不计其数。

其次,异常重试的方式存在一些性能损耗,据我粗略测算,在 MBP 硬件环境中异常中断大概需要消耗 2 微秒,而一次日期解析可能消耗 0.75 微秒。如果提供的 parsePatterns 数量很多,则解析一个日期字符串的循环重试的最终耗时甚至会超过 Redis 读写操作。

那么,有没有办法既可以支持无数多个不规则的日期字符串,同时也没有性能问题的技术方案呢?

dateparser

这就是解决日期字符串解析的灵丹妙药,它是一个高性能且非常智能的 datetime 字符串解析工具。

为了实现高性能与可扩展性,它并没有采用 SimpleDateFormatDateTimeFormatter ,而是正则表达式。通过预定义的正则表达式来自动地捕捉不同格式的日期片段,它可以自动抽取出字符串中存在的 year , month , day , hour , minute , second , zone 等熟悉。

这些预定义的正则表达式片段包括:

(?<week>%s)\W* 可以将 Monday 解析为 week

?(?<year>\d{4})$ 可以将 2019 解析为 year

^(?<year>\d{4})(?<month>\d{2})$ 可以抽取出 201909 内部的 yearmonth

?(?<hour>\d{1,2}) o’clock\W* 可以将 12 o’clock 解析为 hour

更多规则参见 DateParserBuilder.java

如此多的正则表达式,会不会也存在性能隐患呢?如果使用的是 java.util.regex 包来进行循环匹配,随着规则增加,确实会有性能问题。

但是 dateparser 使用 retree 将预定义的一大批正则表达式合并为一颗树,也就是正则匹配树。它可以非常快速地对一大批正则表达式执行并行匹配,内部结构可以理解为字典树,但是树中的节点并不是字母,而是正则匹配节点。

安装 Maven 依赖

可以通过此 maven 坐标引入依赖

    <dependency>
<groupId>com.github.sisyphsu</groupId>
<artifactId>dateparser</artifactId>
<version>1.0.2</version>
</dependency>

基础使用

dateparser 提供了一个 DateParserUtils 工具类,可以直接使用它将字符串解析为 DateCalendarLocalDateTimeOffsetDateTime 等:

    Date date = DateParserUtils.parseDate("Mon Jan 02 15:04:05 -0700 2006");
// Tue Jan 03 06:04:05 CST 2006
Calendar calendar =
DateParserUtils.parseCalendar("Fri Jul 03 2015 18:04:07 GMT+0100 (GMT Daylight Time)");
// 2015-07-03T17:04:07Z
LocalDateTime dateTime =
DateParserUtils.parseDateTime("2019-09-20 10:20:30.12345678 +0200");
// 2019-09-20T16:20:30.123456780
OffsetDateTime offsetDateTime =
DateParserUtils.parseOffsetDateTime("2015-09-30 18:48:56.35272715 +0000 UTC");
// 2015-09-30T18:48:56.352727150Z

需要注意的是,它会根据字符串中标明的 TimeZon e 或 ZoneOffset 自动进行偏移量转换。

创建新 DateParser 实例

由于 DateParser 不是线程安全的,同时 parse 操作通常非常快速(1us),因此 DateParserUtils 内部直接维护了一个 DateParser 单例,然后通过 synchronized 进行并发控制。

如果你想在多线程中高频率、并发地使用它,就应该为不同的线程创建不同的 DateParser 实例:

    DateParser parser = DateParser.newBuilder().build();
Date date = parser.parseDate("Mon Jan 02 15:04:05 -0700 2006");
// Tue Jan 03 06:04:05 CST 2006

DateParser 实例相当笨重一些,所以你应该尽量多的复用它以提高性能。

MM/dddd/MM 的优先级

多数情况下, dateparser 可以按照规则自动地识别出字符串内部的 monthday 片段。

但是对于 MM/dd/yydd/MM/yy ,有时候它就难以区分了。因为世界上多数国家会使用 dd/MM/yy 作为日期的格式,但是也有少数国家会特立独行地使用 MM/dd/yy 作为日期格式,最典型的就是美帝国主义。

因此当 dateparser 遇到类似于 7.8.2019 这样的日期时,它就很难判断到底是 7 月 8 日还是 8 月 7 日。

为解决这个难题, dateparser 内部增加了一个名为 preferMonthFirst 的选项,用于辅助解决这个问题:

    DateParserUtils.preferMonthFirst(true);
DateParserUtils.parseCalendar("08.03.71");
// 1971-08-03
DateParserUtils.preferMonthFirst(false);
DateParserUtils.parseCalendar("08.03.71");
// 1971-03-08

默认情况下,如果无法甄别月与日,则视为月在后。如果你指定了 preferMonthFirsttrue ,则试为月在前。

自定义 Parser

你可以使用 DateParserBuilder 构建自己的日期解析器,通过此 builder ,你可以自定义新的解析规则。

例如,如果你想支持 【2019】 这样的 year 字符串,可以这样做:

    DateParser parser = DateParser.newBuilder().addRule("【(?<year>\\d{4})】").build();
Calendar calendar = parser.parseCalendar("【1991】");
assert calendar.get(Calendar.YEAR) == 1991;

需要注意的是,正则表达式 【(?<year>\\d{4})】 里面的 year 非常重要,它是 dateparser 内置的捕捉关键词。

你也可以增加更加灵活的解析规则,就像这样:

    DateParser parser = DateParser.newBuilder()
.addRule("民国(\\d{3})年", (input, matcher, dt) -> {
int offset = matcher.start(1);
int i0 = input.charAt(offset) - '0';
int i1 = input.charAt(offset + 1) - '0';
int i2 = input.charAt(offset + 2) - '0';
dt.setYear(i0 * 100 + i1 * 10 + i2 + 1911);
})
.build();
Calendar calendar = parser.parseCalendar("民国101年");
assert calendar.get(Calendar.YEAR) == 2012;

这个例子里面,新增了一个捕捉并解析 民国xxx年 的日期规则。

性能对比

首先,在单一日期格式下,对比一下 dateparserSimpleDateFormat 的性能表现:

    Benchmark               Mode  Cnt     Score    Error  Units
SingleBenchmark.java avgt 6 921.632 ± 12.299 ns/op
SingleBenchmark.parser avgt 6 1553.909 ± 70.664 ns/op

可以看到,在日期格式固定且单一的情况下, dateparser 在性能上处于下风,这也在预料之中。

然后,在单一日期格式下,对比一下 dateparserDateTimeFormatter 的性能表现:

    Benchmark                       Mode  Cnt     Score    Error  Units
SingleDateTimeBenchmark.java avgt 6 654.553 ± 16.703 ns/op
SingleDateTimeBenchmark.parser avgt 6 1680.690 ± 34.214 ns/op

可以看到, DateTimeFormatter 的性能表现确实比 S impleDateFormat 更加出色一些。同时 dateparser 的设计初衷是为了应对不规则日期格式,因此在固定格式匹配上存在劣势并不意外。

如果我们将日期格式增加为 16 种时,性能表现就不一样了:

    Benchmark              Mode  Cnt      Score      Error  Units
MultiBenchmark.format avgt 6 47385.021 ± 1083.649 ns/op
MultiBenchmark.parser avgt 6 22852.113 ± 310.720 ns/op

如果换算一下,无论日期格式是一种还是 16 中, dateparser 的性能始终维持在 1.5us ,说明它在算法上是非常稳定的,面对不同的场景不会有什么性能损失

支持的日期格式(部分)

以下为 dateparser 在单元测试中完成测试解析的日期格式样例,具体可以参考源代码。同时需要注意的是,这个列表只是一个子集:

    May 8, 2009 5:57:51 PM
oct 7, 1970
oct 7, '70
oct. 7, 1970
oct. 7, 70
Mon Jan 2 15:04:05 2006
Mon Jan 2 15:04:05 MST 2006
Mon Jan 02 15:04:05 -0700 2006
Monday, 02-Jan-06 15:04:05 MST
Mon, 02 Jan 2006 15:04:05 MST
Tue, 11 Jul 2017 16:28:13 +0200 (CEST)
Mon, 02 Jan 2006 15:04:05 -0700
Thu, 4 Jan 2018 17:53:36 +0000
Mon Aug 10 15:44:11 UTC+0100 2015
Fri Jul 03 2015 18:04:07 GMT+0100 (GMT Daylight Time)
September 17, 2012 10:09am
September 17, 2012 at 10:09am PST-08
September 17, 2012, 10:10:09
October 7, 1970
October 7th, 1970
12 Feb 2006, 19:17
12 Feb 2006 19:17
7 oct 70
7 oct 1970
03 February 2013
1 July 2013
2013-Feb-03
3/31/2014
03/31/2014
08/21/71
8/1/71
4/8/2014 22:05
04/08/2014 22:05
4/8/14 22:05
04/2/2014 03:00:51
8/8/1965 12:00:00 AM
8/8/1965 01:00:01 PM
8/8/1965 01:00 PM
8/8/1965 1:00 PM
8/8/1965 12:00 AM
4/02/2014 03:00:51
03/19/2012 10:11:59
03/19/2012 10:11:59.3186369
2014/3/31
2014/03/31
2014/4/8 22:05
2014/04/08 22:05
2014/04/2 03:00:51
2014/4/02 03:00:51
2012/03/19 10:11:59
2012/03/19 10:11:59.3186369
2014年04月08日
2006-01-02T15:04:05+0000
2009-08-12T22:15:09-07:00
2009-08-12T22:15:09
2009-08-12T22:15:09Z
2014-04-26 17:24:37.3186369
2012-08-03 18:31:59.257000000
2014-04-26 17:24:37.123
2013-04-01 22:43
2013-04-01 22:43:22
2014-12-16 06:20:00 UTC
2014-12-16 06:20:00 GMT
2014-04-26 05:24:37 PM
2014-04-26 13:13:43 +0800
2014-04-26 13:13:43 +0800 +08
2014-04-26 13:13:44 +09:00
2012-08-03 18:31:59.257000000 +0000 UTC
2015-09-30 18:48:56.35272715 +0000 UTC
2015-02-18 00:12:00 +0000 GMT
2015-02-18 00:12:00 +0000 UTC
2015-02-08 03:02:00 +0300 MSK m=+0.000000001
2015-02-08 03:02:00.001 +0300 MSK m=+0.000000001
2017-07-19 03:21:51+00:00
2014-04-26
2014-04
2014
2014-05-11 08:20:13,787
3.31.2014
03.31.2014
08.21.71
2014.03
2014.03.30
20140601
20140722105203
1332151919
1384216367189
1384216367111222
1384216367111222333

原文地址

本文转载至https://sulin.me/2019/38Z4HAT.html

Java日期格式转换不用发愁的更多相关文章

  1. Java日期格式转换

    Java时间格式转换大全 import java.text.*;import java.util.Calendar;public class VeDate {/**   * 获取现在时间   *    ...

  2. java日期格式转换工具类

    原文地址:http://blog.csdn.net/zhiweianran/article/details/7991531 package com.ace.backoffice.utils; impo ...

  3. java日期格式转换大全

    public class DateFormatUtils { private static Log logger = LogFactory.getLog(DateFormatUtils.class); ...

  4. JAVA日期格式转换---让人不得不说的故事

    链接:https://my.oschina.net/xinxingegeya/blog/394821 这是给我自己参考的,大家不惜勿喷 1.举例使用 2.各种作用 3.坑(默认中文日期,加上这个就是英 ...

  5. java 日期格式转换EEE MMM dd HH:mm:ss z yyyy

    SimpleDateFormat parserSDF = new SimpleDateFormat("EEE MMM dd HH:mm:ss zzzz yyyy", Locale. ...

  6. Java时间日期格式转换 转自:http://www.cnblogs.com/edwardlauxh/archive/2010/03/21/1918615.html

    Java时间格式转换大全 import java.text.*; import java.util.Calendar; public class VeDate { /** * 获取现在时间 * * @ ...

  7. java 获取当前日期和特殊日期格式转换

     1.获取当前日期: package com.infomorrow.dao; import java.sql.Timestamp; import java.util.Calendar; import ...

  8. Java练习 SDUT-2246_时间日期格式转换

    时间日期格式转换 Time Limit: 1000 ms Memory Limit: 65536 KiB Problem Description 对于日期的常用格式,在中国常采用格式的是"年 ...

  9. JAVA对象转换为JSON及日期格式转换处理

    1.JSON日期格式转换 默认JSON对DATE类型会转换成一个多属性对象, 而不是单独的一个字符串, 在某些应用处理上不是很方便,  可以利用JsonValueProcessor来实现日期的转换. ...

随机推荐

  1. [个人开源]vue-code-view:一个在线编辑、实时预览的代码交互组件

    组件简介 vue-code-view是一个基于 vue 2.x.轻量级的代码交互组件,在网页中实时编辑运行代码.预览效果的代码交互组件. 使用此组件, 不论 vue 页面还是 Markdown 文档中 ...

  2. 转:(WIN)S04-CH01 PCIE XDMA开发环境搭建以及环路测试

    摘要: 这一章开始主要介绍 XILINX FPGA PICE IP XDMA IP的使用.XDMA IP使用部分教程分LINUX 篇和WINDOWS篇两个部分.通过实战,面向应用,提供给大家 XILI ...

  3. cf13A Numbers(,,)

    题意: Little Petya likes numbers a lot. He found that number 123 in base 16 consists of two digits: th ...

  4. Java Logback简易教程

    本作品采用知识共享署名-非商业性使用 4.0 国际许可协议进行许可. 一.前言 本文以一个简单的项目为例,一步步展示logback的同步和异步配置方法,并且配置的日志要求满足阿里巴巴Java开发手册- ...

  5. MySQL中特别实用的几种SQL语句送给大家

    在写SQL时,经常灵活运用一些SQL语句编写的技巧,可以大大简化程序逻辑.减少程序与数据库的交互次数,有利于数据库高可用性,同时也能显得你的SQL很牛B,让同事们眼前一亮. 目录 实用的SQL 1.插 ...

  6. v-bind使用

    v-bind基本使用 动态地绑定一个或多个属性,或者绑定一个组件 prop 到表达式. 语法:v-bind:属性名 = 属性值 <!-- 绑定一个 attribute --> <im ...

  7. 关于Python中用户输入字符串(与变量名相同)无法作为变量名引用的理解以及解决方案

    在用户登录账号时,我需要在字典中查找是否存在其账号名称以及密码是否正确. 所以,我想将用户输入的账号赋值给变量,在字典中查找是否有此指值. 代码如下: 1 Ya = {'姓名': 'Ya', 'pas ...

  8. 菜鸡的Java笔记 - java 多对多映射

    要求:    1.将数据还原为简单java类    2.数据的输出:        可以根据一个用户输出它对应的角色以及每个角色对应的权限,以及包含的具体的权限详情:        一个权限可以输出具 ...

  9. Mplus数据分析:随机截距交叉之后的做法和如何加协变量,写给粉丝

    记得之前有写过如何用R做随机截距交叉滞后,有些粉丝完全是R小白,还是希望我用mplus做,今天就给大家写写如何用mplus做随机截距交叉滞后. 做之前我们需要知道一些Mplus的默认的设定: obse ...

  10. PAT A1107——并查集

     Social Clusters When register on a social network, you are always asked to specify your hobbies in ...