215

I have a list of Person objects. I want to convert to a Dictionary where the key is the first and last name (concatenated) and the value is the Person object.

The issue is that I have some duplicated people, so this blows up if I use this code:

private Dictionary<string, Person> _people = new Dictionary<string, Person>();

_people = personList.ToDictionary(
    e => e.FirstandLastName,
    StringComparer.OrdinalIgnoreCase);

I know it sounds weird but I don't really care about duplicates names for now. If there are multiple names I just want to grab one. Is there anyway I can write this code above so it just takes one of the names and doesn't blow up on duplicates?

BartoszKP
  • 34,786
  • 15
  • 102
  • 130
leora
  • 188,729
  • 360
  • 878
  • 1,366
  • 1
    The duplicates (based on the key), I'm not sure if you want to keep them or lose them? Keeping them would necessitate a `Dictionary>` (or equivalent). – Anthony Pegram Jul 23 '10 at 14:22
  • @Anthony Pegram - just want to keep one of them. i updated the question to be more explicit – leora Jul 23 '10 at 14:25
  • well you can use distinct before doing ToDictionary. but you would have to override Equals() and GetHashCode() methods for the person class so that CLR knows how to compare person objects – Sujit.Warrier Feb 15 '19 at 06:00
  • @Sujit.Warrier - You could also create an equality comparer to pass to `Distinct` – Kyle Delaney Feb 26 '20 at 02:38

13 Answers13

492

LINQ solution:

// Use the first value in group
var _people = personList
    .GroupBy(p => p.FirstandLastName, StringComparer.OrdinalIgnoreCase)
    .ToDictionary(g => g.Key, g => g.First(), StringComparer.OrdinalIgnoreCase);

// Use the last value in group
var _people = personList
    .GroupBy(p => p.FirstandLastName, StringComparer.OrdinalIgnoreCase)
    .ToDictionary(g => g.Key, g => g.Last(), StringComparer.OrdinalIgnoreCase);

If you prefer a non-LINQ solution then you could do something like this:

// Use the first value in list
var _people = new Dictionary<string, Person>(StringComparer.OrdinalIgnoreCase);
foreach (var p in personList)
{
    if (!_people.ContainsKey(p.FirstandLastName))
        _people[p.FirstandLastName] = p;
}

// Use the last value in list
var _people = new Dictionary<string, Person>(StringComparer.OrdinalIgnoreCase);
foreach (var p in personList)
{
    _people[p.FirstandLastName] = p;
}
Ryan
  • 7,835
  • 2
  • 29
  • 36
LukeH
  • 263,068
  • 57
  • 365
  • 409
  • 6
    @LukeH Minor note: your two snippets are not equivalent: the LINQ variant retains the first element, the non-LINQ snippet retains the last element ? – toong Sep 25 '13 at 14:09
  • 4
    @toong: That's true and definitely worth noting. (Although in this case the OP doesn't seem to care which element they end up with.) – LukeH Sep 25 '13 at 17:01
  • 1
    For "the first value" case: nonLinq solution does dictionary lookup twice but Linq does redundant object instantiations and iterating. Both are not ideal. – SerG Sep 08 '17 at 17:03
  • @SerG Thankfully dictionary lookup is generally considered an O(1) operation and has a negligible impact. – MHollis Jan 10 '18 at 19:51
87

Here's the obvious, non linq solution:

foreach(var person in personList)
{
  if(!myDictionary.ContainsKey(person.FirstAndLastName))
    myDictionary.Add(person.FirstAndLastName, person);
}

If you don't mind always getting the last one added, you can avoid the double lookup like this:

foreach(var person in personList)
{
    myDictionary[person.FirstAndLastName] = person;
}
Charlieface
  • 52,284
  • 6
  • 19
  • 43
Carra
  • 17,808
  • 7
  • 62
  • 75
  • Yeah, about time we update from the .net 2.0 framework at work... @onof Not exactly hard to ignore case. Just add all keys in uppercase. – Carra Jul 23 '10 at 14:42
  • how would i make this case insensitive – leora Jul 23 '10 at 15:04
  • 12
    Or create the dictionary with a StringComparer that will ignore the case, if thats what you need, then your adding/checking code doesn't care if you're ignoring case or not. – Binary Worrier Jul 23 '10 at 15:06
  • To answer the previous comments, some 10 years after the fact... To ignore case, we should use `Dictionary.ContainsKey`, incomparably faster than `Dictionary.Keys.Contains` (O(1) instead of O(N)), and use a case insensitive dictionary. – Alberto Chiesa Oct 01 '20 at 14:43
  • @A.Chiesa that's not true. Both calls are similar. Both are O(1). Latter calls the former internally. – nawfal Apr 20 '22 at 00:39
  • 1
    @nawfal You're right: the framework protects you calling the Contains method of the KeyCollection, which calls ContainsKey. I _think_ I prefer the more explicit `ContainsKey`, but now I know I should not freak out if I see a `Contains` in the wild. Thanks! – Alberto Chiesa Apr 20 '22 at 06:19
