How to build a "defaulting map" data structure

EDIT: This code is apparently not what's required, but I'm leaving it as it's interesting anyway. It basically treats Key1 as taking priority, then Key2, then Key3 etc. I don't really understand the intended priority system yes, but when I do I'll add an answer for that.

I would suggest a triple layer of Dictionaries - each layer has:

Dictionary<int, NextLevel> matches;
NextLevel nonMatch;

So at the first level you'd look up Key1 - if that matches, that gives you the next level of lookup. Otherwise, use the next level which corresponds with "non-match".

Does that make any sense? Here's some sample code (including the example you gave). I'm not entirely happy with the actual implementation, but the idea behind the data structure is sound, I think:

using System;
using System.Collections;
using System.Collections.Generic;

public class Test
{
    static void Main()
    {
        Config config = new Config
        {
            { null,  null,  null,  1 },
            { 1,     null,  null,  2 },
            { 1,     null,  3,     3 },
            { null,  2,     3,     4 },
            { 1,     2,     3,     5 }
        };

        Console.WriteLine(config[1, 2, 3]);
        Console.WriteLine(config[3, 2, 3]);
        Console.WriteLine(config[9, 10, 11]);
        Console.WriteLine(config[1, 10, 11]);
    }
}

// Only implement IEnumerable to allow the collection initializer
// Not really implemented yet - consider how you might want to implement :)
public class Config : IEnumerable
{
    // Aargh - death by generics :)
    private readonly DefaultingMap<int, 
                         DefaultingMap<int, DefaultingMap<int, int>>> map
        = new DefaultingMap<int, DefaultingMap<int, DefaultingMap<int, int>>>();

    public int this[int key1, int key2, int key3]
    {
        get
        {
            return map[key1][key2][key3];
        }
    }

    public void Add(int? key1, int? key2, int? key3, int value)
    {
        map.GetOrAddNew(key1).GetOrAddNew(key2)[key3] = value;
    }

    public IEnumerator GetEnumerator()
    {
        throw new NotSupportedException();
    }
}

internal class DefaultingMap<TKey, TValue>
    where TKey : struct 
    where TValue : new()
{
    private readonly Dictionary<TKey, TValue> mapped = new Dictionary<TKey, TValue>();
    private TValue unmapped = new TValue();

    public TValue GetOrAddNew(TKey? key)
    {
        if (key == null)
        {
            return unmapped;
        }
        TValue ret;
        if (mapped.TryGetValue(key.Value, out ret))
        {
            return ret;
        }
        ret = new TValue();
        mapped[key.Value] = ret;
        return ret;
    }

    public TValue this[TKey key]
    {
        get
        {
            TValue ret;
            if (mapped.TryGetValue(key, out ret))
            {
                return ret;
            }
            return unmapped;
        }
    }

    public TValue this[TKey? key]
    {
        set
        {
            if (key != null)
            {
                mapped[key.Value] = value;
            }
            else
            {
                unmapped = value;
            }
        }
    }
}

To answer your question about something which is generic in the number and type of keys - you can't make the number and type of keys dynamic and use generics - generics are all about providing compile-time information. Of course, you could use ignore static typing and make it dynamic - let me know if you want me to implement that instead.

How many entries will there be, and how often do you need to look them up? You may well be best just keeping all the entries as a list and iterating through them giving a certain "score" to each match (and keeping the best match and its score as you go). Here's an implementation, including your test data - but this uses the keys having priorities (and then summing the matches), as per a previous comment...

using System;
using System.Collections;
using System.Collections.Generic;

public class Test
{
    static void Main()
    {
        Config config = new Config(10, 7, 5)
        {
            { new int?[]{null,  null,  null},  1},
            { new int?[]{1,     null,  null},  2},
            { new int?[]{9,     null,  null},  21},
            { new int?[]{1,     null,  3},     3 },
            { new int?[]{null,  2,     3},     4 },
            { new int?[]{1,     2,     3},     5 }
        };

        Console.WriteLine(config[1, 2, 3]);
        Console.WriteLine(config[3, 2, 3]);
        Console.WriteLine(config[8, 10, 11]);
        Console.WriteLine(config[1, 10, 11]);
        Console.WriteLine(config[9, 2, 3]);
        Console.WriteLine(config[9, 3, 3]);
    }
}

public class Config : IEnumerable
{
    private readonly int[] priorities;
    private readonly List<KeyValuePair<int?[],int>> entries = 
        new List<KeyValuePair<int?[], int>>();

    public Config(params int[] priorities)
    {
        // In production code, copy the array to prevent tampering
        this.priorities = priorities;
    }

    public int this[params int[] keys]
    {
        get
        {
            if (keys.Length != priorities.Length)
            {
                throw new ArgumentException("Invalid entry - wrong number of keys");
            }
            int bestValue = 0;
            int bestScore = -1;
            foreach (KeyValuePair<int?[], int> pair in entries)
            {
                int?[] key = pair.Key;
                int score = 0;
                for (int i=0; i < priorities.Length; i++)
                {
                    if (key[i]==null)
                    {
                        continue;
                    }
                    if (key[i].Value == keys[i])
                    {
                        score += priorities[i];
                    }
                    else
                    {
                        score = -1;
                        break;
                    }
                }
                if (score > bestScore)
                {
                    bestScore = score;
                    bestValue = pair.Value;
                }
            }
            return bestValue;
        }
    }

    public void Add(int?[] keys, int value)
    {
        if (keys.Length != priorities.Length)
        {
            throw new ArgumentException("Invalid entry - wrong number of keys");
        }
        // Again, copy the array in production code
        entries.Add(new KeyValuePair<int?[],int>(keys, value));
    }

    public IEnumerator GetEnumerator()
    {
        throw new NotSupportedException();
    }
}

The above allows a variable number of keys, but only ints (or null). To be honest the API is easier to use if you fix the number of keys...

Tags:

C#

.Net

Algorithm