You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何高效计算排除每周重复时段的时间间隔及反向推算?

这种带重复排除时段的时间计算确实有点绕,我之前做排班系统的时候刚好实现过类似逻辑,核心是把时间线按周拆解,先搞定单周的有效时长,再处理首尾的零散时段,反向推算也是同理——一步步“跳”过排除时段,消耗有效分钟。


一、正向计算:两个日期间的有效分钟数

核心思路

  1. 先算单周基准值:因为排除时段是每周重复的,先算出单周总分钟里,排除掉指定时段后剩下的有效分钟数。
  2. 拆分时间范围:把起始到结束的时间拆成「完整周部分」和「首尾零散时段部分」,完整周直接用单周有效分钟×周数,零散时段单独计算有效时长。
  3. 重叠判断:对零散时段,判断它和排除时段的重叠部分,用总时长减去重叠时长得到有效分钟。

具体步骤&伪代码

首先定义你的排除时段(用周几+时间表示,周一=0,周日=6):

from datetime import datetime, timedelta

# 定义排除时段:(周几开始, 开始时间, 周几结束, 结束时间)
EXCLUDED_SLOTS = [
    (4, datetime.strptime("17:31", "%H:%M"), 5, datetime.strptime("14:26", "%H:%M")),  # 周五17:31-周六14:26
    (1, datetime.strptime("03:37", "%H:%M"), 3, datetime.strptime("01:14", "%H:%M"))   # 周二03:37-周四01:14
]

1. 计算单周有效分钟

WEEK_TOTAL_MIN = 7 * 24 * 60

def get_weekly_excluded_min():
    total_excluded = 0
    for slot in EXCLUDED_SLOTS:
        start_wd, start_t, end_wd, end_t = slot
        # 转成周内总分钟数(周一00:00为0)
        start_min = start_wd * 24 * 60 + start_t.hour * 60 + start_t.minute
        end_min = end_wd * 24 * 60 + end_t.hour * 60 + end_t.minute
        
        if end_min >= start_min:
            # 时段在同一周内
            total_excluded += end_min - start_min
        else:
            # 时段跨周(比如周五到周六)
            total_excluded += (WEEK_TOTAL_MIN - start_min) + end_min
    return total_excluded

weekly_valid_min = WEEK_TOTAL_MIN - get_weekly_excluded_min()  # 你的场景里是6088分钟

2. 计算总有效分钟

def calculate_valid_minutes(start_dt: datetime, end_dt: datetime):
    if start_dt >= end_dt:
        return 0
    
    # 计算完整周数(注意跨年度的周数判断)
    start_year_week = (start_dt.year, start_dt.isocalendar()[1])
    end_year_week = (end_dt.year, end_dt.isocalendar()[1])
    full_weeks = 0
    valid_total = 0
    
    if start_year_week != end_year_week:
        # 先算中间完整周的有效分钟
        total_days = (end_dt - start_dt).days
        full_weeks = total_days // 7
        valid_total += full_weeks * weekly_valid_min
        
        # 计算开始到当周结束的有效分钟
        week_end_dt = start_dt + timedelta(days=(6 - start_dt.weekday()))
        week_end_dt = week_end_dt.replace(hour=23, minute=59, second=59)
        valid_total += calculate_partial_valid(start_dt, min(week_end_dt, end_dt))
        
        # 计算结束周开始到结束的有效分钟
        week_start_dt = end_dt - timedelta(days=end_dt.weekday())
        week_start_dt = week_start_dt.replace(hour=0, minute=0, second=0)
        valid_total += calculate_partial_valid(max(week_start_dt, start_dt), end_dt)
    else:
        # 同一周,直接算时段有效分钟
        valid_total += calculate_partial_valid(start_dt, end_dt)
    
    return valid_total

