Click here to Skip to main content
Click here to Skip to main content

Tree Structured Enumerations

By , 2 Apr 2008
 

Introduction

In this article, I want to show you an approach of how a structured enumeration can be handled by C#.

Background

While playing with my little home project, I stumbled upon a problem of having all those category enumerations displayed in a tree. I wanted to keep it simple - as the enumerations are - while avoiding the need to create structured object hierarchies for every one of them. So, after a bit of thinking, I came up with this solution. I hope you'll find it useful or at least interesting.

Structured Enumeration

First, we have to tickle our old plain list enumeration a bit and convert it to a structured one. I chose the animal categories.. well.. to confess, I'm always having slight difficulties to find a good example, but here it is anyway.

[Structured(100)]
public enum AnimalKind
{
    Unknown         = 00000,

    DomesticAnimals = 00001,
      Dog           = 00100,
        Dalmatin    = 10000,
        Greyhound   = 10001,
        Malamute    = 10002,
        Terrier     = 10003,
      Cat           = 00101,
        
    WildAnimals     = 00002,
      Ape           = 00200,
        Chimpanzee  = 20000,
        Gorrila     = 20001,
        Orangutan   = 20002,
      Deer          = 00201
}

As you might notice, some unknown attribute is used there. Let me introduce it to you.

[AttributeUsage(AttributeTargets.Enum)]
public class StructuredAttribute : Attribute
{
    public int span;

    public int Span
    {
        get { return span; }
        set { span = value; }
    }

    public StructuredAttribute(int span)
    {
        this.span = span;
    }
}

