You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.NET下基于AvailabilityDto查找非工作时段排班间隙问题

高效排查非工作时段排班间隙的正确实现方案

我来帮你搞定这个问题!先拆解下你现有代码里的几个关键问题,再给你一个严谨且高效的实现思路和代码:

现有代码的核心问题

  1. 索引越界风险:你的while循环里直接访问outOfHoursAvailibilities[i + 1],当i是最后一个元素时会抛出NullReferenceException。
  2. 非工作时段筛选错误:原条件m.EndDateTime.Hour <= 9 || m.StartDateTime.Hour >= 17会漏掉那些跨工作/非工作时段的排班(比如8:00-18:00的排班,其实覆盖了早上8-9和晚上17-18的非工作时间,但会被你的条件排除)。
  3. 未考虑搜索日期范围:没有将排班裁剪到用户指定的搜索起止日期内,会处理超出范围的无效数据。
  4. 间隙检测逻辑混乱:循环内的变量赋值和索引递增逻辑错误,导致要么漏检间隙,要么重复处理。

正确实现思路

要准确找到非工作时段的无人值班间隙,我们需要分四步走:

  1. 生成搜索范围内的完整非工作时段块:把搜索期内每天的0:00-9:00、17:00-24:00拆分成连续的时段块。
  2. 提取所有排班中覆盖非工作时段的部分:把每个排班裁剪到搜索范围内,再拆分出其中属于非工作时间的片段。
  3. 合并重叠/连续的覆盖时段:避免重复计算,减少后续对比的复杂度。
  4. 对比完整非工作时段和已覆盖时段:找出未被覆盖的间隙。

完整代码实现

public class AvailabilityDto {
    public DateTime StartDateTime { get; set; }
    public DateTime EndDateTime { get; set; }
}

// 假设你的rota条目类型是这样的,根据实际情况调整
public class RotaEntry {
    public List<AvailabilityDto> Availabilities { get; set; }
}

public class GapFinder {
    public List<AvailabilityDto> FindOutOfHoursGaps(List<RotaEntry> rota, DateTime searchStart, DateTime searchEnd)
    {
        // 1. 收集所有排班中覆盖非工作时段的有效片段
        var allCoveredOutOfHours = new List<AvailabilityDto>();
        foreach (var entry in rota)
        {
            foreach (var avail in entry.Availabilities)
            {
                // 先把排班裁剪到搜索范围内
                var actualStart = avail.StartDateTime < searchStart ? searchStart : avail.StartDateTime;
                var actualEnd = avail.EndDateTime > searchEnd ? searchEnd : avail.EndDateTime;
                if (actualStart >= actualEnd) continue;

                // 提取这个排班中属于非工作时间的部分
                var coveredPortions = ExtractOutOfHoursPortion(actualStart, actualEnd);
                allCoveredOutOfHours.AddRange(coveredPortions);
            }
        }

        // 2. 合并重叠或连续的覆盖时段
        var mergedCovered = MergeOverlappingAvailabilities(allCoveredOutOfHours);

        // 3. 获取搜索范围内所有应被覆盖的非工作时段块
        var allOutOfHoursBlocks = GetAllOutOfHoursBlocks(searchStart, searchEnd);

        // 4. 找出未被覆盖的间隙
        var gaps = FindUncoveredGaps(allOutOfHoursBlocks, mergedCovered);

        return gaps;
    }

    // 生成搜索范围内的全部非工作时段块
    private static List<AvailabilityDto> GetAllOutOfHoursBlocks(DateTime searchStart, DateTime searchEnd)
    {
        var outOfHoursBlocks = new List<AvailabilityDto>();
        var currentDate = searchStart.Date;

        while (currentDate <= searchEnd.Date)
        {
            // 早上非工作时段:00:00 - 09:00
            var morningStart = currentDate;
            var morningEnd = currentDate.AddHours(9);
            AddValidBlock(outOfHoursBlocks, morningStart, morningEnd, searchStart, searchEnd);

            // 晚上非工作时段:17:00 - 24:00
            var eveningStart = currentDate.AddHours(17);
            var eveningEnd = currentDate.AddDays(1);
            AddValidBlock(outOfHoursBlocks, eveningStart, eveningEnd, searchStart, searchEnd);

            currentDate = currentDate.AddDays(1);
        }

        return outOfHoursBlocks;
    }

