7

In C Sharp, how can I set up an if statement that checks if one of several conditions is true? It must be only one of the conditions, if zero or two or more are true the if should be false.

Wilson
  • 8,570
  • 20
  • 66
  • 101
  • @Tim S. It's quite uncouth to enter your answer in the comments of several different other answers. Delete those comments and post your answer as an answer and accept judgement of it from voting - as Jeff intended. – Amy B May 25 '12 at 13:53

6 Answers6

9

You could write a helper method. This has the advantage that it short circuits, only evaluating exactly as many as necessary,

public static bool IsExactlyOneTrue(IEnumerable<Func<bool>> conditions) {
    bool any = false;
    foreach (var condition in conditions) {
        bool result = condition();
        if (any && result) {
            return false;
        }
        any = any | result;
    }
    return any;
}
jason
  • 236,483
  • 35
  • 423
  • 525
  • This method can be simplified to `return !conditions.Where(cond => cond()).Skip(1).Any();` It's only arguably more readable, and it might run a hair slower, but unless this is actually found to be significant, I'd go for the more concise one. – Tim S. May 24 '12 at 21:16
  • 1
    On further looking, this has to be more like this to work right: `var trueConditions = conditions.Where(cond => cond()); if (!trueConditions.Take(1).Any()) return false; return !trueConditions.Skip(1).Any();` I'm starting to like your answer better than my LINQ approach. – Tim S. May 24 '12 at 21:32
  • @TimS.: But that may cause the leading false-resulting expressions to be evaluated twice. I don’t think there’s any elegant way of doing it in LINQ (except by projecting indexes, which defeats the whole purpose). – Douglas May 24 '12 at 21:40
  • Indeed. I just tested it with a false, and then true expression, and it evaluates each twice. It might be fixable, but with more and more complexity that far outweighs Jason's solution. LINQ just isn't made to handle such a situation well. – Tim S. May 24 '12 at 21:55
  • @TimS.: I experimented a bit more, and found that it may be done elegantly in LINQ if one assumes the existence of a predicate-based `IndexOf` operator (which can be easily defined). See my edited answer below. – Douglas May 25 '12 at 05:54
  • Yeah, I thought I might have been able to solve this using LINQ, but it just doesn't work well. – Tim S. May 25 '12 at 14:15
5

You could use compose your booleans into a bool sequence and then apply LINQ:

bool[] conditions = new bool[] { cond1, cond2, cond3, cond4 };
bool singleTrue = conditions.Count(cond => cond) == 1;

For just two booleans, exclusive or becomes much simpler:

bool singleTrue = cond1 != cond2;

Edit: To achieve on-demand evaluation and short-circuiting, we need to promote our bool sequence into a Func<bool> sequence (where each element is a function delegate encapsulating the evaluation of a condition):

IEnumerable<Func<bool>> conditions = // define sequence here
int firstTrue = conditions.IndexOf(cond => cond());
bool singleTrue = firstTrue != -1 && 
                  conditions.Skip(firstTrue + 1).All(cond => !cond());

The above snippet assumes the existence of a predicate-based IndexOf operator, which is not available under the current version of LINQ but may be defined as an extension method like so:

public static int IndexOf<T>(this IEnumerable<T> source, Func<T, bool> predicate)
{
    int i = 0;

    foreach (T element in source)
    {
        if (predicate(element))
            return i;

        i++;
    }

    return -1;
}

Sample data for testing (a breakpoint may be set on each false or true to follow evaluation):

IEnumerable<Func<bool>> conditions = new Func<bool>[] 
{ 
    () => 
        false,
    () => 
        true,
    () => 
        false,
    () => 
        false,
};
Douglas
  • 53,759
  • 13
  • 140
  • 188
5
List<Func<Customer, bool>> criteria = new List<Func<Customer, bool>>();

criteria.Add(c => c.Name.StartsWith("B"));
criteria.Add(c => c.Job == Jobs.Plumber);
criteria.Add(c => c.IsExcellent);

