.NET下基于AvailabilityDto查找非工作时段排班间隙问题
高效排查非工作时段排班间隙的正确实现方案
我来帮你搞定这个问题!先拆解下你现有代码里的几个关键问题,再给你一个严谨且高效的实现思路和代码:
现有代码的核心问题
- 索引越界风险:你的
while循环里直接访问outOfHoursAvailibilities[i + 1],当i是最后一个元素时会抛出NullReferenceException。 - 非工作时段筛选错误:原条件
m.EndDateTime.Hour <= 9 || m.StartDateTime.Hour >= 17会漏掉那些跨工作/非工作时段的排班(比如8:00-18:00的排班,其实覆盖了早上8-9和晚上17-18的非工作时间,但会被你的条件排除)。 - 未考虑搜索日期范围:没有将排班裁剪到用户指定的搜索起止日期内,会处理超出范围的无效数据。
- 间隙检测逻辑混乱:循环内的变量赋值和索引递增逻辑错误,导致要么漏检间隙,要么重复处理。
正确实现思路
要准确找到非工作时段的无人值班间隙,我们需要分四步走:
- 生成搜索范围内的完整非工作时段块:把搜索期内每天的0:00-9:00、17:00-24:00拆分成连续的时段块。
- 提取所有排班中覆盖非工作时段的部分:把每个排班裁剪到搜索范围内,再拆分出其中属于非工作时间的片段。
- 合并重叠/连续的覆盖时段:避免重复计算,减少后续对比的复杂度。
- 对比完整非工作时段和已覆盖时段:找出未被覆盖的间隙。
完整代码实现
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
相关产品推荐
相关产品推荐

