前言

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. Python课程笔记(八)

    一些简单的文件操作,学过linux的话理解感觉不会很难.课程代码 一.OS 目录方法 这个模块提供了一种方便的使用操作系统函数的方法 函数 说明 os.mkdir("path") ...

  2. 2021 CCPC女生赛

    newbie,A了五题铜牌收工 比赛时和队友悠哉游哉做题,想着干饭,最后幸好没滚出铜尾. 贴一下比赛过的代码 A题 签到 队友A的,判断正反方向序列是否符合要求 /*** * @Author: _Kr ...

  3. Luogu P2081 [NOI2012]迷失游乐园 | 期望 DP 基环树

    题目链接 基环树套路题.(然而各种错误调了好久233) 当$m=n-1$时,原图是一棵树. 先以任意点为根做$dp$,求出从每一个点出发,然后只往自己子树里走时路径的期望长度. 接着再把整棵树再扫一遍 ...

  4. popStar手机游戏机机对战程序

    DFS算,五分钟如果答案没有更新,那个解一般来说就很优了. #include <cstdio> #include <iostream> #include <string. ...

  5. linux 内核源代码情景分析——几个重要的数据结构和函数

    页面目录PGD.中间目录PMD和页面表PT分别是由表项pgd_t.pmd_t和pte_t构成的数组,而这些表项都是数据结构 1 /* 2 * These are used to make use of ...

  6. 这一篇 K8S(Kubernetes)集群部署 我觉得还可以!!!

    点赞再看,养成习惯,微信搜索[牧小农]关注我获取更多资讯,风里雨里,小农等你,很高兴能够成为你的朋友. 国内安装K8S的四种途径 Kubernetes 的安装其实并不复杂,因为Kubernetes 属 ...

  7. PTA 列车调度 (25分)

    PTA 列车调度 (25分) [程序实现] #include<bits/stdc++.h> using namespace std; int main(){ int num,n; cin& ...

  8. vue配置请求拦截器和响应拦截器

    首先确保我们已经设置的store.js进行值的存取,这时候我们需要配置请求和响应的拦截器设置 main.js import Vue from 'vue' import App from './App' ...

  9. 关于Thread的interrupt

    关于Thread的interrupt Thread的interrupt方法会引发线程中断. 主要有以下几个作用: 如Object的wait方法,Thread的sleep等等这些能够抛出Interrup ...

  10. selenium基本使用,及cannot find chrome binary解决方案

    什么是selenium? Selenium是一个用于Web应用程序测试的工具. Selenium 测试直接运行在浏览器中,就像真正的用户在操作一样. 支持通过各种driver(FirfoxDriver ...