Customer myCustomer = GetCustomer();

int criteriaCount = criteria
  .Where(q => q(myCustomer))
  // .Take(2)  // optimization
  .Count()
if (criteriaCount == 1)
{
}

Linq implementation of Jason's method signature:

public static bool IsExactlyOneTrue(IEnumerable<Func<bool>> conditions)
{
  int passingConditions = conditions
    .Where(x => x())
    // .Take(2) //optimization
    .Count();
  return passingConditions == 1;
}
Amy B
  • 108,202
  • 21
  • 135
  • 185
  • Great answer. Can also do it without relationship to a specific type: `var conditions = new List>(); conditions.Add(() => foo == bar); if (conditions.Select(x => x.Invoke()).Count(x => x) == 1) { }` – lukiffer May 24 '12 at 21:08
  • Sure, you're just shoving the specific instances into the criteria. My "criteria" collection delays the need to have an applicable instance and is re-usable for as many instances as you have. – Amy B May 24 '12 at 21:12
  • This approach requires evaluating all of the conditions, whether or not it is necessary. I prefer an approach that permits short circuiting. – jason May 25 '12 at 14:42
  • No. Just uncomment the Take and execution will stop at the first moment it can. – Amy B May 25 '12 at 14:44
  • @David B: The `Take` isn't uncommented, now, is it? – jason May 25 '12 at 15:29
4

Going for simplicity, you could just keep a running count:

int totalTrue = 0;
if (A) totalTrue++;
if (B) totalTrue++;
if (C) totalTrue++;
...
return (1 == totalTrue);
Adam V
  • 6,256
  • 3
  • 40
  • 52
  • Personally I'd write that last statement as `return totalTrue == 1;`. I understand the rationale for writing such tests backwards, but I disagree with it -- and the parentheses are unnecessary. – Keith Thompson May 25 '12 at 02:35
  • @KeithThompson, I just want to keep myself in the habit of writing code in that order in case I ever go back to C++. – Adam V May 25 '12 at 13:04
  • @Adam V: When in Rome, do as the Romans do. C# and C++ are not the same language. Don't write C# in C++, and C++ in C#. – jason May 25 '12 at 14:39
  • @AdamV: Why does it make any more sense in C++ than in C#? Both use `=` for assignment and `==` for equality. (Personally, I just find things like `if (x ==1)` jarring.) – Keith Thompson May 25 '12 at 15:11
  • @KeithThompson: In C++ the line `if (x = 1)` will set `x` to 1 and treat it as `true`, causing you to enter the `if` block. In C# the line won't compile because it doesn't treat it as `true`. – Adam V May 25 '12 at 15:20
  • 1
    @Jason: if it were invalid code, or code that changed meaning between languages, I'd agree with you. But since it's just a habit, I'll keep it. – Adam V May 25 '12 at 15:23
  • 1
    @Adam V: Here's the thing. When a C# programmer reads C# code, they expect to see C# written in C#. There are idioms in the language, and code that goes against those idioms is a distraction. – jason May 25 '12 at 15:27
  • In any case, the parentheses are superfluous in either language. – Keith Thompson May 25 '12 at 15:28
2

I think this would do the trick

 int i= 0;
 if ( (!A || ++i <= 1) && 
      (!B || ++i <= 1) && 
      (!C || ++i <= 1) && 
      ... && 
      (i == 1))

If I didn't think wrong on this, this if will be false as soon as i > 1. If i is never incremented and we reach the last condion, will be false since i == 0

Claudio Redi
  • 67,454
  • 15
  • 130
  • 155
0

Most of these answers will work and have 'good performance'. But the simplest answer is:

if( (A & !(B || C)) || 
    (B & !(A || C)) ||
    (C & !(A || B)) )
{
   ...
}

You do end up evaluating A/B/C more than once so this is really only useful when you have simple bools.

Andrew Hanlon
  • 7,271
  • 4
  • 33
  • 53