ValidX时间段验证详解:ISO 8601标准与简化格式

ValidX时间段验证详解:ISO 8601标准与简化格式

📋 目录


引言

"视频时长不超过 15 分钟""优惠券有效期为 3 天""任务最多重试 2 小时 30 分钟"------这些业务里到处是时间段 (Duration)校验。但相比日期/时间点,时间段校验常被新手用"手写正则 + 各自约定"糊弄:有人存 "2.5h",有人存 "150min",有人存 "PT2H30M",格式五花八门,最终线上解析全靠运气。

ValidX 从 v1.0.0 起就内置了专门的时间段验证器 @Duration(链式为 isDuration),统一支持两套主流写法:

  • ISO 8601 标准PT2H30MP1DT12HP1Y2M3D------跨系统交换的通用语言
  • 简化格式2h30m1d12h1y2mo3d------人类可读的日常写法

本文直接对照 DurationValidator 源码与测试,讲清两套格式的完整规则、ISO_8601 / SIMPLE / ANY 三种模式的区别,以及最容易踩的 mo/m 混淆等实现细节。

文中结论对照 ValidX v1.2.0 源码与单元测试核实。


一、时间段是什么:先分清"时间点"与"时间段"

ValidX 的时间验证按语义分两类,别混用:

维度 时间点(Date/Time) 时间段(Duration)
回答的问题 "现在是几点?" "持续了多久?"
例子 2024-01-1514:30:00 PT2H30M2h30m
ValidX 注解 @Date@DateTime@HourMinute... @Duration
语义 日历上的某个时刻 一段长度,不锚定到某个日期

关键区别:PT2H30M 不代表"2 点 30 分",而是"两个半小时"这个长度。一个时间段可以和任意起点相加,本身没有日历位置。


二、两种格式总览与 API 入口

2.1 总览表

格式 类型名 代表值 特点
ISO 8601 DurationFormat.ISO_8601 PT2H30MP1DT12HP1Y2M3D 标准、跨平台通用
简化格式 DurationFormat.SIMPLE 2h30m1d12h1y2mo3d 人类可读、书写简洁
任意(默认) DurationFormat.ANY 两者之一 两种都接受

2.2 API 入口

ValidX 提供注解与链式两条等价入口:

java 复制代码
// 注解方式
@Duration
private String taskDuration;               // 两种格式均可

// 链式方式(ANY:默认两种都接受)
ValidX validator = ValidX.init();
validator.isDuration("PT2H30M");           // true
validator.isDuration("2h30m");             // true

// 指定只收 ISO 格式
validator.isDuration("PT2H30M", Duration.DurationFormat.ISO_8601);

注解属性只有一个 format(默认 ANY),对应上面枚举的三种模式。


三、ISO 8601 标准格式详解

3.1 标准结构 PnYnMnDT\[nHnMnS]

ISO 8601 时间段以字母 P 开头,核心结构为:

复制代码
P [nY] [nM] [nD] [T [nH] [nM] [nS]]

各部分含义([] 表示可选):

符号 含义 例子
P Period(必需前缀) P...
nY P1Y(1 年)
nM 月(日期部分) P2M(2 个月)
nD P3D(3 天)
T 时间分隔符:其后是时分秒 PT...
nH 小时(T 之后) PT4H(4 小时)
nM 分钟(T 之后) PT5M(5 分钟)
nS 秒,支持小数 PT6SPT0.5S

