按日期将两个列表合并为一个

Posted

技术标签:

【中文标题】按日期将两个列表合并为一个【英文标题】:Merging two lists into one by dates 【发布时间】:2019-04-30 10:17:43 【问题描述】:

我有两个插槽列表

public class Slot

    public DateTime Start  get; set; 

    public DateTime End  get; set; 

    public List<Service> Services  get; set; 


public class Service

    public int Id  get; set; 

    public int Duration  get; set; 


public class MergingClass

    public List<Slot> MergeSlots()
    
        var mergedList = new List<Slot>();
        var list1 = new List<Slot>
                        
                            new Slot
                                
                                    Start = new DateTime(2018, 11, 1, 8, 0, 0),
                                    End = new DateTime(2018, 11, 1, 11, 0, 0),
                                    Services = new List<Service>
                                                   
                                                       new Service
                                                           
                                                               Duration = 20,
                                                               Id = 1
                                                           
                                                   
                                ,
                            new Slot
                                
                                    Start = new DateTime(2018, 11, 1, 12, 0, 0),
                                    End = new DateTime(2018, 11, 1, 16, 0, 0),
                                    Services = new List<Service>
                                                   
                                                       new Service
                                                           
                                                               Duration = 20,
                                                               Id = 1
                                                           
                                                   
                                
                        ;

        var list2 = new List<Slot>
                          
                              new Slot
                                  
                                      Start = new DateTime(2018, 11, 1, 8, 0, 0),
                                      End = new DateTime(2018, 11, 1, 11, 0, 0),
                                      Services = new List<Service>
                                                     
                                                         new Service
                                                             
                                                                 Duration = 30,
                                                                 Id = 2
                                                             
                                                     
                                  ,
                              new Slot
                                  
                                      Start = new DateTime(2018, 11, 1, 12, 0, 0),
                                      End = new DateTime(2018, 11, 1, 18, 0, 0),
                                      Services = new List<Service>
                                                     
                                                         new Service
                                                             
                                                                 Duration = 30,
                                                                 Id = 2
                                                             
                                                     
                                  
                          ;
        return mergedList;
    

Start 和 End 是时间块,将除以服务持续时间(服务持续时间是 int 表示分钟)。

所以我有 2 个列表(用于 2 个不同的服务),我需要按开始和结束日期将它们合并到第三个列表(mergedList)中。

在这种情况下,方法 MergeSlots 应该返回:

mergedList = new List<Slot>
                    
                        new Slot
                            
                                Start = new DateTime(2018, 11, 1, 8, 0, 0),
                                End = new DateTime(2018, 11, 1, 11, 0, 0),
                                Services = new List<Service>
                                            
                                                new Service
                                                    
                                                        Duration = 20,
                                                        Id = 1
                                                    ,
                                                new Service
                                                    
                                                        Duration = 30,
                                                        Id = 2
                                                    
                                            
                            ,
                        new Slot
                            
                                Start = new DateTime(2018, 11, 1, 12, 0, 0),
                                End = new DateTime(2018, 11, 1, 16, 0, 0),
                                Services = new List<Service>
                                            
                                                new Service
                                                    
                                                        Duration = 20,
                                                        Id = 1
                                                    ,
                                                new Service
                                                    
                                                        Duration = 30,
                                                        Id = 2
                                                    
                                            
                            ,
                        new Slot
                            
                                Start = new DateTime(2018, 11, 1, 16, 0, 0),
                                End = new DateTime(2018, 11, 1, 18, 0, 0),
                                Services = new List<Service>
                                            
                                                new Service
                                                    
                                                        Duration = 30,
                                                        Id = 2
                                                    
                                            
                            
                    ;

当然,这两个插槽列表都来自我无法影响的系统,并且每次都会有所不同。

我尝试一步一步做,但解决方案又大又丑,而且容易出错:

