Reputation: 24562
I have a variable:
public static List<CardSetWithWordCount> cardSetWithWordCounts;
Where the class looks like this;
public class CardSetWithWordCount
{
public int Id { get; set; }
public string Name { get; set; }
public bool IsToggled { get; set; }
public int TotalWordCount { get; set; }
}
How can I check if:
I was thinking about LINQ as I have used this before but I am not sure this level of checking is possible with LINQ. I am interested to see if anyone knows if it's possible or will I just need to code a forEach loop or some other construct. Any tips would be much appreciated.
Upvotes: 0
Views: 199
Reputation: 205539
Here is an improved version of the pure LINQ solutions from @felix-b's and @Jon Skeet's answers:
var result = cardSetWithWordCounts
.Where(x => x.IsToggled)
.Take(2)
.Select((x, i) => i == 0 ? x.Name : "mixed")
.LastOrDefault();
After limiting the result set to 0,1 or 2 elements, rather than allocating list or using Aggregate
tricks, it utilizes the index providing Select
method overload and conditionally projects different things for the first and the other (in this case 0 or 1) elements. Then the LastOrDefault
method gives the desired result.
Another way is to use normal Select
for projection, then DefaultIfEmpty
to turn 0 element set to 1 element set, and finally Aggregate
to turn 2 element set to singe mixed value (utilizing the fact that this overload of Aggregate
calls the functor only if the sequence contains more than 1 element):
var result = cardSetWithWordCounts
.Where(x => x.IsToggled)
.Take(2)
.Select(x => x.Name)
.DefaultIfEmpty()
.Aggregate((first, second) => "mixed");
Upvotes: 2
Reputation: 8498
EDITED: included performance considerations raised in other answers
The most efficient solution with LINQ involved would be as follows:
string Example()
{
// the following LINQ code performs one full pass at most
// it stops after it finds first 2 matches, as it's enough to return "mixed"
var matched = cardSetWithWordCounts
.Where(x => x.IsToggled) // only take items where IsToggled is true
.Take(2) // take 2 items at most: it's enough to return "mixed"
.ToList(); // List is good because it caches Count and 1st matched item
switch (matched.Count)
{
case 0: return null; // no matches
case 1: return matched[0].Name; // 1 match
default: return "mixed"; // more than 1 match
}
}
The above solution allocates two additional objects: an enumerator and a List<>
.
In fact, a non-LINQ solution will always have a better performance, because even in its minimal form, LINQ allocates one or more enumerator objects. In this case, foreach
, Where
, and Take
each allocate one enumerator object. Thus the fact is, LINQ uses more memory and adds work for the GC.
On the other hand, these small performance penalties can be safely ignored in most of the cases. In general, LINQ should be preferred, because it reduces amount of code and improves readability. .NET applications rarely have strict restrictions on memory usage (an additional KB wouldn't ever be a problem).
The only case where I would rewrite the code without LINQ, is if it runs thousands times per second (or more). Then every bit of what's going under the hood becomes important:
string firstMatchedNameIfAny = null;
for (int i = 0 ; i < cardSetWithWordCounts.Count ; i++)
{
if (cardSetWithWordCounts[i].IsToggled)
{
if (firstMatchedName == null)
{
firstMatchedName = cardSetWithWordCounts[i].Name;
}
else
{
return "mixed";
}
}
}
return firstMatchedNameIfAny;
Upvotes: 3
Reputation: 6086
Using LINQ:
List<string> names = cardSetWithWordCounts.Select((v, i) => new { v, i })
.Where(x => x.v.IsToggled == true))
.Select(x => x.v.Name).ToList();
switch (names.Count)
{
case 0: return null;
case 1: return names.FirstOrDefault();
default: return "mixed";
}
Upvotes: 0
Reputation: 1499800
This is a slightly more efficient version of felix-b's answer - it doesn't require creating a new list. It will return as soon as it's sure of the result, without any need for checking the rest of the elements.
string GetDescription(IEnumerable<CardSetWithWordCount> cardSets)
{
CardSetWithWordCount firstMatch = null;
foreach (var match in cardSets.Where(x => x.IsToggled))
{
if (firstMatch != null)
{
// We've seen one element before, so this is the second one.
return "mixed";
}
firstMatch = match;
}
// We get here if there are fewer than two matches. The variable
// value will be null if we haven't seen any matches, or the first
// match if there was exactly one match. Use the null conditional
// operator to handle both easily.
return firstMatch?.Name;
}
For a more purely LINQ version, I'd use felix-b's answer
To explore other pure LINQ alternatives that don't need to materialize results, you could use Aggregate
.
First, a version that relies on Name
being non-null:
static string GetDescription(IEnumerable<CardSetWithWordCount> cardSets) =>
cardSets
.Where(x => x.IsToggled)
.Take(2)
.Select(match => match.Name)
.Aggregate<string, string>(
null, // Seed
(prev, next) => prev == null ? next : "mixed");
An alternative that doesn't rely on Name
being non-null, but does create a new object if the result is going to be "mixed":
static string GetDescription(IEnumerable<CardSetWithWordCount> cardSets) =>
cardSets
.Where(x => x.IsToggled)
.Take(2)
.Aggregate(
(CardSetWithWordCount) null, // Seed
(prev, next) => prev == null
? next : new CardSetWithWordCount { Name = "mixed" })
?.Name;
All of these ensure that they only evaluate the input once, and stop as soon as the result is known.
Upvotes: 4
Reputation: 34152
I would use a function:
string CheckState(List<cardSetWithWordCount> cardSetWithWordCounts)
{
IEnumerable<cardSetWithWordCount> result = cardSetWithWordCounts.Where(c=> c.IsToggled);
int cnt = result.Count();
if(cnt==0) return null;
else
return cnt==1?result.FirstOrDefault().Name:"Mixed";
}
Upvotes: 1
Reputation: 34421
Using Linq on average will take longer to execute than using a for loop. Linq will always check every item in the list. With a for loop you can stop executing the 2nd time you get a toggle. See code below
class Program
{
public static List<CardSetWithWordCount> cardSetWithWordCounts;
static void Main(string[] args)
{
}
public object CheckCards()
{
string name = "";
int count = 0;
foreach(CardSetWithWordCount card in cardSetWithWordCounts)
{
if (card.IsToggled)
{
count++;
if(count == 1)
{
name = card.Name;
}
else
{
name = "mixed";
break;
}
}
}
if(name == "")
return null;
else
return name;
}
}
public class CardSetWithWordCount
{
public int Id { get; set; }
public string Name { get; set; }
public bool IsToggled { get; set; }
public int TotalWordCount { get; set; }
}
Upvotes: -2
Reputation: 4513
Try this,
int elementCount = cardSetWithWordCounts.Count(y => y.IsToggled);
string result = cardSetWithWordCounts.Where(x => x.IsToggled)
.Select(x => elementCount == 0 ? null : (elementCount == 1 ? x.Name : "mixed"))
.FirstOrDefault();
element count = 0
-> returns null,
element count = 1
-> where criteria returned the item already-> x.Name gave the result
element count = 2
-> it returns mixed
Hope helps,
Upvotes: 1