    // 辅助方法:添加有效的时段块(确保在搜索范围内)
    private static void AddValidBlock(List<AvailabilityDto> blocks, DateTime blockStart, DateTime blockEnd, DateTime searchStart, DateTime searchEnd)
    {
        var actualStart = blockStart < searchStart ? searchStart : blockStart;
        var actualEnd = blockEnd > searchEnd ? searchEnd : blockEnd;
        if (actualStart < actualEnd)
        {
            blocks.Add(new AvailabilityDto { StartDateTime = actualStart, EndDateTime = actualEnd });
        }
    }

    // 提取一个时段中属于非工作时间的部分
    private static List<AvailabilityDto> ExtractOutOfHoursPortion(DateTime start, DateTime end)
    {
        var portions = new List<AvailabilityDto>();
        var currentDate = start.Date;

        while (currentDate <= end.Date)
        {
            var dayStart = currentDate;
            var dayEnd = currentDate.AddDays(1);

            // 早上非工作时段
            var morningStart = dayStart;
            var morningEnd = dayStart.AddHours(9);
            AddOverlapPortion(portions, start, end, morningStart, morningEnd);

            // 晚上非工作时段
            var eveningStart = dayStart.AddHours(17);
            var eveningEnd = dayEnd;
            AddOverlapPortion(portions, start, end, eveningStart, eveningEnd);

            currentDate = currentDate.AddDays(1);
        }

        return portions;
    }

    // 辅助方法:添加两个时段的重叠部分
    private static void AddOverlapPortion(List<AvailabilityDto> portions, DateTime start, DateTime end, DateTime blockStart, DateTime blockEnd)
    {
        var overlapStart = start > blockStart ? start : blockStart;
        var overlapEnd = end < blockEnd ? end : blockEnd;
        if (overlapStart < overlapEnd)
        {
            portions.Add(new AvailabilityDto { StartDateTime = overlapStart, EndDateTime = overlapEnd });
        }
    }

    // 合并重叠或连续的时段
    private static List<AvailabilityDto> MergeOverlappingAvailabilities(List<AvailabilityDto> availabilities)
    {
        if (availabilities == null || availabilities.Count == 0)
            return new List<AvailabilityDto>();

        // 按开始时间排序
        var sorted = availabilities.OrderBy(a => a.StartDateTime).ToList();
        var merged = new List<AvailabilityDto> { sorted[0] };

        foreach (var next in sorted.Skip(1))
        {
            var last = merged.Last();
            if (next.StartDateTime <= last.EndDateTime)
            {
                // 重叠或连续,合并成一个时段
                last.EndDateTime = last.EndDateTime > next.EndDateTime ? last.EndDateTime : next.EndDateTime;
            }
            else
            {
                merged.Add(next);
            }
        }

        return merged;
    }

    // 找出未被覆盖的间隙
    private static List<AvailabilityDto> FindUncoveredGaps(List<AvailabilityDto> fullBlocks, List<AvailabilityDto> coveredBlocks)
    {
        var gaps = new List<AvailabilityDto>();
        int coveredIndex = 0;
        int coveredCount = coveredBlocks.Count;

        foreach (var fullBlock in fullBlocks)
        {
            DateTime currentStart = fullBlock.StartDateTime;
            DateTime currentEnd = fullBlock.EndDateTime;

            // 跳过所有在当前完整时段之前的覆盖时段
            while (coveredIndex < coveredCount && coveredBlocks[coveredIndex].EndDateTime <= currentStart)
            {
                coveredIndex++;
            }

            // 处理与当前完整时段重叠的覆盖时段
            while (coveredIndex < coveredCount && coveredBlocks[coveredIndex].StartDateTime < currentEnd)
            {
                var covered = coveredBlocks[coveredIndex];
                if (covered.StartDateTime > currentStart)
                {
                    // 存在未被覆盖的间隙
                    gaps.Add(new AvailabilityDto
                    {
                        StartDateTime = currentStart,
                        EndDateTime = covered.StartDateTime
                    });
                }
                // 更新当前未被覆盖的起始时间
                currentStart = covered.EndDateTime;
                coveredIndex++;
            }

            // 如果当前完整时段还有剩余未被覆盖的部分
            if (currentStart < currentEnd)
            {
                gaps.Add(new AvailabilityDto
                {
                    StartDateTime = currentStart,
                    EndDateTime = currentEnd
                });
            }
        }

        return gaps;
    }
}

方案优势

  • 准确性:正确处理跨天排班、部分覆盖非工作时段的排班,以及搜索范围的边界情况。
  • 高效性:通过合并重叠时段减少对比次数,时间复杂度主要由排序操作决定(O(n log n),n为排班数量),远优于嵌套循环的低效实现。
  • 可维护性:拆分为多个单一职责的辅助方法,逻辑清晰,便于后续调整非工作时段规则(比如周末、节假日)。

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 03:58:59