Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Select only if condition in LINQ

Tags:

c#

linq

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?

like image 480
zig Avatar asked Oct 07 '20 05:10

zig


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[] { });
like image 185
Enigmativity Avatar answered Oct 20 '22 10:10

Enigmativity


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();
like image 20
Peter Csala Avatar answered Oct 20 '22 11:10

Peter Csala


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;        
    }
}
like image 30
Ray Avatar answered Oct 20 '22 11:10

Ray


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;

                });
like image 33
ekke Avatar answered Oct 20 '22 10:10

ekke


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);
like image 1
Harald Coppoolse Avatar answered Oct 20 '22 11:10

Harald Coppoolse