18

I have a Dictionary<string, List<int>> in my code which I am using in the following manner:

Key           Values  
2011-07-15    1, 2, 3
2011-07-20    4, 5, 6
2010-02-11    7, 8, 9

My code needs to be able to query for all values matching a particular substring in the key. For example, if I had the substring 2011-07 it should return values {1, 2, 3, 4, 5, 6}. A substring of 11 should return all IDs from 1-9.

Can anyone recommend a concise way to achieve this? Or provide a better data structure for retrieving this information?

LeopardSkinPillBoxHat
  • 28,915
  • 15
  • 75
  • 111
  • I think the variety of answers below show that people are assuming different things for what "substring" means. I assume from your comment about 11 that you mean a truly general substring that is not necessarily a prefix, suffix, only a [year|month|day], and is not a regex? – J Trana Oct 19 '11 at 05:04
  • 1
    @J Trana - you are correct, I meant a truly general substring. – LeopardSkinPillBoxHat Oct 19 '11 at 05:10
  • @LeopardSkinPillBoxHat, could you please post what final solution that you went along with? – EndlessSpace Nov 15 '12 at 22:22
  • This feature should easily be implemented by SortedDictionary, but it still isn't. (?) Java has [NavigableMap](https://docs.oracle.com/javase/8/docs/api/java/util/NavigableMap.html). – Iain Mar 04 '16 at 07:00

5 Answers5

13

I would do an extension method :

public static class DictionaryExt
{
    public static IEnumerable<T> PartialMatch<T>(this Dictionary<string, T> dictionary, string partialKey)
    {
        // This, or use a RegEx or whatever.
        IEnumerable<string> fullMatchingKeys = 
            dictionary.Keys.Where(currentKey => currentKey.Contains(partialKey));

        List<T> returnedValues = new List<T>();

        foreach (string currentKey in fullMatchingKeys)
        {
            returnedValues.Add(dictionary[currentKey]);
        }

        return returnedValues;
    }
}

The "cost" of adding values to the dictionary wouldn't change, but the cost of retrieval would be higher, but only when you know you're going with a partial match.

Btw, I'm sure you could transform this in a single Lambda expression, but the concept remains the same.

Edit: In your example, this method would return 2 lists of values, but you can change it to merge the lists. Here is the extension method you could do :

public static IEnumerable<T> PartialMatch<T>(
    this Dictionary<string, IEnumerable<T>> dictionary,
    string partialKey)
{
    // This, or use a RegEx or whatever.
    IEnumerable<string> fullMatchingKeys = 
        dictionary.Keys.Where(currentKey => currentKey.Contains(partialKey));

    List<T> returnedValues = new List<T>();

    foreach (string currentKey in fullMatchingKeys)
    {
        returnedValues.AddRange(dictionary[currentKey]);
    }

    return returnedValues;
}

Edit 2: Come to think of it, you could also make it more generic. With the next extension method, it would work on any dictionary, as long as you provide a comparer that check what you mean by "partial match" :

public static IEnumerable<TValue> PartialMatch<TKey, TValue>(
    this Dictionary<TKey, IEnumerable<TValue>> dictionary,
    TKey partialKey,
    Func<TKey, TKey, bool> comparer)
{
    // This, or use a RegEx or whatever.
    IEnumerable<TKey> fullMatchingKeys = 
        dictionary.Keys.Where(currentKey => comparer(partialKey, currentKey));

    List<TValue> returnedValues = new List<TValue>();

    foreach (TKey currentKey in fullMatchingKeys)
    {
        returnedValues.AddRange(dictionary[currentKey]);
    }

    return returnedValues;
}
Tipx
  • 7,367
  • 4
  • 37
  • 59
  • With the second edit, provided you pass an appropriate comparer method, you could have a dictionary key type of int, and say that 43 partially matches 343756. – Tipx Oct 19 '11 at 04:56
6

You are looking for concise answers. Without fancy indexing at a low-level for text (of which I don't know of any specialized .Net classes), I think dictionary is still your best bet. Query with something like:

myDictionary.Where(kvp => kvp.Key.Contains("11")).SelectMany(kvp => kvp.Value);

You have to search through all keys for a generalized substring anyway without some pretty cool magic (not provided by .Net), so LINQ shouldn't hurt you much here.

LeopardSkinPillBoxHat
  • 28,915
  • 15
  • 75
  • 111
J Trana
  • 2,150
  • 2
  • 20
  • 32
2

If Dictionary uses internally hashes, you are out of luck, as similar strings yield dissimilar hashes. I just implemented solution to this requirement over the weekend in C, an interview test/homework. I used a sorted array as the underlying structure - expensive inserts, but fast lookups (using binary search). To find all entries with key starting with a prefix, I would find the 1st, then just go next, next... For general substring, i.e. not only prefix, my solution would not work. At this moment I do not know what to suggest for the "general substring" search.

Radim Cernej
  • 875
  • 1
  • 10
  • 21
2

You could have three dictionaries. Year, Month, Day.

Note that when you add items to three dictionaries, you are NOT duplicating the items.

When you pull items out using two keys, you could use the LINQ Extension method Intersect() to get the items that match both keys (Use Intersect on the two result sets).

Caveat, doing it this way would not result in the fastest executing code.

Steve Wellens
  • 20,506
  • 2
  • 28
  • 69
1

A concise way would be to use Multivalue Map.

For example:

Dictionary<string, Dictionary<string, List<int>>

why dont you store the 2011-07 as a key and 15 for the inner dictionary key and 1,2,3 as values.

map["2011-07"]["15"]= {1,2,3};

if you want just 2011-07 you can get everything within the other dictionary by traversal.

map["2011-07"] // would return u 1,2,3,4,5,6

and if you want to go to a specific day, 2011-07-15, this would return u only 1,2,3

foreach(var element in map["2011-07"]){

     var values = element.values; // and you can append them to a list.

}

if you will need year/month/day, you will need multilevel dictionaries. or you can use a Tree as well.

DarthVader
  • 52,984
  • 76
  • 209
  • 300