This simple attribute is composed of only one property (an automatic property may be used in C# 3.0, God I love those). The attribute is responsible for determining the span (multiplicator) of the tree levels thus allowing us to distinguish the hierarchy later.

Well.. later is now because this ought to be a short article. I took the liberty to create the utility class to help us deal with the structured enumerations. It consists of two static methods. Let's take a closer look at them.

Methods

The IsChild method is used to determine if one enumeration value is placed under another one. I guess the utility can be easily extended by a method determining the whole chain of the parents from a particular enum value.

public static bool IsChild<ttype>(TType child, TType parent)
{
    Type enumType = typeof(TType);
    Object[] attributeList = enumType.GetCustomAttributes
        (typeof(StructuredAttribute), true);

    if (attributeList.Length > 0)
    {
        StructuredAttribute attribute = (StructuredAttribute)attributeList[0];
        int span = attribute.Span;
        int parentIndex = (int)Convert.ChangeType(parent, typeof(int));
        int childIndex = (int)Convert.ChangeType(child, typeof(int));
        int index = childIndex / span;
        return (index == parentIndex && childIndex != parentIndex);
    }

    return true;
}

Another useful method that may be of interest to us is the CreateList method. It obviously creates a list of the child enumeration values under a particular parental value. It will also allow us to use the output list for the display or various cycle purposes. I can imagine an iterator here.

public static List<ttype> CreateList<ttype>(TType parent)
{
    List<ttype> result = new List<ttype>();
    Type enumType = typeof (TType);
    TType[] enumValues = (TType[]) Enum.GetValues(enumType);
    
    foreach (TType enumValue in enumValues)
    {
        if (IsChild(enumValue, parent))
        {
            result.Add(enumValue);
        }
    }

    return result;
}

Some Examples of Use

This example took all I mentioned above and put it to use. It will dump the tree to a console output while making the levels indented.

private static void DumpTree(AnimalKind parent, Int32 level)
{
    foreach (AnimalKind animalKind in EnumUtility.CreateList(parent))
    {
        string caption = animalKind.ToString();
        int width = caption.Length;
        string output = caption.PadLeft(width + level, ' ');
        Console.WriteLine(output);
        DumpTree(animalKind, level + 1);
    }
}

static void Main()
{
    DumpTree(AnimalKind.Unknown, 0);
    Console.ReadKey();
}

Possible Enhancements

  1. As I said earlier in this article, I can imagine some kind of iterator (or perhaps an indexer) instead of the CreateList method.
  2. The default indexing capabilities of enumerations can be widened by "inheriting" the enumeration from ulong type instead of default uint.
  3. The utility can also be extended with any kind of structuring routine which suits your need such as retrieving the chain of parents for a particular value.

Limitations

  • The complex trees with many levels may find their limit because the indexing will reach the limit of enumeration (ulong). This limitation can be reduced by lowering the span value on the attribute, thus allowing to scale for the count of branches against the count of levels.
  • It is recommended to use some default (zero) value which will then be used to retrieve the level one branches.

Personal Note

I found these structured enumerations quite useful myself dealing with countless - now waiting to be structured - category enumerations. They cut the time needed to create the editable trees where the categories are distinguished from the instance items. Moreover, the enumeration is still one type in the end.

Good luck and I will be pleased to hear your comments.

History

  • 2008-04-02: Missing example was added (Shall I ever get it right the first time?)
  • 2008-04-02: Initial article posted

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Smart K8
Software Developer TelPro spol. s r.o.
Czech Republic Czech Republic
Member
Contacts: EMAIL - smartk8@gmail.com

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
GeneralRe: Suggestions Part 2memberPIEBALDconsult7 Apr '08 - 13:42 
Smart K8 wrote:
I've just stripped away all the code non-concerned in the article

 
And didn't test it?
 

 
I meant, in the line:
 
public static bool IsChild<ttype>(TType child, TType parent)
 
where is ttype used? Is it supposed to be TType and you didn't capitalize it properly?
 

 

Smart K8 wrote:
There's no constraint for Enum

 
Yeah, and there should be. Stupid Microsoft Mad | :mad:
 

Anyway, I spent another whole day today tweaking my implementation of this.
GeneralRe: Suggestions Part 2memberSmart K87 Apr '08 - 20:43 
PIEBALDconsult wrote:
Smart K8 wrote:
I've just stripped away all the code non-concerned in the article

 
And didn't test it?

 
Sure I did.
 
PIEBALDconsult wrote:
I meant, in the line:
 
public static bool IsChild(TType child, TType parent)
 
where is ttype used? Is it supposed to be TType and you didn't capitalize it properly?

 
No, I'm just struggling with the codeproject's < and > a bit.
 
The wisdom is to see things truthfully.

GeneralToo complexmemberPIEBALDconsult2 Apr '08 - 10:54 
Why did you choose to shift the higher-order sub-values up?
 
I would leave them in place (at the high-end), leave low-order fields as 0 to indicate "class", and add (OR) detail as I go.
The "class" parts could also be at the low-end, but I think high-end is clearer.
(Hexadecimal literals would make it easier too. They following doesn't work correctly, but should give a decent conceptual view.)
 
    public enum AnimalKind
    {
        Unknown         = 00000,
        Class           = 90000,
        Subclass        = 90900,
 
        DomesticAnimals = 10000,
          Dog           = 10100,
            Dalmatin    = 10101,
            Greyhound   = 10102,
            Malamute    = 10103,
            Terrier     = 10104,
          Cat           = 10200,
 
        // another technique        
        WildAnimals     = 20000,
          Ape           = WildAnimals + 100,
            Chimpanzee  = 20101,
            Gorilla     = 20102,
            Orangutan   = 20103,
          Deer          = WildAnimals + 200
    }
 
That way bitwise operations can be used (I hope I get this right):
 
if ( ( AnimalKind.Greyhound & AnimalKind.Subclass ) == AnimalKind.Dog ) // Bark for me Rover

GeneralP.S. Too complexmemberPIEBALDconsult2 Apr '08 - 11:09 
This is what I mean:
 
namespace Template
{
    public partial class Template
    {
        public enum AnimalKind
        {
            Unknown         = 0x000000,
            Class           = 0xFF0000,
            Subclass        = 0xFFFF00,
 
            DomesticAnimals = 0x010000,
              Dog           = 0x010100,
                Dalmatin    = 0x010101,
                Greyhound   = 0x010102,
                Malamute    = 0x010103,
                Terrier     = 0x010104,
              Cat           = 0x010200,
 
            // another technique        
            WildAnimals     = 0x020000,
              Ape           = WildAnimals | 0x0100,
                Chimpanzee  = Ape | 0x01,
                Gorilla     = Ape | 0x02,
                Orangutan   = Ape | 0x03,
              Deer          = WildAnimals | 0x0200
        }
 
        public static void
        Main
        (
            string[] args
        )
        {
            AnimalKind a = AnimalKind.Unknown ;
 
            if ( ( args.Length > 0 ) && System.Enum.IsDefined ( typeof(AnimalKind) , args [ 0 ] ) )
            {
                a = (AnimalKind) System.Enum.Parse ( typeof(AnimalKind) , args [ 0 ] ) ;
            }
 
            System.Console.WriteLine
            (
                "{0} {1:X06} {2} {3}"
            ,
                a
            ,
                (int) a
            ,
                a & AnimalKind.Class
            ,
                a & AnimalKind.Subclass
            ) ;
 
            switch ( a & AnimalKind.Subclass )
            {
                case AnimalKind.Dog :
                    System.Console.WriteLine ( "Arf" ) ;
                    break ;
 
                case AnimalKind.Cat :
                    System.Console.WriteLine ( "Meow" ) ;
                    break ;
            }
        }
    }
}

GeneralRe: P.S. Too complexmemberSmart K82 Apr '08 - 11:31 
Hi.. What you are describing is simple the [Flags] attribute.
 
The point is in the combination of scalability (branch count/level count) and the ability to list all the children because of that simple fact. Consider this question:
 
How do you construct a tree with three (or more) levels (A->AB->ABC) and then reconstruct it again to List<TEnumType> GetList<TEnumType>(TEnumType Parent) ?
 
// Why did you choose to shift the higher-order sub-values up?
 
Because of the scalability. It seems more natural that a tree starts small. Check this rephrase:
 
[Structured(100)]
public enum AnimalKind
{
    Unknown         = 00000,
 
    DomesticAnimals = 1,
      Dog           = 100,
        Dalmatin    = 10000,
        Greyhound   = 10001,
        Malamute    = 10002,
        Terrier     = 10003,
      Cat           = 101,
        
    WildAnimals     = 2,
      Ape           = 200,
        Chimpanzee  = 20000,
        Gorrila     = 20001,
        Orangutan   = 20002,
      Deer          = 201
}
 
Do you see the logic now ?
 
regards,
Kate
 
The wisdom is to see things truthfully.
 
The wisdom is to see things truthfully.

GeneralRe: P.S. Too complexmemberPIEBALDconsult2 Apr '08 - 11:35 
Smart K8 wrote:
What you are describing is simple the [Flags] attribute.

 
Not at all.
 

Smart K8 wrote:
Do you see the logic now ?

 
Just as I saw it before, but I don't agree that it's the best course of action.
GeneralRe: P.S. Too complexmemberSmart K82 Apr '08 - 11:44 
Sorry, I got confused with another thread.. I guess I'm getting tired.. It's 23:37 here. I'm going to sleep. I checked your solution and this is ofcourse possible but consider the scenario where the Span (as I call it) won't be 0xFF but something lower than that (for example 0xC) because the complexity of the tree won't be sufficient to hold the number anymore.
 
Class             = 0xFF000000,
Subclass          = 0xFFFF0000,
SubSubClass       = 0xFFFFFF00,
SubSubSubClass    = 0xFFFFFFFF,
SubSubSubSubClass = Hello problems!
 
That's where the (for example) [Structured(0xC)] takes control and also when Natural numbers looks a bit more normal.
 
regards,
Kate
 
The wisdom is to see things truthfully.

GeneralRe: P.S. Too complexmemberPIEBALDconsult2 Apr '08 - 12:42 
Smart K8 wrote:
consider the scenario where the Span...

 
I was simply following your lead of using two digits. But what if you want different spans at different levels? For instance one-digit at the top-most level and two-digits at the next level down?
 
And, no, I haven't worked that out yet either, but I expect yout Attribute-based solution will be the way to go.
 
I also acknowledge that you were after a solution that wasn't tied to a particular enum. We'll get there, baby steps.
 

Smart K8 wrote:
Natural numbers looks a bit more normal

 
You're saying my numbers are unnatural? Big Grin | :-D
 
Well, the great whole purpose of enums is to hide the actual values anyway.
AnswerRe: P.S. Too complex [modified]memberSmart K82 Apr '08 - 19:29 
You're saying my numbers are unnatural?

 
IMHO.. in the end it is not important if you choose decimals, hexadecimals or any other representation of the value. I guess it's up to reader to make it suitable. I just thought that using decimals numbers as an example will be understable to a more general public. Smile | :)
 
And, no, I haven't worked that out yet either, but I expect yout Attribute-based solution will be the way to go.
 
I also acknowledge that you were after a solution that wasn't tied to a particular enum.

 
Exactly, prior to posting the article (or even thinking about the solution itself) I was considering all the ways possible to achieve that with either standard enumerations or any other easy way. It was expected that the consensus that this solution is not reinventing the wheel (as leppie pointed out) is first to be made.
 
The Structured attribute is there only to normalize all the possible granulations of enumerations so then one method can be used to reconstruct the tree + some value is ofcourse in the methods themselves. Big Grin | :-D
 
We'll get there, baby steps.

 
Sure and the reasonable compromise of syntax is what I'm after too. I'm glad for every suggestion and I guess I'm willing to modify the article when the best solution is worked out.
 
regards,
Kate
 
The wisdom is to see things truthfully.
modified on Sunday, April 6, 2008 3:23 AM

GeneralRe: P.S. Too complexmemberPIEBALDconsult2 Apr '08 - 14:26 
And thinking bit-wise allows more granularity of the class sizes. Yours only allows 10, 100, 1000, etc. members per class; but mine allows 3, 7, 15, 31, etc.
GeneralReinventing the wheelmember leppie 2 Apr '08 - 10:17 
What you are looking for is bit masks, and that is built into enum in C# (and many other languages) already.
 
[edit] you article is by no means bad Smile | :) [edit]
 
xacc.ide - now with IronScheme support
IronScheme - 1.0 alpha 2 out now

AnswerRe: Reinventing the wheelmemberSmart K82 Apr '08 - 10:35 
Hi, I'm aware of the FlagsAttribute but if you give it another round I hope you'll notice the slight improvements which are important in this case.
 
regards,
Kate
GeneralRe: Reinventing the wheelmember leppie 2 Apr '08 - 11:10 
Sorry I dont see it Smile | :) You can do the same with enums. You dont even need the Flags attribute. Here is a number hierarchy:
<font color="Blue">enum</font> <font color="Teal">NumberClass</font>
<font color="DarkBlue">{</font>
  Complex <font color="DarkBlue">=</font> <font color="Red">1</font><font color="DarkBlue">,</font>
  Real <font color="DarkBlue">=</font> <font color="Red">2</font> <font color="DarkBlue">|</font> Complex<font color="DarkBlue">,</font>
  Rational <font color="DarkBlue">=</font> <font color="Red">4</font> <font color="DarkBlue">|</font> Real <font color="DarkBlue">,</font>
  BigInteger <font color="DarkBlue">=</font> <font color="Red">8</font> <font color="DarkBlue">|</font> Rational<font color="DarkBlue">,</font>
  Integer <font color="DarkBlue">=</font> <font color="Red">16</font> <font color="DarkBlue">|</font> BigInteger<font color="DarkBlue">,</font>
  NotANumber <font color="DarkBlue">=</font> <font color="Red">0</font>
<font color="DarkBlue">}</font>

 
xacc.ide - now with IronScheme support
IronScheme - 1.0 alpha 2 out now

