package com.gzzn.omms.msgexchange.domain import java.time.LocalDate import java.time.LocalDateTime import java.time.ZoneId /** * 算航班的运营保障日(OPERATION_DAY):拿计划运行时间 SODT 和机场时区推出来。它不是消息 * 接收日、也不是落库日,算出来之后就固定不变。 * * 民航的一天不一定从零点开始,切日边界由 msgx.operation-day.cutoff-hour 配。默认 0 点只是 * 占位,真实口径还要业务确认。见 docs/flight-state.md §2.1。 */ class OperationDayCalculator( zone: ZoneId, cutoffHour: Int, ) { private val zone: ZoneId = zone /** 切日边界小时:SODT 的本地时刻早于这个点就归属前一个运营日。取值 0–23,超出范围会被夹到边界值。 */ private val cutoffHour: Int = cutoffHour.coerceIn(0, 23) /** 把 SODT 换算成运营日;入参为空、全空白或格式不对时返回 null 而不抛异常,由调用方按"运营日算不出来"处理。 */ fun compute(sodt: String?): LocalDate? { if (sodt.isNullOrBlank()) return null val local = parseSodt(sodt.trim()) ?: return null val day = local.atZone(zone).toLocalDate() return if (local.hour < cutoffHour) day.minusDays(1) else day } companion object { /** * SODT 的报文格式:ddMMMyyHHmm,比如 15DEC031723,月份是三字母英文缩写、大小写都认。 * * 两位年份显式按 2000 年展开,不交给 java.time 的 yy 解析——不同实现对两位年的处理 * 不一致,自己展开才能保证各方对年份窗口的理解相同。基准年等真实报文到了再确认。 */ private val SODT_REGEX = Regex("(\\d{1,2})([A-Za-z]{3})(\\d{2})(\\d{2})(\\d{2})") private val MONTHS = mapOf( "JAN" to 1, "FEB" to 2, "MAR" to 3, "APR" to 4, "MAY" to 5, "JUN" to 6, "JUL" to 7, "AUG" to 8, "SEP" to 9, "OCT" to 10, "NOV" to 11, "DEC" to 12, ) internal fun parseSodt(raw: String): LocalDateTime? { val m = SODT_REGEX.matchEntire(raw.uppercase()) ?: return null val (d, mon, yy, hh, mm) = m.destructured val month = MONTHS[mon] ?: return null return runCatching { LocalDateTime.of(2000 + yy.toInt(), month, d.toInt(), hh.toInt(), mm.toInt()) }.getOrNull() } } }