一个支持复杂时间解析的工具类

支持时间格式

  • 时间戳: 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()));
    }


}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值