GeneralRe: Reinventing the wheelmemberSmart K82 Apr '08 - 11:35 
Ok.. So how about a second branch in the same enum ?
 
regards,
Kate
 
The wisdom is to see things truthfully.

GeneralRe: Reinventing the wheelmemberpeterchen2 Apr '08 - 19:30 
enum
{
  Unknown = 0,
  
  Dalmatin = 1,
  Greyhound = 2,
  Malamute = 3,
  Terrier = 4,
 
  DogMask = 0x0F,  //  room for 15 dogs

  CatMask = 0xF0,  // ..and 15 cats
  DomesticMask = 0xFF,
 
  Chimp = 0x0100,
  Gorilla = 0x0200,
  Orange = 0x0400    // I :rose: orange
  ApeMask = 0x0F00,
  DeerMask = 0xF000,
  WildAnimalMask = 0xFF00, 
  
};
 
There are some advantages inyour solution, but for me not enough to warrant the unusual solution. It's pretty limited anyway (whether you use binary masking or some decimal grouping), binary is much more common and thus preferable in my book.
 
We are a big screwed up dysfunctional psychotic happy family - some more screwed up, others more happy, but everybody's psychotic joint venture definition of CP
blog: TDD - the Aha! | Linkify!| FoldWithUs! | sighist


