支持时间格式
- 时间戳: 1637395885585
- 带毫秒的: 2021-11-20T11:11:25.585
- 带毫秒的: 2021-11-20 11:11:25.585
- 不带毫秒的: 2021-11-20 11:11:25
- 不带秒的: 2021-11-20 11:11
- 不带分钟的: 2021-11-20 11
- 只有日期: 2021-11-20
- 带有完整时区的: 2021-11-20T11:11:25.585+03:00[Asia/Aden]
- 带有完整时区的: 2021-11-20T08:11:25.585Z[Europe/London]
- 带有时区偏移量的: 2021-11-20T11:11:25.585+03:00
- 带有时区偏移量的: 2021-11-20T11:11:25.585Z
能将字符串解析成
- unix时间戳
- Instant
- Date
- LocalDateTime
- LocalDate
- DateTime
- ZonedDateTime
- OffsetDateTime
源码
package com.github.azbh111.utils.java.date;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.time.*;
import java.util.*;
/**
* 支持各种格式的时间字符串解析
*
* 时间戳: 1637395885585
* 带毫秒的: 2021-11-20T11:11:25.585
* 带毫秒的: 2021-11-20 11:11:25.585
* 不带毫秒的: 2021-11-20 11:11:25
* 不带秒的: 2021-11-20 11:11
* 不带分钟的: 2021-11-20 11
* 只有日期: 2021-11-20
* 带有完整时区的: 2021-11-20T11:11:25.585+03:00[Asia/Aden]
* 带有完整时区的: 2021-11-20T08:11:25.585Z[Europe/London]
* 带有时区偏移量的: 2021-11-20T11:11:25.585+03:00
* 带有时区偏移量的: 2021-11-20T11:11:25.585Z
*
* @author: zyp
* @since: 2021/6/18 13:36
*/
@Getter
@ToString(callSuper = true)
public class DateParser {
@Setter
private Locale locale = Locale.getDefault();
private TimeZone timeZone = TimeZone.getDefault();
private ZoneId zoneId = timeZone.toZoneId();
public void setTimeZone(TimeZone timeZone) {
this.timeZone = timeZone;
this.zoneId = timeZone.toZoneId();
}
public void setTimeZone(ZoneId zoneId) {
this.zoneId = zoneId;
this.timeZone = TimeZone.getTimeZone(zoneId);
}
public long parseTimestamp(String str) {
if (str == null || str.isEmpty()) {
throw new DateParseException(String.format("can not convert '%s' to timestamp", str));
}
Scanner scanner = new Scanner(str);
return scanner.toTimestamp();
}
public Date parseDate(String str) {
long timestamp = parseTimestamp(str);
return new Date(timestamp);
}
public Date parseSqlDate(String str) {
long timestamp = parseTimestamp(str);
return new java.sql.Date(timestamp);
}
public Instant parseInstant(String str) {
long timestamp = parseTimestamp(str);
return Instant.ofEpochMilli(timestamp);
}
public LocalDateTime parseLocalDateTime(String str) {
Instant instant = parseInstant(str);
return LocalDateTime.ofInstant(instant, zoneId);
}
public LocalDate parseLocalDate(String str) {
return parseLocalDateTime(str).toLocalDate();
}
public LocalTime parseTime(String str) {
return parseLocalDateTime(str).toLocalTime();
}
public ZonedDateTime parseZonedDateTime(String str) {
if (str == null || str.isEmpty()) {
throw new DateParseException(String.format("can not convert '%s' to timestamp", str));
}
Scanner scanner = new Scanner(str);
long timestamp = scanner.toTimestamp();
Instant instant = Instant.ofEpochMilli(timestamp);
return ZonedDateTime.ofInstant(instant, scanner.getTimeZone().toZoneId());
}
public OffsetDateTime parseOffsetDateTime(String str) {
if (str == null || str.isEmpty()) {
throw new DateParseException(String.format("can not convert '%s' to timestamp", str));
}
Scanner scanner = new Scanner(str);
long timestamp = scanner.toTimestamp();
Instant instant = Instant.ofEpochMilli(timestamp);
return OffsetDateTime.ofInstant(instant, scanner.getTimeZone().toZoneId());
}
private class Scanner {
private static final char EOI = 0x1A;
private final String text;
private final char[] data;
private final int dataLen;
private Calendar calendar = null;
private int readPos;
int year;
int month;
int day;
int hour;
int minute;
int second;
int millis;
Integer zoneRawOffset;
String zoneIdName;
@Getter
TimeZone timeZone;
String zoneId;
public Scanner(String text) {
this.text = text.trim();
this.data = this.text.toCharArray();
dataLen = data.length;
readPos = 0;
}
public long toTimestamp() {
if (isNumber()) {
return Long.parseLong(text);
}
// 年
year = readNumber(4, -1, -1);
if (year == -1) {
return -1;
}
skip(1);
// 月
month = readNumber(2, 1, 12);
if (month == -1) {
return -1;
}
skip(1);
// 日
day = readNumber(2, 1, 31);
if (month == -1) {
return -1;
}
char ch = peek();
if (ch == EOI) {
// 读完了
return getTimestamp();
}
// 搜索下一个数字字符
int digitPos = nextDigitPos();
if (digitPos == -1) {
return -1;
}
this.pos(digitPos);
// 小时
hour = readNumber(2, 0, 23);
if (hour == -1) {
return -1;
}
if (peek() == EOI) {
return getTimestamp();
}
skip(1);
// 分钟
minute = readNumber(2, 0, 59);
if (minute == -1) {
return -1;
}
if (peek() == EOI) {
return getTimestamp();
}
skip(1);
// 秒
second = readNumber(2, 0, 60);
if (second == -1) {
return -1;
}
ch = peek();
if (ch == EOI) {
return getTimestamp();
}
if (ch == '.') {
// 毫秒
read();
millis = readNumber(3, 0, 999);
if (millis == -1) {
return -1;
}
}
ch = peek();
if (ch == EOI) {
return getTimestamp();
}
// 时区偏移量
zoneRawOffset = readZoneOffset();
if (zoneRawOffset == null) {
return -1;
}
ch = peek();
if (ch == EOI) {
return getTimestamp();
}
// 时区名
if (ch != '[') {
return -1;
}
read();
ch = charAt(dataLen - 1);
if (ch != ']') {
return -1;
}
zoneId = substring(readPos, dataLen - 1);
return getTimestamp();
}
private char charAt(int index) {
if (index < dataLen) {
return data[index];
}
return EOI;
}
private String substring(int beginIndex, int endIndex) {
return new String(data, beginIndex, endIndex - beginIndex);
}
/**
* 读取时区偏移量
*
* @param
* @return java.lang.Integer 毫秒偏移量, 如果读不到, 返回null
* @author zhengyongpan
* @since 2021/11/20 15:36
*/
private Integer readZoneOffset() {
char ch = peek();
int signal = 1;
if (ch == 'Z') {
read();
zoneIdName = "Z";
return 0;
} else {
if (ch == '+') {
} else if (ch == '-') {
signal = -1;
} else {
return null;
}
int zoneOffsetStartIndex = readPos;
read();
int hourOffset = readNumber(2, 0, 16);
if (hourOffset == -1) {
return null;
}
int offsetMilliseconds = hourOffset * 3600 * 1000;
ch = peek();
if (ch == EOI) {
return offsetMilliseconds * signal;
}
if (ch != ':') {
return offsetMilliseconds * signal;
}
read();
int minuteOffset = readNumber(2, 0, 59);
if (minuteOffset == -1) {
return null;
}
offsetMilliseconds += minuteOffset * 60 * 1000;
int zoneOffsetEndIndex = readPos;
zoneIdName = substring(zoneOffsetStartIndex, zoneOffsetEndIndex);
return offsetMilliseconds * signal;
}
}
private long getTimestamp() {
if (zoneId != null) {
// 优先使用时区
this.timeZone = TimeZone.getTimeZone(zoneId);
calendar = Calendar.getInstance(TimeZone.getTimeZone(zoneId), locale);
} else if (zoneRawOffset != null) {
// 其次使用偏移量
this.timeZone = new SimpleTimeZone(zoneRawOffset, zoneIdName);
calendar = Calendar.getInstance(timeZone, locale);
} else {
// 使用系统时区
this.timeZone = DateParser.this.timeZone;
}
calendar = Calendar.getInstance(this.timeZone, DateParser.this.locale);
calendar.setTimeInMillis(0);
calendar.set(year, month - 1, day, hour, minute, second);
return calendar.getTimeInMillis() + millis;
}
/**
* 搜索下一个数字字符所在的位置
*
* @param
* @return int 如果找不到,返回-1
* @author zhengyongpan
* @since 2021/11/20 15:36
*/
private int nextDigitPos() {
for (int i = readPos; i < dataLen; i++) {
if (Character.isDigit(data[i])) {
return i;
}
}
return -1;
}
/**
* 读取数字
*
* @param maxNumberLen maxNumberLen 最大长度
* @param min min 最小值, -1表示无限制
* @param max max 最大值, -1表示无限制
* @return int
* @author zhengyongpan
* @since 2021/11/20 15:37
*/
private int readNumber(int maxNumberLen, int min, int max) {
if (readPos + maxNumberLen > dataLen) {
return -1;
}
int number = 0;
char y;
while (maxNumberLen-- > 0) {
y = peek();
if (y < '0' || y > '9') {
return -1;
}
number = number * 10 + y - '0';
read();
}
if (min != -1 && number < min) {
return -1;
}
if (max != -1 && number > max) {
return -1;
}
return number;
}
private char peek() {
if (readPos < dataLen) {
return data[readPos];
}
return EOI;
}
private char read() {
if (readPos < dataLen) {
return data[readPos++];
}
return EOI;
}
private void skip(int len) {
this.readPos += len;
}
private void pos(int readPos) {
this.readPos = readPos;
}
private boolean isNumber() {
if (data[0] == '-') {
return true;
}
for (int i = 0; i < dataLen; i++) {
char c = data[i];
if (c < '0' || c > '9') {
return false;
}
}
return true;
}
}
}
测试用例
测试了所有时区,全部通过
package com.github.azbh111.utils.java.date;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
/**
* @author: zyp
* @since: 2021/8/11 18:48
*/
@Slf4j
public class DateParserTest {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
DateTimeFormatter formatterTruncateSeconds = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
DateTimeFormatter formatterTruncateMinute = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
DateTimeFormatter formatterTruncateHour = DateTimeFormatter.ofPattern("yyyy-MM-dd HH");
@Test
public void test() {
long timestamp = System.currentTimeMillis();
for (String availableZoneId : ZoneId.getAvailableZoneIds()) {
ZoneId zoneId = ZoneId.of(availableZoneId);
test(zoneId, timestamp);
}
}
private void test(ZoneId zoneId, long timestamp) {
System.out.println("====");
String timestampStr = String.valueOf(timestamp);
log.info("zoneId: {}", zoneId);
log.info("timestamp: {}", timestamp);
DateParser dateParser = new DateParser();
dateParser.setTimeZone(zoneId);
LocalDateTime localDateTime = dateParser.parseLocalDateTime(timestampStr);
assert timestamp == localDateTime.atZone(zoneId).toInstant().toEpochMilli();
log.info("localDateTime: {}", localDateTime.toString());
assert localDateTime.equals(dateParser.parseLocalDateTime(localDateTime.toString()));
log.info("localDateTimeStr: {}", formatter.format(localDateTime));
assert localDateTime.equals(dateParser.parseLocalDateTime(formatter.format(localDateTime)));
LocalDateTime localDateTimeTruncatedSeconds = localDateTime.truncatedTo(ChronoUnit.SECONDS);
log.info("localDateTimeTruncatedSeconds: {}", formatterTruncateSeconds.format(localDateTime));
assert localDateTimeTruncatedSeconds.equals(dateParser.parseLocalDateTime(formatterTruncateSeconds.format(localDateTime)));
LocalDateTime localDateTimeTruncatedMinute = localDateTime.truncatedTo(ChronoUnit.MINUTES);
log.info("localDateTimeTruncatedMinute: {}", formatterTruncateMinute.format(localDateTime));
assert localDateTimeTruncatedMinute.equals(dateParser.parseLocalDateTime(formatterTruncateMinute.format(localDateTime)));
LocalDateTime localDateTimeTruncatedHour = localDateTime.truncatedTo(ChronoUnit.HOURS);
log.info("localDateTimeTruncatedHour: {}", formatterTruncateHour.format(localDateTime));
assert localDateTimeTruncatedHour.equals(dateParser.parseLocalDateTime(formatterTruncateHour.format(localDateTime)));
LocalDate localDate = localDateTime.toLocalDate();
log.info("localDate: {}", localDate.toString());
assert localDate.equals(dateParser.parseLocalDate(localDate.toString()));
assert localDate.equals(dateParser.parseLocalDateTime(localDate.toString()).toLocalDate());
ZonedDateTime zonedDateTime = localDateTime.atZone(dateParser.getZoneId());
log.info("zonedDateTime: {}", zonedDateTime.toString());
assert zonedDateTime.equals(dateParser.parseZonedDateTime(zonedDateTime.toString()));
OffsetDateTime offsetDateTime = zonedDateTime.toOffsetDateTime();
log.info("offsetDateTime: {}", offsetDateTime.toString());
assert offsetDateTime.equals(dateParser.parseOffsetDateTime(offsetDateTime.toString()));
}
}