因此:

  • PT2H30M = 2 小时 30 分钟(纯时间部分,P 后直接跟 T
  • P1DT12H = 1 天 12 小时
  • P1Y2M3D = 1 年 2 个月 3 天(纯日期部分,无 T
  • P1Y2M3DT4H5M6S = 1 年 2 月 3 天 4 小时 5 分 6 秒(完整形式)

大写 T 的作用是把"日期部分"和"时间部分"分开T 之前的 M 是月,T 之后的 M 是分钟------同一个字母,位置决定含义。

3.2 验证器对 ISO 格式的约束

对照源码,isValidIso8601Duration 在正则匹配之外还做了三条硬性检查:

  1. PPT 单独出现不合法------正则能匹配上空壳,但验证器会显式拒绝:

    java 复制代码
    if (duration.equalsIgnoreCase("P") || duration.equalsIgnoreCase("PT")) {
        return false;
    }

    "P""PT" 都是 false

  2. 不含 T 时,必须带有天(D

    java 复制代码
    if (!duration.toUpperCase().contains("T") && !duration.toUpperCase().matches("^P\\d+D$")) {
        return false;
    }

    也就是说 P1YP2M(只有年月、没有天)会被拒绝;合法纯日期形式必须有 D,如 P1DP1Y2M3D

  3. T 时,T 后必须有至少一个时间单位 ------PT 后为空则拒绝。

此外正则 CASE_INSENSITIVE,所以 ISO 格式大小写不敏感pt2h30m 与小写 PT2H30M 等价(有测试覆盖)。


四、简化格式详解

4.1 数字+单位组合规则

简化格式是"数字+单位"顺序拼接,供日常快速书写:

复制代码
[ny][nmo][nd][nh][nm][ns]

支持的 6 个单位(必须按 年→月→天→时→分→秒 顺序出现,不可乱序):

单位 含义 例子
y / Y 1y
mo / MO 6mo
d / D 3d
h / H 小时 2h
m / M 分钟 30m
s / S 45s

合法值举例(均有测试覆盖):

java 复制代码
validator.isDuration("2h");       // true
validator.isDuration("2h30m");    // true
validator.isDuration("1h30m15s"); // true
validator.isDuration("1d12h30m"); // true
validator.isDuration("1y6mo");    // true
validator.isDuration("90s");      // true

与 ISO 一样大小写不敏感:"2H30M""2h30m" 等价。

校验强制"至少一个非零单位" :纯数字("123")、无效字符("2h30x")都会失败。

4.2 mo 与 m 的区分:月还是分钟

简化格式最容易踩的坑是月与分钟的字母冲突

  • 月 → 必须写 mo(month)
  • 分钟 → 只写 m(minute)

因为 m 已被分钟占用,所以月份必须用双字母 mo 明确区分:

java 复制代码
validator.isDuration("6mo");   // 6 个月(月 = mo)
validator.isDuration("6m");    // 6 分钟(分钟 = m)→ 不同含义!
validator.isDuration("1y2mo3d"); // 1 年 2 个月 3 天

验证器在解析时会特意排除 mo 里的 m(源码用"检查 m 后一位是否紧跟 o"来区分),确保 1y2mo3d 不会被误当成"1 年 2 分钟..."。

建议:跨系统/跨语言传递时优先用 ISO 格式避免歧义;简化格式多用于配置项等人类书写的内部场景。


五、format 参数:ISO / SIMPLE / ANY 三种模式

@Durationformat 属性与链式 isDuration 的第二参数一致,控制"收哪种写法":

模式 行为 适用
ANY(默认) ISO 与简化都接受 兼容历史数据 / 宽松校验
ISO_8601 收 ISO 格式 跨系统协议字段、API 对接
SIMPLE 收简化格式 内部配置、人工录入场景

模式是互斥白名单,不是"优先尝试":

java 复制代码
// ISO_8601 模式下,简化格式会被拒绝
validator.isDuration("PT2H30M", DurationFormat.ISO_8601);  // true
validator.isDuration("2h30m", DurationFormat.ISO_8601);    // false

// SIMPLE 模式下,ISO 格式会被拒绝
validator.isDuration("2h30m", DurationFormat.SIMPLE);      // true
validator.isDuration("PT2H30M", DurationFormat.SIMPLE);    // false

提示:注解里的枚举路径可写 Duration.DurationFormat.ISO_8601;若单独 import 了注解内部枚举,直接写 DurationFormat.ISO_8601 即可。


六、注解方式实战

6.1 基础用法:两种格式都收

java 复制代码
public class TaskDTO {

    // 默认 ANY:ISO 与简化都合法
    @Duration(message = "任务时长格式不合法")
    private String duration;   // "PT2H30M" 或 "2h30m" 均可
}

6.2 只收 ISO 8601:对接第三方协议

java 复制代码
public class VideoUploadDTO {

    // 视频平台协议要求 ISO 8601 时长
    @Duration(format = Duration.DurationFormat.ISO_8601,
              message = "视频时长必须为ISO 8601格式,如 PT1H30M")
    private String duration;
}

6.3 只收简化格式:人工配置

java 复制代码
public class RetryConfigDTO {

    // 运维人员手填配置,用人类可读的简化格式
    @Duration(format = Duration.DurationFormat.SIMPLE,
              message = "重试窗口格式:如 2h30m / 1d12h")
    private String retryWindow;
}

6.4 必填 + 格式:别忘了判空注解

java 复制代码
public class CouponDTO {

    // 必填且格式正确:@NotBlank 管空值,@Duration 管格式
    @NotBlank(message = "有效期不能为空")
    @Duration(message = "有效期格式不合法,如 PT24H 或 24h")
    private String validity;
}

与 ValidX 所有格式注解一致,@Durationnull/"" 返回通过(放行),是否必填由 @NotBlank 等决定------这是设计上的关注点分离,不是 bug。


七、链式方式实战

链式 isDuration 适合 Service 层动态校验,例如从外部接口/配置中心取回的字符串:

java 复制代码
public void validateTaskTimeLimit(String timeLimit) {
    ValidX validator = ValidX.init()
            .config(ValidXConfig.GLOBAL_NOT_EMPTY)          // 必填
            .field("任务时限").isDuration(timeLimit);        // 默认 ANY

    if (!validator.passed()) {
        throw new ValidationException(validator.getErrors());
    }
}

指定格式并搭配字段标签与局部覆盖:

java 复制代码
// 字段标签让报错更可读;allowNull 允许个别可选
ValidX validator = ValidX.init()
        .config(ValidXConfig.GLOBAL_NOT_NULL)
        .field("视频时长").isDuration(video.duration(), Duration.DurationFormat.ISO_8601)
        .field("封面描述时长(可选)").allowNull().isDuration(optionalText);

if (!validator.passed()) {
    System.out.println(validator.getErrors());  // ["视频时长: ...", ...]
}

多种值混合在同一链上验证(来自官方测试):

java 复制代码
ValidX validator = ValidX.init()
        .isDuration("PT2H30M")
        .isDuration("2h30m")
        .isDuration("P1DT12H");

validator.passed();  // true:三种合法写法都被 ANY 接受

八、空值与判空语义

DurationValidator.isValid 的判空逻辑与 ValidX 全线注解一致:

输入 结果 说明
null 通过(true) @NotNull 等负责判空
"" 通过(true) @NotBlank/@NotEmpty 负责判空
非 String 类型 失败(false) 只支持字符串
合法时间段字符串 通过 见上文规则

链式模式下受全局/局部配置影响(有专门测试覆盖):

java 复制代码
// 默认:null 与空串都放行
ValidX v1 = ValidX.init().isDuration(null);      // true
ValidX v2 = ValidX.init().isDuration("");        // true

// GLOBAL_NOT_NULL 下 null 失败;allowNull 可局部豁免
ValidX v3 = ValidX.init().config(ValidXConfig.GLOBAL_NOT_NULL);
v3.isDuration(null);                             // false
v3.allowNull().isDuration(null);                 // true

// GLOBAL_NOT_EMPTY 下空串失败;allowEmpty 可豁免
ValidX v4 = ValidX.init().config(ValidXConfig.GLOBAL_NOT_EMPTY);
v4.isDuration("");                               // false
v4.allowEmpty().isDuration("");                  // true

九、常见有效/无效对照表

写法 ISO_8601 模式 SIMPLE 模式 ANY 模式
PT2H30M
P1DT12H
P1Y2M3D
P1Y2M3DT4H5M6S
pt2h30m(小写)
P1D
P1Y / P2M(无 D 无 T)
P / PT(空壳)
PT 后无单位
2h30m
1d12h30m
1y2mo3d
6mo(6 个月)
1y6mo
90s
123(纯数字)
2h30x(无效字符)
2.5h(简化格式小数)
null / ""

十、典型业务场景

场景 建议格式 示例代码片段
视频/音频上传时长 ISO 8601(协议对接) @Duration(format = ISO_8601) private String videoDuration;
任务/批处理超时窗口 简化格式(人工配置) @Duration(format = SIMPLE) private String timeout = "2h30m";
优惠券/会员有效期 ISO 8601(跨系统) @Duration(format = ISO_8601) private String validity = "P30D";
缓存/Token TTL 简化或 ISO 均可 链式 .field("TTL").isDuration(ttl, DurationFormat.ANY)
活动倒计时/间隔 任选一种并全局统一 建 DTO 统一用 @Duration + 约定格式

工程建议 :一个系统内只选一种主格式 (推荐 ISO 8601),把另一种仅用于兼容存量数据;入库前用 @Duration 校验,别让格式问题拖到解析阶段才暴露。


十一、容易忽略的实现细节与坑

  1. P1Y 单独不合法 。按严格 ISO,P1Y(1 年)其实是合法的;但 ValidX 验证器要求"无 T 时必须有 D",P1YP2M 会被拒绝。需要"X 年"这种表示时写成 P1Y 会踩坑------若你的业务必须支持纯年月,注意这是当前验证器的收窄约束。
  2. mo 是月、m 是分钟 ,简化格式里二者不能混写;6mo6m,语义差了一个数量级。
  3. 单位必须按序书写 。简化格式的正则固定为 年→月→日→时→分→秒30m2h 这类乱序写法不合法。
  4. P/PT 空壳会被显式拒绝 ,不是"解析失败"而是"格式非法",错误消息会进 errors 列表。
  5. 大小写不敏感pt2h30m2H30M 都能通过,字段标准化时注意统一。
  6. 秒可带小数(仅 ISO) :ISO 的 S 部分正则支持 \d+(\.\d+)?(如 PT0.5S);简化格式不支持 小数,2.5h 会失败。
  7. 只支持 String@Duration 只处理字符串,非 String 直接判失败;用 Duration.parse 之类强类型对象前先转成字符串或另走原生 API。
  8. 判空分离@Duration 放行 null/"",必填场景必须叠加 @NotBlank(或链式 notEmpty()/config(GLOBAL_NOT_EMPTY)),这与 ValidX 全局规则一致。

总结

主题 结论
定位 时间段验证 = "持续多长",区别于时间点的"是哪一刻"
ISO 8601 P[nY][nM][nD][T[nH][nM][nS]]TM 是月、TM 是分钟
简化格式 [ny][nmo][nd][nh][nm][ns],单位按序;mo=月、m=分钟
三种模式 ANY(默认都收)/ ISO_8601 / SIMPLE,互斥白名单
API 注解 @Duration(format=...) + 链式 isDuration(value[, format])
空值 放行 null/"",必填叠加 @NotBlank 或全局/局部非空配置
两个最易踩的坑 P1YD/T 会被拒;②mo(月) 与 m(分钟) 混用

时间段校验的通用心法是:一个系统统一一种格式、入库前必校验、跨系统必用 ISO 。ValidX 的 @Duration 把两种格式 + 模式切换做成了开箱即用的注解和链式方法,直接声明即可,不必再手写正则。


项目地址

相关推荐
Lsetea1 小时前
Java 请求 HTTPS 报 PKIX path building failed:从证书链到 truststore 的完整排查
java·https·ssl证书·keytool·truststore
不一样的少年_2 小时前
WebP 压缩到底在干嘛?小白也能看懂的原理拆解
前端·后端·图片资源
不一样的少年_2 小时前
PNG/JPG 如何变成 WebP?真相不是改后缀!
前端·后端·图片资源
蓝桉柒72 小时前
java判断语句
java·开发语言
Zane19942 小时前
String::compareTo凭什么能当参数传?方法引用的四种形式讲透
java·后端
JavaGuide2 小时前
阿里 Qoder 又开源了一个专门给 Claude Code、Codex 做“体检”的项目
前端·后端
不一样的少年_2 小时前
JPEG 压缩到底在干嘛?小白也能看懂的 8 步拆解
前端·后端·图片资源
the局外人2 小时前
学习 FastAPI 的 Day 4:完成用户系统与接口联调(完结)
后端·python·fastapi
青山木2 小时前
Hot 100 --- 打家劫舍
java·数据结构·算法·leetcode·动态规划