Tech Off Thread

22 posts

Forum Read Only

This forum has been made read only by the site admins. No new threads or comments can be added.

Enum ToString() with [System.Flags] Ordering and Oddness

Back to Forum: Tech Off
  • User profile image
    eddwo

    I have an Enum with the flags attribute set, like this:

            [System.Flags]
    public enum DaysOfWeekOutput : byte
    {
    MondayToSunday = MondayToFriday | Weekend,
    MondayToSaturday = MondayToFriday | Saturday,
    MondayToFriday = Monday | Tuesday | Wednesday | Thursday | Friday,
    Weekend = Saturday | Sunday,
    Sunday = 64,
    Saturday = 32,
    Monday = 1,
    Tuesday = 2,
    Wednesday = 4,
    Thursday = 8,
    Friday = 16
    }

    If I set the flags for Monday, Friday, Saturday and Sunday

    I expected the output of ToString() to be

           "Weekend, Monday, Friday"

    as I thought it would traverse the enum in the order specified and output the first name that matched the flags set.

    Instead it is "Monday, Friday, Weekend"

    Is there any way of altering the order of the enum names in the output of ToString()?

    Thats actually a simplified version of what I'm trying to do,
    The XML Schema that I'm working with specifies some inverse flags. So I actually have two enums.

            [System.Flags]
            public enum DaysOfWeekOperating : byte
            {
                MondayToSunday = MondayToFriday | Weekend,
                MondayToSaturday = MondayToFriday | Saturday,
                MondayToFriday = Monday | Tuesday | Wednesday | Thursday | Friday,
                Weekend = Saturday | Sunday,
                Sunday = 64,
                Saturday = 32,
                Monday = 1,
                Tuesday = 2,
                Wednesday = 4,
                Thursday = 8,
                Friday = 16,
                NotMonday = 0,
                NotTuesday = 0,
                NotWednesday = 0,
                NotThursday = 0,
                NotFriday = 0,
                NotSaturday = 0,
            }

    And

            [System.Flags]
            private enum DaysOfWeekNotOperating : byte
            {
                MondayToSunday = 0,
                MondayToSaturday = 0,
                MondayToFriday = 0,
                Weekend = 0,
                Sunday = 0,
                Saturday = 0,
                Monday = 0,
                Tuesday = 0,
                Wednesday = 0,
                Thursday = 0,
                Friday = 0,
                NotMonday = DaysOfWeekOperating.Monday,
                NotTuesday = DaysOfWeekOperating.Tuesday,
                NotWednesday = DaysOfWeekOperating.Wednesday,
                NotThursday = DaysOfWeekOperating.Thursday,
                NotFriday = DaysOfWeekOperating.Friday,
                NotSaturday = DaysOfWeekOperating.Saturday
            }

    If my input string is "MondayToSaturday, NotTuesday, NotThursday" I want to Enum.Parse() that string with both enum types and then subtract the flags set in the notoperating variable from the flags in the operating variable to get  a final value of:

    Monday |  Wednesday | Friday  | Saturday 

    The output of
        DaysOfWeekOperating days = Monday|Wednesday|Friday|Saturday
        days.ToString()

    is

    "NotTuesday, NotSaturday, NotWednesday, NotFriday, NotThursday, Monday, Wednesday, Friday, Saturday"

    Why does it include the NotXXX flags when their value is set to 0?

    I was hoping the Enum system was a good way of representing mutually exclusive relationships eg Weekend = Saturday | Sunday

    But it seems like I can't rely on the output of ToString() being what I expect.









  • User profile image
    eddwo

    Actually the schema is not as forgiving as that:


  • User profile image
    Sven Groot

    The runtime has no concept at all of what order the enum values where defined in, all it sees is the numeric order. It also doesn't actually know what components you used to build a composite value if there's more than one way, it just knows the current numeric value.

    What Enum.ToString does, basically, is this:
    for each member of the enum
       if (current value & member value) != 0 
          add string representation of member value to output
       endif
    endfor

    As you can see, that test will always equal true for members that are 0.

  • User profile image
    Maurits

    Hmmm... is it guaranteed that
        e.ToString() == f.ToString() if-and-only-if e == f
    even across runtime versions and locales, and even for ||-able enums?

  • User profile image
    Maurits

    Sven Groot wrote:
    As you can see, that test will always equal true for members that are 0.


    Unless (current value) == 0, of course Smiley


    Eh... (a & 0 != 0) is false for all values of a.

    It's odd that the OP's experience is at odds with this documentation.  Based on your algorithm the OP's experience I'd have expected different output.  In particular I'd have expected this
      0 - Black
    1 - Black, Red
    2 - Black, Green
    3 - Black, Red, Green
    4 - Black, Blue
    5 - Black, Red, Blue
    6 - Black, Green, Blue
    7 - Black, Red, Green, Blue
    8 - Black
    instead of this
      0 - Black
    1 - Red
    2 - Green
    3 - Red, Green
    4 - Blue
    5 - Red, Blue
    6 - Green, Blue
    7 - Red, Green, Blue
    8 - 8
    Note your algorithm implies this output:
      0 - 0
    1 - Red
    2 - Green
    3 - Red, Green
    4 - Blue
    5 - Red, Blue
    6 - Green, Blue
    7 - Red, Green, Blue
    8 - 8

  • User profile image
    AndyC

    Shouldn't NotMonday be defined as:

    NotMonday =  Tuesday | Wednesday | Thursday | Friday

    etc.

    that probably won't help with the parsing and printing, but it will work better than trying to use two different enums to represent the same data.

  • User profile image
    Maurits

    How about

    enum DaysOfWeek : byte {
        Sunday = 1b << 0;
        Monday = 1b << 1;
        Tuesday = 1b << 2;
        Wednesday = 1b << 3;
        Thursday = 1b << 4;
        Friday = 1b << 5;
        Saturday = 1b << 6;

        Weekend = Saturday | Sunday;
        Weekdays = Monday | Tuesday | ...;
        EveryDay = Weekend | Weekdays;
        EveryDayButMonday = EveryDay & ~Monday; // legal?
        ...
    }

  • User profile image
    Sven Groot

    Maurits wrote:
    Eh... (a & 0 != 0) is false for all values of a.

    That's just me screwing up writing the pseudo-code.

    In actuality the test is "if( (currentValue & values[x]) == values[x] )" and that will always equal true if values[x] is 0.

    Sorry for the confusion.

  • User profile image
    eddwo

    So maybe I can get my desired output ordering by choosing the flag values more carefully. What surprised me was that the ToString() didn't include NotMonday when it did include all the other 0 valued flags. I don't actually store a variable for both enumeration types, I just needed both to be defined in order to use Enum.Parse() to get me the aggregate flag set. Seems like Enums are a sensible thing to use to store this data, and do 80% of the job of parsing the input and creating the output, but I might need to tweak the results a bit.

  • User profile image
    Maurits

    Sven Groot wrote:
    In actuality the test is "if( (currentValue & values[x]) == values[x] )" and that will always equal true if values[x] is 0.


    ... and for multibit values[x] you avoid the overlap problem of 10 & 6 == 2 which is != 0 (incorrectly matches) but which is not == 6 (correctly doesn't match)

    I was just about to post that test but you beat me to it Smiley

    This still leaves open the question of why the code in the documentation doesn't print out Black for all values of the enumeration.  (Or maybe it does?  I haven't tested it.)

  • User profile image
    eddwo

    There must be a special case that rules out a 0 valued flag, but only one of them. Thats why Black doesn't show, and why my Enum doesn't produce NotMonday for every case, but does produce NotTuesday, NotWednesday, NotThursday etc. I think I know what I need to do to make it behave as I want.

  • User profile image
    Maurits

    eddwo wrote:
    There must be a special case that rules out a 0 valued flag, but only one of them.


    I believe you have found a bug Smiley

  • User profile image
    Sven Groot

    I don't have the time to look into this further, but this is the entire routine used by Enum.Format for flags (courtesy of reflector) (note: .Net 2.0):

    private static string InternalFlagsFormat(Type eT, object value)
    {
          ulong num1 = Enum.ToUInt64(value);
          Enum.HashEntry entry1 = Enum.GetHashEntry(eT);
          string[] textArray1 = entry1.names;
          ulong[] numArray1 = entry1.values;
          int num2 = numArray1.Length - 1;
          StringBuilder builder1 = new StringBuilder();
          bool flag1 = true;
          ulong num3 = num1;
          while (num2 >= 0)
          {
                if ((num2 == 0) && (numArray1[num2] == 0))
                {
                      break;
                }
                if ((num1 & numArray1[num2]) == numArray1[num2])
                {
                      num1 -= numArray1[num2];
                      if (!flag1)
                      {
                            builder1.Insert(0, ", ");
                      }
                      builder1.Insert(0, textArray1[num2]);
                      flag1 = false;
                }
                num2--;
          }
          if (num1 != 0)
          {
                return value.ToString();
          }
          if (num3 != 0)
          {
                return builder1.ToString();
          }
          if (numArray1[0] == 0)
          {
                return textArray1[0];
          }
          return "0";
    }
    

    It appears to be this bit:
                if ((num2 == 0) && (numArray1[num2] == 0))
                {
                      break;
                }
    That makes it so it doesn't include the first 0 value. Doesn't look like they were really counting on an enum with more than one 0-valued member.

  • User profile image
    eddwo

    Opps, sorry Sven, I shouldn't have spent so long putting in variable names and comments.

  • User profile image
    Maurits

    This is the bug, as Sven says:

    if ((flagno == 0) && (values[flagno] == 0))
    {
    break; //jump out if the first flag value is 0
    }

    IMHO, it should be this:
    if (values[flagno] == 0)
    {
    // skip this flag if its value is 0
    flagno--;
    continue;
    }

    Or perhaps a for loop is more appropriate, to make sure "continue;" does the right thing and decrements "flagno".

    EDIT: if the values[] are listed in nondecreasing order, a simple break; would suffice:


    if (values[flagno] == 0)
    {
    // we've reached the zero values; nothing more to see here
    break;
    }

  • User profile image
    eddwo

    It sorts the enum values lowest to highest, traverses them in reverse order so it matches the largest set of flags first, and then prepends the matched name to the string builder, hence "Monday, Friday, Weekend".

    So if I want Weekend to come out before Monday, Friday. I need to change the enum so that Saturday = 1, Sunday = 2 and Monday = 4 etc.

    That reflector thing is really cool!

  • User profile image
    Maurits

    Maurits wrote:
    Hmmm... is it guaranteed that
        e.ToString() == f.ToString() if-and-only-if e == f
    even across runtime versions and locales, and even for ||-able enums?


    I'm thinking this is a chimera and enums should never be compared as strings.  If they're saved as strings they should be reparsed before comparing them.

  • User profile image
    AndyC

    eddwo wrote:
    
    So if I want Weekend to come out before Monday, Friday. I need to change the enum so that Saturday = 1, Sunday = 2 and Monday = 4 etc


    I'd be careful about assuming that. Future versions of the framework aren't guaranteed to behave in the same way. Reflector is all well and good, but it's a bad idea to start relying on undocumented implementation details.

Conversation locked

This conversation has been locked by the site admins. No new comments can be made.