Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Union of Two Objects Based on Equality of Their Members

Tags:

c#

Let us start with a class definition for the sake of example:

public class Person
{
    public string FirstName;
    public string LastName;
    public int Age;
    public int Grade;
}

Now let's assume I have a List<Person> called people containing 3 objects:

{"Robby", "Goki", 12, 8}
{"Bobby", "Goki", 10, 8}
{"Sobby", "Goki", 10, 8}

What I am looking for is some way to retrieve the following single Person object:

{null, "Goki", -1, 8}

where fields which are the same in all objects retain their value while fields which have multiple values are replaced with some invalid value.

My first thought consisted of:

Person unionMan = new Person();
if (people.Select(p => p.FirstName).Distinct().Count() == 1)
    unionMan.FirstName = people[0].FirstName;
if (people.Select(p => p.LastName).Distinct().Count() == 1)
    unionMan.LastName = people[0].LastName;
if (people.Select(p => p.Age).Distinct().Count() == 1)
    unionMan.Age = people[0].Age;
if (people.Select(p => p.Grade).Distinct().Count() == 1)
    unionMan.Grade = people[0].Grade;

Unfortunately, the real business object has many more members than four and this is both tedious to write and overwhelming for someone else to see for the first time.

I also considered somehow making use of reflection to put these repetitive checks and assignments in a loop:

string[] members = new string[] { "FirstName", "LastName", "Age", "Grade" };
foreach (string member in members)
{
    if (people.Select(p => p.**member**).Distinct().Count() == 1)
        unionMan.**member** = people[0].**member**;
}

where **member** would be however reflection would allow the retrieval and storage of that particular member (assuming it is possible).

While the first solution would work, and the second I am assuming would work, does anyone have a better alternative solution to this problem? If not, would using reflection as described above be feasible?

like image 283
Dan Bechard Avatar asked Aug 02 '12 20:08

Dan Bechard


3 Answers

It's inefficient to do a distinct of all the values just to count the distinct members. You have a shortcut scenario wherein finding one value in any of the subsequent items that does not have the same value as the first item's member means that you have an invalid state for that column.

Something like this should work, though more work would need to be done if any of the members are arrays, need recursive evaluation or other more complex logic (note I have not tested this):

public static T UnionCombine<T>(this IEnumerable<T> values) where T : new() {
    var newItem = new T();
    var properties = typeof(T).GetProperties();
    for (var prop in properties) {
        var pValueFirst = prop.GetValue(values.First(), null);
        var useDefaultValue = values.Skip(1).Any(v=>!(Object.Equals(pValueFirst, prop.GetValue(v, null))));
        if (!useDefaultValue) prop.SetValue(newItem, pValueFirst, null);
    }
    return newItem;
}
like image 188
Chris Shain Avatar answered Nov 16 '22 02:11

Chris Shain


Your last idea seems good to me, something like this:

List<Person> persons = new List<Person>()
{
    new Person(){ FirstName="Robby", LastName="Goki", Age=12, Grade=8},
    new Person(){ FirstName="Bobby", LastName="Goki", Age=10, Grade=8},
    new Person(){ FirstName="Sobby", LastName="Goki", Age=10, Grade=8},
};

var properties = typeof(Person).GetProperties();

var unionMan = new Person();
foreach (var propertyInfo in properties)
{
    var values = persons.Select(x => propertyInfo.GetValue(x, null)).Distinct();
    if (values.Count() == 1)
        propertyInfo.SetValue(unionMan, propertyInfo.GetValue(persons.First(), null), null);
}

A couple of observations:

  • your class members should be defined as properties and not public members and both get and set accessor must be public
  • the default constructor should define the "invalid" values (as correctly suggested by @RaphaëlAlthaus)

so, the class Person would look like this:

public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int Age { get; set; }
    public int Grade { get; set; }
    public Person()
    {
        this.FirstName = null;
        this.LastName = null;
        this.Age = -1;
        this.Grade = -1;
    }
}
like image 25
digEmAll Avatar answered Nov 16 '22 01:11

digEmAll


Update: Since you don't have control over the Person class, and state is defined in public fields, rather than properties, I have updated the solution to address this.

I would recommend using reflection. You would want to get the FieldInfo (or PropertyInfo) object ahead of time, rather than getting it for each entry in in your LINQ query. You can get them by using Type.GetField and Type.GetProperty. Once you have those, you can simply use FieldInfo/PropertyInfo.GetValue and FieldInfo/PropertyInfo.SetValue.

For example:

Type personType = typeof(Person);
foreach(string member in members)
{   // Get Fields via Reflection
    FieldInfo field = peopleType.GetField(member);
    if(field != null)
    {
        if (people.Select(p => field.GetValue(p, null) ).Distinct().Count() == 1)
        {
            field.SetValue(unionMan, field.GetValue(people[0], null), null);
        }
    }
    else // If member is not a field, check if it's a property instead
    {   // Get Properties via Reflection
        PropertyInfo prop = peopleType.GetProperty(member);
        if(prop != null)
        {
            if (people.Select(p => prop.GetValue(p, null) ).Distinct().Count() == 1)
            {
                prop.SetValue(unionMan, prop.GetValue(people[0], null), null);
            }
        }
    }
}

As you pointed out, you are already setting the "invalid" vlaues in the default constructor, so you shouldn't have to worry about them inside this loop.

Note: In my example, I used the versions of GetField and GetProperties that do not take a BindingFlags parameter. These will only return public members.

like image 1
Jon Senchyna Avatar answered Nov 16 '22 00:11

Jon Senchyna