foreach (var slot in list2)

    var slotWithStartInList1 = list1.FirstOrDefault(x => x.Start <= slot.Start && x.End > slot.Start);
    if (slotWithStartInList1 != null)
    
        if (slot.Start == slotWithStartInList1.Start)
        
            if (slot.End == slotWithStartInList1.End)
            
                slot.Services.AddRange(slotWithStartInList1.Services);
                mergedList.Add(slot);
                continue;
            

            if (slot.End < slotWithStartInList1.End)
            
                slot.Services.AddRange(slotWithStartInList1.Services);
                slotWithStartInList1.Start = slot.End;

                mergedList.Add(slot);
                mergedList.Add(slotWithStartInList1);
                continue;
            

            slotWithStartInList1.Services.AddRange(slot.Services);
            slot.Start = slotWithStartInList1.End;

            mergedList.Add(slotWithStartInList1);
            mergedList.Add(slot);
            continue;
        

        if (slot.End == slotWithStartInList1.End)
        
            slotWithStartInList1.End = slot.Start;
            slot.Services.AddRange(slotWithStartInList1.Services);

            mergedList.Add(slotWithStartInList1);
            mergedList.Add(slot);
            continue;
        

        if (slot.End > slotWithStartInList1.End)
        
            var tempSlot = new Slot
                               
                                   Start = slot.Start,
                                   End = slotWithStartInList1.End,
                                   Services = new List<Services>()
                               ;
            tempSlot.Services.AddRange(slotWithStartInList1.Services);
            tempSlot.Services.AddRange(slot.Services);

            slotWithStartInList1.End = tempSlot.Start;
            slot.Start = tempSlot.End;

            mergedList.Add(tempSlot);
            mergedList.Add(slot);
            mergedList.Add(slotWithStartInList1);
            continue;
        

        var tempSlot2 = new Slot
                           
                               Start = slotWithStartInList1.Start,
                               End = slot.Start,
                               Services = new List<Services>()
                           ;
        tempSlot2.Services.AddRange(slotWithStartInList1.Services);

        slot.Services.AddRange(slotWithStartInList1.Services);
        slotWithStartInList1.Start = slot.End;

        mergedList.Add(tempSlot2);
        mergedList.Add(slot);
        mergedList.Add(slotWithStartInList1);
        continue;
    

    var slotWithEndInList1 = list1.FirstOrDefault(x => x.Start < slot.End && x.End >= slot.End);
    if (slotWithEndInList1 != null)
    
        if (slot.End == slotWithEndInList1.End)
        
            slot.End = slotWithEndInList1.End;
            slotWithEndInList1.Services.AddRange(slot.Services);

            mergedList.Add(slot);
            mergedList.Add(slotWithEndInList1);

            continue;
        

        var tempSlot2 = new Slot
                            
                                Start = slotWithEndInList1.Start,
                                End = slot.End,
                                Services = new List<Services>()
                            ;
        tempSlot2.Services.AddRange(slotWithEndInList1.Services);
        tempSlot2.Services.AddRange(slot.Services);

        slot.End = tempSlot2.Start;
        slotWithEndInList1.Start = tempSlot2.End;

        mergedList.Add(tempSlot2);
        mergedList.Add(slot);
        mergedList.Add(slotWithEndInList1);
        continue;
    

    mergedList.Add(slot);


foreach (var slot in list1)

    if (mergedList.Any(x => x.Start == slot.Start))
    
        continue;
    

    mergedList.Add(slot);


return mergedList;

我可以添加一些私有方法来避免代码重复,但我想知道是否有更好(更简洁、更短)的方法来实现我的目标?

也许一些 linq 扩展?

【问题讨论】:

由于您的程序有效,请考虑将其发布到codereview.stackexchange.com 【参考方案1】:

好吧,我找到了另一个解决方案。可能它比您的代码更具可读性。我创建了一个 Range 类,其中包含代表连续时间范围的所有时隙,其中一些时隙重叠。 Recalculate 方法在时间跨度开始或结束的时间将其拆分为更小的块。对于这些小块,计算与这些块重叠的插槽的服务。

public class Range

    private bool _originalSlotsChanged = false;
    private List<Slot> _originalSlots = new List<Slot>();
    private List<Slot> _recalculatedSlots = new List<Slot>();

    public Range(Slot slot)
    
        AddOriginalSlot(slot);
    

    public DateTime? Start  get; set;  = null;
    public DateTime? End  get; set;  = null;

    public List<Slot> RecalculatedSlots
    
        get
        
            if (_originalSlotsChanged)
                Recalculate();
            return _recalculatedSlots;
        
    

    public void AddOriginalSlot(Slot slot)
    
        if (slot != null)
        
            _originalSlots.Add(slot);
            if (Start == null || slot.Start < Start)
                Start = slot.Start;
            if (End == null || slot.End > End)
                End = slot.End;
            _originalSlotsChanged = true;
        
    

    private void Recalculate()
    
        _recalculatedSlots.Clear();
        var pointsInRange = _originalSlots.Select(s => s.Start);
        pointsInRange = pointsInRange.Union(
            _originalSlots.Select(s => s.End)).Distinct().OrderBy(p => p);
        var arr = pointsInRange.ToArray();
        for (int i = 0; i < arr.Length - 1; i++)
        
            Slot slot = new Slot()
            
                Start = arr[i],
                End = arr[i + 1]
            ;
            AddServicesToNewSlot(slot);
            _recalculatedSlots.Add(slot);
        
    

    private void AddServicesToNewSlot(Slot newSlot)
    
        List<Service> services = new List<Service>();
        foreach (Slot originalSlot in _originalSlots)
        
            if (IsNewSlotInOriginalSlot(originalSlot, newSlot))
                services.AddRange(originalSlot.Services);
        
        newSlot.Services = services.OrderBy(s => s.Id).ToList();
        // optionally check for distinct services here
    

    private bool IsNewSlotInOriginalSlot(Slot originalSlot, Slot newSlot)
    
        return originalSlot.Start <= newSlot.Start && newSlot.Start < originalSlot.End;
    

