0

I have an object Object1 And two lists of this object:

public class Object1
{
    public TimeSpan? Time { get; set; }
    public List<string> Parameters { get; set; }
}

List<Object1> List1 = List<Object1>();
List<Object1> List2 = List<Object1>();

Now, I'd like to merge the two lists, but this should occur on the list Parameters within the object. (Each combination of strings in this list should only occur once.) If the same Parameters exists both in List1 and List2, then the Time in List1 should be overwritten with the one in List2.

For example:

List1:               List2:
 Parameters   Time    Parameters   Time
 1;1          null    1;1          1:20
 1;2          null    1;2          0:51
 1;3          null
 2;5          0:30

Result:
 Parameters   Time
 1;1          1:20
 1;2          0:51
 1;3          null
 2;5          0:30

This is probably achievable by using a for loop, but since the lists can grow quite large, it might not perform that well.

I have already attempted to do this by using the following code, but this just seems to concat the lists.

var query = List1.Concat(List2)
 .GroupBy(x => x.Parameters)
 .Select(g => g.OrderByDescending(x => x.Time).First())
 .ToList();
11
  • 1
    What should it do if the same value for Parameters exists in both lists? Or if more than one value exists in the same list? Commented Mar 22, 2013 at 15:56
  • i guess uses the from the same list if not null Commented Mar 22, 2013 at 15:58
  • 1
    So should the result be sorted? (your attempt seems to suggest it should be). And how do you decide which value to accept when there are duplicates? Or does it not matter? Commented Mar 22, 2013 at 15:58
  • 1
    This looks very much like the merge from a merge sort. You could use the same method. Commented Mar 22, 2013 at 16:00
  • 1
    from o1 in l1 from o2 in l2 where Object1Equal(o1, o2) select o1.Time == null ? new Object1() {Parameters = o1.Parameters, Time = o2.Time} : o1; Commented Mar 22, 2013 at 16:32

2 Answers 2

2

My solution uses LINQ and a custom Comparer instance to compare the Parameters member and makes some assumptions about the items in Parameters. If those assumptions are correct, though, you could make the implementation simpler by using a Param property like the one suggested in Max's answer. Here's the gist of it:

private static List<Object1> MergeLists(List<Object1> list1, List<Object1> list2)
{
    var parameterComparer = new ParameterComparer();

    var distinctParameters = list1.Select(o => o.Parameters)
        .Concat(list2.Select(o => o.Parameters))
        .Distinct(parameterComparer);

    return (from p in distinctParameters
            let o1 = list1.SingleOrDefault(o => parameterComparer.Equals(p, o.Parameters))
            let o2 = list2.SingleOrDefault(o => parameterComparer.Equals(p, o.Parameters))
            let result = o2 ?? o1
            select result).ToList();
}

Here's a fuller test-driven answer. First, the Object1 declaration -- I added a helper constructor to make the declarations more succinct:

public class Object1
{
    public TimeSpan? Time { get; set; }
    public List<string> Parameters { get; set; }

    public Object1(TimeSpan? time, params string[] parameters)
    {
        Time = time;
        Parameters = parameters.ToList();
    }
}

Next, the TestMethod. I defined the Object1Comparer to make the implementation of the test simpler -- it isn't needed for the solution.

[TestMethod]
public void MergeListsTest()
{
    // Arrange
    var list1 = new List<Object1>
                    {
                        new Object1(null, "1", "1"),
                        new Object1(null, "1", "2"),
                        new Object1(null, "1", "3"),
                        new Object1(new TimeSpan(0, 0, 30), "2", "5")
                    };
    var list2 = new List<Object1>
                    {
                        new Object1(new TimeSpan(0, 1, 20), "1", "1"),
                        new Object1(new TimeSpan(0, 0, 51), "1", "2"),
                    };
    var expected = new List<Object1>
                        {
                            new Object1(new TimeSpan(0, 1, 20), "1", "1"),
                            new Object1(new TimeSpan(0, 0, 51), "1", "2"),
                            new Object1(null, "1", "3"),
                            new Object1(new TimeSpan(0, 0, 30), "2", "5")
                        };

    // Act
    List<Object1> actual = MergeLists(list1, list2);

    // Assert
    // Note: need to order the actual result to use CollectionAssert.AreEqual()
    List<Object1> orderedActual = actual.OrderBy(o => string.Join(";", o.Parameters)).ToList();
    CollectionAssert.AreEqual(expected, orderedActual, new Object1Comparer());
}

public class Object1Comparer : IComparer, IComparer<Object1>
{
    public int Compare(Object1 x, Object1 y)
    {
        if (x.Time == null && y.Time == null) return 0;
        if (x.Time == null || y.Time == null) return -1;
        int timeComparison = TimeSpan.Compare(x.Time.Value, y.Time.Value);
        if (timeComparison != 0) return timeComparison;

        if (x.Parameters == null && y.Parameters == null) return 0;
        if (x.Parameters == null || y.Parameters == null) return -1;
        if (x.Parameters.SequenceEqual(y.Parameters)) return 0;
        return -1;
    }

    public int Compare(object x, object y)
    {
        if (x is Object1 && y is Object1)
            return Compare(x as Object1, y as Object1);
        return -1;
    }
}

Finally, here's the implementation of MergeLists:

public class ParameterComparer : IEqualityComparer<List<string>>
{
    public bool Equals(List<string> x, List<string> y)
    {
        if (x == null && y == null) return true;
        if (x == null || y == null) return false;

        return x.SequenceEqual(y);
    }

    public int GetHashCode(List<string> obj)
    {
        if (obj == null) throw new ArgumentNullException("obj");

        // Note: this is not a safe way to get a hash code,
        // but if you're sure that the members are always ordered
        // and will never contain a semi-colon, then it will work.
        return string.Join(";", obj).GetHashCode();
    }
}

private static List<Object1> MergeLists(List<Object1> list1, List<Object1> list2)
{
    var parameterComparer = new ParameterComparer();

    var distinctParameters = list1.Select(o => o.Parameters)
        .Concat(list2.Select(o => o.Parameters))
        .Distinct(parameterComparer);

    return (from p in distinctParameters
            let o1 = list1.SingleOrDefault(o => parameterComparer.Equals(p, o.Parameters))
            let o2 = list2.SingleOrDefault(o => parameterComparer.Equals(p, o.Parameters))
            let result = o2 ?? o1
            select result).ToList();
}
Sign up to request clarification or add additional context in comments.

1 Comment

A nice solution, but maybe a bit overly complex, thanks anyway :)
1

Modify your Object1 like this

public class Object1
{
    public Object1()
    {
        this.Parameters = new List<string>();
    }

    public TimeSpan? Time { get; set; }

    public List<string> Parameters { get; set; }

    public string Param
    {
        get
        {
            return string.Join(",", this.Parameters.OrderBy(o => o).ToArray());
        }

    }
}

Modify your query like this

var query = list1.Concat(list2)
             .GroupBy(x => x.Param) // <- Changed with the new properties
             .Select(g => g.OrderByDescending(x => x.Time).First())
             .ToList();

Max

1 Comment

Excellent, this worked (except that I've left out the .OrderBy(o => o) Since this reorders the Parameters list, causing 1;2 to appear the same as 2;1) Thanks :)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.