在后台接口开发里,前端经常会传类似“20240101-20241231 08:00~18:00”这样的时间区间字符串。如果我们只靠手工用split截取再转类型,不仅代码啰嗦,还容易放过“20241301”这种错误月份。更稳妥的思路是先用正则表达式框定整体格式,再借助java.time提供的类型安全地解析具体日期和时间,最后补上业务上的区间合理性判断。

一、时间区间字符串的结构拆解
要验证一个时间区间,第一步是明确它到底由哪几部分组成。以上面的例子为例,可以拆成三块:起始日期、结束日期、以及每天的有效时段。日期部分通常是连续的数字,比如yyyyMMdd;时间部分则是HH:mm。中间用短横线和波浪号分隔,整体结构相对固定。
如果我们在代码里直接写一堆indexOf和substring,后续维护的人会很难受。一旦格式微调,比如日期改成yyyy-MM-dd,整个解析逻辑都要改。因此把“格式约束”和“语义解析”分开,是降低耦合的关键。正则负责格式约束,java.time负责语义解析,两者职责单一,也方便单测。
1.1 为什么不能只靠正则
正则表达式擅长描述“字符排列”,但不擅长描述“数值范围”和“日历逻辑”。例如“20240230”完全符合d{8}的形状,但二月根本没有三十号。如果只靠正则放行,后面用SimpleDateFormat(已过时)或java.time解析时才会报错,等于把校验压力推到了运行时。
更好的做法是正则只确保“看起来像那么回事”,比如日期是8位数字、时间是4位数字加冒号;随后用LocalDate.parse结合DateTimeFormatter去真正构造日期对象。这样既能挡掉明显乱写的字符串,又能在解析阶段捕获非法日历值。
二、编写匹配时间区间的正则表达式
针对“yyyyMMdd-yyyyMMdd HH:mm~HH:mm”这种格式,我们可以写出如下正则。注意正则里对数字长度做了硬性限制,并保留了分隔符,避免把“2024010120241231”误判为合法。
// 匹配 20240101-20241231 08:00~18:00 这类格式
String regex = "^\d{8}-\d{8} \d{2}:\d{2}~\d{2}:\d{2}$";
boolean formatOk = input.matches(regex);
上面这段正则中,^和$锁定首尾,d{8}要求日期必须是8位纯数字,中间用空格、短横、波浪号明确隔开。如果产品后来要求日期带横线,只需要把正则改成^d{4}-d{2}-d{2}...即可,改动集中、风险可控。
不过正则本身不区分“开始时间是否小于结束时间”,也不管日期是否真实。所以我们通常把正则校验作为第一道关卡,过了之后再进入java.time的精细校验,这样既快又准。
2.1 转义与可读性的平衡
在Java字符串里写正则,反斜杠必须写成双反斜杠,这常让新手看花眼。我们可以把正则提取成静态常量,并加上注释说明每组数字的含义,减少团队协作时的理解成本。
// 日期8位-日期8位 时间~时间
static final String RANGE_REGEX = "^\d{8}-\d{8} \d{2}:\d{2}~\d{2}:\d{2}$";
public static boolean looksLikeRange(String s) {
return s != null && s.matches(RANGE_REGEX);
}
把校验逻辑包在小方法里,后续还能轻松加上“不允许结束日期早于开始日期”的扩展,而不用动到调用方的代码。这种写法也比在业务里散落正则表达式要整洁得多。
三、使用java.time API做语义校验
正则通过之后,就要用java.time把字符串真正变成对象。对于yyyyMMdd,我们用DateTimeFormatter.ofPattern("yyyyMMdd")配合LocalDate;对于HH:mm,则用LocalTime.parse。这样即便字符串形如“20240230”,在parse阶段就会抛出DateTimeException。
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
public static boolean validateRange(String input) {
if (!looksLikeRange(input)) {
return false;
}
// 拆分字符串
String[] datePart = input.substring(0, 17).split("-");
String timePart = input.substring(18);
String[] times = timePart.split("~");
DateTimeFormatter dateFmt = DateTimeFormatter.ofPattern("yyyyMMdd");
DateTimeFormatter timeFmt = DateTimeFormatter.ofPattern("HH:mm");
try {
LocalDate startD = LocalDate.parse(datePart[0], dateFmt);
LocalDate endD = LocalDate.parse(datePart[1], dateFmt);
LocalTime startT = LocalTime.parse(times[0], timeFmt);
LocalTime endT = LocalTime.parse(times[1], timeFmt);
// 业务校验:结束不能早于开始
if (endD.isBefore(startD)) {
return false;
}
if (endD.isEqual(startD) && endT.isBefore(startT)) {
return false;
}
return true;
} catch (DateTimeParseException e) {
return false;
}
}
在上面的代码里,我们先调用前面定义好的looksLikeRange做格式初筛,再用substring和split把各部分拆开。这里split的结果直接交给parse,如果某段不是合法日历值,就会进入catch返回false。比起自己写判断闰年的逻辑,java.time已经帮我们处理好了所有边界。
另外,注意我们额外判断了结束日期早于开始日期、或同一天但结束时间早于开始时间的情况。这类“格式对但语义错”的问题,正则无能为力,只能靠java.time的对象比较来解决。这也正是两套工具配合的价值。
3.1 使用DateTimeFormatter严格模式
默认情况下,DateTimeFormatter在某些版本对宽松解析比较宽容,比如把“20241301”当成“2025年01月”往后推。为避免这种隐式转换,可以显式设置ResolverStyle.STRICT,让解析严格按日历进行。
import java.time.format.ResolverStyle;
DateTimeFormatter strictDateFmt = DateTimeFormatter
.ofPattern("yyyyMMdd")
.withResolverStyle(ResolverStyle.STRICT);
// 此时解析 20241301 会直接抛异常
开启严格模式后,像13月、32号这种值都会立刻失败,而不是被悄悄挪到下一个合法日期。对于校验场景,这能更早暴露调用方的传参错误,减少脏数据流入业务层。
四、完整示例与调用方式
下面给出一个可直接运行的完整片段,把前面说的正则和java.time校验串起来,方便在工具类里复用。
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.format.ResolverStyle;
public class TimeRangeValidator {
static final String RANGE_REGEX = "^\d{8}-\d{8} \d{2}:\d{2}~\d{2}:\d{2}$";
public static boolean validate(String input) {
if (input == null || !input.matches(RANGE_REGEX)) {
return false;
}
String dateSeg = input.substring(0, 17);
String timeSeg = input.substring(18);
String[] dates = dateSeg.split("-");
String[] times = timeSeg.split("~");
DateTimeFormatter df = DateTimeFormatter.ofPattern("yyyyMMdd")
.withResolverStyle(ResolverStyle.STRICT);
DateTimeFormatter tf = DateTimeFormatter.ofPattern("HH:mm")
.withResolverStyle(ResolverStyle.STRICT);
try {
LocalDate sD = LocalDate.parse(dates[0], df);
LocalDate eD = LocalDate.parse(dates[1], df);
LocalTime sT = LocalTime.parse(times[0], tf);
LocalTime eT = LocalTime.parse(times[1], tf);
if (eD.isBefore(sD) || (eD.isEqual(sD) && eT.isBefore(sT))) {
return false;
}
return true;
} catch (DateTimeParseException ex) {
return false;
}
}
public static void main(String[] args) {
System.out.println(validate("20240101-20241231 08:00~18:00")); // true
System.out.println(validate("20240230-20240301 08:00~18:00")); // false
}
}
这个工具类把正则常量、严格格式化器、以及区间比较都收敛在一个方法里。main方法里的两个例子分别覆盖了合法区间和非法日期,可以直观看到校验结果。实际项目中,还可以把失败原因细分后抛出不同异常,方便接口层返回更精确的错误提示。
整体来看,正则解决“长得对不对”,java.time解决“真不真、合不合理”。两者组合既能挡住大部分乱输,又能利用JDK自带日历体系避免重复造轮子,是处理时间区间字符串校验时性价比很高的方案。