现在,我们可以合并所有槽,对它们进行排序,将它们分成范围,然后得到结果。

var slotList = list1.Union(list2).OrderBy(s => s.Start);
Range lastRange = null;
var rangeList = new List<Range>();
foreach (Slot slot in slotList)

    if (lastRange == null || slot.Start >= lastRange.End.Value)
    
        lastRange = new Range(slot);
        rangeList.Add(lastRange);
    
    else
    
        lastRange.AddOriginalSlot(slot);
    


foreach (var range in rangeList)
    foreach (var slot in range.RecalculatedSlots)
        
            Console.WriteLine($"Slot slot.Start - slot.End");
            foreach (Service service in slot.Services)
                Console.WriteLine($"  Service service.Id: service.Duration");
        

Console.ReadLine();

对于您的示例,这是控制台输出:

【讨论】:

非常感谢您的努力,它看起来不错,我现在将对其进行测试,我们将看看它是如何工作的! 这段代码通过了单元测试。我不得不说我印象深刻,这正是我所需要的,谢谢!现在我只需要了解它是如何工作的,并学习如何在这个级别上编码...... 很高兴能为您提供帮助。要了解它是如何工作的,最好拿一张纸、一支铅笔,画出一些代表时间范围的线,并行地逐步执行代码并在图中查看发生了什么。【参考方案2】:

您可以为您的 Slot 类创建一个比较器类,然后使用一些 linq 来获得所需的结果。这是比较器:

public class SlotComparer : IEqualityComparer<Slot>

    public bool Equals(Slot x, Slot y)
    
        return x.Start.Equals(y.Start) 
               && x.End.Equals(y.End);
    

    public int GetHashCode(Slot slot)
    
        return (slot.Start.ToLongDateString() 
                + slot.End.ToLongDateString()).GetHashCode();
    

以下是应用它的方法。这会执行您的 foreach 循环中发生的事情。

SlotComparer sc = new SlotComparer();
var mergedList = list1
    .Union(list2, sc)
    .OrderBy(s => s.Start)
    .ThenBy(s => s.End)
    .ToList();
foreach (var distinctSlot in mergedList)

    var slotFromList1 = list1.FirstOrDefault(s => sc.Equals(s, distinctSlot));
    var slotFromList2 = list2.FirstOrDefault(s => sc.Equals(s, distinctSlot));
    var services = new List<Service>();
    if (slotFromList1 != null)
        services.AddRange(slotFromList1.Services);
    if (slotFromList2 != null)
        services.AddRange(slotFromList2.Services);
    distinctSlot.Services = services.OrderBy(s => s.Id).ToList();

return mergedList;

如果两个列表中的同一个槽可能有相同的服务,您可以为您的服务类创建一个类似的比较器并使用另一个联合操作。

【讨论】:

此解决方案仅在插槽相等时才有帮助。但是我需要解决许多其他情况,例如列表 1 中的插槽包含列表 2 中的插槽时。(slot1.start slot2.end @garret,现在我发现我误解了您的问题,很抱歉尝试根据我的期望编辑您的帖子。我想对于所有可能的情况都不会有一个简单而优雅的解决方案,因为您可以有重叠和包含句点。但这确实是一个很好的问题。

以上是关于按日期将两个列表合并为一个的主要内容,如果未能解决你的问题,请参考以下文章

将来自两个不同 wordpress 的帖子合并到一个按日期排序的帖子页面

如何在 PostgreSQL 中合并两个查询?

按日期不等的日期合并数据框

BigQuery:按日期将子选择合并为一行

php 将两个日期值合并为一个

python将两个csv文件按列合并为一个csv