GeneralRe: Reinventing the wheelmemberSmart K82 Apr '08 - 19:53 
Now we're getting somewhere..
 
DogMask = 0x0F,  //  room for 15 dogs  
CatMask = 0xF0,  // ..and 15 cats
 
This example - as you pointed out in comments - uses a span (or granularity.. used by PIEBALDconsult) of 16 thus allowing room for 15 items. Sure if you need to make another enumeration and you want to have more than 16 items you'll make span wider for example.
 
FieldPlants = 0x00FF, // room for 255 field plants
GardenPlants = 0xFF00, // ..and 255 garden plants
 
Now try to use ONE method to reconstruct the tree for both enumerations. Not possible ?
 
Whether you'll choose hexadecimals over decimals or high-ordering over low-ordering is not a concern of this article as much. Rather it is a normalized way to reconstruct a tree from the enumerations with different granularities.

regards,
Kate
 
The wisdom is to see things truthfully.

GeneralRe: Reinventing the wheelmemberSmart K82 Apr '08 - 21:22 
Ok, I took some time and tried your proposed solution the result tree for parent (DogMask) is as follows..
 
DogMask
  Dalmatin
  Greyhound
  Malamute
    Dalmatin (?)
    Greyhound (?)
  Terrier
 
and the correct result should be
 