48

A Linq-solution using Distinct() and and no grouping is:

var _people = personList
    .Select(item => new { Key = item.Key, FirstAndLastName = item.FirstAndLastName })
    .Distinct()
    .ToDictionary(item => item.Key, item => item.FirstFirstAndLastName, StringComparer.OrdinalIgnoreCase);

I don't know if it is nicer than LukeH's solution but it works as well.

Tillito
  • 7,718
  • 7
  • 34
  • 31
  • Are you sure that works? How is Distinct going to compare the new reference type you create? I would think you would need to pass some kind of IEqualityComparer into Distinct to get this work as intended. – Simon Gillbee Jul 25 '14 at 15:28
  • 6
    Ignore my previous comment. See http://stackoverflow.com/questions/543482/linq-select-distinct-with-anonymous-types – Simon Gillbee Jul 25 '14 at 15:29
  • If you want to override how distinct is determined check out http://stackoverflow.com/questions/489258/linqs-distinct-on-a-particular-property – James McMahon May 19 '16 at 22:21
30

This should work with lambda expression:

personList.Distinct().ToDictionary(i => i.FirstandLastName, i => i);
Luc Wollants
  • 880
  • 9
  • 27
Ankit Dass
  • 521
  • 5
  • 7
  • 2
    It must be: `personList.Distinct().ToDictionary(i => i.FirstandLastName, i => i);` – Gh61 Aug 08 '14 at 08:08
  • 9
    This will only work if the default IEqualityComparer for the Person class compares by first and last name, ignoring case. Otherwise write such an IEqualityComparer and use the relevant Distinct overload. Also your ToDIctionary method should take a case-insensitive comparer to match the OP's requirement. – Joe Nov 23 '16 at 12:54
20

You can create an extension method similar to ToDictionary() with the difference being that it allows duplicates. Something like:

    public static Dictionary<TKey, TElement> SafeToDictionary<TSource, TKey, TElement>(
        this IEnumerable<TSource> source, 
        Func<TSource, TKey> keySelector, 
        Func<TSource, TElement> elementSelector, 
        IEqualityComparer<TKey> comparer = null)
    {
        var dictionary = new Dictionary<TKey, TElement>(comparer);

        if (source == null)
        {
            return dictionary;
        }

        foreach (TSource element in source)
        {
            dictionary[keySelector(element)] = elementSelector(element);
        }

        return dictionary; 
    }

In this case, if there are duplicates, then the last value wins.

Eric
  • 1,945
  • 3
  • 23
  • 33
  • 2
    I cannot believe this answer is not the most voted. `ToLookup`, `Distinct` etc. have all dire performance implications. This is, AFAICT, the only option that performs roughly at the same speed as the original `ToDictionary` – Alberto Chiesa Oct 01 '20 at 14:34
  • Exactly. Other answers do an extra iteration + extra memory allocation – Alex from Jitbit Mar 10 '21 at 08:49
17

You can also use the ToLookup LINQ function, which you then can use almost interchangeably with a Dictionary.

_people = personList
    .ToLookup(e => e.FirstandLastName, StringComparer.OrdinalIgnoreCase);
_people.ToDictionary(kl => kl.Key, kl => kl.First()); // Potentially unnecessary

This will essentially do the GroupBy in LukeH's answer, but will give the hashing that a Dictionary provides. So, you probably don't need to convert it to a Dictionary, but just use the LINQ First function whenever you need to access the value for the key.

Community
  • 1
  • 1
palswim
  • 11,856
  • 6
  • 53
  • 77
7

To handle eliminating duplicates, implement an IEqualityComparer<Person> that can be used in the Distinct() method, and then getting your dictionary will be easy. Given:

class PersonComparer : IEqualityComparer<Person>
{
    public bool Equals(Person x, Person y)
    {
        return x.FirstAndLastName.Equals(y.FirstAndLastName, StringComparison.OrdinalIgnoreCase);
    }

    public int GetHashCode(Person obj)
    {
        return obj.FirstAndLastName.ToUpper().GetHashCode();
    }
}

class Person
{
    public string FirstAndLastName { get; set; }
}

Get your dictionary:

