欢迎光临
我们一直在努力

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

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

📋 目录

  • 引言
  • 一、时间段是什么:先分清"时间点"与"时间段"
  • 二、两种格式总览与 API 入口
  • 三、ISO 8601 标准格式详解
    • 3.1 标准结构 P[nY][nM][nD][T[nH][nM][nS]]
    • 3.2 验证器对 ISO 格式的约束
  • 四、简化格式详解
    • 4.1 数字+单位组合规则
    • 4.2 mo 与 m 的区分:月还是分钟
  • 五、format 参数:ISO / SIMPLE / ANY 三种模式
  • 六、注解方式实战
  • 七、链式方式实战
  • 八、空值与判空语义
  • 九、常见有效/无效对照表
  • 十、典型业务场景
  • 十一、容易忽略的实现细节与坑
  • 总结
  • 项目地址

引言

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

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

  • ISO 8601 标准:PT2H30M、P1DT12H、P1Y2M3D——跨系统交换的通用语言
  • 简化格式:2h30m、1d12h、1y2mo3d——人类可读的日常写法

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

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


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

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

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

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


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

2.1 总览表

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

2.2 API 入口

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

// 注解方式
@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 标准结构 P[nY][nM][nD][T[nH][nM][nS]]

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 秒,支持小数 PT6S、PT0.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 在正则匹配之外还做了三条硬性检查:

  • P 和 PT 单独出现不合法——正则能匹配上空壳,但验证器会显式拒绝:

    if (duration.equalsIgnoreCase("P") || duration.equalsIgnoreCase("PT")) {
    return false;
    }

    "P"、"PT" 都是 false。

  • 不含 T 时,必须带有天(D):

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

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

  • 含 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

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

    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 明确区分:

    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 三种模式

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

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

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

    // 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 基础用法:两种格式都收

    public class TaskDTO {

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

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

    public class VideoUploadDTO {

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

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

    public class RetryConfigDTO {

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

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

    public class CouponDTO {

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

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


    七、链式方式实战

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

    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());
    }
    }

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

    // 字段标签让报错更可读;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()); // ["视频时长: …", …]
    }

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

    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) 只支持字符串
    合法时间段字符串 通过 见上文规则

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

    // 默认: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 校验,别让格式问题拖到解析阶段才暴露。


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

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

  • 总结

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

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


    项目地址

    • GitHub:https://github.com/vipxieliang/ValidX
    • Gitee:https://gitee.com/vipxieliang/ValidX
    • Maven Central:https://central.sonatype.com/artifact/io.github.vipxieliang/validx
    赞(0)
    未经允许不得转载:171主机测评 » ValidX时间段验证详解:ISO 8601标准与简化格式
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址