-2

I am comparing 2 lists of objects using a custom comparer like so:

public class LocationEqualityComparer : IEqualityComparer<LocationData>
{
    public bool Equals(LocationData x, LocationData y)
    {
        var idComparer = string.Equals(x.Id, y.Id, 
            System.StringComparison.OrdinalIgnoreCase); 
        var nameComparer = string.Equals(x.Name, y.Name, 
            System.StringComparison.OrdinalIgnoreCase);
        var addressComparer = string.Equals(x.Address, y.Address, 
            System.StringComparison.OrdinalIgnoreCase);
        var postcodeComparer = string.Equals(x.PostCode, y.PostCode, 
            System.StringComparison.OrdinalIgnoreCase); 
     
        if (idComparer && nameComparer && addressComparer && postcodeComparer) 
        {
            return true; 
        }

        return false; 
    }
}

This works great for me when using Linq to check the equality using: If I have two lists of LocationData (previousRun and currentRun, I get the correct result with: List<LocationData> result = previousRun.Intersect(currentRun, new LocationEqualityComparer()).ToList();

I am also able to check which items have been added or deleted between the lists using Except in Linq.

What I want to be able to do is check if an item has been UPDATED between the lists. This is because they represent an old list (previous run) and a new list (current run). So for example the LocationData object will have the same Id, same address and same postcode but might have a slightly different name.

Does anyone know how I can get a list of objects that have been updated between lists (i.e. only one or maybe two properties have changed) but not defined as added or deleted?

Thank you

Rufus L
  • 36,127
  • 5
  • 30
  • 43
Jordan1993
  • 864
  • 1
  • 10
  • 28
  • 1
    This ans will help you :https://stackoverflow.com/questions/23581379/compare-two-lists-of-object-for-new-changed-updated-on-a-specific-property – Gagan Burde Aug 04 '20 at 15:35
  • I think you could have another Comparer class that checks for same ID and whatever is different among the properties that can be changed. – insane_developer Aug 04 '20 at 15:38
  • I am not sure what the mystery is. You have a set of conditions that need to be satisfied for an item to be "updated" so write a method or an object that performs the comparison. I personally wouldn't write an Equality Comparer because you are not looking for "equality" per say, buy you could just as well do it this way if you wanted to leverage LINQ. The only thing that changes is the logic inside the comparer. – JuanR Aug 04 '20 at 15:39
  • 1
    `Except` should return items which have been altered. If you have `LocationData { Id = "1", Name = "Bob", Address = "", PostCode = ""}` in `previousRun`, and `LocationData { Id = "1", Name = "Bobby", Address = "", PostCode = ""}` in `currentRun`, then the updated `LocationData` should be returned by `currentRun.Except(previousRun, new LocationEqualityComparer())` – Joshua Robinson Aug 04 '20 at 16:03

3 Answers3

0

You could simply write a method that does the property comparison, but which returns true if a specific number of properties match (you said 1 or 2, so I guess it's variable?):

public static bool IsUpdated(LocationData previous, LocationData current, 
    int numPropsToMatch = 2)
{
    // If they are equal, return false
    if (new LocationEqualityComparer().Equals(previous, current)) return false;

    int numMatchingProps = 0;

    if (string.Equals(previous.Id, current.Id,
        System.StringComparison.OrdinalIgnoreCase)) numMatchingProps++;
    
    if (string.Equals(previous.Name, current.Name,
        System.StringComparison.OrdinalIgnoreCase)) numMatchingProps++;
    
    if (string.Equals(previous.Address, current.Address,
        System.StringComparison.OrdinalIgnoreCase)) numMatchingProps++;
    
    if (string.Equals(previous.PostCode, current.PostCode,
        System.StringComparison.OrdinalIgnoreCase)) numMatchingProps++;

    // Change to == if you *only* want a specific number to match
    return numMatchingProps >= numPropsToMatch;
}

Then you can just use this method in your Linq statement:

List<LocationData> updated = currentRun
    .Where(curr => previousRun.Any(prev => IsUpdated(prev, curr)))
    .ToList();

Note that it's highly likely that more than one location will have the same postal code, so that should probably not be included, but since it wasn't specified I left it.

Rufus L
  • 36,127
  • 5
  • 30
  • 43
0

You could say that two things are the same if they are structurally the same, or you could say they are the same if their identifies are the same.

Your comparer basically checks if they are structurally equal. That is, you are comparing all fields to figure out if all properties are the same.

That means you are treating LocationData as a value-object.

However, it has an id, which indicates that it is truly an entity.

Those should be treated differently and you can read more about these concepts here and here.

With the assumption that the "Id" in fact is there to uniquely identify the location, which would be the purpose of an "Id" in most cases, the problem of finding updates becomes trivial.

One way could be with an extension-class such as this:

public static class LocationDataExt
{
    public static bool IsUpdated(this LocationData previous, LocationData current)
    {
        if (!string.Equals(previous.Id, current.Id, StringComparison.OrdinalIgnoreCase))
            return false;   // it is not updated, because it is not the same entity

        // else, return true if any other property has changed
        return !string.Equals(previous.Name, current.Name, StringComparison.OrdinalIgnoreCase) ||
                !string.Equals(previous.Address, current.Address, StringComparison.OrdinalIgnoreCase) ||
                !string.Equals(previous.PostCode, current.PostCode, StringComparison.OrdinalIgnoreCase);

    }
}

Where you'd use it to find the ones updates like this:

var updated = currentRun
            .Where(current => previousRun.Any(previous => previous.IsUpdated(current)))
            .ToList();
Stokke
  • 1,871
  • 2
  • 13
  • 18
-1

Well, to get items that were updated between runs you need to write up new IEqualityComparer for such a case. Basically checking that ID is still the same as it was, but anything else may be changed, like Name, Address, and so on. Here is an example of such comparer with a test - works on my side.

public class LocationIdEqualityComparer : IEqualityComparer<LocationData>
{
    public bool Equals(LocationData x, LocationData y)
    {
        bool idComparer = string.Equals(x.Id, y.Id,
            StringComparison.OrdinalIgnoreCase);
        bool nameComparer = string.Equals(x.Name, y.Name,
            StringComparison.OrdinalIgnoreCase);
        bool addressComparer = string.Equals(x.Address, y.Address,
            StringComparison.OrdinalIgnoreCase);
        bool postcodeComparer = string.Equals(x.PostCode, y.PostCode,
            StringComparison.OrdinalIgnoreCase);
        
        // so you need to check that ID is the same, but everything else may be different
        return idComparer && (!nameComparer || !addressComparer || !postcodeComparer);
    }

    public int GetHashCode(LocationData obj)
    {
        return obj.Id.GetHashCode();
    }
}

class TestUpdatedItemsInList
{
    [Test]
    public void TestItemsAreUpdated()
    {
        List<LocationData> originalList = new List<LocationData>
        {
            new LocationData("1", "first", "somewhere1", "postCode1"),
            new LocationData("2", "second", "somewhere2", "postCode2"),
            new LocationData("3", "third", "somewhere3", "postCode3"),
            new LocationData("4", "fourth", "somewhere4", "postCode4"),
        };

        List<LocationData> updatedList = new List<LocationData>
        {
            new LocationData("1", "1st", "somewhere1", "postCode1"),
            new LocationData("2", "second", "who knows where", "postCode2"),
            new LocationData("3", "third", "somewhere3", "updated postCode3"),
            new LocationData("4", "fourth", "somewhere4", "postCode4"),
            new LocationData("5", "fifth", "somewhere5", "postCode5"),
            new LocationData("6", "sixth", "somewhere6", "postCode6"),
        };
        
        // newly added and updated items will end up here
        var differentItems = updatedList.Except(originalList, new LocationFullEqualityComparer());
        // only updated items will be here
        var updatedItems = updatedList.Except(originalList, new LocationIdEqualityComparer());
        // only non-changed items will be here (item 4)
        var itemsWithoutChanges = updatedList.Intersect(originalList, new LocationFullEqualityComparer());


        Assert.That(differentItems, Has.Exactly(5).Items);
        Assert.That(updatedItems, Has.Exactly(3).Items);

        Assert.That(itemsWithoutChanges, Has.Exactly(1).Items);
    }
}

public class LocationData
{
    public LocationData(string id, string name, string address, string postCode)
    {
        Id = id;
        Name = name;
        Address = address;
        PostCode = postCode;
    }

    public string Id { get; set; }
    public string Name { get; set; }
    public string Address { get; set; }
    public string PostCode { get; set; }

    public override string ToString()
    {
        return $"{Id}, {Name}, {Address}, {PostCode}";
    }
}

// code provided by you
public class LocationFullEqualityComparer : IEqualityComparer<LocationData>
{
    public bool Equals(LocationData x, LocationData y)
    {
        bool idComparer = string.Equals(x.Id, y.Id,
            StringComparison.OrdinalIgnoreCase);
        bool nameComparer = string.Equals(x.Name, y.Name,
            StringComparison.OrdinalIgnoreCase);
        bool addressComparer = string.Equals(x.Address, y.Address,
            StringComparison.OrdinalIgnoreCase);
        bool postcodeComparer = string.Equals(x.PostCode, y.PostCode,
            StringComparison.OrdinalIgnoreCase);

        return idComparer && nameComparer && addressComparer && postcodeComparer;
    }

    public int GetHashCode(LocationData obj)
    {
        return obj.Id.GetHashCode();
    }
}