Select only if condition in LINQ

Viewed 349

I'm not really sure if I have the correct title for this Q. anyway I'm trying to parse a delimited string into a list of Enum elements:

public enum MyEnum { Enum1, Enum2, Enum3 }

Given an input of:

string s = "Enum2, enum3, Foo,";

I would like to output only the parts that exists in MyEnum (ignoring the case):
[MyEnum.Enum2, MyEnum.Enum3]

IEnumerable<MyEnum> sl = 
    s.Split(new char[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries)                         
    .Select(a => { if (Enum.TryParse(a, true, out MyEnum e)) return e; else return nothing ??? })

How do I return "nothing" from the Select() if TryParse() fails?
I could do an ugly thing like:

IEnumerable<MyEnum> sl = 
    s.Split(new char[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries)                
    .Where(a => Enum.TryParse(a, true, out MyEnum dummy))
    .Select(a => Enum.Parse(typeof(MyEnum), a, true));

Doing the parsing job twice for no good reason,
But I'm sure I'm missing something trivial.
How can this be done in an efficient and elegant way?

5 Answers

It's super simple if you use nullable values for the enum:

IEnumerable<MyEnum> sl =
    s
        .Split(new char[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries)
        .Select(a => { if (Enum.TryParse(a, true, out MyEnum e)) return (MyEnum?)e; else return null; })
        .Where(x => x.HasValue)
        .Select(x => x.Value);

You can even reduce it down further:

        .Select(a => Enum.TryParse(a, true, out MyEnum e) ? (MyEnum?)e : null)

Or you could use SelectMany and avoid the nullables:

IEnumerable<MyEnum> sl =
    s
        .Split(new char[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries)
        .SelectMany(a => Enum.TryParse(a, true, out MyEnum e) ? new[] { e } : new MyEnum[] { });

You can designate the value -1 to be an invalid enum value thereby serving as a placeholder for any invalid strings and allow them to be subsequently excluded. Also, this avoids parsing the strings more than once.

var invalidEnum = (MyEnum) (-1);

var sl = s.Split(new char[] {',', ' '}, StringSplitOptions.RemoveEmptyEntries)
    .Select(a =>
    {
        var isValid = Enum.TryParse(a, true, out MyEnum myEnum);

        return isValid ? myEnum : invalidEnum;
    })
    .Where(a => a != invalidEnum);

Potentially -1 could already be used by the enum if explicitly defined (unlike in the original example where default minimum is zero (0) so using -1 is ok). To minimize this risk the following values could be used instead of -1:

var invalidEnum = (MyEnum) (int.MinValue);
// or
var invalidEnum = (MyEnum) (int.MaxValue);

If still some concern that these highly unlikely values being taken then could find the next available number not used by the enum and assign it as the invalid enum:

var invalidEnum = (MyEnum) int.MinValue;

foreach(var number in Enumerable.Range(int.MinValue, int.MaxValue))
{
    if (!Enum.IsDefined(typeof(MyEnum), number))
    {
        invalidEnum = (MyEnum) number;
        break;        
    }
}

Even though the Enum.TryParse do the heavy lifting it is sub-optimal. It can generate false-positive result.

For example, if your s string looks like this:

string s = "Enum2, enum3, Foo, 4";

then Enum.TryParse will able to convert Enum2, enum3 and 4 to a valid MyEnum instance.

In order to filter out 4 from the result set you have to call Enum.IsDefined. Here is a sample how can you combine TryParse and IsDefined:

s.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries)
  .Where(value => Enum.TryParse<MyEnum>(value, ignoreCase: true, out var parsedValue) && Enum.IsDefined(typeof(MyEnum), parsedValue))
  .Select(value => (MyEnum)Enum.Parse(typeof(MyEnum), value, ignoreCase: true))
  .ToList();

You can use the LINQ aggregate method that can be used for cases like this:

var sl= s.Split(new char[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries)
                .Aggregate(new List<MyEnum> (),(List<MyEnum> enums,string a)=>
                {
                    if (Enum.TryParse(a, true, out MyEnum e))
                    {
                        enums.Add(e);
                    }

                    return enums;

                });

After you have divided your string s into substrings, you only want to keep only the enum values of the substrings that are proper myEnums.

You already knew how to divide your original string into substrings:

string s = ...
var subStrings = .Split(new char[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries);

Use a Select to try to parse each subString into a nullable MyEnum. If that fails, the nullable has no value; otherwise the nullable has the MyEnum value.

Remove the nullables that have no values, and Select the Values of the remaining ones:

var result = subStrings.Select(subString => new
{
    if (Enum.TryParse(subString, true out MyEnum parsedEnum)
    {
        return new Nullable<MyEnum>(parsedEnum);
    }
    else
    {
        return new Nullable<MyEnum>(null);
    }
})
.Where(nullableEnum => nullableEnum.HasValue);
.Select(nullableEnum => nullableEnum.Value);
Related