1
votes

I have a list contains another list where I supposed to get the common elements.

Class model:

PlanInfo has shiftName, ilist of Plan. Plan has start time, end time

public class Plan
{
    public int Start { get; set; }
    public int End { get; set; }
}

public class PlanInfo
{
    public string  ShiftName { get; set; }

    public IList<Plan> lstPlan { get; set; }
}

iList of PlanInfo contains

[“shift1”, [1000,1200]], 
[“shift2”,[1000,1100]],
[“shift3”,[1000,1200]]

Expected output in this should be empty since 1000,1200 doesn’t exist in shift2

[“shift1”, [1000,1200]], 
[“shift2”,[[1000,1200],[1000,1100]],
[“shift3”,[1000,1200]]

Should return [1000,1200] since it’s common in all lists.

I tried using intersect, but here IList<PlanInfo is not fixed length. it could have more than one records.

Kindly suggest me which LINQ query serve the above result

2
can you provide class model for your data ?Parimal Raj
It will be better if you can add your class to be part of question itsefl.MKR

2 Answers

1
votes

Hmm, If I understand the requirements: Given a list of PlanInfo, find any Plans common to all PlanInfo...

var totalPlanInfos = planInfos.Count();
var commonPlans = planInfos
    .SelectMany(x => x.Plans
        .Select(p => new { x.ShiftName, Plan = p }))
    .GroupBy(x => x.Plan)
    .Where(x => x.Count() == totalPlanInfos)
    .Select(x => x.Key)
    .ToList();

This assumes that a Plan can only be counted once within a PlanInfo. (No duplicate plans) This also assumes that the plan info references for the same start/end times are pointing to the same object instance. If not, then you cannot group on the Plan, you will need a unique key (Like a plan ID) to group on. If these are EF entities pulled from a DbContext then they will be the same reference.

First get the total # of plan infos. In your example this would return 3. Next, for all plan infos, use SelectMany to fetch the Plans, but compose that down into the PlanInfo.ShiftName + the Plan. This flattens your one to many. Next group by the Plan so that we can count the # of PlanInfos that each Plan appears in. Any/all counts that match the total number of PlanInfos means a Plan that appears in all PlanInfos, Select the Key to get that grouped Plan(s) and that should have it.

Edit: adding an example...

    [Test]
    public void TestPlanCheck()
    {
        var plan1 = new Plan { Start = 1, End = 2 };
        var plan2 = new Plan { Start = 2, End = 3 };
        var plan3 = new Plan { Start = 3, End = 4 };

        var planInfos = new List<PlanInfo>
        {
             new PlanInfo{ Name = "Test1", Plans = new []{ plan1, plan2}.ToList() },
             new PlanInfo{Name = "Test2", Plans = new []{plan2, plan3}.ToList()},
             new PlanInfo{Name = "Test3", Plans = new []{ plan3, plan2}.ToList() }
        };

        var totalPlanInfos = planInfos.Count();
        var commonPlans = planInfos
            .SelectMany(x => x.Plans
                .Select(p => new { x.Name, Plan = p }))
            .GroupBy(x => x.Plan)
            .Where(x => x.Count() == totalPlanInfos)
            .Select(x => x.Key)
            .ToList();

    }

    private class Plan
    {
        public int Start { get; set; }
        public int End { get; set; }
    }

    private class PlanInfo
    {
        public string Name { get; set; }
        public List<Plan> Plans { get; set; }
    }

That was the test I had run using these stub classes. In this case the test will return back 1 match, for the Plan 2 value.

To outline the issue with ensuring plan references for the same start/end times match: If the setup looked like this:

    [Test]
    public void TestPlanCheck()
    {
        var plan1 = new Plan { Start = 1, End = 2 };
        var plan2A = new Plan { Start = 2, End = 3 };
        var plan2B = new Plan { Start = 2, End = 3 };
        var plan3 = new Plan { Start = 3, End = 4 };

        var planInfos = new List<PlanInfo>
        {
             new PlanInfo{ Name = "Test1", Plans = new []{ plan1, plan2A}.ToList() },
             new PlanInfo{Name = "Test2", Plans = new []{plan2B, plan3}.ToList()},
             new PlanInfo{Name = "Test3", Plans = new []{ plan3, plan2B}.ToList() }
        };

        var totalPlanInfos = planInfos.Count();
        var commonPlans = planInfos
            .SelectMany(x => x.Plans
                .Select(p => new { x.Name, Plan = p }))
            .GroupBy(x => x.Plan)
            .Where(x => x.Count() == totalPlanInfos)
            .Select(x => x.Key)
            .ToList();
    }

In this case even though plan 2A and 2B have the same start/end time, the group by would not group them together because they represent 2 references to 2 objects. This though would be fine:

        var plan2A = new Plan { Start = 2, End = 3 };
        var plan2B = plan2A;

Both point to the same reference. If you do have different references for the same plan ranges, you would need a planID then group on a PlanId. Ideally though I would check why the references don't match because they should to avoid potential errors based on assumptions of equality.

0
votes

One can use Aggregate with Intersect on PlanInfo.Plans like:

var plansCommon = planInfoList.Select(p => p.Plans)
                              .Aggregate<IEnumerable<Plan>>((p1, p2) =>
                                                 p1.Intersect(p2, new PlanComparer()))
                               .ToList();

// Implement IEqualityComparer 
class PlanComparer : IEqualityComparer<Plan>
{
    public bool Equals(Plan x, Plan y)
    {
        if (x.Start == y.Start &&
                        x.End == y.End)
            return true;

        return false;
    }

    public int GetHashCode(Plan obj)
    {
        return obj.Start.GetHashCode() ^ obj.End.GetHashCode();
    }
}

The Intersect will recursively apply on Plans list of each PlanInfo to provide list of Plan common across all.