DogMask 
  Dalmatin
  Greyhound
  Malamute
  Terrier
 
Also another enumeration reconstructed tree:
 
WildAnimalMask
  Chimp
  Gorilla
  Orange
  ApeMask
    Chimp
    Gorilla
    Orange
  DeerMask
 
and again the correct result should be:
 
WildAnimalMask
  ApeMask
  DeerMask

So tell me what is going on here, how do you propose to reconstruct a TREE for a particular enumeration value from your suggested enumeration ?
 
regards,
Kate
 
The wisdom is to see things truthfully.

GeneralRe: Reinventing the wheelmemberPIEBALDconsult5 Apr '08 - 17:17 
Smart K8 wrote:
Whether you'll choose hexadecimals over decimals or high-ordering over low-ordering is not a concern of this article as much. Rather it is a normalized way to reconstruct a tree from the enumerations with different granularities.

 
Yes, but it should also be fairly efficient and maintainable, and I suspect that having the class (or whatever) value shift around is the opposite of that.
 

Smart K8 wrote:
Now try to use ONE method to reconstruct the tree for both enumerations.

 
Now that it's the weekend I have more time to think about this. So what form should the tree take?
 
I guess before we address that; what functions should be available?
GeneralRe: Reinventing the wheelmemberSmart K85 Apr '08 - 21:38 
PIEBALDconsult wrote:
Smart K8 wrote:
Whether you'll choose hexadecimals over decimals or high-ordering over low-ordering is not a concern of this article as much. Rather it is a normalized way to reconstruct a tree from the enumerations with different granularities.

 
Yes, but it should also be fairly efficient and maintainable, and I suspect that having the class (or whatever) value shift around is the opposite of that.

 
You surely are right on the point of efficiency and maintainability. I was just answering to leppie who didn't get that this idea is not a reinventing the wheel and it is not possible to structure the tree using the bitwise masks this will only get you all the parents but not an immediate one.
 
Smart K8 wrote:
Now try to use ONE method to reconstruct the tree for both enumerations.

 
Now that it's the weekend I have more time to think about this. So what form should the tree take?
 
I guess before we address that; what functions should be available?

 
Basically you should have the EnumType.IsChild(EnumType parent) which will detect if a specified enumeration value is a child of a specific direct parent (I'll try to elaborate on the Extension methods in C# if there is a way to extend the enumerations via System.Enum but I guess it is not possible). The listing method is only the cream on the top of it. With the IsChild (or an alternative method) you should be able to reconstruct a tree (either visual one or non-visual one). That's the main point of the article. If there's better way of achieving that with enumerations in different hierarchy style (either low-ordering) it will be an improvement (and I'll be happy to hear about it).
 
regards,
Kate
 
The wisdom is to see things truthfully.

GeneralRe: Reinventing the wheelmemberPIEBALDconsult5 Apr '08 - 18:13 
Mask! Yeah, that was the term I needed!
GeneralRe: Reinventing the wheelmemberPIEBALDconsult6 Apr '08 - 17:22 
leppie wrote:
[edit] you article is by no means bad [edit]

 
I just wanted to second this. It's an interesting concept; it just needs a cleaner implementation.

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web01 | 2.6.130516.1 | Last Updated 2 Apr 2008
Article Copyright 2008 by Smart K8
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid