Best Practice to find best matching instance in a List C#

Viewed 131

For sure very simple question for most of you. But I am struggling with a solution at the moment.

Imagine you have a list of cats (List) where each cat has a list of babys (Kitten)

public class Cat
    {
        public string Name { get; set; }
        public int Age { get; set; }
        public string Race { get; set; }
        public bool Gender { get; set; }
        public List<Kitten> Babys { get; set; }  
    }

public class Kitten
    {
        public string Name { get; set; }
        public double Age { get; set; }
        public bool Gender { get; set; }
    }

now I want to find the Cat that has the most matches for given requirements. It could easily be the case that a cat matches only 2 of 3 requirements. I simple want to find the cat that has the most matches to my requirements.

where my requirements could be:

  • Name has to be "Micky"
  • Age is 42
  • Has a Kitten named "Mini"

My actual solution would be to compare all properties and take the one with the highest count of matching properties. But this is not generic and I am sure there are mutch better ways to do it.

Thanks in advance

4 Answers

Well, I have no opportunity to test this solution, but you can try this:

Assume that you have a list of cats:

var cats = new List<Cat>();

Now you have defined what are your criteria:

var desiredName = "Micky";
var desiredAge = 42;
var desiredKitten = "Mini";

And then you have to get your desired cat:

var desiredCat = cats
        .Select(c => new {
            Rating = 
                Convert.ToInt32(c.Age == desiredAge) +       // Here you check first criteria
                Convert.ToInt32(c.Name == desiredName) +     // Check second
                Convert.ToInt32(c.Babys.Count(b => b.Name == desiredKitten) > 0),   // And the third one
            c })
        .OrderByDescending(obj => obj.Rating) // Here you order them by number of matching criteria
        .Select(obj => obj.c) // Then you select only cats from your custom object
        .First(); // And get the first of them

Please check if this works for you. And if you need more specific answer or some edits for me to add.

If you really will compare 2 ou 3 requirements you can simplify using Linq by:

// try to find with 3 requirements
var foundCats = catList.Where(t => t.Name == desiredName && 
                                   t.Age == desiredAge &&
                                   t.Babys.Any(k => k.Name == desiredKitten)
                             ).ToList();

if (foundCats.Any())
{
    // you found the desired cat (or cats)
    return foundCats;
}

// try to find with 2 requirements
foundCats = catList.Where(t => 
    (t.Name == desiredName && t.Age == desiredAge) ||
    (t.Name == desiredName && t.Babys.Any(k => k.Name == desiredKitten)) ||
    (t.Age == desiredAge && t.Babys.Any(k => k.Name == desiredKitten)
).ToList();

if (foundCats.Any())
{
    // you found the desired cat (or cats)
    return foundCats;
}

// try to find with only 1 requirement
foundCats = catList.Where(t => t.Name == desiredName || 
                               t.Age == desiredAge ||
                               t.Babys.Any(k => k.Name == desiredKitten)
                         ).ToList();
return foundCats;

So, I see that the problem is you don't know if in any near future you will have more properties, so I will suggest going to the hardway and make reflection, the following is ugly af but you can probably (you will) make it better and hopefully serves you well as guiadance:

public static List<Cat> CheckProperties(List<Cat> inCatList, Cat inQueryCat)
{
    Dictionary<Cat, List<PropertyInfo>> dict = new Dictionary<Cat, List<PropertyInfo>>();

    foreach (PropertyInfo pI in inQueryCat.GetType().GetProperties())
    {
        var value = pI.GetValue(inQueryCat);

        if (value != null)
        {
            var cats = inCatList.Where(cat => cat.GetType().GetProperty(pI.Name).GetValue(cat).Equals(value));

            foreach (Cat cat in cats)
            {
                if (dict.ContainsKey(cat))
                {
                    dict[cat].Add(pI);
                }
                else
                {
                    dict.Add(cat, new List<PropertyInfo>() {pI});
                }
            }
        }
    }

    int max = Int32.MinValue;
    foreach (KeyValuePair<Cat, List<PropertyInfo>> keyValuePair in dict)
    {
        if (keyValuePair.Value.Count > max)
        {
            max = keyValuePair.Value.Count;
        }
    }

    return dict.Where(pair => pair.Value.Count == max).Select(pair => pair.Key).ToList();
}

While this is the most generic solution there is (need some edge case improvements):

  public class ReflectCmpare
    {
        public PropertyInfo PropertyInfo { get; set; }
        public dynamic Value { get; set; }
    }


    public Cat GetBestCat(List<Cat> listOfCats, List<ReflectCmpare> catParamsToCompare, List<ReflectCmpare> kittensParamsToCompare)
    {
        var bestScore = 0;
        var ret = listOfCats[0];
        foreach (var cat in listOfCats)
        {
            var score = catParamsToCompare.Sum(param => param.PropertyInfo.GetValue(cat, null) == param.Value ? 1 : 0);
            foreach (var baby in cat.Babys)
            {
                score+= kittensParamsToCompare.Sum(param => param.PropertyInfo.GetValue(baby, null) == param.Value ? 1 : 0);
            }

            if (score <= bestScore) continue;
            bestScore = score;
            ret = cat;
        }
        return ret;
    }

You should really think about just doing simple compare function considering this objects is not dynamic this is the way to go:

public Cat GetBestCat(List<Cat> listOfCats, string name , int? age , bool? gender, string race ,string babyName,int? babyAge,bool? babyGender )
    {
        var ret = listOfCats[0];
        var highestScore = 0;
        foreach (var cat in listOfCats)
        {
            var score = 0;
            score += name != null && cat.Name.Equals(name) ? 1 : 0;
            score += age.HasValue && cat.Age.Equals(age.Value) ? 1 : 0;
            score += gender.HasValue && cat.Gender.Equals(gender.Value) ? 1 : 0;
            score += race != null && cat.Race.Equals(race) ? 1 : 0;
            score += name != null && cat.Name.Equals(name) ? 1 : 0;
            score += cat.Babys
                .Where(k => babyName==null || k.Name.Equals(babyName))
                .Where(k => !babyAge.HasValue || k.Age.Equals(babyAge.Value))
                .Any(k => !babyGender.HasValue || k.Gender.Equals(babyGender.Value))?1:0;
            if (score <= highestScore) continue;
            highestScore = score;
            ret = cat;
        }

        return ret;
    }
Related