-1

I want a way to access a value in a nested dictionary using the full path for that value as the string keys concatenateds.

public Dictionary<string, object> info =
    new Dictionary<string, object>
    {
        {
            "Gen",
            new Dictionary<string, string>
            {
                {"name", "Genesis"},
                {"chapters", "50"},
                {"before", ""},
                {"after", "Exod"}
            }
        },
        {
            "Exod",
            new Dictionary<string, string>
            {
                {"name", "Exodus"},
                {"chapters", "40"},
                {"before", "Gen"},
                {"after", "Lev"}
            }
        }
    };

string some1 = info["gen"]["name"]; // to get "Genesis"
string some2 = info["gen, name"]; //not work, but i would need some similar

Currently, I can just access a nested dictionary throw a lot of brackelets, like on some1, but since I can get the full path as a string (and easy get as a array) I really want a way to do as the some2, making my code more dynamic.

  • Have you considered making a method that takes in your dictionary structure and keys? `GetFoo(info, ["gen", "name"])` – schil227 Aug 14 '19 at 19:06
  • 2
    Is there any reason you're using a nested dictionary instead of a custom class? – Rufus L Aug 14 '19 at 19:09
  • @RufusL Because my structure can be much more complex and is always mutable, hardly I gonna have two structure with the same format. – André Smaniotto Aug 14 '19 at 19:25
  • @schil227 On my example I am getting the data, but I also need make an attribution, like `info["gen, name"] = "Genebra"` – André Smaniotto Aug 14 '19 at 19:33
  • 1
    The general answer is, "no, you can't do that". – Rufus L Aug 14 '19 at 19:37
  • are you ultimately trying to basically compose and inject a string (`"gen, name"`) into the dictionary to get/set values? if that's the case, your best bet is to create getter/setter methods to alter your info that knows to parse the input string properly, though I suspect there are better ways of doing this. – schil227 Aug 14 '19 at 19:59

1 Answers1

1

I'm out of square brackets, but I do have some extensions methods if it's of any interest.

They can traverse endlessly and get an object or set a generic value to the end of a key chain of dictionary. The Get() returns an object, but that's easily fixable to a generic type if necessary.

Since the keys are based on params you could throw in a dynamic arrays of keys if you have a very dynamic environment.

Works like this:

    var exists1 = info.Exists("Gen", "name", "One key too much"); // false
    var exists2 = info.Exists("Gen", "chapters"); // true

    var value1 = info.Get("Gen", "name", "One key too much"); // null
    var value2 = info.Get("Gen", "chapters"); // "50"

    var valueToSet = "myNewValue";

    var successSet1 = info.TrySet(valueToSet, "Gen", "before", "One key too much"); // false
    var successSet2 = info.TrySet(valueToSet, "Gen", "after"); // true | "Gen" -> "after" set to "myNewValue"

    var dictionary1 = info.FindSubDictionary("Gen", "name", "One key too much"); // null
    var dictionary2 = info.FindSubDictionary("Gen", "chapters"); // "Gen" sub dictionary

I haven't tested it much, nor given much thought into what it should return when, but if you find it useful, it could be something to start to tinker around with.

4 extension methods mostly working together, offering Exists, Get, TrySet and (what should be) an internal one to resolve the dictionary tree.

TrySet:

/// <summary>
/// Tries to set a value to any dictionary found at the end of the params keys, or returns false
/// </summary>
public static bool TrySet<T>(this System.Collections.IDictionary dictionary, T value, params string[] keys)
{
    // Get the deepest sub dictionary, set if available
    var subDictionary = dictionary.FindSubDictionary(keys);
    if (subDictionary == null) return false;

    subDictionary[keys.Last()] = value;
    return true;
}

Get:

/// <summary>
/// Returns a value from the last key, assuming there is a dictionary available for every key but last
/// </summary>
public static object Get(this System.Collections.IDictionary dictionary, params string[] keys)
{
    var subDictionary = dictionary.FindSubDictionary(keys);
    if (subDictionary == null) return null; // Or throw

