package com.common.util;
import java.text.SimpleDateFormat;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
import java.util.concurrent.ConcurrentHashMap;
/**
* <p>
* <code>DateTimeUtils</code>
* </p>
* Description: 时间工具类
*/
public class DateTimeUtils {
/**
* 系统默认时区
*/
public static final ZoneId DEFAULT = ZoneId.systemDefault();
/**
* UTC时区
*/
public static final ZoneId UTC = ZoneId.of("UTC");
/**
* 东8区
*/
public static final ZoneId GMT8 = ZoneId.of("GMT+8");
/**
* UTC时区
*/
public static final TimeZone UTC_ZONE = TimeZone.getTimeZone("UTC");
/**
* 东8区
*/
public static final TimeZone GMT8_ZONE = TimeZone.getTimeZone("GMT+8");
/**
* 默认时区
*/
public static final TimeZone DEFAULT_ZONE = TimeZone.getDefault();
/**
* 格式化器
*/
private static final Map<String, DateTimeFormatter> FORMATTER_MAP = new ConcurrentHashMap<>();
static {
FORMATTER_MAP.put("yyyy-MM-dd'T'HH:mm:ss.sss", DateTimeFormatter.ISO_LOCAL_DATE_TIME);
FORMATTER_MAP.put("yyyy-MM-dd HH:mm:ss", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
FORMATTER_MAP.put("yyyy-MM-dd HH:mm", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"));
FORMATTER_MAP.put("yyyy-MM-dd HH", DateTimeFormatter.ofPattern("yyyy-MM-dd HH"));
FORMATTER_MAP.put("yyyy-MM-dd", DateTimeFormatter.ofPattern("yyyy-MM-dd"));
FORMATTER_MAP.put("yyyy-MM", DateTimeFormatter.ofPattern("yyyy-MM"));
FORMATTER_MAP.put("yyyy", DateTimeFormatter.ofPattern("yyyy"));
FORMATTER_MAP.put("yyyyMMddHHmmss", DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
FORMATTER_MAP.put("yyyyMMddHHmm", DateTimeFormatter.ofPattern("yyyyMMddHHmm"));
FORMATTER_MAP.put("yyyyMMddHH", DateTimeFormatter.ofPattern("yyyyMMddHH"));
FORMATTER_MAP.put("yyyyMMdd", DateTimeFormatter.ofPattern("yyyyMMdd"));
FORMATTER_MAP.put("yyyyMM", DateTimeFormatter.ofPattern("yyyyMM"));
}
/**
* 根据给定的模式字符串获取DateTimeFormatter实例。
* 此方法通过模式字符串从一个全局缓存中获取DateTimeFormatter。如果缓存中不存在对应的DateTimeFormatter,
* 则会根据模式字符串创建一个新的实例并添加到缓存中。
* 这种做法避免了对相同模式字符串多次创建DateTimeFormatter实例,提高了性能。
*
* @param pattern 日期时间格式模式字符串,用于创建DateTimeFormatter。
* @return 根据给定模式字符串对应的DateTimeFormatter实例。
*/
public static DateTimeFormatter getFormatter(String pattern) {
return FORMATTER_MAP.computeIfAbsent(pattern, k -> DateTimeFormatter.ofPattern(pattern));
}
/**
* 获取当前时间的长整型值。
* <p>
* 本方法通过调用System.currentTimeMillis()获取自1970年1月1日00:00:00 GMT以来的毫秒数。
* 这是一个常用的操作,用于获取当前时间点或作为时间戳。
*
* @return 当前时间的长整型值,单位为毫秒。
*/
public static long currentTime() {
return System.currentTimeMillis();
}
/**
* 根据指定的时间戳、日期格式和时区,格式化日期时间。
* <p>
* 该方法将长期时间戳转换为指定时区的日期时间字符串。它使用提供的模式来定义日期时间的格式化方式。
*
* @param timestamp 时间戳,以毫秒为单位自1970年1月1日00:00:00 GMT以来的持续时间。
* @param pattern 日期时间的格式模式,例如"yyyy-MM-dd HH:mm:ss"。
* @param zoneId 时区标识,用于将时间戳转换为指定时区的日期时间。
* @return 格式化后的日期时间字符串。
*/
public static String format(long timestamp, String pattern, ZoneId zoneId) {
return format(ZonedDateTime.ofInstant(Instant.ofEpochMilli(timestamp), zoneId), pattern);
}
/**
* 根据指定的模式格式化给定的ZonedDateTime对象。
* <p>
* 该方法通过解析模式字符串,创建一个DateTimeFormatter实例,然后使用这个实例对ZonedDateTime对象进行格式化。
* 这种方式允许灵活地以各种格式表示日期和时间。
*
* @param zonedDateTime 需要被格式化的ZonedDateTime对象。
* @param pattern 用于格式化的模式字符串,遵循DateTimeFormatter的模式规则。
* @return 格式化后的字符串。
*/
public static String format(ZonedDateTime zonedDateTime, String pattern) {
return zonedDateTime.format(getFormatter(pattern));
}
/**
* 将指定格式的日期字符串解析为Date对象。
* 该方法首先使用提供的模式解析日期字符串,然后将解析得到的LocalDate与最小的LocalTime和指定的时区结合,
* 创建一个ZonedDateTime对象,最后从中提取Instant,转换为Date对象返回。
* 这种方法适用于需要精确控制日期解析格式和时区的场景。
*
* @param parse 待解析的日期字符串。只能包含完整的日期信息(年月日),不能包含时间信息(时分秒)。
* @param pattern 日期字符串的解析模式。
* @param zoneId 指定时区,用于将解析的日期时间转换为对应的Instant。
* @return 解析后的Date对象。
*/
public static ZonedDateTime parseDate(String parse, String pattern, ZoneId zoneId) {
return LocalDate.parse(parse, getFormatter(pattern)).atTime(LocalTime.MIN).atZone(zoneId);
}
/**
* 根据指定的格式解析时间字符串为Date对象。
* 该方法通过LocalTime.parse解析时间字符串,然后将其与1年1月1日的日期合并,
* 最后根据指定的时区转换为Date对象。
*
* @param parse 需要解析的时间字符串。只能包含完整的时间信息(时分秒),不能包含日期信息(年月日)。
* @param pattern 时间字符串的格式。
* @param zoneId 指定时区的ID,用于将时间转换为特定时区的Date对象。
* @return 解析后的Date对象。
*/
public static ZonedDateTime parseTime(String parse, String pattern, ZoneId zoneId) {
return LocalTime.parse(parse, getFormatter(pattern))
.atDate(LocalDate.of(1, 1, 1)).atZone(zoneId);
}
/**
* 将指定格式的日期时间字符串解析为Date对象。
* <p>
* 此方法通过提供的字符串和模式来创建一个Date对象,这允许灵活地处理不同格式的日期时间输入。
* 使用ZoneId来处理时区问题,确保日期时间的解析是基于指定时区的。
*
* @param parse 待解析的日期时间字符串。只能包含完整的日期和时间信息(年月日时分秒)
* @param pattern 日期时间字符串的格式模式。
* @param zoneId 时区标识,用于将解析的日期时间转换为指定时区的Date对象。
* @return 解析后的Date对象。
*/
public static ZonedDateTime parseDateTime(String parse, String pattern, ZoneId zoneId) {
return LocalDateTime.parse(parse, getFormatter(pattern)).atZone(zoneId);
}
/**
* 将ZonedDateTime对象转换为Date对象。
* <p>
* 该方法通过提取ZonedDateTime对象中的瞬间时间信息,并将其转换为Date对象,从而实现了类型转换。
* 这种转换保持了时间的精确性,不涉及时区的变更,仅是数据类型的转换。
*
* @param date 表示一个特定时刻和时区的时间,用于转换的源对象。
* @return 返回一个Date对象,表示与输入的ZonedDateTime对象相对应的瞬间时间。
*/
public static Date toDate(ZonedDateTime date) {
return Date.from(date.toInstant());
}
/**
* 将java.util.Date转换为java.time.ZonedDateTime。
* 这个方法提供了从旧的日期时间API到新的日期时间API的转换,使得能够利用新的API的功能和便利。
*
* @param date 一个java.util.Date对象,代表要转换的日期时间。
* @param zoneId 一个ZoneId对象,代表要应用于转换后的ZonedDateTime的时区。
* @return 返回一个ZonedDateTime对象,表示给定的日期时间在指定时区中的时间。
*/
public static ZonedDateTime toZonedDateTime(Date date, ZoneId zoneId) {
return date.toInstant().atZone(zoneId);
}
/**
* 根据指定的模式和时区,解析字符串为日期对象。
* <p>
* 此方法提供了一个灵活的方式来解析日期字符串,通过自定义的模式和指定的时区。
* 它封装了SimpleDateFormat的使用,简化了日期解析的过程。
*
* @param parse 待解析的日期字符串。
* @param pattern 日期字符串的模式,用于解析日期。
* @param timeZone 解析日期时使用的时区。
* @return 解析后的Date对象。
* @throws RuntimeException 如果解析过程中发生异常。
*/
public static Date parse(String parse, String pattern, TimeZone timeZone) {
try {
SimpleDateFormat format = new SimpleDateFormat(pattern);
format.setTimeZone(timeZone);
return format.parse(parse);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 在给定的日期上添加指定的时间单位数量。
* <p>
* 如果指定的数量为0,则直接返回原始日期。
* 使用Java 8的日期时间API来执行日期的加法操作。
*
* @param date 要进行加法操作的原始日期。
* @param number 要添加的时间单位的数量。未来时间为正数,过去时间为负数。
* @param unit 要添加的时间单位类型。
* @return 添加指定时间单位后的日期。
*/
public static ZonedDateTime add(ZonedDateTime date, int number, ChronoUnit unit) {
if (number == 0) {
return date;
}
return date.plus(number, unit);
}
/**
* 根据指定的时间单位,获取给定日期的开始时间。
* 该方法将根据不同的时间单位,返回从年、月、日、小时到分钟的开始时间。
*
* @param date 输入的日期
* @param unit 时间单位,用于确定返回的开始时间的精度。
* @return 返回给定日期根据指定时间单位的开始时间。
*/
public static ZonedDateTime getStart(ZonedDateTime date, ChronoUnit unit) {
LocalDateTime local = date.toLocalDateTime();
switch (unit) {
case YEARS:
return LocalDateTime.of(LocalDate.of(local.getYear(), 1, 1), LocalTime.MIN)
.atZone(date.getZone());
case MONTHS:
return LocalDateTime.of(LocalDate.of(local.getYear(), local.getMonth(), 1), LocalTime.MIN)
.atZone(date.getZone());
case DAYS:
return LocalDateTime.of(local.toLocalDate(), LocalTime.MIN).atZone(date.getZone());
case HOURS:
return LocalDateTime.of(local.toLocalDate(), LocalTime.of(local.getHour(), 0, 0, 0))
.atZone(date.getZone());
case MINUTES:
return LocalDateTime.of(local.toLocalDate(), LocalTime.of(local.getHour(), local.getMinute(), 0, 0))
.atZone(date.getZone());
default:
return LocalDateTime.of(local.toLocalDate(), LocalTime.of(local.getHour(), local.getMinute(), local.getSecond(), 0))
.atZone(date.getZone());
}
}
/**
* 计算给定时间单位下的结束时间。
* 该方法通过获取指定时间单位的开始时间,然后加上一个单位时间长度,再减去1纳秒,来得到该时间单位的结束时间。
* 这样做的目的是为了确保结束时间严格大于开始时间,且属于同一个时间单位的最后一个瞬间。
*
* @param date 开始时间点,用于计算时间单位的结束时间。
* @param unit 时间单位,用于计算结束时间。
* @return 返回指定时间单位的结束时间。
*/
public static ZonedDateTime getEnd(ZonedDateTime date, ChronoUnit unit) {
return add(getStart(date, unit), 1, unit).minusNanos(1);
}
/**
* 将时间截断到指定的时间单位后进行比较。
*
* @param a 第一个Date对象
* @param b 第二个Date对象
* @param unit 截断的时间单位。
* @return 返回比较的结果,当a大于b时返回true,其他返回false。
*/
public static boolean isAfter(ZonedDateTime a, ZonedDateTime b, ChronoUnit unit) {
if (a == null || b == null) {
if (a == null && b == null) {
return false;
} else {
return a != null;
}
}
LocalDateTime localA = a.toLocalDateTime();
LocalDateTime localB = b.toLocalDateTime();
if (unit == null) {
return localA.isAfter(localB);
}
switch (unit) {
case YEARS:
return LocalDate.of(localA.getYear(), 1, 1)
.isAfter(LocalDate.of(localB.getYear(), 1, 1));
case MONTHS:
return LocalDate.of(localA.getYear(), localA.getMonth(), 1)
.isAfter(LocalDate.of(localB.getYear(), localB.getMonth(), 1));
case DAYS:
return localA.toLocalDate().isAfter(localB.toLocalDate());
case HOURS:
return LocalDateTime.of(localA.toLocalDate(), localA.toLocalTime().truncatedTo(ChronoUnit.HOURS))
.isAfter(LocalDateTime.of(localB.toLocalDate(), localB.toLocalTime().truncatedTo(ChronoUnit.HOURS)));
case MINUTES:
return LocalDateTime.of(localA.toLocalDate(), localA.toLocalTime().truncatedTo(ChronoUnit.MINUTES))
.isAfter(LocalDateTime.of(localB.toLocalDate(), localB.toLocalTime().truncatedTo(ChronoUnit.MINUTES)));
case SECONDS:
return LocalDateTime.of(localA.toLocalDate(), localA.toLocalTime().truncatedTo(ChronoUnit.SECONDS))
.isAfter(LocalDateTime.of(localB.toLocalDate(), localB.toLocalTime().truncatedTo(ChronoUnit.SECONDS)));
default:
return localA.isAfter(localB);
}
}
/**
* 判断给定的Date对象是否在两个给定的Date对象之间。
*
* @param date 待比较Date对象
* @param a 第一个Date对象
* @param b 第二个Date对象
* @param compModel 比较模式,1:只比较日期,2:只比较时间,其他:比较日期和时间。
* @return 返回比较的结果,当date在a和b之间时返回true,其他返回false。
*/
public static boolean isBetween(ZonedDateTime date, ZonedDateTime a, ZonedDateTime b, int compModel) {
if (date == null || a == null || b == null) {
return false;
}
if (a.toInstant().toEpochMilli() == b.toInstant().toEpochMilli()) {
return false;
}
LocalDateTime localDate = LocalDateTime.ofInstant(date.toInstant(), DEFAULT);
LocalDateTime localA = LocalDateTime.ofInstant(a.toInstant(), DEFAULT);
LocalDateTime localB = LocalDateTime.ofInstant(b.toInstant(), DEFAULT);
switch (compModel) {
case 1:
// 只比较日期
if (a.toInstant().toEpochMilli() == b.toInstant().toEpochMilli()) {
return localDate.toLocalDate().isBefore(localA.toLocalDate())
&& localDate.toLocalDate().isAfter(localB.toLocalDate());
} else {
return localDate.toLocalDate().isAfter(localA.toLocalDate())
&& localDate.toLocalDate().isBefore(localB.toLocalDate());
}
case 2:
// 只比较时间
if (a.toInstant().toEpochMilli() == b.toInstant().toEpochMilli()) {
return localDate.toLocalTime().isBefore(localA.toLocalTime())
&& localDate.toLocalTime().isAfter(localB.toLocalTime());
} else {
return localDate.toLocalTime().isAfter(localA.toLocalTime())
&& localDate.toLocalTime().isBefore(localB.toLocalTime());
}
default:
// 比较日期和时间
if (a.toInstant().toEpochMilli() == b.toInstant().toEpochMilli()) {
return localDate.isBefore(localA) && localDate.isAfter(localB);
} else {
return localDate.isAfter(localA) && localDate.isBefore(localB);
}
}
}
/**
* 获取所有可用的时区ID集合。
* <p>
* 本方法通过调用Java标准库中的ZoneId.getAvailableZoneIds()方法,获取全球所有可用的时区ID。
* 这些ID可以用于标识不同的地理位置对应的时区信息。
*
* @return 返回一个包含所有可用时区ID的Set集合。
*/
private static Set<String> getAllZoneIds() {
return ZoneId.getAvailableZoneIds();
}
/**
* 获取所有可用的时间区域ID。
*
* @return 包含所有可用时间区域ID的字符串数组。
* <p>
* 此方法调用Java的TimeZone类的getAvailableIDs方法,该方法返回一个字符串数组,
* 数组中的每个字符串代表一个可用的时间区域ID。这些ID可以用于识别全球不同的时间区域。
*/
private static String[] getAllTimeZones() {
return TimeZone.getAvailableIDs();
}
public static void main(String[] args) throws Exception {
long currentTime = currentTime();
System.out.println(format(currentTime, "yyyy-MM-dd HH:mm:ss", UTC));
System.out.println(format(currentTime, "yyyy-MM-dd HH:mm:ss", GMT8));
System.out.println("------------------------------------------");
System.out.println(parseDate("2023-07-04", "yyyy-MM-dd", GMT8));
System.out.println(parseDate("2023-07-04", "yyyy-MM-dd", UTC));
System.out.println("------------------------------------------");
System.out.println(parseTime("10:01:01", "HH:mm:ss", GMT8));
System.out.println(parseDateTime("2023-07-04 10:01:01", "yyyy-MM-dd HH:mm:ss", GMT8));
System.out.println("------------------------------------------");
System.out.println(parse("2023-07", "yyyy-MM", GMT8_ZONE));
System.out.println(parse("2023-07-04 10:01", "yyyy-MM-dd HH:mm", GMT8_ZONE));
System.out.println(parse("2023-07-04 10:01:01", "yyyy-MM-dd HH:mm:ss", GMT8_ZONE));
System.out.println("------------------------------------------");
System.out.println(add(ZonedDateTime.now(), 1, ChronoUnit.DAYS));
System.out.println(add(ZonedDateTime.now(), -1, ChronoUnit.DAYS));
System.out.println("------------------------------------------");
ZonedDateTime a = parseDateTime("2023-07-06 10:02:01", "yyyy-MM-dd HH:mm:ss", GMT8);
ZonedDateTime b = parseDateTime("2023-07-04 11:01:03", "yyyy-MM-dd HH:mm:ss", GMT8);
System.out.println(isAfter(a, b, ChronoUnit.YEARS));
System.out.println(isAfter(a, b, ChronoUnit.MONTHS));
System.out.println(isAfter(a, b, ChronoUnit.DAYS));
System.out.println(isAfter(a, b, ChronoUnit.HOURS));
System.out.println(isAfter(a, b, ChronoUnit.MINUTES));
System.out.println(isAfter(a, b, ChronoUnit.SECONDS));
System.out.println(isAfter(a, b, null));
System.out.println("------------------------------------------");
ZonedDateTime now = ZonedDateTime.now(GMT8);
System.out.println(getStart(now, ChronoUnit.DAYS));
System.out.println(getEnd(now, ChronoUnit.YEARS));
System.out.println("------------------------------------------");
System.out.println(isBetween(add(now, 2, ChronoUnit.DAYS),
add(now, 1, ChronoUnit.DAYS),
add(now, 3, ChronoUnit.DAYS), 1));
System.out.println(isBetween(add(now, 2, ChronoUnit.DAYS),
add(now, 1, ChronoUnit.DAYS),
add(now, 3, ChronoUnit.DAYS), 2));
System.out.println(isBetween(add(now, 2, ChronoUnit.DAYS),
add(now, 1, ChronoUnit.DAYS),
add(now, 3, ChronoUnit.DAYS), 3));
System.out.println("------------------------------------------");
System.out.println(LocalDateTime.now(UTC));
System.out.println(LocalDate.now(UTC));
System.out.println(LocalTime.now(UTC));
System.out.println("------------------------------------------");
// 2024-07-10
System.out.println(now.format(DateTimeFormatter.ISO_LOCAL_DATE));
// 2024-07-10+08:00
System.out.println(now.format(DateTimeFormatter.ISO_OFFSET_DATE));
// 2024-07-10+08:00
System.out.println(now.format(DateTimeFormatter.ISO_DATE));
// 14:10:16.746
System.out.println(now.format(DateTimeFormatter.ISO_LOCAL_TIME));
// 14:10:16.746+08:00
System.out.println(now.format(DateTimeFormatter.ISO_OFFSET_TIME));
// 14:10:16.746+08:00
System.out.println(now.format(DateTimeFormatter.ISO_TIME));
// 2024-07-10T14:10:16.746
System.out.println(now.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
// 2024-07-10T14:10:16.746+08:00
System.out.println(now.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME));
// 2024-07-10T14:10:16.746+08:00[GMT+08:00]
System.out.println(now.format(DateTimeFormatter.ISO_ZONED_DATE_TIME));
// 2024-07-10T14:10:16.746+08:00[GMT+08:00]
System.out.println(now.format(DateTimeFormatter.ISO_DATE_TIME));
// 2024-192+08:00
System.out.println(now.format(DateTimeFormatter.ISO_ORDINAL_DATE));
// 2024-W28-3+08:00
System.out.println(now.format(DateTimeFormatter.ISO_WEEK_DATE));
// 2024-07-10T06:10:16.746Z
System.out.println(now.format(DateTimeFormatter.ISO_INSTANT));
// 20240710+0800
System.out.println(now.format(DateTimeFormatter.BASIC_ISO_DATE));
// Wed, 10 Jul 2024 14:10:16 +0800
System.out.println(now.format(DateTimeFormatter.RFC_1123_DATE_TIME));
System.out.println("------------------------------------------");
}
}
DateTimeUtils
于 2024-07-10 16:01:02 首次发布