2
\$\begingroup\$

I'm writing a command-line utility and I need to find commands (and parameters) by name. The name can either be a full name like save or a shortcut s.

I thought I use a dictionary with an ISet key and a custom comparer. At first I had a list and searched for the name with LINQ but I'd like to have something more convenient. The performance doesn't matter - this time convenience goes first. There will be at most a few dozens of commands. I know I could use a string and map each name to the command but this isn't cool :-)


First, there is a NameSet that is the base class for concrete sets.

class NameSet : HashSet<string>
{
    protected NameSet(IEnumerable<string> keys, IEqualityComparer<string> keyComparer)
    : base(keys ?? throw new ArgumentNullException(nameof(keys)), keyComparer)
    { }
}

one with the suffix CI which stands for Case Insensitive (like the collation in Sql Server)

class NameSetCI : NameSet 
{
    private NameSetCI(IEnumerable<string> keys, IEqualityComparer<string> keyComparer)
    : base(keys, keyComparer) 
    {}

    public static NameSetCI Create(params string[] keys) => 
        new NameSetCI(keys, StringComparer.OrdinalIgnoreCase);
}

the other with the suffix CS which obviously stands for Case Sensitive.

class NameSetCS : NameSet
{
    private NameSetCS(IEnumerable<string> keys, IEqualityComparer<string> keyComparer)
    : base(keys, keyComparer)
    { }

    public static NameSetCS Create(params string[] keys) => 
        new NameSetCS(keys, StringComparer.Ordinal);
}

The comparer for this is very simple. It just looks if there is any overlapping set.

internal class SetComparer : IEqualityComparer<ISet<string>>
{
    public bool Equals(ISet<string> x, ISet<string> y) => x.Overlaps(y);

    public int GetHashCode(ISet<string> obj) => 0; // Force Equals.
}

With the hash code 0 it doesn't seem to be O(1) anymore but all the keys are in one place an the logic is just a single Overlaps method. LINQ wouldn't be faster anyway and it would mean a lot more work.


Example:

var dic = new Dictionary<NameSetCI, string>(new SetComparer());

dic.Add(NameSetCI.Create("foo", "bar"), "fb");
dic.Add(NameSetCI.Create("qux"), "q");
dic[NameSetCI.Create("baz")] = "b";
dic[NameSetCI.Create("bar")].Dump(); // fb
dic.Add(NameSetCI.Create("foo"), "f"); // bam!
\$\endgroup\$
3
\$\begingroup\$

You can just use a single class with a generic Create method which has constraints for IEqualityComparer<string>:

internal class NameSetGeneric : HashSet<string>
{
    private NameSetGeneric(IEnumerable<string> keys, IEqualityComparer<string> keyComparer)
        : base(keys ?? throw new ArgumentNullException(nameof(keys)), keyComparer)
    {
    }

    public static NameSetGeneric Create<T>(T comparer, params string[] keys)
        where T : IEqualityComparer<string> => new NameSetGeneric(keys, comparer);
}

You can even go further and make the whole class generic, but that's only if you want to work with different data types.

Example usage:

var dic = new Dictionary<NameSetGeneric, string>(new SetComparer());
dic.Add(NameSetGeneric.Create(StringComparer.OrdinalIgnoreCase, "foo", "bar"), "fb");
dic.Add(NameSetGeneric.Create(StringComparer.OrdinalIgnoreCase, "qux"), "q");
dic[NameSetGeneric.Create(StringComparer.OrdinalIgnoreCase, "baz")] = "b";
dic[NameSetGeneric.Create(StringComparer.OrdinalIgnoreCase, "bar")].Dump(); //fb
dic.Add(NameSetGeneric.Create(StringComparer.OrdinalIgnoreCase, "foo"), "f"); // bam!
\$\endgroup\$
5
  • \$\begingroup\$ True, there actually could be only one key-class but this has also one downside: you could add keys with different comparers. CI and CS. Having specific keys you can always be sure all of them work the same :) You're right about making the whole class generic. Reuseability for other types is also a good thing. Next time the names could be ints. \$\endgroup\$ – t3chb0t Feb 18 '17 at 17:48
  • \$\begingroup\$ Oh, I already spent all my votes... take this virtual +1 and I'll get back to you tommorow and give you a real one :-] \$\endgroup\$ – t3chb0t Feb 18 '17 at 17:48
  • \$\begingroup\$ True, didn't consider that, but it might even be a bonus as you can have all commands in a single dict. \$\endgroup\$ – Denis Feb 18 '17 at 17:59
  • \$\begingroup\$ This actually might even work. I think if I used a different hash code for each comparer then the Equals method should be called only for two CI keys or two CS keys but not for CI and CI. So after all only keys of the same comparer type would be compared. \$\endgroup\$ – t3chb0t Feb 18 '17 at 18:03
  • \$\begingroup\$ @t3chb0t Give it a try :) \$\endgroup\$ – Denis Feb 18 '17 at 18:17

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.