Setting enum flags by string (in any order)

Posted on

Problem

I want to set flags which are given in string representation in any order. And it is intimidating, because you have to check like 2^3 possible options in my occassion. I was just wondering is there any better way to do that than just simple brute-force. Any optimization and readability improvment would be great. This is my code:

[Flags]
public enum salaryFeatures
{
Children = 1,
Graduate = 2,
Disability = 4
}

class Program
{

public static void Main()
{
    string Employee = "10023 Mark Male 6.7 70 30 20 Children Graduate";
    salaryFeatures f;
    f = GetFeaturesByString(Employee);
}

public static salaryFeatures GetFeaturesByString(string fields)
{
    bool children = false;
    bool graduate = false;
    bool disability = false;
    salaryFeatures features = new salaryFeatures();

    if (fields.ToLower().Contains("children")) children = true;
    if (fields.ToLower().Contains("graduate")) graduate = true;
    if (fields.ToLower().Contains("disability")) disability = true;

    if (children && graduate && disability)
    {
        return features = salaryFeatures.Children | salaryFeatures.Disability | salaryFeatures.Graduate;
    }
    if (children && graduate)
    {
        return features = salaryFeatures.Children | salaryFeatures.Graduate;
    }
    if (children && disability)
    {
        return features = salaryFeatures.Children | salaryFeatures.Disability;
    }
    if (children)
    {
        return features = salaryFeatures.Children;
    }
    if (graduate && disability)
    {
        return features = salaryFeatures.Graduate | salaryFeatures.Disability;
    }
    if (graduate)
    {
        return features = salaryFeatures.Graduate;
    }
    if (disability)
    {
        return features = salaryFeatures.Disability;
    }
    return features;
}
}

Solution

Here is my take on your code

[Flags]
public enum SalaryFeatures
{
  None= 0,
  Children = 1,
  Graduate = 2,
  Disability = 4
}

class Program
{

  public static void Main()
  {
    string Employee = "10023 Mark Male 6.7 70 30 20 Children Graduate";
    SalaryFeatures f;
    f = GetFeaturesByString(Employee);
    // f.Dump(); //LinqPad only
  }

  public static SalaryFeatures GetFeaturesByString(string fields)
  {
    SalaryFeatures features = SalaryFeatures.None;

    if (fields.IndexOf("children", StringComparison.OrdinalIgnoreCase) >= 0) features |= SalaryFeatures.Children;
    if (fields.IndexOf("graduate", StringComparison.OrdinalIgnoreCase) >= 0) features |= SalaryFeatures.Graduate;
    if (fields.IndexOf("disability", StringComparison.OrdinalIgnoreCase) >= 0) features |= SalaryFeatures.Disability;

    return features;
  }
}

First of the definition of your Enumeration is not complete. You need to define a None value for it

Use None as the name of the flag enumerated constant whose value is
zero. You cannot use the None enumerated constant in a bitwise AND
operation to test for a flag because the result is always zero.
However, you can perform a logical, not a bitwise, comparison between
the numeric value and the None enumerated constant to determine
whether any bits in the numeric value are set.

See also the Guidelines when defining a Flags enum. I also change the naming of the enum to PascalCase (SalaryFeatures).

After that I have improved your method GetFeaturesByString to use bitwise OR to combine the values depending on the values that are found in your string. I also use the method IndexOf(string, StringComparison) to check if the search string exists in the source string, instead of using ToLower() and Contains().

It would be a good idea to extract the salary features from the string before you start processing them. You should put this in method (extension?) that specializes in this.

var text = "10023 Mark Male 6.7 70 30 20 Children Graduate";
var fieldNames = Enum.GetNames(typeof(salaryFeatures)).Where(n => text.IndexOf(n) >= 0);

Now after having found the fields you can parse them in another method, ignore the case and build the flags with linq’s Aggregate extension:

var salaryFeatures = fieldNames.Aggregate(SalaryFeatures.None, (result, next) 
    => result |= (SalaryFeatures)Enum.Parse(typeof(SalaryFeatures), next, true));
  • use the Aggregate extension to concatenate the flags
  • use the ignoreCase = true
  • the salaryFeatures should actually be named with PascalCase SalaryFeatures
  • if you defined one additional flag None you could use it instead of (salaryFeatures)0

Both these changes will allow you to add more flags in future without modifying those methods.

Here is another variant using the Aggregate extension method:

https://dotnetfiddle.net/7RrGTE

[Flags]
public enum SalaryFeatures
{
    None = 0,
    Children = 1,
    Graduate = 2,
    Disability = 4
}

//...

    var employee = "Mark: Disability Male with Children"; 
    var features =
        Enumerable.
        Range(0, Enum.GetValues(typeof(SalaryFeatures)).Length - 1).
        Aggregate
        (
            SalaryFeatures.None,
            (f, b) =>
                employee.
                Contains(
                ((SalaryFeatures)(1 << b)).
                ToString()) ?
                f | (SalaryFeatures)(1 << b)
                :
                f
        );
    var f = (int)features;
    Console.WriteLine(f); // expected: 5

(where “Length – 1” is to account for the presence of SalaryFeatures.None)

Leave a Reply

Your email address will not be published. Required fields are marked *