List<Person> people = new List<Person>()
{
    new Person() { FirstAndLastName = "Bob Sanders" },
    new Person() { FirstAndLastName = "Bob Sanders" },
    new Person() { FirstAndLastName = "Jane Thomas" }
};

Dictionary<string, Person> dictionary =
    people.Distinct(new PersonComparer()).ToDictionary(p => p.FirstAndLastName, p => p);
Anthony Pegram
  • 123,721
  • 27
  • 225
  • 246
4

In case we want all the Person (instead of only one Person) in the returning dictionary, we could:

var _people = personList
    .GroupBy(p => p.FirstandLastName)
    .ToDictionary(g => g.Key, g => g.Select(x=>x));
KyleMit
  • 30,350
  • 66
  • 462
  • 664
Shane Lu
  • 1,056
  • 1
  • 12
  • 21
  • 2
    Sorry, ignore my review-edit (I can't find where to delete my review-edit). I just wanted to add a suggestion about using g.First() instead of g.Select(x => x). – Alex 75 Jun 04 '19 at 07:38
  • How about 'StringComparer.OrdinalIgnoreCase'? GroupBy in your sample ignores it. – Valentyn Zakharenko Nov 19 '21 at 17:50
3

The issue with most of the other answers is that they use Distinct, GroupBy or ToLookup, which creates an extra Dictionary under the hood. Equally ToUpper creates extra string. This is what I did, which is an almost an exact copy of Microsoft's code except for one change:

    public static Dictionary<TKey, TSource> ToDictionaryIgnoreDup<TSource, TKey>
        (this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, IEqualityComparer<TKey> comparer = null) =>
        source.ToDictionaryIgnoreDup(keySelector, i => i, comparer);

    public static Dictionary<TKey, TElement> ToDictionaryIgnoreDup<TSource, TKey, TElement>
        (this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TSource, TElement> elementSelector, IEqualityComparer<TKey> comparer = null)
    {
        if (keySelector == null)
            throw new ArgumentNullException(nameof(keySelector));
        if (elementSelector == null)
            throw new ArgumentNullException(nameof(elementSelector));
        var d = new Dictionary<TKey, TElement>(comparer ?? EqualityComparer<TKey>.Default);
        foreach (var element in source)
            d[keySelector(element)] = elementSelector(element);
        return d;
    }

Because a set on the indexer causes it to add the key, it will not throw, and will also do only one key lookup. You can also give it an IEqualityComparer, for example StringComparer.OrdinalIgnoreCase

Charlie
  • 31
  • 1
2
        DataTable DT = new DataTable();
        DT.Columns.Add("first", typeof(string));
        DT.Columns.Add("second", typeof(string));

        DT.Rows.Add("ss", "test1");
        DT.Rows.Add("sss", "test2");
        DT.Rows.Add("sys", "test3");
        DT.Rows.Add("ss", "test4");
        DT.Rows.Add("ss", "test5");
        DT.Rows.Add("sts", "test6");

        var dr = DT.AsEnumerable().GroupBy(S => S.Field<string>("first")).Select(S => S.First()).
            Select(S => new KeyValuePair<string, string>(S.Field<string>("first"), S.Field<string>("second"))).
           ToDictionary(S => S.Key, T => T.Value);

        foreach (var item in dr)
        {
            Console.WriteLine(item.Key + "-" + item.Value);
        }
King
  • 179
  • 5
  • I suggest you to improve your example by reading the [Minimal, Complete and verifiable example](http://stackoverflow.com/help/mcve). – IlGala Apr 27 '16 at 12:36
2

Using LINQ's equivalent of foldLeft functionality

persons.Aggregate(new Dictionary<string,Person>(StringComparer.OrdinalIgnoreCase),
  (acc, current) => { 
    acc[current.FirstAndLastName] = current; 
    return acc; 
  }); 
nazgul
  • 449
  • 1
  • 4
  • 10
0

Starting from Carra's solution you can also write it as:

foreach(var person in personList.Where(el => !myDictionary.ContainsKey(el.FirstAndLastName)))
{
    myDictionary.Add(person.FirstAndLastName, person);
}
Cinquo
  • 633
  • 1
  • 6
  • 17
  • 3
    Not that anyone would ever attempt to use this, but don't attempt to use this. Modifying collections as you are iterating them is a bad idea. – kidmosey Jan 11 '18 at 13:28
0

You can use DistinctBy in .NET 6 and above:

_people = personList.DistinctBy(x => x.FirstandLastName).ToDictionary(
    e => e.FirstandLastName,
    StringComparer.OrdinalIgnoreCase);
vahid tajari
  • 1,163
  • 10
  • 20