    return subDictionary[keys.Last()];
}

Exists:

/// <summary>
/// Returns whether the param list of keys has dictionaries all the way down to the final key
/// </summary>
public static bool Exists(this System.Collections.IDictionary dictionary, params string[] keys)
{
    // If we have no keys, we have traversed all the keys, and should have dictionaries all the way down.
    // (needs a fix for initial empty key params though)
    if (keys.Count() == 0) return true;

    // If the dictionary contains the first key in the param list, and the value is another dictionary, 
    // return that dictionary with first key removed (recursing down)
    if (dictionary.Contains(keys.First()) && dictionary[keys.First()] is System.Collections.IDictionary subDictionary)
        return subDictionary.Exists(keys.Skip(1).ToArray());

    // If we didn't have a dictionary, but we have multiple keys left, there are not enough dictionaries for all keys
    if (keys.Count() > 1) return false; 

    // If we get here, we have 1 key, and we have a dictionary, we simply check whether the last value exists,
    // thus completing our recursion
    return dictionary.Contains(keys.First());
}

FindSubDictionary:

/// <summary>
/// Returns the possible dictionary that exists for all keys but last. (should eventually be set to private)
/// </summary>
public static System.Collections.IDictionary FindSubDictionary(this System.Collections.IDictionary dictionary, params string[] keys)
{
    // If it doesn't exist, don't bother
    if (!dictionary.Exists(keys)) return null; // Or throw

    // If we reached end of keys, or got 0 keys, return
    if (keys.Count() == 0) return null; // Or throw

    // Look in the current dictionary if the first key is another dictionary.
    return dictionary[keys.First()] is System.Collections.IDictionary subDictionary
        ? subDictionary.FindSubDictionary(keys.Skip(1).ToArray()) // If it is, follow the subdictionary down after removing the key just used
        : keys.Count() == 1 // If we only have one key remaining, the last key should be for a value in the current dictionary. 
            ? dictionary // Return the current dictionary as it's the proper last one
            : null; // (or throw). If we didn't find a dictionary and we have remaining keys, the dictionary tree is invalid
}

Edit: Noticed you wanted CSV-keys. It should be easy to just construct from above, but I made a quick example using a single csv-key with a bonus optional custom separator.

    var csvResult1 = info.Get("Gen, chapters, One key too much"); // null
    var csvResult2 = info.Get("Gen, chapters"); // "50"

    // With custom separator
    var csvResult3 = info.Get("Gen;chapters;One key too much", separator: ";"); 
    var csvResult4 = info.Get("Gen; chapters", separator: ";"); 

Code:

/// <summary>
/// Returns a value from the last key of a csv string with keys, assuming there is a dictionary available for every key but the last.
/// </summary>
public static object Get(this System.Collections.IDictionary dictionary, string csvKeys, string separator = ",")
{
    if (String.IsNullOrEmpty(csvKeys)) throw new ArgumentNullException("Csv key input parameters is not allowed to be null or empty", nameof(csvKeys));

    var keys = csvKeys.Split(separator).Select(k => k.Trim());
    var subDictionary = dictionary.FindSubDictionary(keys.ToArray());
    if (subDictionary == null) return null; // Or throw

    return subDictionary[keys.Last()];
}

You better test that last one though, I didn't run it. Should run fine though... I just can't give any guarantees it will :)

Classe
  • 256
  • 1
  • 6
  • I was wishing something more simple, but that must solve. Thanks a lot! – André Smaniotto Aug 15 '19 at 14:05
  • It did get a bit messy, three of the methods are really just helpers to illustrate usage. It really just boils down to the dictionary recursion which could be simplified to just this: `if (!keys.Any()) return current; return current.Contains(keys[0]) && current[keys[0]] is IDictionary child ? child.FindDictionary(keys.Skip(1).ToArray()) : null;`. Pretty much just a matter of preference how to use it in the end :) I do recommend changing it up to better suit your exact need. – Classe Aug 15 '19 at 18:22