def calculate_partial_valid(start_dt: datetime, end_dt: datetime):
    # 计算同周内两个时间点的有效分钟
    start_min = start_dt.weekday() * 24 * 60 + start_dt.hour * 60 + start_dt.minute
    end_min = end_dt.weekday() * 24 * 60 + end_dt.hour * 60 + end_dt.minute
    total_duration = int((end_dt - start_dt).total_seconds() // 60)
    
    # 计算和排除时段的重叠时长
    overlap_min = 0
    for slot in EXCLUDED_SLOTS:
        s_wd, s_t, e_wd, e_t = slot
        s_slot_min = s_wd * 24 * 60 + s_t.hour * 60 + s_t.minute
        e_slot_min = e_wd * 24 * 60 + e_t.hour * 60 + e_t.minute
        
        if e_slot_min < s_slot_min:
            # 跨周的排除时段,分两部分计算重叠
            # 第一部分:slot开始到周结束
            overlap1 = max(0, min(WEEK_TOTAL_MIN, end_min) - max(s_slot_min, start_min))
            # 第二部分:周开始到slot结束
            overlap2 = max(0, min(e_slot_min, end_min) - max(0, start_min))
            overlap_min += overlap1 + overlap2
        else:
            # 普通时段,直接算重叠
            overlap_min += max(0, min(e_slot_min, end_min) - max(s_slot_min, start_min))
    
    return total_duration - overlap_min

二、反向推算:给定时间+X有效分钟后的目标时间

核心思路

  1. 先跳完整有效周:如果要加的有效分钟数远大于单周有效分钟,直接先加上对应周数,减少循环次数。
  2. 逐段消耗有效分钟:从起始时间开始,先走当前的有效时段,直到遇到排除时段就直接跳到时段结束,继续消耗剩余分钟,直到剩余分钟为0。

伪代码实现

def add_valid_minutes(start_dt: datetime, add_min: int):
    current_dt = start_dt
    remaining_min = add_min
    
    # 先处理完整的有效周
    if remaining_min >= weekly_valid_min:
        full_weeks = remaining_min // weekly_valid_min
        current_dt += timedelta(weeks=full_weeks)
        remaining_min -= full_weeks * weekly_valid_min
    
    # 处理剩余的有效分钟
    while remaining_min > 0:
        # 找到下一个排除时段的开始和结束时间
        next_exclude = find_next_exclusion(current_dt)
        if not next_exclude:
            # 没有后续排除时段,直接加剩余分钟
            current_dt += timedelta(minutes=remaining_min)
            remaining_min = 0
            break
        
        exclude_start, exclude_end = next_exclude
        # 计算当前到排除时段开始的有效时长
        available_min = int((exclude_start - current_dt).total_seconds() // 60)
        
        if available_min >= remaining_min:
            # 剩余分钟能在当前有效时段内消耗完
            current_dt += timedelta(minutes=remaining_min)
            remaining_min = 0
        else:
            # 用完当前有效时段,跳到排除时段结束
            current_dt = exclude_end
            remaining_min -= available_min
    
    return current_dt

def find_next_exclusion(current_dt: datetime):
    # 找到当前时间之后最近的排除时段
    current_wd = current_dt.weekday()
    current_min = current_wd * 24 * 60 + current_dt.hour * 60 + current_dt.minute
    next_slots = []
    
    for slot in EXCLUDED_SLOTS:
        s_wd, s_t, e_wd, e_t = slot
        s_slot_min = s_wd * 24 * 60 + s_t.hour * 60 + s_t.minute
        e_slot_min = e_wd * 24 * 60 + e_t.hour * 60 + e_t.minute
        
        # 计算当前周/下一周的排除时段时间
        if s_slot_min >= current_min:
            # 当前周的排除时段还没开始
            slot_start = current_dt - timedelta(minutes=current_min) + timedelta(minutes=s_slot_min)
            slot_end = current_dt - timedelta(minutes=current_min) + timedelta(minutes=e_slot_min)
            if e_slot_min < s_slot_min:
                slot_end += timedelta(weeks=1)
        else:
            # 当前周的排除时段已过,取下一周的
            slot_start = current_dt - timedelta(minutes=current_min) + timedelta(weeks=1) + timedelta(minutes=s_slot_min)
            slot_end = current_dt - timedelta(minutes=current_min) + timedelta(weeks=1) + timedelta(minutes=e_slot_min)
            if e_slot_min < s_slot_min:
                slot_end += timedelta(weeks=1)
        
        next_slots.append( (slot_start, slot_end) )
    
    # 返回最早的排除时段
    if not next_slots:
        return None
    next_slots.sort(key=lambda x: x[0])
    return next_slots[0]

三、关键注意事项
  • 时区统一:如果涉及跨时区场景,一定要统一用UTC时间或者指定时区计算,避免夏令时、时区偏移导致的时间误差。
  • 边界值处理:要测试刚好在排除时段开始/结束的时间点,确保重叠计算和跳转逻辑正确(比如开始时间正好是排除时段结束,应该立刻进入有效时段)。
  • 跨年度周isocalendar()返回的周数可能跨年度(比如12月31日可能是下一年的第1周),计算完整周数时要结合年份判断,避免出错。

内容的提问来源于stack exchange,提问作者Owen

火山引擎 最新活动